from __future__ import annotations from govoplan_core.core.modules import with_documentation_structured_translations from govoplan_tasks.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.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( CapabilityDocumentation, DocumentationCondition, DocumentationLink, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleManifest, NavItem, PermissionDefinition, ProductAvailabilityExplanation, ProductAreaContribution, ProductSurfaceContribution, QuickAccessTool, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.tasks import ( CAPABILITY_TASK_COMMANDS, WorkItemProviderRegistration, ) from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_tasks.backend.db import models as task_models from govoplan_tasks.backend.dsar_provider import ( TASKS_DSAR_CAPABILITY, TasksDsarProvider, ) from govoplan_tasks.backend.service import SqlTaskService MODULE_ID = "tasks" MODULE_NAME = "Tasks" MODULE_VERSION = "0.1.22" READ_SCOPE = "tasks:item:read" WRITE_SCOPE = "tasks:item:write" ADMIN_SCOPE = "tasks:item:admin" 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="Tasks", level="tenant", module_id=module_id, resource=resource, action=action, ) def _router(context: ModuleContext): from govoplan_tasks.backend.router import create_router return create_router(context.registry) def _service(context: ModuleContext) -> SqlTaskService: return SqlTaskService(context.registry) def _dsar_provider(_context: ModuleContext) -> TasksDsarProvider: return TasksDsarProvider() def _tenant_summary(session, tenant_id: str) -> dict[str, int]: total = ( session.query(task_models.TaskItem) .filter(task_models.TaskItem.tenant_id == tenant_id) .count() ) open_items = ( session.query(task_models.TaskItem) .filter( task_models.TaskItem.tenant_id == tenant_id, task_models.TaskItem.status.in_( ("open", "in_progress", "deferred", "blocked") ), ) .count() ) return {"tasks": total, "open_tasks": open_items} PERMISSIONS = ( _permission( READ_SCOPE, "View assigned work", "Read explicit and contributed work visible to the current account, group, role, or function.", ), _permission( WRITE_SCOPE, "Manage assigned work", "Create explicit tasks and advance visible task state.", ), _permission( ADMIN_SCOPE, "Administer tenant work", "Read and recover all explicit tasks in the tenant.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="work_participant", name="Work participant", description="Read and advance assigned work and create explicit tasks.", permissions=(READ_SCOPE, WRITE_SCOPE), ), RoleTemplate( slug="work_supervisor", name="Work supervisor", description="Inspect and recover tenant-wide work in addition to participating.", permissions=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE), ), ) DOCUMENTATION = ( DocumentationTopic( id="tasks.data-subject-requests", title="Task data-subject requests", summary=( "Export account-assigned task data and lifecycle attribution while " "keeping shared institutional work under owner review." ), body=( "Tasks correlates exact account and actor identifiers only inside the " "active tenant. Account-assigned explicit Tasks contribute bounded " "title, summary, required action, lifecycle state, due dates, the exact " "matching account assignment, source references, and any create, update, " "or completion activities performed by the subject. Group, role, function, " "and anyone visibility is not inferred from external directories and other " "assignment targets are excluded. When the subject acted on a Task without " "being its direct account assignee, only minimized lifecycle attribution " "and source identity are exported. Provenance, arbitrary metadata, request " "hashes, idempotency keys, and source-module payloads are excluded; source " "references are never traversed. Task attribution is retained as immutable " "accountability evidence. Assignment or content erasure requires manual " "source-owner and retention review because a Task can be shared institutional " "work; the provider performs no automatic mutation." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "tenant_admin", "operator", "auditor"), related_modules=("core", "workflow_engine", "approvals", "notifications"), translations={ "de": { "title": "Betroffenenanfragen für Aufgaben", "summary": ( "Kontobezogene Aufgabendaten und Lebenszykluszuordnungen exportieren, " "während gemeinsam verantwortete institutionelle Arbeit der fachlichen Prüfung unterliegt." ), "body": ( "Tasks gleicht ausschließlich exakte Konto- und Akteurskennungen innerhalb des aktiven Mandanten ab. " "Direkt einem Konto zugewiesene Aufgaben tragen begrenzte Angaben zu Titel, Zusammenfassung, erforderlicher " "Handlung, Lebenszyklusstatus, Fristen, exakter Kontozuweisung, Quellverweisen sowie vom Betroffenen ausgeführten " "Erstellungs-, Änderungs- oder Abschlussaktivitäten bei. Sichtbarkeit für Gruppen, Rollen, Funktionen oder alle " "wird nicht aus externen Verzeichnissen abgeleitet; andere Zuweisungsziele bleiben ausgeschlossen. Hat die " "betroffene Person an einer Aufgabe gehandelt, ohne deren direkte Kontozuweisung zu sein, werden nur minimierte " "Lebenszykluszuordnung und Quellidentität exportiert. Herkunftsmetadaten, beliebige Metadaten, Anfrage-Hashes, " "Idempotenzschlüssel und Nutzdaten des Quellmoduls bleiben ausgeschlossen; Quellverweise werden niemals verfolgt. " "Aufgabenzuordnungen bleiben als unveränderlicher Verantwortungsnachweis erhalten. Die Löschung einer Zuweisung " "oder von Inhalten erfordert eine manuelle Prüfung durch Quellverantwortliche und Aufbewahrungsstelle, da eine " "Aufgabe gemeinsam verantwortete institutionelle Arbeit sein kann; der Anbieter nimmt keine automatische Änderung vor." ), } }, metadata={ "help_contexts": [ "tasks.page.inbox", "tasks.page.detail", "tasks.field.assignment", "privacy.data-subject-requests", ], "consequence_classes": { "export_assigned_task": ( "Returns bounded Task-owned data and exact account assignment only." ), "review_assignment_erasure": ( "Requires the Task source owner and retention authority before " "changing shared work." ), "retain_actor_attribution": ( "Preserves minimized Task lifecycle accountability evidence." ), }, }, ), DocumentationTopic( id="tasks.quick-access-and-product-area", title="Work in product navigation and Quick Access", summary="Keep assigned work available in the Work area and the optional right-side Quick Access rail.", body=( "Tasks contributes its authorized workspace to the stable Work destination at /work. The owner route " "/tasks remains available through All available tools and as a compatible deep link. When Quick Access is enabled, " "a bounded seven-item authorized inbox and detail can appear beside the current page. Explicit Tasks can be " "started or completed there; work from another provider exposes only that provider's launch path. Every load " "and command is rechecked by Tasks, and completion returns a typed work-item reference to the host. Views may " "hide or reorder the contribution, but neither presentation grants task access or copies completion state." ), layer="configured", documentation_types=("user", "admin"), audience=("user", "tenant_admin", "module_admin"), related_modules=("quick_access", "views"), translations={ "de": { "title": "Arbeit in Produktnavigation und Schnellzugriff", "summary": "Zugewiesene Arbeit im Produktbereich Arbeit und optional in der rechten Schnellzugriffsleiste verwenden.", "body": ( "Tasks ordnet den berechtigten Arbeitsbereich dem stabilen Produktziel Arbeit unter /work zu. Der Eigentümerpfad " "/tasks bleibt unter Alle verfügbaren Werkzeuge und als kompatibler Direktlink erreichbar. Ist der Schnellzugriff aktiviert, " "kann ein begrenzter, berechtigungsgeprüfter Arbeitsvorrat mit sieben Einträgen und Details neben der " "aktuellen Seite erscheinen. Explizite Tasks lassen sich dort beginnen oder abschließen; fremde Quellen " "behalten ihre eigenen Befehle und Startpfade. Jeder Aufruf wird erneut durch Tasks geprüft. Ansichten " "dürfen den Beitrag ausblenden oder ordnen, erteilen aber keine Aufgabenberechtigung." ), } }, metadata={"kind": "reference", "help_contexts": ["tasks.quick_access.work"]}, order=9, ), DocumentationTopic( id="tasks.work-inbox", title="Unified work inbox", summary="Resume explicit tasks and module-owned work requiring attention.", body=( "The Work inbox combines explicit Tasks with work contributed by enabled modules. " "Each source keeps ownership of its commands and completion state. Tasks does not turn a " "Workflow handoff, Postbox message, approval, or notification into a copied task. Filters, " "due dates, priorities, and source links help the current actor resume work safely." ), layer="configured", documentation_types=("user", "admin"), audience=("user", "operator", "tenant_admin", "module_admin"), related_modules=( "workflow_engine", "notifications", "postbox", "approvals", "views", "dashboard", ), conditions=( DocumentationCondition( required_modules=("tasks",), required_scopes=(READ_SCOPE,), ), ), links=( DocumentationLink( label="Tasks domain", href="govoplan-tasks/docs/TASKS_DOMAIN.md", kind="repository", ), ), translations={ "de": { "title": "Gemeinsamer Arbeitsvorrat", "summary": "Explizite Aufgaben und Arbeitsvorgänge anderer Module sicher fortsetzen.", "body": ( "Der Arbeitsvorrat verbindet explizite Aufgaben mit Arbeitsobjekten aktivierter Module. " "Jede Quelle behält die Verantwortung für Befehle und Abschlussstatus. Tasks kopiert " "keine Workflow-Übergabe, Postfachnachricht, Freigabe oder Benachrichtigung in einen " "zweiten Fachzustand. Filter, Fristen, Prioritäten und Quellverweise helfen beim sicheren Fortsetzen." ), } }, metadata={ "kind": "workflow", "help_contexts": [ "tasks.route.work", "tasks.page.inbox", "tasks.page.detail", "tasks.action.create", "tasks.action.advance", "tasks.field.assignment", "tasks.field.due-at", "tasks.field.priority", ] }, ), ) manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, dependencies=("access",), optional_dependencies=( "idm", "organizations", "workflow_engine", "workflow", "notifications", "postbox", "approvals", "views", "dashboard", "search", ), required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, ), provides_interfaces=( ModuleInterfaceProvider(name=CAPABILITY_TASK_COMMANDS, version="1.0.0"), ModuleInterfaceProvider(name="tasks.work_items", version="1.0.0"), ModuleInterfaceProvider(name=TASKS_DSAR_CAPABILITY, version="0.1.0"), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, route_factory=_router, nav_items=( NavItem( path="/tasks", label="Work", icon="list-checks", required_any=(READ_SCOPE,), order=21, surface_id="tasks.route.work", ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/tasks-webui", routes=( FrontendRoute( path="/tasks", component="TasksPage", required_any=(READ_SCOPE,), order=21, surface_id="tasks.route.work", ), ), view_surfaces=( ViewSurface( id="tasks.page.inbox", module_id=MODULE_ID, kind="section", label="Work inbox", parent_id="tasks.route.work", order=20, ), ViewSurface( id="tasks.page.detail", module_id=MODULE_ID, kind="section", label="Work details", parent_id="tasks.route.work", order=30, ), ViewSurface( id="tasks.action.create", module_id=MODULE_ID, kind="action", label="Create task", parent_id="tasks.page.inbox", order=40, ), ViewSurface( id="tasks.action.advance", module_id=MODULE_ID, kind="action", label="Advance task", parent_id="tasks.page.detail", order=50, ), ViewSurface( id="tasks.widget.open-work", module_id=MODULE_ID, kind="section", label="Open work widget", order=60, ), ViewSurface( id="tasks.quick_access.work", module_id=MODULE_ID, kind="quick_access", label="Work Quick Access", order=70, ), ), product_areas=( ProductAreaContribution( id="work", module_id=MODULE_ID, label="i18n:govoplan-core.product_area.work", icon="list-checks", description="i18n:govoplan-core.product_area.work_description", surface_ids=("tasks.route.work",), order=10, ), ), product_surfaces=( ProductSurfaceContribution( id="work.items", module_id=MODULE_ID, label="i18n:govoplan-core.product_surface.work", description="i18n:govoplan-core.product_surface.work_description", icon="list-checks", entry_path="/work", route_path="/tasks", surface_ids=("tasks.route.work",), presentations=("task", "reader"), help_context_ids=("tasks.route.work",), documentation_topic_ids=("tasks.quick-access-and-product-area",), required_any=(READ_SCOPE,), order=10, unavailable=ProductAvailabilityExplanation( reason="authorization", title="i18n:govoplan-core.product_surface.unavailable", description="i18n:govoplan-core.product_surface.unavailable_description", resolution="i18n:govoplan-core.product_surface.unavailable_resolution", responsible_role="i18n:govoplan-core.access_administrator", ), ), ), quick_access_tools=( QuickAccessTool( id="tasks.work", module_id=MODULE_ID, category_id="work", label="i18n:govoplan-tasks.work", description="i18n:govoplan-tasks.quick_access_description", surface_id="tasks.quick_access.work", icon="list-checks", full_page_path="/tasks", required_any=(READ_SCOPE,), order=10, modes=("browse", "resume"), returned_reference_kinds=("tasks.work-item",), help_context_id="tasks.quick_access.work", ), ), ), tenant_summary_providers=(_tenant_summary,), capability_factories={ CAPABILITY_TASK_COMMANDS: _service, TASKS_DSAR_CAPABILITY: _dsar_provider, }, capability_documentation={ CAPABILITY_TASK_COMMANDS: CapabilityDocumentation( label="Task commands", summary="Creates replay-safe explicit tasks without importing the Tasks implementation.", contract_version="1.0.0", ), TASKS_DSAR_CAPABILITY: CapabilityDocumentation( label="Tasks data-subject request provider", summary=( "Exports account-assigned Tasks and minimized actor attribution " "with governed non-executable erasure outcomes." ), contract_version="0.1.0", ), }, work_item_providers=( WorkItemProviderRegistration(id="tasks.explicit", factory=_service, order=10), ), 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( task_models.TaskAssignment, task_models.TaskItem, label="Tasks" ), retirement_notes="Destructive retirement removes explicit task state after a database snapshot; contributed work remains with its owner.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( task_models.TaskItem, task_models.TaskAssignment, label="Tasks" ), ), documentation=DOCUMENTATION, architecture=declared_module_architecture( layer="human_work_procedure", kind="domain", maturity="vertical_slice", documentation_ref="docs/TASKS_DOMAIN.md", test_ref="tests/test_tasks.py", known_limits=( "Function assignment resolution depends on the optional IDM directory; source-owned inline commands remain deep links in this first slice.", ), supported_authority_modes=("native_authoritative", "linked_reference"), owned_concepts=("explicit task", "task assignment", "unified work inbox"), non_owned_concepts=( "workflow instance", "notification", "postbox message", "approval request", "domain object", ), reference_packages=( "product.service-to-decision", "product.governed-communication", "product.governed-data-assurance", ), migration_docs=("docs/TASKS_DOMAIN.md",), recovery_docs=("docs/TASKS_DOMAIN.md",), security_docs=("docs/TASKS_DOMAIN.md",), operations_docs=("docs/TASKS_DOMAIN.md",), ), ) manifest = with_documentation_structured_translations( manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS ) def get_manifest() -> ModuleManifest: return manifest __all__ = [ "ADMIN_SCOPE", "MODULE_ID", "MODULE_VERSION", "READ_SCOPE", "WRITE_SCOPE", "get_manifest", "manifest", ]