diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index ec826cd..49fd522 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -16,6 +16,7 @@ from typing import ( TYPE_CHECKING, Any, Final, + Generic, Literal, Protocol, TypeAlias, @@ -103,8 +104,9 @@ if TYPE_CHECKING: K = TypeVar("K") V = TypeVar("V") -T = TypeVar("T", bound="QWidget") - +T = TypeVar("T") +W = TypeVar("W", bound="QWidget") +M = TypeVar("M", bound="Module") DEBUG: bool = True DEBUG_SEARCH_WIDGET: bool = False @@ -224,9 +226,9 @@ class CustomForm(Protocol): class Module(Protocol): - def sync_state_to_GUI(self) -> None: ... + def __sync_state_to_GUI(self) -> None: ... # type: ignore - def sync_GUI_to_state(self) -> None: ... + def __sync_GUI_to_state(self) -> None: ... # type: ignore def validate(self) -> None: ... @@ -314,24 +316,29 @@ class Session: @dc.dataclass(slots=True) -class Page_MainPage_State: +class PageState(Generic[M]): + child_modules: list[M] = dc.field(default_factory=list) + + +@dc.dataclass(slots=True, kw_only=True) +class Page_MainPage_State(PageState[Module]): session: Session row_count: int = 0 -@dc.dataclass(slots=True) -class Page_NewInitRec_State: +@dc.dataclass(slots=True, kw_only=True) +class Page_NewInitRec_State(PageState[Module]): session: Session -@dc.dataclass(slots=True) -class Page_InitRecCompany_State: +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRecCompany_State(PageState[Module]): session: Session un_id: RecId | None = None -@dc.dataclass(slots=True) -class Page_InitRecPerson_State: +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRecPerson_State(PageState[Module]): session: Session pers_id: RecId | None = None @@ -341,8 +348,8 @@ class ConsultingType(enum.StrEnum): INDIVIUAL = enum.auto() -@dc.dataclass(slots=True) -class Page_Consulting_State: +@dc.dataclass(slots=True, kw_only=True) +class Page_Consulting_State(PageState[Module]): session: Session un_id: RecId | None = None pers_id: RecId | None = None @@ -352,8 +359,8 @@ class Page_Consulting_State: ) -@dc.dataclass(slots=True) -class Page_Consulting_ConsultingSession_State: +@dc.dataclass(slots=True, kw_only=True) +class Page_Consulting_ConsultingSession_State(PageState[Module]): nutzer_id: UserId # must be set with the session (always known) nutzer_name: str # must be set with the session (always known) beratung_id: int | None = None # known and not known entries @@ -369,20 +376,19 @@ class Page_Consulting_ConsultingSession_State: rueckmeldung: str | None = None -@dc.dataclass(slots=True) -class Page_Consulting_Table_State: - rows: list[Page_Consulting_ConsultingSession] = dc.field(default_factory=list) +@dc.dataclass(slots=True, kw_only=True) +class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"]): row_states: list[Page_Consulting_ConsultingSession_State] = dc.field(default_factory=list) -@dc.dataclass(slots=True) -class Page_CompanyProfile_State: +@dc.dataclass(slots=True, kw_only=True) +class Page_CompanyProfile_State(PageState[Module]): session: Session rec_id: RecId | None = None def _add_widget_to_layout( - form_field: FormField[T], + form_field: FormField[W], widget: QWidget, target_layout: QLayout, widget_only: bool = False, @@ -407,10 +413,10 @@ def _add_widget_to_layout( def build_and_add_widget( - form_field: FormField[T], + form_field: FormField[W], parent_layout: QLayout, prefix: str = "", -) -> tuple[str, T]: +) -> tuple[str, W]: no_scroll_filter = NoScrollFilter(parent_layout) if prefix and form_field.key: full_key = f"{prefix}{COLUMN_SEP}{form_field.key}" if prefix else form_field.key @@ -603,7 +609,7 @@ def build_and_add_widget( _add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only) - return full_key, cast(T, widget) + return full_key, cast(W, widget) def _build_ui_recursively( @@ -1599,7 +1605,7 @@ class AutoForm(QWidget): def sync_state_to_GUI(self) -> None: self._load_data(self.STATE.rec_id) - def sync_GUI_to_state(self) -> None: + def __sync_GUI_to_state(self) -> None: form_data = self._get_form_data() self.STATE.form_data = form_data @@ -1609,12 +1615,13 @@ class AutoForm(QWidget): self, new_state: AutoForm_State, ) -> None: - self.STATE.rec_id = new_state.rec_id - self.STATE.form_data = new_state.form_data + load_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: - self.sync_GUI_to_state() + self.__sync_GUI_to_state() return self.STATE @@ -2685,10 +2692,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: @@ -2698,10 +2705,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: @@ -2896,7 +2903,7 @@ class Page_InitRecCompany(QWidget): def reset_form(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, @@ -2905,8 +2912,9 @@ class Page_InitRecCompany(QWidget): ) self.auto_form.load_state(auto_form_state) - def sync_GUI_to_state(self) -> None: - self.auto_form.sync_GUI_to_state() + def __sync_GUI_to_state(self) -> None: + # TODO behaviour not consistent + self.auto_form.__sync_GUI_to_state() def validate(self) -> None: ... @@ -2916,10 +2924,10 @@ class Page_InitRecCompany(QWidget): ) -> None: self.STATE.un_id = new_state.un_id - self.sync_state_to_GUI() + self.__sync_state_to_GUI() def get_state(self) -> Page_InitRecCompany_State: - self.sync_GUI_to_state() + self.__sync_GUI_to_state() return self.STATE def save_data(self) -> None: @@ -3061,7 +3069,7 @@ class Page_InitRecPerson(QWidget): def reset_form(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, @@ -3070,8 +3078,8 @@ 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 __sync_GUI_to_state(self) -> None: + self.auto_form.__sync_GUI_to_state() def validate(self) -> None: ... @@ -3081,10 +3089,10 @@ class Page_InitRecPerson(QWidget): ) -> None: self.STATE.pers_id = new_state.pers_id - self.sync_state_to_GUI() + self.__sync_state_to_GUI() def get_state(self) -> Page_InitRecPerson_State: - self.sync_GUI_to_state() + self.__sync_GUI_to_state() return self.STATE def save_data(self) -> None: @@ -3344,7 +3352,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") @@ -3368,7 +3376,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: ... @@ -3379,7 +3387,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: ... @@ -3737,6 +3745,7 @@ class Page_Consulting(QWidget): # container_layout.addSpacing(50) table_state = Page_Consulting_Table_State() 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) @@ -3827,23 +3836,31 @@ class Page_Consulting(QWidget): # self.cons_entries.remove(row_widget) def _debug_get_data_table(self) -> None: - self.table_sessions.get_data() + self.table_sessions.get_state() # TODO placeholder, check if needed - def sync_state_to_GUI(self) -> None: + 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) - def sync_GUI_to_state(self) -> None: ... + # 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, ) -> None: - logger_gui.debug("[Consulting Page] Trigger type buttons...") + logger_page_consulting.debug("[Consulting Page] Trigger type buttons...") cons_type: ConsultingType if button_id == 0: @@ -3853,7 +3870,7 @@ class Page_Consulting(QWidget): else: raise RuntimeError(f"No consulting type defined for button id: {button_id}") - logger_gui.debug("[Consulting Page] Selected cons_type: %s", cons_type) + logger_page_consulting.debug("[Consulting Page] Selected cons_type: %s", cons_type) self.STATE.cons_type = cons_type def load_from_state( @@ -3862,12 +3879,15 @@ class Page_Consulting(QWidget): ) -> None: # TODO add correct loading behaviour # TODO add validation (correct type with necessary information) - logger_gui.debug("[Consulting] Loading with request:\n%s", 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 + 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 + 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() class Page_Consulting_ConsultingSession(QWidget): @@ -3885,12 +3905,15 @@ class Page_Consulting_ConsultingSession(QWidget): assert isinstance(state, Page_Consulting_ConsultingSession_State) self.SITE_STATE = site_state self.STATE = state + layout = QHBoxLayout(self) layout.setContentsMargins(0, 2, 0, 2) # Sehr kompakt layout.setSpacing(10) layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) ROW_HEIGHT = 70 + # ?? widget registry? + user_field_def = FormField[QLineEdit]( "", FormFieldType.TEXT, @@ -3962,10 +3985,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: - # TODO set correct timestamp + def __sync_state_to_GUI(self) -> None: logger_page_consulting.debug( "[Page Consulting -- Consulting Session]: State = %s", self.STATE ) @@ -3977,7 +3999,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) @@ -3985,11 +4007,47 @@ class Page_Consulting_ConsultingSession(QWidget): self.STATE.anmerkungen = get_widget_value(self.plain_text) self.STATE.rueckmeldung = get_widget_value(self.response) - def get_data(self) -> Page_Consulting_ConsultingSession_State: - self.sync_gui_to_state() + def get_state(self) -> Page_Consulting_ConsultingSession_State: + self.__sync_GUI_to_state() return self.STATE + def validate(self) -> None: + pass + + def save_data(self) -> None: + pass + + def load_state( + self, + new_state: Page_Consulting_ConsultingSession_State, + ) -> None: + load_state(self.STATE, new_state) + self.__sync_state_to_GUI() + + +def load_state( + target: T, + source: T, + deep: bool = False, +) -> None: + """ + transfer all field values generically from 'source' to 'target' + """ + if not (dc.is_dataclass(target) and dc.is_dataclass(source)): + raise TypeError("Both source and atrget must be dataclasses") + + if type(target) is not type(source): + raise TypeError("Both source and target must have the same type") + + for field in dc.fields(source): + new_value = getattr(source, field.name) + + if deep: + new_value = copy.deepcopy(new_value) + + setattr(target, field.name, new_value) + class Page_Consulting_Table(QWidget): """child of 'Page_Consulting'""" @@ -4004,6 +4062,7 @@ class Page_Consulting_Table(QWidget): self.resize(600, 400) self.STATE = state + # TODO eliminate site state self.SITE_STATE = site_state main_layout = QVBoxLayout(self) @@ -4154,11 +4213,6 @@ class Page_Consulting_Table(QWidget): # } # """) - if DEBUG: - # add rows for debugging - self.add_row() - self.add_row() - def add_row( self, row_state: Page_Consulting_ConsultingSession_State | None = None, @@ -4174,47 +4228,76 @@ class Page_Consulting_Table(QWidget): index = self.container_layout.count() - 1 self.container_layout.insertWidget(index, row) - self.STATE.rows.append(row) + self.STATE.child_modules.append(row) def remove_row( self, table_row_widget: Page_Consulting_ConsultingSession, ) -> None: self.container_layout.removeWidget(table_row_widget) - self.STATE.rows.remove(table_row_widget) + self.STATE.child_modules.remove(table_row_widget) table_row_widget.deleteLater() def reset(self) -> None: - while self.STATE.rows: - self.remove_row(self.STATE.rows[0]) + 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] = [] - for i in range(self.container_layout.count() - 1): - item = self.container_layout.itemAt(i) - assert item, "item not found" - widget = item.widget() - assert widget, "no widget obtained" + logger_page_consulting.debug( + "[Page -- Consulting] Table - Child modules: %s", + pformat(self.STATE.child_modules), + ) + for row in self.STATE.child_modules: + row_data.append(row.get_state()) - if isinstance(widget, Page_Consulting_ConsultingSession): - row_data.append(widget.get_data()) + # TODO remove + # for i in range(self.container_layout.count() - 1): + # item = self.container_layout.itemAt(i) + # assert item, "item not found" + # widget = item.widget() + # assert widget, "no widget obtained" - assert len(row_data) == len(self.STATE.rows), "mismatch in row count and data length" + # if isinstance(widget, Page_Consulting_ConsultingSession): + # row_data.append(widget.get_state()) + + assert len(row_data) == len(self.STATE.child_modules), ( + "mismatch in row count and data length" + ) self.STATE.row_states = row_data - def get_data(self) -> Page_Consulting_Table_State: + 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) return self.STATE + def load_state( + self, + new_state: Page_Consulting_Table_State, + ) -> None: + logger_page_consulting.debug( + "[Page -- Consulting] Table: Call loading from state, %s", new_state + ) + self.STATE.row_states = new_state.row_states + + self.__sync_state_to_GUI() + + def validate(self) -> None: + pass + + def save_data(self) -> None: + pass + ####################################################################