from __future__ import annotations from pathlib import Path from govoplan_core.core.files import CAPABILITY_FILES_ARTIFACT_STORE from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.templates import ( CAPABILITY_TEMPLATE_CATALOG, CAPABILITY_TEMPLATE_RENDERER, ) from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_templates.backend.db import models as template_models MODULE_ID = "templates" MODULE_NAME = "Templates" MODULE_VERSION = "0.1.16" READ_SCOPE = "templates:template:read" WRITE_SCOPE = "templates:template:write" PUBLISH_SCOPE = "templates:template:publish" RENDER_SCOPE = "templates:template:render" ADMIN_SCOPE = "templates:template: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=MODULE_NAME, level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission(READ_SCOPE, "View templates", "Read template definitions, revisions, and render evidence."), _permission(WRITE_SCOPE, "Manage templates", "Create and revise reusable templates."), _permission(PUBLISH_SCOPE, "Publish templates", "Publish immutable template revisions for final output."), _permission(RENDER_SCOPE, "Render templates", "Preview and render governed output from supplied snapshots."), _permission(ADMIN_SCOPE, "Administer templates", "Manage all tenant, group, and user templates."), ) ROLE_TEMPLATES = ( RoleTemplate( slug="template_manager", name="Template manager", description="Create, publish, and render reusable typed templates.", permissions=(READ_SCOPE, WRITE_SCOPE, PUBLISH_SCOPE, RENDER_SCOPE), ), ) DOCUMENTATION = ( DocumentationTopic( id="templates.library", title="Template library", summary="Create versioned templates with explicit usages and required data fields.", body=( "Templates are reusable, scoped definitions. Every edit creates an immutable revision. " "Publish the revision that consumers may use for final output. A compatibility check explains " "missing fields, unsupported usages, and unavailable output formats before rendering." ), layer="available", documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), metadata={ "seed": True, "help_contexts": [ "templates.page", "templates.library", "templates.editor", "templates.state.read-only", ], }, ), DocumentationTopic( id="templates.printable-output", title="Printable template output", summary="Render labels, envelopes, letters, and list layouts from frozen input snapshots.", body=( "Preview output may use a draft revision. Final output requires a published revision and an " "idempotency key. Results pin the template hash, input hash, renderer version, item/page counts, " "diagnostics, and output digest. Files stores artifacts when available and authorized; otherwise " "Templates provides a bounded download. Browser printing is the supported baseline output path." ), layer="available", documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), related_modules=("files", "dist_lists", "campaigns", "audit"), metadata={ "seed": True, "help_contexts": [ "templates.preview", "templates.action.validate-preview", "templates.action.render-final", "templates.evidence.render", ], }, ), DocumentationTopic( id="templates.reference.fields-and-consequences", title="Template fields and lifecycle consequences", summary="Scope, usage, data contract, publication, rendering, and deletion semantics for reusable Templates.", body=( "Visibility determines which tenant, group, or user scope may discover the Template; inherited Templates may be read-only. " "Usages are capability contexts that constrain where a Template may be selected. Required fields form the compatibility " "contract checked against supplied data before rendering. Saving creates a new immutable revision. Publishing marks one " "revision as available for final output without rewriting older revisions or evidence. Preview validates and renders bounded " "sample output; final rendering requires the published revision and records template, input, and output hashes plus renderer " "evidence. Files may retain the artifact when its optional capability is available. Deletion removes the Template from future " "selection but does not rewrite retained render evidence." ), layer="available", documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), related_modules=("files", "dist_lists", "campaigns", "audit", "policy"), metadata={ "seed": True, "help_contexts": [ "templates.field.type", "templates.field.locale", "templates.field.visibility", "templates.field.usages", "templates.field.required-data", "templates.action.publish", "templates.action.delete", ], "consequence_classes": { "save_revision": "Creates a new immutable Template revision.", "publish_revision": "Makes the selected immutable revision eligible for final consumer output.", "render_final": "Creates retained render evidence and may persist an artifact through Files.", "delete_template": "Prevents future selection without rewriting retained revisions or render evidence.", }, }, ), ) def _router(_context: ModuleContext): from govoplan_templates.backend.router import router return router def _catalog(context: ModuleContext): from govoplan_templates.backend.capabilities import catalog_capability return catalog_capability(context) def _renderer(context: ModuleContext): from govoplan_templates.backend.capabilities import renderer_capability return renderer_capability(context) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: return { "templates": session.query(template_models.TemplateDefinition).filter( template_models.TemplateDefinition.tenant_id == tenant_id, template_models.TemplateDefinition.deleted_at.is_(None), ).count(), "template_renders": session.query(template_models.TemplateRender).filter( template_models.TemplateRender.tenant_id == tenant_id, ).count(), } manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, dependencies=(), optional_dependencies=("files", "dist_lists", "campaigns", "audit"), optional_capabilities=(CAPABILITY_FILES_ARTIFACT_STORE,), provides_interfaces=( ModuleInterfaceProvider(name=CAPABILITY_TEMPLATE_CATALOG, version=MODULE_VERSION), ModuleInterfaceProvider(name=CAPABILITY_TEMPLATE_RENDERER, version=MODULE_VERSION), ), requires_interfaces=( ModuleInterfaceRequirement( name=CAPABILITY_FILES_ARTIFACT_STORE, version_min="0.1.14", version_max_exclusive="0.2.0", optional=True, ), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, nav_items=( NavItem( path="/templates", label=MODULE_NAME, icon="layout-template", required_any=(READ_SCOPE, WRITE_SCOPE, PUBLISH_SCOPE, RENDER_SCOPE, ADMIN_SCOPE), order=75, ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/templates-webui", routes=( FrontendRoute( path="/templates", component="TemplatesPage", required_any=(READ_SCOPE, WRITE_SCOPE, PUBLISH_SCOPE, RENDER_SCOPE, ADMIN_SCOPE), order=75, ), ), nav_items=( NavItem( path="/templates", label=MODULE_NAME, icon="layout-template", required_any=(READ_SCOPE, WRITE_SCOPE, PUBLISH_SCOPE, RENDER_SCOPE, ADMIN_SCOPE), order=75, ), ), view_surfaces=( ViewSurface(id="templates.page", module_id=MODULE_ID, kind="route", label="Templates", order=75), ViewSurface(id="templates.library", module_id=MODULE_ID, kind="section", label="Template library", order=10), ViewSurface(id="templates.editor", module_id=MODULE_ID, kind="section", label="Template editor", order=20), ViewSurface(id="templates.preview", module_id=MODULE_ID, kind="section", label="Template preview and output", order=30), ), ), route_factory=_router, capability_factories={ CAPABILITY_TEMPLATE_CATALOG: _catalog, CAPABILITY_TEMPLATE_RENDERER: _renderer, }, 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( template_models.TemplateRender, template_models.TemplateRevision, template_models.TemplateDefinition, label=MODULE_NAME, ), retirement_notes=( "Destructive retirement removes template definitions, immutable revisions, bounded outputs, " "and render evidence after the installer captures a database snapshot." ), ), uninstall_guard_providers=( persistent_table_uninstall_guard( template_models.TemplateDefinition, template_models.TemplateRevision, template_models.TemplateRender, label=MODULE_NAME, ), ), documentation=DOCUMENTATION, architecture=declared_module_architecture( layer="content_records_evidence", kind="domain", maturity="vertical_slice", documentation_ref="docs/TEMPLATE_BOUNDARY.md", test_ref="tests/test_templates.py", known_limits=( "The baseline emits safe deterministic HTML/text for browser or OS printing; PDF and printer delivery remain connector concerns.", ), supported_authority_modes=("native_authoritative",), owned_concepts=("template definition", "template revision", "template render evidence"), non_owned_concepts=("recipient", "campaign", "file asset", "printer endpoint"), recovery_docs=("docs/TEMPLATE_BOUNDARY.md",), security_docs=("docs/TEMPLATE_BOUNDARY.md",), operations_docs=("README.md",), ), ) def get_manifest() -> ModuleManifest: return manifest