generic typing of field definitions and value getter function for single widgets

This commit is contained in:
2026-07-03 13:00:56 +02:00
parent deeb038956
commit afa8fb351d
2 changed files with 149 additions and 77 deletions
+7 -2
View File
@@ -4,12 +4,17 @@ import dataclasses as dc
import enum import enum
import uuid import uuid
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Final from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar
import babel import babel
from wce_crm.custom_widget_registry import CUSTOM_WIDGET_NAMES from wce_crm.custom_widget_registry import CUSTOM_WIDGET_NAMES
if TYPE_CHECKING:
from PySide6.QtWidgets import QWidget
T = TypeVar("T", bound="QWidget")
class FormFieldType(enum.StrEnum): class FormFieldType(enum.StrEnum):
GROUP = enum.auto() GROUP = enum.auto()
@@ -31,7 +36,7 @@ class FormFieldType(enum.StrEnum):
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class FormField: class FormField(Generic[T]):
label: str label: str
type: FormFieldType type: FormFieldType
children: Sequence[FormField] = dc.field(default_factory=list) children: Sequence[FormField] = dc.field(default_factory=list)
+118 -51
View File
@@ -12,7 +12,17 @@ from collections import defaultdict
from collections.abc import Container, Iterable, Sequence from collections.abc import Container, Iterable, Sequence
from pathlib import Path from pathlib import Path
from pprint import pformat from pprint import pformat
from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast from typing import (
TYPE_CHECKING,
Any,
Final,
Literal,
Protocol,
TypeAlias,
TypedDict,
TypeVar,
cast,
)
from typing_extensions import override from typing_extensions import override
from dopt_basics import datetime as dopt_dt from dopt_basics import datetime as dopt_dt
@@ -92,6 +102,7 @@ if TYPE_CHECKING:
K = TypeVar("K") K = TypeVar("K")
V = TypeVar("V") V = TypeVar("V")
T = TypeVar("T", bound="QWidget")
DEBUG: bool = True DEBUG: bool = True
@@ -274,7 +285,7 @@ class CustomWidget(QWidget):
def _add_widget_to_layout( def _add_widget_to_layout(
form_field: FormField, form_field: FormField[T],
widget: QWidget, widget: QWidget,
target_layout: QLayout, target_layout: QLayout,
widget_only: bool = False, widget_only: bool = False,
@@ -299,10 +310,10 @@ def _add_widget_to_layout(
def build_and_add_widget( def build_and_add_widget(
form_field: FormField, form_field: FormField[T],
parent_layout: QLayout, parent_layout: QLayout,
prefix: str = "", prefix: str = "",
) -> tuple[str, QWidget]: ) -> tuple[str, T]:
no_scroll_filter = NoScrollFilter(parent_layout) no_scroll_filter = NoScrollFilter(parent_layout)
if prefix and form_field.key: if prefix and form_field.key:
full_key = f"{prefix}{COLUMN_SEP}{form_field.key}" if prefix else form_field.key full_key = f"{prefix}{COLUMN_SEP}{form_field.key}" if prefix else form_field.key
@@ -311,7 +322,6 @@ def build_and_add_widget(
else: else:
full_key = form_field.key full_key = form_field.key
widget: QWidget
layout_widget_only: bool = False layout_widget_only: bool = False
install_no_scroll_filter: bool = True install_no_scroll_filter: bool = True
@@ -496,7 +506,7 @@ def build_and_add_widget(
_add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only) _add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only)
return full_key, widget return full_key, cast(T, widget)
def _build_ui_recursively( def _build_ui_recursively(
@@ -711,13 +721,59 @@ def get_form_data(
ignored_keys.append(key) ignored_keys.append(key)
continue continue
if form_field.type is FormFieldType.TEXT_DATE:
value = get_widget_value(widget, "date")
elif form_field.type is FormFieldType.TEXT_DATETIME:
value = get_widget_value(widget, "datetime")
else:
value = get_widget_value(widget)
# elif isinstance(widget, QDateEdit):
# qt_date = widget.date()
# value = qt_date.toPython()
# elif isinstance(widget, FlexibleDateInput):
# value = widget.get_pydate()
# elif isinstance(widget, FlexibleDateTimeInput):
# value = widget.get_pydatetime()
# elif isinstance(widget, QComboBox):
# value = widget.currentData()
# elif isinstance(
# widget,
# (
# DynamicListWidget,
# DynamicDropdownWidgetNumeric,
# DynamicDropdownWidgetOption,
# CustomWidget,
# ),
# ):
# # this is a special data structure with some assumptions of the widget's internals
# # DynamicListWidget: this is a list -> each dynamic list contains a list
# # of such dictionaries (handled below)
# value = widget.get_form_data()
if isinstance(value, dict):
raw_data.update(value)
else:
raw_data[key] = value
logger_get_data.debug("[base:get_form_data]>>>>> RAW DATA:\n%s", pformat(raw_data))
return raw_data
def get_widget_value(
widget: QWidget,
text_type: Literal["default", "date", "datetime"] = "default",
) -> Any:
value: Any | None = None
if isinstance(widget, QLineEdit): if isinstance(widget, QLineEdit):
data = widget.text() data = widget.text()
if data != "": if data != "":
value = data value = data
if form_field.type is FormFieldType.TEXT_DATE: if text_type == "date":
value = datetime.datetime.strptime(value, DATE_FMT).date() value = datetime.datetime.strptime(value, DATE_FMT).date()
elif form_field.type is FormFieldType.TEXT_DATETIME: elif text_type == "datetime":
value = datetime.datetime.strptime(value, DATETIME_FMT) value = datetime.datetime.strptime(value, DATETIME_FMT)
elif isinstance(widget, QPlainTextEdit): elif isinstance(widget, QPlainTextEdit):
data = widget.toPlainText() data = widget.toPlainText()
@@ -746,14 +802,7 @@ def get_form_data(
# of such dictionaries (handled below) # of such dictionaries (handled below)
value = widget.get_form_data() value = widget.get_form_data()
if isinstance(value, dict): return value
raw_data.update(value)
else:
raw_data[key] = value
logger_get_data.debug("[base:get_form_data]>>>>> RAW DATA:\n%s", pformat(raw_data))
return raw_data
def set_widget_value( def set_widget_value(
@@ -3301,17 +3350,19 @@ class Session:
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class Page_Consulting_ConsultingSession_State: class Page_Consulting_ConsultingSession_State:
user_id: UserId # must be set with the session (always known) nutzer_id: UserId # must be set with the session (always known)
user_name: str # must be set with the session (always known) nutzer_name: str # must be set with the session (always known)
cons_session_id: int | None = None # known and not known entries beratung_id: int | None = None # known and not known entries
timestamp: datetime.datetime = dc.field( zeitstempel: datetime.datetime = dc.field(
default_factory=lambda: datetime.datetime.now(tz=datetime.UTC) default_factory=lambda: datetime.datetime.now(tz=datetime.UTC)
) )
partner: str = "" ansprechpartner: str | None = None
interaction_scheme: str = "" # set string like in other dropdowns or use integer (enum) interaction_scheme: str | None = None
topic: str = "" # TODO should be linked to something, target unclear (need specification by customer) topic: str | None = (
notes: str = "" None # TODO should be linked to something, target unclear (need specification by customer)
response: str = "" )
anmerkungen: str | None = None
rueckmeldung: str | None = None
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
@@ -3614,23 +3665,38 @@ class Page_Consulting_ConsultingSession(QWidget):
layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
ROW_HEIGHT = 70 ROW_HEIGHT = 70
self.user_field = QLineEdit( user_field_def = FormField[QLineEdit](
placeholderText="wird über aktiven Nutzer bei Anlage gesetzt" "",
FormFieldType.TEXT,
readonly=True,
placeholder="wird über aktiven Nutzer bei Anlage gesetzt",
) )
self.user_field.setReadOnly(True) _, self.user_field = build_and_add_widget(user_field_def, layout)
self.user_field.setProperty("styleClass", "stempel")
# ?? date or datetime? # ?? date or datetime?
# self.timestamp = FlexibleDateInput() timestamp_def = FormField[FlexibleDateTimeInput]("", FormFieldType.DATETIME_FLEXIBLE)
# self.timestamp.set_pydate(datetime.datetime.now()) _, self.timestamp = build_and_add_widget(timestamp_def, layout)
self.timestamp = FlexibleDateTimeInput()
self.timestamp.set_pydatetime(datetime.datetime.now()) self.timestamp.set_pydatetime(datetime.datetime.now())
self.contact = QLineEdit(placeholderText="Freitext") contact_def = FormField[QLineEdit]("", FormFieldType.TEXT, placeholder="Freitext")
self.contact_type = QLineEdit(placeholderText="sollte Dropdown sein") # Dropdown _, self.contact = build_and_add_widget(contact_def, layout)
self.crm_link = QLineEdit(placeholderText="CRM Links")
self.plain_text = QPlainTextEdit() contact_type_def = FormField[QComboBox](
self.response = QPlainTextEdit() "",
FormFieldType.DROPDOWN,
placeholder="sollte Dropdown sein",
options=[("Persönlich", None), ("Telefon", None), ("Mail", None)],
)
_, self.contact_type = build_and_add_widget(contact_type_def, layout)
crm_link_def = FormField[QLineEdit]("", FormFieldType.TEXT, placeholder="CRM Links")
_, self.crm_link = build_and_add_widget(crm_link_def, layout)
plain_text_def = FormField[QPlainTextEdit]("", FormFieldType.LONGTEXT)
_, self.plain_text = build_and_add_widget(plain_text_def, layout)
response_def = FormField[QPlainTextEdit]("", FormFieldType.LONGTEXT)
_, self.response = build_and_add_widget(response_def, layout)
self.btn_delete = QPushButton("") self.btn_delete = QPushButton("")
self.btn_delete.setFlat(True) # Entfernt den blockigen Rahmen self.btn_delete.setFlat(True) # Entfernt den blockigen Rahmen
@@ -3639,6 +3705,7 @@ class Page_Consulting_ConsultingSession(QWidget):
self.btn_delete.setStyleSheet("color: #cc0000; font-weight: bold;") self.btn_delete.setStyleSheet("color: #cc0000; font-weight: bold;")
self.btn_delete.clicked.connect(lambda: self.delete_request.emit(self)) self.btn_delete.clicked.connect(lambda: self.delete_request.emit(self))
self.btn_delete.setFixedWidth(30) self.btn_delete.setFixedWidth(30)
layout.addWidget(self.btn_delete)
widgets = ( widgets = (
self.user_field, self.user_field,
@@ -3652,7 +3719,7 @@ class Page_Consulting_ConsultingSession(QWidget):
) )
for widget in widgets: for widget in widgets:
widget.setFixedHeight(ROW_HEIGHT) widget.setFixedHeight(ROW_HEIGHT)
layout.addWidget(widget) # layout.addWidget(widget)
# column stretching: must be matched with header # column stretching: must be matched with header
num_widgets = len(widgets) num_widgets = len(widgets)
@@ -3676,22 +3743,22 @@ class Page_Consulting_ConsultingSession(QWidget):
logger_page_consulting.debug( logger_page_consulting.debug(
"[Page Consulting -- Consulting Session]: State = %s", self.STATE "[Page Consulting -- Consulting Session]: State = %s", self.STATE
) )
set_widget_value(self.user_field, self.STATE.user_name) set_widget_value(self.user_field, self.STATE.nutzer_name)
set_widget_value(self.timestamp, self.STATE.timestamp.astimezone(TIMEZONE_CEST)) set_widget_value(self.timestamp, self.STATE.zeitstempel.astimezone(TIMEZONE_CEST))
set_widget_value(self.contact, self.STATE.partner) set_widget_value(self.contact, self.STATE.ansprechpartner)
set_widget_value(self.contact_type, self.STATE.interaction_scheme) set_widget_value(self.contact_type, self.STATE.interaction_scheme)
set_widget_value(self.crm_link, self.STATE.topic) set_widget_value(self.crm_link, self.STATE.topic)
set_widget_value(self.plain_text, self.STATE.notes) set_widget_value(self.plain_text, self.STATE.anmerkungen)
set_widget_value(self.response, self.STATE.response) set_widget_value(self.response, self.STATE.rueckmeldung)
def _sync_gui_to_state(self) -> None: def _sync_gui_to_state(self) -> None:
# TODO: get timestamp from custom field self.STATE.timestamp # TODO: get timestamp from custom field self.STATE.timestamp
self.STATE.timestamp = self.timestamp.get_pydatetime() self.STATE.zeitstempel = self.timestamp.get_pydatetime()
self.STATE.partner = self.contact.text() self.STATE.ansprechpartner = get_widget_value(self.contact)
self.STATE.interaction_scheme = self.contact_type.text() self.STATE.interaction_scheme = get_widget_value(self.contact_type)
self.STATE.topic = self.crm_link.text() # TODO: change to correct link self.STATE.topic = get_widget_value(self.crm_link) # TODO: change to correct link
self.STATE.notes = self.plain_text.toPlainText() self.STATE.anmerkungen = get_widget_value(self.plain_text)
self.STATE.response = self.response.toPlainText() self.STATE.rueckmeldung = get_widget_value(self.response)
def get_data(self) -> Page_Consulting_ConsultingSession_State: def get_data(self) -> Page_Consulting_ConsultingSession_State:
self._sync_gui_to_state() self._sync_gui_to_state()
@@ -3873,8 +3940,8 @@ class Page_Consulting_Table(QWidget):
) -> None: ) -> None:
if row_state is None: if row_state is None:
row_state = Page_Consulting_ConsultingSession_State( row_state = Page_Consulting_ConsultingSession_State(
user_id=self.SITE_STATE.session.user_id, nutzer_id=self.SITE_STATE.session.user_id,
user_name=self.SITE_STATE.session.user_name, nutzer_name=self.SITE_STATE.session.user_name,
) )
row = Page_Consulting_ConsultingSession(site_state=self.SITE_STATE, state=row_state) row = Page_Consulting_ConsultingSession(site_state=self.SITE_STATE, state=row_state)