from __future__ import annotations from govoplan_core.core.modules import with_documentation_structured_translations from govoplan_scheduling.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS from pathlib import Path from govoplan_core.core.access import ( CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER, ) from govoplan_core.core.calendar import CAPABILITY_CALENDAR_SCHEDULING from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( CapabilityDocumentation, DocumentationTopic, DocumentationCondition, DocumentationLink, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, ProductAreaContribution, PublicFrontendRoute, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.people import ( CAPABILITY_ACCESS_PEOPLE_SEARCH, CAPABILITY_ADDRESSES_PEOPLE_SEARCH, ) from govoplan_core.core.poll import CAPABILITY_POLL_SCHEDULING, PollCapabilityError from govoplan_core.core.poll_participation import ( CAPABILITY_POLL_PARTICIPATION_GATEWAY, PollResponseGatewayRef, poll_participation_gateway_provider, ) from govoplan_core.core.policy import CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_scheduling.backend.db import models as scheduling_models # noqa: F401 - populate Scheduling ORM metadata from govoplan_scheduling.backend.dsar_provider import SCHEDULING_DSAR_CAPABILITY MODULE_ID = "scheduling" MODULE_NAME = "Scheduling" MODULE_VERSION = "0.1.22" READ_SCOPE = "scheduling:schedule:read" WRITE_SCOPE = "scheduling:schedule:write" ADMIN_SCOPE = "scheduling:schedule:admin" RESPOND_SCOPE = "scheduling:availability:write" def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Scheduling", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission( READ_SCOPE, "View scheduling", "Read scheduling polls, proposals, participant state, and selected outcomes.", ), _permission( WRITE_SCOPE, "Manage own scheduling", "Create scheduling polls and manage requests for which the account is the organizer.", ), _permission( ADMIN_SCOPE, "Administer scheduling", "Manage every tenant scheduling request and configure scheduling policies, external participation, and retention defaults.", ), _permission( RESPOND_SCOPE, "Respond to scheduling polls", "Submit and update own scheduling availability responses.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="scheduling_manager", name="Scheduling manager", description="Create scheduling polls and manage candidate slots and outcomes for requests the account organizes.", permissions=(READ_SCOPE, WRITE_SCOPE, RESPOND_SCOPE), ), RoleTemplate( slug="scheduling_participant", name="Scheduling participant", description="Read assigned scheduling polls and submit own availability.", permissions=(READ_SCOPE, RESPOND_SCOPE), ), ) DOCUMENTATION = ( DocumentationTopic( id="scheduling.workspace-layout", title="Scheduling workspace layout", summary="Find workspace actions and read consistently arranged content.", body="The workspace always keeps Reload and New scheduling request at the upper right, including empty, detail, and editor states. Reload sits immediately before New. Creation remains visible but disabled while saving or without creation/write permission; it never disappears inside the request-list card. An open editor keeps its own Save and Discard actions and the unsaved-change guard applies before switching requests. Administrators grant existing Scheduling permissions; this layout adds no permission or automatic invitation or Calendar action.", layer="static", documentation_types=("user", "admin"), audience=("user", "module_admin", "operator"), order=5, translations={"de": { "title": "Terminfindung: Aufbau des Arbeitsbereichs", "summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.", "body": "Der Arbeitsbereich zeigt Neu laden und Neue Terminanfrage immer oben rechts, auch in leerem Zustand, Detailansicht und Bearbeitung. Neu laden steht unmittelbar vor Neu. Das Anlegen bleibt beim Speichern oder ohne Schreibrecht sichtbar, aber deaktiviert; es verschwindet nicht innerhalb der Anfragekartenleiste. Ein geöffneter Editor behält Speichern und Verwerfen; beim Wechsel schützt die Rückfrage ungespeicherte Änderungen. Administratoren vergeben bestehende Scheduling-Rechte; dieses Layout fügt weder Rechte noch automatische Einladungen oder Kalenderaktionen hinzu.", }}, ), DocumentationTopic( id="scheduling.module-boundary", title="Scheduling module boundary", summary="Meeting scheduling and Terminfindung built on reusable Poll availability primitives.", body=( "Scheduling owns meeting scheduling workflows, candidate slots, participant availability " "collection, conflict explanation, reminders, and decision handoff. It depends on Poll " "for reusable availability matrices and poll-backed workflow context, and can optionally " "trigger Evaluation for post-event feedback. " "Access is optional: when installed, Scheduling can use principal resolution, permission " "evaluation, groups, and role templates; without it, reduced signed-link or local organizer " "flows remain possible." ), layer="available", documentation_types=("admin", "user"), audience=("user", "operator", "module_admin", "product_owner"), related_modules=( "poll", "evaluation", "calendar", "appointments", "mail", "notifications", "portal", ), translations={ "de": { "title": "Modulgrenze von Scheduling", "summary": "Terminplanung und Terminfindung auf Grundlage wiederverwendbarer Verfügbarkeitsbausteine von Poll.", "body": ( "Scheduling besitzt Abläufe zur Terminfindung, Terminkandidaten, Erfassung der Teilnehmendenverfügbarkeit, " "Konflikterklärung, Erinnerungen und Entscheidungsübergabe. Für wiederverwendbare Verfügbarkeitsmatrizen und den " "pollgestützten Workflow-Kontext verwendet es Poll und kann optional Evaluation für Rückmeldungen nach einem Termin " "auslösen. Access ist optional: Ist es installiert, kann Scheduling Hauptpersonenauflösung, Berechtigungsprüfung, Gruppen " "und Rollenvorlagen verwenden; ohne Access bleiben eingeschränkte Abläufe über signierte Links oder lokale Organisation möglich." ), } }, metadata={"seed": True}, ), DocumentationTopic( id="scheduling.privacy.data-subject-requests", title="Review Scheduling data in a data-subject request", summary="Collect tenant-scoped participation and coordination metadata while leaving Poll responses and Calendar effects with their owners.", body=( "Scheduling's DSAR provider searches the effective tenant by normalized participant email, membership, identity or bound-account references, and namespaced Scheduling request or participant references. " "It isolates the matching participant, request and candidate-slot context, and matching notification envelopes. It does not export Poll or invitation identifiers, participation-gateway state, reusable enrollment links or proof hashes, anonymous-password hashes, Calendar event or hold identifiers, free/busy conflict detail, notification payloads or errors, token material, opaque metadata, or unrelated participants. Poll remains authoritative for actual availability choices and response-retirement evidence; Calendar remains authoritative for event, hold, and synchronization state. " "Decided, handed-off, cancelled, archived, responded, notified, or removed state is retained with a reason. Active shared scheduling content requires coordinated manual review. The provider can anonymize and retire only a participant who has no invitation, response, enrollment, notification, or terminal decision evidence, and revalidates all of those conditions under tenant-bound row locks before acting." ), layer="configured", documentation_types=("admin",), audience=( "privacy_officer", "scheduling_manager", "records_manager", "operator", ), order=5, conditions=( DocumentationCondition( required_modules=("scheduling", "access"), any_scopes=( "access:privacy:read", "access:privacy:manage", "access:privacy:erase", ), ), ), links=( DocumentationLink( label="Data-subject requests", href="/admin?section=tenant-data-subject-requests", kind="runtime", ), DocumentationLink( label="Scheduling module guide", href="govoplan-scheduling/README.md", kind="repository", ), ), related_modules=("access", "audit", "poll", "calendar", "notifications"), translations={ "de": { "title": "Scheduling-Daten in einer Betroffenenanfrage prüfen", "summary": ( "Mandantenbezogene Teilnahme- und Koordinationsmetadaten erfassen, während Poll-Antworten und Calendar-Wirkungen " "bei ihren zuständigen Modulen verbleiben." ), "body": ( "Der DSAR-Provider von Scheduling durchsucht den wirksamen Mandanten anhand normalisierter E-Mail-Adressen der " "Teilnehmenden, Mitgliedschafts-, Identitäts- oder gebundener Kontoverweise sowie namensraumgebundener Scheduling-Anfrage- " "oder Teilnehmendenverweise. Er isoliert die passende Person, ihren Anfrage- und Terminkandidatenkontext sowie passende " "Benachrichtigungshüllen. Nicht exportiert werden Poll- oder Einladungskennungen, Zustände des Teilnahme-Gateways, " "wiederverwendbare Einschreibelinks oder Nachweishashes, Hashes anonymer Passwörter, Calendar-Ereignis- oder Haltekennungen, " "Details zu Frei-/Belegt-Konflikten, Benachrichtigungsnutzdaten oder -fehler, Tokenmaterial, undurchsichtige Metadaten oder " "andere Teilnehmende. Poll bleibt maßgeblich für tatsächliche Verfügbarkeitsangaben und Nachweise zur Beendigung von Antworten; " "Calendar bleibt maßgeblich für Ereignisse, Vormerkungen und Synchronisationszustände. Entschiedene, übergebene, abgesagte, " "archivierte, beantwortete, benachrichtigte oder entfernte Zustände werden mit Begründung aufbewahrt. Aktive gemeinsame " "Terminplanungsinhalte erfordern eine koordinierte manuelle Prüfung. Der Provider darf nur Personen anonymisieren und " "ausmustern, zu denen keine Einladung, Antwort, Einschreibung, Benachrichtigung oder abschließende Entscheidung vorliegt, " "und prüft alle Bedingungen unter mandantengebundenen Zeilensperren erneut." ), } }, metadata={ "kind": "workflow", "route": "/admin?section=tenant-data-subject-requests", "screen": "Data-subject requests", "help_contexts": ["admin.privacy.data-subject-requests"], "prerequisites": [ "The privacy request and Scheduling selectors have been independently authorized and corroborated.", "The reviewer can coordinate with Poll and Calendar owners when a response or event is involved.", ], "steps": [ "Run the Scheduling provider search and review participant, request, slot, and notification dispositions.", "Run the Poll provider for actual availability choices and the Calendar provider for event or hold state.", "Retain terminal decision and delivery evidence with its reason.", "Execute anonymization only for an approved participant classified as unengaged after revalidation.", ], "limitations": [ "Poll choices and invitation evidence are not copied into Scheduling's export.", "Participant erasure remains manual whenever shared responses, delivery, self-enrollment, Calendar effects, or terminal decisions exist.", ], }, ), DocumentationTopic( id="scheduling.find-and-decide-meeting-time", title="Find and decide a meeting time", summary="Create candidate slots, invite internal or external participants, compare availability, and turn the selected slot into a calendar event when Calendar is available.", body=( "Scheduling records participant requirements, quorum and weighting constraints, response deadlines, reminders, and yes/no/maybe availability through Poll. " "Calendar-aware organizers can inspect conflicts and create tentative holds before deciding. After a decision, Scheduling releases unused holds, creates or links the final event, and records notification handoff state. " "Signed external links expose only the bounded request information allowed by the request's participation and privacy policy. Their governed Poll invitation resolves the tenant before Scheduling runs, so tenant module policy can withdraw public participation without weakening token validation." ), layer="configured", documentation_types=("user",), audience=("user", "organizer", "participant"), conditions=( DocumentationCondition( any_scopes=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE, RESPOND_SCOPE), ), ), related_modules=("poll", "calendar", "notifications", "mail"), translations={ "de": { "title": "Termin finden und entscheiden", "summary": ( "Terminkandidaten anlegen, interne oder externe Personen einladen, Verfügbarkeiten vergleichen und den gewählten Termin " "bei verfügbarem Calendar in ein Kalenderereignis überführen." ), "body": ( "Scheduling erfasst Anforderungen an Teilnehmende, Quorum- und Gewichtungsregeln, Antwortfristen, Erinnerungen und " "Ja-/Nein-/Vielleicht-Verfügbarkeiten über Poll. Organisierende mit Calendar-Anbindung können vor der Entscheidung Konflikte " "prüfen und vorläufige Vormerkungen anlegen. Nach der Entscheidung gibt Scheduling nicht verwendete Vormerkungen frei, " "legt das endgültige Ereignis an oder verknüpft es und zeichnet die Übergabe an Notifications auf. Signierte externe Links " "zeigen nur die begrenzten Anfrageinformationen, welche die Teilnahme- und Datenschutzrichtlinie der Anfrage erlaubt. Die " "gesteuerte Poll-Einladung löst den Mandanten auf, bevor Scheduling ausgeführt wird. Dadurch kann die Mandanten-Modulrichtlinie " "die öffentliche Teilnahme zurückziehen, ohne die Tokenprüfung abzuschwächen." ), } }, metadata={ "kind": "workflow", "route": "/scheduling", "screen": "Scheduling", "help_contexts": [ "scheduling.list", "scheduling.request", "scheduling.editor", "scheduling.public-participation", ], "steps": [ "Prepare candidate times and participation controls.", "Collect and review availability.", "Close the poll and confirm the selected time.", "Hand the decision to Calendar when configured.", ], "outcome": "A recorded scheduling decision with bounded participation and optional Calendar handoff.", "verification": "The request detail shows the decided slot, lifecycle state, participant aggregate, and any Calendar event reference.", }, ), DocumentationTopic( id="scheduling.calendar-coordination", title="Configure scheduling calendar coordination", summary="Understand the Calendar capability and permissions required for conflict checks, tentative holds, and final event handoff.", body=( "Calendar coordination remains optional. It is available only when Calendar contributes its picker capability and the actor can read calendars and availability and write events. " "A disabled Calendar control therefore names the missing integration or authority instead of silently accepting a configuration that cannot run. " "At decision time, a selected tentative hold is promoted in place while every unused hold is submitted to Calendar for durable release. Cancellation likewise waits until Calendar accepts every release. " "Partial or unavailable Calendar effects keep the Scheduling lifecycle incomplete and expose retry-required cleanup state; an exact retry reuses the recorded event and operation identities instead of duplicating effects. " "Administrators recover synchronized failures through Calendar's outbound-change reconciliation and then repeat the Scheduling action. Scheduling stores only operation identifiers, last-known state, and pending slot references; Calendar remains authoritative for event and outbox evidence. " "Administrators should enable the Calendar module and grant the bounded calendar, availability, and event permissions needed by the organizer; Scheduling never imports Calendar internals." ), layer="configured", documentation_types=("admin", "user"), audience=("organizer", "module_admin", "tenant_admin"), related_modules=("calendar", "access", "policy"), translations={ "de": { "title": "Calendar-Koordination für Scheduling konfigurieren", "summary": ( "Die Calendar-Fähigkeit und Berechtigungen für Konfliktprüfungen, vorläufige Vormerkungen und die endgültige " "Ereignisübergabe verstehen." ), "body": ( "Die Calendar-Koordination bleibt optional. Sie ist nur verfügbar, wenn Calendar seine Auswahlfähigkeit bereitstellt und " "die handelnde Person Kalender und Verfügbarkeiten lesen sowie Ereignisse schreiben darf. Eine deaktivierte Calendar-Steuerung " "nennt daher die fehlende Integration oder Berechtigung, statt eine nicht ausführbare Konfiguration stillschweigend zu " "akzeptieren. Bei der Entscheidung wird eine ausgewählte vorläufige Vormerkung direkt bestätigt und jede ungenutzte " "Vormerkung zur dauerhaften Freigabe an Calendar übergeben. Auch eine Absage wartet, bis Calendar jede Freigabe angenommen hat. " "Teilweise oder nicht verfügbare Calendar-Wirkungen halten den Scheduling-Lebenszyklus unvollständig und zeigen einen " "bereinigungsbedürftigen Wiederholungszustand; eine identische Wiederholung verwendet die aufgezeichneten Ereignis- und " "Vorgangskennungen wieder, statt Wirkungen zu duplizieren. Administrierende beheben synchronisierte Fehler über Calendars " "Abgleich ausgehender Änderungen und wiederholen anschließend die Scheduling-Aktion. Scheduling speichert nur " "Vorgangskennungen, zuletzt bekannten Zustand und Verweise auf ausstehende Termine; Calendar bleibt maßgeblich für Ereignis- " "und Outbox-Nachweise. Administrierende sollten Calendar aktivieren und der organisierenden Person die begrenzten Kalender-, " "Verfügbarkeits- und Ereignisberechtigungen gewähren; Scheduling importiert keine Calendar-Interna." ), } }, metadata={ "kind": "reference", "help_contexts": [ "scheduling.calendar-integration", "scheduling.calendar-coordination", ], }, ), DocumentationTopic( id="scheduling.participation-governance", title="Govern public scheduling participation", summary="Resolve disabled guest invitations without weakening signed-link privacy or participation policy.", body=( "Public invitation links are issued only when the configured response, privacy, password, email, and update controls can be enforced by the public participation gateway. " "When enforcement is unavailable, signed-in participants may continue to respond through their assigned request, but the system does not issue a weaker guest link. " "A system or tenant administrator must restore the governed Poll/public-participation capability or keep the request limited to signed-in participation." ), layer="configured", documentation_types=("admin",), audience=("operator", "module_admin", "tenant_admin"), related_modules=("poll", "policy", "access"), translations={ "de": { "title": "Öffentliche Teilnahme an Terminplanungen steuern", "summary": ( "Deaktivierte Gasteinladungen behandeln, ohne Datenschutz signierter Links oder Teilnahmerichtlinien abzuschwächen." ), "body": ( "Öffentliche Einladungslinks werden nur ausgestellt, wenn das Gateway für öffentliche Teilnahme die konfigurierten " "Antwort-, Datenschutz-, Passwort-, E-Mail- und Änderungsregeln durchsetzen kann. Ist diese Durchsetzung nicht verfügbar, " "dürfen angemeldete Teilnehmende weiterhin über ihre zugewiesene Anfrage antworten; das System stellt jedoch keinen " "schwächeren Gastlink aus. Eine System- oder Mandantenadministration muss die gesteuerte Poll-/Teilnahmefähigkeit " "wiederherstellen oder die Anfrage auf angemeldete Teilnahme beschränken." ), } }, metadata={ "kind": "pattern", "help_contexts": [ "scheduling.public-participation-blocker", "scheduling.public-participation", ], }, ), DocumentationTopic( id="scheduling.public-self-enrollment", title="Use governed public self-enrollment links", summary="Issue reusable scheduling links with explicit capacity, expiry, identity, account-binding, and abuse controls.", body=( "A public self-enrollment link is distinct from a participant-specific invitation. " "Organizers must choose a capacity and future expiry, and can independently allow anonymous and signed-in enrollment. " "Every participant supplies a display name; email is required only when the request policy says so. Anonymous participants create and retain a separate recovery proof, which is submitted in the request body and is never embedded in the link, logs, analytics, or durable clear text. " "Signed-in participants must explicitly confirm account binding. A later signed-in submission may bind an anonymous enrollment only when its recovery proof is supplied; the binding is audited. " "Deployment policy can disable self-enrollment or cap its maximum capacity. Redis provides shared fixed-window throttling when configured, while development uses the bounded single-node fallback. Existing personalized invitation links are unchanged. " "Revoking or expiring the reusable link prevents new access immediately. Capacity is serialized with participant creation, retries are idempotent through the caller's idempotency key, and existing proof holders can update only while request policy permits updates. Other participants receive only the request's existing aggregate or governed roster projection." ), layer="configured", documentation_types=("admin", "user"), audience=("organizer", "participant", "module_admin", "tenant_admin"), conditions=( DocumentationCondition( any_scopes=(WRITE_SCOPE, ADMIN_SCOPE, RESPOND_SCOPE), ), ), related_modules=("poll", "access", "policy"), translations={ "de": { "title": "Gesteuerte öffentliche Selbsteinschreibelinks verwenden", "summary": ( "Wiederverwendbare Terminplanungslinks mit ausdrücklicher Kapazität, Laufzeit, Identitäts-, Kontobindungs- und " "Missbrauchsschutzsteuerung ausstellen." ), "body": ( "Ein öffentlicher Selbsteinschreibelink unterscheidet sich von einer personenspezifischen Einladung. Organisierende müssen " "Kapazität und zukünftigen Ablauf festlegen und können anonyme sowie angemeldete Einschreibung unabhängig zulassen. Alle " "Teilnehmenden geben einen Anzeigenamen an; eine E-Mail-Adresse ist nur erforderlich, wenn die Anfragerichtlinie dies verlangt. " "Anonyme Teilnehmende erzeugen und verwahren einen getrennten Wiederherstellungsnachweis. Dieser wird im Anfragekörper " "übermittelt und niemals in Link, Protokollen, Analysen oder dauerhaftem Klartext abgelegt. Angemeldete Teilnehmende müssen " "die Kontobindung ausdrücklich bestätigen. Eine spätere angemeldete Einreichung darf eine anonyme Einschreibung nur mit " "deren Wiederherstellungsnachweis binden; die Bindung wird auditiert. Die Deployment-Richtlinie kann Selbsteinschreibung " "deaktivieren oder die maximale Kapazität begrenzen. Redis stellt bei Konfiguration eine gemeinsame Drosselung mit festem " "Zeitfenster bereit; in der Entwicklung gilt der begrenzte Einzelknoten-Rückfall. Vorhandene persönliche Einladungslinks " "bleiben unverändert. Widerruf oder Ablauf des wiederverwendbaren Links verhindert neuen Zugriff sofort. Die Kapazität wird " "gemeinsam mit dem Anlegen der Person serialisiert, Wiederholungen sind über den Idempotenzschlüssel idempotent und bestehende " "Nachweisinhaber dürfen nur ändern, solange die Anfragerichtlinie Änderungen erlaubt. Andere Teilnehmende erhalten nur die " "bereits vorhandene Aggregat- oder gesteuerte Teilnehmerlistenansicht der Anfrage." ), } }, metadata={ "kind": "workflow", "route": "/scheduling", "help_contexts": [ "scheduling.public-self-enrollment", "scheduling.public-self-enrollment-governance", ], "steps": [ "Choose a bounded capacity, expiry, and permitted identity modes.", "Copy the newly issued link; the raw credential is shown only once.", "Monitor enrollment count and revoke the link when it is no longer needed.", "Require recovery proof before updating or binding an anonymous response.", ], "verification": "The link list shows status, expiry, capacity use, access modes, and revocation without redisplaying its credential.", }, ), ) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: from govoplan_scheduling.backend.db.models import ( SchedulingCandidateSlot, SchedulingNotification, SchedulingParticipant, SchedulingPublicEnrollmentLink, SchedulingRequest, ) return { "scheduling_requests": ( session.query(SchedulingRequest) .filter( SchedulingRequest.tenant_id == tenant_id, SchedulingRequest.deleted_at.is_(None), ) .count() ), "scheduling_candidate_slots": ( session.query(SchedulingCandidateSlot) .filter( SchedulingCandidateSlot.tenant_id == tenant_id, SchedulingCandidateSlot.deleted_at.is_(None), ) .count() ), "scheduling_participants": ( session.query(SchedulingParticipant) .filter( SchedulingParticipant.tenant_id == tenant_id, SchedulingParticipant.deleted_at.is_(None), ) .count() ), "scheduling_notifications": ( session.query(SchedulingNotification) .filter( SchedulingNotification.tenant_id == tenant_id, SchedulingNotification.status == "pending", ) .count() ), "scheduling_public_enrollment_links": ( session.query(SchedulingPublicEnrollmentLink) .filter( SchedulingPublicEnrollmentLink.tenant_id == tenant_id, SchedulingPublicEnrollmentLink.revoked_at.is_(None), ) .count() ), } def _scheduling_router(context: ModuleContext): from govoplan_scheduling.backend.runtime import configure_runtime from govoplan_scheduling.backend.router import router configure_runtime(registry=context.registry, settings=context.settings) return router def _scheduling_dsar_provider(context: ModuleContext) -> object: del context from govoplan_scheduling.backend.dsar_provider import SchedulingDsarProvider return SchedulingDsarProvider() def _public_tenant_resolver(request: object, session: object) -> str | None: path_params = getattr(request, "path_params", {}) request_id = str(path_params.get("request_id") or "").strip() token = str(path_params.get("token") or "").strip() path = str(getattr(getattr(request, "url", None), "path", "")) if not request_id or not token: return None from govoplan_scheduling.backend.db.models import ( SchedulingPublicEnrollmentLink, SchedulingRequest, ) from govoplan_scheduling.backend.security import public_credential_hash from govoplan_core.db.base import utcnow if "/scheduling/public-enrollment/" in path: link = ( session.query(SchedulingPublicEnrollmentLink) .filter( SchedulingPublicEnrollmentLink.request_id == request_id, SchedulingPublicEnrollmentLink.token_hash == public_credential_hash(token), SchedulingPublicEnrollmentLink.revoked_at.is_(None), SchedulingPublicEnrollmentLink.expires_at > utcnow(), ) .one_or_none() ) return link.tenant_id if link is not None else None if "/scheduling/public/" not in path: return None app = getattr(request, "app", None) registry = getattr(getattr(app, "state", None), "govoplan_registry", None) provider = poll_participation_gateway_provider(registry) if provider is None: return None gateway = PollResponseGatewayRef( module_id=MODULE_ID, resource_type="scheduling_request", resource_id=request_id, ) try: invitation = provider.resolve_public_invitation( session, token=token, gateway=gateway, ) except PollCapabilityError: return None scheduling_request = ( session.query(SchedulingRequest) .filter( SchedulingRequest.id == request_id, SchedulingRequest.tenant_id == invitation.tenant_id, SchedulingRequest.poll_id == invitation.poll_id, SchedulingRequest.deleted_at.is_(None), ) .one_or_none() ) return scheduling_request.tenant_id if scheduling_request is not None else None manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, dependencies=("poll",), optional_dependencies=( "access", "calendar", "appointments", "evaluation", "mail", "notifications", "policy", "portal", "workflow_engine", "tasks", "idm", "organizations", "addresses", ), optional_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_CALENDAR_SCHEDULING, CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY, CAPABILITY_ACCESS_PEOPLE_SEARCH, CAPABILITY_ADDRESSES_PEOPLE_SEARCH, ), required_capabilities=( CAPABILITY_POLL_SCHEDULING, CAPABILITY_POLL_PARTICIPATION_GATEWAY, ), provides_interfaces=( ModuleInterfaceProvider( name="scheduling.candidate_slots", version=MODULE_VERSION ), ModuleInterfaceProvider( name="scheduling.decision_handoff", version=MODULE_VERSION ), ModuleInterfaceProvider(name=SCHEDULING_DSAR_CAPABILITY, version="0.1.0"), ), requires_interfaces=( ModuleInterfaceRequirement( name="poll.option_ordering", version_min="0.1.11", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name="poll.availability_matrix", version_min="0.1.11", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name="poll.response_collection", version_min="0.1.11", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name="poll.workflow_context", version_min="0.1.11", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name="poll.governed_participation", version_min="0.1.11", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name="evaluation.feedback", version_min="0.1.8", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="notifications.dispatch", version_min="0.1.8", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_ACCESS_PEOPLE_SEARCH, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="calendar.scheduling", version_min="0.1.9", version_max_exclusive="0.2.0", optional=True, ), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, nav_items=( NavItem( path="/scheduling", label="Scheduling", icon="calendar-clock", required_any=(READ_SCOPE,), order=56, ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/scheduling-webui", routes=( FrontendRoute( path="/scheduling", component="SchedulingPage", required_any=(READ_SCOPE,), order=56, ), ), public_routes=( PublicFrontendRoute( path="/scheduling/public/:requestId/:token", component="SchedulingPublicPage", order=10, ), PublicFrontendRoute( path="/scheduling/enrol/:requestId/:token", component="SchedulingEnrollmentPage", order=11, ), ), nav_items=( NavItem( path="/scheduling", label="Scheduling", icon="calendar-clock", required_any=(READ_SCOPE,), order=56, ), ), product_areas=( ProductAreaContribution( id="meetings-decisions", module_id=MODULE_ID, label="i18n:govoplan-core.product_area.meetings_decisions", icon="calendar", description="i18n:govoplan-core.product_area.meetings_decisions_description", surface_ids=( "scheduling.nav.scheduling", "scheduling.route.scheduling", ), order=50, ), ), view_surfaces=( ViewSurface( id="scheduling.widget.open-requests", module_id=MODULE_ID, kind="section", label="Scheduling requests widget", order=45, ), ), ), route_factory=_scheduling_router, public_tenant_resolver=_public_tenant_resolver, tenant_summary_providers=(_tenant_summary,), migration_spec=MigrationSpec( module_id=MODULE_ID, metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( scheduling_models.SchedulingRequest, scheduling_models.SchedulingCandidateSlot, scheduling_models.SchedulingParticipant, scheduling_models.SchedulingPublicEnrollmentLink, scheduling_models.SchedulingNotification, label="Scheduling", ), retirement_notes="Destructive retirement drops scheduling-owned database tables after the installer captures a database snapshot.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( scheduling_models.SchedulingRequest, scheduling_models.SchedulingCandidateSlot, scheduling_models.SchedulingParticipant, scheduling_models.SchedulingPublicEnrollmentLink, scheduling_models.SchedulingNotification, label="Scheduling", ), ), documentation=DOCUMENTATION, capability_factories={ SCHEDULING_DSAR_CAPABILITY: _scheduling_dsar_provider, }, capability_documentation={ SCHEDULING_DSAR_CAPABILITY: CapabilityDocumentation( label="Scheduling data-subject request provider", summary="Finds isolated Scheduling participation and coordination metadata and classifies governed erasure actions.", contract_version="0.1.0", documentation_types=("admin",), audience=("privacy_officer", "scheduling_manager", "records_manager"), ), }, architecture=declared_module_architecture( layer="communication_participation", kind="domain", maturity="vertical_slice", documentation_ref="README.md", test_ref="tests/test_service.py", known_limits=( "Reference deployment notification delivery and every calendar-provider constraint remain incomplete.", ), owned_concepts=( "scheduling request", "candidate slot", "scheduling participant", "public self-enrollment link", "scheduling decision", ), non_owned_concepts=( "poll response primitive", "calendar event", "mail delivery", ), recovery_docs=("README.md",), security_docs=("README.md",), ), ) manifest = with_documentation_structured_translations( manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS ) def get_manifest() -> ModuleManifest: return manifest