feat: add reusable content fragments

This commit is contained in:
2026-08-07 14:53:34 +02:00
parent 6f8a70f864
commit 21ed6270a3
7 changed files with 192 additions and 2 deletions
@@ -4,17 +4,30 @@ from collections.abc import Mapping, Sequence
from sqlalchemy.orm import Session
from govoplan_core.audit.logging import audit_from_principal
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.events import (
EventActorRef,
EventObjectRef,
EventTenantRef,
PlatformEvent,
emit_platform_event,
)
from govoplan_core.core.modules import ModuleContext
from govoplan_core.core.templates import (
TemplateCatalogProvider,
TemplateCompatibility,
TemplateContentDraftRequest,
TemplateContentLibraryProvider,
TemplateRef,
)
from govoplan_templates.backend.rendering import SqlTemplateRenderer
from govoplan_templates.backend.schemas import TemplateCreateRequest
from govoplan_templates.backend.service import (
READ_SCOPE,
WRITE_SCOPE,
compatibility,
create_template,
get_template,
get_template_revision,
list_templates,
@@ -108,6 +121,71 @@ class SqlTemplateCatalog(TemplateCatalogProvider):
)
class SqlTemplateContentLibrary(TemplateContentLibraryProvider):
def create_content_draft(
self,
session: object,
principal: object,
*,
request: TemplateContentDraftRequest,
) -> TemplateRef:
sql_session, api_principal = _context(session, principal)
_require_write(api_principal)
if request.template_type not in {"content_fragment", "email", "generic"}:
raise ValueError(
"Reusable content drafts must be a content fragment, email, or generic template."
)
item, revision = create_template(
sql_session,
api_principal,
TemplateCreateRequest(
name=request.name,
description=request.description,
scope_type=request.scope_type,
scope_id=request.scope_id,
template_type=request.template_type,
usages=list(request.usages),
locale=request.locale,
required_fields=[],
output_profiles=[],
content_text=request.content_text,
content_html=request.content_html,
layout={},
metadata={
**dict(request.metadata),
"created_through": "templates.content_library",
},
),
)
audit_from_principal(
sql_session,
api_principal,
action="templates.template.created",
object_type="template",
object_id=item.id,
details={
"revision": revision.revision,
"definition_hash": revision.definition_hash,
"template_type": revision.template_type,
"usages": list(revision.usages or []),
"source": "content_library_capability",
},
commit=False,
)
emit_platform_event(
sql_session,
PlatformEvent(
type="templates.template.created.v1",
module_id="templates",
actor=EventActorRef(type="account", id=api_principal.account_id),
tenant=EventTenantRef(id=api_principal.tenant_id),
resource=EventObjectRef(type="template", id=item.id),
classification="internal",
),
)
return template_ref(item, revision, read_only=False)
def catalog_capability(_context: ModuleContext) -> SqlTemplateCatalog:
return SqlTemplateCatalog()
@@ -116,6 +194,10 @@ def renderer_capability(context: ModuleContext) -> SqlTemplateRenderer:
return SqlTemplateRenderer(context.registry)
def content_library_capability(_context: ModuleContext) -> SqlTemplateContentLibrary:
return SqlTemplateContentLibrary()
def _context(session: object, principal: object) -> tuple[Session, ApiPrincipal]:
if not isinstance(session, Session):
raise TypeError("Template catalogue access requires a SQLAlchemy session.")
@@ -137,6 +219,14 @@ def _require_read(principal: ApiPrincipal) -> None:
raise PermissionError(f"Template catalogue access requires {READ_SCOPE}.")
def _require_write(principal: ApiPrincipal) -> None:
if not any(
principal.has(scope)
for scope in (WRITE_SCOPE, "templates:template:admin")
):
raise PermissionError(f"Template draft creation requires {WRITE_SCOPE}.")
def _read_only(principal: ApiPrincipal, scope_type: str, scope_id: str | None) -> bool:
if principal.has("templates:template:admin") or scope_type == "tenant":
return False
@@ -147,6 +237,8 @@ def _read_only(principal: ApiPrincipal, scope_type: str, scope_id: str | None) -
__all__ = [
"SqlTemplateCatalog",
"SqlTemplateContentLibrary",
"catalog_capability",
"content_library_capability",
"renderer_capability",
]