diff --git a/pyproject.toml b/pyproject.toml index 2c69174..89cadfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-scheduling" -version = "0.1.18" +version = "0.1.20" description = "GovOPlaN meeting scheduling and Terminfindung module seed." readme = "README.md" requires-python = ">=3.12" diff --git a/src/govoplan_scheduling/__init__.py b/src/govoplan_scheduling/__init__.py index 6465439..a788522 100644 --- a/src/govoplan_scheduling/__init__.py +++ b/src/govoplan_scheduling/__init__.py @@ -2,4 +2,4 @@ __all__ = ["__version__"] -__version__ = "0.1.18" +__version__ = "0.1.20" diff --git a/src/govoplan_scheduling/backend/manifest.py b/src/govoplan_scheduling/backend/manifest.py index bb21749..a895441 100644 --- a/src/govoplan_scheduling/backend/manifest.py +++ b/src/govoplan_scheduling/backend/manifest.py @@ -2,9 +2,15 @@ from __future__ import annotations from pathlib import Path -from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER +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.module_guards import ( + drop_table_retirement_provider, + persistent_table_uninstall_guard, +) from govoplan_core.core.modules import ( CapabilityDocumentation, DocumentationTopic, @@ -42,7 +48,7 @@ from govoplan_scheduling.backend.dsar_provider import SCHEDULING_DSAR_CAPABILITY MODULE_ID = "scheduling" MODULE_NAME = "Scheduling" -MODULE_VERSION = "0.1.19" +MODULE_VERSION = "0.1.20" READ_SCOPE = "scheduling:schedule:read" WRITE_SCOPE = "scheduling:schedule:write" ADMIN_SCOPE = "scheduling:schedule:admin" @@ -64,10 +70,26 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio 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."), + _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 = ( @@ -102,7 +124,28 @@ DOCUMENTATION = ( layer="available", documentation_types=("admin", "user"), audience=("user", "operator", "module_admin", "product_owner"), - related_modules=("poll", "evaluation", "calendar", "appointments", "mail", "notifications", "portal"), + 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( @@ -116,7 +159,12 @@ DOCUMENTATION = ( ), layer="configured", documentation_types=("admin",), - audience=("privacy_officer", "scheduling_manager", "records_manager", "operator"), + audience=( + "privacy_officer", + "scheduling_manager", + "records_manager", + "operator", + ), order=5, conditions=( DocumentationCondition( @@ -141,6 +189,29 @@ DOCUMENTATION = ( ), ), 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", @@ -180,6 +251,24 @@ DOCUMENTATION = ( ), ), 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", @@ -216,9 +305,35 @@ DOCUMENTATION = ( 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"], + "help_contexts": [ + "scheduling.calendar-integration", + "scheduling.calendar-coordination", + ], }, ), DocumentationTopic( @@ -234,6 +349,21 @@ DOCUMENTATION = ( 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": [ @@ -263,6 +393,30 @@ DOCUMENTATION = ( ), ), 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", @@ -283,27 +437,45 @@ DOCUMENTATION = ( def _tenant_summary(session, tenant_id: str) -> dict[str, int]: - from govoplan_scheduling.backend.db.models import SchedulingCandidateSlot, SchedulingNotification, SchedulingParticipant, SchedulingPublicEnrollmentLink, SchedulingRequest + 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)) + .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)) + .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)) + .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") + .filter( + SchedulingNotification.tenant_id == tenant_id, + SchedulingNotification.status == "pending", + ) .count() ), "scheduling_public_enrollment_links": ( @@ -340,7 +512,10 @@ def _public_tenant_resolver(request: object, session: object) -> str | None: if not request_id or not token: return None - from govoplan_scheduling.backend.db.models import SchedulingPublicEnrollmentLink, SchedulingRequest + 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 @@ -349,7 +524,8 @@ def _public_tenant_resolver(request: object, session: object) -> str | None: session.query(SchedulingPublicEnrollmentLink) .filter( SchedulingPublicEnrollmentLink.request_id == request_id, - SchedulingPublicEnrollmentLink.token_hash == public_credential_hash(token), + SchedulingPublicEnrollmentLink.token_hash + == public_credential_hash(token), SchedulingPublicEnrollmentLink.revoked_at.is_(None), SchedulingPublicEnrollmentLink.expires_at > utcnow(), ) @@ -395,7 +571,21 @@ manifest = ModuleManifest( 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_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, @@ -409,25 +599,82 @@ manifest = ModuleManifest( 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.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), + 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),), + 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", @@ -451,7 +698,15 @@ manifest = ModuleManifest( order=11, ), ), - nav_items=(NavItem(path="/scheduling", label="Scheduling", icon="calendar-clock", required_any=(READ_SCOPE,), order=56),), + nav_items=( + NavItem( + path="/scheduling", + label="Scheduling", + icon="calendar-clock", + required_any=(READ_SCOPE,), + order=56, + ), + ), product_areas=( ProductAreaContribution( id="meetings-decisions", @@ -459,7 +714,10 @@ manifest = ModuleManifest( 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"), + surface_ids=( + "scheduling.nav.scheduling", + "scheduling.route.scheduling", + ), order=50, ), ), @@ -520,9 +778,21 @@ manifest = ModuleManifest( 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"), + 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",), ), diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 66ef1a0..4c5ce2b 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -7,6 +7,19 @@ from govoplan_scheduling.backend.manifest import get_manifest class SchedulingManifestTests(unittest.TestCase): + def test_all_static_topics_have_complete_german_content(self) -> None: + manifest = get_manifest() + for topic in manifest.documentation: + german = (topic.translations or {}).get("de", {}) + self.assertEqual( + {"title", "summary", "body"}, + set(german), + topic.id, + ) + self.assertTrue( + all(str(value).strip() for value in german.values()), topic.id + ) + def test_manifest_contract(self) -> None: manifest = get_manifest() @@ -24,7 +37,9 @@ class SchedulingManifestTests(unittest.TestCase): self.assertIn("auth.principalResolver", manifest.optional_capabilities) self.assertIn("poll.scheduling", manifest.required_capabilities) self.assertIn("calendar.scheduling", manifest.optional_capabilities) - self.assertIn("policy.schedulingParticipantPrivacy", manifest.optional_capabilities) + self.assertIn( + "policy.schedulingParticipantPrivacy", manifest.optional_capabilities + ) self.assertIn("access.people_search", manifest.optional_capabilities) self.assertIn("addresses.people_search", manifest.optional_capabilities) self.assertIn("evaluation", manifest.optional_dependencies) @@ -39,15 +54,41 @@ class SchedulingManifestTests(unittest.TestCase): ], [route.path for route in manifest.frontend.public_routes], ) - self.assertIn("poll.availability_matrix", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("poll.response_collection", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("poll.workflow_context", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("poll.governed_participation", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("notifications.dispatch", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("access.people_search", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("addresses.people_search", {interface.name for interface in manifest.requires_interfaces}) - self.assertIn("calendar.scheduling", {interface.name for interface in manifest.requires_interfaces}) - required_interfaces = {interface.name: interface for interface in manifest.requires_interfaces} + self.assertIn( + "poll.availability_matrix", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "poll.response_collection", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "poll.workflow_context", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "poll.governed_participation", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "notifications.dispatch", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "access.people_search", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "addresses.people_search", + {interface.name for interface in manifest.requires_interfaces}, + ) + self.assertIn( + "calendar.scheduling", + {interface.name for interface in manifest.requires_interfaces}, + ) + required_interfaces = { + interface.name: interface for interface in manifest.requires_interfaces + } for interface_name in ( "poll.availability_matrix", "poll.response_collection", @@ -61,14 +102,18 @@ class SchedulingManifestTests(unittest.TestCase): self.assertEqual("workflow", workflow.metadata["kind"]) self.assertEqual("/scheduling", workflow.metadata["route"]) self.assertIn("scheduling.request", workflow.metadata["help_contexts"]) - self.assertIn("scheduling.public-participation", workflow.metadata["help_contexts"]) + self.assertIn( + "scheduling.public-participation", workflow.metadata["help_contexts"] + ) self.assertIn( "scheduling.calendar-coordination", documentation["scheduling.calendar-coordination"].metadata["help_contexts"], ) self.assertIn( "scheduling.public-self-enrollment", - documentation["scheduling.public-self-enrollment"].metadata["help_contexts"], + documentation["scheduling.public-self-enrollment"].metadata[ + "help_contexts" + ], ) diff --git a/webui/package.json b/webui/package.json index bd94d71..191f4a5 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/scheduling-webui", - "version": "0.1.18", + "version": "0.1.20", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/src/module.ts b/webui/src/module.ts index f37ceea..5974815 100644 --- a/webui/src/module.ts +++ b/webui/src/module.ts @@ -60,7 +60,7 @@ const schedulingDashboardWidgets: DashboardWidgetsUiCapability = { export const schedulingModule: PlatformWebModule = { id: "scheduling", label: "Scheduling", - version: "0.1.18", + version: "0.1.20", dependencies: ["poll"], optionalDependencies: ["access", "calendar", "mail", "notifications", "workflow", "appointments", "addresses"], translations: generatedTranslations,