Implement governed hybrid campaign delivery

This commit is contained in:
2026-08-02 13:58:37 +02:00
parent b38597f2be
commit 7733265cc8
40 changed files with 2531 additions and 122 deletions
@@ -28,6 +28,16 @@ from govoplan_core.core.postbox import (
PostboxEvidenceProvider,
PostboxTargetRef,
)
from govoplan_core.core.templates import (
CAPABILITY_TEMPLATE_CATALOG,
CAPABILITY_TEMPLATE_RENDERER,
TemplateCatalogProvider,
TemplateCompatibility,
TemplateRef,
TemplateRenderRequest,
TemplateRenderResult,
TemplateRendererProvider,
)
from govoplan_campaign.backend.runtime import capability
@@ -37,6 +47,8 @@ POSTBOX_CAPABILITY = CAPABILITY_POSTBOX_DELIVERY
POSTBOX_DIRECTORY_CAPABILITY = CAPABILITY_POSTBOX_DIRECTORY
POSTBOX_EVIDENCE_CAPABILITY = CAPABILITY_POSTBOX_EVIDENCE
APPROVALS_CAPABILITY = CAPABILITY_APPROVAL_REQUESTS
TEMPLATE_CATALOG_CAPABILITY = CAPABILITY_TEMPLATE_CATALOG
TEMPLATE_RENDERER_CAPABILITY = CAPABILITY_TEMPLATE_RENDERER
class OptionalModuleUnavailable(RuntimeError):
@@ -89,6 +101,10 @@ class ApprovalGateUnavailable(OptionalModuleUnavailable):
pass
class TemplateOutputUnavailable(OptionalModuleUnavailable):
pass
class _PreparedCampaignSnapshot:
def __init__(self, directory: Path, path: Path, raw_json: dict[str, Any]) -> None:
self._directory = directory
@@ -493,6 +509,102 @@ class ApprovalCampaignIntegration:
)
class TemplatesCampaignIntegration:
def __init__(
self,
catalog_delegate: object | None = None,
renderer_delegate: object | None = None,
) -> None:
self._catalog = (
catalog_delegate
if isinstance(catalog_delegate, TemplateCatalogProvider)
else None
)
self._renderer = (
renderer_delegate
if isinstance(renderer_delegate, TemplateRendererProvider)
else None
)
@property
def available(self) -> bool:
return self._catalog is not None and self._renderer is not None
def list_templates(
self,
session: object,
principal: object,
*,
query: str = "",
limit: int = 100,
) -> tuple[TemplateRef, ...]:
if self._catalog is None:
return ()
return tuple(
self._catalog.list_templates(
session,
principal,
query=query,
usage="campaign_print",
limit=limit,
)
)
def check_compatibility(
self,
session: object,
principal: object,
*,
template_id: str,
revision: int | None,
output_format: str,
available_fields: dict[str, str] | tuple[str, ...],
) -> TemplateCompatibility:
if self._catalog is None:
raise TemplateOutputUnavailable(
"Printable output is unavailable because Templates is not active."
)
return self._catalog.check_compatibility(
session,
principal,
template_id=template_id,
revision=revision,
usage="campaign_print",
output_format=output_format,
available_fields=available_fields,
)
def get_template(
self,
session: object,
principal: object,
*,
template_id: str,
revision: int,
) -> TemplateRef | None:
if self._catalog is None:
return None
return self._catalog.get_template(
session,
principal,
template_id=template_id,
revision=revision,
)
def render(
self,
session: object,
principal: object,
*,
request: TemplateRenderRequest,
) -> TemplateRenderResult:
if self._renderer is None:
raise TemplateOutputUnavailable(
"Printable output is unavailable because Templates is not active."
)
return self._renderer.render(session, principal, request=request)
def files_integration() -> FilesCampaignIntegration:
return FilesCampaignIntegration(capability(FILES_CAPABILITY))
@@ -511,3 +623,10 @@ def postbox_integration() -> PostboxCampaignIntegration:
def approvals_integration() -> ApprovalCampaignIntegration:
return ApprovalCampaignIntegration(capability(APPROVALS_CAPABILITY))
def templates_integration() -> TemplatesCampaignIntegration:
return TemplatesCampaignIntegration(
capability(TEMPLATE_CATALOG_CAPABILITY),
capability(TEMPLATE_RENDERER_CAPABILITY),
)