From 807d07f28e3034c772dd3d6df15c5f7c2715a656 Mon Sep 17 00:00:00 2001 From: foefl Date: Thu, 21 May 2026 16:48:46 +0200 Subject: [PATCH] using flattening --- prototypes/t_qt_2.py | 930 +++++++++++++++++-------------------------- prototypes/tests.py | 243 ++++++++++- 2 files changed, 596 insertions(+), 577 deletions(-) diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index f39e272..c9aff69 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -4,46 +4,42 @@ import copy import dataclasses as dc import datetime import enum +import logging import pickle import re import sys import time import uuid -from collections.abc import Sequence +from collections.abc import Container, Sequence from pathlib import Path -from pprint import pprint +from pprint import pformat, pprint from typing import Annotated, Any, Final, Protocol, TypeAlias, TypedDict, cast from typing_extensions import override import babel +from dopt_basics.logging import BASE_LOGGER, setup_logging from pydantic import BaseModel, ConfigDict, EmailStr, Field, ValidationError, field_validator from PySide6.QtCore import ( QDate, QEvent, - QModelIndex, QObject, - QStringListModel, Qt, QTimer, Signal, ) -from PySide6.QtGui import QAction, QStandardItem, QStandardItemModel +from PySide6.QtGui import QAction from PySide6.QtWidgets import ( QApplication, QComboBox, QCompleter, QDateEdit, - QDialog, - QDialogButtonBox, QFormLayout, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QLayout, QLineEdit, - QListWidget, QMainWindow, QMessageBox, QPlainTextEdit, @@ -57,6 +53,18 @@ from PySide6.QtWidgets import ( from wce_crm.backend import initial_recording as be_init_rec +setup_logging(enable_stderr=True) + +DEBUG: Final[bool] = True +DEBUG_SEARCH_WIDGET: Final[bool] = True + +logger = BASE_LOGGER.getChild("wce") +logger.setLevel(logging.DEBUG) +logger_search_widget = logger.getChild("search_widget") +logger_search_widget.setLevel(logging.DEBUG) +logger_get_data = logger.getChild("get_data") +logger_get_data.setLevel(logging.DEBUG) + QSS = """ *[styleClass="stempel"] { background-color: #f1f5f9; @@ -70,8 +78,9 @@ QSS = """ border: 1px dashed #cbd5e1; } """ -DROPDOWN_DEFAULT: str = "--- Bitte wählen ---" -DYNAMIC_LIST_KEY_PATTERN = re.compile(r"-\[(\d+)\]") +DROPDOWN_DEFAULT: Final[str] = "--- Bitte wählen ---" +DYNAMIC_LIST_KEY_PATTERN: Final[re.Pattern] = re.compile(r"-\[(\d+)\]") +COLUMN_SEP: Final[str] = "__" def save_pydantic_model_dict( @@ -159,8 +168,8 @@ def get_list_germany_states() -> CountryList: ) -COUNTRY_LIST = get_country_list_german() -GERMAN_STATE_LIST = get_list_germany_states() +COUNTRY_LIST: Final[CountryList] = get_country_list_german() +GERMAN_STATE_LIST: Final[CountryList] = get_list_germany_states() def get_leafs(data): @@ -337,7 +346,7 @@ def _build_ui_recursively( ) -> None: no_scroll_filter = NoScrollFilter(parent_layout) for field in schema: - full_key = f"{prefix}.{field.key}" if prefix else field.key + full_key = f"{prefix}{COLUMN_SEP}{field.key}" if prefix else field.key widget: QWidget match field.type: @@ -660,6 +669,45 @@ def _get_nested( return current +@dc.dataclass(slots=True) +class SubForm: + entry_box: QWidget + prefix_parent: str + index: int + prefix: str = "" + + def __post_init__(self) -> None: + self.prefix = f"{self.prefix_parent}-[{self.index}]" + + def update_index(self, new_idx: int) -> None: + self.index = new_idx + self.prefix = f"{self.prefix_parent}-[{self.index}]" + + +def change_sub_form_widget_registry( + widget_registry: WidgetRegistry, + sub_form: SubForm, + new_idx: int, +) -> None: + if sub_form.index == new_idx: + return + + old_key_part = sub_form.prefix + new_key_part = f"{sub_form.prefix_parent}-[{new_idx}]" + + for key in tuple(widget_registry.keys()): + splitted = key.split(COLUMN_SEP) + key_part, rest = splitted[0], splitted[1:] + + if key_part == old_key_part: + old_key = COLUMN_SEP.join([old_key_part] + rest) + new_key = COLUMN_SEP.join([new_key_part] + rest) + widget_registry[new_key] = widget_registry[old_key] + del widget_registry[key] + + sub_form.update_index(new_idx) + + def update_sub_forms( widget_registry: WidgetRegistry, sub_forms: Sequence[SubForm], @@ -687,11 +735,17 @@ def update_sub_forms( def get_form_data( widget_registry: WidgetRegistry, + filter_keys: Container[str] = tuple(), ) -> dict[str, Any]: raw_data = {} for key, registry_entry in widget_registry.items(): value: Any | None = None + if filter_keys: + data_key = key.split(COLUMN_SEP)[-1] + if data_key not in filter_keys: + continue + widget = registry_entry["widget"] if isinstance(widget, QLineEdit): data = widget.text() @@ -712,20 +766,80 @@ def get_form_data( form_data = widget.get_form_data() value = [val for val in form_data.values()] # print(">>>>>>>>> Key: ", key) - # print(">>>>>>>>> Form Data:") - # pprint(form_data) - elif isinstance(widget, DynamicDropdownWidget): + # print(">>>>>>>>> Form Data after call:") + # pprint(value) + elif isinstance(widget, (DynamicDropdownWidget, CustomWidget)): # this is a special data structure with some assumptions of the widget's structure value = widget.get_form_data() - # print(">>>>>>>>> Key: ", key) - # print(">>>>>>>>> Form Data after call:") - # pprint(form_data) + print(">>>>>>>>> Key: ", key) + print(">>>>>>>>> Form Data after call:") + pprint(value) - _insert_nested(raw_data, key.split("."), value) + # _insert_nested(raw_data, key.split(COLUMN_SEP), value) + logger_get_data.debug("Key: %s", key) + logger_get_data.debug("Value: %s", value) + + if isinstance(value, dict): + raw_data.update(value) + else: + raw_data[key] = value + + logger_get_data.debug(">>>>> RAW DATA:\n%s", pformat(raw_data)) return raw_data +def set_widget_value( + widget: QWidget, + value: Any, +) -> None: + if value is None: + return + elif value is True: + value = "ja" + elif value is False: + value = "nein" + + if isinstance(widget, QLineEdit): + widget.setText(str(value)) + + elif isinstance(widget, QPlainTextEdit): + widget.setPlainText(str(value)) + + elif isinstance(widget, QDateEdit): + assert isinstance(value, datetime.date) + set_date = QDate(value.year, value.month, value.day) + if not set_date.isValid(): + raise ValueError(f"Could not parse date field value >{value}<") + widget.setDate(set_date) + + elif isinstance(widget, QComboBox): + index = widget.findData(value) + if index >= 0: + widget.setCurrentIndex(index) + + elif isinstance(widget, DynamicListWidget): + assert isinstance(value, list) + widget.set_form_data(value) + + elif isinstance(widget, (DynamicDropdownWidget, Grunderfassung_SuchWidget)): + assert isinstance(value, dict) + widget.set_form_data(value) + + +def set_form_data( + widget_registry: WidgetRegistry, + data: dict[str, Any], +) -> None: + for key, entry in widget_registry.items(): + widget = entry["widget"] + + key_path = key.split(COLUMN_SEP) + value = _get_nested(data, key_path) + + set_widget_value(widget, value) + + def validate_form_data( widget_registry: WidgetRegistry, ) -> list[str]: @@ -794,14 +908,31 @@ def validate_form_data( class Grunderfassung_Unternehmen(BaseModel): - Projektrelevanz: Grunderfassung_Projektrelevanz | None = None - Kontaktperson: Grunderfassung_Kontaktperson | None = None - Stammdaten: Grunderfassung_Stammdaten | None = None - WeitereInfos: Grunderfassung_WeitereInfos | None = None - Schulbildung: list[Grunderfassung_Schulbildung] | None = None - HoehereBildung: list[Grunderfassung_HoehereBildung] | None = None - Arbeitserfahrung: list[Grunderfassung_Arbeitserfahrung] | None = None - Sprachkenntnisse: list[Grunderfassung_Sprachen] | None = None + Metadaten_erstellung: datetime.datetime | None = ( + None # default in SQLAlchemy with lambda and timezone-aware datetime) + ) + Metadaten_aktualisierung: datetime.datetime | None # see above + Metadaten_nutzer: str | None = None + Grunderfassung_fallnummer: str + Grunderfassung_notiz: str | None + + Partnersuche: Grunderfassung_PartnerSuche + Projektrelevanz: Grunderfassung_Projektrelevanz + Kontaktperson: Grunderfassung_Kontaktperson + Stammdaten: Grunderfassung_Stammdaten + WeitereInfos: Grunderfassung_WeitereInfos + Schulbildung: list[Grunderfassung_Schulbildung] + HoehereBildung: list[Grunderfassung_HoehereBildung] + Arbeitserfahrung: list[Grunderfassung_Arbeitserfahrung] + Sprachkenntnisse: list[Grunderfassung_Sprachen] + + +class Grunderfassung_PartnerSuche(BaseModel): + model_config = ConfigDict(str_strip_whitespace=True) + + PartnerSuche_un_suche: int + PartnerSuche_person_suche: int + PartnerSuche_kanal_aufmerksamkeit: str | None class Grunderfassung_Projektrelevanz(BaseModel): @@ -949,353 +1080,23 @@ class Grunderfassung_Sprachen(BaseModel): SP_datum_nachweis: datetime.date | None -class CompanyForm_Search_old(QWidget): - company_selected = Signal(int) +def postprocess_pydantic( + model: Grunderfassung_Unternehmen, +) -> dict[str, Any]: + logger.debug("Call to postprocessing of Pydantic model") - def __init__(self): - super().__init__() + CURRENT_USER = "TEST_USER" + model.Metadaten_nutzer = CURRENT_USER + flat_data: dict[str, Any] = {} + for data_dict in get_leaf_dicts(model.model_dump()): + flat_data.update(data_dict) - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - form_layout = QFormLayout() - form_layout.setSpacing(10) - # TODO: remove title - title = QLabel("--- Suche Unternehmen ---") - title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold; - main_layout.addWidget(title) - # --- SEARCH --- - self.search_input = QLineEdit(placeholderText="Tippen zum Suchen...") - form_layout.addRow("Suche:", self.search_input) - self.completer = QCompleter() - self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) - self.completer.setFilterMode(Qt.MatchFlag.MatchContains) - self.search_input.setCompleter(self.completer) - self.completer.activated[QModelIndex].connect(self.search_result_selected) # type: ignore - # --- FILLED FIELDS --- - # name - self.company_input = QLineEdit(placeholderText="Name des Partners") - form_layout.addRow("Name Unternehmen/Netzwerkpartner:", self.company_input) - # street - street_layout = QHBoxLayout() - street_layout.setContentsMargins(0, 0, 0, 0) # Wichtig: Verhindert doppelte Abstände! - street_layout.setSpacing(10) - self.street_input = QLineEdit(placeholderText="Straße") - self.number_input = QLineEdit(placeholderText="Nr.") - self.number_input.setMaximumWidth(80) - street_layout.addWidget(self.street_input, stretch=3) - street_layout.addWidget(self.number_input, stretch=1) - form_layout.addRow("Straße / Nr.:", street_layout) - # ZIP, city - # --- 3. Kombinierte Zeile: PLZ & Ort --- - city_layout = QHBoxLayout() - city_layout.setContentsMargins(0, 0, 0, 0) - city_layout.setSpacing(10) - self.zip_input = QLineEdit(placeholderText="PLZ") - self.city_input = QLineEdit(placeholderText="Ort") - self.zip_input.setMaximumWidth(100) # PLZ ist immer relativ kurz - city_layout.addWidget(self.zip_input, stretch=1) - city_layout.addWidget(self.city_input, stretch=3) - form_layout.addRow("PLZ / Ort:", city_layout) + logger.debug("Flatted data dict:\n%s", pformat(flat_data)) - # add fom layout - main_layout.addLayout(form_layout) - - self.autofilled_fields: tuple[QLineEdit, ...] = ( - self.company_input, - self.street_input, - self.number_input, - self.zip_input, - self.city_input, - ) - for field in self.autofilled_fields: - field.setReadOnly(True) - field.setProperty("styleClass", "stempel") - - self.update_search_data() - - def fill_out(self, comp_info: be_init_rec.CompanyInfo): - self.company_input.setText(comp_info["ma_unternehmensname"]) - self.street_input.setText(comp_info["ma_strasse"]) - self.number_input.setText(comp_info["ma_hausnummer"]) - self.zip_input.setText(comp_info["ma_plz"]) - self.city_input.setText(comp_info["ma_ort"]) - - def clear_autofilled_fields(self) -> None: - self.search_input.clear() - for field in self.autofilled_fields: - field.clear() - - def update_search_data(self) -> None: - self.clear_autofilled_fields() - search_items = QStandardItemModel() - search_choices = be_init_rec.comp_search_choices() - for item, db_index in search_choices: - qitem = QStandardItem(item) - qitem.setData(db_index, Qt.ItemDataRole.UserRole) - search_items.appendRow(qitem) - - self.completer.setModel(search_items) - - def search_result_selected(self, index: QModelIndex): - ma_id = index.data(Qt.ItemDataRole.UserRole) - comp_info = be_init_rec.comp_search_get_info( - ma_id=ma_id, - ) - self.fill_out(comp_info) - self.company_selected.emit(ma_id) + return flat_data -# class CompanyForm_Search_Data(TypedDict): -# ma_id: int -# an_id: int - - -# class Grunderfassung_Suche_old(QWidget): -# # company_selected = Signal(int) - -# def __init__( -# self, -# # form_fields: Sequence[FormField], -# label: str = "Suche", -# prefix: str = "", -# ): -# super().__init__() -# # self.form_fields = form_fields -# self.label = label -# self.prefix = prefix -# # self.widget_registry: WidgetRegistry = {} -# self.export_data: CompanyForm_Search_Data = {"ma_id": -1, "an_id": -1} -# self.PROPERTY_MA_ID = "user_ma_id" - -# main_layout = QVBoxLayout(self) -# main_layout.setContentsMargins(0, 0, 0, 0) -# form_layout = QFormLayout() -# form_layout.setSpacing(10) -# # TODO: remove title? -# title = QLabel(self.label) -# title.setStyleSheet("font-size: 14px; font-weight: bold;") -# main_layout.addWidget(title) -# # --- SEARCH --- -# self.company_search_input = QLineEdit(placeholderText="Tippen zum Suchen...") -# form_layout.addRow("Suche:", self.company_search_input) -# self.company_search_completer = QCompleter() -# self.company_search_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) -# self.company_search_completer.setFilterMode(Qt.MatchFlag.MatchContains) -# self.company_search_input.setCompleter(self.company_search_completer) -# self.company_search_completer.activated[QModelIndex].connect( # type: ignore -# self._selected_company_on_completer -# ) -# self.company_search_input.textChanged.connect(self._search_company_change_manually) -# # --- FILLED FIELDS --- -# # name -# self.company_name = QLineEdit(placeholderText="Name des Partners") -# form_layout.addRow("Name Unternehmen/Netzwerkpartner:", self.company_name) -# # street -# street_layout = QHBoxLayout() -# street_layout.setContentsMargins(0, 0, 0, 0) -# street_layout.setSpacing(10) -# self.company_street = QLineEdit(placeholderText="Straße") -# self.company_streetnumber = QLineEdit(placeholderText="Nr.") -# self.company_streetnumber.setMaximumWidth(80) -# street_layout.addWidget(self.company_street, stretch=3) -# street_layout.addWidget(self.company_streetnumber, stretch=1) -# form_layout.addRow("Straße / Nr.:", street_layout) -# # ZIP, city -# # --- 3. Kombinierte Zeile: PLZ & Ort --- -# city_layout = QHBoxLayout() -# city_layout.setContentsMargins(0, 0, 0, 0) -# city_layout.setSpacing(10) -# self.company_zip = QLineEdit(placeholderText="PLZ") -# self.company_city = QLineEdit(placeholderText="Ort") -# self.company_zip.setMaximumWidth(100) # PLZ ist immer relativ kurz -# city_layout.addWidget(self.company_zip, stretch=1) -# city_layout.addWidget(self.company_city, stretch=3) -# form_layout.addRow("PLZ / Ort:", city_layout) - -# # !! ------------------------------------------------------------------- -# # Integration of person search -# # TODO: remove title -# spacing_layout = QVBoxLayout() -# spacing_layout.addSpacing(15) -# form_layout.addRow(spacing_layout) - -# title = QLabel("--- Suche Nutzer ---") -# title.setStyleSheet("font-size: 14px; font-weight: bold;") -# main_layout.addWidget(title) -# # --- SEARCH --- -# self.person_search_input = QComboBox() -# self.person_search_input.setEditable(True) -# self.person_search_input.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) -# line_edit = self.person_search_input.lineEdit() -# assert line_edit -# line_edit.setPlaceholderText("Suchen...") -# # --- FILLED FIELDS --- -# form_layout.addRow("Suche Ansprechpartner:", self.person_search_input) -# self.person_completer = self.person_search_input.completer() -# assert self.person_completer -# self.person_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) -# self.person_completer.setFilterMode(Qt.MatchFlag.MatchContains) -# self.person_completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) -# # self.person_search_input.activated.connect(self._selected_person_on_completer) -# self.person_search_input.currentIndexChanged.connect( -# self._selected_person_on_completer -# ) -# # salutation -# self.person_titel = QLineEdit() -# self.person_anrede = QLineEdit() -# hor_layout = QHBoxLayout() -# hor_layout.addWidget(self.person_anrede, stretch=1) -# hor_layout.addWidget(self.person_titel, stretch=1) -# form_layout.addRow("Anrede / Titel:", hor_layout) -# # names -# self.person_nachname = QLineEdit() -# self.person_vorname = QLineEdit() -# hor_layout = QHBoxLayout() -# hor_layout.addWidget(self.person_nachname, stretch=1) -# hor_layout.addWidget(self.person_vorname, stretch=1) -# form_layout.addRow("Nachname / Vorname:", hor_layout) -# # phones -# phone_layout = QHBoxLayout() -# phone_layout.setContentsMargins(0, 0, 0, 0) -# phone_layout.setSpacing(10) -# self.person_landline_number = QLineEdit() -# self.person_mobile_number = QLineEdit() -# phone_layout.addWidget(self.person_landline_number, stretch=1) -# phone_layout.addWidget(self.person_mobile_number, stretch=1) -# form_layout.addRow("Telefon Festnetz / Mobil:", phone_layout) -# # additional -# self.person_email = QLineEdit() -# form_layout.addRow("E-Mail:", self.person_email) -# self.person_funktion = QLineEdit() -# form_layout.addRow("Funktion:", self.person_funktion) - -# # ------------------------------------------------------------------------ - -# # add fom layout -# main_layout.addLayout(form_layout) -# main_layout.addSpacing(20) -# main_layout.addWidget( -# QLabel('>>>> Platzhalter "Wie sind Sie auf uns aufmerksam geworden?"') -# ) - -# self.autofilled_fields: tuple[QLineEdit, ...] = ( -# self.company_name, -# self.company_street, -# self.company_streetnumber, -# self.company_zip, -# self.company_city, -# self.person_titel, -# self.person_anrede, -# self.person_nachname, -# self.person_vorname, -# self.person_landline_number, -# self.person_mobile_number, -# self.person_email, -# self.person_funktion, -# ) - -# for field in self.autofilled_fields: -# field.setReadOnly(True) -# field.setProperty("styleClass", "stempel") - -# self.update_company_data() -# # self.update_person_data(None) - -# def fill_out_company(self, comp_info: be_init_rec.CompanyInfo): -# self.company_name.setText(comp_info["ma_unternehmensname"]) -# self.company_street.setText(comp_info["ma_strasse"]) -# self.company_streetnumber.setText(comp_info["ma_hausnummer"]) -# self.company_zip.setText(comp_info["ma_plz"]) -# self.company_city.setText(comp_info["ma_ort"]) - -# def fill_out( -# self, -# info: be_init_rec.ContactPersonInfo, -# ) -> None: -# self.person_titel.setText(info["an_titel"]) -# self.person_anrede.setText(info["an_anrede"]) -# self.person_nachname.setText(info["an_nachname"]) -# self.person_vorname.setText(info["an_vorname"]) -# self.person_landline_number.setText(info["an_festnetz"]) -# self.person_mobile_number.setText(info["an_mobil"]) -# self.person_email.setText(info["an_mail"]) -# self.person_funktion.setText(info["an_position"]) - -# def clear_autofilled_fields(self) -> None: -# self.company_search_input.clear() -# self.person_search_input.clear() -# for field in self.autofilled_fields: -# field.clear() - -# def update_company_data(self) -> None: -# self.clear_autofilled_fields() -# search_items = QStandardItemModel() -# search_choices = be_init_rec.comp_search_choices() -# for item, db_index in search_choices: -# qitem = QStandardItem(item) -# qitem.setData(db_index, Qt.ItemDataRole.UserRole) -# search_items.appendRow(qitem) - -# self.company_search_completer.setModel(search_items) - -# def update_person_data( -# self, -# ma_id: int | None, -# ) -> None: -# self.clear_autofilled_fields() -# search_choices = be_init_rec.contact_person_search_choices(ma_id, True) -# for item, db_index in search_choices: -# self.person_search_input.addItem(item, db_index) -# self.person_search_input.setCurrentIndex(0) - -# def _selected_company_on_completer( -# self, -# index: QModelIndex, -# ) -> None: -# ma_id = index.data(Qt.ItemDataRole.UserRole) -# comp_info = be_init_rec.comp_search_get_info( -# ma_id=ma_id, -# ) -# self.company_search_input.setProperty(self.PROPERTY_MA_ID, ma_id) -# self.fill_out_company(comp_info) -# # self.company_selected.emit(ma_id) -# self.update_person_data(ma_id) - -# def _selected_person_on_completer( -# self, -# index: int, -# ): -# an_id = self.person_search_input.itemData(index) -# comp_info = be_init_rec.contact_person_search_get_info( -# an_id=an_id, -# ) -# self.fill_out(comp_info) - -# def _search_company_change_manually( -# self, -# _: str, -# ) -> None: -# self.company_search_input.setProperty(self.PROPERTY_MA_ID, -1) - -# def get_form_data(self) -> CompanyForm_Search_Data: -# ma_id = self.company_search_input.property(self.PROPERTY_MA_ID) -# self.export_data["ma_id"] = ma_id - -# return self.export_data - -# def set_form_data( -# self, -# ma_id: int, -# ) -> None: -# comp_info = be_init_rec.comp_search_get_info( -# ma_id=ma_id, -# ) -# self.company_search_input.setText(comp_info["ma_unternehmensname"]) -# self.fill_out_company(comp_info) -# # self.company_selected.emit(ma_id) - - -class Grunderfassung_Suche(CustomWidget): +class Grunderfassung_SuchWidget(CustomWidget): def __init__( self, form_fields: Sequence[FormField], @@ -1327,22 +1128,28 @@ class Grunderfassung_Suche(CustomWidget): self.prefix, ) - self.COMPANY_SEARCH_INPUT_KEY = "kontaktliste_un_suche" + self.COMPANY_SEARCH_INPUT_KEY = "PartnerSuche_un_suche" lookup = search_widgets_by_key(self.widget_registry, self.COMPANY_SEARCH_INPUT_KEY) assert len(lookup) == 1 self.company_search_input = cast(QComboBox, lookup[0]["widget"]) self.company_search_input.currentIndexChanged.connect(self._selected_company) - self.PERSON_SEARCH_INPUT_KEY = "kontaktliste_person_suche" + self.PERSON_SEARCH_INPUT_KEY = "PartnerSuche_person_suche" lookup = search_widgets_by_key(self.widget_registry, self.PERSON_SEARCH_INPUT_KEY) assert len(lookup) == 1 self.person_search_input = cast(QComboBox, lookup[0]["widget"]) self.person_search_input.currentIndexChanged.connect(self._selected_person) - self.CHANNEL_AWARENESS_KEY = "kontaktliste_kanal_aufmerksamkeit" + self.CHANNEL_AWARENESS_KEY = "PartnerSuche_kanal_aufmerksamkeit" lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_AWARENESS_KEY) assert len(lookup) == 1 - self.channel_awareness = lookup[0] + self.channel_awareness = cast(QComboBox, lookup[0]["widget"]) + + self.DATA_EXPORT_FIELDS = ( + self.COMPANY_SEARCH_INPUT_KEY, + self.PERSON_SEARCH_INPUT_KEY, + self.CHANNEL_AWARENESS_KEY, + ) self.company_widgets: dict[str, QWidget] = {} self.person_widgets: dict[str, QWidget] = {} @@ -1373,21 +1180,22 @@ class Grunderfassung_Suche(CustomWidget): elif field.info in self.properties_person: self.person_widgets[field.info] = widget - button = QPushButton("Print Registry") - button.clicked.connect(self.print_registry) - main_layout.addWidget(button) - button = QPushButton("Reset form") - button.clicked.connect(self.reset_form) - main_layout.addWidget(button) - button = QPushButton("Get form data") - button.clicked.connect(self.get_form_data) - main_layout.addWidget(button) - button = QPushButton("Set form data") - button.clicked.connect(self.set_form_data) - main_layout.addWidget(button) - button = QPushButton("Validate") - button.clicked.connect(self.validate_form_data) - main_layout.addWidget(button) + if DEBUG_SEARCH_WIDGET: + button = QPushButton("Print Registry") + button.clicked.connect(self.print_registry) + main_layout.addWidget(button) + button = QPushButton("Reset form") + button.clicked.connect(self.reset_form) + main_layout.addWidget(button) + button = QPushButton("Get form data") + button.clicked.connect(self.get_form_data) + main_layout.addWidget(button) + button = QPushButton("Set form data") + button.clicked.connect(self.set_form_data) + main_layout.addWidget(button) + button = QPushButton("Validate") + button.clicked.connect(self.validate_form_data) + main_layout.addWidget(button) self.update_company_data() @@ -1423,6 +1231,8 @@ class Grunderfassung_Suche(CustomWidget): set_widget_value(widget, data[key]) def _clear_company_fields(self) -> None: + self.person_search_input.clear() + self.channel_awareness.setCurrentIndex(0) for widget in self.company_widgets.values(): widget = cast(QLineEdit, widget) widget.clear() @@ -1462,7 +1272,6 @@ class Grunderfassung_Suche(CustomWidget): data = be_init_rec.comp_search_get_info( ma_id=ma_id, ) - # self.company_search_input.setProperty(self.PROPERTY_MA_ID, ma_id) self.fill_out_company(data) self.update_person_data(ma_id) @@ -1489,54 +1298,43 @@ class Grunderfassung_Suche(CustomWidget): @override def validate_form_data(self) -> list[str]: errors = validate_form_data(self.widget_registry) - print("\n>>>> Validation:") - pprint(errors) return errors @override def get_form_data(self) -> dict[str, Any]: - # TODO decide if value is checked here or later in the Pydantic validation - raw_data = get_form_data(self.widget_registry) - print("raw_data:\n") - pprint(raw_data) - # leaf dictionary contains data fields - data = tuple(get_leaf_dicts(raw_data))[0] - print("leaf dicts:\n") - pprint(data) + form_data = get_form_data(self.widget_registry, filter_keys=self.DATA_EXPORT_FIELDS) + logger_get_data.debug("\n\n\n------- Form data CustomWidget:\n%s", pformat(form_data)) - self.export_data[self.COMPANY_SEARCH_INPUT_KEY] = data[self.COMPANY_SEARCH_INPUT_KEY] - self.export_data[self.PERSON_SEARCH_INPUT_KEY] = data[self.PERSON_SEARCH_INPUT_KEY] - self.export_data[self.CHANNEL_AWARENESS_KEY] = data[self.CHANNEL_AWARENESS_KEY] + return form_data + # TODO check removal - print("\nExport data:") - pprint(self.export_data) + form_data = tuple(get_leaf_dicts(form_data))[0] - return self.export_data + if DEBUG_SEARCH_WIDGET: + self.export_data = form_data + logger_search_widget.debug( + "form_data Grunderfassung_SuchWidget:\n%s", pformat(form_data) + ) + + return form_data @override def set_form_data( self, data: dict[str, Any], ) -> None: - if not self.export_data: - raise RuntimeError() - data = self.export_data - ma_id = data[self.COMPANY_SEARCH_INPUT_KEY] - comp_info = be_init_rec.comp_search_get_info( - ma_id=ma_id, - ) - set_widget_value(self.company_search_input, ma_id) - self.fill_out_company(comp_info) - self.update_person_data(ma_id) - an_id = data[self.PERSON_SEARCH_INPUT_KEY] - pers_info = be_init_rec.contact_person_search_get_info( - an_id=an_id, - ) - set_widget_value(self.person_search_input, an_id) - self.fill_out_person(pers_info) - widget = cast(QComboBox, self.channel_awareness["widget"]) - value = data[self.CHANNEL_AWARENESS_KEY] - set_widget_value(widget, value) + if DEBUG_SEARCH_WIDGET: + if not self.export_data: + raise RuntimeError("No data. Get form data first.") + data = self.export_data + + formatted_data = {} + _insert_nested(formatted_data, self.prefix.split(COLUMN_SEP), data) + + if DEBUG_SEARCH_WIDGET: + logger_search_widget.debug("\nformatted data:\n%s", pformat(formatted_data)) + + set_form_data(self.widget_registry, formatted_data) class ContactPersonForm_Search(QWidget): @@ -1735,6 +1533,20 @@ class AutoForm(QWidget): # --- LAYOUT --- self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) + + if DEBUG: + self.test_button = QPushButton("Initialisiere Laden") + self.test_button.clicked.connect(self.on_load_clicked) + self.test_button.setFixedHeight(50) + self.test_button.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + self.main_layout.addWidget(self.test_button) + button = QPushButton("GET DATA") + button.clicked.connect(self.get_form_data) + self.main_layout.addWidget(button) + self.main_layout.addSpacing(10) + self.top_level_form_layout = QFormLayout() self.main_layout.addLayout(self.top_level_form_layout) self.top_level_form_layout.setSpacing(10) @@ -1773,12 +1585,8 @@ class AutoForm(QWidget): self.layout_btn.addWidget(self.reset_btn) # test button - self.main_layout.addSpacing(10) - self.test_button = QPushButton("Initialisiere Laden") - self.test_button.clicked.connect(self.on_load_clicked) - self.test_button.setFixedHeight(50) - self.test_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - self.main_layout.addWidget(self.test_button) + # self.main_layout.addSpacing(10) + # self.main_layout.addWidget(self.test_button) def _disable_save(self) -> None: self.save_btn.setEnabled(False) @@ -1796,19 +1604,8 @@ class AutoForm(QWidget): # TODO change logic to database backend print(">>>> LOAD CLICKED") loaded_data = load_pydantic_model_dict() - # print("Loaded dictionary:") - # pprint(loaded) - # print(">>>> Widget registry:") - # pprint_registry(self.widget_registry) - for key, entry in self.widget_registry.items(): - widget = entry["widget"] - - # get data from dictionary - key_path = key.split(".") - value = _get_nested(loaded_data, key_path) - - set_widget_value(widget, value) + self.set_form_data(loaded_data) def on_save_clicked(self) -> None: self._disable_save() @@ -1829,13 +1626,13 @@ class AutoForm(QWidget): print("Erfolg! Alle Daten sind valide.") print("Get form data call...") form_data = self.get_form_data() - print("------------>>>>>>>>> Get form data") - pprint(form_data) + logger.debug("------------>>>>>>>>> Get form data\n%s", pformat(form_data)) + logger.debug("\n------------>>>>>>>>> Call Pydantic") - print("\n------------>>>>>>>>> Call Pydantic") try: validated_data = Grunderfassung_Unternehmen(**form_data) - pprint(validated_data.model_dump()) + logger.debug("%s", pformat(validated_data.model_dump())) + postprocess_pydantic(validated_data) except ValidationError as e: # catch errors and show them in GUI fehler_texte = [] @@ -1874,96 +1671,16 @@ class AutoForm(QWidget): reset_form(self.widget_registry) def get_form_data(self) -> dict[str, Any]: + logger_get_data.debug("[AutoForm] Call get form data") raw_data = get_form_data(self.widget_registry) return raw_data - -def set_widget_value( - widget: QWidget, - value: Any, -) -> None: - if value is None: - return - elif value is True: - value = "ja" - elif value is False: - value = "nein" - - if isinstance(widget, QLineEdit): - widget.setText(str(value)) - - elif isinstance(widget, QPlainTextEdit): - widget.setPlainText(str(value)) - - elif isinstance(widget, QDateEdit): - assert isinstance(value, datetime.date) - set_date = QDate(value.year, value.month, value.day) - if not set_date.isValid(): - raise ValueError(f"Could not parse date field value >{value}<") - widget.setDate(set_date) - - elif isinstance(widget, QComboBox): - index = widget.findData(value) - if index >= 0: - widget.setCurrentIndex(index) - - elif isinstance(widget, DynamicListWidget): - # print("\n-----------------\n\nCustom widget...") - # print(f"Key: {key}") - # print("Widget type: ", type(widget).__name__) - # print("current value or data format: ") - # pprint(value) - assert isinstance(value, list) - widget.set_form_data(value) - - elif isinstance(widget, DynamicDropdownWidget): - # print("\n-----------------\n\nCustom widget, skip...") - # print(f"Key: {key}") - # print("Widget type: ", type(widget).__name__) - # print("current value or data format: ") - # pprint(value) - assert isinstance(value, dict) - widget.set_form_data(value) - - -@dc.dataclass(slots=True) -class SubForm: - entry_box: QWidget - prefix_parent: str - index: int - prefix: str = "" - - def __post_init__(self) -> None: - self.prefix = f"{self.prefix_parent}-[{self.index}]" - - def update_index(self, new_idx: int) -> None: - self.index = new_idx - self.prefix = f"{self.prefix_parent}-[{self.index}]" - - -def change_sub_form_widget_registry( - widget_registry: WidgetRegistry, - sub_form: SubForm, - new_idx: int, -) -> None: - if sub_form.index == new_idx: - return - - old_key_part = sub_form.prefix - new_key_part = f"{sub_form.prefix_parent}-[{new_idx}]" - - for key in tuple(widget_registry.keys()): - splitted = key.split(".") - key_part, rest = splitted[0], splitted[1:] - - if key_part == old_key_part: - old_key = ".".join([old_key_part] + rest) - new_key = ".".join([new_key_part] + rest) - widget_registry[new_key] = widget_registry[old_key] - del widget_registry[key] - - sub_form.update_index(new_idx) + def set_form_data( + self, + data: dict[str, Any], + ) -> None: + set_form_data(self.widget_registry, data) class DynamicListWidget(QWidget): @@ -1976,6 +1693,7 @@ class DynamicListWidget(QWidget): form_fields: Sequence[FormField], label: str = "Eintrag", prefix: str = "", + add_empty_entry: bool = False, ): super().__init__() self.form_fields = form_fields @@ -1995,7 +1713,7 @@ class DynamicListWidget(QWidget): # sub forms self.sub_forms: list[SubForm] = [] # button to add more sub forms - self.add_btn = QPushButton("+ Weitere hinzufügen") + self.add_btn = QPushButton("+ Hinzufügen") self.add_btn.setStyleSheet( "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" ) @@ -2006,7 +1724,9 @@ class DynamicListWidget(QWidget): self.widget_registry_base_size = len(self.widget_registry) # add empty sub form as initial value - self.add_entry() + self.add_empty_entry = add_empty_entry + if self.add_empty_entry: + self.add_entry() def add_entry(self): number_form = len(self.sub_forms) + 1 @@ -2056,15 +1776,20 @@ class DynamicListWidget(QWidget): while self.sub_forms: self.remove_entry(self.sub_forms[0]) - self.add_entry() + if self.add_empty_entry: + self.add_entry() def validate_form_data(self) -> list[str]: return validate_form_data(self.widget_registry) def get_form_data(self): - raw_data = get_form_data(self.widget_registry) + # raw_data = get_form_data(self.widget_registry) + form_data = get_form_data(self.widget_registry) + logger_get_data.debug( + "\n\n\n------- Form data DynamicListWidget:\n%s", pformat(form_data) + ) - return raw_data + return form_data def set_form_data( self, @@ -2130,7 +1855,6 @@ class DynamicDropdownWidget(QWidget): 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() @@ -2224,7 +1948,15 @@ class DynamicDropdownWidget(QWidget): return validate_form_data(self.widget_registry) def get_form_data(self): - raw_data = get_form_data(self.widget_registry) + # raw_data = get_form_data(self.widget_registry) + form_data = get_form_data(self.widget_registry) + logger_get_data.debug( + "\n\n\n------- Form data DynamicDropdownWidget:\n%s", pformat(form_data) + ) + + return form_data + # TODO check removal + # each sub form has its own numbered key: We need to get rid of these # This widget has one first dropdown value and then an arbitrary number of # list values @@ -2235,20 +1967,20 @@ class DynamicDropdownWidget(QWidget): # print(">>>> Dynamic dropdown: ") # pprint(raw_data) - export_dict: dict[str, Any] = {} - children_values: list[str] | None = None + # export_dict: dict[str, Any] = {} + # children_values: list[str] | None = None - for idx, data_dict in enumerate(get_leaf_dicts(raw_data)): - if idx == 0: - export_dict.update(data_dict) - else: - for key in data_dict: - if key not in export_dict: - children_values = export_dict.setdefault(key, []) - assert children_values is not None - children_values.append(data_dict[key]) + # for idx, data_dict in enumerate(get_leaf_dicts(raw_data)): + # if idx == 0: + # export_dict.update(data_dict) + # else: + # for key in data_dict: + # if key not in export_dict: + # children_values = export_dict.setdefault(key, []) + # assert children_values is not None + # children_values.append(data_dict[key]) - return export_dict + # return export_dict def set_form_data( self, @@ -2448,7 +2180,7 @@ class NewEntrySelect_view(QWidget): CUSTOM_WIDGETS: Final[dict[str, type[CustomWidget]]] = { - "grunderfassung_suche": Grunderfassung_Suche, + "grunderfassung_suche": Grunderfassung_SuchWidget, } @@ -2457,14 +2189,14 @@ FORM_FIELDS_SEARCH_HEAD = [ "Suche", FormFieldType.EXTENDED_DROPDOWN, required=True, - key="kontaktliste_un_suche", + key="PartnerSuche_un_suche", placeholder="Suche...", ), FormField( "Name Unternehmen/Netzwerkpartner", FormFieldType.TEXT, required=False, - key="kontaktliste_un_name", + key="PartnerSuche_un_name", readonly=True, info="ma_unternehmensname", ), @@ -2472,7 +2204,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Straße", FormFieldType.TEXT, required=False, - key="kontaktliste_un_straße", + key="PartnerSuche_un_straße", readonly=True, info="ma_strasse", ), @@ -2480,7 +2212,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Hausnummer", FormFieldType.TEXT, required=False, - key="kontaktliste_un_hausnummer", + key="PartnerSuche_un_hausnummer", readonly=True, info="ma_hausnummer", ), @@ -2488,7 +2220,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "PLZ", FormFieldType.TEXT, required=False, - key="kontaktliste_un_PLZ", + key="PartnerSuche_un_PLZ", readonly=True, info="ma_plz", ), @@ -2496,7 +2228,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Ort", FormFieldType.TEXT, required=False, - key="kontaktliste_un_ort", + key="PartnerSuche_un_ort", readonly=True, info="ma_ort", ), @@ -2504,15 +2236,14 @@ FORM_FIELDS_SEARCH_HEAD = [ "Suche Ansprechpartner", FormFieldType.EXTENDED_DROPDOWN, required=True, - key="kontaktliste_person_suche", - info="ma_ort", + key="PartnerSuche_person_suche", placeholder="Suche...", ), FormField( "Titel", FormFieldType.TEXT, required=False, - key="kontaktliste_person_titel", + key="PartnerSuche_person_titel", readonly=True, info="an_titel", ), @@ -2520,7 +2251,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Anrede", FormFieldType.TEXT, required=False, - key="kontaktliste_person_anrede", + key="PartnerSuche_person_anrede", readonly=True, info="an_anrede", ), @@ -2528,7 +2259,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Name", FormFieldType.TEXT, required=False, - key="kontaktliste_person_name", + key="PartnerSuche_person_name", readonly=True, info="an_nachname", ), @@ -2536,7 +2267,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Vorname", FormFieldType.TEXT, required=False, - key="kontaktliste_person_vorname", + key="PartnerSuche_person_vorname", readonly=True, info="an_vorname", ), @@ -2544,7 +2275,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Telefon", FormFieldType.TEXT, required=False, - key="kontaktliste_person_telefon", + key="PartnerSuche_person_telefon", readonly=True, info="an_festnetz", ), @@ -2552,7 +2283,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Mobil", FormFieldType.TEXT, required=False, - key="kontaktliste_person_mobilfunk", + key="PartnerSuche_person_mobilfunk", readonly=True, info="an_mobil", ), @@ -2560,7 +2291,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "E-Mail", FormFieldType.TEXT, required=False, - key="kontaktliste_person_email", + key="PartnerSuche_person_email", readonly=True, info="an_mail", ), @@ -2568,7 +2299,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Funktion im Unternehmen", FormFieldType.TEXT, required=False, - key="kontaktliste_person_funktion", + key="PartnerSuche_person_funktion", readonly=True, info="an_position", ), @@ -2576,7 +2307,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Wie sind Sie auf uns aufmerksam geworden?", FormFieldType.DROPDOWN, required=False, - key="kontaktliste_kanal_aufmerksamkeit", + key="PartnerSuche_kanal_aufmerksamkeit", options=[ ("Agentur für Arbeit", None), ("Ausländerbehörde", None), @@ -3012,7 +2743,27 @@ FORM_FIELDS_WORK_EXPERIENCE = [ FormFieldType.DROPDOWN, required=False, key="AE_branche", - options=[("DROPDOWN-LISTE AN ANDERER STELLE DEFINIERT", None)], + options=[ + (part, None) + for part in ( + "Metallerzeugung & -bearbeitung", + "Elektro, Energie, Chemie", + "IT & Software", + "Kunststoff, Papier, Textil", + "Logistik, Verkehr, Transport", + "Handwerk, Bau, Grüne Berufe", + "Gesundheit & Pflege", + "Tourismus & Gastronomie", + "Handel", + "Bildung & Soziales", + "Entwicklung, Planung, Qualität", + "Administration, Finanzen, Verwaltung", + "Marketing, Design, Vertrieb", + "Einkauf, Lager, Wartung", + "Sonstige", + "Keine Schwerpunkte, branchenübergreifende Rekrutierung", + ) + ], ), FormField( "Berufsbezeichnung/Tätigkeit", @@ -3125,11 +2876,44 @@ FORM_FIELDS_LANGUAGES = [ FORM_FIELDS = [ + FormField( + "Ersteintrag Datum", + FormFieldType.DATE, + required=False, + key="Metadaten_erstellung", + readonly=True, + ), + FormField( + "Aktualisierung Datum", + FormFieldType.DATE, + required=False, + key="Metadaten_aktualisierung", + readonly=True, + ), + FormField( + "Aktualisierung Nutzer", + FormFieldType.TEXT, + required=False, + key="Metadaten_nutzer", + readonly=True, + ), + FormField( + "Fallnummer", + FormFieldType.TEXT, + required=True, + key="Grunderfassung_fallnummer", + ), + FormField( + "Notizen", + FormFieldType.LONGTEXT, + required=False, + key="Grunderfassung_notiz", + ), FormField( "Suche", FormFieldType.CUSTOM, custom_widget="grunderfassung_suche", - key="grunderfassung_suche", + key="Partnersuche", children=FORM_FIELDS_SEARCH_HEAD, ), FormField( diff --git a/prototypes/tests.py b/prototypes/tests.py index eed7b4f..944cdd1 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -1,14 +1,252 @@ # %% import dataclasses as dc +import datetime import enum +import json import re from collections.abc import Sequence +from pprint import pprint from typing import Any import babel -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from PySide6.QtCore import QDate, Qt +# %% +gui_rohdaten = { + "projekt_name": "Mars Rover", + "meilensteine": [ + {"titel": "Triebwerk Test", "finanzen__betrag": 5000.0, "finanzen__waehrung": "EUR"}, + {"titel": "Software Beta", "finanzen__betrag": 1200.0, "finanzen__waehrung": "EUR"}, + ], +} + + +class FlatBaseModel(BaseModel): + @classmethod + def _unflatten_dict(cls, flat_dict: dict) -> dict: + """Hilfsmethode: Macht aus {'a__b': 1} wieder {'a': {'b': 1}}""" + result = {} + for key, value in flat_dict.items(): + if "__" in key: + parts = key.split("__") + aktuell = result + for part in parts[:-1]: + if part not in aktuell or not isinstance(aktuell[part], dict): + aktuell[part] = {} + aktuell = aktuell[part] + aktuell[parts[-1]] = value + else: + result[key] = value + return result + + @model_validator(mode="before") + @classmethod + def __unflatten_input(cls, data: Any) -> Any: + """Eingangskontrolle: Verarbeitet flache DB/GUI-Daten zu Pydantic-Strukturen.""" + if not isinstance(data, dict): + return data + + # 1. Haupt-Struktur wiederherstellen + unflattened = cls._unflatten_dict(data) + pprint(unflattened) + + # 2. Schleife über die Felder, um JSON-Listen von Sub-Modellen zu entpacken + for key, value in unflattened.items(): + if isinstance(value, str) and value.startswith("[") and value.endswith("]"): + try: + parsed = json.loads(value) + if isinstance(parsed, list): + # Falls die Liste Dictionaries enthält, ent-flachen wir diese einzeln + unflattened[key] = [ + cls._unflatten_dict(item) if isinstance(item, dict) else item + for item in parsed + ] + else: + unflattened[key] = parsed + except json.JSONDecodeError: + pass + if isinstance(value, (list)): + unflattened[key] = [ + cls._unflatten_dict(item) if isinstance(item, dict) else item + for item in value + ] + + pprint(unflattened) + + return unflattened + + def to_db(self) -> dict[str, Any]: + """Ausgang für die DB: Flach, Listen sind JSON-Strings.""" + nested = super().model_dump() + return self.__flatten_dict(nested, serialize_lists=True) + + def to_gui(self) -> dict[str, Any]: + """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen für Widgets.""" + nested = super().model_dump() + return self.__flatten_dict(nested, serialize_lists=False) + + @classmethod + def __flatten_dict( + cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True + ) -> dict: + """Rekursiver Alleskönner zum Abflachen von Strukturen.""" + items = [] + for k, v in nested_dict.items(): + new_key = f"{parent_key}__{k}" if parent_key else k + + if isinstance(v, dict): + items.extend(cls.__flatten_dict(v, new_key, serialize_lists).items()) + elif isinstance(v, list): + processed_list = [] + for item in v: + if isinstance(item, dict): + # WICHTIG: Das Dict in der Liste wird isoliert abgeflacht (ohne parent_key), + # da es ein eigenständiges Zeilen-Objekt im DynamicListWidget bleibt! + processed_list.append( + cls.__flatten_dict(item, serialize_lists=serialize_lists) + ) + else: + processed_list.append(item) + + if serialize_lists: + items.append((new_key, json.dumps(processed_list))) + else: + items.append((new_key, processed_list)) + else: + items.append((new_key, v)) + return dict(items) + + +# %% +class BudgetDetails(BaseModel): + betrag: float + waehrung: str = "EUR" + + +class Meilenstein(BaseModel): + titel: str + finanzen: BudgetDetails # <--- Verschachtelung innerhalb der Liste! + + +class ProjektModell(FlatBaseModel): + projekt_name: str + meilensteine: list[Meilenstein] # <--- Die Liste von Modellen + + +projekt = ProjektModell(**gui_rohdaten) + +# %% +projekt.meilensteine + +# %% +target_dict = { + "t1": "test", + "t2": "test2", + "t3": ["t3-1", "t3-2", "t3-3"], + "t4": { + "t4-1": "t4-1", + "t4-2": "t4-2", + }, + "t5": [ + {"sub1": "sub1-1"}, + {"sub2": "sub2-1"}, + {"sub3": "sub3-1"}, + ], +} + +target_dict = { + "Stammdaten": { + "Stammdaten_PLZ": "4523", + "Stammdaten_anrede_anschrift": "Sehr geehrter Herr", + "Stammdaten_anzahl_kinder": {"alter": [3, 7, 10, 14], "anzahl": 4}, + "Stammdaten_aufenthaltsort": "Ausland EU/EWR", + "Stammdaten_bundesland": None, + "Stammdaten_email": "max.mustermann@test.at", + "Stammdaten_familienstand": "ledig", + "Stammdaten_festnetznummer": "123456789", + "Stammdaten_geburtsdatum": datetime.date(1990, 4, 11), + "Stammdaten_hausnummer": "12", + "Stammdaten_herkunftsland": "AT", + "Stammdaten_land": None, + "Stammdaten_mobilfunknummer": "123456789", + "Stammdaten_name": "Mustermann", + "Stammdaten_ort": "Ort in Österreich", + "Stammdaten_rueckkehrer": False, + "Stammdaten_staatsangehoerigkeit": "AT", + "Stammdaten_strasse": "Teststraße", + "Stammdaten_titel": "Test", + "Stammdaten_vorname": "Max", + }, + "Schulbildung": [ + {"Schulbildung-[1]__SB_abschluss": None, "Schulbildung-[1]__SB_abschlussgrad": None}, + {"Schulbildung-[2]__SB_abschluss": None, "Schulbildung-[2]__SB_abschlussgrad": None}, + ], +} + + +def flatten_for_db( + nested_dict: dict, + parent_key: str = "", + sep: str = ".", +) -> dict[str, Any]: + items = [] + for k, v in nested_dict.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + + if isinstance(v, dict): + items.extend(flatten_for_db(v, new_key, sep=sep).items()) + elif isinstance(v, list): + items.append((new_key, json.dumps(v))) + else: + items.append((new_key, v)) + + return dict(items) + + +def unflatten_from_db( + flat_dict: dict, + sep: str = ".", +) -> dict[str, Any]: + result = {} + for key, value in flat_dict.items(): + if isinstance(value, str) and (value.startswith("[") and value.endswith("]")): + try: + value = json.loads(value) + except json.JSONDecodeError: + pass + + parts = key.split(sep) + aktuell = result + for part in parts[:-1]: + if part not in aktuell: + aktuell[part] = {} + aktuell = aktuell[part] + + aktuell[parts[-1]] = value + + return result + + +flatted = flatten_for_db(target_dict, sep="__") +pprint(flatted) + +unflatted = unflatten_from_db(flatted, sep="__") +print("\n\n") +pprint(unflatted) + + +# %% +flatted + + +# %% +string = """ +Metallerzeugung & -bearbeitung; Elektro, Energie, Chemie; IT & Software; Kunststoff, Papier, Textil; Logistik, Verkehr, Transport; Handwerk, Bau, Grüne Berufe; Gesundheit & Pflege; Tourismus & Gastronomie; Handel; Bildung & Soziales; Entwicklung, Planung, Qualität; Administration, Finanzen, Verwaltung; Marketing, Design, Vertrieb; Einkauf, Lager, Wartung; Sonstige; Keine Schwerpunkte, branchenübergreifende Rekrutierung +""" +parts = string.strip().split(";") +[part.strip() for part in parts] + # %% DYNAMIC_LIST_KEY_PATTERN = re.compile(r"-\[(\d+)\]") @@ -153,9 +391,6 @@ def get_country_list_german() -> CountryList: ) -# %% -laender_liste - # %% DYNAMIC_LIST_KEY_PATTERN = r"-\[(\d+)\]" key = "Schulbildung-[12].7b8da0f7-7a0e-4f71-878a-85616099e849"