Dev #7

Merged
foefl merged 74 commits from dev into main 2026-07-31 14:19:01 +00:00
5 changed files with 292 additions and 157 deletions
Showing only changes of commit 9cfe53509f - Show all commits
+26
View File
@@ -0,0 +1,26 @@
def _get_leafs(data):
if isinstance(data, dict):
for value in data.values():
yield from _get_leafs(value)
elif isinstance(data, (list, tuple, set)):
for item in data:
yield from _get_leafs(item)
else:
yield data
def _get_leaf_dicts(data):
if isinstance(data, dict):
has_inner_dicts = False
for value in data.values():
for inner_dict in _get_leaf_dicts(value):
has_inner_dicts = True
yield inner_dict
if not has_inner_dicts:
yield data
elif isinstance(data, (list, tuple, set)):
for item in data:
yield from _get_leaf_dicts(item)
+3 -3
View File
@@ -147,7 +147,7 @@ def initrec_company_get_initial_recording(
row = ret.fetchone()
assert row, "row was not obtained"
return row._asdict()
return row._asdict() # type: ignore
def initrec_company_delete_initial_recording(
@@ -196,7 +196,7 @@ def initrec_person_update_initial_recording(
def initrec_person_get_initial_recording(
id_: RecId,
) -> dict[str, Any]:
logger.debug("[Call backend] get_initial_recording")
logger.debug("[Call backend] get_initial_recording person")
stmt = db.grunderfassung_personen.select().where(
db.grunderfassung_personen.c.pers_id == id_
)
@@ -209,7 +209,7 @@ def initrec_person_get_initial_recording(
row = ret.fetchone()
assert row, "row was not obtained"
return row._asdict()
return row._asdict() # type: ignore
def initrec_person_delete_initial_recording(
+37
View File
@@ -13,10 +13,47 @@ from pydantic import (
field_validator,
model_validator,
)
from pydantic_core import ErrorDetails
ValidAge = Annotated[int, Field(ge=0, le=99)]
# // validation translation
GERMAN_ERROR_MESSAGES: Final[dict[str, str]] = {
"missing": "Dieses Feld ist ein Pflichtfeld.",
"int_parsing": "Bitte eine gültige Ganzzahl eingeben.",
"float_parsing": "Bitte eine gültige Dezimalzahl eingeben.",
"string_too_short": "Der Text ist zu kurz. Er muss mindestens {min_length} Zeichen lang sein.",
"string_too_long": "Der Text ist zu lang. Er darf maximal {max_length} Zeichen lang sein.",
"greater_than": "Der Wert muss größer als {gt} sein.",
}
def translate_pydantic_errors(
errors: list[ErrorDetails],
) -> list[ErrorDetails]:
translated_errors: list[ErrorDetails] = []
for error in errors:
err_type = error.get("type", None)
if err_type is None or err_type not in GERMAN_ERROR_MESSAGES:
error["msg"] = "[fehleden Übersetzung, nur Englisch]: " + error["msg"]
translated_errors.append(error)
continue
msg_template = GERMAN_ERROR_MESSAGES[err_type]
ctx = error.get("ctx", {})
try:
error["msg"] = msg_template.format(**ctx)
except KeyError:
error["msg"] = msg_template
translated_errors.append(error)
return translated_errors
def _parse_json(value: Any) -> str:
if isinstance(value, datetime.date):
return value.isoformat()
+2
View File
@@ -911,6 +911,7 @@ INITREC_COMP = [
required=False,
key="Metadaten_nutzer",
readonly=True,
ignore_get_data=True,
),
FormField(
"Fallnummer",
@@ -956,6 +957,7 @@ INITREC_PERSON = [
required=False,
key="Metadaten_nutzer",
readonly=True,
ignore_get_data=True,
),
FormField(
"Wiedereintrittsdatum",
+224 -154
View File
@@ -4,13 +4,11 @@ import copy
import dataclasses as dc
import datetime
import enum
import pickle
import re
import sys
import traceback
from collections import defaultdict
from collections.abc import Container, Iterable, Sequence
from pathlib import Path
from pprint import pformat
from typing import (
TYPE_CHECKING,
@@ -26,7 +24,6 @@ from typing import (
)
from typing_extensions import override
from dopt_basics import datetime as dopt_dt
from pydantic import (
ValidationError,
)
@@ -36,7 +33,6 @@ from PySide6.QtCore import (
QEvent,
QObject,
Qt,
QTime,
QTimer,
QtMsgType,
Signal,
@@ -84,6 +80,7 @@ from wce_crm.data_models import (
FlatBaseModel,
Grunderfassung_Personen,
Grunderfassung_Unternehmen,
translate_pydantic_errors,
)
from wce_crm.form_defs import (
INITREC_COMP,
@@ -166,36 +163,6 @@ def unmerge_dict_to_list(
return [dict(zip(keys, row)) for row in zip(*value_lists)]
# TODO check removal or move to basics package
def _get_leafs(data):
if isinstance(data, dict):
for value in data.values():
yield from _get_leafs(value)
elif isinstance(data, (list, tuple, set)):
for item in data:
yield from _get_leafs(item)
else:
yield data
# TODO check removal or move to basics package
def _get_leaf_dicts(data):
if isinstance(data, dict):
has_inner_dicts = False
for value in data.values():
for inner_dict in _get_leaf_dicts(value):
has_inner_dicts = True
yield inner_dict
if not has_inner_dicts:
yield data
elif isinstance(data, (list, tuple, set)):
for item in data:
yield from _get_leaf_dicts(item)
def pprint_registry(widget_registry: WidgetRegistry) -> None:
print("\n\n>>> Widget registry:")
for key, entry in widget_registry.items():
@@ -665,8 +632,6 @@ def _build_ui_recursively(
else:
full_key = field.key
widget: QWidget
match field.type:
case FormFieldType.GROUP:
group_box = QGroupBox(field.label)
@@ -686,11 +651,6 @@ def _build_ui_recursively(
widget_registry=widget_registry,
prefix=prefix,
)
# TODO remove
# widget_registry[full_key] = {
# "widget": widget,
# "form_field": field,
# }
keys.append(full_key)
return keys
@@ -722,7 +682,7 @@ def reset_form(
form_field = registry_entry["form_field"]
# TODO check if this behaviour is correct in other contexts, deactivated because of
# TODO company search widget
# company search widget
# if form_field.readonly:
# continue
@@ -1410,6 +1370,7 @@ class AutoForm(QWidget):
) -> None:
super().__init__()
self.STATE = state
self.SESSION_USER_KEY: Final[str] = "Metadaten_nutzer"
self.cfg = state.cfg
self.setStyleSheet("""
QGroupBox {
@@ -1515,40 +1476,21 @@ class AutoForm(QWidget):
self.debug_form_data: dict[str, Any] = {}
if self.add_buttons:
self.layout_btn = QHBoxLayout()
self.main_layout.addLayout(self.layout_btn)
# save
self.save_btn_txt_enabled = "Speichern (Strg + S)"
self.save_btn_txt_disabled = "Wird gespeichert..."
self.save_btn = QPushButton(self.save_btn_txt_enabled)
self.save_btn.setShortcut("Ctrl+S")
self.save_btn.setFixedHeight(50)
self.save_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.edit_buttons = EditButtons(add_reset_btn=True)
reset_btn = self.edit_buttons.reset_btn
assert reset_btn
self.save_btn = self.edit_buttons.save_btn
self.reset_btn = reset_btn
self.delete_btn = self.edit_buttons.delete_btn
self.save_btn.clicked.connect(self.save_data)
self.layout_btn.addWidget(self.save_btn)
# reset
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
self.reset_btn.setShortcut("Ctrl+Z")
self.reset_btn.setFixedHeight(50)
self.reset_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.reset_btn.clicked.connect(self.reset_form)
self.layout_btn.addWidget(self.reset_btn)
# delete
self.delete_btn = QPushButton("Eintrag löschen (Strg + L)")
self.delete_btn.setShortcut("Ctrl+L")
self.delete_btn.setFixedHeight(50)
self.delete_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.delete_btn.clicked.connect(self._delete_data)
self.layout_btn.addWidget(self.delete_btn)
self.main_layout.addWidget(self.edit_buttons)
logger_auto_form.debug(
"Initialised Auto Form Widget. Registry:%s",
"[Auto-Form] Initialised Auto Form Widget. Registry:%s",
pformat_registry(self.widget_registry),
)
@@ -1561,11 +1503,11 @@ class AutoForm(QWidget):
except ValueError:
index = None
self.STATE.rec_id = index
logger_gui.debug("Set index to %d, new state: %s", index, self.STATE)
logger_gui.debug("[Auto-Form] Set index to %s, new state: %s", index, self.STATE)
def _disable_save(self) -> None:
self.save_btn.setEnabled(False)
self.save_btn.setText(self.save_btn_txt_disabled)
self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled)
def _enable_save(
self,
@@ -1573,7 +1515,7 @@ class AutoForm(QWidget):
) -> None:
QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True))
QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S"))
self.save_btn.setText(self.save_btn_txt_enabled)
self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled)
def _activate_delete(self) -> None:
if self.STATE.rec_id is not None:
@@ -1589,35 +1531,32 @@ class AutoForm(QWidget):
def _load_data(
self,
lookup_id: int | None = None,
lookup_id: int | None,
) -> None:
if DEBUG_NO_DATABASE:
return
logger_auto_form.info(">>>> LOAD CLICKED")
if lookup_id is None:
# only debugging
lookup_id = self.STATE.rec_id
logger_auto_form.info("[Auto-Form] Load data method...")
self.reset_form()
logger_auto_form.debug("Lookup ID: %d", lookup_id)
logger_auto_form.debug("[Auto-Form] Lookup ID: %s", lookup_id)
if lookup_id is not None and lookup_id > 0:
if lookup_id is not None:
logger_auto_form.debug("Load from DB:")
loaded_data = self.cfg.data_get(lookup_id)
else:
logger_auto_form.error("Loading: Lookup ID 0 or NULL. Do nothing!")
logger_auto_form.debug("[Auto-Form] Loading: Lookup ID NULL. Do nothing!")
return
logger_auto_form.debug(
"Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data)
"[Auto-Form] Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data)
)
model = self.cfg.model(**loaded_data)
logger_auto_form.debug("Loaded to Pydantic.")
logger_auto_form.debug("Convert to GUI structure...")
logger_auto_form.debug("[Auto-Form] Loaded to Pydantic.")
logger_auto_form.debug("[Auto-Form] Convert to GUI structure...")
form_data = model.to_gui()
logger_auto_form.debug("Set form data...")
logger_auto_form.debug("Form data:\n%s", pformat(form_data))
logger_auto_form.debug("[Auto-Form] Set form data...")
logger_auto_form.debug("[Auto-Form] Form data:\n%s", pformat(form_data))
self._set_form_data(form_data)
self.STATE.rec_id = lookup_id
self._activate_delete()
@@ -1625,12 +1564,13 @@ class AutoForm(QWidget):
def reset_form(self) -> None:
reset_form(self.widget_registry)
self.STATE.rec_id = None
self.STATE.form_data = None
self._activate_delete()
def _get_form_data(self) -> dict[str, Any]:
form_data = get_form_data(self.widget_registry)
logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call get form data:")
logger_auto_form.debug("Form Data:\n%s", pformat(form_data))
logger_auto_form.debug("[Auto-Form] Call get form data:")
logger_auto_form.debug("[Auto-Form] Form Data:\n%s", pformat(form_data))
if DEBUG_GET_SET:
self.debug_form_data = form_data
@@ -1641,7 +1581,7 @@ class AutoForm(QWidget):
self,
data: dict[str, Any],
) -> None:
logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call set form data:")
logger_auto_form.debug("[Auto-Form] Call set form data...")
if DEBUG_GET_SET:
data = self.debug_form_data
@@ -1651,7 +1591,10 @@ class AutoForm(QWidget):
self._load_data(self.STATE.rec_id)
def _sync_GUI_to_state(self) -> None:
self.STATE.form_data = self._get_form_data()
form_data = self._get_form_data()
# TODO change usage of user ID (needs changes in model and backend)
form_data[self.SESSION_USER_KEY] = self.STATE.session.user_name
self.STATE.form_data = form_data
def validate(self) -> list[str]:
return validate_form_data(self.widget_registry)
@@ -1661,8 +1604,6 @@ class AutoForm(QWidget):
new_state: AutoForm_State,
) -> None:
set_page_state(self.STATE, new_state)
# self.STATE.rec_id = new_state.rec_id
# self.STATE.form_data = new_state.form_data
self._sync_state_to_GUI()
def get_state(self) -> AutoForm_State:
@@ -1675,50 +1616,29 @@ class AutoForm(QWidget):
errors = self.validate()
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)
logger_auto_form.error(
"[Auto-Form] Error during GUI validation phase:\n%s", pformat(errors)
)
QMessageBox.warning(self, "Fehlende Angaben", error_text)
GUI_validation_error_handling(errors)
self._enable_save()
return
# no errors: data can be saved
logger_auto_form.info("Erfolg! Alle Daten sind valide.")
logger_auto_form.info("Get form data call...")
form_data = self._get_form_data()
logger_auto_form.info("[Auto-Form] Success! All data is valid")
logger_auto_form.info("[Auto-Form] Get form data call...")
state = self.get_state()
form_data = state.form_data
assert form_data, "form data not set"
logger_auto_form.debug(
"\n\n------------>>>>>>>>> Get form data\n%s", pformat(form_data)
)
logger_auto_form.debug("[Auto-Form] Get form data\n%s", pformat(form_data))
try:
logger_auto_form.debug("------------>>>>>>>>> Call Pydantic")
logger_auto_form.debug("[Auto-Form] Call Pydantic...")
validated_data = self.cfg.model(**form_data)
# # TODO add user?
except ValidationError as e:
# catch errors and show them in GUI
error_texts = []
# Pydantic detailed error list
for error in e.errors():
logger_gui.error("Error during validation phase:\n%s", pformat(error))
error_field = str(error["loc"][0])
reason = error["msg"]
error_texts.append(f"- {error_field}: {reason}")
# TODO: Is this needed and feasible?
# formatting red
# if fehlerhaftes_feld in self.widgets:
# self.widgets[fehlerhaftes_feld].setStyleSheet(
# "border: 1px solid red; background: #fef2f2;"
# )
# tell user what went wrong
QMessageBox.warning(self, "Eingabefehler", "\n".join(error_texts))
logger_auto_form.error(
"[Auto-Form] Error during Pydantic validation phase:\n%s", pformat(e.errors())
)
pydantic_validation_error_handling(e)
else:
if DEBUG_NO_DATABASE:
return
@@ -1726,20 +1646,20 @@ class AutoForm(QWidget):
# save data to database
db_data = validated_data.to_db(exclude=self.cfg.ignored_keys)
logger_auto_form.debug(
("Form data with 'exlude' (must be saved in the database):\n%s"),
"[Auto-Form] Form data with 'exlude' (must be saved in the database):\n%s",
pformat(db_data),
)
if self.STATE.rec_id is None:
logger_auto_form.debug("Insert triggered")
logger_auto_form.debug("[Auto-Form] Insert triggered...")
init_rec_id = self.cfg.data_insert(db_data)
assert isinstance(init_rec_id, int)
self.STATE.rec_id = init_rec_id
else:
logger_auto_form.debug("Update triggered")
logger_auto_form.debug("[Auto-Form] Update triggered...")
self.cfg.data_update(self.STATE.rec_id, db_data)
logger_auto_form.info("Data saved successfully")
logger_auto_form.info("[Auto-Form] Data saved successfully")
self.update_triggered.emit()
self._activate_delete()
finally:
@@ -3688,19 +3608,6 @@ class Page_Consulting(QWidget):
# state management
self.STATE = Page_Consulting_State(session=session)
# self.current_id: InitRecId = -1
# self.COMPANY_FETAURES_MASTER_DATA: dict[str, str] = {
# "ma_unternehmensname": "Name:",
# "ma_branche": "Branche:",
# "ma_strasse": "Straße:",
# "ma_hausnummer": "Hausnummer:",
# "ma_plz": "PLZ:",
# "ma_ort": "Ort:",
# "ma_website": "Webseite:",
# "ma_mail": "E-Mail:",
# "ma_telefonnummer": "Telefon:",
# }
# Hauptlayout der Seite
outer_layout = QHBoxLayout(self)
vert_layout = QVBoxLayout()
@@ -3907,8 +3814,6 @@ class Page_Consulting(QWidget):
self,
new_state: Page_Consulting_State,
) -> None:
# TODO add correct loading behaviour
# TODO add validation (correct type with necessary information)
logger_page_consulting.debug("[Consulting] Loading with request:\n%s", new_state)
set_page_state(self.STATE, new_state)
# child modules
@@ -4696,19 +4601,184 @@ def global_exception_handler(exc_type, exc_value, exc_traceback):
# message to user
# check if QApplication exists otherwise crashing of crash handler possible)
if QApplication.instance():
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Icon.Critical)
msg_box.setWindowTitle("Kritischer Fehler")
msg_box.setText(
"Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert."
msg_box = get_error_message_box(
QMessageBox.Icon.Critical,
"Kritischer Fehler",
"Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert.",
error_msg,
)
# details for user or screenshots
msg_box.setDetailedText(error_msg)
# msg_box.setIcon(QMessageBox.Icon.Critical)
# msg_box.setWindowTitle("Kritischer Fehler")
# msg_box.setText(
# "Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert."
# )
# # details for user or screenshots
# msg_box.setDetailedText(error_msg)
msg_box.exec()
sys.exit(1)
def get_error_message_box(
msg_type: QMessageBox.Icon,
title: str,
message: str,
detailed_text: str | None = None,
) -> QMessageBox:
msg_box = QMessageBox()
msg_box.setIcon(msg_type)
msg_box.setWindowTitle(title)
msg_box.setText(message)
if detailed_text is not None:
msg_box.setDetailedText(detailed_text)
return msg_box
def pydantic_validation_error_handling(
val_error: ValidationError,
) -> None:
error_texts: list[str] = []
translated_errors = translate_pydantic_errors(val_error.errors())
for error in translated_errors:
error_field = str(error["loc"][0])
reason = error["msg"]
path = "".join(error["loc"][:-1]) # type: ignore
error_texts.append(f"- {error_field}: {reason}, (Pfad: {path})")
msg_box = get_error_message_box(
QMessageBox.Icon.Warning,
"Fehler bei der Validierung der Eingabedaten",
(
"Bei der Validierung der Eingaben ist ein Fehler aufgetreten. Details "
"sind unten angefügt."
),
"\n".join(error_texts),
)
msg_box.exec()
def GUI_validation_error_handling(
errors: list[str],
) -> None:
error_text = "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n" + "\n".join(
errors
)
msg_box = get_error_message_box(
QMessageBox.Icon.Warning,
"Fehlende oder fehlerhafte Angaben",
error_text,
)
msg_box.exec()
# @dc.dataclass(slots=True)
# class EditButton:
# btn: QWidget
# txt_enabled: str
# txt_disabled: str
# @dc.dataclass(slots=True, kw_only=True)
# class EditButtonLayout:
# layout: QLayout
# save: EditButton
# delete: EditButton
# reset: EditButton | None
class EditButtons(QWidget):
def __init__(
self,
add_reset_btn: bool,
) -> None:
super().__init__()
layout_btn = QHBoxLayout(self)
layout_btn.setContentsMargins(0, 0, 0, 0)
# main_layout.addLayout(layout_btn) # TODO add in component
# save
self.save_btn_txt_enabled = "Speichern (Strg + S)"
self.save_btn_txt_disabled = "Wird gespeichert..."
self.save_btn = QPushButton(self.save_btn_txt_enabled)
self.save_btn.setShortcut("Ctrl+S")
self.save_btn.setFixedHeight(50)
self.save_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# save_btn.clicked.connect(save_data) # TODO add in component
layout_btn.addWidget(self.save_btn)
# reset
self.reset_button: QPushButton | None = None
if add_reset_btn:
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
self.reset_btn.setShortcut("Ctrl+Z")
self.reset_btn.setFixedHeight(50)
self.reset_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
# reset_btn.clicked.connect(reset_form) # TODO add in component
layout_btn.addWidget(self.reset_btn)
# delete
self.delete_btn = QPushButton("Eintrag löschen (Strg + L)")
self.delete_btn.setShortcut("Ctrl+L")
self.delete_btn.setFixedHeight(50)
self.delete_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# delete_btn.clicked.connect(self._delete_data) # TODO add in component
layout_btn.addWidget(self.delete_btn)
# def get_edit_buttons(
# add_reset_btn: bool,
# ) -> EditButtonLayout:
# layout_btn = QHBoxLayout()
# # main_layout.addLayout(layout_btn) # TODO add in component
# # save
# save_btn_txt_enabled = "Speichern (Strg + S)"
# save_btn_txt_disabled = "Wird gespeichert..."
# save_btn = QPushButton(save_btn_txt_enabled)
# save_btn.setShortcut("Ctrl+S")
# save_btn.setFixedHeight(50)
# save_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# # save_btn.clicked.connect(save_data) # TODO add in component
# edit_button_save = EditButton(save_btn, "Speichern (Strg + S)", "Wird gespeichert...")
# layout_btn.addWidget(save_btn)
# # reset
# edit_button_reset: EditButton | None = None
# if add_reset_btn:
# reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
# reset_btn.setShortcut("Ctrl+Z")
# reset_btn.setFixedHeight(50)
# reset_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# # reset_btn.clicked.connect(reset_form) # TODO add in component
# edit_button_reset = EditButton(
# save_btn, "Zurücksetzen (Strg + Z)", "Zurücksetzen (Strg + Z)"
# )
# layout_btn.addWidget(reset_btn)
# # delete
# delete_btn = QPushButton("Eintrag löschen (Strg + L)")
# delete_btn.setShortcut("Ctrl+L")
# delete_btn.setFixedHeight(50)
# delete_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# edit_button_delete = EditButton(
# save_btn, "Eintrag löschen (Strg + L)", "Eintrag löschen (Strg + L)"
# )
# # delete_btn.clicked.connect(self._delete_data) # TODO add in component
# layout_btn.addWidget(delete_btn)
# return EditButtonLayout(
# layout=layout_btn,
# save=edit_button_save,
# delete=edit_button_delete,
# reset=edit_button_reset,
# )
def qt_message_handler(mode, context, message):
"""forwards internal Qt C++ warnings to Python loggers"""
if mode == QtMsgType.QtInfoMsg: