From 249449cf592c05c12441776ccbefc93ae7783fdc Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 18 May 2026 16:53:39 +0200 Subject: [PATCH] better automatic export, prepare data validation for backend export --- pdm.lock | 44 +++- prototypes/t_qt_2.py | 542 ++++++++++++++++++++++++++++++++++++++----- prototypes/tests.py | 64 +++++ pyproject.toml | 2 +- 4 files changed, 593 insertions(+), 59 deletions(-) diff --git a/pdm.lock b/pdm.lock index 4ff5697..59a9d73 100644 --- a/pdm.lock +++ b/pdm.lock @@ -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" diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index 9620755..ebe20ee 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -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""" diff --git a/prototypes/tests.py b/prototypes/tests.py index ea3f451..135f0ae 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index a966567..8810bdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"}