feat: add reusable content fragments
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user