From a7accaa20c2888e7f1ad33e0f922b4a09080b881 Mon Sep 17 00:00:00 2001 From: foefl Date: Wed, 13 May 2026 11:52:20 +0200 Subject: [PATCH] nested validation and error messages --- prototypes/t_qt_2.py | 193 +++++++++++++++++++++++++------------------ prototypes/tests.py | 13 +++ 2 files changed, 126 insertions(+), 80 deletions(-) diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index 60d569e..a33eac0 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -2,6 +2,7 @@ from __future__ import annotations import dataclasses as dc import enum +import re import sys import time import uuid @@ -319,8 +320,10 @@ class FormField: elif self.type is FormFieldType.DROPDOWN: self.dropdown_options = tuple(DropdownOption(op[0], op[1]) for op in options) - for child in self.children: - child.parent = self + if self.children: + self.required = any((child.required for child in self.children)) + for child in self.children: + child.parent = self class WidgetRegistryEntry(TypedDict): @@ -700,7 +703,7 @@ FORM_FIELDS_SCHOOL = [ FormField( "Abschluss", FormFieldType.TEXT, - required=False, + required=True, ), FormField( "Abschlussgrad laut Dokument", @@ -1157,6 +1160,104 @@ def _insert_nested( target_dict[key_path[-1]] = value +def get_form_data( + widget_registry: WidgetRegistry, +) -> dict[str, Any]: + raw_data = {} + for key, registry_entry in widget_registry.items(): + value: Any | None = None + + widget = registry_entry["widget"] + if isinstance(widget, QLineEdit): + value = widget.text() + elif isinstance(widget, QPlainTextEdit): + value = widget.toPlainText() + elif isinstance(widget, QDateEdit): + qt_date = widget.date() + value = qt_date.toPython() + elif isinstance(widget, QComboBox): + value = widget.currentData() + elif isinstance(widget, DynamicListWidget): + # 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()] + + _insert_nested(raw_data, key.split("."), value) + + return raw_data + + +DYNAMIC_LIST_KEY_PATTERN = re.compile(r"-\[(\d+)\]") + + +def validate_form_data( + widget_registry: WidgetRegistry, +) -> list[str]: + errors: list[str] = [] + + for key, registry_entry in widget_registry.items(): + error_post: bool = False + widget = registry_entry["widget"] + form_field = registry_entry["form_field"] + + if not form_field.readonly: + widget.setStyleSheet("") + + if not form_field.required: + continue + + dynamic_list_num: str = "" + if ( + form_field.parent is not None + and form_field.parent.type is FormFieldType.DYNAMIC_LIST + ): + # get also number of group for enhanced error messages + matches = DYNAMIC_LIST_KEY_PATTERN.search(key) + if matches: + dynamic_list_num = matches.group(1) + + if isinstance(widget, (QLineEdit, QDateEdit)): + if widget.text().strip(): + continue + error_post = True + elif isinstance(widget, QPlainTextEdit): + if widget.toPlainText().strip(): + continue + error_post = True + elif isinstance(widget, QComboBox): + if widget.currentData() is not None: + continue + error_post = True + elif isinstance(widget, DynamicListWidget): + errors_widget = widget.validate_form_data() + if not errors_widget: + continue + errors.extend(errors_widget) + + if not error_post: + continue + + error = form_field.label.replace("*", "").replace(":", "") + if form_field.parent is not None: + parent_label = form_field.parent.label.replace("*", "").replace(":", "") + if dynamic_list_num: + error = f"{parent_label} → {f'{parent_label} {dynamic_list_num}'} → {error}" + else: + error = f"{parent_label} → {error}" + error = error.replace("&&", "&") + errors.append(error) + # optical feedback to highlight erroneous cells + widget.setStyleSheet(""" + border: 1px solid #ef4444; + background-color: #ffe9e9; + padding: 4px; + border-radius: 4px; + """) + + return errors + + class AutoForm(QWidget): def __init__( self, @@ -1249,45 +1350,13 @@ class AutoForm(QWidget): def on_save_clicked(self) -> None: self._disable_save() - errors: list[str] = [] - - for registry_entry in self.widget_registry.values(): - widget = registry_entry["widget"] - form_field = registry_entry["form_field"] - - if not form_field.readonly: - widget.setStyleSheet("") - - if not form_field.required: - continue - - if isinstance(widget, (QLineEdit, QDateEdit)): - if widget.text().strip(): - continue - elif isinstance(widget, QPlainTextEdit): - if widget.toPlainText().strip(): - continue - elif isinstance(widget, QComboBox): - if widget.currentData() is not None: - continue - - error = form_field.label.replace("*", "").replace(":", "") - if form_field.parent is not None: - error = f"{form_field.parent.label}: {error}" - errors.append(error) - # optical feedback to highlight erroneous cells - widget.setStyleSheet(""" - border: 1px solid #ef4444; - background-color: #ffe9e9; - padding: 4px; - border-radius: 4px; - """) + errors = validate_form_data(self.widget_registry) if errors: # errors: abort saving and show pop up window error_text = ( - "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n- " - + "\n- ".join(errors) + "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n▸ " + + "\n▸ ".join(errors) ) QMessageBox.warning(self, "Fehlende Angaben", error_text) self._enable_save() @@ -1310,30 +1379,7 @@ class AutoForm(QWidget): reset_form(self.widget_registry) def get_form_data(self) -> dict[str, Any]: - # raise NotImplementedError() - - raw_data = {} - for key, registry_entry in self.widget_registry.items(): - value: Any | None = None - - widget = registry_entry["widget"] - if isinstance(widget, QLineEdit): - value = widget.text() - elif isinstance(widget, QPlainTextEdit): - value = widget.toPlainText() - elif isinstance(widget, QDateEdit): - qt_date = widget.date() - value = qt_date.toPython() - elif isinstance(widget, QComboBox): - value = widget.currentData() - elif isinstance(widget, DynamicListWidget): - # TODO add method - # value = widget.get_data() - # this should be a list: each dynamic list contains a list - # of such dictionaries - value = "test" - - _insert_nested(raw_data, key.split("."), value) + raw_data = get_form_data(self.widget_registry) return raw_data @@ -1442,10 +1488,6 @@ class DynamicListWidget(QWidget): self.inner_layout.insertWidget(self.inner_layout.count() - 1, entry_box) self.update_sub_forms() - # print("------------>>>>>>>>> Added entry, length widget registry:") - # pprint(len(self.widget_registry)) - # pprint(list(self.widget_registry.keys())) - def remove_entry( self, subform_to_remove: SubForm, @@ -1454,9 +1496,6 @@ class DynamicListWidget(QWidget): subform_to_remove.entry_box.deleteLater() self.sub_forms.remove(subform_to_remove) self.update_sub_forms() - # print("------------>>>>>>>>> Removed entry, length widget registry:") - # pprint(len(self.widget_registry)) - # pprint(list(self.widget_registry.keys())) def update_sub_forms(self): for index, sub_form in enumerate(self.sub_forms, start=1): @@ -1470,22 +1509,17 @@ class DynamicListWidget(QWidget): def reset_form(self) -> None: reset_form(self.widget_registry) + 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): - raise NotImplementedError() + raw_data = get_form_data(self.widget_registry) - results = [] - for form in self.sub_forms: - # Hier bauen wir ein Dictionary pro Sub-Formular - entry_data = { - "strasse": form["widgets"]["strasse"].text(), - "ort": form["widgets"]["ort"].text(), - } - results.append(entry_data) - return results + return raw_data class ClickableCell(QFrame): @@ -1604,7 +1638,6 @@ class NewEntrySelect_view(QWidget): btn_company.setFixedWidth(300) btn_company.setFixedHeight(40) btn_company.clicked.connect(lambda: self.company_requested.emit()) - # btn_company.clicked.connect(lambda: print("Unternehmen gewählt")) btn_person = QPushButton("Individualperson →") btn_person.setFixedWidth(300) diff --git a/prototypes/tests.py b/prototypes/tests.py index b3e6016..c098eec 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -1,6 +1,7 @@ # %% import dataclasses as dc import enum +import re from PySide6.QtCore import QDate, Qt @@ -27,6 +28,18 @@ class FormField: self.label += "*" +# %% +DYNAMIC_LIST_KEY_PATTERN = r"-\[(\d+)\]" +key = "Schulbildung-[12].7b8da0f7-7a0e-4f71-878a-85616099e849" + +matches = re.search(DYNAMIC_LIST_KEY_PATTERN, key) + +# %% +matches + +# %% +matches.group(1) + # %% t_str = "asd.yxcxc.dfgjj.aasdsdsdsd.sdsdsdsd" splitted = t_str.split(".")