From 16ae57368797ed08a3e69eb6f051bb26ca9513b6 Mon Sep 17 00:00:00 2001 From: foefl Date: Wed, 8 Jul 2026 15:28:50 +0200 Subject: [PATCH] defining common interfaces for modules --- src/wce_crm/gui.py | 564 ++++++++++++++++++++++++----------------- src/wce_crm/logging.py | 3 + 2 files changed, 334 insertions(+), 233 deletions(-) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index ed0a70e..66475df 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -93,6 +93,7 @@ from wce_crm.form_defs import ( ) from wce_crm.logging import ( logger_auto_form, + logger_custom_fields, logger_get_data, logger_gui, logger_page_consulting, @@ -107,6 +108,7 @@ V = TypeVar("V") T = TypeVar("T") W = TypeVar("W", bound="QWidget") M = TypeVar("M", bound="Module") +S = TypeVar("S", bound="PageState") DEBUG: bool = True DEBUG_SEARCH_WIDGET: bool = False @@ -225,16 +227,45 @@ class CustomForm(Protocol): def validate_form_data(self) -> list[str]: ... -class Module(Protocol): - def __sync_state_to_GUI(self) -> None: ... # type: ignore +class WrapperModule(Protocol): + def _sync_state_to_GUI(self) -> None: + """build states for all child modules and use their `load_state` method + to initialise them with the new data + """ + ... - def __sync_GUI_to_state(self) -> None: ... # type: ignore - - def validate(self) -> None: ... + def _sync_GUI_to_state(self) -> None: + """call `get_state` method on all child modules and assign respective + properties to state of parent module + """ + ... def load_state(self, new_state: Any) -> None: ... - def get_state(self) -> Any: ... + def get_state(self) -> Any: + """should always be a simple call to `_sync_GUI_to_state` and then returning + the module's state + + Returns + ------- + Any + state of the respective module + """ + ... + + +class Module(WrapperModule, Protocol): + def validate(self) -> list[str]: + """should be a list of field names which could not be validated successfully, + propagates through: parent components must call this on all child modules and extend + their own error list with the return value + + Returns + ------- + list[str] + list of error fields (form field label) + """ + ... def save_data(self) -> None: ... @@ -268,26 +299,6 @@ class AutoFormDelete(Protocol): ) -> None: ... -@dc.dataclass(slots=True) -class AutoFormConfig: - model: type[FlatBaseModel] - data_insert: AutoFormInsert - data_update: AutoFormUpdate - data_get: AutoFormGet - data_delete: AutoFormDelete - form_fields: Sequence[FormField] - ignored_keys: Iterable[str] = tuple() - add_buttons: bool = True - - -@dc.dataclass(slots=True) -class AutoForm_State: - session: Session - cfg: AutoFormConfig - rec_id: RecId | None = None - form_data: dict[str, Any] | None = None - - class CustomWidget(QWidget): def __init__( self, @@ -337,6 +348,26 @@ class Page_InitRecCompany_State(PageState[Module]): un_id: RecId | None = None +@dc.dataclass(slots=True, kw_only=True) +class AutoFormConfig: + model: type[FlatBaseModel] + data_insert: AutoFormInsert + data_update: AutoFormUpdate + data_get: AutoFormGet + data_delete: AutoFormDelete + form_fields: Sequence[FormField] + ignored_keys: Iterable[str] = tuple() + add_buttons: bool = True + + +@dc.dataclass(slots=True, kw_only=True) +class AutoForm_State(PageState[Module]): + session: Session + cfg: AutoFormConfig + rec_id: RecId | None = None + form_data: dict[str, Any] | None = None + + @dc.dataclass(slots=True, kw_only=True) class Page_InitRecPerson_State(PageState[Module]): session: Session @@ -415,6 +446,7 @@ def _add_widget_to_layout( def build_and_add_widget( form_field: FormField[W], parent_layout: QLayout, + widget_registry: WidgetRegistry | None = None, prefix: str = "", ) -> tuple[str, W]: no_scroll_filter = NoScrollFilter(parent_layout) @@ -609,6 +641,12 @@ def build_and_add_widget( _add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only) + if widget_registry is not None: + widget_registry[full_key] = { + "widget": widget, + "form_field": form_field, + } + return full_key, cast(W, widget) @@ -642,11 +680,17 @@ def _build_ui_recursively( parent_layout.addRow(group_box) case _: - full_key, widget = build_and_add_widget(field, parent_layout, prefix=prefix) - widget_registry[full_key] = { - "widget": widget, - "form_field": field, - } + full_key, _ = build_and_add_widget( + field, + parent_layout, + widget_registry=widget_registry, + prefix=prefix, + ) + # TODO remove + # widget_registry[full_key] = { + # "widget": widget, + # "form_field": field, + # } keys.append(full_key) return keys @@ -1010,6 +1054,7 @@ def validate_form_data( for key, registry_entry in widget_registry.items(): error_post: bool = False + custom_formatting: bool = False widget = registry_entry["widget"] form_field = registry_entry["form_field"] @@ -1041,16 +1086,10 @@ def validate_form_data( if widget.currentData() is not None: continue error_post = True - elif isinstance(widget, FlexibleDateInput): - date = widget.get_date() - if date and date.isValid(): - continue - error_post = True - elif isinstance(widget, FlexibleDateTimeInput): - datetime = widget.get_datetime() - if datetime and datetime.isValid(): - continue - error_post = True + elif isinstance(widget, (FlexibleDateInput, FlexibleDateTimeInput)): + valid = widget.validate() + error_post = not valid + custom_formatting = True elif isinstance( widget, ( @@ -1078,12 +1117,21 @@ def validate_form_data( 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; - """) + if not custom_formatting: + widget.setStyleSheet(""" + border: 1px solid #ef4444; + background-color: #ffe9e9; + padding: 4px; + border-radius: 4px; + """) + + return errors + + +def validate_child_modules(modules: Iterable[Module]) -> list[str]: + errors: list[str] = [] + for mod in modules: + errors.extend(mod.validate()) return errors @@ -1487,7 +1535,7 @@ class AutoForm(QWidget): self.reset_btn.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) - self.reset_btn.clicked.connect(self._reset_form) + 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)") @@ -1527,7 +1575,7 @@ class AutoForm(QWidget): QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S")) self.save_btn.setText(self.save_btn_txt_enabled) - def _activation_delete(self) -> None: + def _activate_delete(self) -> None: if self.STATE.rec_id is not None: self.delete_btn.setEnabled(True) else: @@ -1537,7 +1585,7 @@ class AutoForm(QWidget): assert self.STATE.rec_id is not None, "deletion initialised despite no index set" self.cfg.data_delete(self.STATE.rec_id) self.update_triggered.emit() - self._reset_form() + self.reset_form() def _load_data( self, @@ -1550,7 +1598,7 @@ class AutoForm(QWidget): if lookup_id is None: # only debugging lookup_id = self.STATE.rec_id - self._reset_form() + self.reset_form() logger_auto_form.debug("Lookup ID: %d", lookup_id) @@ -1572,15 +1620,12 @@ class AutoForm(QWidget): logger_auto_form.debug("Form data:\n%s", pformat(form_data)) self._set_form_data(form_data) self.STATE.rec_id = lookup_id - self._activation_delete() + self._activate_delete() - def _validate_form_data(self) -> list[str]: - return validate_form_data(self.widget_registry) - - def _reset_form(self) -> None: + def reset_form(self) -> None: reset_form(self.widget_registry) self.STATE.rec_id = None - self._activation_delete() + self._activate_delete() def _get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry) @@ -1602,32 +1647,32 @@ class AutoForm(QWidget): set_form_data(self.widget_registry, data) - def sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: self._load_data(self.STATE.rec_id) - def __sync_GUI_to_state(self) -> None: - form_data = self._get_form_data() - self.STATE.form_data = form_data + def _sync_GUI_to_state(self) -> None: + self.STATE.form_data = self._get_form_data() - def validate(self) -> None: ... + def validate(self) -> list[str]: + return validate_form_data(self.widget_registry) def load_state( self, new_state: AutoForm_State, ) -> None: - load_state(self.STATE, new_state) + 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() + self._sync_state_to_GUI() def get_state(self) -> AutoForm_State: - self.__sync_GUI_to_state() + self._sync_GUI_to_state() return self.STATE def save_data(self) -> None: self._disable_save() - errors = self._validate_form_data() + errors = self.validate() if errors: # errors: abort saving and show pop up window @@ -1696,7 +1741,7 @@ class AutoForm(QWidget): logger_auto_form.info("Data saved successfully") self.update_triggered.emit() - self._activation_delete() + self._activate_delete() finally: # always re-enable save, even if error occurred self._enable_save() @@ -1740,7 +1785,7 @@ class FlexibleDateInput(QWidget): self.button.clicked.connect(self.show_calendar) self.calendar.activated.connect(self.set_date) # signal to validate - self.line_edit.editingFinished.connect(self.validate_input) + self.line_edit.editingFinished.connect(self._validate_input) self.line_edit.installEventFilter(self) self.setReadOnly(False) @@ -1754,10 +1799,10 @@ class FlexibleDateInput(QWidget): if watched == self.line_edit: if event.type() == QEvent.Type.KeyPress: if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): # type: ignore - self.validate_input() + self._validate_input() return True # mark event as processed elif event.type() == QEvent.Type.FocusOut: - self.validate_input() + self._validate_input() # no return to not block focus change return super().eventFilter(watched, event) @@ -1798,7 +1843,7 @@ class FlexibleDateInput(QWidget): self.line_edit.setInputMask("99.99.9999;_") self.line_edit.setPlaceholderText("TT.MM.JJJJ") - def validate_input(self) -> None: + def _validate_input(self) -> None: logger_gui.debug("[Widget -- FlexibleDateInput] Trigger input validation") if self._read_only: return @@ -1824,6 +1869,16 @@ class FlexibleDateInput(QWidget): else: self.line_edit.setStyleSheet("") + def validate(self) -> bool: + logger_custom_fields.debug("[CustomField - FlexDate]: Call validate...") + self._validate_input() + date = self.get_date() + if date and date.isValid(): + logger_custom_fields.debug("[CustomField - FlexDate]: returning valid") + return True + logger_custom_fields.debug("[CustomField - FlexDate]: returning invalid") + return False + def set_date( self, date: QDate, @@ -1905,7 +1960,7 @@ class FlexibleDateTimeInput(QWidget): self.calendar.activated.connect(self.set_date_from_calendar) # signal to validate - self.line_edit.editingFinished.connect(self.validate_input) + self.line_edit.editingFinished.connect(self._validate_input) self.line_edit.installEventFilter(self) self.setReadOnly(False) @@ -1919,10 +1974,10 @@ class FlexibleDateTimeInput(QWidget): if watched == self.line_edit: if event.type() == QEvent.Type.KeyPress: if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): # type: ignore - self.validate_input() + self._validate_input() return True # mark event as processed elif event.type() == QEvent.Type.FocusOut: - self.validate_input() + self._validate_input() # no return to not block focus change return super().eventFilter(watched, event) @@ -1963,7 +2018,7 @@ class FlexibleDateTimeInput(QWidget): self.line_edit.setInputMask("99.99.9999 99:99;_") self.line_edit.setPlaceholderText("TT.MM.JJJJ hh:mm") - def validate_input(self) -> None: + def _validate_input(self) -> None: logger_gui.debug("[Widget -- FlexibleDateTimeInput] Trigger input validation") if self._read_only: return @@ -1990,6 +2045,16 @@ class FlexibleDateTimeInput(QWidget): else: self.line_edit.setStyleSheet("") + def validate(self) -> bool: + logger_custom_fields.debug("[CustomField - FlexDatetime]: Call validate...") + self._validate_input() + datetime = self.get_datetime() + if datetime and datetime.isValid(): + logger_custom_fields.debug("[CustomField - FlexDatetime]: returning valid") + return True + logger_custom_fields.debug("[CustomField - FlexDatetime]: returning invalid") + return False + def set_date_from_calendar( self, date: QDate, @@ -2692,10 +2757,10 @@ class Page_NewInitRec(QWidget): logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) self.person_requested.emit(req_state) - def __sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: pass - def __sync_GUI_to_state(self) -> None: + def _sync_GUI_to_state(self) -> None: pass def validate(self) -> None: @@ -2705,10 +2770,10 @@ class Page_NewInitRec(QWidget): self, new_state: Page_NewInitRec_State, ) -> None: - self.__sync_state_to_GUI() + self._sync_state_to_GUI() def get_state(self) -> Page_NewInitRec_State: - self.__sync_GUI_to_state() + self._sync_GUI_to_state() return self.STATE def save_data(self) -> None: @@ -2900,10 +2965,10 @@ class Page_InitRecCompany(QWidget): logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) self.company_profile_requested.emit(req_state) - def reset_form(self) -> None: - self.auto_form._reset_form() + def reset(self) -> None: + self.auto_form.reset_form() - def __sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: auto_form_state = AutoForm_State( session=self.STATE.session, cfg=self.AUTO_FORM_CFG, @@ -2912,26 +2977,22 @@ class Page_InitRecCompany(QWidget): ) self.auto_form.load_state(auto_form_state) - def __sync_GUI_to_state(self) -> None: - # TODO behaviour not consistent - self.auto_form.__sync_GUI_to_state() - - def validate(self) -> None: ... + def _sync_GUI_to_state(self) -> None: + auto_form_state = self.auto_form.get_state() + self.STATE.un_id = auto_form_state.rec_id def load_state( self, new_state: Page_InitRecCompany_State, ) -> None: - self.STATE.un_id = new_state.un_id + set_page_state(self.STATE, new_state) - self.__sync_state_to_GUI() + self._sync_state_to_GUI() def get_state(self) -> Page_InitRecCompany_State: - self.__sync_GUI_to_state() - return self.STATE + self._sync_GUI_to_state() - def save_data(self) -> None: - self.auto_form.save_data() + return self.STATE class Page_InitRecPerson(QWidget): @@ -3067,9 +3128,9 @@ class Page_InitRecPerson(QWidget): self.selection_county.update() def reset_form(self) -> None: - self.auto_form._reset_form() + self.auto_form.reset_form() - def __sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: auto_form_state = AutoForm_State( session=self.STATE.session, cfg=self.AUTO_FORM_CFG, @@ -3078,25 +3139,21 @@ class Page_InitRecPerson(QWidget): ) self.auto_form.load_state(auto_form_state) - def __sync_GUI_to_state(self) -> None: - self.auto_form.__sync_GUI_to_state() - - def validate(self) -> None: ... + def _sync_GUI_to_state(self) -> None: + auto_form_state = self.auto_form.get_state() + self.STATE.pers_id = auto_form_state.rec_id def load_state( self, new_state: Page_InitRecPerson_State, ) -> None: - self.STATE.pers_id = new_state.pers_id - - self.__sync_state_to_GUI() + set_page_state(self.STATE, new_state) + self._sync_state_to_GUI() def get_state(self) -> Page_InitRecPerson_State: - self.__sync_GUI_to_state() - return self.STATE + self._sync_GUI_to_state() - def save_data(self) -> None: - self.auto_form.save_data() + return self.STATE class Page_CompanyProfile(QWidget): @@ -3352,7 +3409,7 @@ class Page_CompanyProfile(QWidget): self.add_btn_pauschal.setEnabled(True) self.add_btn_individual.setEnabled(True) - def __sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: if self.STATE.rec_id is None: # just clear and do nothing else logger_gui.debug("just reset, ID is None") @@ -3376,7 +3433,7 @@ class Page_CompanyProfile(QWidget): self.table_master_data.load_data(data) self.set_company_name(data["Name:"]) - def __sync_GUI_to_state(self) -> None: ... + def _sync_GUI_to_state(self) -> None: ... def validate(self) -> None: ... @@ -3387,7 +3444,7 @@ class Page_CompanyProfile(QWidget): logger_gui.debug("Called function to load with ID: %s", new_state.rec_id) self.STATE.rec_id = new_state.rec_id - self.__sync_state_to_GUI() + self._sync_state_to_GUI() def get_state(self) -> Any: ... @@ -3715,9 +3772,14 @@ class Page_Consulting(QWidget): # layout_change_title.addWidget(change_title_btn) # change_title_btn.clicked.connect(self.set_company_name) # container_layout.addLayout(layout_change_title) - btn_get_data_table = QPushButton("Call 'get_data' from table") - btn_get_data_table.clicked.connect(self._debug_get_data_table) + btn_get_data_table = QPushButton("Debug call 'get_state' from table and page") + btn_get_data_table.clicked.connect(self._debug_get_state) container_layout.addWidget(btn_get_data_table) + btn_validate_data_table = QPushButton( + "Debuig call 'validate' from table and page" + ) + btn_validate_data_table.clicked.connect(self._debug_validate) + container_layout.addWidget(btn_validate_data_table) separator2 = QFrame() separator2.setFrameShape(QFrame.Shape.HLine) @@ -3747,24 +3809,6 @@ class Page_Consulting(QWidget): self.table_sessions = Page_Consulting_Table(site_state=self.STATE, state=table_state) self.STATE.child_modules.append(self.table_sessions) container_layout.addWidget(self.table_sessions) - # container_layout.addSpacing(50) - - # self.layout_sessions = QVBoxLayout() - # self.layout_sessions.setContentsMargins(0, 0, 0, 0) - # container_layout.addLayout(self.layout_sessions) - # self.btn_add_cons_session = QPushButton("+ Hinzufügen") - # self.btn_add_cons_session.setStyleSheet( - # "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" - # ) - # self.btn_add_cons_session.clicked.connect(self.add_cons_entry) - # self.layout_sessions.addWidget(self.btn_add_cons_session) - # self.layout_sessions.addStretch(1) - - # self.cons_entries: list[QWidget] = [] - - # self.layout_sessions.addStretch() - - # container_layout.addStretch() # --- STYLE --- self.setStyleSheet(""" @@ -3798,70 +3842,6 @@ class Page_Consulting(QWidget): # } # """) - # def add_cons_entry(self) -> None: - # logger_gui.debug("[Consulting Page] Adding entry...") - - # row_widget = QWidget() - # entry_row = QHBoxLayout(row_widget) - # entry_fields = QHBoxLayout() - # entry_row.addLayout(entry_fields) - - # txt1 = QLineEdit() - # txt2 = QPlainTextEdit() - # txt2.setMaximumHeight(100) - # entry_fields.addWidget(txt1) - # entry_fields.addWidget(txt2) - - # del_btn = QPushButton("🗑️") - # del_btn.setFixedSize(30, 30) - # # Lambda with default parameter to delete exactly this(!) box - # del_btn.clicked.connect( - # lambda checked=False, widget=row_widget: self.remove_cons_entry(widget) - # ) - # entry_row.addWidget(del_btn) - - # row_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) - # # row_widget.setMaximumHeight(200) - - # self.cons_entries.append(row_widget) - # self.layout_sessions.insertWidget(self.layout_sessions.count() - 2, row_widget) - - # def remove_cons_entry( - # self, - # row_widget: QWidget, - # ) -> None: - # logger_gui.debug("[Consulting Page]: Delete clicked...") - # self.layout_sessions.removeWidget(row_widget) - # row_widget.deleteLater() - # self.cons_entries.remove(row_widget) - - def _debug_get_data_table(self) -> None: - self.table_sessions.get_state() - - # TODO placeholder, check if needed - def __sync_state_to_GUI(self) -> None: - match self.STATE.cons_type: - case ConsultingType.PAUSCHAL: - self.type_pauschal_btn.setChecked(True) - case ConsultingType.INDIVIUAL: - self.type_individual_btn.setChecked(True) - - # deactivate if at least one ID is set (type is pre-defined by how - # the site is called) - if self.STATE.un_id or self.STATE.pers_id: - self.type_pauschal_btn.setEnabled(False) - self.type_individual_btn.setEnabled(False) - - # call to child modules not needed because these are initialised by their own - # "load_state" method which again calls the modules sync method - - def __sync_GUI_to_state(self) -> None: - # call to child modules not needed because their syncing method is called when - # the "get_state" method is called --> this has to be managed within this method - - table_state = self.table_sessions.get_state() - self.STATE.cons_sessions = table_state.row_states - def type_consulting_changed( self, button_id: int, @@ -3879,21 +3859,68 @@ class Page_Consulting(QWidget): logger_page_consulting.debug("[Consulting Page] Selected cons_type: %s", cons_type) self.STATE.cons_type = cons_type - def load_from_state( + def _debug_get_state(self) -> None: + logger_page_consulting.debug("[Consulting Page] Call get state on table...") + _ = self.table_sessions.get_state() + logger_page_consulting.debug("[Consulting Page] Call get state on page...") + state = self.get_state() + logger_page_consulting.debug("[Consulting Page] Page state:\n%s", state) + + def _debug_validate(self) -> None: + logger_page_consulting.debug("[Consulting Page] Call validate data on table...") + errors = self.table_sessions.validate() + logger_page_consulting.debug("[Consulting Page] Table errors:\n%s", pformat(errors)) + logger_page_consulting.debug("[Consulting Page] Call get state on page...") + errors = self.validate() + logger_page_consulting.debug("[Consulting Page] Page errors:\n%s", pformat(errors)) + + def _sync_state_to_GUI(self) -> None: + match self.STATE.cons_type: + case ConsultingType.PAUSCHAL: + self.type_pauschal_btn.setChecked(True) + case ConsultingType.INDIVIUAL: + self.type_individual_btn.setChecked(True) + + # deactivate if at least one ID is set (type is pre-defined by how + # the site is called) + if self.STATE.un_id or self.STATE.pers_id: + self.type_pauschal_btn.setEnabled(False) + self.type_individual_btn.setEnabled(False) + + # call to child modules' syncing method not needed because these are initialised by + # their own "load_state" method which again calls the modules sync method + + def _sync_GUI_to_state(self) -> None: + # call to child modules' syncing method not needed because their syncing method is + # called when the "get_state" method is called --> this has to be managed within + # this method + table_state = self.table_sessions.get_state() + self.STATE.cons_sessions = table_state.row_states + + def validate(self) -> list[str]: + errors: list[str] = [] + errors.extend(self.table_sessions.validate()) + + return errors + + def load_state( 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) - load_state(self.STATE, new_state) - # self.STATE.un_id = new_state.un_id - # self.STATE.pers_id = new_state.pers_id - # self.STATE.cons_type = new_state.cons_type + set_page_state(self.STATE, new_state) + # child modules table_state = Page_Consulting_Table_State(row_states=self.STATE.cons_sessions) self.table_sessions.load_state(table_state) - self.__sync_state_to_GUI() + self._sync_state_to_GUI() + + def get_state(self) -> Page_Consulting_State: + self._sync_GUI_to_state() + + return self.STATE class Page_Consulting_ConsultingSession(QWidget): @@ -3918,40 +3945,91 @@ class Page_Consulting_ConsultingSession(QWidget): layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) ROW_HEIGHT = 70 - # ?? widget registry? + self.widget_registry: WidgetRegistry = {} user_field_def = FormField[QLineEdit]( - "", + "Nutzername", FormFieldType.TEXT, readonly=True, placeholder="wird über aktiven Nutzer bei Anlage gesetzt", + enable_uuid_key=True, ) _, self.user_field = build_and_add_widget(user_field_def, layout) # ?? date or datetime? - timestamp_def = FormField[FlexibleDateTimeInput]("", FormFieldType.DATETIME_FLEXIBLE) - _, self.timestamp = build_and_add_widget(timestamp_def, layout) + timestamp_def = FormField[FlexibleDateTimeInput]( + "Zeitstempel", + FormFieldType.DATETIME_FLEXIBLE, + required=True, + enable_uuid_key=True, + ) + _, self.timestamp = build_and_add_widget( + timestamp_def, + layout, + widget_registry=self.widget_registry, + ) self.timestamp.set_pydatetime(datetime.datetime.now()) - contact_def = FormField[QLineEdit]("", FormFieldType.TEXT, placeholder="Freitext") - _, self.contact = build_and_add_widget(contact_def, layout) + contact_def = FormField[QLineEdit]( + "Kontaktperson", + FormFieldType.TEXT, + placeholder="Freitext", + required=True, + enable_uuid_key=True, + ) + _, self.contact = build_and_add_widget( + contact_def, + layout, + widget_registry=self.widget_registry, + ) contact_type_def = FormField[QComboBox]( - "", + "Kontakttyp", FormFieldType.DROPDOWN, - placeholder="sollte Dropdown sein", options=[("Persönlich", None), ("Telefon", None), ("Mail", None)], + required=True, + enable_uuid_key=True, + ) + _, self.contact_type = build_and_add_widget( + contact_type_def, + layout, + widget_registry=self.widget_registry, ) - _, self.contact_type = build_and_add_widget(contact_type_def, layout) - crm_link_def = FormField[QLineEdit]("", FormFieldType.TEXT, placeholder="CRM Links") - _, self.crm_link = build_and_add_widget(crm_link_def, layout) + crm_link_def = FormField[QLineEdit]( + "CRM-Verknüpfung", + FormFieldType.TEXT, + placeholder="CRM Links", + required=True, + enable_uuid_key=True, + ) + _, self.crm_link = build_and_add_widget( + crm_link_def, + layout, + widget_registry=self.widget_registry, + ) - plain_text_def = FormField[QPlainTextEdit]("", FormFieldType.LONGTEXT) - _, self.plain_text = build_and_add_widget(plain_text_def, layout) + plain_text_def = FormField[QPlainTextEdit]( + "Anmerkungen", + FormFieldType.LONGTEXT, + enable_uuid_key=True, + ) + _, self.plain_text = build_and_add_widget( + plain_text_def, + layout, + widget_registry=self.widget_registry, + ) - response_def = FormField[QPlainTextEdit]("", FormFieldType.LONGTEXT) - _, self.response = build_and_add_widget(response_def, layout) + response_def = FormField[QPlainTextEdit]( + "Rückmeldung", + FormFieldType.LONGTEXT, + enable_uuid_key=True, + ) + _, self.response = build_and_add_widget( + response_def, + layout, + widget_registry=self.widget_registry, + ) self.btn_delete = QPushButton("✕") self.btn_delete.setFlat(True) # Entfernt den blockigen Rahmen @@ -3991,9 +4069,9 @@ class Page_Consulting_ConsultingSession(QWidget): layout.setStretch(i, stretch) self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) - self.__sync_state_to_GUI() # ?? only needed if entry is from database? + self._sync_state_to_GUI() # ?? only needed if entry is from database? - def __sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: logger_page_consulting.debug( "[Page Consulting -- Consulting Session]: State = %s", self.STATE ) @@ -4005,7 +4083,7 @@ class Page_Consulting_ConsultingSession(QWidget): set_widget_value(self.plain_text, self.STATE.anmerkungen) set_widget_value(self.response, self.STATE.rueckmeldung) - def __sync_GUI_to_state(self) -> None: + def _sync_GUI_to_state(self) -> None: self.STATE.zeitstempel = self.timestamp.get_pydatetime() self.STATE.ansprechpartner = get_widget_value(self.contact) self.STATE.interaction_scheme = get_widget_value(self.contact_type) @@ -4014,12 +4092,16 @@ class Page_Consulting_ConsultingSession(QWidget): self.STATE.rueckmeldung = get_widget_value(self.response) def get_state(self) -> Page_Consulting_ConsultingSession_State: - self.__sync_GUI_to_state() + self._sync_GUI_to_state() return self.STATE - def validate(self) -> None: - pass + def validate(self) -> list[str]: + logger_page_consulting.debug( + ">>>>> Call validate on Session. Widget registry:\n%s", + pprint_registry(self.widget_registry), + ) + return validate_form_data(self.widget_registry) def save_data(self) -> None: pass @@ -4028,13 +4110,13 @@ class Page_Consulting_ConsultingSession(QWidget): self, new_state: Page_Consulting_ConsultingSession_State, ) -> None: - load_state(self.STATE, new_state) - self.__sync_state_to_GUI() + set_page_state(self.STATE, new_state) + self._sync_state_to_GUI() -def load_state( - target: T, - source: T, +def set_page_state( + target: S, + source: S, deep: bool = False, ) -> None: """ @@ -4047,6 +4129,9 @@ def load_state( raise TypeError("Both source and target must have the same type") for field in dc.fields(source): + if field.name == "child_modules": + continue + new_value = getattr(source, field.name) if deep: @@ -4248,12 +4333,12 @@ class Page_Consulting_Table(QWidget): while self.STATE.child_modules: self.remove_row(self.STATE.child_modules[0]) - def __sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self) -> None: self.reset() for row_state in self.STATE.row_states: self.add_row(row_state) - def __sync_GUI_to_state(self) -> None: + def _sync_GUI_to_state(self) -> None: logger_page_consulting.debug("[Page -- Consulting] Table: Call syncing GUI to state") row_data: list[Page_Consulting_ConsultingSession_State] = [] @@ -4281,7 +4366,7 @@ class Page_Consulting_Table(QWidget): def get_state(self) -> Page_Consulting_Table_State: logger_page_consulting.debug("[Page Consulting -- Table] Syncing GUI to state...") - self.__sync_GUI_to_state() + self._sync_GUI_to_state() logger_page_consulting.debug("[Page Consulting -- Table]: State = %s", self.STATE) @@ -4294,12 +4379,16 @@ class Page_Consulting_Table(QWidget): logger_page_consulting.debug( "[Page -- Consulting] Table: Call loading from state, %s", new_state ) - self.STATE.row_states = new_state.row_states + set_page_state(self.STATE, new_state) - self.__sync_state_to_GUI() + self._sync_state_to_GUI() - def validate(self) -> None: - pass + def validate(self) -> list[str]: + errors: list[str] = [] + for m in self.STATE.child_modules: + errors.extend(m.validate()) + + return errors def save_data(self) -> None: pass @@ -4578,7 +4667,7 @@ class MainWindow(QMainWindow): self, new_state: Page_Consulting_State, ) -> None: - self.consulting_page.load_from_state(new_state) + self.consulting_page.load_state(new_state) self._navigate_to_target(self.consulting_page) # --- MENÜ LOGIK --- @@ -4632,6 +4721,15 @@ def qt_message_handler(mode, context, message): logger_gui.critical(message) +if TYPE_CHECKING: + # TODO add all relevant modules + # enable static type checking for the protocol + _m1: type[Module] = Page_Consulting + _m2: type[Module] = Page_Consulting_Table + _m3: type[Module] = Page_Consulting_ConsultingSession + _w1: type[WrapperModule] = Page_InitRecCompany + _w2: type[WrapperModule] = Page_InitRecPerson + if __name__ == "__main__": sys.excepthook = global_exception_handler qInstallMessageHandler(qt_message_handler) diff --git a/src/wce_crm/logging.py b/src/wce_crm/logging.py index 9d0cfd7..db7b996 100644 --- a/src/wce_crm/logging.py +++ b/src/wce_crm/logging.py @@ -37,6 +37,9 @@ logger_get_data.setLevel(logging.DEBUG) logger_auto_form = logger_gui.getChild("get_data_auto_form") logger_auto_form.setLevel(logging.DEBUG) +logger_custom_fields = logger_gui.getChild("custom_fields") +logger_custom_fields.setLevel(logging.DEBUG) + logger_page_consulting = logger_gui.getChild("page_consulting") logger_page_consulting.setLevel(logging.DEBUG)