diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml
index 1f2a46d..dac665a 100644
--- a/data_model/project_db_schema.aml
+++ b/data_model/project_db_schema.aml
@@ -86,7 +86,7 @@ Grunderfassung_Dokumente
geloescht "timestamp with time zone" nullable
speicher_pfad text
dateiname_original text
- dateigroesse int nullable
+ dateigroesse_bytes int nullable
mime_type text
anmerkung text
diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py
index 38b1989..cadb37e 100644
--- a/src/wce_crm/data_models.py
+++ b/src/wce_crm/data_models.py
@@ -25,6 +25,7 @@ from wce_crm.types import ConsultingType, EntityIds, EntityType, RecordingType
if TYPE_CHECKING:
from wce_crm.gui import InitRecForm, Page_Consulting_ConsultingSession # noqa: F401
+ from wce_crm.gui_components.initrec_file_selection import FileItemWidget
from wce_crm.types import ConsId, EntityType, RecId, UserId
ValidAge = Annotated[int, Field(ge=0, le=99)]
@@ -267,29 +268,28 @@ class Page_InitRec_Form_State(PageState[Module]):
@dc.dataclass(slots=True, kw_only=True)
-class Page_InitRec_DocumentSection_State:
+class Page_InitRec_DocumentSection_State(PageState["FileItemWidget"]):
session: Session
pers_id: int | None
+ doc_count: int = 0
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
locked: bool
geloescht: bool = False
- @property
- def doc_count(self) -> int:
- return len(self.docs)
-
@dc.dataclass(slots=True, kw_only=True)
-class Page_InitRec_DocumentSection_Doc_State:
+class Page_InitRec_DocumentSection_Doc_State(PageState[Module]):
+ session: Session
id: int | None
pers_id: int | None
datum_erstellt_utc: datetime.datetime = dc.field(
default_factory=lambda: datetime.datetime.now(datetime.UTC)
)
-
- speicher_pfad: Path
- dateiname_original: str
- dateigroesse: int | None
+ speicher_pfad: Path | None
+ dateiname_original: str = ""
+ dateigroesse_bytes: int | None = None
+ mime_type: str = ""
+ anmerkung: str = ""
locked: bool
geloescht: bool = False
diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py
index b1a1c6d..1baa0b8 100644
--- a/src/wce_crm/gui.py
+++ b/src/wce_crm/gui.py
@@ -130,7 +130,6 @@ if TYPE_CHECKING:
)
from wce_crm.types import (
ConsId,
- ExtMaId,
LinkingConsultationsEntryCompany,
LinkingConsultationsEntryPerson,
)
@@ -5626,7 +5625,7 @@ class Page_Consulting_ConsultingSession(QWidget):
self.btn_delete.setEnabled(False)
else:
unlock_form(self.widget_registry)
- self.btn_delete.setEnabled(False)
+ self.btn_delete.setEnabled(True)
def _sync_GUI_to_state(self) -> None:
self.STATE.zeitstempel = self.timestamp.get_pydatetime()
diff --git a/prototypes/initrec_file_selection.py b/src/wce_crm/gui_components/initrec_file_selection.py
similarity index 54%
rename from prototypes/initrec_file_selection.py
rename to src/wce_crm/gui_components/initrec_file_selection.py
index acbb1b6..4f80cde 100644
--- a/prototypes/initrec_file_selection.py
+++ b/src/wce_crm/gui_components/initrec_file_selection.py
@@ -5,7 +5,8 @@ import os
import sys
from collections.abc import Iterable
from pathlib import Path
-from typing import TYPE_CHECKING
+from pprint import pformat
+from typing import TYPE_CHECKING, Literal
from PySide6.QtCore import (
QFileInfo,
@@ -32,11 +33,23 @@ from PySide6.QtWidgets import (
QWidget,
)
+import wce_crm.constants
from wce_crm.constants import TIMEZONE_CEST
+from wce_crm.data_models import (
+ Page_InitRec_DocumentSection_Doc_State,
+ Page_InitRec_DocumentSection_State,
+ Session,
+ set_page_state,
+)
+from wce_crm.logging import logger_gui as logger
if TYPE_CHECKING:
from wce_crm.data_models import Module
+DEBUG: bool = True
+if not wce_crm.constants.Config.DEVELOPMENT_STATE:
+ DEBUG = False
+
def get_mime_type(
file_path: Path,
@@ -49,7 +62,9 @@ def get_mime_type(
return mime_type.name()
-def _format_file_size(size_in_bytes):
+def _format_file_size(
+ size_in_bytes: int,
+) -> str:
"""converts bytes in readable string, e.g. KB or MB"""
if size_in_bytes < 1024:
return f"{size_in_bytes} B"
@@ -142,20 +157,16 @@ class _DropZoneWidget(QFrame):
self.files_dropped.emit(files_pathlib)
-class _FileItemWidget(QFrame):
+class FileItemWidget(QFrame):
request_remove = Signal(QWidget) # signal to parent to remove entry
def __init__(
self,
- file_path: Path,
+ state: Page_InitRec_DocumentSection_Doc_State,
parent: QWidget | None = None,
):
super().__init__(parent)
- self.file_path = file_path
- self._created_at_UTC = datetime.datetime.now(tz=datetime.UTC)
- self.created_at_string = self._created_at_UTC.astimezone(TIMEZONE_CEST).strftime(
- "%d.%m.%Y %H:%M"
- )
+ self.STATE = state
self.setStyleSheet("""
_FileItemWidget {
@@ -173,34 +184,19 @@ class _FileItemWidget(QFrame):
layout.setContentsMargins(8, 6, 8, 6)
# file icon: use dummy to avoid icons with checkmarks (synchronised folder)
- icon_label = QLabel()
- provider = QFileIconProvider()
- file_icon = provider.icon(QFileInfo(f"dummy{file_path.suffix}"))
- icon_label.setPixmap(file_icon.pixmap(28, 28))
- layout.addWidget(icon_label)
+ self.icon_label = QLabel()
+ self.icon_provider = QFileIconProvider()
+ layout.addWidget(self.icon_label)
# file info: name, size, date, mime type
info_vbox = QVBoxLayout()
info_vbox.setSpacing(2)
- self.original_name = file_path.name
- self.mime_type = get_mime_type(file_path)
- self._raw_file_size: int | None = None
- self.file_size_display: str
- try:
- self._raw_file_size = file_path.stat().st_size
- self.file_size_display = _format_file_size(self._raw_file_size)
- except OSError:
- self.file_size_display = "Unbekannt"
+ self.file_size_display: str = ""
- name_label = QLabel(f"{self.original_name}")
- meta_label = QLabel(
- (
- f"{self.file_size_display} • Hinzugefügt: "
- f"{self.created_at_string}"
- )
- )
- meta_label.setStyleSheet("font-size: 11px;")
+ self.name_label = QLabel()
+ self.meta_label = QLabel()
+ self.meta_label.setStyleSheet("font-size: 11px;")
# additional note field for more comprehensible file descriptions
self.note_edit = QLineEdit()
@@ -218,24 +214,24 @@ class _FileItemWidget(QFrame):
}
""")
- info_vbox.addWidget(name_label)
- info_vbox.addWidget(meta_label)
+ info_vbox.addWidget(self.name_label)
+ info_vbox.addWidget(self.meta_label)
info_vbox.addWidget(self.note_edit)
layout.addLayout(info_vbox, stretch=2)
# category dropdown: not needed by WCE, but still included to re-enable later
# TODO maybe add later
self.category_combo = QComboBox()
- self.category_combo.addItems(
- [
- "Allgemein / Unsortiert",
- "Identitätsnachweis",
- "Vertrag",
- "Gewerbeanmeldung",
- "Sonstiges",
- ]
- )
- self.category_combo.setFixedWidth(160)
+ # self.category_combo.addItems(
+ # [
+ # "Allgemein / Unsortiert",
+ # "Identitätsnachweis",
+ # "Vertrag",
+ # "Gewerbeanmeldung",
+ # "Sonstiges",
+ # ]
+ # )
+ # self.category_combo.setFixedWidth(160)
layout.addWidget(self.category_combo)
self.category_combo.setVisible(False)
@@ -255,17 +251,91 @@ class _FileItemWidget(QFrame):
layout.addWidget(self.btn_preview)
layout.addWidget(self.btn_delete)
- def _open_preview(self):
- QDesktopServices.openUrl(QUrl.fromLocalFile(self.file_path))
+ if DEBUG:
+ btn_state = QPushButton("STATE")
+ btn_state.setToolTip("Debug call...")
+ btn_state.setCursor(Qt.CursorShape.PointingHandCursor)
+ btn_state.clicked.connect(self.get_state)
+ layout.addWidget(btn_state)
- # TODO change to protocol/interface with state
- def get_data(self):
- return {
- "file_path": self.file_path,
- "created_at": self._created_at_UTC,
- "category": None,
- "note": self.note_edit.text().strip(),
- }
+ self._sync_state_to_GUI(None)
+
+ def _sync_state_to_GUI(
+ self,
+ section: Literal["locking", "content"] | None,
+ ) -> None:
+
+ if section is None or section == "content":
+ created_at_string = self.STATE.datum_erstellt_utc.astimezone(
+ TIMEZONE_CEST
+ ).strftime("%d.%m.%Y %H:%M")
+
+ file_icon = self.icon_provider.icon(
+ QFileInfo(f"dummy{self.STATE.speicher_pfad.suffix}")
+ )
+ self.icon_label.setPixmap(file_icon.pixmap(28, 28))
+
+ self.STATE.dateiname_original = self.STATE.speicher_pfad.name
+ self.STATE.mime_type = get_mime_type(self.STATE.speicher_pfad)
+
+ try:
+ self.STATE.dateigroesse_bytes = self.STATE.speicher_pfad.stat().st_size
+ self.file_size_display = _format_file_size(self.STATE.dateigroesse_bytes)
+ except OSError:
+ self.file_size_display = "Unbekannt"
+
+ self.name_label.setText(f"{self.STATE.dateiname_original}")
+ self.meta_label.setText(
+ (
+ f"{self.file_size_display} • Hinzugefügt: "
+ f"{created_at_string}"
+ )
+ )
+ self.note_edit.setText(self.STATE.anmerkung)
+
+ if section is None or section == "locking":
+ if self.STATE.locked:
+ self.btn_delete.setEnabled(False)
+ self.note_edit.setReadOnly(True)
+ else:
+ self.btn_delete.setEnabled(True)
+ self.note_edit.setReadOnly(False)
+
+ def _sync_GUI_to_state(self) -> None:
+ self.STATE.anmerkung = self.note_edit.text().strip()
+
+ def _open_preview(self):
+ QDesktopServices.openUrl(QUrl.fromLocalFile(self.STATE.speicher_pfad))
+
+ def validate(self) -> list[str]:
+ err_list: list[str] = []
+ return err_list
+
+ def save_data(self) -> None:
+ pass
+
+ def load_state(
+ self,
+ new_state: Page_InitRec_DocumentSection_Doc_State,
+ ) -> None:
+ set_page_state(self.STATE, new_state)
+ self._sync_state_to_GUI(None)
+
+ def lock(self) -> None:
+ self.STATE.locked = True
+ self._sync_state_to_GUI("locking")
+
+ def unlock(self) -> None:
+ self.STATE.locked = False
+ self._sync_state_to_GUI("locking")
+
+ def get_state(self) -> Page_InitRec_DocumentSection_Doc_State:
+ self._sync_GUI_to_state()
+ logger.debug(
+ "[Module -- DocumentSelection] Current state of row:\n%s",
+ pformat(self.STATE),
+ )
+ return self.STATE
class DocumentSectionWidget(QWidget):
@@ -275,9 +345,11 @@ class DocumentSectionWidget(QWidget):
def __init__(
self,
+ state: Page_InitRec_DocumentSection_State,
parent: QWidget | None = None,
):
super().__init__(parent)
+ self.STATE = state
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
@@ -304,14 +376,127 @@ class DocumentSectionWidget(QWidget):
main_layout.addWidget(self.file_list_widget)
+ def _sync_state_to_GUI(self, section: Literal["content", "locking"] | None) -> None:
+ self.setUpdatesEnabled(False)
+
+ if section is None or section == "content":
+ self.remove_all_rows()
+ for doc in self.STATE.docs:
+ if not doc.geloescht:
+ self.add_row(doc, True)
+
+ self.count_changed.emit(self.STATE.doc_count)
+
+ if section is None or section == "locking":
+ if self.STATE.locked:
+ for m in self.STATE.child_modules:
+ m.lock()
+ else:
+ for m in self.STATE.child_modules:
+ m.unlock()
+
+ self.setUpdatesEnabled(True)
+
+ def _sync_GUI_to_state(self) -> None:
+ logger.debug(
+ (
+ "[Module -- DocumentSelection] Document selection widget: Call "
+ "syncing GUI to state"
+ )
+ )
+ logger.debug(
+ "[Module -- DocumentSelection] Document selection widget - Child modules: %s",
+ pformat(self.STATE.child_modules),
+ )
+
+ # Child modules are ACTIVE elements, viewable and changeable by the user.
+ # The states are already part of this widget and linked by reference, but
+ # the current user input must be synced with these states. Therefore, the
+ # `get_state` method of these children is called, which implicitly updates
+ # their state. The returned state reference is not needed.
+ for m in self.STATE.child_modules:
+ _ = m.get_state()
+
+ self.STATE.doc_count = self.file_list_widget.count()
+
+ def lock(self) -> None:
+ self.STATE.locked = True
+ self._sync_state_to_GUI("locking")
+
+ def unlock(self) -> None:
+ self.STATE.locked = False
+ self._sync_state_to_GUI("locking")
+
+ def validate(self) -> list[str]:
+ err_list: list[str] = []
+ for m in self.STATE.child_modules:
+ err_list.extend(m.validate())
+
+ return err_list
+
+ def load_state(
+ self,
+ new_state: Page_InitRec_DocumentSection_State,
+ ) -> None:
+ set_page_state(self.STATE, new_state)
+ self._sync_state_to_GUI(None)
+
+ def save_data(self) -> None:
+ pass
+
+ def remove_all_rows(self) -> None:
+ while self.STATE.child_modules:
+ self._remove_row(self.STATE.child_modules[0])
+
+ def add_row(
+ self,
+ row_state: Page_InitRec_DocumentSection_Doc_State | None,
+ state_exists: bool,
+ ) -> None:
+ if row_state is None:
+ new_state = Page_InitRec_DocumentSection_Doc_State(
+ session=self.STATE.session,
+ id=None,
+ pers_id=self.STATE.pers_id,
+ speicher_pfad=None,
+ locked=self.STATE.locked,
+ )
+
+ item_widget = FileItemWidget(new_state)
+ item_widget.request_remove.connect(self._remove_row)
+ # embed in QListWidget
+ list_item = QListWidgetItem(self.file_list_widget)
+ # min size to avoid cropping of text
+ size_hint = item_widget.sizeHint()
+ size_hint.setHeight(size_hint.height() + 4)
+ list_item.setSizeHint(size_hint)
+
+ self.file_list_widget.addItem(list_item)
+ self.file_list_widget.setItemWidget(list_item, item_widget)
+
+ self.STATE.child_modules.append(item_widget)
+ if not state_exists:
+ self.STATE.docs.append(new_state)
+
+ self.STATE.doc_count = self.file_list_widget.count()
+
def _add_files(
self,
filepaths: Iterable[Path],
):
for path in filepaths:
# create custom row widget
- item_widget = _FileItemWidget(path)
- item_widget.request_remove.connect(self._remove_file_item)
+ new_state = Page_InitRec_DocumentSection_Doc_State(
+ session=self.STATE.session,
+ id=None,
+ pers_id=self.STATE.pers_id,
+ speicher_pfad=path,
+ locked=self.STATE.locked,
+ )
+ self.STATE.docs.append(new_state)
+ item_widget = FileItemWidget(new_state)
+ item_widget.request_remove.connect(self._remove_row)
+ self.STATE.child_modules.append(item_widget)
# embed in QListWidget
list_item = QListWidgetItem(self.file_list_widget)
@@ -323,9 +508,10 @@ class DocumentSectionWidget(QWidget):
self.file_list_widget.addItem(list_item)
self.file_list_widget.setItemWidget(list_item, item_widget)
+ self.STATE.doc_count = self.file_list_widget.count()
self.count_changed.emit(self.file_list_widget.count())
- def _remove_file_item(
+ def _remove_row(
self,
widget: QWidget,
):
@@ -333,9 +519,23 @@ class DocumentSectionWidget(QWidget):
for i in range(self.file_list_widget.count()):
item = self.file_list_widget.item(i)
if self.file_list_widget.itemWidget(item) == widget:
+ assert isinstance(widget, FileItemWidget)
self.file_list_widget.takeItem(i)
+ self.STATE.child_modules.remove(widget)
break
- self.count_changed.emit(self.file_list_widget.count())
+ # do not remove the corresponding state since it is tracked
+ # to be post-processed when the database saving operation is triggered
+ self.STATE.
+ self.count_changed.emit())
+
+ def get_state(self) -> Page_InitRec_DocumentSection_State:
+ self._sync_GUI_to_state()
+ logger.debug(
+ "[Module -- DocumentSelection] Current state of document selection widget:\n%s",
+ pformat(self.STATE),
+ )
+
+ return self.STATE
# TODO change to protocol/interface with state
def get_all_documents_data(self):
@@ -344,8 +544,8 @@ class DocumentSectionWidget(QWidget):
for i in range(self.file_list_widget.count()):
item = self.file_list_widget.item(i)
widget = self.file_list_widget.itemWidget(item)
- if isinstance(widget, _FileItemWidget):
- documents.append(widget.get_data())
+ if isinstance(widget, FileItemWidget):
+ documents.append(widget.get_state())
return documents
@@ -366,7 +566,11 @@ class _MainWindow(QMainWindow):
layout.addWidget(self.header_label)
# custom widget
- self.doc_section = DocumentSectionWidget()
+ self.SESSION = Session(user_id=42, user_name="TEST-USER")
+ state = Page_InitRec_DocumentSection_State(
+ session=self.SESSION, pers_id=None, locked=False
+ )
+ self.doc_section = DocumentSectionWidget(state)
self.doc_section.count_changed.connect(self.update_header)
layout.addWidget(self.doc_section)
@@ -406,7 +610,7 @@ def main() -> None:
if TYPE_CHECKING:
# enable static type checking for the protocol
_m1: type[Module] = DocumentSectionWidget
- _m2: type[Module] = _FileItemWidget
+ _m2: type[Module] = FileItemWidget
if __name__ == "__main__":
main()