Add poll workflow and signed participation support

This commit is contained in:
2026-07-12 17:32:07 +02:00
parent d0e68f5aa6
commit fc9b7c287c
10 changed files with 519 additions and 16 deletions

View File

@@ -19,6 +19,8 @@ class PollManifestTests(unittest.TestCase):
self.assertIsNotNone(manifest.route_factory)
self.assertIsNotNone(manifest.migration_spec)
self.assertIn("poll.availability_matrix", {interface.name for interface in manifest.provides_interfaces})
self.assertIn("poll.workflow_context", {interface.name for interface in manifest.provides_interfaces})
self.assertIn("poll.signed_participation", {interface.name for interface in manifest.provides_interfaces})
self.assertIn("poll:response:write", {permission.scope for permission in manifest.permissions})

View File

@@ -6,21 +6,41 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from govoplan_core.db.base import Base
from govoplan_poll.backend.db.models import Poll, PollOption, PollResponse
from govoplan_poll.backend.schemas import PollAnswerInput, PollCreateRequest, PollOptionInput, PollSubmitResponseRequest
from govoplan_poll.backend.service import PollError, create_poll, open_poll, poll_result_summary_by_id, submit_poll_response
from govoplan_poll.backend.db.models import Poll, PollInvitation, PollOption, PollResponse
from govoplan_poll.backend.schemas import (
PollAnswerInput,
PollCreateRequest,
PollInvitationCreateRequest,
PollOptionInput,
PollSubmitResponseRequest,
)
from govoplan_poll.backend.service import (
PollError,
create_poll,
create_poll_invitation,
open_poll,
poll_result_summary_by_id,
submit_poll_response,
submit_poll_response_with_token,
)
class PollServiceTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(self.engine, tables=[Poll.__table__, PollOption.__table__, PollResponse.__table__])
Base.metadata.create_all(
self.engine,
tables=[Poll.__table__, PollOption.__table__, PollResponse.__table__, PollInvitation.__table__],
)
self.Session = sessionmaker(bind=self.engine)
self.session: Session = self.Session()
def tearDown(self) -> None:
self.session.close()
Base.metadata.drop_all(self.engine, tables=[PollResponse.__table__, PollOption.__table__, Poll.__table__])
Base.metadata.drop_all(
self.engine,
tables=[PollInvitation.__table__, PollResponse.__table__, PollOption.__table__, Poll.__table__],
)
self.engine.dispose()
def test_single_choice_response_can_update_existing_respondent(self) -> None:
@@ -74,6 +94,56 @@ class PollServiceTests(unittest.TestCase):
payload=PollSubmitResponseRequest(answers=[PollAnswerInput(option_key="yes")]),
)
def test_yes_no_maybe_uses_default_options(self) -> None:
poll = create_poll(
self.session,
tenant_id="tenant-1",
user_id=None,
payload=PollCreateRequest(
title="Tentative decision",
kind="yes_no_maybe",
allow_anonymous=True,
),
)
open_poll(self.session, tenant_id="tenant-1", poll_id=poll.id)
response = submit_poll_response(
self.session,
tenant_id="tenant-1",
poll_id=poll.id,
payload=PollSubmitResponseRequest(answers=[PollAnswerInput(option_key="maybe")]),
)
summary = poll_result_summary_by_id(self.session, tenant_id="tenant-1", poll_id=poll.id)
self.assertEqual([option.key for option in poll.options], ["yes", "no", "maybe"])
self.assertEqual(response.answers[0]["option_key"], "maybe")
self.assertEqual(summary["response_count"], 1)
def test_poll_can_store_scheduling_workflow_context(self) -> None:
poll = create_poll(
self.session,
tenant_id="tenant-1",
user_id="user-1",
payload=PollCreateRequest(
title="Find a steering group slot",
kind="availability",
context_module="scheduling",
context_resource_type="scheduling_request",
context_resource_id="request-1",
workflow_state="collecting_availability",
workflow_steps=[
{"id": "collect", "label": "Collect availability", "status": "active"},
{"id": "decide", "label": "Choose slot", "status": "pending"},
],
options=[PollOptionInput(key="slot-1", label="Monday")],
),
)
self.assertEqual(poll.context_module, "scheduling")
self.assertEqual(poll.context_resource_id, "request-1")
self.assertEqual(poll.workflow_state, "collecting_availability")
self.assertEqual(poll.workflow_steps[0]["id"], "collect")
def test_ranked_choice_summary_uses_borda_score(self) -> None:
poll = create_poll(
self.session,
@@ -176,6 +246,35 @@ class PollServiceTests(unittest.TestCase):
self.assertEqual(values["slot-1"], {"available": 1, "unavailable": 1})
self.assertEqual(values["slot-2"], {"maybe": 1, "available": 1})
def test_signed_invitation_can_submit_without_anonymous_policy(self) -> None:
poll = create_poll(
self.session,
tenant_id="tenant-1",
user_id="user-1",
payload=PollCreateRequest(
title="External vote",
kind="yes_no_maybe",
),
)
open_poll(self.session, tenant_id="tenant-1", poll_id=poll.id)
invitation, token = create_poll_invitation(
self.session,
tenant_id="tenant-1",
poll_id=poll.id,
payload=PollInvitationCreateRequest(respondent_label="External participant"),
)
response = submit_poll_response_with_token(
self.session,
token=token,
payload=PollSubmitResponseRequest(answers=[PollAnswerInput(option_key="yes")]),
)
self.assertEqual(response.respondent_id, f"invitation:{invitation.id}")
self.assertEqual(response.respondent_label, "External participant")
self.assertEqual(response.metadata_["invitation_id"], invitation.id)
self.assertIsNotNone(invitation.last_used_at)
if __name__ == "__main__":
unittest.main()