docs(scheduling): complete German reference coverage
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-23 20:11:47 +02:00
parent 0fd297231b
commit 118fe395b0
6 changed files with 367 additions and 52 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-scheduling" name = "govoplan-scheduling"
version = "0.1.18" version = "0.1.20"
description = "GovOPlaN meeting scheduling and Terminfindung module seed." description = "GovOPlaN meeting scheduling and Terminfindung module seed."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -2,4 +2,4 @@
__all__ = ["__version__"] __all__ = ["__version__"]
__version__ = "0.1.18" __version__ = "0.1.20"
+306 -36
View File
@@ -2,9 +2,15 @@ from __future__ import annotations
from pathlib import Path 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.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 ( from govoplan_core.core.modules import (
CapabilityDocumentation, CapabilityDocumentation,
DocumentationTopic, DocumentationTopic,
@@ -42,7 +48,7 @@ from govoplan_scheduling.backend.dsar_provider import SCHEDULING_DSAR_CAPABILITY
MODULE_ID = "scheduling" MODULE_ID = "scheduling"
MODULE_NAME = "Scheduling" MODULE_NAME = "Scheduling"
MODULE_VERSION = "0.1.19" MODULE_VERSION = "0.1.20"
READ_SCOPE = "scheduling:schedule:read" READ_SCOPE = "scheduling:schedule:read"
WRITE_SCOPE = "scheduling:schedule:write" WRITE_SCOPE = "scheduling:schedule:write"
ADMIN_SCOPE = "scheduling:schedule:admin" ADMIN_SCOPE = "scheduling:schedule:admin"
@@ -64,10 +70,26 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio
PERMISSIONS = ( PERMISSIONS = (
_permission(READ_SCOPE, "View scheduling", "Read scheduling polls, proposals, participant state, and selected outcomes."), _permission(
_permission(WRITE_SCOPE, "Manage own scheduling", "Create scheduling polls and manage requests for which the account is the organizer."), READ_SCOPE,
_permission(ADMIN_SCOPE, "Administer scheduling", "Manage every tenant scheduling request and configure scheduling policies, external participation, and retention defaults."), "View scheduling",
_permission(RESPOND_SCOPE, "Respond to scheduling polls", "Submit and update own scheduling availability responses."), "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 = ( ROLE_TEMPLATES = (
@@ -102,7 +124,28 @@ DOCUMENTATION = (
layer="available", layer="available",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "product_owner"), 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}, metadata={"seed": True},
), ),
DocumentationTopic( DocumentationTopic(
@@ -116,7 +159,12 @@ DOCUMENTATION = (
), ),
layer="configured", layer="configured",
documentation_types=("admin",), documentation_types=("admin",),
audience=("privacy_officer", "scheduling_manager", "records_manager", "operator"), audience=(
"privacy_officer",
"scheduling_manager",
"records_manager",
"operator",
),
order=5, order=5,
conditions=( conditions=(
DocumentationCondition( DocumentationCondition(
@@ -141,6 +189,29 @@ DOCUMENTATION = (
), ),
), ),
related_modules=("access", "audit", "poll", "calendar", "notifications"), 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={ metadata={
"kind": "workflow", "kind": "workflow",
"route": "/admin?section=tenant-data-subject-requests", "route": "/admin?section=tenant-data-subject-requests",
@@ -180,6 +251,24 @@ DOCUMENTATION = (
), ),
), ),
related_modules=("poll", "calendar", "notifications", "mail"), 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={ metadata={
"kind": "workflow", "kind": "workflow",
"route": "/scheduling", "route": "/scheduling",
@@ -216,9 +305,35 @@ DOCUMENTATION = (
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("organizer", "module_admin", "tenant_admin"), audience=("organizer", "module_admin", "tenant_admin"),
related_modules=("calendar", "access", "policy"), 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={ metadata={
"kind": "reference", "kind": "reference",
"help_contexts": ["scheduling.calendar-integration", "scheduling.calendar-coordination"], "help_contexts": [
"scheduling.calendar-integration",
"scheduling.calendar-coordination",
],
}, },
), ),
DocumentationTopic( DocumentationTopic(
@@ -234,6 +349,21 @@ DOCUMENTATION = (
documentation_types=("admin",), documentation_types=("admin",),
audience=("operator", "module_admin", "tenant_admin"), audience=("operator", "module_admin", "tenant_admin"),
related_modules=("poll", "policy", "access"), 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={ metadata={
"kind": "pattern", "kind": "pattern",
"help_contexts": [ "help_contexts": [
@@ -263,6 +393,30 @@ DOCUMENTATION = (
), ),
), ),
related_modules=("poll", "access", "policy"), 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={ metadata={
"kind": "workflow", "kind": "workflow",
"route": "/scheduling", "route": "/scheduling",
@@ -283,27 +437,45 @@ DOCUMENTATION = (
def _tenant_summary(session, tenant_id: str) -> dict[str, int]: 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 { return {
"scheduling_requests": ( "scheduling_requests": (
session.query(SchedulingRequest) 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() .count()
), ),
"scheduling_candidate_slots": ( "scheduling_candidate_slots": (
session.query(SchedulingCandidateSlot) 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() .count()
), ),
"scheduling_participants": ( "scheduling_participants": (
session.query(SchedulingParticipant) 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() .count()
), ),
"scheduling_notifications": ( "scheduling_notifications": (
session.query(SchedulingNotification) session.query(SchedulingNotification)
.filter(SchedulingNotification.tenant_id == tenant_id, SchedulingNotification.status == "pending") .filter(
SchedulingNotification.tenant_id == tenant_id,
SchedulingNotification.status == "pending",
)
.count() .count()
), ),
"scheduling_public_enrollment_links": ( "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: if not request_id or not token:
return None 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_scheduling.backend.security import public_credential_hash
from govoplan_core.db.base import utcnow from govoplan_core.db.base import utcnow
@@ -349,7 +524,8 @@ def _public_tenant_resolver(request: object, session: object) -> str | None:
session.query(SchedulingPublicEnrollmentLink) session.query(SchedulingPublicEnrollmentLink)
.filter( .filter(
SchedulingPublicEnrollmentLink.request_id == request_id, 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.revoked_at.is_(None),
SchedulingPublicEnrollmentLink.expires_at > utcnow(), SchedulingPublicEnrollmentLink.expires_at > utcnow(),
) )
@@ -395,7 +571,21 @@ manifest = ModuleManifest(
name=MODULE_NAME, name=MODULE_NAME,
version=MODULE_VERSION, version=MODULE_VERSION,
dependencies=("poll",), 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=( optional_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -409,25 +599,82 @@ manifest = ModuleManifest(
CAPABILITY_POLL_PARTICIPATION_GATEWAY, CAPABILITY_POLL_PARTICIPATION_GATEWAY,
), ),
provides_interfaces=( provides_interfaces=(
ModuleInterfaceProvider(name="scheduling.candidate_slots", version=MODULE_VERSION), ModuleInterfaceProvider(
ModuleInterfaceProvider(name="scheduling.decision_handoff", version=MODULE_VERSION), name="scheduling.candidate_slots", version=MODULE_VERSION
),
ModuleInterfaceProvider(
name="scheduling.decision_handoff", version=MODULE_VERSION
),
ModuleInterfaceProvider(name=SCHEDULING_DSAR_CAPABILITY, version="0.1.0"), ModuleInterfaceProvider(name=SCHEDULING_DSAR_CAPABILITY, version="0.1.0"),
), ),
requires_interfaces=( requires_interfaces=(
ModuleInterfaceRequirement(name="poll.option_ordering", version_min="0.1.11", version_max_exclusive="0.2.0"), ModuleInterfaceRequirement(
ModuleInterfaceRequirement(name="poll.availability_matrix", version_min="0.1.11", version_max_exclusive="0.2.0"), name="poll.option_ordering",
ModuleInterfaceRequirement(name="poll.response_collection", version_min="0.1.11", version_max_exclusive="0.2.0"), version_min="0.1.11",
ModuleInterfaceRequirement(name="poll.workflow_context", version_min="0.1.11", version_max_exclusive="0.2.0"), 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(
ModuleInterfaceRequirement(name="notifications.dispatch", version_min="0.1.8", version_max_exclusive="0.2.0", optional=True), name="poll.availability_matrix",
ModuleInterfaceRequirement(name=CAPABILITY_ACCESS_PEOPLE_SEARCH, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True), version_min="0.1.11",
ModuleInterfaceRequirement(name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True), version_max_exclusive="0.2.0",
ModuleInterfaceRequirement(name="calendar.scheduling", version_min="0.1.9", version_max_exclusive="0.2.0", optional=True), ),
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, permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES, 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( frontend=FrontendModule(
module_id=MODULE_ID, module_id=MODULE_ID,
package_name="@govoplan/scheduling-webui", package_name="@govoplan/scheduling-webui",
@@ -451,7 +698,15 @@ manifest = ModuleManifest(
order=11, 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=( product_areas=(
ProductAreaContribution( ProductAreaContribution(
id="meetings-decisions", id="meetings-decisions",
@@ -459,7 +714,10 @@ manifest = ModuleManifest(
label="i18n:govoplan-core.product_area.meetings_decisions", label="i18n:govoplan-core.product_area.meetings_decisions",
icon="calendar", icon="calendar",
description="i18n:govoplan-core.product_area.meetings_decisions_description", 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, order=50,
), ),
), ),
@@ -520,9 +778,21 @@ manifest = ModuleManifest(
maturity="vertical_slice", maturity="vertical_slice",
documentation_ref="README.md", documentation_ref="README.md",
test_ref="tests/test_service.py", test_ref="tests/test_service.py",
known_limits=("Reference deployment notification delivery and every calendar-provider constraint remain incomplete.",), known_limits=(
owned_concepts=("scheduling request", "candidate slot", "scheduling participant", "public self-enrollment link", "scheduling decision"), "Reference deployment notification delivery and every calendar-provider constraint remain incomplete.",
non_owned_concepts=("poll response primitive", "calendar event", "mail delivery"), ),
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",), recovery_docs=("README.md",),
security_docs=("README.md",), security_docs=("README.md",),
), ),
+57 -12
View File
@@ -7,6 +7,19 @@ from govoplan_scheduling.backend.manifest import get_manifest
class SchedulingManifestTests(unittest.TestCase): 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: def test_manifest_contract(self) -> None:
manifest = get_manifest() manifest = get_manifest()
@@ -24,7 +37,9 @@ class SchedulingManifestTests(unittest.TestCase):
self.assertIn("auth.principalResolver", manifest.optional_capabilities) self.assertIn("auth.principalResolver", manifest.optional_capabilities)
self.assertIn("poll.scheduling", manifest.required_capabilities) self.assertIn("poll.scheduling", manifest.required_capabilities)
self.assertIn("calendar.scheduling", manifest.optional_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("access.people_search", manifest.optional_capabilities)
self.assertIn("addresses.people_search", manifest.optional_capabilities) self.assertIn("addresses.people_search", manifest.optional_capabilities)
self.assertIn("evaluation", manifest.optional_dependencies) self.assertIn("evaluation", manifest.optional_dependencies)
@@ -39,15 +54,41 @@ class SchedulingManifestTests(unittest.TestCase):
], ],
[route.path for route in manifest.frontend.public_routes], [route.path for route in manifest.frontend.public_routes],
) )
self.assertIn("poll.availability_matrix", {interface.name for interface in manifest.requires_interfaces}) self.assertIn(
self.assertIn("poll.response_collection", {interface.name for interface in manifest.requires_interfaces}) "poll.availability_matrix",
self.assertIn("poll.workflow_context", {interface.name for interface in manifest.requires_interfaces}) {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(
self.assertIn("access.people_search", {interface.name for interface in manifest.requires_interfaces}) "poll.response_collection",
self.assertIn("addresses.people_search", {interface.name for interface in manifest.requires_interfaces}) {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.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 ( for interface_name in (
"poll.availability_matrix", "poll.availability_matrix",
"poll.response_collection", "poll.response_collection",
@@ -61,14 +102,18 @@ class SchedulingManifestTests(unittest.TestCase):
self.assertEqual("workflow", workflow.metadata["kind"]) self.assertEqual("workflow", workflow.metadata["kind"])
self.assertEqual("/scheduling", workflow.metadata["route"]) self.assertEqual("/scheduling", workflow.metadata["route"])
self.assertIn("scheduling.request", workflow.metadata["help_contexts"]) 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( self.assertIn(
"scheduling.calendar-coordination", "scheduling.calendar-coordination",
documentation["scheduling.calendar-coordination"].metadata["help_contexts"], documentation["scheduling.calendar-coordination"].metadata["help_contexts"],
) )
self.assertIn( self.assertIn(
"scheduling.public-self-enrollment", "scheduling.public-self-enrollment",
documentation["scheduling.public-self-enrollment"].metadata["help_contexts"], documentation["scheduling.public-self-enrollment"].metadata[
"help_contexts"
],
) )
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/scheduling-webui", "name": "@govoplan/scheduling-webui",
"version": "0.1.18", "version": "0.1.20",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -60,7 +60,7 @@ const schedulingDashboardWidgets: DashboardWidgetsUiCapability = {
export const schedulingModule: PlatformWebModule = { export const schedulingModule: PlatformWebModule = {
id: "scheduling", id: "scheduling",
label: "Scheduling", label: "Scheduling",
version: "0.1.18", version: "0.1.20",
dependencies: ["poll"], dependencies: ["poll"],
optionalDependencies: ["access", "calendar", "mail", "notifications", "workflow", "appointments", "addresses"], optionalDependencies: ["access", "calendar", "mail", "notifications", "workflow", "appointments", "addresses"],
translations: generatedTranslations, translations: generatedTranslations,