adaptive dynamic dropdown and addition "Projektrelevanz"

This commit is contained in:
2026-05-27 13:37:27 +02:00
parent 4d76da2c9b
commit 3235d7f8a9
2 changed files with 131 additions and 83 deletions

View File

@@ -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

View File

@@ -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),