refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter
This commit is contained in:
parent
06ab25fb47
commit
03af41e837
74 changed files with 6089 additions and 5183 deletions
|
|
@ -1,11 +1,9 @@
|
|||
"""Mitarbeiter-einstellen form component for kirche-einfach flows.
|
||||
|
||||
Standalone by design: this module shares NO code with the other form
|
||||
modules (no imports from them). The LFX runtime executes the embedded
|
||||
module source without our ``kirche`` package, so every form component
|
||||
must be self-contained (stdlib + ``lfx`` only). Do NOT refactor the
|
||||
duplicated machinery below into a shared import — it would break flow
|
||||
execution. Copy this file (or ``bauvorhaben_form.py``) as the starting
|
||||
Thin variant: shared runtime lives in ``kirche._form_core`` (imported here
|
||||
for the Langflow palette); ``scripts/make_flow_fixtures.py:kirche_node()``
|
||||
inlines the core into the exported ``code`` so the LFX runtime stays
|
||||
self-contained. Copy this file (or ``bauvorhaben_form.py``) as the starting
|
||||
point for future forms.
|
||||
|
||||
A flow that starts with :class:`CustomInputMitarbeiter` declares the
|
||||
|
|
@ -33,8 +31,8 @@ Langflow playground), all visible declared defaults apply.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import json # noqa: F401 -- used by inlined _form_core at runtime
|
||||
import re # noqa: F401 -- used by inlined _form_core at runtime
|
||||
from typing import ClassVar
|
||||
|
||||
from lfx.custom import Component
|
||||
|
|
@ -46,94 +44,16 @@ from lfx.io import (
|
|||
)
|
||||
from lfx.schema.data import Data
|
||||
|
||||
from kirche._form_core import (
|
||||
PLUMBING_INPUTS,
|
||||
_field_conditions,
|
||||
_json_safe,
|
||||
collect_form_data,
|
||||
parse_envelope,
|
||||
)
|
||||
|
||||
def _text(value) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if hasattr(value, "text"):
|
||||
return value.text or ""
|
||||
return str(value)
|
||||
|
||||
|
||||
def _json_safe(value):
|
||||
"""Plain JSON value for one field: unwrap LFX wrappers, keep scalars."""
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
return [_json_safe(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _json_safe(item) for key, item in value.items()}
|
||||
text = getattr(value, "text", None)
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
data = getattr(value, "data", None)
|
||||
if isinstance(data, dict):
|
||||
return _json_safe(data)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _split_conditions(text: str) -> list:
|
||||
parts: list = []
|
||||
buf: list = []
|
||||
quoted = False
|
||||
for char in text:
|
||||
if char == '"':
|
||||
quoted = not quoted
|
||||
buf.append(char)
|
||||
elif char == "&" and not quoted:
|
||||
parts.append("".join(buf))
|
||||
buf = []
|
||||
else:
|
||||
buf.append(char)
|
||||
parts.append("".join(buf))
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
|
||||
|
||||
def _parse_condition(token: str):
|
||||
field, sep, value = token.partition("=")
|
||||
field, value = field.strip(), value.strip()
|
||||
if not sep or not field or not value:
|
||||
return None
|
||||
if len(value) >= 2 and value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
lowered = value.lower()
|
||||
if lowered == "true":
|
||||
return field, True
|
||||
if lowered == "false":
|
||||
return field, False
|
||||
try:
|
||||
return field, int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return field, float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return field, value
|
||||
|
||||
|
||||
def _field_conditions(info: object) -> list:
|
||||
"""``[(trigger, value)]`` from a field's ``info`` markers."""
|
||||
conditions: list = []
|
||||
|
||||
def _collect(match) -> str:
|
||||
for token in _split_conditions(match.group(1)):
|
||||
parsed = _parse_condition(token)
|
||||
if parsed is not None:
|
||||
conditions.append(parsed)
|
||||
return ""
|
||||
|
||||
_VISIBLE_RE.sub(_collect, info if isinstance(info, str) else "")
|
||||
return conditions
|
||||
|
||||
|
||||
#: Reserved input carrying the run envelope — a real runtime input, so it
|
||||
#: must stay out of the output. Never name a form field ``form_values``.
|
||||
PLUMBING_INPUTS = frozenset({"form_values"})
|
||||
|
||||
#: ``[visible_if ...]`` marker (same syntax as the backend parser in
|
||||
#: ``app/flows/input_schema.py`` — keep the two in sync).
|
||||
_VISIBLE_RE = re.compile(r"\[visible_if\s+([^\]]+)\]")
|
||||
# <-- _form_core inlined by scripts/make_flow_fixtures.py:kirche_node()
|
||||
# (LFX executes embedded source without the kirche package) -->
|
||||
|
||||
|
||||
class CustomInputMitarbeiter(Component):
|
||||
|
|
@ -223,9 +143,11 @@ class CustomInputMitarbeiter(Component):
|
|||
"""
|
||||
inputs = getattr(self, "_inputs", None)
|
||||
info = ""
|
||||
structured = None
|
||||
if isinstance(inputs, dict) and name in inputs:
|
||||
info = getattr(inputs[name], "info", "") or ""
|
||||
for trigger, expected in _field_conditions(info):
|
||||
structured = getattr(inputs[name], "visible_if", None)
|
||||
for trigger, expected in _field_conditions(info, structured):
|
||||
if trigger == name or trigger not in effective:
|
||||
# Self-reference or unknown trigger: fail open (visible),
|
||||
# mirroring the backend parser.
|
||||
|
|
@ -239,33 +161,14 @@ class CustomInputMitarbeiter(Component):
|
|||
return True
|
||||
|
||||
def collect(self) -> Data:
|
||||
try:
|
||||
envelope = json.loads(_text(self.form_values) or "{}")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
envelope = {}
|
||||
payload: dict = {}
|
||||
asked: list | None = None
|
||||
if isinstance(envelope, dict):
|
||||
# Asked-field list counts only in true envelope shape
|
||||
# ({"payload": {...}, "fields": [...]}); a raw dict or a form
|
||||
# field literally named "fields" must not confuse it.
|
||||
if isinstance(envelope.get("payload"), dict):
|
||||
payload = envelope["payload"]
|
||||
fields = envelope.get("fields")
|
||||
if isinstance(fields, list) and all(isinstance(item, str) for item in fields):
|
||||
asked = [item for item in fields if item]
|
||||
else:
|
||||
payload = envelope
|
||||
payload, asked = parse_envelope(self.form_values)
|
||||
# Effective trigger values: submitted first, code defaults after.
|
||||
declared = self._field_names()
|
||||
effective = {name: _json_safe(self._field_value(name)) for name in declared}
|
||||
for key, val in payload.items():
|
||||
if isinstance(key, str) and key:
|
||||
effective[key] = _json_safe(val)
|
||||
field_values = {name: _json_safe(self._field_value(name)) for name in declared}
|
||||
effective, scope = collect_form_data(declared, field_values, payload, asked)
|
||||
# Defaults only for asked AND visible fields (hidden branch fields
|
||||
# stay out, like the frontend never submits them); without an asked
|
||||
# list, all visible declared fields (Langflow playground).
|
||||
scope = [name for name in (asked if asked is not None else declared) if name in declared]
|
||||
data = {name: effective[name] for name in scope if self._is_visible(name, effective)}
|
||||
# Submitted values win over the field defaults. Unknown keys are
|
||||
# kept: the frontend may already know renamed fields this code
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue