Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7bd5b3b4 | ||
|
|
00bec0ed94 | ||
|
|
cfcd723496 | ||
|
|
06afa79795 | ||
|
|
727c70f756 |
@@ -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.
|
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.
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ Form definitions should carry:
|
|||||||
option, constraint, draft, attachment, signature, policy, and handoff editing.
|
option, constraint, draft, attachment, signature, policy, and handoff editing.
|
||||||
- Forms Runtime performs value validation against the resolved definition; the
|
- Forms Runtime performs value validation against the resolved definition; the
|
||||||
definition owner does not persist submissions.
|
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
|
## Recovery And Operations
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
state, heading, and narrow-layout geometry. These contracts do not change
|
||||||
filter scope, permissions, revision semantics, or publication consequences.
|
filter scope, permissions, revision semantics, or publication consequences.
|
||||||
English and German catalogues cover the owned route and editor vocabulary.
|
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.
|
||||||
|
|||||||
@@ -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
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-forms"
|
name = "govoplan-forms"
|
||||||
version = "0.1.19"
|
version = "0.1.23"
|
||||||
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,3 +1,3 @@
|
|||||||
"""GovOPlaN Forms module."""
|
"""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.']}}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from __future__ import annotations
|
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 pathlib import Path
|
||||||
|
|
||||||
from govoplan_core.core.access import (
|
from govoplan_core.core.access import (
|
||||||
@@ -13,6 +16,7 @@ from govoplan_core.core.module_guards import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.modules import (
|
from govoplan_core.core.modules import (
|
||||||
CapabilityDocumentation,
|
CapabilityDocumentation,
|
||||||
|
DocumentationCondition,
|
||||||
DocumentationLink,
|
DocumentationLink,
|
||||||
DocumentationTopic,
|
DocumentationTopic,
|
||||||
FrontendModule,
|
FrontendModule,
|
||||||
@@ -33,6 +37,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 +51,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.23"
|
||||||
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 +100,14 @@ 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 +120,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 +218,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 +228,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=(
|
||||||
@@ -276,6 +298,27 @@ manifest = ModuleManifest(
|
|||||||
kind="repository",
|
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={
|
metadata={
|
||||||
"kind": "reference",
|
"kind": "reference",
|
||||||
"help_contexts": ["forms.semantic-documentation"],
|
"help_contexts": ["forms.semantic-documentation"],
|
||||||
@@ -301,6 +344,20 @@ manifest = ModuleManifest(
|
|||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("user", "operator", "module_admin", "auditor"),
|
audience=("user", "operator", "module_admin", "auditor"),
|
||||||
related_modules=("core", "forms_runtime", "docs", "audit"),
|
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={
|
metadata={
|
||||||
"help_contexts": ["forms.catalogue", "privacy.data-subject-requests"],
|
"help_contexts": ["forms.catalogue", "privacy.data-subject-requests"],
|
||||||
"consequence_classes": {
|
"consequence_classes": {
|
||||||
@@ -314,6 +371,8 @@ manifest = ModuleManifest(
|
|||||||
title="Reusable form definitions",
|
title="Reusable form definitions",
|
||||||
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
|
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
|
||||||
body=(
|
body=(
|
||||||
|
"Documentation books sit beside Form definitions and the revision dialog title. The separate "
|
||||||
|
"Form meaning label carries the exact definition’s semantic documentation book. "
|
||||||
"Each revision fixes field types, options, constraints, draft, attachment, signature, policy, and handoff requirements. "
|
"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, "
|
"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 "
|
"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",
|
layer="configured",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("user", "operator", "module_admin", "product_owner"),
|
audience=("user", "operator", "module_admin", "product_owner"),
|
||||||
|
conditions=(
|
||||||
|
DocumentationCondition(
|
||||||
|
any_scopes=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE)
|
||||||
|
),
|
||||||
|
),
|
||||||
links=(
|
links=(
|
||||||
DocumentationLink(
|
DocumentationLink(
|
||||||
label="Forms boundary and recovery",
|
label="Forms boundary and recovery",
|
||||||
@@ -329,7 +393,26 @@ manifest = ModuleManifest(
|
|||||||
kind="repository",
|
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={
|
metadata={
|
||||||
|
"kind": "workflow",
|
||||||
"seed": True,
|
"seed": True,
|
||||||
"help_contexts": [
|
"help_contexts": [
|
||||||
"forms.navigation",
|
"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 "
|
"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 +451,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 +487,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.",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -413,7 +523,7 @@ manifest = ModuleManifest(
|
|||||||
"workflow instance",
|
"workflow instance",
|
||||||
),
|
),
|
||||||
reference_packages=("product.service-to-decision",),
|
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",),
|
recovery_docs=("docs/FORMS_BOUNDARY.md",),
|
||||||
security_docs=("docs/FORMS_BOUNDARY.md",),
|
security_docs=("docs/FORMS_BOUNDARY.md",),
|
||||||
operations_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:
|
def get_manifest() -> ModuleManifest:
|
||||||
return manifest
|
return manifest
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -11,6 +11,14 @@ from govoplan_core.core.semantic_documentation import (
|
|||||||
|
|
||||||
|
|
||||||
class FormsInterfaceDocumentationContractTests(unittest.TestCase):
|
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:
|
def test_route_and_surfaces_remain_declared(self) -> None:
|
||||||
frontend = manifest.frontend
|
frontend = manifest.frontend
|
||||||
self.assertIsNotNone(frontend)
|
self.assertIsNotNone(frontend)
|
||||||
@@ -26,7 +34,10 @@ class FormsInterfaceDocumentationContractTests(unittest.TestCase):
|
|||||||
reference = topics["forms.reference.fields-and-consequences"]
|
reference = topics["forms.reference.fields-and-consequences"]
|
||||||
self.assertIn("forms.catalogue", guide.metadata["help_contexts"])
|
self.assertIn("forms.catalogue", guide.metadata["help_contexts"])
|
||||||
self.assertGreaterEqual(len(guide.metadata["privacy_notes"]), 3)
|
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("publish", reference.metadata["consequence_classes"])
|
||||||
self.assertIn("import_package", reference.metadata["consequence_classes"])
|
self.assertIn("import_package", reference.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/forms-webui",
|
"name": "@govoplan/forms-webui",
|
||||||
"version": "0.1.19",
|
"version": "0.1.23",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { FormGrid,
|
|||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
|
TextWithHelp,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FormField as Field,
|
FormField as Field,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -193,6 +194,7 @@ export default function FormDefinitionDialog({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
title={definition ? `Revise ${definition.title}` : "New Form definition"}
|
title={definition ? `Revise ${definition.title}` : "New Form definition"}
|
||||||
|
titleHelp={<DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} />}
|
||||||
onClose={requestClose}
|
onClose={requestClose}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
portal
|
portal
|
||||||
@@ -207,9 +209,8 @@ export default function FormDefinitionDialog({
|
|||||||
}>
|
}>
|
||||||
<div className="form-definition-editor">
|
<div className="form-definition-editor">
|
||||||
<div className="form-definition-help">
|
<div className="form-definition-help">
|
||||||
<DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} />
|
|
||||||
{definition && <>
|
{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">
|
<a className="btn btn-secondary" href={semanticAuthoringHref(definition)} target="_blank" rel="noreferrer">
|
||||||
<BookOpen size={16} aria-hidden="true" /> Document form meaning
|
<BookOpen size={16} aria-hidden="true" /> Document form meaning
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
<main className="forms-page">
|
<main className="forms-page">
|
||||||
<WorkspaceFrame className="forms-shell" label="Form definitions" interfaceId="forms.catalogue" helpContextId="forms.page.catalogue" helpModuleId="forms">
|
<WorkspaceFrame className="forms-shell" label="Form definitions" interfaceId="forms.catalogue" helpContextId="forms.page.catalogue" helpModuleId="forms">
|
||||||
<WorkspaceActionBar
|
<WorkspaceActionBar
|
||||||
|
title="Form definitions"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />}
|
||||||
scope="workspace"
|
scope="workspace"
|
||||||
variant="collection"
|
variant="collection"
|
||||||
refreshable
|
refreshable
|
||||||
@@ -187,7 +189,6 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
<span className="forms-count">{total}</span>
|
<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>}
|
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">
|
<PageScrollViewport className="forms-list-viewport">
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
|
"Form meaning": "Form meaning",
|
||||||
"i18n:govoplan-forms.form_definitions": "Form definitions",
|
"i18n:govoplan-forms.form_definitions": "Form definitions",
|
||||||
"i18n:govoplan-forms.loading_reason": "Form definitions are still loading.",
|
"i18n:govoplan-forms.loading_reason": "Form definitions are still loading.",
|
||||||
"i18n:govoplan-forms.busy_reason": "Another Form definition action is still running.",
|
"i18n:govoplan-forms.busy_reason": "Another Form definition action is still running.",
|
||||||
@@ -74,6 +75,7 @@ const en = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const de: Record<keyof typeof en, string> = {
|
const de: Record<keyof typeof en, string> = {
|
||||||
|
"Form meaning": "Formularbedeutung",
|
||||||
"i18n:govoplan-forms.form_definitions": "Formulardefinitionen",
|
"i18n:govoplan-forms.form_definitions": "Formulardefinitionen",
|
||||||
"i18n:govoplan-forms.loading_reason": "Formulardefinitionen werden noch geladen.",
|
"i18n:govoplan-forms.loading_reason": "Formulardefinitionen werden noch geladen.",
|
||||||
"i18n:govoplan-forms.busy_reason": "Eine andere Aktion für Formulardefinitionen läuft noch.",
|
"i18n:govoplan-forms.busy_reason": "Eine andere Aktion für Formulardefinitionen läuft noch.",
|
||||||
|
|||||||
+1
-1
@@ -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.21",
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user