5 Commits
Author SHA1 Message Date
zemion 0a7bd5b3b4 fix(ui): align contextual documentation with headings
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:46 +02:00
zemion 00bec0ed94 fix(packaging): expose immutable WebUI Git package for v0.1.23
Module Package Release / publish-packages (push) Successful in 11s
2026-09-08 02:06:09 +02:00
zemion cfcd723496 docs: complete German structured documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 01:15:34 +02:00
zemion 06afa79795 docs(forms): complete German reference coverage
Module Package Release / publish-packages (push) Successful in 12s
2026-08-23 20:27:25 +02:00
zemion 727c70f756 Release Forms v0.1.20 configuration package provider
Module Package Release / publish-packages (push) Successful in 12s
2026-08-22 18:04:34 +02:00
16 changed files with 840 additions and 12 deletions
+18
View File
@@ -39,3 +39,21 @@ PYTHONPATH=src:/mnt/DATA/git/govoplan-core/src \
```
See [docs/FORMS_BOUNDARY.md](docs/FORMS_BOUNDARY.md) for the boundary decision.
## Git-source WebUI package
The repository root exposes `@govoplan/forms-webui` for Git-tagged release
dependencies. It mirrors the owning `webui/package.json` version, public
TypeScript/CSS exports and peer requirements, with entry paths under
`webui/src`. Consumers provide the shared Core/React peers; the facade runs no
development or install scripts. The source archive contains `webui/src`, this
README and any repository license file. Run module development checks from `webui/`; Python
installation remains governed by `pyproject.toml`.
Das Repository stellt `@govoplan/forms-webui` am Wurzelpfad für versionierte
Git-Abhängigkeiten bereit. Version, öffentliche TypeScript-/CSS-Exporte und
Peer-Anforderungen entsprechen `webui/package.json`; die Einstiegspfade liegen
unter `webui/src`. Gemeinsame Core-/React-Peers stellt die einbindende Anwendung
bereit. Die Fassade führt keine Entwicklungs- oder Installationsskripte aus.
Entwicklungsprüfungen bleiben in `webui/`, die Python-Installation weiterhin in
`pyproject.toml` definiert.
+4
View File
@@ -70,6 +70,10 @@ Form definitions should carry:
option, constraint, draft, attachment, signature, policy, and handoff editing.
- Forms Runtime performs value validation against the resolved definition; the
definition owner does not persist submissions.
- Forms consumes Core's title-help and text-help layout contracts for its
catalogue and revision dialog. Workflow books sit beside their heading;
semantic documentation stays beside the Form meaning label for the exact
definition. The placement contract does not move content ownership to Core.
## Recovery And Operations
+6
View File
@@ -36,3 +36,9 @@ shared section-header toolbar surface. Core therefore owns viewport, search,
state, heading, and narrow-layout geometry. These contracts do not change
filter scope, permissions, revision semantics, or publication consequences.
English and German catalogues cover the owned route and editor vocabulary.
Documentation books belong immediately beside their contextual text. The
catalogue uses Core's workspace title and `titleHelp`; the revision dialog uses
its title help, and the separate Form meaning label carries semantic help for
that exact definition. Field help stays beside its label. This uses Core layout
contracts and does not change revision, publication, or runtime ownership.
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@govoplan/forms-webui",
"version": "0.1.23",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
"module": "webui/src/index.ts",
"types": "webui/src/index.ts",
"exports": {
".": {
"types": "./webui/src/index.ts",
"import": "./webui/src/index.ts"
},
"./styles/forms.css": "./webui/src/styles/forms.css"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
},
"files": [
"webui/src",
"README.md",
"LICENSE"
]
}
+2 -2
View File
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-forms"
version = "0.1.19"
version = "0.1.23"
description = "Immutable reusable form definitions for GovOPlaN."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = ["govoplan-core>=0.1.18", "govoplan-access>=0.1.18"]
dependencies = ["govoplan-core>=0.1.35", "govoplan-access>=0.1.18"]
[tool.setuptools.packages.find]
where = ["src"]
+1 -1
View File
@@ -1,3 +1,3 @@
"""GovOPlaN Forms module."""
__version__ = "0.1.18"
__version__ = "0.1.23"
@@ -0,0 +1,359 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
from govoplan_core.core.configuration_packages import (
ConfigurationApplyResult,
ConfigurationDiagnostic,
ConfigurationExportResult,
ConfigurationExportSelection,
ConfigurationPackageFragment,
ConfigurationPlanItem,
ConfigurationPreflightContext,
ConfigurationPreflightResult,
ConfigurationProvider,
ConfigurationProviderDescription,
)
from govoplan_core.db.session import get_database
from govoplan_forms.backend.service import (
FormDefinitionStoreError,
assess_form_definition_fragment,
export_form_definition_fragment,
get_form_definition,
import_form_definition_fragment,
list_form_definitions,
)
FORMS_CONFIGURATION_CAPABILITY = "forms.configuration"
_WRITE_SCOPES = frozenset(
{
"forms:definition:write",
"admin:settings:write",
"system:settings:write",
"system:governance:write",
}
)
@dataclass(frozen=True, slots=True)
class _ConfigurationPrincipal:
tenant_id: str
account_id: str | None = None
class SqlFormsConfigurationProvider(ConfigurationProvider):
module_id = "forms"
def describe(self) -> ConfigurationProviderDescription:
return ConfigurationProviderDescription(
module_id=self.module_id,
fragment_types=("definition",),
schema_refs={
"definition": "govoplan/forms/configuration/definition.v1",
},
exported_scopes=("tenant",),
)
def preflight(
self,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> ConfigurationPreflightResult:
with get_database().session() as session:
return _preflight_definition(session, fragment, context)
def apply(
self,
fragment: ConfigurationPackageFragment,
supplied_data: Mapping[str, Any],
context: ConfigurationPreflightContext,
) -> ConfigurationApplyResult:
del supplied_data
with get_database().session() as session:
result = _apply_definition(session, fragment, context)
if not any(item.severity == "blocker" for item in result.diagnostics):
session.commit()
return result
def export(
self,
selection: ConfigurationExportSelection,
context: ConfigurationPreflightContext,
) -> ConfigurationExportResult:
tenant_id = selection.tenant_id or context.tenant_id
if not tenant_id:
return ConfigurationExportResult(diagnostics=(_tenant_required(),))
principal = _ConfigurationPrincipal(
tenant_id=tenant_id,
account_id=context.operator_user_id,
)
selected_ids = {
item.removeprefix("form:")
for item in selection.object_refs
if item.startswith("form:")
}
with get_database().session() as session:
definitions, _total = list_form_definitions(
session,
principal,
limit=200,
)
fragments = tuple(
ConfigurationPackageFragment(
module_id=self.module_id,
fragment_type="definition",
fragment_id=definition.reference.object_id,
payload={
"fragment": export_form_definition_fragment(
definition,
exported_at=datetime.now(UTC),
exported_by=context.operator_user_id,
),
"on_conflict": "new_revision",
},
)
for definition in definitions
if not selected_ids
or definition.reference.object_id in selected_ids
)
return ConfigurationExportResult(fragments=fragments)
def health(
self,
import_result: ConfigurationApplyResult,
context: ConfigurationPreflightContext,
) -> tuple[ConfigurationDiagnostic, ...]:
del context
return tuple(
item for item in import_result.diagnostics if item.severity == "blocker"
)
def _preflight_definition(
session: Any,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> ConfigurationPreflightResult:
if fragment.fragment_type != "definition":
return ConfigurationPreflightResult(diagnostics=(_unsupported(fragment),))
if not context.tenant_id:
return ConfigurationPreflightResult(
diagnostics=(_tenant_required(fragment),),
plan=(_plan("blocked", fragment, "Select a target tenant."),),
)
if not (_WRITE_SCOPES & context.operator_scopes):
return ConfigurationPreflightResult(
diagnostics=(_write_scope_required(fragment),),
plan=(_plan("blocked", fragment, "Forms write authority is missing."),),
)
principal = _ConfigurationPrincipal(
tenant_id=context.tenant_id,
account_id=context.operator_user_id,
)
try:
parsed = _definition_payload(fragment)
assessment = assess_form_definition_fragment(
session,
principal,
fragment=parsed["fragment"],
)
except (FormDefinitionStoreError, ValueError) as exc:
return ConfigurationPreflightResult(
diagnostics=(_invalid(fragment, str(exc)),),
plan=(_plan("blocked", fragment, "Forms fragment is invalid."),),
)
target_form_id = parsed["target_form_id"] or str(
assessment["source"]["form_id"]
)
current = get_form_definition(
session,
principal,
form_id=target_form_id,
)
if current is not None and _is_replay(current.metadata, parsed["fragment"]):
return ConfigurationPreflightResult(
plan=(_plan("noop", fragment, "The same source definition is already imported."),)
)
if current is not None and parsed["on_conflict"] != "new_revision":
return ConfigurationPreflightResult(
diagnostics=(_conflict(fragment, target_form_id),),
plan=(_plan("blocked", fragment, "Existing definition is preserved by package policy."),),
)
return ConfigurationPreflightResult(
plan=(_plan(
"update" if current is not None else "create",
fragment,
f"{'Create a new revision of' if current is not None else 'Create'} Form definition {target_form_id} as a draft.",
),)
)
def _apply_definition(
session: Any,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> ConfigurationApplyResult:
preflight = _preflight_definition(session, fragment, context)
if any(item.severity == "blocker" for item in preflight.diagnostics):
return ConfigurationApplyResult(diagnostics=preflight.diagnostics)
parsed = _definition_payload(fragment)
assert context.tenant_id is not None
principal = _ConfigurationPrincipal(
tenant_id=context.tenant_id,
account_id=context.operator_user_id,
)
source = parsed["fragment"].get("provenance")
source_form_id = (
str(source.get("form_id") or "").strip()
if isinstance(source, Mapping)
else ""
)
target_form_id = parsed["target_form_id"] or source_form_id
current = get_form_definition(session, principal, form_id=target_form_id)
if current is not None and _is_replay(current.metadata, parsed["fragment"]):
return ConfigurationApplyResult()
imported = import_form_definition_fragment(
session,
principal,
fragment=parsed["fragment"],
target_form_id=target_form_id,
target_key=parsed["target_key"],
expected_revision=current.reference.version if current is not None else None,
change_reason=parsed["change_reason"],
recorded_at=datetime.now(UTC),
)
reference = f"form:{imported.reference.object_id}:{imported.reference.version}"
key = fragment.fragment_id or imported.reference.object_id
if current is None:
return ConfigurationApplyResult(created_refs={key: reference})
return ConfigurationApplyResult(updated_refs={key: reference})
def _definition_payload(fragment: ConfigurationPackageFragment) -> dict[str, Any]:
allowed = {
"fragment",
"target_form_id",
"target_key",
"change_reason",
"on_conflict",
}
unknown = sorted(set(fragment.payload) - allowed)
if unknown:
raise FormDefinitionStoreError(
f"Forms configuration payload contains unsupported fields: {', '.join(unknown)}."
)
source = fragment.payload.get("fragment")
if not isinstance(source, Mapping):
raise FormDefinitionStoreError(
"Forms configuration definition requires a fragment object."
)
on_conflict = str(fragment.payload.get("on_conflict") or "preserve").strip().casefold()
if on_conflict not in {"preserve", "new_revision"}:
raise FormDefinitionStoreError(
"Forms configuration on_conflict must be preserve or new_revision."
)
return {
"fragment": dict(source),
"target_form_id": _optional_text(fragment.payload.get("target_form_id")),
"target_key": _optional_text(fragment.payload.get("target_key")),
"change_reason": _optional_text(fragment.payload.get("change_reason"))
or "Imported through a reviewed configuration package.",
"on_conflict": on_conflict,
}
def _is_replay(metadata: Mapping[str, Any], source: Mapping[str, Any]) -> bool:
package_import = metadata.get("package_import")
if not isinstance(package_import, Mapping):
return False
return bool(source.get("definition_sha256")) and (
str(package_import.get("source_sha256") or "")
== str(source.get("definition_sha256") or "")
)
def _plan(
action: str,
fragment: ConfigurationPackageFragment,
summary: str,
) -> ConfigurationPlanItem:
return ConfigurationPlanItem(
action=action, # type: ignore[arg-type]
module_id=fragment.module_id,
fragment_type=fragment.fragment_type,
fragment_id=fragment.fragment_id,
summary=summary,
)
def _tenant_required(
fragment: ConfigurationPackageFragment | None = None,
) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="tenant_required",
message="Forms configuration import and export require a target tenant.",
module_id="forms",
object_ref=(fragment.fragment_id or fragment.fragment_type) if fragment else None,
resolution="Select a tenant before continuing.",
)
def _write_scope_required(fragment: ConfigurationPackageFragment) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="forms_configuration_write_scope_required",
message="The operator may not import Form definitions for this tenant.",
module_id="forms",
object_ref=fragment.fragment_id or fragment.fragment_type,
resolution="Use an approved Forms designer or system configuration administrator.",
)
def _conflict(fragment: ConfigurationPackageFragment, form_id: str) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="forms_configuration_conflict",
message=f"Form definition {form_id!r} already exists and preserve is selected.",
module_id="forms",
object_ref=form_id,
resolution="Keep the local definition, choose a different target id, or review a package that explicitly creates a new revision.",
)
def _invalid(fragment: ConfigurationPackageFragment, message: str) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="forms_configuration_payload_invalid",
message=message,
module_id="forms",
object_ref=fragment.fragment_id or fragment.fragment_type,
resolution="Use a Forms definition fragment compatible with the installed provider schema.",
)
def _unsupported(fragment: ConfigurationPackageFragment) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="fragment_type_unsupported",
message=f"Forms configuration does not support fragment type {fragment.fragment_type!r}.",
module_id="forms",
object_ref=fragment.fragment_id or fragment.fragment_type,
)
def _optional_text(value: object) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
__all__ = [
"FORMS_CONFIGURATION_CAPABILITY",
"SqlFormsConfigurationProvider",
]
@@ -0,0 +1,84 @@
"""German translations for public structured documentation metadata."""
from __future__ import annotations
from typing import Any
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'forms.data-subject-requests': {'consequence_classes': {'exclude_form_semantics': 'Gibt keinen '
'Schema-, Feld- '
'oder '
'Submission-Value-Inhalt '
'zurück.',
'export_definition_attribution': 'Returns '
'minimierten '
'die '
'unveränderliche '
'Revisionsaktivität.'}},
'forms.definitions': {'privacy_notes': ['Definitionskataloge enthalten Schemata und '
'Richtlinienreferenzen, keine eingereichten '
'Formularwerte.',
'Die Paketbewertung gewährt keinen Zugriff auf '
'referenzierte Laufzeiteinreichungen oder externe '
'Anbieter.',
'Veröffentlichte Zugänglichkeits- und '
'Lokalisierungsinhalte sind überall dort sichtbar, wo die '
'genaue Definition autorisiert ist.']},
'forms.reference.fields-and-consequences': {'consequence_classes': {'import_package': 'Erstellt '
'einen '
'lokalen '
'Entwurf '
'und behält '
'die '
'Herkunft '
'des Pakets '
'ohne '
'automatische '
'Veröffentlichung.',
'publish': 'Macht die genaue '
'Revision für '
'zukünftige '
'autorisierte '
'Instanzen '
'verfügbar.',
'retire': 'Stoppt die '
'zukünftige '
'Nutzung, während '
'Definitionen und '
'genaue '
'Laufzeitreferenzen '
'beibehalten '
'werden.',
'save_revision': 'Erstellt '
'eine '
'unveränderliche '
'Definitionsrevision '
'mit einem '
'Änderungsgrund.'},
'limitations': ['Der Paketimport veröffentlicht '
'niemals automatisch eine '
'Formularrevision.',
'Laufzeiteingaben und eingereichte '
'Werte werden niemals als '
'Definitionskonfiguration exportiert.',
'Generische Paket-Rollback erfordert '
'die Voranwendung Datenbank Snapshot '
'beibehalten.'],
'operational_consequences': ['Preserve blockiert eine '
'widersprüchliche lokale '
'Definition; new '
'revision muss explizit '
'überprüft werden.',
'Das erneute Anwenden '
'eines identischen '
'Quell-Digests ist '
'idempotent und erzeugt '
'keine Revision.',
'Importierte '
'Definitionen erfordern '
'eine normale '
'Überprüfung und '
'Veröffentlichung von '
'Formularen vor der '
'Verwendung zur '
'Laufzeit.']}}
+117 -2
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
from govoplan_core.core.modules import with_documentation_structured_translations
from govoplan_forms.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
from pathlib import Path
from govoplan_core.core.access import (
@@ -13,6 +16,7 @@ from govoplan_core.core.module_guards import (
)
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationCondition,
DocumentationLink,
DocumentationTopic,
FrontendModule,
@@ -33,6 +37,7 @@ from govoplan_core.core.semantic_documentation import (
)
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_forms.backend.configuration_provider import FORMS_CONFIGURATION_CAPABILITY
from govoplan_forms.backend.db import models as form_models
from govoplan_forms.backend.dsar_provider import (
FORMS_DSAR_CAPABILITY,
@@ -46,7 +51,7 @@ from govoplan_forms.backend.semantic_subjects import (
MODULE_ID = "forms"
MODULE_NAME = "Forms"
MODULE_VERSION = "0.1.19"
MODULE_VERSION = "0.1.23"
READ_SCOPE = "forms:definition:read"
WRITE_SCOPE = "forms:definition:write"
ADMIN_SCOPE = "forms:definition:admin"
@@ -95,6 +100,14 @@ def _semantic_subjects(
return FormsSemanticDocumentationSubjectProvider()
def _configuration_provider(_context: ModuleContext):
from govoplan_forms.backend.configuration_provider import (
SqlFormsConfigurationProvider,
)
return SqlFormsConfigurationProvider()
manifest = ModuleManifest(
id=MODULE_ID,
name=MODULE_NAME,
@@ -107,6 +120,7 @@ manifest = ModuleManifest(
),
provides_interfaces=(
ModuleInterfaceProvider(name="forms.definitions", version="0.1.0"),
ModuleInterfaceProvider(name=FORMS_CONFIGURATION_CAPABILITY, version="0.1.0"),
ModuleInterfaceProvider(name=FORMS_DSAR_CAPABILITY, version="0.1.0"),
ModuleInterfaceProvider(
name=SEMANTIC_SUBJECT_CAPABILITY,
@@ -204,6 +218,7 @@ manifest = ModuleManifest(
),
capability_factories={
CAPABILITY_FORM_DEFINITIONS: _definitions,
FORMS_CONFIGURATION_CAPABILITY: _configuration_provider,
FORMS_DSAR_CAPABILITY: _dsar_provider,
SEMANTIC_SUBJECT_CAPABILITY: _semantic_subjects,
},
@@ -213,6 +228,13 @@ manifest = ModuleManifest(
summary="Resolves exact tenant-bound form schemas without exposing Forms tables.",
contract_version="0.1.0",
),
FORMS_CONFIGURATION_CAPABILITY: CapabilityDocumentation(
label="Forms configuration-package provider",
summary="Preflights, imports, and exports immutable Form definition fragments as tenant-local drafts with source provenance.",
contract_version="0.1.0",
documentation_types=("admin",),
audience=("forms_designer", "system_admin", "operator"),
),
FORMS_DSAR_CAPABILITY: CapabilityDocumentation(
label="Forms data-subject request provider",
summary=(
@@ -276,6 +298,27 @@ manifest = ModuleManifest(
kind="repository",
),
),
translations={
"de": {
"title": "Konfigurierte Bedeutung von Formularen und Feldern dokumentieren",
"summary": (
"Mandanteneigene semantische Erläuterungen an ein berechtigtes Formular, Feld oder einen Abschnitt anfügen, ohne das "
"Schema zu verändern."
),
"body": (
"Ist Docs installiert, liefert Forms dokumentationssichere Subjekte für jede zugängliche aktuelle Definition sowie ihre "
"stabilen Felder und Abschnitte. Die Subjektidentität übersteht Änderungen an Bezeichnung und Reihenfolge. Ein gelöschter "
"und später neu angelegter Schlüssel erhält eine neue Abstammungsidentität; ältere Dokumentation bleibt ausdrücklich "
"verwaist, statt stillschweigend neu angefügt zu werden. Fingerabdrücke ändern sich nur, wenn sich relevante Formular-, "
"Feld-, Lokalisierungs-, Hierarchie-, Validierungs- oder Sichtbarkeitssemantik ändert, und fordern dann eine redaktionelle "
"Prüfung an; Docs-Inhalte werden niemals automatisch veröffentlicht oder entwertet. Semantischer Text darf Bedeutung, "
"Erhebungszweck, Interpretation, beabsichtigte und nicht beabsichtigte Verwendung sowie Beispiele erläutern, aber weder "
"Feldtyp, Pflichtstatus, Einschränkungen, Validierung, Optionen, Richtlinie noch übermittelte Werte überstimmen. Forms prüft "
"Mandant und Leseberechtigung für Ermittlung, direkte Auflösung, Kontexthilfe, Suche und Docs-Projektion erneut. Fehlt Docs, "
"funktionieren Formularerstellung und statische Hilfe unverändert weiter."
),
}
},
metadata={
"kind": "reference",
"help_contexts": ["forms.semantic-documentation"],
@@ -301,6 +344,20 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "auditor"),
related_modules=("core", "forms_runtime", "docs", "audit"),
translations={
"de": {
"title": "Betroffenenanfragen für Formulardefinitionen",
"summary": "Aktivität der Definitionsautoren exportieren, ohne Schemata als übermittelte Werte zu behandeln.",
"body": (
"Forms gleicht nur eine exakte mandantenbezogene Kontokennung ab und kann eine bereits verifizierte Suche auf ein "
"Formular oder eine Definitionsrevision begrenzen. Ausgegeben werden unveränderliche Revisionskennung, "
"Lebenszykluszustand und Zeitpunkte der Definitionsarbeit der betroffenen Person. Titel, Suchtext, Schemanutzdaten, "
"Feldsemantik, Richtlinienverweise und Inhalte von Änderungsgründen sind nicht enthalten. Forms speichert keine "
"übermittelten Werte; Forms Runtime und der zuständige Service exportieren diese Datensätze getrennt. Die Zuordnung der "
"Definitionsarbeit bleibt mit der unveränderlichen Schemahistorie erhalten."
),
}
},
metadata={
"help_contexts": ["forms.catalogue", "privacy.data-subject-requests"],
"consequence_classes": {
@@ -314,6 +371,8 @@ manifest = ModuleManifest(
title="Reusable form definitions",
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
body=(
"Documentation books sit beside Form definitions and the revision dialog title. The separate "
"Form meaning label carries the exact definitions semantic documentation book. "
"Each revision fixes field types, options, constraints, draft, attachment, signature, policy, and handoff requirements. "
"Publishing is explicit; existing submissions continue to retain their exact revision. The catalogue, filter, empty states, "
"section headings, and definition field groups use the shared responsive layout language and reflow at narrow widths without "
@@ -322,6 +381,11 @@ manifest = ModuleManifest(
layer="configured",
documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "product_owner"),
conditions=(
DocumentationCondition(
any_scopes=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE)
),
),
links=(
DocumentationLink(
label="Forms boundary and recovery",
@@ -329,7 +393,26 @@ manifest = ModuleManifest(
kind="repository",
),
),
translations={
"de": {
"title": "Wiederverwendbare Formulardefinitionen erstellen",
"summary": (
"Unveränderliche versionierte Schemata erstellen, die Forms Runtime und institutionelle Services verwenden."
),
"body": (
"Dokumentationsbücher stehen neben Formulardefinitionen und dem Titel des Revisionsdialogs. "
"Die separate Bezeichnung Formularbedeutung trägt das Buch zur semantischen Dokumentation der "
"genauen Definition. "
"Jede Revision legt Feldtypen, Optionen, Einschränkungen sowie Anforderungen an Entwurf, Anlagen, Signaturen, "
"Richtlinien und Übergaben fest. Die Veröffentlichung erfolgt ausdrücklich; vorhandene Einreichungen behalten ihre "
"exakte Revision. Katalog, Filter, Leerzustände, Abschnittsüberschriften und Feldgruppen der Definition verwenden die "
"gemeinsame responsive Layoutsprache und ordnen sich bei geringer Breite neu an, ohne Filterumfang, Berechtigungen oder "
"Lebenszykluswirkungen zu verändern."
),
}
},
metadata={
"kind": "workflow",
"seed": True,
"help_contexts": [
"forms.navigation",
@@ -355,6 +438,7 @@ manifest = ModuleManifest(
"by Forms Runtime when that module is present. Publishing makes a revision available for new instances; existing "
"instances retain their prior exact revision. Retirement prevents future use without deleting definitions or submissions. "
"Package import always creates a local draft and retains source provenance; it never silently publishes an imported revision. "
"The Forms configuration provider validates the digest-bound source fragment, target tenant, operator authority, local conflicts, and replay provenance during preflight. The conservative conflict policy preserves an existing definition unless the reviewed package explicitly requests a new revision. Reapplying the same source digest is a no-op. Export emits only definition configuration and provenance; runtime submissions and submitted values remain outside the package."
),
layer="configured",
documentation_types=("admin", "user"),
@@ -367,6 +451,22 @@ manifest = ModuleManifest(
kind="repository",
),
),
translations={
"de": {
"title": "Felder und Lebenszyklusfolgen von Formulardefinitionen",
"summary": "Schema, Veröffentlichung, Lokalisierung, Richtlinien, Nachweise, Pakete und Übergaben unveränderlicher Formularrevisionen verstehen.",
"body": (
"Der stabile Schlüssel kennzeichnet die Definition; jedes Speichern erzeugt eine neue unveränderliche Revision. "
"Feldschlüssel, Typen, Einschränkungen, Sichtbarkeitsbedingungen, Seiten, Abschnitte, Auswahlwerte, Hilfen, Übersetzungen und Barrierefreiheitshinweise bilden das exakte Laufzeitschema. "
"Anhangs-, Signatur-, Richtlinien-, Entwurfs- und Übergabevorgaben werden durch Forms Runtime erzwungen, sofern das Modul vorhanden ist. "
"Die Veröffentlichung stellt eine Revision für neue Instanzen bereit; bestehende Instanzen behalten ihre genaue frühere Revision. Die Stilllegung verhindert künftige Nutzung, ohne Definitionen oder Einreichungen zu löschen. "
"Ein Paketimport erzeugt immer einen lokalen Entwurf, bewahrt die Herkunft und veröffentlicht niemals stillschweigend. "
"Der Forms-Konfigurationsprovider prüft vorab das digest-gebundene Quellfragment, den Zielmandanten, die Berechtigung, lokale Konflikte und Wiederholungsnachweise. "
"Die vorsichtige Konfliktregel erhält eine vorhandene Definition, sofern das geprüfte Paket nicht ausdrücklich eine neue Revision verlangt. Dieselbe Quelldigest erneut anzuwenden ist ein Leerlauf. "
"Der Export enthält ausschließlich Definitionskonfiguration und Herkunft; Laufzeiteinreichungen und eingegebene Werte bleiben außerhalb des Pakets."
),
}
},
metadata={
"seed": True,
"help_contexts": [
@@ -387,6 +487,16 @@ manifest = ModuleManifest(
"retire": "Stops future use while retaining definitions and exact runtime references.",
"import_package": "Creates a local draft and retains package provenance without automatic publication.",
},
"limitations": [
"Package import never publishes a Form revision automatically.",
"Runtime submissions and submitted values are never exported as definition configuration.",
"Generic package rollback requires the retained pre-apply database snapshot.",
],
"operational_consequences": [
"Preserve blocks a conflicting local definition; new_revision must be explicitly reviewed.",
"Reapplying an identical source digest is idempotent and creates no revision.",
"Imported definitions require normal Forms review and publication before runtime use.",
],
},
),
),
@@ -413,7 +523,7 @@ manifest = ModuleManifest(
"workflow instance",
),
reference_packages=("product.service-to-decision",),
migration_docs=("docs/FORMS_BOUNDARY.md",),
migration_docs=("docs/FORMS_BOUNDARY.md", "docs/INTERFACE_PATTERN_MIGRATION.md"),
recovery_docs=("docs/FORMS_BOUNDARY.md",),
security_docs=("docs/FORMS_BOUNDARY.md",),
operations_docs=("docs/FORMS_BOUNDARY.md",),
@@ -421,5 +531,10 @@ manifest = ModuleManifest(
)
manifest = with_documentation_structured_translations(
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
)
def get_manifest() -> ModuleManifest:
return manifest
+194
View File
@@ -0,0 +1,194 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.configuration_packages import (
ConfigurationPackageFragment,
ConfigurationPreflightContext,
ConfigurationProvider,
)
from govoplan_core.core.institutional import (
FormDefinition,
FormFieldDefinition,
FormLocalization,
FormPageDefinition,
FormSectionDefinition,
InstitutionalReference,
TemporalRevision,
)
from govoplan_forms.backend.configuration_provider import (
FORMS_CONFIGURATION_CAPABILITY,
SqlFormsConfigurationProvider,
_apply_definition,
_preflight_definition,
)
from govoplan_forms.backend.db.models import FormDefinitionRevision
from govoplan_forms.backend.manifest import get_manifest
from govoplan_forms.backend.service import (
export_form_definition_fragment,
get_form_definition,
)
@dataclass(frozen=True)
class Principal:
tenant_id: str
account_id: str = "operator-1"
def source_definition(title: str = "Resident parking permit") -> FormDefinition:
return FormDefinition(
reference=InstitutionalReference(
kind="form",
owner_module="forms",
object_id="resident-parking-permit-application",
tenant_id="reference-package",
version="3",
),
key="resident-parking-permit-application",
temporal=TemporalRevision(
revision="3",
recorded_at=datetime(2026, 8, 22, tzinfo=UTC),
change_reason="Reference package revision.",
),
title=title,
description="Apply for a resident parking permit through a digital or assisted channel.",
fields=(
FormFieldDefinition(key="applicant_name", label="Name", required=True, constraints={"min_length": 2, "max_length": 200}),
FormFieldDefinition(key="applicant_email", label="Email", required=True, constraints={"format": "email"}),
FormFieldDefinition(key="residence_address", label="Primary residence", required=True, constraints={"max_length": 500}),
FormFieldDefinition(key="licence_plate", label="Licence plate", required=True, constraints={"max_length": 20}),
),
publication_state="published",
max_attachments=4,
policy_refs=("law:resident-parking-permit", "records:resident-parking-permit"),
handoff_kinds=("case", "workflow"),
pages=(
FormPageDefinition(
key="application",
title="Application",
sections=(
FormSectionDefinition(
key="applicant-and-vehicle",
title="Applicant and vehicle",
field_keys=("applicant_name", "applicant_email", "residence_address", "licence_plate"),
),
),
),
),
fallback_locale="de",
localizations=(
FormLocalization(
locale="de",
title="Anwohnerparkausweis beantragen",
description="Einen Anwohnerparkausweis digital oder mit Unterstützung beantragen.",
field_labels={
"applicant_name": "Name",
"applicant_email": "E-Mail-Adresse",
"residence_address": "Hauptwohnsitz",
"licence_plate": "Kennzeichen",
},
page_titles={"application": "Antrag"},
section_titles={"applicant-and-vehicle": "Antragstellende Person und Fahrzeug"},
),
),
)
class FormsConfigurationProviderTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite+pysqlite:///:memory:")
FormDefinitionRevision.__table__.create(self.engine)
self.session = Session(self.engine)
self.context = ConfigurationPreflightContext(
tenant_id="tenant-1",
operator_user_id="operator-1",
operator_scopes=frozenset({"system:governance:write"}),
installed_modules={"forms": "0.1.20"},
capabilities=frozenset({FORMS_CONFIGURATION_CAPABILITY}),
)
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
def fragment(self, definition: FormDefinition | None = None, *, on_conflict: str = "new_revision") -> ConfigurationPackageFragment:
return ConfigurationPackageFragment(
module_id="forms",
fragment_type="definition",
fragment_id="resident-parking-permit-application",
payload={
"fragment": export_form_definition_fragment(
definition or source_definition(),
exported_at=datetime(2026, 8, 22, 12, tzinfo=UTC),
exported_by="package-author",
),
"on_conflict": on_conflict,
"change_reason": "Install the reviewed resident parking permit reference form.",
},
)
def test_provider_is_registered_and_runtime_checkable(self) -> None:
provider = get_manifest().capability_factories[FORMS_CONFIGURATION_CAPABILITY](None) # type: ignore[arg-type]
self.assertIsInstance(provider, ConfigurationProvider)
self.assertIsInstance(provider, SqlFormsConfigurationProvider)
self.assertEqual(("definition",), provider.describe().fragment_types)
def test_import_is_tenant_local_draft_and_same_source_replay_is_noop(self) -> None:
fragment = self.fragment()
preflight = _preflight_definition(self.session, fragment, self.context)
applied = _apply_definition(self.session, fragment, self.context)
self.session.commit()
replay_preflight = _preflight_definition(self.session, fragment, self.context)
replay = _apply_definition(self.session, fragment, self.context)
self.assertEqual("create", preflight.plan[0].action)
self.assertEqual(1, len(applied.created_refs))
imported = get_form_definition(
self.session,
Principal("tenant-1"),
form_id="resident-parking-permit-application",
)
assert imported is not None
self.assertEqual("tenant-1", imported.reference.tenant_id)
self.assertEqual("draft", imported.publication_state)
self.assertEqual(
"reference-package",
imported.metadata["package_import"]["source_tenant_id"],
)
self.assertEqual("noop", replay_preflight.plan[0].action)
self.assertEqual({}, replay.created_refs)
self.assertEqual({}, replay.updated_refs)
def test_preserve_reports_conflict_and_missing_authority_blocks(self) -> None:
first = self.fragment()
_apply_definition(self.session, first, self.context)
self.session.commit()
changed = self.fragment(source_definition("Changed reference"), on_conflict="preserve")
unauthorized = ConfigurationPreflightContext(
tenant_id="tenant-1",
operator_user_id="operator-2",
)
conflict = _preflight_definition(self.session, changed, self.context)
denied = _preflight_definition(self.session, changed, unauthorized)
self.assertIn(
"forms_configuration_conflict",
{item.code for item in conflict.diagnostics},
)
self.assertIn(
"forms_configuration_write_scope_required",
{item.code for item in denied.diagnostics},
)
if __name__ == "__main__":
unittest.main()
+12 -1
View File
@@ -11,6 +11,14 @@ from govoplan_core.core.semantic_documentation import (
class FormsInterfaceDocumentationContractTests(unittest.TestCase):
def test_all_static_topics_have_complete_german_content(self) -> None:
for topic in manifest.documentation:
german = (topic.translations or {}).get("de", {})
self.assertEqual({"title", "summary", "body"}, set(german), topic.id)
self.assertTrue(
all(str(value).strip() for value in german.values()), topic.id
)
def test_route_and_surfaces_remain_declared(self) -> None:
frontend = manifest.frontend
self.assertIsNotNone(frontend)
@@ -26,7 +34,10 @@ class FormsInterfaceDocumentationContractTests(unittest.TestCase):
reference = topics["forms.reference.fields-and-consequences"]
self.assertIn("forms.catalogue", guide.metadata["help_contexts"])
self.assertGreaterEqual(len(guide.metadata["privacy_notes"]), 3)
self.assertIn("forms.field.publication-state", reference.metadata["help_contexts"])
self.assertEqual("workflow", guide.metadata["kind"])
self.assertIn(
"forms.field.publication-state", reference.metadata["help_contexts"]
)
self.assertIn("publish", reference.metadata["consequence_classes"])
self.assertIn("import_package", reference.metadata["consequence_classes"])
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/forms-webui",
"version": "0.1.19",
"version": "0.1.23",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -6,6 +6,7 @@ import { FormGrid,
ConfirmDialog,
Dialog,
DocumentationHelpLink,
TextWithHelp,
DismissibleAlert,
FormField as Field,
IconButton,
@@ -193,6 +194,7 @@ export default function FormDefinitionDialog({
<Dialog
open={open}
title={definition ? `Revise ${definition.title}` : "New Form definition"}
titleHelp={<DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} />}
onClose={requestClose}
closeDisabled={busy}
portal
@@ -207,9 +209,8 @@ export default function FormDefinitionDialog({
}>
<div className="form-definition-editor">
<div className="form-definition-help">
<DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} />
{definition && <>
<DocumentationHelpLink reference={semanticFormDocumentation(definition)} />
<TextWithHelp help={<DocumentationHelpLink reference={semanticFormDocumentation(definition)} />}>Form meaning</TextWithHelp>
<a className="btn btn-secondary" href={semanticAuthoringHref(definition)} target="_blank" rel="noreferrer">
<BookOpen size={16} aria-hidden="true" /> Document form meaning
</a>
+2 -1
View File
@@ -162,6 +162,8 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
<main className="forms-page">
<WorkspaceFrame className="forms-shell" label="Form definitions" interfaceId="forms.catalogue" helpContextId="forms.page.catalogue" helpModuleId="forms">
<WorkspaceActionBar
title="Form definitions"
titleHelp={<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />}
scope="workspace"
variant="collection"
refreshable
@@ -187,7 +189,6 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
<span className="forms-count">{total}</span>
</>}
createAction={<Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? FORMS_I18N.writeReason : undefined} onClick={() => setEditing("new")}><Plus size={16} aria-hidden="true" />New definition</Button>}
helpAction={<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />}
/>
<PageScrollViewport className="forms-list-viewport">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
+2
View File
@@ -1,6 +1,7 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
const en = {
"Form meaning": "Form meaning",
"i18n:govoplan-forms.form_definitions": "Form definitions",
"i18n:govoplan-forms.loading_reason": "Form definitions are still loading.",
"i18n:govoplan-forms.busy_reason": "Another Form definition action is still running.",
@@ -74,6 +75,7 @@ const en = {
} as const;
const de: Record<keyof typeof en, string> = {
"Form meaning": "Formularbedeutung",
"i18n:govoplan-forms.form_definitions": "Formulardefinitionen",
"i18n:govoplan-forms.loading_reason": "Formulardefinitionen werden noch geladen.",
"i18n:govoplan-forms.busy_reason": "Eine andere Aktion für Formulardefinitionen läuft noch.",
+1 -1
View File
@@ -9,7 +9,7 @@ const FormsPage = lazy(() => import("./features/forms/FormsPage"));
export const formsModule: PlatformWebModule = {
id: "forms",
label: "i18n:govoplan-forms.form_definitions",
version: "0.1.19",
version: "0.1.21",
dependencies: ["access"],
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy", "docs"],
translations: generatedTranslations,