259 lines
8.3 KiB
Python
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",
|
|
]
|