Release Forms v0.1.20 configuration package provider
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-22 18:04:34 +02:00
parent b9132d076d
commit 727c70f756
7 changed files with 603 additions and 7 deletions
+2 -2
View File
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-forms" name = "govoplan-forms"
version = "0.1.19" version = "0.1.20"
description = "Immutable reusable form definitions for GovOPlaN." description = "Immutable reusable form definitions for GovOPlaN."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] 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] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
+1 -1
View File
@@ -1,3 +1,3 @@
"""GovOPlaN Forms module.""" """GovOPlaN Forms module."""
__version__ = "0.1.18" __version__ = "0.1.20"
@@ -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",
]
+45 -2
View File
@@ -33,6 +33,7 @@ from govoplan_core.core.semantic_documentation import (
) )
from govoplan_core.core.views import ViewSurface from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base 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.db import models as form_models
from govoplan_forms.backend.dsar_provider import ( from govoplan_forms.backend.dsar_provider import (
FORMS_DSAR_CAPABILITY, FORMS_DSAR_CAPABILITY,
@@ -46,7 +47,7 @@ from govoplan_forms.backend.semantic_subjects import (
MODULE_ID = "forms" MODULE_ID = "forms"
MODULE_NAME = "Forms" MODULE_NAME = "Forms"
MODULE_VERSION = "0.1.19" MODULE_VERSION = "0.1.20"
READ_SCOPE = "forms:definition:read" READ_SCOPE = "forms:definition:read"
WRITE_SCOPE = "forms:definition:write" WRITE_SCOPE = "forms:definition:write"
ADMIN_SCOPE = "forms:definition:admin" ADMIN_SCOPE = "forms:definition:admin"
@@ -95,6 +96,12 @@ def _semantic_subjects(
return FormsSemanticDocumentationSubjectProvider() return FormsSemanticDocumentationSubjectProvider()
def _configuration_provider(_context: ModuleContext):
from govoplan_forms.backend.configuration_provider import SqlFormsConfigurationProvider
return SqlFormsConfigurationProvider()
manifest = ModuleManifest( manifest = ModuleManifest(
id=MODULE_ID, id=MODULE_ID,
name=MODULE_NAME, name=MODULE_NAME,
@@ -107,6 +114,7 @@ manifest = ModuleManifest(
), ),
provides_interfaces=( provides_interfaces=(
ModuleInterfaceProvider(name="forms.definitions", version="0.1.0"), 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=FORMS_DSAR_CAPABILITY, version="0.1.0"),
ModuleInterfaceProvider( ModuleInterfaceProvider(
name=SEMANTIC_SUBJECT_CAPABILITY, name=SEMANTIC_SUBJECT_CAPABILITY,
@@ -204,6 +212,7 @@ manifest = ModuleManifest(
), ),
capability_factories={ capability_factories={
CAPABILITY_FORM_DEFINITIONS: _definitions, CAPABILITY_FORM_DEFINITIONS: _definitions,
FORMS_CONFIGURATION_CAPABILITY: _configuration_provider,
FORMS_DSAR_CAPABILITY: _dsar_provider, FORMS_DSAR_CAPABILITY: _dsar_provider,
SEMANTIC_SUBJECT_CAPABILITY: _semantic_subjects, SEMANTIC_SUBJECT_CAPABILITY: _semantic_subjects,
}, },
@@ -213,6 +222,13 @@ manifest = ModuleManifest(
summary="Resolves exact tenant-bound form schemas without exposing Forms tables.", summary="Resolves exact tenant-bound form schemas without exposing Forms tables.",
contract_version="0.1.0", 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( FORMS_DSAR_CAPABILITY: CapabilityDocumentation(
label="Forms data-subject request provider", label="Forms data-subject request provider",
summary=( summary=(
@@ -354,7 +370,8 @@ manifest = ModuleManifest(
"become the exact runtime schema. Attachment, signature, policy, draft, and permitted-handoff settings are enforced " "become the exact runtime schema. Attachment, signature, policy, draft, and permitted-handoff settings are enforced "
"by Forms Runtime when that module is present. Publishing makes a revision available for new instances; existing " "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. " "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." "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", layer="configured",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
@@ -367,6 +384,22 @@ manifest = ModuleManifest(
kind="repository", 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={ metadata={
"seed": True, "seed": True,
"help_contexts": [ "help_contexts": [
@@ -387,6 +420,16 @@ manifest = ModuleManifest(
"retire": "Stops future use while retaining definitions and exact runtime references.", "retire": "Stops future use while retaining definitions and exact runtime references.",
"import_package": "Creates a local draft and retains package provenance without automatic publication.", "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.",
],
}, },
), ),
), ),
+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()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/forms-webui", "name": "@govoplan/forms-webui",
"version": "0.1.19", "version": "0.1.20",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -9,7 +9,7 @@ const FormsPage = lazy(() => import("./features/forms/FormsPage"));
export const formsModule: PlatformWebModule = { export const formsModule: PlatformWebModule = {
id: "forms", id: "forms",
label: "i18n:govoplan-forms.form_definitions", label: "i18n:govoplan-forms.form_definitions",
version: "0.1.19", version: "0.1.20",
dependencies: ["access"], dependencies: ["access"],
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy", "docs"], optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy", "docs"],
translations: generatedTranslations, translations: generatedTranslations,