generated from dopt-python/py311
Upload and management for documents in initial recording #6
@@ -2,4 +2,5 @@ DOPT_STOP_FOLDER_NAME=python
|
|||||||
DOPT_DB_CRM=data/wce_crm.db
|
DOPT_DB_CRM=data/wce_crm.db
|
||||||
DOPT_DB_MAIN=data/wce_grunderfassung.db
|
DOPT_DB_MAIN=data/wce_grunderfassung.db
|
||||||
DOPT_PATH_LOGGING=data/logs
|
DOPT_PATH_LOGGING=data/logs
|
||||||
|
DOPT_PATH_FILE_STORAGE=data/files
|
||||||
DOPT_ALEMBIC_BASE=python/alembic
|
DOPT_ALEMBIC_BASE=python/alembic
|
||||||
@@ -5,4 +5,5 @@
|
|||||||
- DOPT_DB_CRM: path to CRM database, relative to base path
|
- DOPT_DB_CRM: path to CRM database, relative to base path
|
||||||
- DOPT_DB_MAIN: path to main database, relative to base path
|
- DOPT_DB_MAIN: path to main database, relative to base path
|
||||||
- DOPT_PATH_LOGGING: path to logging folder, relative to base path
|
- DOPT_PATH_LOGGING: path to logging folder, relative to base path
|
||||||
|
- DOPT_PATH_FILE_STORAGE: path to folder for saving files
|
||||||
- DOPT_ALEMBIC_BASE: path to all relevant alembic file
|
- DOPT_ALEMBIC_BASE: path to all relevant alembic file
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import copy
|
|||||||
import datetime
|
import datetime
|
||||||
from collections.abc import Container, Mapping
|
from collections.abc import Container, Mapping
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
from typing import TYPE_CHECKING, Any, Final, TypeVar, cast
|
||||||
|
|
||||||
import polars as pl
|
import polars as pl
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@@ -12,7 +12,12 @@ from dopt_basics.result_pattern import wrap_result
|
|||||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||||
|
|
||||||
from wce_crm import db
|
from wce_crm import db
|
||||||
from wce_crm.constants import TIMEZONE_CEST
|
from wce_crm.backend.document_service import (
|
||||||
|
DocumentRepository,
|
||||||
|
DocumentService,
|
||||||
|
FileStorageService,
|
||||||
|
)
|
||||||
|
from wce_crm.constants import TIMEZONE_CEST, Config
|
||||||
from wce_crm.data_models import (
|
from wce_crm.data_models import (
|
||||||
FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN,
|
FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN,
|
||||||
FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN,
|
FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN,
|
||||||
@@ -22,7 +27,7 @@ from wce_crm.data_models import (
|
|||||||
InitRec,
|
InitRec,
|
||||||
Page_InitRec_Form_Data,
|
Page_InitRec_Form_Data,
|
||||||
)
|
)
|
||||||
from wce_crm.logging import logger_back as logger
|
from wce_crm.logging import logger_backend as logger
|
||||||
from wce_crm.types import (
|
from wce_crm.types import (
|
||||||
CompanyInfo,
|
CompanyInfo,
|
||||||
CompanyProfileConsultationEntry,
|
CompanyProfileConsultationEntry,
|
||||||
@@ -45,6 +50,11 @@ if TYPE_CHECKING:
|
|||||||
K = TypeVar("K")
|
K = TypeVar("K")
|
||||||
V = TypeVar("V")
|
V = TypeVar("V")
|
||||||
|
|
||||||
|
INITREC_DOC_SERVICE: Final[DocumentService] = DocumentService(
|
||||||
|
repo=DocumentRepository(file_storage_dir=Config.PATH_FILE_STORAGE),
|
||||||
|
storage=FileStorageService(storage_dir=Config.PATH_FILE_STORAGE),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _transform_for_gui_output(
|
def _transform_for_gui_output(
|
||||||
data: pl.DataFrame,
|
data: pl.DataFrame,
|
||||||
@@ -244,6 +254,10 @@ def page_initrec_to_db(
|
|||||||
data.form_data.metadata = in_filter
|
data.form_data.metadata = in_filter
|
||||||
data.form_data.person = out_filter
|
data.form_data.person = out_filter
|
||||||
|
|
||||||
|
# documents
|
||||||
|
assert data.ids.pers_id
|
||||||
|
data.docs = INITREC_DOC_SERVICE.to_db(conn, pers_id=data.ids.pers_id, docs=data.docs)
|
||||||
|
|
||||||
# company
|
# company
|
||||||
if data.recording_type is RecordingType.WITH_COMPANY:
|
if data.recording_type is RecordingType.WITH_COMPANY:
|
||||||
# insert into company table and add additional information
|
# insert into company table and add additional information
|
||||||
@@ -378,11 +392,16 @@ def page_initrec_from_db(
|
|||||||
"[InitRec -- backend] Loaded entries which seem to be deleted - IDs: %s", ids
|
"[InitRec -- backend] Loaded entries which seem to be deleted - IDs: %s", ids
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# documents
|
||||||
|
with db.ENGINE.connect() as conn:
|
||||||
|
docs = INITREC_DOC_SERVICE.from_db(conn, pers_id=ids.pers_id)
|
||||||
|
|
||||||
return InitRec(
|
return InitRec(
|
||||||
ids=ids,
|
ids=ids,
|
||||||
recording_type=recording_type,
|
recording_type=recording_type,
|
||||||
geloescht=geloescht,
|
geloescht=geloescht,
|
||||||
form_data=form_data,
|
form_data=form_data,
|
||||||
|
docs=docs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from pprint import pformat
|
||||||
|
from typing import TYPE_CHECKING, Any, Final, cast
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from wce_crm import db
|
||||||
|
from wce_crm.constants import TIMEZONE_CEST
|
||||||
|
from wce_crm.data_models import Grunderfassung_Dokument
|
||||||
|
from wce_crm.logging import logger_initrec_docs as logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from wce_crm.types import RecId
|
||||||
|
|
||||||
|
|
||||||
|
class FileStorageService:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
storage_dir: Path,
|
||||||
|
):
|
||||||
|
self.storage_dir = storage_dir
|
||||||
|
|
||||||
|
def store_file(
|
||||||
|
self,
|
||||||
|
src_path: Path,
|
||||||
|
) -> Path:
|
||||||
|
"""Copies file to the application's file storage, assgn an UUID and gives back
|
||||||
|
the absolute path."""
|
||||||
|
src_path = src_path.resolve()
|
||||||
|
assert src_path.is_file(), "provided path is not a file"
|
||||||
|
|
||||||
|
if not src_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Die Datei {src_path.name} konnte nicht gefunden werden. "
|
||||||
|
f"Existiert diese noch oder wurde sie gelöscht?"
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_now = datetime.datetime.now(tz=TIMEZONE_CEST)
|
||||||
|
month = str(ts_now.month).zfill(2)
|
||||||
|
year = str(ts_now.year)
|
||||||
|
target_dir = self.storage_dir / year / month
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
new_filename = f"{uuid.uuid4().hex}{src_path.suffix}"
|
||||||
|
target_path = target_dir / new_filename
|
||||||
|
|
||||||
|
shutil.copy2(src_path, target_path)
|
||||||
|
|
||||||
|
return target_path
|
||||||
|
|
||||||
|
def delete_file(
|
||||||
|
self,
|
||||||
|
absolute_path: Path,
|
||||||
|
) -> None:
|
||||||
|
if absolute_path.is_relative_to(self.storage_dir) and absolute_path.exists():
|
||||||
|
absolute_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentRepository:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
file_storage_dir: Path,
|
||||||
|
):
|
||||||
|
self.file_storage_dir = file_storage_dir
|
||||||
|
|
||||||
|
def from_db(
|
||||||
|
self,
|
||||||
|
conn: sa.Connection,
|
||||||
|
pers_id: RecId,
|
||||||
|
) -> list[Grunderfassung_Dokument]:
|
||||||
|
stmt = sa.select(db.t_grunderfassung_dokumente).where(
|
||||||
|
db.t_grunderfassung_dokumente.c.pers_id == pers_id,
|
||||||
|
db.t_grunderfassung_dokumente.c.geloescht.is_(None),
|
||||||
|
)
|
||||||
|
|
||||||
|
ret = conn.execute(stmt)
|
||||||
|
|
||||||
|
docs: list[Grunderfassung_Dokument] = []
|
||||||
|
for row in ret.mappings():
|
||||||
|
row = dict(row)
|
||||||
|
row["speicher_pfad"] = self.file_storage_dir / row["speicher_pfad"]
|
||||||
|
docs.append(Grunderfassung_Dokument(**row))
|
||||||
|
|
||||||
|
return docs
|
||||||
|
|
||||||
|
def to_db(
|
||||||
|
self,
|
||||||
|
conn: sa.Connection,
|
||||||
|
pers_id: RecId,
|
||||||
|
docs: list[Grunderfassung_Dokument],
|
||||||
|
) -> list[Grunderfassung_Dokument]:
|
||||||
|
rows_for_db_insert: list[dict[str, Any]] = []
|
||||||
|
rows_for_db_update: list[dict[str, Any]] = []
|
||||||
|
docs_inserted: list[Grunderfassung_Dokument] = []
|
||||||
|
|
||||||
|
for doc in docs:
|
||||||
|
doc.pers_id = pers_id
|
||||||
|
if doc.id is None:
|
||||||
|
row_data = doc.model_dump(exclude={"id"})
|
||||||
|
rows_for_db_insert.append(row_data)
|
||||||
|
docs_inserted.append(doc)
|
||||||
|
else:
|
||||||
|
row_data = doc.model_dump()
|
||||||
|
# new bind param to avoid name clashes
|
||||||
|
row_data["b_id"] = row_data["id"]
|
||||||
|
del row_data["id"]
|
||||||
|
rows_for_db_update.append(row_data)
|
||||||
|
|
||||||
|
# prepare path as relative
|
||||||
|
file_path = row_data.get("speicher_pfad")
|
||||||
|
if isinstance(file_path, Path):
|
||||||
|
if file_path.is_absolute():
|
||||||
|
file_path = file_path.relative_to(self.file_storage_dir)
|
||||||
|
row_data["speicher_pfad"] = str(file_path)
|
||||||
|
|
||||||
|
if rows_for_db_update:
|
||||||
|
# ... update
|
||||||
|
logger.debug(
|
||||||
|
"[InitRec -- Docs] Call update for docs:\n%s", pformat(rows_for_db_update)
|
||||||
|
)
|
||||||
|
stmt = db.t_grunderfassung_dokumente.update().where(
|
||||||
|
db.t_grunderfassung_dokumente.c.id == sa.bindparam("b_id")
|
||||||
|
)
|
||||||
|
conn.execute(stmt, rows_for_db_update)
|
||||||
|
|
||||||
|
if rows_for_db_insert:
|
||||||
|
# ... insert
|
||||||
|
logger.debug(
|
||||||
|
"[InitRec -- Docs] Call insert for docs:\n%s", pformat(rows_for_db_insert)
|
||||||
|
)
|
||||||
|
stmt = db.t_grunderfassung_dokumente.insert().returning(
|
||||||
|
db.t_grunderfassung_dokumente.c.id
|
||||||
|
)
|
||||||
|
res = conn.execute(stmt, rows_for_db_insert)
|
||||||
|
new_doc_ids = cast(list[int], [row[0] for row in res.fetchall()])
|
||||||
|
|
||||||
|
assert len(docs_inserted) == len(new_doc_ids)
|
||||||
|
for doc, new_id in zip(docs_inserted, new_doc_ids):
|
||||||
|
doc.id = new_id
|
||||||
|
|
||||||
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentService:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
repo: DocumentRepository,
|
||||||
|
storage: FileStorageService,
|
||||||
|
):
|
||||||
|
self.repo = repo
|
||||||
|
self.storage = storage
|
||||||
|
|
||||||
|
def to_db(
|
||||||
|
self,
|
||||||
|
conn: sa.Connection,
|
||||||
|
pers_id: RecId,
|
||||||
|
docs: list[Grunderfassung_Dokument],
|
||||||
|
) -> list[Grunderfassung_Dokument]:
|
||||||
|
try:
|
||||||
|
for doc in docs:
|
||||||
|
if doc.id is None:
|
||||||
|
doc.speicher_pfad = self.storage.store_file(doc.speicher_pfad)
|
||||||
|
except Exception:
|
||||||
|
logger.error(
|
||||||
|
"[InitRec -- Docs] Error during file saving procedure",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
docs = self.repo.to_db(conn, pers_id=pers_id, docs=docs)
|
||||||
|
return docs
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.error(
|
||||||
|
"[InitRec -- Docs] Error during DB saving procedure",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
for doc in docs:
|
||||||
|
# The delete method checks if the provided saving path is really
|
||||||
|
# relative to the storage path. Otherwise, no delete is performed to
|
||||||
|
# ensure that original files are never deleted
|
||||||
|
self.storage.delete_file(doc.speicher_pfad)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def from_db(
|
||||||
|
self,
|
||||||
|
conn: sa.Connection,
|
||||||
|
pers_id: RecId,
|
||||||
|
) -> list[Grunderfassung_Dokument]:
|
||||||
|
return self.repo.from_db(conn, pers_id=pers_id)
|
||||||
@@ -19,13 +19,21 @@ assert BASE_PATH
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
DEVELOPMENT_STATE: bool = bool(os.getenv("DOPT_DEVELOPMENT", None))
|
DEVELOPMENT_STATE: bool = bool(os.getenv("DOPT_DEVELOPMENT", None))
|
||||||
DB_PATH_KONTAKTLISTE: Path = BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db")
|
DB_PATH_KONTAKTLISTE: Path = (
|
||||||
DB_PATH_MAIN: Path = BASE_PATH / os.getenv(
|
BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db")
|
||||||
"DOPT_DB_MAIN", "data/db/wce_grunderfassung.db"
|
).resolve()
|
||||||
)
|
DB_PATH_MAIN: Path = (
|
||||||
PATH_LOGGING: Path = BASE_PATH / os.getenv("DOPT_PATH_LOGGING", "data/d-opt.log")
|
BASE_PATH / os.getenv("DOPT_DB_MAIN", "data/db/wce_grunderfassung.db")
|
||||||
|
).resolve()
|
||||||
|
PATH_LOGGING: Path = (BASE_PATH / os.getenv("DOPT_PATH_LOGGING", "data/logs")).resolve()
|
||||||
|
PATH_FILE_STORAGE: Path = (
|
||||||
|
BASE_PATH / os.getenv("DOPT_PATH_FILE_STORAGE", "data/files")
|
||||||
|
).resolve()
|
||||||
LOG_FILENAME: str = "dopt.log"
|
LOG_FILENAME: str = "dopt.log"
|
||||||
ALEMBIC_PATH: Path = BASE_PATH / os.getenv("DOPT_ALEMBIC_BASE", "python/alembic")
|
ALEMBIC_PATH: Path = (
|
||||||
|
BASE_PATH / os.getenv("DOPT_ALEMBIC_BASE", "python/alembic")
|
||||||
|
).resolve()
|
||||||
|
|
||||||
|
|
||||||
TIMEZONE_CEST: Final[zoneinfo.ZoneInfo] = zoneinfo.ZoneInfo("Europe/Berlin")
|
TIMEZONE_CEST: Final[zoneinfo.ZoneInfo] = zoneinfo.ZoneInfo("Europe/Berlin")
|
||||||
|
COLUMN_SEP: Final[str] = "__"
|
||||||
|
|||||||
+22
-25
@@ -20,6 +20,7 @@ from pydantic import (
|
|||||||
)
|
)
|
||||||
from pydantic_core import ErrorDetails
|
from pydantic_core import ErrorDetails
|
||||||
|
|
||||||
|
from wce_crm.constants import COLUMN_SEP
|
||||||
from wce_crm.form_defs import INITREC_COMP, INITREC_METADATEN, INITREC_PERSON, FormField
|
from wce_crm.form_defs import INITREC_COMP, INITREC_METADATEN, INITREC_PERSON, FormField
|
||||||
from wce_crm.types import ConsultingType, EntityIds, EntityType, RecordingType
|
from wce_crm.types import ConsultingType, EntityIds, EntityType, RecordingType
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ if TYPE_CHECKING:
|
|||||||
from wce_crm.types import ConsId, EntityType, RecId, UserId
|
from wce_crm.types import ConsId, EntityType, RecId, UserId
|
||||||
|
|
||||||
ValidAge = Annotated[int, Field(ge=0, le=99)]
|
ValidAge = Annotated[int, Field(ge=0, le=99)]
|
||||||
COLUMN_SEP: Final[str] = "__"
|
|
||||||
M = TypeVar("M", bound="Module")
|
M = TypeVar("M", bound="Module")
|
||||||
S = TypeVar("S", bound="PageState")
|
S = TypeVar("S", bound="PageState")
|
||||||
|
|
||||||
@@ -224,6 +224,7 @@ class Page_InitRec_State(PageState["InitRecForm"]):
|
|||||||
initrec_form_states: dict[EntityType, Page_InitRec_Form_State] = dc.field(
|
initrec_form_states: dict[EntityType, Page_InitRec_Form_State] = dc.field(
|
||||||
init=False, default_factory=dict
|
init=False, default_factory=dict
|
||||||
)
|
)
|
||||||
|
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
relevant_entities = frozenset(
|
relevant_entities = frozenset(
|
||||||
@@ -270,7 +271,6 @@ class Page_InitRec_Form_State(PageState[Module]):
|
|||||||
@dc.dataclass(slots=True, kw_only=True)
|
@dc.dataclass(slots=True, kw_only=True)
|
||||||
class Page_InitRec_DocumentSection_State(PageState["FileItemWidget"]):
|
class Page_InitRec_DocumentSection_State(PageState["FileItemWidget"]):
|
||||||
session: Session
|
session: Session
|
||||||
pers_id: int | None
|
|
||||||
doc_count: int = 0
|
doc_count: int = 0
|
||||||
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
|
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
|
||||||
locked: bool
|
locked: bool
|
||||||
@@ -281,7 +281,7 @@ class Page_InitRec_DocumentSection_State(PageState["FileItemWidget"]):
|
|||||||
class Page_InitRec_DocumentSection_Doc_State(PageState[Module]):
|
class Page_InitRec_DocumentSection_Doc_State(PageState[Module]):
|
||||||
session: Session
|
session: Session
|
||||||
id: int | None
|
id: int | None
|
||||||
pers_id: int | None
|
# pers_id: int | None
|
||||||
datum_hinzugefuegt: datetime.datetime = dc.field(
|
datum_hinzugefuegt: datetime.datetime = dc.field(
|
||||||
default_factory=lambda: datetime.datetime.now(datetime.UTC)
|
default_factory=lambda: datetime.datetime.now(datetime.UTC)
|
||||||
)
|
)
|
||||||
@@ -294,28 +294,6 @@ class Page_InitRec_DocumentSection_Doc_State(PageState[Module]):
|
|||||||
geloescht: bool = False
|
geloescht: bool = False
|
||||||
|
|
||||||
|
|
||||||
class Grunderfassung_Dokumente(BaseModel):
|
|
||||||
docs: list[Grunderfassung_Dokument]
|
|
||||||
|
|
||||||
|
|
||||||
class Grunderfassung_Dokument(BaseModel):
|
|
||||||
model_config = ConfigDict(str_strip_whitespace=True)
|
|
||||||
|
|
||||||
id: int | None
|
|
||||||
pers_id: int | None
|
|
||||||
speicher_pfad: Path
|
|
||||||
dateiname_original: str = ""
|
|
||||||
datum_hinzugefuegt: AwareDatetime
|
|
||||||
dateigroesse_bytes: int | None = None
|
|
||||||
mime_type: str = ""
|
|
||||||
anmerkung: str = ""
|
|
||||||
|
|
||||||
# default in SQLAlchemy with lambda and timezone-aware datetime
|
|
||||||
erstellt: AwareDatetime | None = Field(default=None, exclude=True)
|
|
||||||
aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above
|
|
||||||
geloescht: AwareDatetime | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dc.dataclass(slots=True, kw_only=True)
|
@dc.dataclass(slots=True, kw_only=True)
|
||||||
class Page_Consulting_State(PageState[Module]):
|
class Page_Consulting_State(PageState[Module]):
|
||||||
session: Session
|
session: Session
|
||||||
@@ -485,11 +463,30 @@ class FlatBaseModel(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# ** InitRec
|
# ** InitRec
|
||||||
|
class Grunderfassung_Dokument(BaseModel):
|
||||||
|
model_config = ConfigDict(str_strip_whitespace=True)
|
||||||
|
|
||||||
|
id: int | None
|
||||||
|
pers_id: int | None
|
||||||
|
speicher_pfad: Path
|
||||||
|
dateiname_original: str
|
||||||
|
datum_hinzugefuegt: AwareDatetime
|
||||||
|
dateigroesse_bytes: int | None = None
|
||||||
|
mime_type: str
|
||||||
|
anmerkung: str
|
||||||
|
|
||||||
|
# default in SQLAlchemy with lambda and timezone-aware datetime
|
||||||
|
erstellt: AwareDatetime | None = Field(default=None, exclude=True)
|
||||||
|
aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above
|
||||||
|
geloescht: AwareDatetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class InitRec(BaseModel):
|
class InitRec(BaseModel):
|
||||||
ids: EntityIds
|
ids: EntityIds
|
||||||
recording_type: RecordingType
|
recording_type: RecordingType
|
||||||
geloescht: AwareDatetime | None = None
|
geloescht: AwareDatetime | None = None
|
||||||
form_data: Page_InitRec_Form_Data
|
form_data: Page_InitRec_Form_Data
|
||||||
|
docs: list[Grunderfassung_Dokument]
|
||||||
|
|
||||||
|
|
||||||
# mapping between initrec form field keys and database properties
|
# mapping between initrec form field keys and database properties
|
||||||
|
|||||||
+1
-1
@@ -525,7 +525,7 @@ t_grunderfassung_dokumente: Table = Table(
|
|||||||
UTCDateTime,
|
UTCDateTime,
|
||||||
nullable=True,
|
nullable=True,
|
||||||
),
|
),
|
||||||
Column("speicherpfad", sa.Text, nullable=False),
|
Column("speicher_pfad", sa.Text, nullable=False),
|
||||||
Column("dateiname_original", sa.Text, nullable=False),
|
Column("dateiname_original", sa.Text, nullable=False),
|
||||||
Column("dateigroesse_bytes", sa.Integer, nullable=True),
|
Column("dateigroesse_bytes", sa.Integer, nullable=True),
|
||||||
Column("datum_hinzugefuegt", UTCDateTime, nullable=False),
|
Column("datum_hinzugefuegt", UTCDateTime, nullable=False),
|
||||||
|
|||||||
+156
-22
@@ -82,17 +82,19 @@ from PySide6.QtWidgets import (
|
|||||||
import wce_crm.constants
|
import wce_crm.constants
|
||||||
from wce_crm import types
|
from wce_crm import types
|
||||||
from wce_crm.backend import backend
|
from wce_crm.backend import backend
|
||||||
from wce_crm.constants import TIMEZONE_CEST
|
from wce_crm.constants import COLUMN_SEP, TIMEZONE_CEST
|
||||||
from wce_crm.data_models import (
|
from wce_crm.data_models import (
|
||||||
COLUMN_SEP,
|
|
||||||
Beratungsgespraech_Einzelgespraech,
|
Beratungsgespraech_Einzelgespraech,
|
||||||
Beratungsgespraech_Vorgang,
|
Beratungsgespraech_Vorgang,
|
||||||
|
Grunderfassung_Dokument,
|
||||||
InitRec,
|
InitRec,
|
||||||
Page_CompanyProfile_State,
|
Page_CompanyProfile_State,
|
||||||
Page_Consulting_ConsultingSession_State,
|
Page_Consulting_ConsultingSession_State,
|
||||||
Page_Consulting_Linking_State,
|
Page_Consulting_Linking_State,
|
||||||
Page_Consulting_State,
|
Page_Consulting_State,
|
||||||
Page_Consulting_Table_State,
|
Page_Consulting_Table_State,
|
||||||
|
Page_InitRec_DocumentSection_Doc_State,
|
||||||
|
Page_InitRec_DocumentSection_State,
|
||||||
Page_InitRec_Form_Data,
|
Page_InitRec_Form_Data,
|
||||||
Page_InitRec_Form_State,
|
Page_InitRec_Form_State,
|
||||||
Page_InitRec_State,
|
Page_InitRec_State,
|
||||||
@@ -111,6 +113,7 @@ from wce_crm.gui_components.dialogs import (
|
|||||||
get_user_confirmation,
|
get_user_confirmation,
|
||||||
pydantic_validation_error_handling,
|
pydantic_validation_error_handling,
|
||||||
)
|
)
|
||||||
|
from wce_crm.gui_components.initrec_file_selection import DocumentSectionWidget
|
||||||
from wce_crm.logging import (
|
from wce_crm.logging import (
|
||||||
logger_custom_fields,
|
logger_custom_fields,
|
||||||
logger_get_data,
|
logger_get_data,
|
||||||
@@ -3083,12 +3086,17 @@ CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = {
|
|||||||
|
|
||||||
|
|
||||||
class CollapsibleBox(QWidget):
|
class CollapsibleBox(QWidget):
|
||||||
def __init__(self, title="", parent=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
title: str = "",
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# 1. Header-Button mit Pfeil
|
# 1. Header-Button mit Pfeil
|
||||||
self.toggle_button = QToolButton()
|
self.toggle_button = QToolButton()
|
||||||
self.toggle_button.setText(f" {title}")
|
self.base_text = f" {title}"
|
||||||
|
self.toggle_button.setText(self.base_text)
|
||||||
self.toggle_button.setCheckable(True)
|
self.toggle_button.setCheckable(True)
|
||||||
self.toggle_button.setChecked(False)
|
self.toggle_button.setChecked(False)
|
||||||
self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||||||
@@ -3108,7 +3116,10 @@ class CollapsibleBox(QWidget):
|
|||||||
main_layout.addWidget(self.toggle_button)
|
main_layout.addWidget(self.toggle_button)
|
||||||
main_layout.addWidget(self.content_area)
|
main_layout.addWidget(self.content_area)
|
||||||
|
|
||||||
def _on_toggle(self, checked: bool):
|
def _on_toggle(
|
||||||
|
self,
|
||||||
|
checked: bool,
|
||||||
|
) -> None:
|
||||||
# Pfeilrichtung ändern & Inhalt ein-/ausblenden
|
# Pfeilrichtung ändern & Inhalt ein-/ausblenden
|
||||||
self.toggle_button.setArrowType(
|
self.toggle_button.setArrowType(
|
||||||
Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow
|
Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow
|
||||||
@@ -3118,18 +3129,25 @@ class CollapsibleBox(QWidget):
|
|||||||
def setContentLayout(
|
def setContentLayout(
|
||||||
self,
|
self,
|
||||||
layout: QLayout,
|
layout: QLayout,
|
||||||
):
|
) -> None:
|
||||||
self.content_area.setLayout(layout)
|
self.content_area.setLayout(layout)
|
||||||
|
|
||||||
def setContentWidget(
|
def setContentWidget(
|
||||||
self,
|
self,
|
||||||
widget: QWidget,
|
widget: QWidget,
|
||||||
):
|
) -> None:
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setContentsMargins(0, 0, 0, 0) # Keine zusätzlichen Ränder
|
layout.setContentsMargins(0, 0, 0, 0) # Keine zusätzlichen Ränder
|
||||||
layout.addWidget(widget)
|
layout.addWidget(widget)
|
||||||
self.setContentLayout(layout)
|
self.setContentLayout(layout)
|
||||||
|
|
||||||
|
def set_additional_text(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
) -> None:
|
||||||
|
new_txt = self.base_text + text
|
||||||
|
self.toggle_button.setText(new_txt)
|
||||||
|
|
||||||
def set_open(
|
def set_open(
|
||||||
self,
|
self,
|
||||||
open: bool,
|
open: bool,
|
||||||
@@ -3362,13 +3380,30 @@ class Page_InitRec(QWidget):
|
|||||||
QLineEdit, search_widget.company_widgets["ma_unternehmensname"]
|
QLineEdit, search_widget.company_widgets["ma_unternehmensname"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ** documents
|
||||||
|
self.docs_box = CollapsibleBox("Dokumente")
|
||||||
|
docs_widget_state = Page_InitRec_DocumentSection_State(
|
||||||
|
session=self.STATE.session,
|
||||||
|
docs=self.STATE.docs,
|
||||||
|
locked=self.STATE.locked,
|
||||||
|
geloescht=self.STATE.geloescht,
|
||||||
|
)
|
||||||
|
self.docs_widget = DocumentSectionWidget(docs_widget_state)
|
||||||
|
self.docs_box.setContentWidget(self.docs_widget)
|
||||||
|
self.container_layout.addWidget(self.docs_box)
|
||||||
|
self.docs_widget.doc_count_changed.connect(self._trigger_update_docs_box_title)
|
||||||
|
|
||||||
self.container_layout.addStretch()
|
self.container_layout.addStretch()
|
||||||
self._update_layout()
|
self._update_layout()
|
||||||
|
|
||||||
def _debug_print_state(self) -> None:
|
def _debug_print_state(self) -> None:
|
||||||
logger_initrecform.info(
|
self._sync_GUI_to_state()
|
||||||
|
logger_page_initrec.info(
|
||||||
"[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids)
|
"[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids)
|
||||||
)
|
)
|
||||||
|
logger_page_initrec.info(
|
||||||
|
"[Page -- InitRec] Printing state Docs:\n%s", pformat(self.STATE.docs)
|
||||||
|
)
|
||||||
|
|
||||||
def _request_company_profile(self) -> None:
|
def _request_company_profile(self) -> None:
|
||||||
assert self.STATE.ids.un_id is not None, (
|
assert self.STATE.ids.un_id is not None, (
|
||||||
@@ -3378,9 +3413,16 @@ class Page_InitRec(QWidget):
|
|||||||
session=self.STATE.session,
|
session=self.STATE.session,
|
||||||
un_id=self.STATE.ids.un_id,
|
un_id=self.STATE.ids.un_id,
|
||||||
)
|
)
|
||||||
logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state)
|
logger_page_initrec.debug("[Page -- InitRec] State to call: %s", req_state)
|
||||||
self.company_profile_requested.emit(req_state)
|
self.company_profile_requested.emit(req_state)
|
||||||
|
|
||||||
|
def _trigger_update_docs_box_title(
|
||||||
|
self,
|
||||||
|
count: int,
|
||||||
|
) -> None:
|
||||||
|
add_txt = f" ({count})"
|
||||||
|
self.docs_box.set_additional_text(add_txt)
|
||||||
|
|
||||||
def _update_layout(self) -> None:
|
def _update_layout(self) -> None:
|
||||||
self.initrec_form_metadata_box.set_open(True)
|
self.initrec_form_metadata_box.set_open(True)
|
||||||
if self.STATE.recording_type is RecordingType.WITH_COMPANY:
|
if self.STATE.recording_type is RecordingType.WITH_COMPANY:
|
||||||
@@ -3518,6 +3560,7 @@ class Page_InitRec(QWidget):
|
|||||||
def _reset(self) -> None:
|
def _reset(self) -> None:
|
||||||
for form in self.STATE.child_modules:
|
for form in self.STATE.child_modules:
|
||||||
form.reset_form()
|
form.reset_form()
|
||||||
|
self.STATE.docs.clear()
|
||||||
self.STATE.ids = EntityIds()
|
self.STATE.ids = EntityIds()
|
||||||
self.STATE.geloescht = False
|
self.STATE.geloescht = False
|
||||||
|
|
||||||
@@ -3543,7 +3586,6 @@ class Page_InitRec(QWidget):
|
|||||||
logger_initrecform.debug("[Page -- InitRec] Loading: No ID set. Do nothing!")
|
logger_initrecform.debug("[Page -- InitRec] Loading: No ID set. Do nothing!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO add loading logic with backend call
|
|
||||||
initrec = backend.page_initrec_from_db(ids)
|
initrec = backend.page_initrec_from_db(ids)
|
||||||
|
|
||||||
# TODO re-enable later
|
# TODO re-enable later
|
||||||
@@ -3612,6 +3654,31 @@ class Page_InitRec(QWidget):
|
|||||||
case _:
|
case _:
|
||||||
raise RuntimeError(f"Unspecified entity type {ent_type}")
|
raise RuntimeError(f"Unspecified entity type {ent_type}")
|
||||||
|
|
||||||
|
if update_only:
|
||||||
|
relevant_gui_states = [s for s in self.STATE.docs if not s.geloescht]
|
||||||
|
rel_doc_states = [s for s in data.docs if s.geloescht is None]
|
||||||
|
assert len(relevant_gui_states) == len(rel_doc_states)
|
||||||
|
for gui_state, pydantic_state in zip(relevant_gui_states, rel_doc_states):
|
||||||
|
gui_state.id = pydantic_state.id
|
||||||
|
gui_state.speicher_pfad = pydantic_state.speicher_pfad
|
||||||
|
else:
|
||||||
|
self.STATE.docs.clear()
|
||||||
|
for pydantic_state in data.docs:
|
||||||
|
# Deleted sessions are not part of the retrieved data from the backend if this
|
||||||
|
# page is initialised from a fresh backend call to get data. (Updates do
|
||||||
|
# indeed have data which must be filtered -- see above)
|
||||||
|
doc_state = Page_InitRec_DocumentSection_Doc_State(
|
||||||
|
session=self.STATE.session,
|
||||||
|
id=pydantic_state.id,
|
||||||
|
speicher_pfad=pydantic_state.speicher_pfad,
|
||||||
|
dateiname_original=pydantic_state.dateiname_original,
|
||||||
|
datum_hinzugefuegt=pydantic_state.datum_hinzugefuegt,
|
||||||
|
mime_type=pydantic_state.mime_type,
|
||||||
|
anmerkung=pydantic_state.anmerkung,
|
||||||
|
locked=self.STATE.locked,
|
||||||
|
)
|
||||||
|
self.STATE.docs.append(doc_state)
|
||||||
|
|
||||||
if sync_to_GUI:
|
if sync_to_GUI:
|
||||||
self._sync_state_to_GUI(None)
|
self._sync_state_to_GUI(None)
|
||||||
|
|
||||||
@@ -3630,6 +3697,14 @@ class Page_InitRec(QWidget):
|
|||||||
self._reset()
|
self._reset()
|
||||||
self._update_layout()
|
self._update_layout()
|
||||||
|
|
||||||
|
doc_widget_state = Page_InitRec_DocumentSection_State(
|
||||||
|
session=self.STATE.session,
|
||||||
|
docs=self.STATE.docs,
|
||||||
|
locked=self.STATE.locked,
|
||||||
|
geloescht=self.STATE.geloescht,
|
||||||
|
)
|
||||||
|
self.docs_widget.load_state(doc_widget_state)
|
||||||
|
|
||||||
if section is None or section == "banners":
|
if section is None or section == "banners":
|
||||||
self._clear_info_banners()
|
self._clear_info_banners()
|
||||||
if self.STATE.ids.pers_id is None:
|
if self.STATE.ids.pers_id is None:
|
||||||
@@ -3642,10 +3717,12 @@ class Page_InitRec(QWidget):
|
|||||||
if self.STATE.locked:
|
if self.STATE.locked:
|
||||||
for form in self.STATE.child_modules:
|
for form in self.STATE.child_modules:
|
||||||
form.lock()
|
form.lock()
|
||||||
|
self.docs_widget.lock()
|
||||||
self.edit_buttons.set_modify_state(False, set_delete_btn=True)
|
self.edit_buttons.set_modify_state(False, set_delete_btn=True)
|
||||||
else:
|
else:
|
||||||
for form in self.STATE.child_modules:
|
for form in self.STATE.child_modules:
|
||||||
form.unlock()
|
form.unlock()
|
||||||
|
self.docs_widget.unlock()
|
||||||
self.edit_buttons.set_modify_state(True, set_delete_btn=False)
|
self.edit_buttons.set_modify_state(True, set_delete_btn=False)
|
||||||
self._activate_delete()
|
self._activate_delete()
|
||||||
|
|
||||||
@@ -3693,6 +3770,9 @@ class Page_InitRec(QWidget):
|
|||||||
case _:
|
case _:
|
||||||
raise RuntimeError(f"Unspecified entity type {form_state.ent_type}")
|
raise RuntimeError(f"Unspecified entity type {form_state.ent_type}")
|
||||||
|
|
||||||
|
docs_widget_state = self.docs_widget.get_state()
|
||||||
|
self.STATE.docs = docs_widget_state.docs
|
||||||
|
|
||||||
logger_page_initrec.debug(
|
logger_page_initrec.debug(
|
||||||
"[Page -- InitRec] Syncing GUI to state: IDs after...\n%s",
|
"[Page -- InitRec] Syncing GUI to state: IDs after...\n%s",
|
||||||
pformat(self.STATE.ids),
|
pformat(self.STATE.ids),
|
||||||
@@ -3729,6 +3809,8 @@ class Page_InitRec(QWidget):
|
|||||||
|
|
||||||
errors.extend(m.validate())
|
errors.extend(m.validate())
|
||||||
|
|
||||||
|
errors.extend(self.docs_widget.validate())
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def lock(self) -> None:
|
def lock(self) -> None:
|
||||||
@@ -3763,13 +3845,35 @@ class Page_InitRec(QWidget):
|
|||||||
"[Page -- InitRec] This was saved in the database "
|
"[Page -- InitRec] This was saved in the database "
|
||||||
"and is marked for deletion - fast path."
|
"and is marked for deletion - fast path."
|
||||||
)
|
)
|
||||||
deleted_datetime = datetime.datetime.now(datetime.UTC)
|
ts_deleted = datetime.datetime.now(datetime.UTC)
|
||||||
assert self.STATE.form_data, "cannot delete entry with uninitialised form data"
|
assert self.STATE.form_data, "cannot delete entry with uninitialised form data"
|
||||||
|
|
||||||
|
pydantic_docs: list[Grunderfassung_Dokument] = []
|
||||||
|
for doc_state in self.STATE.docs:
|
||||||
|
if doc_state.id is None:
|
||||||
|
logger_page_initrec.debug(
|
||||||
|
(
|
||||||
|
"[Page -- InitRec] State which was never saved in the database "
|
||||||
|
"and is marked for deletion - ignore."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
doc_state_dict = dc.asdict(doc_state)
|
||||||
|
del doc_state_dict["geloescht"]
|
||||||
|
doc = Grunderfassung_Dokument(
|
||||||
|
geloescht=ts_deleted,
|
||||||
|
pers_id=self.STATE.ids.pers_id,
|
||||||
|
**doc_state_dict,
|
||||||
|
)
|
||||||
|
pydantic_docs.append(doc)
|
||||||
|
|
||||||
data_to_db = InitRec(
|
data_to_db = InitRec(
|
||||||
ids=self.STATE.ids,
|
ids=self.STATE.ids,
|
||||||
recording_type=self.STATE.recording_type,
|
recording_type=self.STATE.recording_type,
|
||||||
geloescht=deleted_datetime,
|
geloescht=ts_deleted,
|
||||||
form_data=db_data,
|
form_data=db_data,
|
||||||
|
docs=pydantic_docs,
|
||||||
)
|
)
|
||||||
returned = backend.page_initrec_to_db(data_to_db)
|
returned = backend.page_initrec_to_db(data_to_db)
|
||||||
self._load_from_pydantic(returned)
|
self._load_from_pydantic(returned)
|
||||||
@@ -3817,6 +3921,45 @@ class Page_InitRec(QWidget):
|
|||||||
case _:
|
case _:
|
||||||
raise RuntimeError(f"Unspecified entity type {ent_type}")
|
raise RuntimeError(f"Unspecified entity type {ent_type}")
|
||||||
|
|
||||||
|
ts_deleted: datetime.datetime | None = None
|
||||||
|
if self.STATE.geloescht:
|
||||||
|
ts_deleted = datetime.datetime.now(datetime.UTC)
|
||||||
|
|
||||||
|
pydantic_docs: list[Grunderfassung_Dokument] = []
|
||||||
|
for doc_state in self.STATE.docs:
|
||||||
|
if doc_state.id is None and doc_state.geloescht:
|
||||||
|
logger_page_initrec.debug(
|
||||||
|
(
|
||||||
|
"[Page -- InitRec] State which was never saved in the database "
|
||||||
|
"and is marked for deletion - ignore."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ts_deleted_doc: datetime.datetime | None = None
|
||||||
|
if ts_deleted is not None:
|
||||||
|
# if process is deleted, all sessions must be marked as deleted
|
||||||
|
ts_deleted_doc = ts_deleted
|
||||||
|
elif doc_state.geloescht:
|
||||||
|
ts_deleted_doc = datetime.datetime.now(datetime.UTC)
|
||||||
|
|
||||||
|
doc_state_dict = dc.asdict(doc_state)
|
||||||
|
del doc_state_dict["geloescht"]
|
||||||
|
doc = Grunderfassung_Dokument(
|
||||||
|
geloescht=ts_deleted_doc,
|
||||||
|
pers_id=self.STATE.ids.pers_id,
|
||||||
|
**doc_state_dict,
|
||||||
|
)
|
||||||
|
pydantic_docs.append(doc)
|
||||||
|
|
||||||
|
data_to_db = InitRec(
|
||||||
|
ids=self.STATE.ids,
|
||||||
|
recording_type=self.STATE.recording_type,
|
||||||
|
geloescht=ts_deleted,
|
||||||
|
form_data=db_data,
|
||||||
|
docs=pydantic_docs,
|
||||||
|
)
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger_page_initrec.error(
|
logger_page_initrec.error(
|
||||||
"[Page -- InitRec] Error during Pydantic validation phase:\n%s",
|
"[Page -- InitRec] Error during Pydantic validation phase:\n%s",
|
||||||
@@ -3834,19 +3977,10 @@ class Page_InitRec(QWidget):
|
|||||||
"[Page -- InitRec] Form data with 'exlude' (must be saved in the database):\n%s",
|
"[Page -- InitRec] Form data with 'exlude' (must be saved in the database):\n%s",
|
||||||
pformat(db_data),
|
pformat(db_data),
|
||||||
)
|
)
|
||||||
deleted_datetime: datetime.datetime | None = None
|
|
||||||
if self.STATE.geloescht:
|
|
||||||
deleted_datetime = datetime.datetime.now(datetime.UTC)
|
|
||||||
try:
|
try:
|
||||||
# TODO change to result pattern
|
# TODO change to result pattern
|
||||||
data_to_db = InitRec(
|
|
||||||
ids=self.STATE.ids,
|
|
||||||
recording_type=self.STATE.recording_type,
|
|
||||||
geloescht=deleted_datetime,
|
|
||||||
form_data=db_data,
|
|
||||||
)
|
|
||||||
returned = backend.page_initrec_to_db(data_to_db)
|
returned = backend.page_initrec_to_db(data_to_db)
|
||||||
|
|
||||||
self._load_from_pydantic(returned, update_only=True, sync_to_GUI=False)
|
self._load_from_pydantic(returned, update_only=True, sync_to_GUI=False)
|
||||||
self.update_triggered.emit()
|
self.update_triggered.emit()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Literal
|
|||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (
|
||||||
QFileInfo,
|
QFileInfo,
|
||||||
QMimeDatabase,
|
QMimeDatabase,
|
||||||
|
QSignalBlocker,
|
||||||
Qt,
|
Qt,
|
||||||
QUrl,
|
QUrl,
|
||||||
Signal,
|
Signal,
|
||||||
@@ -38,7 +39,6 @@ import wce_crm.constants
|
|||||||
from wce_crm.constants import TIMEZONE_CEST
|
from wce_crm.constants import TIMEZONE_CEST
|
||||||
from wce_crm.data_models import (
|
from wce_crm.data_models import (
|
||||||
Grunderfassung_Dokument,
|
Grunderfassung_Dokument,
|
||||||
Grunderfassung_Dokumente,
|
|
||||||
Page_InitRec_DocumentSection_Doc_State,
|
Page_InitRec_DocumentSection_Doc_State,
|
||||||
Page_InitRec_DocumentSection_State,
|
Page_InitRec_DocumentSection_State,
|
||||||
Session,
|
Session,
|
||||||
@@ -91,6 +91,15 @@ class _DropZoneWidget(QFrame):
|
|||||||
border-color: #0078D4;
|
border-color: #0078D4;
|
||||||
background-color: #F0F6FF;
|
background-color: #F0F6FF;
|
||||||
}
|
}
|
||||||
|
/* optics in locked state */
|
||||||
|
_DropZoneWidget:disabled {
|
||||||
|
border: 2px dashed #D0D0D0;
|
||||||
|
background-color: #F2F2F2;
|
||||||
|
}
|
||||||
|
_DropZoneWidget:disabled {
|
||||||
|
border: 2px dashed #D0D0D0;
|
||||||
|
background-color: #F2F2F2;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
STYLE_DRAG_ACTIVE = """
|
STYLE_DRAG_ACTIVE = """
|
||||||
@@ -119,10 +128,25 @@ class _DropZoneWidget(QFrame):
|
|||||||
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
layout.addWidget(self.label)
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
def changeEvent(
|
||||||
|
self,
|
||||||
|
event,
|
||||||
|
) -> None:
|
||||||
|
if event.type() == event.Type.EnabledChange:
|
||||||
|
if not self.isEnabled():
|
||||||
|
self.label.setText("🔒 <b>Dateiupload deaktiviert</b> (Formular gesperrt)")
|
||||||
|
self.setCursor(Qt.CursorShape.ForbiddenCursor)
|
||||||
|
else:
|
||||||
|
self.label.setText(
|
||||||
|
"📁 <b>Dateien hierher ziehen</b> oder <u>klicken</u> zum Auswählen"
|
||||||
|
)
|
||||||
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
super().changeEvent(event)
|
||||||
|
|
||||||
def dragEnterEvent(
|
def dragEnterEvent(
|
||||||
self,
|
self,
|
||||||
event: QDragEnterEvent,
|
event: QDragEnterEvent,
|
||||||
):
|
) -> None:
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
# check if MDB/explorer gives at least one real file (no folder)
|
# check if MDB/explorer gives at least one real file (no folder)
|
||||||
has_files = False
|
has_files = False
|
||||||
@@ -136,10 +160,16 @@ class _DropZoneWidget(QFrame):
|
|||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
self.setStyleSheet(self.STYLE_DRAG_ACTIVE)
|
self.setStyleSheet(self.STYLE_DRAG_ACTIVE)
|
||||||
|
|
||||||
def dragLeaveEvent(self, event):
|
def dragLeaveEvent(
|
||||||
|
self,
|
||||||
|
event,
|
||||||
|
) -> None:
|
||||||
self.setStyleSheet(self.STYLE_NORMAL)
|
self.setStyleSheet(self.STYLE_NORMAL)
|
||||||
|
|
||||||
def dropEvent(self, event: QDropEvent):
|
def dropEvent(
|
||||||
|
self,
|
||||||
|
event: QDropEvent,
|
||||||
|
) -> None:
|
||||||
self.dragLeaveEvent(None)
|
self.dragLeaveEvent(None)
|
||||||
|
|
||||||
valid_files: list[Path] = []
|
valid_files: list[Path] = []
|
||||||
@@ -153,7 +183,10 @@ class _DropZoneWidget(QFrame):
|
|||||||
if valid_files:
|
if valid_files:
|
||||||
self.files_dropped.emit(valid_files)
|
self.files_dropped.emit(valid_files)
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(
|
||||||
|
self,
|
||||||
|
event,
|
||||||
|
) -> None:
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen")
|
files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen")
|
||||||
if files:
|
if files:
|
||||||
@@ -167,21 +200,31 @@ class FileItemWidget(QFrame):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
state: Page_InitRec_DocumentSection_Doc_State,
|
state: Page_InitRec_DocumentSection_Doc_State,
|
||||||
|
qt_list_item: QListWidgetItem,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
):
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.STATE = state
|
self.STATE = state
|
||||||
|
self.qt_list_item = qt_list_item
|
||||||
|
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
_FileItemWidget {
|
FileItemWidget {
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
border: 1px solid #E0E0E0;
|
border: 1px solid #E0E0E0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
_FileItemWidget:hover {
|
FileItemWidget:hover {
|
||||||
border-color: #B0B0B0;
|
border-color: #B0B0B0;
|
||||||
}
|
}
|
||||||
|
/* optics in locked state */
|
||||||
|
FileItemWidget:disabled {
|
||||||
|
background-color: #F8F8F8;
|
||||||
|
border-color: #E5E5E5;
|
||||||
|
}
|
||||||
|
FileItemWidget QLabel:disabled {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
layout = QHBoxLayout(self)
|
layout = QHBoxLayout(self)
|
||||||
@@ -224,7 +267,7 @@ class FileItemWidget(QFrame):
|
|||||||
layout.addLayout(info_vbox, stretch=2)
|
layout.addLayout(info_vbox, stretch=2)
|
||||||
|
|
||||||
# category dropdown: not needed by WCE, but still included to re-enable later
|
# category dropdown: not needed by WCE, but still included to re-enable later
|
||||||
# TODO maybe add later
|
# ?? maybe add later
|
||||||
self.category_combo = QComboBox()
|
self.category_combo = QComboBox()
|
||||||
# self.category_combo.addItems(
|
# self.category_combo.addItems(
|
||||||
# [
|
# [
|
||||||
@@ -292,10 +335,12 @@ class FileItemWidget(QFrame):
|
|||||||
)
|
)
|
||||||
self.icon_label.setPixmap(file_icon.pixmap(28, 28))
|
self.icon_label.setPixmap(file_icon.pixmap(28, 28))
|
||||||
|
|
||||||
|
if not self.STATE.dateiname_original:
|
||||||
self.STATE.dateiname_original = self.STATE.speicher_pfad.name
|
self.STATE.dateiname_original = self.STATE.speicher_pfad.name
|
||||||
self.STATE.mime_type = get_mime_type(self.STATE.speicher_pfad)
|
self.STATE.mime_type = get_mime_type(self.STATE.speicher_pfad)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not self.STATE.dateigroesse_bytes or self.STATE.dateigroesse_bytes == 0:
|
||||||
self.STATE.dateigroesse_bytes = self.STATE.speicher_pfad.stat().st_size
|
self.STATE.dateigroesse_bytes = self.STATE.speicher_pfad.stat().st_size
|
||||||
self.file_size_display = _format_file_size(self.STATE.dateigroesse_bytes)
|
self.file_size_display = _format_file_size(self.STATE.dateigroesse_bytes)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -308,6 +353,7 @@ class FileItemWidget(QFrame):
|
|||||||
f"{created_at_string}</span>"
|
f"{created_at_string}</span>"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
print(f"\n\n\n {self.STATE.anmerkung=} \n {self.STATE} \n\n\n")
|
||||||
self.note_edit.setText(self.STATE.anmerkung)
|
self.note_edit.setText(self.STATE.anmerkung)
|
||||||
|
|
||||||
if section is None or section == "locking":
|
if section is None or section == "locking":
|
||||||
@@ -358,13 +404,13 @@ class FileItemWidget(QFrame):
|
|||||||
class DocumentSectionWidget(QWidget):
|
class DocumentSectionWidget(QWidget):
|
||||||
"""main widget for file selection"""
|
"""main widget for file selection"""
|
||||||
|
|
||||||
count_changed = Signal(int) # sends current number of files contained by the widget
|
doc_count_changed = Signal(int) # sends current number of files contained by the widget
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
state: Page_InitRec_DocumentSection_State,
|
state: Page_InitRec_DocumentSection_State,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
):
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.STATE = state
|
self.STATE = state
|
||||||
|
|
||||||
@@ -393,24 +439,29 @@ class DocumentSectionWidget(QWidget):
|
|||||||
|
|
||||||
main_layout.addWidget(self.file_list_widget)
|
main_layout.addWidget(self.file_list_widget)
|
||||||
|
|
||||||
def _sync_state_to_GUI(self, section: Literal["content", "locking"] | None) -> None:
|
def _sync_state_to_GUI(
|
||||||
|
self,
|
||||||
|
section: Literal["content", "locking"] | None,
|
||||||
|
) -> None:
|
||||||
self.setUpdatesEnabled(False)
|
self.setUpdatesEnabled(False)
|
||||||
|
|
||||||
if section is None or section == "content":
|
if section is None or section == "content":
|
||||||
self.remove_all_rows()
|
with QSignalBlocker(self):
|
||||||
|
self._remove_all_rows()
|
||||||
for doc in self.STATE.docs:
|
for doc in self.STATE.docs:
|
||||||
if not doc.geloescht:
|
if not doc.geloescht:
|
||||||
self._add_row(doc, True)
|
self.add_row(doc, True)
|
||||||
|
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||||
self.count_changed.emit(self.STATE.doc_count)
|
|
||||||
|
|
||||||
if section is None or section == "locking":
|
if section is None or section == "locking":
|
||||||
if self.STATE.locked:
|
if self.STATE.locked:
|
||||||
for m in self.STATE.child_modules:
|
for m in self.STATE.child_modules:
|
||||||
m.lock()
|
m.lock()
|
||||||
|
self.drop_zone.setEnabled(False)
|
||||||
else:
|
else:
|
||||||
for m in self.STATE.child_modules:
|
for m in self.STATE.child_modules:
|
||||||
m.unlock()
|
m.unlock()
|
||||||
|
self.drop_zone.setEnabled(True)
|
||||||
|
|
||||||
self.setUpdatesEnabled(True)
|
self.setUpdatesEnabled(True)
|
||||||
|
|
||||||
@@ -461,21 +512,20 @@ class DocumentSectionWidget(QWidget):
|
|||||||
def save_data(self) -> None:
|
def save_data(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_all_rows(self) -> None:
|
def _remove_all_rows(self) -> None:
|
||||||
while self.STATE.child_modules:
|
while self.STATE.child_modules:
|
||||||
self._remove_row(self.STATE.child_modules[0])
|
self._remove_row(self.STATE.child_modules[0])
|
||||||
|
|
||||||
# !! the row is never None, there are no 'empty' rows
|
def add_row(
|
||||||
# TODO
|
|
||||||
def _add_row(
|
|
||||||
self,
|
self,
|
||||||
row_state: Page_InitRec_DocumentSection_Doc_State,
|
row_state: Page_InitRec_DocumentSection_Doc_State,
|
||||||
state_exists: bool,
|
state_exists: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
item_widget = FileItemWidget(row_state)
|
|
||||||
item_widget.request_remove.connect(self._remove_row)
|
|
||||||
# embed in QListWidget
|
# embed in QListWidget
|
||||||
list_item = QListWidgetItem(self.file_list_widget)
|
list_item = QListWidgetItem(self.file_list_widget)
|
||||||
|
item_widget = FileItemWidget(row_state, list_item)
|
||||||
|
item_widget.request_remove.connect(self._remove_row)
|
||||||
|
|
||||||
# min size to avoid cropping of text
|
# min size to avoid cropping of text
|
||||||
size_hint = item_widget.sizeHint()
|
size_hint = item_widget.sizeHint()
|
||||||
size_hint.setHeight(size_hint.height() + 4)
|
size_hint.setHeight(size_hint.height() + 4)
|
||||||
@@ -488,29 +538,23 @@ class DocumentSectionWidget(QWidget):
|
|||||||
if not state_exists:
|
if not state_exists:
|
||||||
self.STATE.docs.append(row_state)
|
self.STATE.docs.append(row_state)
|
||||||
|
|
||||||
# TODO change trigger
|
|
||||||
self.STATE.doc_count = self.file_list_widget.count()
|
self.STATE.doc_count = self.file_list_widget.count()
|
||||||
|
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||||
|
|
||||||
def _remove_row(
|
def _remove_row(
|
||||||
self,
|
self,
|
||||||
widget: QWidget,
|
widget: FileItemWidget,
|
||||||
):
|
):
|
||||||
# TODO set list item directly when the row is created and reference it back
|
row = self.file_list_widget.row(widget.qt_list_item)
|
||||||
# search and delete row
|
|
||||||
for i in range(self.file_list_widget.count()):
|
if row >= 0:
|
||||||
item = self.file_list_widget.item(i)
|
self.file_list_widget.takeItem(row)
|
||||||
if self.file_list_widget.itemWidget(item) == widget:
|
|
||||||
assert isinstance(widget, FileItemWidget)
|
|
||||||
taken_item = self.file_list_widget.takeItem(i)
|
|
||||||
self.STATE.child_modules.remove(widget)
|
self.STATE.child_modules.remove(widget)
|
||||||
del taken_item
|
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
break
|
|
||||||
# do not remove the corresponding state since it is tracked
|
# do not remove the corresponding state since it is tracked
|
||||||
# to be post-processed when the database saving operation is triggered
|
# to be post-processed when the database saving operation is triggered
|
||||||
# TODO change trigger
|
|
||||||
self.STATE.doc_count = self.file_list_widget.count()
|
self.STATE.doc_count = self.file_list_widget.count()
|
||||||
self.count_changed.emit(self.STATE.doc_count)
|
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||||
|
|
||||||
def get_state(self) -> Page_InitRec_DocumentSection_State:
|
def get_state(self) -> Page_InitRec_DocumentSection_State:
|
||||||
self._sync_GUI_to_state()
|
self._sync_GUI_to_state()
|
||||||
@@ -524,38 +568,26 @@ class DocumentSectionWidget(QWidget):
|
|||||||
def add_new_rows(
|
def add_new_rows(
|
||||||
self,
|
self,
|
||||||
filepaths: Iterable[Path],
|
filepaths: Iterable[Path],
|
||||||
):
|
) -> None:
|
||||||
|
with QSignalBlocker(self):
|
||||||
for path in filepaths:
|
for path in filepaths:
|
||||||
# create custom row widget
|
# create custom row widget
|
||||||
new_state = Page_InitRec_DocumentSection_Doc_State(
|
new_state = Page_InitRec_DocumentSection_Doc_State(
|
||||||
session=self.STATE.session,
|
session=self.STATE.session,
|
||||||
id=None,
|
id=None,
|
||||||
pers_id=self.STATE.pers_id,
|
|
||||||
speicher_pfad=path,
|
speicher_pfad=path,
|
||||||
locked=self.STATE.locked,
|
locked=self.STATE.locked,
|
||||||
)
|
)
|
||||||
self._add_row(new_state, state_exists=False)
|
self.add_row(new_state, state_exists=False)
|
||||||
|
|
||||||
# TODO change trigger
|
|
||||||
self.STATE.doc_count = self.file_list_widget.count()
|
self.STATE.doc_count = self.file_list_widget.count()
|
||||||
self.count_changed.emit(self.STATE.doc_count)
|
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||||
|
|
||||||
# TODO change to protocol/interface with state
|
|
||||||
# def get_all_documents_data(self):
|
|
||||||
# """Methode zum Auslesen aller angehängten Dateien inkl. Metadaten für das Speichern."""
|
|
||||||
# documents = []
|
|
||||||
# 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_state())
|
|
||||||
# return documents
|
|
||||||
|
|
||||||
|
|
||||||
class _MainWindow(QMainWindow):
|
class _MainWindow(QMainWindow):
|
||||||
"""main window for internal test only, not the this module's component"""
|
"""main window for internal test only, not the this module's component"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("Prototyp: Dokumente & Anhänge")
|
self.setWindowTitle("Prototyp: Dokumente & Anhänge")
|
||||||
self.resize(650, 500)
|
self.resize(650, 500)
|
||||||
@@ -570,11 +602,9 @@ class _MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# custom widget
|
# custom widget
|
||||||
self.SESSION = Session(user_id=42, user_name="TEST-USER")
|
self.SESSION = Session(user_id=42, user_name="TEST-USER")
|
||||||
state = Page_InitRec_DocumentSection_State(
|
state = Page_InitRec_DocumentSection_State(session=self.SESSION, locked=False)
|
||||||
session=self.SESSION, pers_id=None, locked=False
|
|
||||||
)
|
|
||||||
self.doc_section = DocumentSectionWidget(state)
|
self.doc_section = DocumentSectionWidget(state)
|
||||||
self.doc_section.count_changed.connect(self.update_header)
|
self.doc_section.doc_count_changed.connect(self.update_header)
|
||||||
layout.addWidget(self.doc_section)
|
layout.addWidget(self.doc_section)
|
||||||
|
|
||||||
# --- TEST BUTTONS ---
|
# --- TEST BUTTONS ---
|
||||||
@@ -623,7 +653,10 @@ class _MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
def update_header(self, count):
|
def update_header(
|
||||||
|
self,
|
||||||
|
count: int,
|
||||||
|
) -> None:
|
||||||
"""update counter in corresponding label, must be mapped to section heading in form"""
|
"""update counter in corresponding label, must be mapped to section heading in form"""
|
||||||
self.header_label.setText(f"<b>📎 Dokumente & Anhänge ({count})</b>")
|
self.header_label.setText(f"<b>📎 Dokumente & Anhänge ({count})</b>")
|
||||||
|
|
||||||
@@ -654,23 +687,21 @@ class _MainWindow(QMainWindow):
|
|||||||
validated = Grunderfassung_Dokument(**dump_data)
|
validated = Grunderfassung_Dokument(**dump_data)
|
||||||
docs.append(validated)
|
docs.append(validated)
|
||||||
|
|
||||||
validated_section = Grunderfassung_Dokumente(docs=docs)
|
validated_section = [m.model_dump() for m in docs]
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
("[Module -- DocumentSelection] Pydantic validation. Model:\n%s"),
|
("[Module -- DocumentSelection] Pydantic validation. Model:\n%s"),
|
||||||
pformat(validated_section.model_dump()),
|
pformat(validated_section),
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_given_state(self) -> None:
|
def load_given_state(self) -> None:
|
||||||
new_state = Page_InitRec_DocumentSection_State(
|
new_state = Page_InitRec_DocumentSection_State(
|
||||||
session=self.SESSION,
|
session=self.SESSION,
|
||||||
pers_id=None,
|
|
||||||
docs=[
|
docs=[
|
||||||
Page_InitRec_DocumentSection_Doc_State(
|
Page_InitRec_DocumentSection_Doc_State(
|
||||||
child_modules=[],
|
child_modules=[],
|
||||||
session=Session(user_id=42, user_name="TEST-USER"),
|
session=Session(user_id=42, user_name="TEST-USER"),
|
||||||
id=None,
|
id=None,
|
||||||
pers_id=None,
|
|
||||||
datum_hinzugefuegt=datetime.datetime(
|
datum_hinzugefuegt=datetime.datetime(
|
||||||
2026, 7, 31, 6, 38, 7, 120949, tzinfo=datetime.timezone.utc
|
2026, 7, 31, 6, 38, 7, 120949, tzinfo=datetime.timezone.utc
|
||||||
),
|
),
|
||||||
@@ -686,7 +717,6 @@ class _MainWindow(QMainWindow):
|
|||||||
child_modules=[],
|
child_modules=[],
|
||||||
session=Session(user_id=42, user_name="TEST-USER"),
|
session=Session(user_id=42, user_name="TEST-USER"),
|
||||||
id=None,
|
id=None,
|
||||||
pers_id=None,
|
|
||||||
datum_hinzugefuegt=datetime.datetime(
|
datum_hinzugefuegt=datetime.datetime(
|
||||||
2026, 7, 31, 6, 39, 46, 903040, tzinfo=datetime.timezone.utc
|
2026, 7, 31, 6, 39, 46, 903040, tzinfo=datetime.timezone.utc
|
||||||
),
|
),
|
||||||
@@ -704,7 +734,6 @@ class _MainWindow(QMainWindow):
|
|||||||
child_modules=[],
|
child_modules=[],
|
||||||
session=Session(user_id=42, user_name="TEST-USER"),
|
session=Session(user_id=42, user_name="TEST-USER"),
|
||||||
id=None,
|
id=None,
|
||||||
pers_id=None,
|
|
||||||
datum_hinzugefuegt=datetime.datetime(
|
datum_hinzugefuegt=datetime.datetime(
|
||||||
2026, 7, 31, 6, 39, 46, 909045, tzinfo=datetime.timezone.utc
|
2026, 7, 31, 6, 39, 46, 909045, tzinfo=datetime.timezone.utc
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -47,11 +47,14 @@ logger_page_initrec = logger_gui.getChild("page_initrec")
|
|||||||
logger_page_initrec.setLevel(logging.DEBUG)
|
logger_page_initrec.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
logger_initrecform = logger_gui.getChild("initrecform")
|
logger_initrecform = logger_gui.getChild("initrecform")
|
||||||
logger_initrecform.setLevel(logging.DEBUG)
|
logger_initrecform.setLevel(logging.WARNING)
|
||||||
|
|
||||||
logger_prototyping = logger_gui.getChild("prototyping")
|
logger_prototyping = logger_gui.getChild("prototyping")
|
||||||
logger_prototyping.setLevel(logging.DEBUG)
|
logger_prototyping.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# ** Backend
|
# ** Backend
|
||||||
logger_back = logger_base.getChild("backend")
|
logger_backend = logger_base.getChild("backend")
|
||||||
logger_back.setLevel(logging.DEBUG)
|
logger_backend.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
logger_initrec_docs = logger_base.getChild("initrec_docs")
|
||||||
|
logger_initrec_docs.setLevel(logging.DEBUG)
|
||||||
|
|||||||
Reference in New Issue
Block a user