From 5de54ccd2e896cd1daf634147190dab7c0eb666e Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 19 Aug 2026 21:54:54 +0200 Subject: [PATCH] feat(templates): persist reusable content requirements --- src/govoplan_templates/backend/capabilities.py | 16 +++++++++++++++- src/govoplan_templates/backend/manifest.py | 6 ++++-- tests/test_templates.py | 11 +++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/govoplan_templates/backend/capabilities.py b/src/govoplan_templates/backend/capabilities.py index 1cab570..e2aa2a7 100644 --- a/src/govoplan_templates/backend/capabilities.py +++ b/src/govoplan_templates/backend/capabilities.py @@ -146,7 +146,16 @@ class SqlTemplateContentLibrary(TemplateContentLibraryProvider): template_type=request.template_type, usages=list(request.usages), locale=request.locale, - required_fields=[], + 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, @@ -168,6 +177,11 @@ class SqlTemplateContentLibrary(TemplateContentLibraryProvider): "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, diff --git a/src/govoplan_templates/backend/manifest.py b/src/govoplan_templates/backend/manifest.py index f9743a1..dbad2d6 100644 --- a/src/govoplan_templates/backend/manifest.py +++ b/src/govoplan_templates/backend/manifest.py @@ -104,7 +104,8 @@ DOCUMENTATION = ( body=( "Content fragments retain text and HTML variants, usage constraints, locale, scope, revision, and publication state in Templates. " "Campaign can load a fragment or complete email part through the optional content-library capability. Saving from Campaign creates " - "a draft and never publishes it automatically. Inserting content changes only the current Campaign draft; existing Campaign versions " + "a draft with its declared required-field contract and never publishes it automatically. Consumers show missing required fields before " + "applying content. Inserting content changes only the current Campaign draft; existing Campaign versions " "and Template revisions remain unchanged." ), layer="available", @@ -118,7 +119,8 @@ DOCUMENTATION = ( "body": ( "Inhaltsbausteine speichern Text- und HTML-Fassungen, Verwendungszwecke, Sprache, Geltungsbereich, Revision und " "Veröffentlichungsstatus in Templates. Campaign kann Bausteine oder vollständige E-Mail-Teile über die optionale " - "Inhaltsbibliothek laden. Das Speichern aus Campaign legt einen Entwurf an und veröffentlicht ihn niemals automatisch. " + "Inhaltsbibliothek laden. Das Speichern aus Campaign legt einen Entwurf mit dem deklarierten Pflichtfeldvertrag an und " + "veröffentlicht ihn niemals automatisch. Fehlende Pflichtfelder werden vor dem Anwenden angezeigt. " "Das Einfügen ändert nur den aktuellen Kampagnenentwurf; bestehende Kampagnenversionen und Template-Revisionen bleiben unverändert." ), } diff --git a/tests/test_templates.py b/tests/test_templates.py index 4c4da81..9fd8e3a 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -13,6 +13,7 @@ from govoplan_core.core.templates import ( CAPABILITY_TEMPLATE_RENDERER, TemplateCompatibilityError, TemplateContentDraftRequest, + TemplateFieldRequirement, TemplateRenderError, TemplateRenderRequest, ) @@ -182,6 +183,12 @@ class TemplateServiceTests(unittest.TestCase): usages=("campaign.content",), locale="de", content_text="Mit freundlichen Grüßen", + required_fields=( + TemplateFieldRequirement( + path="local.display_name", + label="Display name", + ), + ), metadata={"campaign_targets": ["text"]}, ), ) @@ -190,6 +197,10 @@ class TemplateServiceTests(unittest.TestCase): self.assertEqual("draft", result.status) self.assertEqual("content_fragment", result.template_type) self.assertEqual("Mit freundlichen Grüßen", result.revision.content_text) + self.assertEqual( + "local.display_name", + result.revision.required_fields[0].path, + ) self.assertEqual(["text"], result.revision.metadata["campaign_targets"]) self.assertEqual( "templates.content_library",