diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index a3c1ad3..d806d35 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -71,6 +71,7 @@ setup_logging(enable_stderr=True) DEBUG: Final[bool] = True DEBUG_SEARCH_WIDGET: Final[bool] = False +DEBUG_NO_DATABASE: Final[bool] = False logger = BASE_LOGGER.getChild("wce") logger.setLevel(logging.DEBUG) @@ -78,8 +79,8 @@ 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.INFO) -logger_get_data_auto_form = logger.getChild("get_data_auto_form") -logger_get_data_auto_form.setLevel(logging.DEBUG) +logger_auto_form = logger.getChild("get_data_auto_form") +logger_auto_form.setLevel(logging.DEBUG) QSS = """ *[styleClass="stempel"] { @@ -377,7 +378,8 @@ class FormFieldType(enum.StrEnum): DROPDOWN = enum.auto() EXTENDED_DROPDOWN = enum.auto() DYNAMIC_LIST = enum.auto() - DYNAMIC_DROPDOWN = enum.auto() + DYNAMIC_DROPDOWN_NUMERIC = enum.auto() + DYNAMIC_DROPDOWN_BOOLEAN = enum.auto() TEXT_SEARCH = enum.auto() CUSTOM = enum.auto() TEXT_DATE = enum.auto() @@ -447,7 +449,7 @@ class FormField: self.dropdown_options = tuple(DropdownOption(op[0], op[1]) for op in options) if self.children: - self.required = any((child.required for child in self.children)) + self.required = self.required or any((child.required for child in self.children)) for child in self.children: child.parent = self @@ -739,9 +741,13 @@ def _build_ui_recursively( } parent_layout.addRow(widget) - case FormFieldType.DYNAMIC_DROPDOWN: + case ( + FormFieldType.DYNAMIC_DROPDOWN_NUMERIC + | FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN + ): widget = DynamicDropdownWidget( field.children, + field.type, field.label, prefix=f"{full_key}", ) @@ -1136,7 +1142,7 @@ class Grunderfassung_Unternehmen(FlatBaseModel): Grunderfassung_notiz: str | None Partnersuche: Grunderfassung_PartnerSuche - Projektrelevanz: Grunderfassung_Projektrelevanz + Projektrelevanz: Grunderfassung_ProjektrelevanzStatus Kontaktperson: Grunderfassung_Kontaktperson Stammdaten: Grunderfassung_Stammdaten WeitereInfos: Grunderfassung_WeitereInfos @@ -1154,10 +1160,17 @@ class Grunderfassung_PartnerSuche(BaseModel): kanal_aufmerksamkeit: str | None -class Grunderfassung_Projektrelevanz(BaseModel): +class Grunderfassung_ProjektrelevanzStatus(BaseModel): + model_config = ConfigDict(str_strip_whitespace=True) + + status: Grunderfassung_ProjektrelevanzStatus_Status + + +class Grunderfassung_ProjektrelevanzStatus_Status(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) relevanz: bool + foerderperiode: list[str | None] | None = None @field_validator("relevanz", mode="before") @classmethod @@ -1563,6 +1576,8 @@ def search_widgets_by_key( class AutoForm(QWidget): + """a widget, which is managed by a code-defined field definition collection""" + save_clicked_form = Signal() # formular saved (data changed for front page) def __init__( @@ -1609,6 +1624,11 @@ class AutoForm(QWidget): self.main_layout.setContentsMargins(0, 0, 0, 0) if DEBUG: + separator1 = QFrame() + separator1.setFrameShape(QFrame.Shape.HLine) + separator1.setFrameShadow(QFrame.Shadow.Sunken) + self.main_layout.addWidget(separator1) + self.test_button = QPushButton("Initialisiere Laden") self.test_button.clicked.connect(self.load_data) self.test_button.setFixedHeight(50) @@ -1632,6 +1652,12 @@ class AutoForm(QWidget): button_db_index.clicked.connect(self._set_db_index) self.main_layout.addWidget(button_db_index) + separator2 = QFrame() + separator2.setFrameShape(QFrame.Shape.HLine) + separator2.setFrameShadow(QFrame.Shadow.Sunken) + self.main_layout.addWidget(separator2) + self.main_layout.addSpacing(5) + self.main_layout.addSpacing(10) self.top_level_form_layout = QFormLayout() @@ -1697,29 +1723,32 @@ class AutoForm(QWidget): self, lookup_id: int | None = None, ) -> None: - # TODO change logic to database backend - logger_get_data.info(">>>> LOAD CLICKED") + if DEBUG_NO_DATABASE: + return + + logger_auto_form.info(">>>> LOAD CLICKED") if lookup_id is None or lookup_id == 0: lookup_id = self.current_id self.reset_form() - logger_get_data_auto_form.debug("Lookup ID: %d", lookup_id) + logger_auto_form.debug("Lookup ID: %d", lookup_id) if lookup_id > 0: - logger_get_data_auto_form.debug("Load from DB:") + logger_auto_form.debug("Load from DB:") loaded_data = self.cfg.data_get(lookup_id) + # TODO remove loading from file (obsolete) else: - logger_get_data_auto_form.debug("Load from pickled object:") + logger_auto_form.debug("Load from pickled object:") loaded_data = load_pydantic_model_dict_db() - logger_get_data_auto_form.debug( + logger_auto_form.debug( "Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data) ) model = Grunderfassung_Unternehmen(**loaded_data) - logger_get_data_auto_form.debug("Loaded to Pydantic.") - logger_get_data_auto_form.debug("Convert to GUI structure...") + logger_auto_form.debug("Loaded to Pydantic.") + logger_auto_form.debug("Convert to GUI structure...") form_data = model.to_gui() - logger_get_data_auto_form.debug("Set form data...") + logger_auto_form.debug("Set form data...") # logger_get_data_auto_form.debug("Form data:\n%s", pformat(form_data)) self.set_form_data(form_data) self.current_id = lookup_id @@ -1739,17 +1768,18 @@ class AutoForm(QWidget): return # no errors: data can be saved - logger.info("Erfolg! Alle Daten sind valide.") - logger.info("Get form data call...") + logger_auto_form.info("Erfolg! Alle Daten sind valide.") + logger_auto_form.info("Get form data call...") form_data = self.get_form_data() - logger.debug("\n\n------------>>>>>>>>> Get form data\n%s", pformat(form_data)) - logger.debug("------------>>>>>>>>> Call Pydantic") + logger_auto_form.debug( + "\n\n------------>>>>>>>>> Get form data\n%s", pformat(form_data) + ) try: + logger_auto_form.debug("------------>>>>>>>>> Call Pydantic") validated_data = self.cfg.model(**form_data) # # TODO add user? - # logger.debug("%s", pformat(validated_data.model_dump())) except ValidationError as e: # catch errors and show them in GUI error_texts = [] @@ -1773,23 +1803,25 @@ class AutoForm(QWidget): # tell user what went wrong QMessageBox.warning(self, "Eingabefehler", "\n".join(error_texts)) else: + if DEBUG_NO_DATABASE: + return # !! this code is only called if the 'try' block was successful # save_pydantic_model_dict_db(validated_data) # TODO save data to database db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) - logger.debug( + logger_auto_form.debug( ("Form data with 'exlude' (must be saved in the database):\n%s"), pformat(db_data), ) if self.current_id < 0: - logger.debug("Insert triggered") + logger_auto_form.debug("Insert triggered") self.cfg.data_insert(db_data) else: - logger.debug("Update triggered") + logger_auto_form.debug("Update triggered") self.cfg.data_update(self.current_id, db_data) - logger_get_data.info("Data saved successfully") + logger_auto_form.info("Data saved successfully") self.save_clicked_form.emit() self.reset_form() finally: @@ -1805,8 +1837,8 @@ class AutoForm(QWidget): def get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry) - logger_get_data.debug("\n\n>>>>>>> [AutoForm] Call get form data:") - logger_get_data.debug("Form Data:\n%s", pformat(form_data)) + logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call get form data:") + logger_auto_form.debug("Form Data:\n%s", pformat(form_data)) return form_data @@ -1819,7 +1851,7 @@ class AutoForm(QWidget): class DynamicListWidget(QWidget): """ - A Widget, which can generate and manage an arbitrary number of sub forms. + a widget, which can generate and manage an arbitrary number of sub forms. """ def __init__( @@ -1899,15 +1931,12 @@ class DynamicListWidget(QWidget): self.update_sub_forms() def update_sub_forms(self): - # TODO: check if needed update_sub_forms( self.widget_registry, sub_forms=self.sub_forms, base_label=self.base_label, ) - # pprint_registry(self.widget_registry) - def reset_form(self) -> None: while self.sub_forms: self.remove_entry(self.sub_forms[0]) @@ -1923,6 +1952,7 @@ class DynamicListWidget(QWidget): return errors def get_form_data(self) -> list[dict[str, Any]]: + # TODO code cleansing # raw_data = get_form_data(self.widget_registry) # form_data = get_form_data(self.widget_registry) # logger_get_data.debug( @@ -1975,15 +2005,21 @@ class DynamicListWidget(QWidget): set_form_data(current_sub_form.registry, sub_form_data) +class DynamicDropdownType(enum.IntEnum): + NUMERIC = enum.auto() + BOOLEAN = enum.auto() + + class DynamicDropdownWidget(QWidget): """ A Widget, which can generate and manage an arbitrary number of sub forms with additional - information on a combobox selection (integer in combobox). + information on a combobox selection (combobox). """ def __init__( self, form_fields: Sequence[FormField], + type: FormFieldType, label_add_info: str = "Eintrag", prefix: str = "", ): @@ -2004,7 +2040,7 @@ class DynamicDropdownWidget(QWidget): "child, which is a single field definition" ) ) - + self.type = type self.assigned_form_field = assigned_form_fields[0] self.label_add_info = label_add_info @@ -2035,27 +2071,38 @@ class DynamicDropdownWidget(QWidget): self.sub_forms: list[SubForm] = [] - self.dropdown_widget.currentTextChanged.connect(self.on_anzahl_changed) + self.dropdown_widget.currentTextChanged.connect(self._selection_changed_changed) - def on_anzahl_changed( + def _selection_changed_changed( self, text: str, ) -> None: - target_count: int - if text == DROPDOWN_DEFAULT: - target_count = 0 - else: - target_count = int(text) current_count = len(self.sub_forms) + value: int + match self.type: + case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC: + if text == DROPDOWN_DEFAULT: + value = 0 + else: + value = int(text) + current_count = len(self.sub_forms) - if target_count > current_count: - differenz = target_count - current_count - for _ in range(differenz): + case FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN: + if text == "ja": + value = 1 + else: + value = 0 + + case _: + raise ValueError("Undefined DynamicDropdownWidget type") + + if value > current_count: + diff = value - current_count + for _ in range(diff): self._add_row() - - elif target_count < current_count: - differenz = current_count - target_count - for _ in range(differenz): + elif value < current_count: + diff = current_count - value + for _ in range(diff): self._remove_row() def _add_row(self) -> None: @@ -2067,7 +2114,8 @@ class DynamicDropdownWidget(QWidget): sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form) form_field_def = copy.copy(self.assigned_form_field) - form_field_def.label = form_field_def.enhanced_label(f"{number_form}") + if self.type is FormFieldType.DYNAMIC_DROPDOWN_NUMERIC: + form_field_def.label = form_field_def.enhanced_label(f"{number_form}") sub_form.full_keys = _build_ui_recursively( schema=[form_field_def], @@ -2265,21 +2313,6 @@ class NewEntrySelect_view(QWidget): layout.addWidget(btn_company) layout.addWidget(btn_person) - # self.info_label = QLabel("Zusatzinfos...") - # self.info_label.setWordWrap(True) - # self.info_label.setStyleSheet("font-size: 16px; color: #475569; margin-top: 10px;") - # layout.addWidget(self.info_label) - - # def update_content(self, data): - # # Diese Methode füllt die Seite mit den echten Daten - # self.title_label.setText(f"Details für: {data.get('c1', 'Unbekannt')}") - # self.info_label.setText( - # f"Beschreibung: {data.get('c2')}\n\n" - # f"Abteilung: {data.get('c3', 'Keine Angabe')}\n" - # f"Status: {data.get('c4')}\n" - # f"Datum: {data.get('date')}" - # ) - CUSTOM_WIDGETS: Final[dict[str, type[CustomWidget]]] = { "grunderfassung_suche": Grunderfassung_SuchWidget, @@ -2640,7 +2673,7 @@ FORM_FIELDS_MASTER_DATA = [ ), FormField( "Anzahl Kinder", - FormFieldType.DYNAMIC_DROPDOWN, + FormFieldType.DYNAMIC_DROPDOWN_NUMERIC, required=False, tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", key="anzahl_kinder", @@ -3027,10 +3060,22 @@ FORM_FIELDS = [ children=[ FormField( "Projektrelevanz", - FormFieldType.DROPDOWN, - key="relevanz", - required=True, - options=[("ja", None), ("nein", None)], + FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN, + key="status", + children=[ + FormField( + "Relevanz", + FormFieldType.DROPDOWN, + required=True, + options=[("ja", None), ("nein", None)], + key="relevanz", + children=[ + FormField( + "Förderperiode", FormFieldType.TEXT, key="foerderperiode" + ), + ], + ), + ], ), ], ), @@ -3235,16 +3280,23 @@ class MainWindow(QMainWindow): new_btn.setFixedWidth(100) new_btn.setFixedHeight(40) new_btn.clicked.connect(self.show_new_entry_select) + vert_layout.addWidget(new_btn) - # TODO remove - update_btn = QPushButton("UPDATE") - update_btn.setFixedWidth(100) - update_btn.setFixedHeight(40) - update_btn.clicked.connect(self.update_grid) - clear_btn = QPushButton("CLEAR") - clear_btn.setFixedWidth(100) - clear_btn.setFixedHeight(40) - clear_btn.clicked.connect(self._clear_layout) + if DEBUG: + update_btn = QPushButton("UPDATE") + update_btn.setFixedWidth(100) + update_btn.setFixedHeight(40) + update_btn.clicked.connect(self.update_grid) + clear_btn = QPushButton("CLEAR") + clear_btn.setFixedWidth(100) + clear_btn.setFixedHeight(40) + clear_btn.clicked.connect(self._clear_layout) + vert_layout.addWidget(update_btn) + vert_layout.addWidget(clear_btn) + separator = QFrame() + separator.setFrameShape(QFrame.Shape.HLine) + separator.setFrameShadow(QFrame.Shadow.Sunken) + vert_layout.addWidget(separator) # container for table or grid container = QWidget() @@ -3257,9 +3309,6 @@ class MainWindow(QMainWindow): scroll_area.setFrameShape(QFrame.Shape.NoFrame) scroll_area.setWidget(container) - vert_layout.addWidget(new_btn) - vert_layout.addWidget(update_btn) - vert_layout.addWidget(clear_btn) vert_layout.addSpacing(20) vert_layout.addWidget(scroll_area) # springs left and right of container to center the it diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index 879c78e..3323eae 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -360,12 +360,11 @@ grunderfassung_unternehmen: sql.Table = Table( Column("Kontaktperson__KP_name_partner", sql.Text, nullable=True), Column("Kontaktperson__KP_titel", sql.Text, nullable=True), Column("Kontaktperson__KP_vorname", sql.Text, nullable=True), - # Column("Metadaten_aktualisierung", sql.Text, nullable=True), - # Column("Metadaten_erstellung", sql.Text, nullable=True), Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True), Column("Partnersuche__person_suche", sql.Integer, nullable=True), Column("Partnersuche__un_suche", sql.Integer, nullable=True), - Column("Projektrelevanz__relevanz", sql.Boolean, nullable=True), + Column("Projektrelevanz__status__relevanz", sql.Boolean, nullable=True), + Column("Projektrelevanz__status__foerderperiode", sql.Text, nullable=True), Column("Schulbildung", sql.Text, nullable=True), Column("Sprachkenntnisse", sql.Text, nullable=True), Column("Stammdaten__PLZ", sql.Text, nullable=True),