From 93b17e154c1bd9e1aa19e66431fcda5bf68f80ad Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 13 Feb 2026 21:38:25 +0100 Subject: [PATCH] =?UTF-8?q?angefangen,=20datagrabber=20anzulegen=20(nicht?= =?UTF-8?q?=20lauff=C3=A4hig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 8 +- ui/tab_a_logic.py | 135 ++++++++++++++++++- ui/tab_a_ui.py | 321 +++++++++++++++++++++++++++------------------- 3 files changed, 327 insertions(+), 137 deletions(-) diff --git a/main.py b/main.py index 66f2b59..87ab81e 100644 --- a/main.py +++ b/main.py @@ -3,11 +3,15 @@ from qgis.utils import plugins from sn_basis.ui.dockmanager import DockManager from sn_plan41.ui.dockwidget import DockWidget +from sn_basis.modules.DataGrabber import DataGrabber +from sn_basis.modules.Pruefmanager import Pruefmanager class Plan41: def __init__(self, iface): self.iface = iface + self.pruefmanager=Pruefmanager(ui_modus="qgis") + self.data_grabber=DataGrabber(pruefmanager=self.pruefmanager) self.action = None self.dockwidget = None @@ -45,7 +49,9 @@ class Plan41: self.dockwidget = DockWidget( self.iface.mainWindow(), subtitle=self.plugin_name, - ) + pruefmanager=self.pruefmanager, + data_grabber=self.data_grabber) + self.dockwidget.setObjectName(self.dock_name) # Action-Referenz im Dock speichern diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py index e930cb5..61d18aa 100644 --- a/ui/tab_a_logic.py +++ b/ui/tab_a_logic.py @@ -2,18 +2,30 @@ sn_plan41/ui/tab_a_logic.py – Fachlogik für Tab A (Daten) """ -from typing import Optional +from __future__ import annotations -from sn_basis.functions.variable_wrapper import (#type: ignore +from typing import Any, Dict, List, Optional, Tuple +from collections.abc import Mapping as _Mapping + +from sn_basis.functions.variable_wrapper import ( # type: ignore get_variable, set_variable, ) -from sn_basis.functions.sys_wrapper import (#type:ignore +from sn_basis.functions.sys_wrapper import ( # type: ignore file_exists, write_text, ) -from sn_basis.functions.ly_existence_wrapper import layer_exists#type:ignore -from sn_basis.functions.ly_metadata_wrapper import get_layer_type#type:ignore +from sn_basis.functions.ly_existence_wrapper import layer_exists # type: ignore +from sn_basis.functions.ly_metadata_wrapper import get_layer_type # type: ignore + +# Prüfer-Typen (werden als Instanzen erwartet) +from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore +from sn_basis.modules.linkpruefer import Linkpruefer # type: ignore +from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore + +# Typalias für Klarheit +Row = Dict[str, Any] +DataDict = Dict[str, List[Row]] class TabALogic: @@ -22,8 +34,22 @@ class TabALogic: - Verfahrens-Datenbank - optionale Linkliste - Verfahrensgebiet-Layer + + Diese Klasse erwartet beim Erzeugen Instanzen von Pruefmanager, Linkpruefer + und Stilpruefer. Die validate_and_filter_rows-Methode verwendet diese + Instanzen über self. """ + def __init__(self, pruefmanager: Pruefmanager, link_pruefer: Linkpruefer, stil_pruefer: Stilpruefer) -> None: + """ + :param pruefmanager: Instanz des Pruefmanagers (für verarbeite/report) + :param link_pruefer: Instanz des Linkpruefers (mit methode pruefe) + :param stil_pruefer: Instanz des Stilpruefers (mit methode pruefe) + """ + self.pruefmanager = pruefmanager + self.link_pruefer = link_pruefer + self.stil_pruefer = stil_pruefer + # ------------------------------- # Verfahrens-Datenbank # ------------------------------- @@ -61,7 +87,6 @@ class TabALogic: self.set_verfahrens_db(path) return True - # ------------------------------- # Lokale Linkliste # ------------------------------- @@ -113,7 +138,6 @@ class TabALogic: set_variable("verfahrensgebiet_layer", layer_id, scope="project") - def load_verfahrensgebiet_layer_id(self) -> Optional[str]: """ Lädt die gespeicherte Layer-ID. @@ -130,3 +154,100 @@ class TabALogic: layer_type = get_layer_type(layer) return layer_type == "vector" + + # ------------------------------- + # Validierung und Filterung von data_dict + # ------------------------------- + + def validate_and_filter_rows(self, data_dict: DataDict) -> Tuple[DataDict, List[Any]]: + """ + Validiert und filtert die Zeilen aus `data_dict`. + + Erwartete Struktur von `data_dict`: {'rows': [ {attr}, ... ]}. + + Für jede Zeile werden die folgenden Attribute gelesen: + ident = attr['ident'] (Pflicht) + thema = attr['Inhalt'] (optional) + url = attr['Link'] (Pflicht) + stildatei = attr['Stildatei'] (optional) + provider = attr['Provider'] (Pflicht, wird uppercased) + + Verhalten + - Pflichtfelder (ident, Link, Provider) müssen vorhanden und nicht-leer sein, + sonst wird die Zeile verworfen. + - Wenn Link nicht leer ist, wird self.link_pruefer.pruefe(url) aufgerufen. + - Ist das Ergebnis ok: Zeile wird behalten. + - Ist das Ergebnis nicht ok: Zeile wird verworfen; das verarbeitete + pruef_ergebnis wird gesammelt. + - Wenn Stildatei nicht leer ist, wird self.stil_pruefer.pruefe(stildatei) aufgerufen. + - Ist das Ergebnis ok: der Wert bleibt erhalten. + - Ist das Ergebnis nicht ok: das Feld `Stildatei` wird in der zurückgegebenen + Zeile auf None gesetzt; das verarbeitete pruef_ergebnis wird gesammelt. + - Alle pruef_ergebnis-Objekte werden an self.pruefmanager.verarbeite(...) übergeben. + Die verarbeiteten Ergebnisse werden in der Rückgabe-Liste gesammelt. + + Rückgabe + - (valid_data_dict, processed_results) + valid_data_dict: {'rows': [valid_row1, valid_row2, ...]} + processed_results: Liste der vom Pruefmanager verarbeiteten pruef_ergebnis-Objekte + """ + processed_results: List[Any] = [] + valid_rows: List[Row] = [] + + # Grundstruktur prüfen + if not isinstance(data_dict, dict): + return {"rows": []}, processed_results + + rows = data_dict.get("rows", []) + if not isinstance(rows, (list, tuple)): + return {"rows": []}, processed_results + + for raw in rows: + # Sicherstellen, dass raw ein Mapping ist + if not isinstance(raw, _Mapping): + continue + + ident = raw.get("ident") + inhalt = raw.get("Inhalt") + link = raw.get("Link") + stildatei = raw.get("Stildatei") + provider = raw.get("Provider") + + # Pflichtfelder prüfen + if not ident or not link or not provider: + continue + + # Provider normalisieren + provider_norm = str(provider).upper() + + # Link prüfen + pe_link = self.link_pruefer.pruefe(link) + processed_link = self.pruefmanager.verarbeite(pe_link) + if not getattr(processed_link, "ok", False): + processed_results.append(processed_link) + continue # Zeile verwerfen + + # Stil prüfen (falls vorhanden) + if stildatei: + pe_stil = self.stil_pruefer.pruefe(stildatei) + processed_stil = self.pruefmanager.verarbeite(pe_stil) + if not getattr(processed_stil, "ok", False): + processed_results.append(processed_stil) + stildatei_value: Optional[str] = None + else: + stildatei_value = stildatei + else: + stildatei_value = None + + # Validierte Zeile zusammenbauen + validated_row: Row = { + "ident": ident, + "Inhalt": inhalt, + "Link": link, + "Stildatei": stildatei_value, + "Provider": provider_norm, + } + valid_rows.append(validated_row) + + result_dict: DataDict = {"rows": valid_rows} + return result_dict, processed_results diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py index 3674db2..bb60bcd 100644 --- a/ui/tab_a_ui.py +++ b/ui/tab_a_ui.py @@ -1,10 +1,9 @@ -""" -sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) -""" +# sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) +from __future__ import annotations from typing import Optional -from sn_basis.functions.qt_wrapper import ( +from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, @@ -12,52 +11,69 @@ from sn_basis.functions.qt_wrapper import ( QToolButton, QFileDialog, QMessageBox, - QTabWidget, - ToolButtonTextBesideIcon, - ArrowDown, + ToolButtonTextBesideIcon, + ArrowDown, ArrowRight, SizePolicyPreferred, SizePolicyMaximum, + ComboBox, +) +from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox +from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel +from sn_basis.functions.variable_wrapper import get_variable, set_variable -) -from sn_basis.functions.qgisui_wrapper import ( - QgsFileWidget, - QgsMapLayerComboBox, - add_dock_widget, -) -from sn_basis.functions.qgiscore_wrapper import ( - QgsProject, - QgsMapLayerProxyModel, -) -from sn_basis.functions.message_wrapper import ( - info, - warning, - error, -) -from sn_basis.functions.dialog_wrapper import ask_yes_no -from sn_basis.functions.sys_wrapper import file_exists +from sn_plan41.ui.tab_a_logic import TabALogic -from sn_plan41.ui.tab_a_logic import TabALogic +# Prüf‑Workflow / DataGrabber (werden zur Laufzeit vom Pruefmanager/Pruefern verwendet) +from sn_basis.modules.Dateipruefer import Dateipruefer +from sn_basis.modules.Pruefmanager import Pruefmanager +from sn_basis.modules.DataGrabber import DataGrabber +from sn_basis.modules.linkpruefer import Linkpruefer +from sn_basis.modules.stilpruefer import Stilpruefer +# Raumfilter-Optionen +RAUMFILTER_VAR = "Raumfilter" +RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne") +RAUMFILTER_DEFAULT = "Pufferlayer" +pm = Pruefmanager(ui_modus="qgis") +lp = Linkpruefer() +sp = Stilpruefer() class TabA(QWidget): """ UI-Klasse für Tab A (Daten). - Enthält ausschließlich UI-Code und delegiert Logik an TabALogic. + + Diese bereinigte Version enthält ausschließlich UI-Elemente und + einfache, nicht-validierende Callback-Handler. Alle fachlichen Prüfungen + und Fehlerbehandlungen werden zur Laufzeit vom Pruefmanager und den Prüfern + übernommen. """ tab_title = "Daten" - - def __init__(self, parent=None, build_ui: bool=True): + def __init__(self, parent=None, pruefmanager=None, link_pruefer=None, stil_pruefer=None, build_ui=True): super().__init__(parent) - self.parent=parent - self.tab_title="Daten" + self.parent = parent + self.tab_title = "Daten" - self.logic = TabALogic() + # Logik-Adapter (TabALogic verwaltet persistente Projektvariablen) + self.logic = TabALogic(pruefmanager=pruefmanager, link_pruefer=link_pruefer, stil_pruefer=stil_pruefer) + + # Prüfmanager-Instanz (UI-Modus wird zur Laufzeit vom Pruefmanager gehandhabt) + self.pruefmanager = Pruefmanager(ui_modus="qgis") + + # DataGrabber-Instanz (synchroner Aufruf; Prüfungen übernimmt Pruefmanager/Pruefer) + self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager) + + # Platzhalter, die vom Plugin oder Nutzer gesetzt werden können + self._attributes_list = [] # optionale Attributliste (z. B. Excel-Import) + self._pufferlayer = None # optionaler Layer (Verfahrensgebiet) self.verfahrens_db: Optional[str] = None self.lokale_linkliste: Optional[str] = None + # UI-Widget-Referenz für Raumfilter + self._raumfilter_combo: Optional[ComboBox] = None + if build_ui: self._build_ui() self._restore_state() @@ -65,23 +81,18 @@ class TabA(QWidget): # --------------------------------------------------------- # UI-Aufbau # --------------------------------------------------------- - def _build_ui(self) -> None: main_layout = QVBoxLayout() main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4) - # ------------------------------- - # Verfahrens-Datenbank - # ------------------------------- - + # Verfahrens-Datenbank Gruppe self.group_button = QToolButton() self.group_button.setText("Verfahrens-Datenbank") self.group_button.setCheckable(True) self.group_button.setChecked(True) self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon) self.group_button.setArrowType(ArrowDown) - self.group_button.setStyleSheet("font-weight: bold;") self.group_button.toggled.connect(self._toggle_group) main_layout.addWidget(self.group_button) @@ -89,7 +100,6 @@ class TabA(QWidget): self.group_content = QWidget() self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) - group_layout = QVBoxLayout() group_layout.setSpacing(2) group_layout.setContentsMargins(10, 4, 4, 4) @@ -111,17 +121,13 @@ class TabA(QWidget): self.group_content.setLayout(group_layout) main_layout.addWidget(self.group_content) - # ------------------------------- # Optionale Linkliste - # ------------------------------- - self.optional_button = QToolButton() self.optional_button.setText("Optional: Lokale Linkliste") self.optional_button.setCheckable(True) self.optional_button.setChecked(False) self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon) self.optional_button.setArrowType(ArrowRight) - self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") self.optional_button.toggled.connect(self._toggle_optional) main_layout.addWidget(self.optional_button) @@ -145,10 +151,7 @@ class TabA(QWidget): self.optional_content.setVisible(False) main_layout.addWidget(self.optional_content) - # ------------------------------- # Layer-Auswahl - # ------------------------------- - layer_label = QLabel("Verfahrensgebiet-Layer auswählen") layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(layer_label) @@ -159,24 +162,83 @@ class TabA(QWidget): self.layer_combo.layerChanged.connect(self._on_layer_changed) main_layout.addWidget(self.layer_combo) + # Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl) + main_layout.addWidget(QLabel("Raumfilter")) + self._raumfilter_combo = ComboBox(self) + # Fülle Optionen (Wrapper stellt addItems bereit) + try: + self._raumfilter_combo.addItems(list(RAUMFILTER_OPTIONS)) + except Exception: + # fallback: iterativ hinzufügen, falls Wrapper andere API hat + for opt in RAUMFILTER_OPTIONS: + if hasattr(self._raumfilter_combo, "addItem"): + self._raumfilter_combo.addItem(opt) + + # Initialisiere Auswahl aus Projekt-Variable oder Default + stored = get_variable(RAUMFILTER_VAR, scope="project") + if isinstance(stored, str) and stored in RAUMFILTER_OPTIONS: + try: + self._raumfilter_combo.setCurrentText(stored) + except Exception: + try: + idx = self._raumfilter_combo.findText(stored) + if idx is not None and idx >= 0: + self._raumfilter_combo.setCurrentIndex(idx) + except Exception: + pass + else: + try: + self._raumfilter_combo.setCurrentText(RAUMFILTER_DEFAULT) + except Exception: + try: + idx = self._raumfilter_combo.findText(RAUMFILTER_DEFAULT) + if idx is not None and idx >= 0: + self._raumfilter_combo.setCurrentIndex(idx) + except Exception: + pass + # persistiere Default, falls noch kein Wert gesetzt + if not stored: + set_variable(RAUMFILTER_VAR, RAUMFILTER_DEFAULT, scope="project") + + # Signal: bei Änderung Variable setzen + try: + self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed) + except Exception: + try: + self._raumfilter_combo.current_text_changed.connect(self._on_raumfilter_changed) + except Exception: + pass + + main_layout.addWidget(self._raumfilter_combo) + + # Aktion: Fachdaten laden + self.btn_load = QPushButton("Fachdaten laden") + self.btn_load.clicked.connect(self._on_load_fachdaten) + main_layout.addWidget(self.btn_load) + main_layout.addStretch(1) self.setLayout(main_layout) # --------------------------------------------------------- - # State Restore + # State Restore (UI-Wiederherstellung ohne Prüfungen) # --------------------------------------------------------- - def _restore_state(self) -> None: db = self.logic.load_verfahrens_db() if db: self.verfahrens_db = db - self.file_widget.setFilePath(db) + try: + self.file_widget.setFilePath(db) + except Exception: + pass self._update_group_color() link = self.logic.load_linkliste() if link: self.lokale_linkliste = link - self.linkliste_widget.setFilePath(link) + try: + self.linkliste_widget.setFilePath(link) + except Exception: + pass layer_id = self.logic.load_verfahrensgebiet_layer_id() if layer_id: @@ -184,78 +246,42 @@ class TabA(QWidget): if layer: self.layer_combo.setLayer(layer) + # Raumfilter aus Variable wiederherstellen (falls Combo existiert) + try: + stored = get_variable(RAUMFILTER_VAR, scope="project") + if stored and self._raumfilter_combo is not None: + try: + self._raumfilter_combo.setCurrentText(stored) + except Exception: + idx = self._raumfilter_combo.findText(stored) + if idx is not None and idx >= 0: + self._raumfilter_combo.setCurrentIndex(idx) + except Exception: + pass + # --------------------------------------------------------- - # UI-Callbacks + # UI-Callbacks (ohne Prüfungen / Exceptions) # --------------------------------------------------------- - - def _toggle_group(self, checked: bool): - """ - Klappt den Gruppenbereich ein oder aus. - """ - if not hasattr(self, "group_button"): - return - - self.group_button.setArrowType( - ArrowDown if checked else ArrowRight -) - + def _toggle_group(self, checked: bool) -> None: + self.group_button.setArrowType(ArrowDown if checked else ArrowRight) self.group_content.setVisible(checked) - - def _toggle_optional(self, checked: bool): - """ - Klappt den optionalen Bereich ein oder aus. - """ - if not hasattr(self, "optional_button"): - return - - self.group_button.setArrowType( - ArrowDown if checked else ArrowRight -) - + def _toggle_optional(self, checked: bool) -> None: + self.optional_button.setArrowType(ArrowDown if checked else ArrowRight) self.optional_content.setVisible(checked) - def _on_verfahrens_db_changed(self, path: str) -> None: - if not path: - self.verfahrens_db = None - self.logic.set_verfahrens_db(None) - self._update_group_color() - return - - if not path.lower().endswith(".gpkg"): - path += ".gpkg" - self.file_widget.setFilePath(path) - - if not file_exists(path): - warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.file_widget.setFilePath("") - return - self.verfahrens_db = path self.logic.set_verfahrens_db(path) self._update_group_color() def _on_linkliste_changed(self, path: str) -> None: - if not path: - self.lokale_linkliste = None - self.logic.set_linkliste(None) - return - - if not path.lower().endswith(".xlsx"): - path += ".xlsx" - self.linkliste_widget.setFilePath(path) - - if not file_exists(path): - warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.linkliste_widget.setFilePath("") - return - self.lokale_linkliste = path self.logic.set_linkliste(path) def _on_layer_changed(self, layer) -> None: self.logic.save_verfahrensgebiet_layer(layer) + self._pufferlayer = layer def _create_new_gpkg(self) -> None: file_path, _ = QFileDialog.getSaveFileName( @@ -264,45 +290,82 @@ class TabA(QWidget): "", "Geopackage (*.gpkg)", ) - if not file_path: return if not file_path.lower().endswith(".gpkg"): file_path += ".gpkg" - if file_exists(file_path): - overwrite = ask_yes_no( - "Datei existiert bereits", - f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", - default=False, - parent=self, - ) - if not overwrite: - return - - if not self.logic.create_new_verfahrens_db(file_path): - error("Fehler", "Die Datei konnte nicht angelegt werden.") - return - + # Delegation an TabALogic; TabALogic / Pruefmanager übernehmen Prüfungen + self.logic.create_new_verfahrens_db(file_path) self.verfahrens_db = file_path - self.file_widget.setFilePath(file_path) + try: + self.file_widget.setFilePath(file_path) + except Exception: + pass self._update_group_color() - info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}") + + def _on_load_fachdaten(self) -> None: + """ + Platzhalter-Handler für 'Fachdaten laden'. + + Keine Prüfungen oder Exception-Handling hier. Die fachliche Prüfung + und Fehlerbehandlung erfolgen zur Laufzeit durch den Pruefmanager und + die Prüfer, die vom DataGrabber verwendet werden. + """ + pfad = self.file_widget.filePath() + + # Dateipruefer wird zur Laufzeit verwendet; hier nur der Aufruf + pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True) + ergebnis = pruefer.pruefe() + ergebnis = self.pruefmanager.verarbeite(ergebnis) + + zielpfad = None + if ergebnis.kontext is not None: + try: + zielpfad = str(ergebnis.kontext) + except Exception: + zielpfad = ergebnis.kontext + + self.data_grabber.run( + attributes_list=self._attributes_list, + pufferlayer=self._pufferlayer, + zielpfad=zielpfad, + temporaer=(ergebnis.aktion == "temporaer_erzeugen"), + temporaer_erlaubt=True, + ) + + # --------------------------------------------------------- + # Raumfilter Callback + # --------------------------------------------------------- + def _on_raumfilter_changed(self, value: str) -> None: + # Persistiere Auswahl in Projekt-Variable; Prüfungen übernimmt die Laufzeitlogik + set_variable(RAUMFILTER_VAR, value, scope="project") # --------------------------------------------------------- # UI-Helfer # --------------------------------------------------------- + def _prompt_user_to_select_file(self) -> None: + fname, _ = QFileDialog.getOpenFileName( + self, + "Verfahrens-DB auswählen", + "", + "Geopackage (*.gpkg)", + ) + if fname: + try: + self.file_widget.setFilePath(fname) + except Exception: + try: + self.file_widget.setFileName(fname) + except Exception: + self.file_widget.setProperty("filePath", fname) + self.verfahrens_db = fname + self.logic.set_verfahrens_db(fname) + self._update_group_color() - def _update_group_color(self): - """ - Aktualisiert die Darstellung der Gruppenüberschrift. - """ - if not hasattr(self, "group_button"): - return - + def _update_group_color(self) -> None: if self.verfahrens_db: self.group_button.setStyleSheet("font-weight: bold;") else: self.group_button.setStyleSheet("") -