Files
govoplan-templates/src/govoplan_templates/backend/capabilities.py
T

259 lines
8.3 KiB
Python

from __future__ import annotations
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,
template_ref,
)
class SqlTemplateCatalog(TemplateCatalogProvider):
def list_templates(
self,
session: object,
principal: object,
*,
query: str = "",
usage: str | None = None,
template_type: str | None = None,
locale: str | None = None,
limit: int = 100,
) -> Sequence[TemplateRef]:
sql_session, api_principal = _context(session, principal)
_require_read(api_principal)
rows = list_templates(
sql_session,
api_principal,
query=query,
usage=usage,
template_type=template_type,
locale=locale,
limit=limit,
)
return tuple(
template_ref(
row,
get_template_revision(sql_session, row, published_preferred=True),
read_only=_read_only(api_principal, row.scope_type, row.scope_id),
)
for row in rows
)
def get_template(
self,
session: object,
principal: object,
*,
template_id: str,
revision: int | None = None,
) -> TemplateRef | None:
sql_session, api_principal = _context(session, principal)
_require_read(api_principal)
try:
row = get_template(sql_session, api_principal, template_id)
item_revision = get_template_revision(
sql_session,
row,
revision=revision,
published_preferred=revision is None,
)
except ValueError:
return None
return template_ref(
row,
item_revision,
read_only=_read_only(api_principal, row.scope_type, row.scope_id),
)
def check_compatibility(
self,
session: object,
principal: object,
*,
template_id: str,
revision: int | None = None,
usage: str | None = None,
output_format: str | None = None,
available_fields: Mapping[str, str] | Sequence[str] = (),
) -> TemplateCompatibility:
sql_session, api_principal = _context(session, principal)
_require_read(api_principal)
row = get_template(sql_session, api_principal, template_id)
item_revision = get_template_revision(
sql_session,
row,
revision=revision,
published_preferred=revision is None,
)
return compatibility(
item_revision,
usage=usage,
output_format=output_format,
available_fields=available_fields,
)
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=[
{
"path": field.path,
"value_type": field.value_type,
"label": field.label,
"required": field.required,
"description": field.description,
}
for field in request.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 []),
"required_fields": [
str(field.get("path") or "")
for field in revision.required_fields or []
if field.get("path")
],
"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()
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.")
if not isinstance(principal, ApiPrincipal):
raise TypeError("Template catalogue access requires an API principal.")
return session, principal
def _require_read(principal: ApiPrincipal) -> None:
if not any(
principal.has(scope)
for scope in (
READ_SCOPE,
"templates:template:write",
"templates:template:publish",
"templates:template:admin",
)
):
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
if scope_type == "user":
return scope_id != principal.account_id
return scope_id not in principal.group_ids
__all__ = [
"SqlTemplateCatalog",
"SqlTemplateContentLibrary",
"catalog_capability",
"content_library_capability",
"renderer_capability",
]