dynamic dropdown with optional trigger for sub forms

This commit is contained in:
2026-05-27 17:44:59 +02:00
parent 5a5e232c24
commit dd0c98d51b

View File

@@ -71,14 +71,15 @@ setup_logging(enable_stderr=True)
DEBUG: Final[bool] = True
DEBUG_SEARCH_WIDGET: Final[bool] = False
DEBUG_NO_DATABASE: Final[bool] = False
DEBUG_NO_DATABASE: Final[bool] = True
DEBUG_GET_SET: Final[bool] = True
logger = BASE_LOGGER.getChild("wce")
logger.setLevel(logging.DEBUG)
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.setLevel(logging.DEBUG)
logger_auto_form = logger.getChild("get_data_auto_form")
logger_auto_form.setLevel(logging.DEBUG)
@@ -379,7 +380,7 @@ class FormFieldType(enum.StrEnum):
EXTENDED_DROPDOWN = enum.auto()
DYNAMIC_LIST = enum.auto()
DYNAMIC_DROPDOWN_NUMERIC = enum.auto()
DYNAMIC_DROPDOWN_BOOLEAN = enum.auto()
DYNAMIC_DROPDOWN_OPTION = enum.auto()
TEXT_SEARCH = enum.auto()
CUSTOM = enum.auto()
TEXT_DATE = enum.auto()
@@ -420,6 +421,7 @@ class FormField:
custom_widget: str = ""
init_label: str = dc.field(init=False)
ignore_get_data: bool = False
trigger_value: str = ""
def __post_init__(
self,
@@ -448,6 +450,11 @@ class FormField:
if self.type in (FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN):
self.dropdown_options = tuple(DropdownOption(op[0], op[1]) for op in options)
if self.type is FormFieldType.DYNAMIC_DROPDOWN_OPTION and not self.trigger_value:
raise ValueError(
"Dynamic Dropdown Option Widget must have a defined option or decision value"
)
if self.children:
self.required = self.required or any((child.required for child in self.children))
for child in self.children:
@@ -741,13 +748,22 @@ def _build_ui_recursively(
}
parent_layout.addRow(widget)
case (
FormFieldType.DYNAMIC_DROPDOWN_NUMERIC
| FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN
):
widget = DynamicDropdownWidget(
case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC:
widget = DynamicDropdownWidgetNumeric(
field.children,
field.type,
field.label,
prefix=f"{full_key}",
)
widget_registry[full_key] = {
"widget": widget,
"form_field": field,
}
parent_layout.addRow(widget)
case FormFieldType.DYNAMIC_DROPDOWN_OPTION:
widget = DynamicDropdownWidgetOption(
field.children,
field.trigger_value,
field.label,
prefix=f"{full_key}",
)
@@ -831,14 +847,16 @@ def reset_form(
widget.setCurrentIndex(0)
else:
widget.setCurrentIndex(-1)
elif isinstance(widget, DynamicListWidget):
# dynamic list widget manages its widgets by itself
widget.reset_form()
elif isinstance(widget, DynamicDropdownWidget):
# dynamic list widget manages its widgets by itself
widget.reset_form()
elif isinstance(widget, CustomWidget):
# dynamic list widget manages its widgets by itself
elif isinstance(
widget,
(
DynamicListWidget,
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
CustomWidget,
),
):
# custom widget classes manage their widgets on their own
widget.reset_form()
widget.setStyleSheet("")
@@ -975,11 +993,13 @@ def get_form_data(
elif isinstance(widget, QComboBox):
value = widget.currentData()
elif isinstance(widget, DynamicListWidget):
# TODO add to other custom widgets
# this should be a list: each dynamic list contains a list
# of such dictionaries
value = widget.get_form_data()
elif isinstance(widget, (DynamicDropdownWidget, CustomWidget)):
elif isinstance(
widget, (DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, CustomWidget)
):
# this is a special data structure with some assumptions of the widget's internals
value = widget.get_form_data()
@@ -1036,7 +1056,14 @@ def set_widget_value(
assert isinstance(value, list)
widget.set_form_data(value)
elif isinstance(widget, (DynamicDropdownWidget, Grunderfassung_SuchWidget)):
elif isinstance(
widget,
(
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
Grunderfassung_SuchWidget,
),
):
assert isinstance(value, dict)
widget.set_form_data(value)
@@ -1058,7 +1085,14 @@ def set_form_data(
# key_path = key.split(COLUMN_SEP)
# value = _get_nested(data, key_path)
# value = data.get(key, None)
if isinstance(widget, (DynamicDropdownWidget, Grunderfassung_SuchWidget)):
if isinstance(
widget,
(
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
Grunderfassung_SuchWidget,
),
):
value = data
else:
value = data[key]
@@ -1104,7 +1138,15 @@ def validate_form_data(
if widget.currentData() is not None:
continue
error_post = True
elif isinstance(widget, (DynamicListWidget, DynamicDropdownWidget, CustomWidget)):
elif isinstance(
widget,
(
DynamicListWidget,
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
CustomWidget,
),
):
errors_widget = widget.validate_form_data()
if not errors_widget:
continue
@@ -1641,10 +1683,15 @@ class AutoForm(QWidget):
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.main_layout.addWidget(self.test_button)
button = QPushButton("GET DATA")
button.setFixedHeight(35)
button.clicked.connect(self.get_form_data)
self.main_layout.addWidget(button)
button_get = QPushButton("GET DATA")
button_get.setFixedHeight(35)
button_get.clicked.connect(self.get_form_data)
self.main_layout.addWidget(button_get)
button_set = QPushButton("SET DATA")
button_set.setFixedHeight(35)
button_set.clicked.connect(self.set_form_data)
self.main_layout.addWidget(button_set)
id_field_layout = QHBoxLayout()
id_field_layout.setContentsMargins(0, 0, 0, 0)
@@ -1682,6 +1729,8 @@ class AutoForm(QWidget):
# buttons (save and reset)
self.add_buttons = self.cfg.add_buttons
self.debug_form_data: dict[str, Any] = {}
if self.add_buttons:
self.layout_btn = QHBoxLayout()
self.main_layout.addLayout(self.layout_btn)
@@ -1850,12 +1899,19 @@ class AutoForm(QWidget):
logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call get form data:")
logger_auto_form.debug("Form Data:\n%s", pformat(form_data))
if DEBUG_GET_SET:
self.debug_form_data = form_data
return form_data
def set_form_data(
self,
data: dict[str, Any],
) -> None:
logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call set form data:")
if DEBUG_GET_SET:
data = self.debug_form_data
set_form_data(self.widget_registry, data)
@@ -2015,12 +2071,7 @@ 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):
class DynamicDropdownWidgetNumeric(QWidget):
"""
A Widget, which can generate and manage an arbitrary number of sub forms with additional
information on a combobox selection (combobox).
@@ -2029,7 +2080,6 @@ class DynamicDropdownWidget(QWidget):
def __init__(
self,
form_fields: Sequence[FormField],
type: FormFieldType,
label_add_info: str = "Eintrag",
prefix: str = "",
):
@@ -2089,22 +2139,10 @@ class DynamicDropdownWidget(QWidget):
) -> None:
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)
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
@@ -2124,7 +2162,6 @@ class DynamicDropdownWidget(QWidget):
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
form_field_def = copy.copy(self.assigned_form_field)
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(
@@ -2216,6 +2253,195 @@ class DynamicDropdownWidget(QWidget):
set_form_data(sub_form.registry, sub_form_data)
class DynamicDropdownWidgetOption(QWidget):
"""
A Widget, which can generate and manage an arbitrary number of sub forms with additional
information on a combobox selection (combobox).
"""
def __init__(
self,
form_fields: Sequence[FormField],
trigger_value: str,
label_add_info: str = "Eintrag",
prefix: str = "",
):
super().__init__()
# form_fields = form_fields.children
if len(form_fields) == 0 or len(form_fields) > 1:
raise ValueError(
"Dynamic Dropdown Widget must have only one child, which is a dropdown widget"
)
self.combobox_field = form_fields[0]
assigned_form_fields = self.combobox_field.children
if len(assigned_form_fields) < 1:
raise ValueError(
(
"Option Dynamic Dropdown Widget dropdown element must have at least one "
"child field definition"
)
)
self.trigger_value = trigger_value
self.assigned_form_fields = assigned_form_fields
self.label_add_info = label_add_info
self.prefix = prefix
self.widget_registry: WidgetRegistry = {}
# layout for group component
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout = QFormLayout()
self.main_layout.addLayout(self.form_layout)
self.full_keys = _build_ui_recursively(
[self.combobox_field],
self.form_layout,
self.widget_registry,
prefix=f"{self.prefix}",
)
dropdown_widget_entry = tuple(self.widget_registry.values())[0]
dropdown_widget = dropdown_widget_entry["widget"]
assert isinstance(dropdown_widget, QComboBox)
self.dropdown_widget = dropdown_widget
self.rows_container = QWidget()
self.rows_layout = QVBoxLayout(self.rows_container)
self.rows_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.addWidget(self.rows_container)
self.sub_forms: list[SubForm] = []
self.dropdown_widget.currentTextChanged.connect(self._selection_changed_changed)
def _selection_changed_changed(
self,
text: str,
) -> None:
current_count = len(self.sub_forms)
value: int
if text == self.trigger_value:
value = 1
else:
value = 0
if value > current_count:
diff = value - current_count
for _ in range(diff):
self._add_row()
elif value < current_count:
diff = current_count - value
for _ in range(diff):
self._remove_row()
def _add_row(self) -> None:
number_form = len(self.sub_forms) + 1
container = QWidget()
container.setContentsMargins(0, 0, 0, 0)
form_layout = QFormLayout(container)
form_layout.setContentsMargins(10, 0, 0, 0)
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
form_field_def = copy.copy(self.assigned_form_fields)
# form_field_def.label = form_field_def.enhanced_label(f"{number_form}")
sub_form.full_keys = _build_ui_recursively(
schema=form_field_def,
parent_layout=form_layout,
widget_registry=sub_form.registry,
prefix=f"{self.prefix}",
)
self.rows_layout.addWidget(container)
self.sub_forms.append(sub_form)
self.update_sub_forms()
def _remove_row(self) -> None:
last_form = self.sub_forms.pop()
box_to_remove = last_form.entry_box
self.rows_layout.removeWidget(box_to_remove)
box_to_remove.deleteLater()
self.update_sub_forms()
def update_sub_forms(self) -> None:
update_sub_forms(
self.widget_registry,
sub_forms=self.sub_forms,
)
def _get_combined_registry(self, sub_forms_only: bool = False) -> WidgetRegistry:
whole_registry: WidgetRegistry = {}
if not sub_forms_only:
whole_registry = self.widget_registry.copy()
for sub in self.sub_forms:
whole_registry.update(sub.registry)
return whole_registry
def reset_form(self) -> None:
# resets dynamic content when dropdown is set back to default value
self.dropdown_widget.setCurrentIndex(0)
def validate_form_data(self) -> list[str]:
errors = validate_form_data(self.widget_registry)
for form in self.sub_forms:
errors.extend(validate_form_data(form.registry))
return errors
def get_form_data(self) -> dict[str, Any]:
whole_registry = self._get_combined_registry()
form_data = get_form_data(whole_registry)
# for sub in self.sub_forms:
# form_data.update(get_form_data(sub.registry))
logger_get_data.debug(
"Form data DynamicDropdownWidgetOption:\n%s", pformat(form_data)
)
return form_data
def set_form_data(
self,
data: dict[str, Any],
) -> None:
# delete all rows
while self.sub_forms:
self._remove_row()
# fill in value of combobox field
assert len(self.full_keys) == 1
num_subforms: int = -1
data = data.copy()
for key in self.full_keys:
widget = self.widget_registry[key]["widget"]
value = data[key]
set_widget_value(widget, value)
# TODO need to get the correct index
if value is None or value != self.trigger_value:
num_subforms = 0
else:
num_subforms = 1
del data[key]
# assert key in whole_registry, "key not in Dynamic DD Option"
whole_registry = self._get_combined_registry(sub_forms_only=True)
logger_get_data.debug("[DynamicDD-Option] Whole widget registry:\n")
pprint_registry(whole_registry)
logger_get_data.debug(">>>>>>>>> Call set_form_data for DynamicDropdown")
logger_get_data.debug("Data before set of subforms:%s", pformat(data))
assert len(self.sub_forms) == num_subforms
if not self.sub_forms:
return
set_form_data(whole_registry, data)
class NoScrollFilter(QObject):
"""disables scrolling in fields"""
@@ -3021,48 +3247,48 @@ FORM_FIELDS_LANGUAGES = [
FORM_FIELDS = [
FormField(
"Ersteintrag Datum",
FormFieldType.TEXT_DATETIME,
required=False,
key="Metadaten_erstellung",
readonly=True,
ignore_get_data=True,
),
FormField(
"Aktualisierung Datum",
FormFieldType.TEXT_DATETIME,
required=False,
key="Metadaten_aktualisierung",
readonly=True,
ignore_get_data=True,
),
FormField(
"Aktualisierung Nutzer",
FormFieldType.TEXT,
required=False,
key="Metadaten_nutzer",
readonly=True,
),
FormField(
"Fallnummer",
FormFieldType.TEXT,
required=True,
key="Grunderfassung_fallnummer",
),
FormField(
"Notizen",
FormFieldType.LONGTEXT,
required=False,
key="Grunderfassung_notiz",
),
FormField(
"Suche",
FormFieldType.CUSTOM,
custom_widget="grunderfassung_suche",
key="Partnersuche",
children=FORM_FIELDS_SEARCH_HEAD,
),
# FormField(
# "Ersteintrag Datum",
# FormFieldType.TEXT_DATETIME,
# required=False,
# key="Metadaten_erstellung",
# readonly=True,
# ignore_get_data=True,
# ),
# FormField(
# "Aktualisierung Datum",
# FormFieldType.TEXT_DATETIME,
# required=False,
# key="Metadaten_aktualisierung",
# readonly=True,
# ignore_get_data=True,
# ),
# FormField(
# "Aktualisierung Nutzer",
# FormFieldType.TEXT,
# required=False,
# key="Metadaten_nutzer",
# readonly=True,
# ),
# FormField(
# "Fallnummer",
# FormFieldType.TEXT,
# required=True,
# key="Grunderfassung_fallnummer",
# ),
# FormField(
# "Notizen",
# FormFieldType.LONGTEXT,
# required=False,
# key="Grunderfassung_notiz",
# ),
# FormField(
# "Suche",
# FormFieldType.CUSTOM,
# custom_widget="grunderfassung_suche",
# key="Partnersuche",
# children=FORM_FIELDS_SEARCH_HEAD,
# ),
FormField(
"Status && Projektrelevanz",
FormFieldType.GROUP,
@@ -3070,8 +3296,9 @@ FORM_FIELDS = [
children=[
FormField(
"Projektrelevanz",
FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN,
FormFieldType.DYNAMIC_DROPDOWN_OPTION,
key="status",
trigger_value="ja",
children=[
FormField(
"Relevanz",
@@ -3083,54 +3310,56 @@ FORM_FIELDS = [
FormField(
"Förderperiode", FormFieldType.TEXT, key="foerderperiode"
),
FormField("Feld 2", FormFieldType.TEXT, key="feld_2"),
FormField("Feld 3", FormFieldType.TEXT, key="feld_3"),
],
),
],
),
],
),
FormField(
"Daten Kontaktperson",
FormFieldType.GROUP,
key="Kontaktperson",
children=FORM_FIELDS_CONTACT_PERSON,
),
FormField(
"Stammdaten",
FormFieldType.GROUP,
key="Stammdaten",
children=FORM_FIELDS_MASTER_DATA,
),
FormField(
"Weitere Informationen",
FormFieldType.GROUP,
key="WeitereInfos",
children=FORM_FIELDS_ADDITIONAL_DATA,
),
FormField(
"Schulbildung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_SCHOOL,
key="Schulbildung",
),
FormField(
"Studium/Ausbildung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_HIGHER_EDUCATION,
key="HoehereBildung",
),
FormField(
"Arbeitserfahrung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_WORK_EXPERIENCE,
key="Arbeitserfahrung",
),
FormField(
"Sprachkenntnisse",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_LANGUAGES,
key="Sprachkenntnisse",
),
# FormField(
# "Daten Kontaktperson",
# FormFieldType.GROUP,
# key="Kontaktperson",
# children=FORM_FIELDS_CONTACT_PERSON,
# ),
# FormField(
# "Stammdaten",
# FormFieldType.GROUP,
# key="Stammdaten",
# children=FORM_FIELDS_MASTER_DATA,
# ),
# FormField(
# "Weitere Informationen",
# FormFieldType.GROUP,
# key="WeitereInfos",
# children=FORM_FIELDS_ADDITIONAL_DATA,
# ),
# FormField(
# "Schulbildung",
# FormFieldType.DYNAMIC_LIST,
# children=FORM_FIELDS_SCHOOL,
# key="Schulbildung",
# ),
# FormField(
# "Studium/Ausbildung",
# FormFieldType.DYNAMIC_LIST,
# children=FORM_FIELDS_HIGHER_EDUCATION,
# key="HoehereBildung",
# ),
# FormField(
# "Arbeitserfahrung",
# FormFieldType.DYNAMIC_LIST,
# children=FORM_FIELDS_WORK_EXPERIENCE,
# key="Arbeitserfahrung",
# ),
# FormField(
# "Sprachkenntnisse",
# FormFieldType.DYNAMIC_LIST,
# children=FORM_FIELDS_LANGUAGES,
# key="Sprachkenntnisse",
# ),
]
CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig(