Resolve public poll invitations to tenant policy
This commit is contained in:
@@ -32,6 +32,7 @@ from govoplan_poll.backend.participation import (
|
|||||||
PollInvitationRevocationRef,
|
PollInvitationRevocationRef,
|
||||||
PollOptionMutationRef,
|
PollOptionMutationRef,
|
||||||
PollParticipationContextRef,
|
PollParticipationContextRef,
|
||||||
|
PollPublicInvitationRef,
|
||||||
PollResponseGatewayRef,
|
PollResponseGatewayRef,
|
||||||
)
|
)
|
||||||
from govoplan_poll.backend.participation_service import (
|
from govoplan_poll.backend.participation_service import (
|
||||||
@@ -323,6 +324,28 @@ class SqlPollSchedulingProvider(PollSchedulingProvider):
|
|||||||
except (PollError, ValidationError) as exc:
|
except (PollError, ValidationError) as exc:
|
||||||
raise PollCapabilityError(str(exc)) from exc
|
raise PollCapabilityError(str(exc)) from exc
|
||||||
|
|
||||||
|
def resolve_public_invitation(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
gateway: PollResponseGatewayRef,
|
||||||
|
) -> PollPublicInvitationRef:
|
||||||
|
try:
|
||||||
|
invitation = governed_invitation(
|
||||||
|
session,
|
||||||
|
token=token,
|
||||||
|
gateway=gateway,
|
||||||
|
)
|
||||||
|
except (PollError, ValidationError) as exc:
|
||||||
|
raise PollCapabilityError(str(exc)) from exc
|
||||||
|
return PollPublicInvitationRef(
|
||||||
|
invitation_id=invitation.id,
|
||||||
|
tenant_id=invitation.tenant_id,
|
||||||
|
poll_id=invitation.poll_id,
|
||||||
|
gateway=gateway,
|
||||||
|
)
|
||||||
|
|
||||||
def submit_governed_response(
|
def submit_governed_response(
|
||||||
self,
|
self,
|
||||||
session: object,
|
session: object,
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ DOCUMENTATION = (
|
|||||||
body=(
|
body=(
|
||||||
"An invitation or signed participation link determines which poll and participant identity a response belongs to. "
|
"An invitation or signed participation link determines which poll and participant identity a response belongs to. "
|
||||||
"The poll policy controls anonymity, response updates, result visibility, open and close times, and whether Maybe is allowed. "
|
"The poll policy controls anonymity, response updates, result visibility, open and close times, and whether Maybe is allowed. "
|
||||||
"Submitting a response is atomic: capacity and choice constraints are checked before the saved response replaces any earlier answer."
|
"Submitting a response is atomic: capacity and choice constraints are checked before the saved response replaces any earlier answer. "
|
||||||
|
"A valid signed link also resolves its tenant before Poll runs, so tenant module policy can withdraw the public surface without exposing another tenant's state."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("user",),
|
documentation_types=("user",),
|
||||||
@@ -117,6 +118,21 @@ def _poll_router(_context: ModuleContext):
|
|||||||
return router
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
def _public_tenant_resolver(request: object, session: object) -> str | None:
|
||||||
|
path_params = getattr(request, "path_params", {})
|
||||||
|
token = str(path_params.get("token") or "").strip()
|
||||||
|
path = str(getattr(getattr(request, "url", None), "path", ""))
|
||||||
|
if not token or "/poll/public/" not in path:
|
||||||
|
return None
|
||||||
|
from govoplan_poll.backend.service import PollError, get_poll_by_invitation_token
|
||||||
|
|
||||||
|
try:
|
||||||
|
poll = get_poll_by_invitation_token(session, token=token)
|
||||||
|
except PollError:
|
||||||
|
return None
|
||||||
|
return poll.tenant_id
|
||||||
|
|
||||||
|
|
||||||
def _poll_scheduling_provider(context: ModuleContext) -> object:
|
def _poll_scheduling_provider(context: ModuleContext) -> object:
|
||||||
del context
|
del context
|
||||||
from govoplan_poll.backend.capabilities import SqlPollSchedulingProvider
|
from govoplan_poll.backend.capabilities import SqlPollSchedulingProvider
|
||||||
@@ -147,6 +163,7 @@ manifest = ModuleManifest(
|
|||||||
permissions=PERMISSIONS,
|
permissions=PERMISSIONS,
|
||||||
role_templates=ROLE_TEMPLATES,
|
role_templates=ROLE_TEMPLATES,
|
||||||
route_factory=_poll_router,
|
route_factory=_poll_router,
|
||||||
|
public_tenant_resolver=_public_tenant_resolver,
|
||||||
tenant_summary_providers=(_tenant_summary,),
|
tenant_summary_providers=(_tenant_summary,),
|
||||||
capability_factories={
|
capability_factories={
|
||||||
CAPABILITY_POLL_SCHEDULING: _poll_scheduling_provider,
|
CAPABILITY_POLL_SCHEDULING: _poll_scheduling_provider,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from govoplan_core.core.poll_participation import (
|
|||||||
PollParticipationContextRef,
|
PollParticipationContextRef,
|
||||||
PollParticipationGatewayProvider,
|
PollParticipationGatewayProvider,
|
||||||
PollParticipationPolicy,
|
PollParticipationPolicy,
|
||||||
|
PollPublicInvitationRef,
|
||||||
PollResponseGatewayRef,
|
PollResponseGatewayRef,
|
||||||
participation_token_fingerprint,
|
participation_token_fingerprint,
|
||||||
poll_participation_gateway_provider,
|
poll_participation_gateway_provider,
|
||||||
@@ -37,6 +38,7 @@ __all__ = [
|
|||||||
"PollParticipationContextRef",
|
"PollParticipationContextRef",
|
||||||
"PollParticipationGatewayProvider",
|
"PollParticipationGatewayProvider",
|
||||||
"PollParticipationPolicy",
|
"PollParticipationPolicy",
|
||||||
|
"PollPublicInvitationRef",
|
||||||
"PollResponseGatewayRef",
|
"PollResponseGatewayRef",
|
||||||
"participation_token_fingerprint",
|
"participation_token_fingerprint",
|
||||||
"poll_participation_gateway_provider",
|
"poll_participation_gateway_provider",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PollManifestTests(unittest.TestCase):
|
|||||||
self.assertFalse(manifest.required_capabilities)
|
self.assertFalse(manifest.required_capabilities)
|
||||||
self.assertIn("auth.principalResolver", manifest.optional_capabilities)
|
self.assertIn("auth.principalResolver", manifest.optional_capabilities)
|
||||||
self.assertIsNotNone(manifest.route_factory)
|
self.assertIsNotNone(manifest.route_factory)
|
||||||
|
self.assertIsNotNone(manifest.public_tenant_resolver)
|
||||||
self.assertIsNotNone(manifest.migration_spec)
|
self.assertIsNotNone(manifest.migration_spec)
|
||||||
self.assertIn("poll.availability_matrix", {interface.name for interface in manifest.provides_interfaces})
|
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.workflow_context", {interface.name for interface in manifest.provides_interfaces})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
@@ -9,6 +10,7 @@ from sqlalchemy.orm import Session, sessionmaker
|
|||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
from govoplan_core.core.poll import PollResponseRef, PollResponseSubmissionProvider, PollSchedulingProvider
|
from govoplan_core.core.poll import PollResponseRef, PollResponseSubmissionProvider, PollSchedulingProvider
|
||||||
from govoplan_poll.backend.capabilities import SqlPollSchedulingProvider
|
from govoplan_poll.backend.capabilities import SqlPollSchedulingProvider
|
||||||
|
from govoplan_poll.backend.manifest import get_manifest
|
||||||
from govoplan_poll.backend.db.models import Poll, PollInvitation, PollLifecycleTransition, PollOption, PollResponse
|
from govoplan_poll.backend.db.models import Poll, PollInvitation, PollLifecycleTransition, PollOption, PollResponse
|
||||||
from govoplan_poll.backend.schemas import (
|
from govoplan_poll.backend.schemas import (
|
||||||
PollAnswerInput,
|
PollAnswerInput,
|
||||||
@@ -304,6 +306,13 @@ class PollServiceTests(unittest.TestCase):
|
|||||||
poll_id=poll.id,
|
poll_id=poll.id,
|
||||||
payload=PollInvitationCreateRequest(respondent_label="External participant"),
|
payload=PollInvitationCreateRequest(respondent_label="External participant"),
|
||||||
)
|
)
|
||||||
|
resolver = get_manifest().public_tenant_resolver
|
||||||
|
self.assertIsNotNone(resolver)
|
||||||
|
request = SimpleNamespace(
|
||||||
|
path_params={"token": token},
|
||||||
|
url=SimpleNamespace(path=f"/api/v1/poll/public/{token}"),
|
||||||
|
)
|
||||||
|
self.assertEqual("tenant-1", resolver(request, self.session))
|
||||||
|
|
||||||
response = submit_poll_response_with_token(
|
response = submit_poll_response_with_token(
|
||||||
self.session,
|
self.session,
|
||||||
|
|||||||
Reference in New Issue
Block a user