Add poll workflow and signed participation support
This commit is contained in:
@@ -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})
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user