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: Final[bool] = True
DEBUG_SEARCH_WIDGET: Final[bool] = False 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 = BASE_LOGGER.getChild("wce")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger_search_widget = logger.getChild("search_widget") logger_search_widget = logger.getChild("search_widget")
logger_search_widget.setLevel(logging.DEBUG) logger_search_widget.setLevel(logging.DEBUG)
logger_get_data = logger.getChild("get_data") 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 = logger.getChild("get_data_auto_form")
logger_auto_form.setLevel(logging.DEBUG) logger_auto_form.setLevel(logging.DEBUG)
@@ -379,7 +380,7 @@ class FormFieldType(enum.StrEnum):
EXTENDED_DROPDOWN = enum.auto() EXTENDED_DROPDOWN = enum.auto()
DYNAMIC_LIST = enum.auto() DYNAMIC_LIST = enum.auto()
DYNAMIC_DROPDOWN_NUMERIC = enum.auto() DYNAMIC_DROPDOWN_NUMERIC = enum.auto()
DYNAMIC_DROPDOWN_BOOLEAN = enum.auto() DYNAMIC_DROPDOWN_OPTION = enum.auto()
TEXT_SEARCH = enum.auto() TEXT_SEARCH = enum.auto()
CUSTOM = enum.auto() CUSTOM = enum.auto()
TEXT_DATE = enum.auto() TEXT_DATE = enum.auto()
@@ -420,6 +421,7 @@ class FormField:
custom_widget: str = "" custom_widget: str = ""
init_label: str = dc.field(init=False) init_label: str = dc.field(init=False)
ignore_get_data: bool = False ignore_get_data: bool = False
trigger_value: str = ""
def __post_init__( def __post_init__(
self, self,
@@ -448,6 +450,11 @@ class FormField:
if self.type in (FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN): if self.type in (FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN):
self.dropdown_options = tuple(DropdownOption(op[0], op[1]) for op in options) 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: if self.children:
self.required = self.required or 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: for child in self.children:
@@ -741,13 +748,22 @@ def _build_ui_recursively(
} }
parent_layout.addRow(widget) parent_layout.addRow(widget)
case ( case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC:
FormFieldType.DYNAMIC_DROPDOWN_NUMERIC widget = DynamicDropdownWidgetNumeric(
| FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN
):
widget = DynamicDropdownWidget(
field.children, 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, field.label,
prefix=f"{full_key}", prefix=f"{full_key}",
) )
@@ -831,14 +847,16 @@ def reset_form(
widget.setCurrentIndex(0) widget.setCurrentIndex(0)
else: else:
widget.setCurrentIndex(-1) widget.setCurrentIndex(-1)
elif isinstance(widget, DynamicListWidget): elif isinstance(
# dynamic list widget manages its widgets by itself widget,
widget.reset_form() (
elif isinstance(widget, DynamicDropdownWidget): DynamicListWidget,
# dynamic list widget manages its widgets by itself DynamicDropdownWidgetNumeric,
widget.reset_form() DynamicDropdownWidgetOption,
elif isinstance(widget, CustomWidget): CustomWidget,
# dynamic list widget manages its widgets by itself ),
):
# custom widget classes manage their widgets on their own
widget.reset_form() widget.reset_form()
widget.setStyleSheet("") widget.setStyleSheet("")
@@ -975,11 +993,13 @@ def get_form_data(
elif isinstance(widget, QComboBox): elif isinstance(widget, QComboBox):
value = widget.currentData() value = widget.currentData()
elif isinstance(widget, DynamicListWidget): elif isinstance(widget, DynamicListWidget):
# TODO add to other custom widgets
# this should be a list: each dynamic list contains a list # this should be a list: each dynamic list contains a list
# of such dictionaries # of such dictionaries
value = widget.get_form_data() value = widget.get_form_data()
elif isinstance(
elif isinstance(widget, (DynamicDropdownWidget, CustomWidget)): widget, (DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, CustomWidget)
):
# this is a special data structure with some assumptions of the widget's internals # this is a special data structure with some assumptions of the widget's internals
value = widget.get_form_data() value = widget.get_form_data()
@@ -1036,7 +1056,14 @@ def set_widget_value(
assert isinstance(value, list) assert isinstance(value, list)
widget.set_form_data(value) widget.set_form_data(value)
elif isinstance(widget, (DynamicDropdownWidget, Grunderfassung_SuchWidget)): elif isinstance(
widget,
(
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
Grunderfassung_SuchWidget,
),
):
assert isinstance(value, dict) assert isinstance(value, dict)
widget.set_form_data(value) widget.set_form_data(value)
@@ -1058,7 +1085,14 @@ def set_form_data(
# key_path = key.split(COLUMN_SEP) # key_path = key.split(COLUMN_SEP)
# value = _get_nested(data, key_path) # value = _get_nested(data, key_path)
# value = data.get(key, None) # value = data.get(key, None)
if isinstance(widget, (DynamicDropdownWidget, Grunderfassung_SuchWidget)): if isinstance(
widget,
(
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
Grunderfassung_SuchWidget,
),
):
value = data value = data
else: else:
value = data[key] value = data[key]
@@ -1104,7 +1138,15 @@ def validate_form_data(
if widget.currentData() is not None: if widget.currentData() is not None:
continue continue
error_post = True error_post = True
elif isinstance(widget, (DynamicListWidget, DynamicDropdownWidget, CustomWidget)): elif isinstance(
widget,
(
DynamicListWidget,
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
CustomWidget,
),
):
errors_widget = widget.validate_form_data() errors_widget = widget.validate_form_data()
if not errors_widget: if not errors_widget:
continue continue
@@ -1641,10 +1683,15 @@ class AutoForm(QWidget):
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
self.main_layout.addWidget(self.test_button) self.main_layout.addWidget(self.test_button)
button = QPushButton("GET DATA")
button.setFixedHeight(35) button_get = QPushButton("GET DATA")
button.clicked.connect(self.get_form_data) button_get.setFixedHeight(35)
self.main_layout.addWidget(button) 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 = QHBoxLayout()
id_field_layout.setContentsMargins(0, 0, 0, 0) id_field_layout.setContentsMargins(0, 0, 0, 0)
@@ -1682,6 +1729,8 @@ class AutoForm(QWidget):
# buttons (save and reset) # buttons (save and reset)
self.add_buttons = self.cfg.add_buttons self.add_buttons = self.cfg.add_buttons
self.debug_form_data: dict[str, Any] = {}
if self.add_buttons: if self.add_buttons:
self.layout_btn = QHBoxLayout() self.layout_btn = QHBoxLayout()
self.main_layout.addLayout(self.layout_btn) 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("\n\n>>>>>>> [AutoForm] Call get form data:")
logger_auto_form.debug("Form Data:\n%s", pformat(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 return form_data
def set_form_data( def set_form_data(
self, self,
data: dict[str, Any], data: dict[str, Any],
) -> None: ) -> 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) set_form_data(self.widget_registry, data)
@@ -2015,12 +2071,7 @@ class DynamicListWidget(QWidget):
set_form_data(current_sub_form.registry, sub_form_data) set_form_data(current_sub_form.registry, sub_form_data)
class DynamicDropdownType(enum.IntEnum): class DynamicDropdownWidgetNumeric(QWidget):
NUMERIC = enum.auto()
BOOLEAN = enum.auto()
class DynamicDropdownWidget(QWidget):
""" """
A Widget, which can generate and manage an arbitrary number of sub forms with additional A Widget, which can generate and manage an arbitrary number of sub forms with additional
information on a combobox selection (combobox). information on a combobox selection (combobox).
@@ -2029,7 +2080,6 @@ class DynamicDropdownWidget(QWidget):
def __init__( def __init__(
self, self,
form_fields: Sequence[FormField], form_fields: Sequence[FormField],
type: FormFieldType,
label_add_info: str = "Eintrag", label_add_info: str = "Eintrag",
prefix: str = "", prefix: str = "",
): ):
@@ -2089,22 +2139,10 @@ class DynamicDropdownWidget(QWidget):
) -> None: ) -> None:
current_count = len(self.sub_forms) current_count = len(self.sub_forms)
value: int value: int
match self.type: if text == DROPDOWN_DEFAULT:
case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC: value = 0
if text == DROPDOWN_DEFAULT: else:
value = 0 value = int(text)
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: if value > current_count:
diff = value - current_count diff = value - current_count
@@ -2124,8 +2162,7 @@ class DynamicDropdownWidget(QWidget):
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form) sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
form_field_def = copy.copy(self.assigned_form_field) 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}")
form_field_def.label = form_field_def.enhanced_label(f"{number_form}")
sub_form.full_keys = _build_ui_recursively( sub_form.full_keys = _build_ui_recursively(
schema=[form_field_def], schema=[form_field_def],
@@ -2216,6 +2253,195 @@ class DynamicDropdownWidget(QWidget):
set_form_data(sub_form.registry, sub_form_data) 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): class NoScrollFilter(QObject):
"""disables scrolling in fields""" """disables scrolling in fields"""
@@ -3021,48 +3247,48 @@ FORM_FIELDS_LANGUAGES = [
FORM_FIELDS = [ FORM_FIELDS = [
FormField( # FormField(
"Ersteintrag Datum", # "Ersteintrag Datum",
FormFieldType.TEXT_DATETIME, # FormFieldType.TEXT_DATETIME,
required=False, # required=False,
key="Metadaten_erstellung", # key="Metadaten_erstellung",
readonly=True, # readonly=True,
ignore_get_data=True, # ignore_get_data=True,
), # ),
FormField( # FormField(
"Aktualisierung Datum", # "Aktualisierung Datum",
FormFieldType.TEXT_DATETIME, # FormFieldType.TEXT_DATETIME,
required=False, # required=False,
key="Metadaten_aktualisierung", # key="Metadaten_aktualisierung",
readonly=True, # readonly=True,
ignore_get_data=True, # ignore_get_data=True,
), # ),
FormField( # FormField(
"Aktualisierung Nutzer", # "Aktualisierung Nutzer",
FormFieldType.TEXT, # FormFieldType.TEXT,
required=False, # required=False,
key="Metadaten_nutzer", # key="Metadaten_nutzer",
readonly=True, # readonly=True,
), # ),
FormField( # FormField(
"Fallnummer", # "Fallnummer",
FormFieldType.TEXT, # FormFieldType.TEXT,
required=True, # required=True,
key="Grunderfassung_fallnummer", # key="Grunderfassung_fallnummer",
), # ),
FormField( # FormField(
"Notizen", # "Notizen",
FormFieldType.LONGTEXT, # FormFieldType.LONGTEXT,
required=False, # required=False,
key="Grunderfassung_notiz", # key="Grunderfassung_notiz",
), # ),
FormField( # FormField(
"Suche", # "Suche",
FormFieldType.CUSTOM, # FormFieldType.CUSTOM,
custom_widget="grunderfassung_suche", # custom_widget="grunderfassung_suche",
key="Partnersuche", # key="Partnersuche",
children=FORM_FIELDS_SEARCH_HEAD, # children=FORM_FIELDS_SEARCH_HEAD,
), # ),
FormField( FormField(
"Status && Projektrelevanz", "Status && Projektrelevanz",
FormFieldType.GROUP, FormFieldType.GROUP,
@@ -3070,8 +3296,9 @@ FORM_FIELDS = [
children=[ children=[
FormField( FormField(
"Projektrelevanz", "Projektrelevanz",
FormFieldType.DYNAMIC_DROPDOWN_BOOLEAN, FormFieldType.DYNAMIC_DROPDOWN_OPTION,
key="status", key="status",
trigger_value="ja",
children=[ children=[
FormField( FormField(
"Relevanz", "Relevanz",
@@ -3083,54 +3310,56 @@ FORM_FIELDS = [
FormField( FormField(
"Förderperiode", FormFieldType.TEXT, key="foerderperiode" "Förderperiode", FormFieldType.TEXT, key="foerderperiode"
), ),
FormField("Feld 2", FormFieldType.TEXT, key="feld_2"),
FormField("Feld 3", FormFieldType.TEXT, key="feld_3"),
], ],
), ),
], ],
), ),
], ],
), ),
FormField( # FormField(
"Daten Kontaktperson", # "Daten Kontaktperson",
FormFieldType.GROUP, # FormFieldType.GROUP,
key="Kontaktperson", # key="Kontaktperson",
children=FORM_FIELDS_CONTACT_PERSON, # children=FORM_FIELDS_CONTACT_PERSON,
), # ),
FormField( # FormField(
"Stammdaten", # "Stammdaten",
FormFieldType.GROUP, # FormFieldType.GROUP,
key="Stammdaten", # key="Stammdaten",
children=FORM_FIELDS_MASTER_DATA, # children=FORM_FIELDS_MASTER_DATA,
), # ),
FormField( # FormField(
"Weitere Informationen", # "Weitere Informationen",
FormFieldType.GROUP, # FormFieldType.GROUP,
key="WeitereInfos", # key="WeitereInfos",
children=FORM_FIELDS_ADDITIONAL_DATA, # children=FORM_FIELDS_ADDITIONAL_DATA,
), # ),
FormField( # FormField(
"Schulbildung", # "Schulbildung",
FormFieldType.DYNAMIC_LIST, # FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_SCHOOL, # children=FORM_FIELDS_SCHOOL,
key="Schulbildung", # key="Schulbildung",
), # ),
FormField( # FormField(
"Studium/Ausbildung", # "Studium/Ausbildung",
FormFieldType.DYNAMIC_LIST, # FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_HIGHER_EDUCATION, # children=FORM_FIELDS_HIGHER_EDUCATION,
key="HoehereBildung", # key="HoehereBildung",
), # ),
FormField( # FormField(
"Arbeitserfahrung", # "Arbeitserfahrung",
FormFieldType.DYNAMIC_LIST, # FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_WORK_EXPERIENCE, # children=FORM_FIELDS_WORK_EXPERIENCE,
key="Arbeitserfahrung", # key="Arbeitserfahrung",
), # ),
FormField( # FormField(
"Sprachkenntnisse", # "Sprachkenntnisse",
FormFieldType.DYNAMIC_LIST, # FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_LANGUAGES, # children=FORM_FIELDS_LANGUAGES,
key="Sprachkenntnisse", # key="Sprachkenntnisse",
), # ),
] ]
CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig( CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig(