feat(poll): extend response editing contract
This commit is contained in:
@@ -58,6 +58,21 @@ class PollUpdateCommand:
|
|||||||
closes_at: datetime | None
|
closes_at: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PollOptionUpdateCommand:
|
||||||
|
"""Complete mutable option snapshot supplied by an owning module.
|
||||||
|
|
||||||
|
Providers must treat an exact repeat as a no-op. When the snapshot
|
||||||
|
changes, answers for this option are invalidated while answers for other
|
||||||
|
options are retained.
|
||||||
|
"""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
description: str | None = None
|
||||||
|
value: Mapping[str, object] | None = None
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class PollInvitationCommand:
|
class PollInvitationCommand:
|
||||||
respondent_id: str | None = None
|
respondent_id: str | None = None
|
||||||
@@ -102,6 +117,14 @@ class PollInvitationRef:
|
|||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PollAnswerRef:
|
||||||
|
option_id: str | None = None
|
||||||
|
option_key: str | None = None
|
||||||
|
value: object = None
|
||||||
|
rank: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class PollResponseRef:
|
class PollResponseRef:
|
||||||
invitation_id: str | None
|
invitation_id: str | None
|
||||||
@@ -110,6 +133,9 @@ class PollResponseRef:
|
|||||||
# consumer can use it to reconcile an authenticated response without
|
# consumer can use it to reconcile an authenticated response without
|
||||||
# trusting client-supplied invitation metadata.
|
# trusting client-supplied invitation metadata.
|
||||||
respondent_id: str | None = None
|
respondent_id: str | None = None
|
||||||
|
# Optional for backwards compatibility. Providers that support response
|
||||||
|
# editing expose the authoritative, still-valid answers here.
|
||||||
|
answers: tuple[PollAnswerRef, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -144,6 +170,19 @@ class PollSchedulingProvider(Protocol):
|
|||||||
) -> PollRef:
|
) -> PollRef:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
def update_option(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
poll_id: str,
|
||||||
|
option_id: str,
|
||||||
|
command: PollOptionUpdateCommand,
|
||||||
|
) -> PollOptionRef:
|
||||||
|
"""Update one option and invalidate only answers for that option."""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
def open_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
|
def open_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
36
tests/test_poll_contract.py
Normal file
36
tests/test_poll_contract.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from govoplan_core.core.poll import PollAnswerRef, PollOptionUpdateCommand, PollResponseRef
|
||||||
|
|
||||||
|
|
||||||
|
class PollContractTests(unittest.TestCase):
|
||||||
|
def test_response_ref_remains_backwards_compatible_and_can_carry_answers(self) -> None:
|
||||||
|
submitted_at = datetime(2026, 7, 20, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
legacy = PollResponseRef(None, submitted_at)
|
||||||
|
detailed = PollResponseRef(
|
||||||
|
invitation_id="invitation-1",
|
||||||
|
submitted_at=submitted_at,
|
||||||
|
respondent_id="person-1",
|
||||||
|
answers=(PollAnswerRef(option_id="option-1", option_key="slot-1", value="available"),),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(legacy.answers, ())
|
||||||
|
self.assertEqual(detailed.answers[0].value, "available")
|
||||||
|
|
||||||
|
def test_option_update_command_is_a_complete_immutable_snapshot(self) -> None:
|
||||||
|
command = PollOptionUpdateCommand(
|
||||||
|
label="Tuesday 10:00",
|
||||||
|
value={"slot_id": "slot-1"},
|
||||||
|
metadata={"source": "scheduling"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(command.label, "Tuesday 10:00")
|
||||||
|
self.assertEqual(command.value, {"slot_id": "slot-1"})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user