better automatic export, prepare data validation for backend export

This commit is contained in:
2026-05-18 16:53:39 +02:00
parent 2931bf0876
commit 249449cf59
4 changed files with 593 additions and 59 deletions

44
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint", "nb", "tests"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:ae1cefda69bbf1f63d7c5d8c8b19f5dae8525f42c8550e9a44b186d7f57fe7bc"
content_hash = "sha256:611d8f56a617297efdc1daedd4a2cd7c21e58bc0a57288a8f230db9015a6adef"
[[metadata.targets]]
requires_python = ">=3.11,<3.14"
@@ -951,6 +951,17 @@ files = [
{file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
]
[[package]]
name = "dnspython"
version = "2.8.0"
requires_python = ">=3.10"
summary = "DNS toolkit"
groups = ["default"]
files = [
{file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"},
{file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"},
]
[[package]]
name = "docutils"
version = "0.22.4"
@@ -976,6 +987,21 @@ files = [
{file = "dopt_basics-0.2.4.tar.gz", hash = "sha256:c21fbe183bec5eab4cfd1404e10baca670035801596960822d0019e6e885983f"},
]
[[package]]
name = "email-validator"
version = "2.3.0"
requires_python = ">=3.8"
summary = "A robust email address syntax and deliverability validation library."
groups = ["default"]
dependencies = [
"dnspython>=2.0.0",
"idna>=2.0.0",
]
files = [
{file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"},
{file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"},
]
[[package]]
name = "execnet"
version = "2.1.2"
@@ -2781,6 +2807,22 @@ files = [
{file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"},
]
[[package]]
name = "pydantic"
version = "2.13.4"
extras = ["email"]
requires_python = ">=3.9"
summary = "Data validation using Python type hints"
groups = ["default"]
dependencies = [
"email-validator>=2.0.0",
"pydantic==2.13.4",
]
files = [
{file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"},
{file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"},
]
[[package]]
name = "pygments"
version = "2.20.0"

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import copy
import dataclasses as dc
import datetime
import enum
import re
import sys
@@ -8,9 +10,10 @@ import time
import uuid
from collections.abc import Sequence
from pprint import pprint
from typing import Any, Protocol, TypeAlias, TypedDict
from typing import Annotated, Any, Protocol, TypeAlias, TypedDict
import babel
from pydantic import BaseModel, ConfigDict, EmailStr, Field, ValidationError, field_validator
from PySide6.QtCore import QDate, QModelIndex, QStringListModel, Qt, QTimer, Signal
from PySide6.QtGui import QAction, QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
@@ -83,7 +86,137 @@ def get_country_list_german() -> CountryList:
)
def get_list_germany_states() -> CountryList:
states: list[tuple[str, str]] = []
short_code_to_name: dict[str, str] = {}
STATE_LIST: list[tuple[str, str]] = [
("Bayern", "BY"),
("Niedersachen", "NI"),
("Baden-Württemberg", "BW"),
("Berlin", "BE"),
("Brandenburg", "BB"),
("Bremen", "HB"),
("Hamburg", "HH"),
("Hessen", "HE"),
("Mecklenburg", "MV"),
("Nordrhein-Westfalen", "NW"),
("Rheinland-Pfalz", "RP"),
("Saarland", "SL"),
("Sachsen", "SN"),
("Sachsen-Anhalt", "ST"),
("Schleswig-Holstein", "SH"),
("Thüringen", "TH"),
]
STATE_LIST.sort(key=lambda x: x[1])
for iso_code, country_name in STATE_LIST:
states.append((country_name, iso_code))
short_code_to_name[iso_code] = country_name
return CountryList(
iso_to_country=short_code_to_name,
for_dropdown=tuple(states),
)
COUNTRY_LIST = get_country_list_german()
GERMAN_STATE_LIST = get_country_list_german()
def pprint_registry(widget_registry: WidgetRegistry) -> None:
print("---\n\n>>> Widget registry:")
for key, entry in widget_registry.items():
print(f"Key: {key}")
print(f"\twidget: {entry['widget']}")
print(f"\tfield key: {entry['form_field'].key}")
print(f"\tfield type: {entry['form_field'].type}")
class Grunderfassung_Unternehmen(BaseModel):
Projektrelevanz: Grunderfassung_Projektrelevanz
Kontaktperson: Grunderfassung_Kontaktperson
Stammdaten: Grunderfassung_Stammdaten
class Grunderfassung_Projektrelevanz(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
Projektrelevanz_relevanz: bool
@field_validator("Projektrelevanz_relevanz", mode="before")
@classmethod
def str_to_bool(cls, value: Any) -> Any:
if isinstance(value, str):
value = value.strip().lower()
if value == "ja":
return True
elif value == "nein":
return False
raise ValueError("Wert muss 'ja', 'nein', True oder False sein.")
return value
class Grunderfassung_Kontaktperson(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
KP_name_partner: str | None
KP_titel: str | None
KP_anrede_anschrift: str | None
KP_name: str | None
KP_vorname: str | None
KP_festnetznummer: str | None
KP_mobilfunknummer: str | None
KP_email: EmailStr | None
KP_funktion_beziehung: str | None
KP_adresse: str | None
ValidAge = Annotated[int, Field(ge=0, le=99)]
class Grunderfassung_Stammdaten(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
Stammdaten_titel: str | None
Stammdaten_anrede_anschrift: str
Stammdaten_name: str
Stammdaten_vorname: str | None
Stammdaten_geburtsdatum: datetime.date | None
Stammdaten_herkunftsland: str
Stammdaten_staatsangehoerigkeit: str | None
Stammdaten_rueckkehrer: bool | None
Stammdaten_aufenthaltsort: str | None
Stammdaten_strasse: str | None
Stammdaten_hausnummer: str | None
Stammdaten_PLZ: str | None
Stammdaten_ort: str | None
Stammdaten_bundesland: str | None
Stammdaten_land: str | None
Stammdaten_festnetznummer: str | None
Stammdaten_mobilfunknummer: str | None
Stammdaten_email: EmailStr | None
Stammdaten_familienstand: str | None
Stammdaten_anzahl_kinder: int | None
Stammdaten_alter_kinder: list[ValidAge] = Field(default_factory=list)
@field_validator("Stammdaten_rueckkehrer", mode="before")
@classmethod
def str_to_bool(cls, value: Any) -> Any:
if isinstance(value, str):
value = value.strip().lower()
if value == "ja":
return True
elif value == "nein":
return False
raise ValueError("Wert muss 'ja', 'nein', True oder False sein.")
return value
class CompanyForm_Search(QWidget):
@@ -302,6 +435,7 @@ class FormFieldType(enum.StrEnum):
DROPDOWN = enum.auto()
EXTENDED_DROPDOWN = enum.auto()
DYNAMIC_LIST = enum.auto()
DYNAMIC_DROPDOWN = enum.auto()
@dc.dataclass(slots=True)
@@ -334,6 +468,7 @@ class FormField:
dropdown_options: Sequence[DropdownOption] = dc.field(default=tuple(), init=False)
key: str = ""
tooltip: str = ""
init_label: str = dc.field(init=False)
def __post_init__(
self,
@@ -343,6 +478,7 @@ class FormField:
self.key = str(uuid.uuid4())
self.label = self.label.strip()
self.init_label = self.label.replace("*", "").replace(":", "")
if not self.label.endswith(":") and self.type is not FormFieldType.GROUP:
self.label += ":"
if self.required:
@@ -361,6 +497,32 @@ class FormField:
for child in self.children:
child.parent = self
def enhanced_label(
self,
add_text: str,
) -> str:
enhanced_label = self.init_label + f" {add_text}"
if not enhanced_label.endswith(":") and self.type is not FormFieldType.GROUP:
enhanced_label += ":"
if self.required:
enhanced_label += "*"
return enhanced_label
def enhanced_label(
base_label: str,
add_text: str,
add_colon: bool = False,
) -> str:
label = base_label.strip().replace("*", "").replace(":", "")
if add_text:
label = label + f" {add_text}"
if add_colon and not label.endswith(":"):
label += ":"
return label
class WidgetRegistryEntry(TypedDict):
widget: QWidget
@@ -475,7 +637,7 @@ FORM_FIELDS_CONTACT_PERSON = [
FormField(
"Name Unternehmen/Netzwerkpartner (pre-filled von Suche)",
FormFieldType.TEXT,
key="t1",
key="KP_name_partner",
required=False,
placeholder="Text wird nach gewähltem Unternehmen angezeigt",
readonly=True,
@@ -483,7 +645,7 @@ FORM_FIELDS_CONTACT_PERSON = [
FormField(
"Titel",
FormFieldType.TEXT,
key="t2",
key="KP_titel",
required=False,
tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person "
@@ -493,49 +655,49 @@ FORM_FIELDS_CONTACT_PERSON = [
FormField(
"Anrede_Anschrift",
FormFieldType.TEXT,
key="t3",
key="KP_anrede_anschrift",
required=True,
),
FormField(
"Name",
FormFieldType.TEXT,
key="t4",
key="KP_name",
required=True,
),
FormField(
"Vorname",
FormFieldType.TEXT,
key="t5",
key="KP_vorname",
required=False,
),
FormField(
"Festnetznummer",
FormFieldType.TEXT,
key="t6",
key="KP_festnetznummer",
required=False,
),
FormField(
"Mobilfunknummer",
FormFieldType.TEXT,
key="t7",
key="KP_mobilfunknummer",
required=False,
),
FormField(
"E-Mail",
FormFieldType.TEXT,
key="t8",
key="KP_email",
required=False,
),
FormField(
"Funktion/Beziehung zur beratenden Person",
FormFieldType.TEXT,
key="t9",
key="KP_funktion_beziehung",
required=False,
),
FormField(
"Adresse",
FormFieldType.LONGTEXT,
key="t10",
key="KP_adresse",
required=False,
),
]
@@ -544,6 +706,7 @@ FORM_FIELDS_MASTER_DATA = [
FormField(
"Titel",
FormFieldType.TEXT,
key="Stammdaten_titel",
required=False,
tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person "
@@ -553,21 +716,25 @@ FORM_FIELDS_MASTER_DATA = [
FormField(
"Anrede",
FormFieldType.TEXT,
key="Stammdaten_anrede_anschrift",
required=True,
),
FormField(
"Name",
FormFieldType.TEXT,
key="Stammdaten_name",
required=True,
),
FormField(
"Vorname",
FormFieldType.TEXT,
key="Stammdaten_vorname",
required=False,
),
FormField(
"Geburtsdatum",
FormFieldType.DATE,
key="Stammdaten_geburtsdatum",
required=False,
tooltip=(
"* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt "
@@ -576,21 +743,26 @@ FORM_FIELDS_MASTER_DATA = [
),
FormField(
"Herkunftsland",
FormFieldType.DROPDOWN,
FormFieldType.EXTENDED_DROPDOWN,
key="Stammdaten_herkunftsland",
required=True,
options=[("LÄNDERLISTE NOCH ZU ERGÄNZEN", None)],
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Staatsangehörigkeit",
FormFieldType.DROPDOWN,
FormFieldType.EXTENDED_DROPDOWN,
key="Stammdaten_herkunftsland",
required=False,
options=[("LÄNDERLISTE NOCH ZU ERGÄNZEN", None)],
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Rückkehrer",
FormFieldType.DROPDOWN,
key="Stammdaten_rueckkehrer",
required=False,
options=[("ja", None), ("nein", None)],
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
@@ -598,34 +770,40 @@ FORM_FIELDS_MASTER_DATA = [
FormField(
"Wo befindet sich die Person?",
FormFieldType.DROPDOWN,
key="Stammdaten_aufenthaltsort",
required=True,
options=[("Inland", None), ("Ausland EU/EWR", None), ("Ausland Drittstaat", None)],
),
FormField(
"Straße",
FormFieldType.TEXT,
key="Stammdaten_strasse",
required=False,
),
FormField(
"Hausnummer",
FormFieldType.TEXT,
key="Stammdaten_hausnummer",
required=False,
),
FormField(
"PLZ",
FormFieldType.TEXT,
key="Stammdaten_PLZ",
required=False,
),
FormField(
"Ort",
FormFieldType.TEXT,
key="Stammdaten_ort",
required=False,
),
FormField(
"Bundesland",
FormFieldType.DROPDOWN,
key="Stammdaten_bundesland",
required=False,
options=[("BUNDESLÄNDER NOCH ZU ERGÄNZEN", None)],
options=GERMAN_STATE_LIST.for_dropdown,
tooltip=(
"nur wenn Inland angegeben und die Angabe zieht es in keine Dokumente "
"rüber! Liste Bundesländer verwenden"
@@ -634,35 +812,63 @@ FORM_FIELDS_MASTER_DATA = [
FormField(
"Land",
FormFieldType.TEXT,
key="Stammdaten_land",
required=False,
),
FormField(
"Festnetznummer",
FormFieldType.TEXT,
key="Stammdaten_festnetznummer",
required=False,
),
FormField(
"Mobilfunknummer",
FormFieldType.TEXT,
key="Stammdaten_mobilfunknummer",
required=False,
),
FormField(
"E-Mail",
FormFieldType.TEXT,
key="Stammdaten_email",
required=False,
),
FormField(
"Familienstand",
FormFieldType.TEXT,
key="Stammdaten_familienstand",
required=False,
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
),
# FormField(
# "Anzahl Kinder",
# FormFieldType.DYNAMIC_DROPDOWN,
# key="Stammdaten_anzahl_kinder",
# required=False,
# options=[(str(x), None) for x in range(11)],
# tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
# ),
FormField(
"Anzahl Kinder",
FormFieldType.DROPDOWN,
FormFieldType.DYNAMIC_DROPDOWN,
required=False,
options=[(str(x), None) for x in range(11)],
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
key="Stammdaten_anzahl_kinder",
children=[
FormField(
"Anzahl Kinder",
FormFieldType.DROPDOWN,
required=False,
options=[(str(x), None) for x in range(11)],
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
key="Stammdaten_anzahl_kinder",
children=[
FormField(
"Alter Kind", FormFieldType.TEXT, key="Stammdaten_alter_kinder"
),
],
),
],
),
]
@@ -736,25 +942,21 @@ FORM_FIELDS_ADDITIONAL_DATA = [
]
FORM_FIELDS_SCHOOL = [
FormField("Abschluss", FormFieldType.TEXT, required=False, key="abschluss"),
FormField(
"Abschluss",
FormFieldType.TEXT,
required=True,
),
FormField(
"Abschlussgrad laut Dokument",
FormFieldType.TEXT,
required=False,
"Abschlussgrad laut Dokument", FormFieldType.TEXT, required=False, key="abschlussgrad"
),
FormField(
"Schule",
FormFieldType.TEXT,
required=False,
key="schule",
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
key="ort",
),
FormField(
"Land",
@@ -764,15 +966,12 @@ FORM_FIELDS_SCHOOL = [
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
),
FormField(
"Abschlussjahr",
FormFieldType.TEXT,
required=False,
),
FormField("Abschlussjahr", FormFieldType.TEXT, required=False, key="abschlussjahr"),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
key="bemerkung",
),
]
@@ -946,12 +1145,12 @@ FORM_FIELDS = [
FormField(
"Status && Projektrelevanz",
FormFieldType.GROUP,
key="state_relevance",
key="Projektrelevanz",
children=[
FormField(
"Projektrelevanz",
FormFieldType.DROPDOWN,
key="projektrelevanz",
key="Projektrelevanz_relevanz",
required=True,
options=[("ja", None), ("nein", None)],
),
@@ -960,30 +1159,57 @@ FORM_FIELDS = [
FormField(
"Daten Kontaktperson",
FormFieldType.GROUP,
key="data_contact_person",
key="Kontaktperson",
children=FORM_FIELDS_CONTACT_PERSON,
),
FormField(
"Stammdaten",
FormFieldType.GROUP,
key="Stammdaten",
children=FORM_FIELDS_MASTER_DATA,
),
FormField(
"Schulbildung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_SCHOOL,
key="Schulbildung",
),
FormField(
"Test Länderauswahl",
FormFieldType.GROUP,
key="countries",
children=[
FormField(
"Länderauswahl",
FormFieldType.EXTENDED_DROPDOWN,
key="country",
required=True,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
),
],
),
# FormField(
# "Test Länderauswahl",
# FormFieldType.GROUP,
# key="countries",
# children=[
# FormField(
# "Länderauswahl",
# FormFieldType.EXTENDED_DROPDOWN,
# key="country",
# required=True,
# placeholder="Suche...",
# options=COUNTRY_LIST.for_dropdown,
# ),
# ],
# ),
# FormField(
# "Anzahl Kinder (dynamischer Dropdown)",
# FormFieldType.DYNAMIC_DROPDOWN,
# required=False,
# options=[(str(x), None) for x in range(11)],
# tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
# key="DynamicDropdown",
# children=[
# FormField(
# "Anzahl Kinder",
# FormFieldType.DROPDOWN,
# required=False,
# options=[(str(x), None) for x in range(11)],
# tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
# key="MainDropdown",
# children=[
# FormField("Alter Kind", FormFieldType.TEXT),
# ],
# ),
# ],
# ),
# FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON),
# FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA),
# FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA),
@@ -1184,6 +1410,18 @@ def _build_ui_recursively(
}
parent_layout.addRow(widget)
case FormFieldType.DYNAMIC_DROPDOWN:
widget = DynamicDropdownWidget(
field.children,
field.label,
prefix=f"{full_key}",
)
widget_registry[full_key] = {
"widget": widget,
"form_field": field,
}
parent_layout.addRow(widget)
case _:
raise NotImplementedError(f"Not supported field type: {field.type.value}")
@@ -1240,6 +1478,9 @@ def reset_form(
elif isinstance(widget, DynamicListWidget):
# dynamic list widget manages its widgets by itself
widget.reset_form()
elif isinstance(widget, DynamicDropdownWidget):
# dynamic list widget manages its widgets by itself
widget.reset_form()
widget.setStyleSheet("")
@@ -1255,6 +1496,31 @@ def _insert_nested(
target_dict[key_path[-1]] = value
def update_sub_forms(
widget_registry: WidgetRegistry,
sub_forms: Sequence[SubForm],
base_label: str = "",
):
total_num_sub_forms = len(sub_forms)
for index, sub_form in enumerate(sub_forms, start=1):
if isinstance(sub_form.entry_box, QGroupBox) and base_label:
sub_form.entry_box.setTitle(f"{base_label} {index}")
change_sub_form_widget_registry(
widget_registry,
sub_form,
index,
)
for key in tuple(widget_registry.keys()):
matches = DYNAMIC_LIST_KEY_PATTERN.search(key)
if not matches:
continue
counter_sub_form = int(matches.group(1))
if counter_sub_form > total_num_sub_forms:
del widget_registry[key]
def get_form_data(
widget_registry: WidgetRegistry,
) -> dict[str, Any]:
@@ -1277,6 +1543,15 @@ def get_form_data(
# of such dictionaries
form_data = widget.get_form_data()
value = [val for val in form_data.values()]
# print(">>>>>>>>> Form Data:")
# pprint(form_data)
elif isinstance(widget, DynamicDropdownWidget):
# this should be a list: each dynamic list contains a list
# of such dictionaries
form_data = widget.get_form_data()
value = [val for val in form_data.values()]
# print(">>>>>>>>> Form Data:")
# pprint(form_data)
_insert_nested(raw_data, key.split("."), value)
@@ -1459,6 +1734,16 @@ class AutoForm(QWidget):
print("Erfolg! Alle Daten sind valide.")
print("Get form data call...")
form_data = self.get_form_data()
post_proc_1 = form_data["Stammdaten"]["Stammdaten_anzahl_kinder"]
Stammdaten_anzahl_kinder = post_proc_1[0]["Stammdaten_anzahl_kinder-[0]"][
"'Stammdaten_anzahl_kinder'"
]
Stammdaten_alter_kinder: list[str] = []
if len(post_proc_1) > 1:
for i in range(1, len(post_proc_1)):
content = post_proc_1[i]
print("------------>>>>>>>>> Get form data")
pprint(form_data)
# ------------------------------------------------------------
@@ -1478,7 +1763,7 @@ class AutoForm(QWidget):
@dc.dataclass(slots=True)
class SubForm:
entry_box: QGroupBox
entry_box: QWidget
prefix_parent: str
index: int
prefix: str = ""
@@ -1529,6 +1814,7 @@ class DynamicListWidget(QWidget):
super().__init__()
self.form_fields = form_fields
self.label = label
self.base_label = enhanced_label(label, add_text="")
self.prefix = prefix
self.widget_registry: WidgetRegistry = {}
@@ -1551,6 +1837,8 @@ class DynamicListWidget(QWidget):
self.inner_layout.addWidget(self.add_btn)
self.widget_registry_base_size = len(self.widget_registry)
# add empty sub form as initial value
self.add_entry()
@@ -1590,13 +1878,13 @@ class DynamicListWidget(QWidget):
self.update_sub_forms()
def update_sub_forms(self):
for index, sub_form in enumerate(self.sub_forms, start=1):
sub_form.entry_box.setTitle(f"{self.label} {index}")
change_sub_form_widget_registry(
self.widget_registry,
sub_form,
index,
)
update_sub_forms(
self.widget_registry,
sub_forms=self.sub_forms,
base_label=self.base_label,
)
# pprint_registry(self.widget_registry)
def reset_form(self) -> None:
reset_form(self.widget_registry)
@@ -1614,6 +1902,146 @@ class DynamicListWidget(QWidget):
return raw_data
class DynamicDropdownWidget(QWidget):
"""
A Widget, which can generate and manage an arbitrary number of sub forms with additional
information on a combobox selection (integer in combobox).
"""
def __init__(
self,
form_fields: Sequence[FormField],
label_add_info: str = "Eintrag",
prefix: str = "",
):
super().__init__()
# form_fields = form_fields.children
if len(form_fields) == 0 or len(form_fields) > 1:
raise ValueError(
"Dynamic Dropdown Widget must have only one child, which is a dropdown widget"
)
self.combobox_field = form_fields[0]
assigned_form_fields = self.combobox_field.children
if len(assigned_form_fields) == 0 or len(assigned_form_fields) > 1:
raise ValueError(
(
"Dynamic Dropdown Widget's dropdown element must have only one "
"child, which is a single field definition"
)
)
self.assigned_form_field = assigned_form_fields[0]
self.label_add_info = label_add_info
self.prefix = prefix
self.widget_registry: WidgetRegistry = {}
# layout for group component
# self.group_box = QGroupBox(label)
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout = QFormLayout()
self.main_layout.addLayout(self.form_layout)
_build_ui_recursively(
[self.combobox_field],
self.form_layout,
self.widget_registry,
prefix=f"{self.prefix}-[0]",
)
dropdown_widget_entry = tuple(self.widget_registry.values())[0]
dropdown_widget = dropdown_widget_entry["widget"]
assert isinstance(dropdown_widget, QComboBox)
self.dropdown_widget = dropdown_widget
self.rows_container = QWidget()
self.rows_layout = QVBoxLayout(self.rows_container)
self.rows_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.addWidget(self.rows_container)
self.sub_forms: list[SubForm] = []
self.dropdown_widget.currentTextChanged.connect(self.on_anzahl_changed)
self.widget_registry_base_size = len(self.widget_registry)
def on_anzahl_changed(
self,
text: str,
) -> None:
target_count: int
if text == DROPDOWN_DEFAULT:
target_count = 0
else:
target_count = int(text)
current_count = len(self.sub_forms)
if target_count > current_count:
differenz = target_count - current_count
for _ in range(differenz):
self._add_row()
elif target_count < current_count:
differenz = current_count - target_count
for _ in range(differenz):
self._remove_row()
def _add_row(self) -> None:
number_form = len(self.sub_forms) + 1
container = QWidget()
container.setContentsMargins(0, 0, 0, 0)
form_layout = QFormLayout(container)
form_layout.setContentsMargins(10, 0, 0, 0)
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
form_field_def = copy.copy(self.assigned_form_field)
form_field_def.label = form_field_def.enhanced_label(f"{number_form}")
_build_ui_recursively(
schema=[form_field_def],
parent_layout=form_layout,
widget_registry=self.widget_registry,
prefix=f"{self.prefix}-[{number_form}]",
)
self.rows_layout.addWidget(container)
self.sub_forms.append(sub_form)
self.update_sub_forms()
def _remove_row(self) -> None:
last_form = self.sub_forms.pop()
box_to_remove = last_form.entry_box
self.rows_layout.removeWidget(box_to_remove)
box_to_remove.deleteLater()
self.update_sub_forms()
def update_sub_forms(self) -> None:
update_sub_forms(
self.widget_registry,
sub_forms=self.sub_forms,
)
# pprint_registry(self.widget_registry)
def reset_form(self) -> None:
# resets dynamic content when dropdown is set back to default value
self.dropdown_widget.setCurrentIndex(0)
def validate_form_data(self) -> list[str]:
return validate_form_data(self.widget_registry)
def load_form_data(self) -> None:
# TODO add way to load data when initialised (probably with click)
...
def get_form_data(self):
raw_data = get_form_data(self.widget_registry)
return raw_data
class ClickableCell(QFrame):
"""cell in the table on the startup screen"""

View File

@@ -3,10 +3,74 @@ import dataclasses as dc
import enum
import re
from collections.abc import Sequence
from typing import Any
import babel
from PySide6.QtCore import QDate, Qt
# %%
DYNAMIC_LIST_KEY_PATTERN = re.compile(r"-\[(\d+)\]")
dynamic_content = {
"Stammdaten_anzahl_kinder-[0]": {"Stammdaten_anzahl_kinder": "5"},
"Stammdaten_anzahl_kinder-[1]": {"Stammdaten_alter_kinder": "23213"},
"Stammdaten_anzahl_kinder-[2]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[3]": {"Stammdaten_alter_kinder": "123213"},
"Stammdaten_anzahl_kinder-[4]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[5]": {"Stammdaten_alter_kinder": "123123"},
}
def find_dynamic_content(content: dict[str, Any]) -> dict[str, Any] | None:
found = None
for key in dynamic_content.keys():
if DYNAMIC_LIST_KEY_PATTERN.search(key):
# found an match: this is dynamic content dictionary
print("found")
found = dynamic_content
break
return found
# %%
new_content = {
"Stammdaten": {
"Stammdaten_PLZ": "",
"Stammdaten_anrede_anschrift": "asdasdas",
"Stammdaten_anzahl_kinder": [
{
"Stammdaten_anzahl_kinder-[0]": {"Stammdaten_anzahl_kinder": "5"},
"Stammdaten_anzahl_kinder-[1]": {"Stammdaten_alter_kinder": "23213"},
"Stammdaten_anzahl_kinder-[2]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[3]": {"Stammdaten_alter_kinder": "123213"},
"Stammdaten_anzahl_kinder-[4]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[5]": {"Stammdaten_alter_kinder": "123123"},
}
],
}
}
def flat_dict(contents):
for x in contents:
if isinstance(contents, dict):
yield from flat_dict(tuple(contents[x]))
elif isinstance(x, (list, tuple, set)):
yield from flat_dict(x)
else:
yield x
# %%
for x in flat_dict(new_content):
print(x)
# %%
find_dynamic_content(dynamic_content)
# %%
@dc.dataclass(slots=True)

View File

@@ -5,7 +5,7 @@ description = "GUI for CRM of NAFKA project with WCE"
authors = [
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
]
dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0", "sqlalchemy>=2.0.49", "polars>=1.40.1", "dopt-basics>=0.2.4", "pydantic>=2.13.4", "babel>=2.18.0"]
dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0", "sqlalchemy>=2.0.49", "polars>=1.40.1", "dopt-basics>=0.2.4", "pydantic[email]>=2.13.4", "babel>=2.18.0"]
requires-python = "<3.14,>=3.11"
readme = "README.md"
license = {text = "LicenseRef-Proprietary"}