3 Commits

4 changed files with 458 additions and 387 deletions

137
modules/listenauswerter.py Normal file
View File

@@ -0,0 +1,137 @@
#sn_plan41/modules/listenauswerter.py
from typing import Any, Dict, List, Mapping, Optional, Tuple
from collections.abc import Mapping as _Mapping
# Prüfer-Typen (werden als Instanzen erwartet)
from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore
class Listenauswerter:
"""
Validiert Zeilen aus einem DataDict, das vom DataGrabber stammt.
Erwartet wird die Struktur::
{"rows": [ {attr}, ... ]}
Die Linkprüfung entfällt vollständig, da der DataGrabber nur gültige
Links liefert. Diese Methode prüft ausschließlich die Konsistenz der
Zeilen mit dem erwarteten Datenschema und führt optional eine
Stilprüfung durch.
"""
def __init__(self, pruefmanager, stil_pruefer):
""" Parameters
----------
pruefmanager: Instanz des Pruefmanagers, der pruef_ergebnis verarbeitet.
stil_pruefer: Instanz des Stilpruefers, der Stildateien prüft.
"""
self.pruefmanager = pruefmanager
self.stil_pruefer = stil_pruefer
def validate_rows(
self,
data_dict: Dict[str, List[Mapping[str, Any]]]
) -> Tuple[Dict[str, List[Mapping[str, Any]]], List[Any]]:
"""
Validiert die Zeilen aus ``data_dict`` anhand des erwarteten Schemas.
Erwartete Felder pro Zeile
--------------------------
Pflichtfelder:
- ``ident``: eindeutige Kennung
- ``Link``: bereits geprüfter Link (vom DataGrabber garantiert gültig)
- ``Provider``: Datenquelle (wird in Großbuchstaben normalisiert)
Optionale Felder:
- ``Inhalt``: thematische Beschreibung
- ``Stildatei``: Pfad zur Stildatei (falls vorhanden)
Verhalten
---------
- Zeilen, denen Pflichtfelder fehlen oder deren Werte leer sind,
werden verworfen.
- ``Provider`` wird in Großbuchstaben normalisiert.
- Wenn ``Stildatei`` vorhanden ist, wird sie durch
``self.stil_pruefer.pruefe(...)`` geprüft.
- Bei OK bleibt der Wert erhalten.
- Bei nicht OK wird ``Stildatei`` auf ``None`` gesetzt und das
verarbeitete Prüfergebnis gesammelt.
- Alle Prüfergebnisse werden durch ``self.pruefmanager.verarbeite(...)``
geleitet und in der Rückgabe gesammelt.
Rückgabe
--------
Tuple[Dict[str, List[Mapping[str, Any]]]], List[Any]]
- ``valid_data_dict``: enthält nur Zeilen, die dem Schema entsprechen
- ``processed_results``: Liste der verarbeiteten Prüfergebnisse
Hinweise
--------
- Diese Methode führt **keine Linkprüfung** durch.
- Die Verantwortung für die Linkvalidität liegt vollständig beim DataGrabber.
- Die Methode verändert die Zeilen nur minimal (ProviderNormalisierung,
Stildatei ggf. auf ``None``).
"""
processed_results: List[Any] = []
valid_rows: List[Mapping[str, Any]] = []
# 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:
# Fehler dokumentieren
pe = pruef_ergebnis(
ok=False,
meldung="Pflichtfelder fehlen oder sind leer",
aktion="pflichtfelder_fehlen",
kontext=raw,
)
processed_results.append(self.pruefmanager.verarbeite(pe))
continue
# Provider normalisieren
provider_norm = str(provider).upper()
# Stildatei 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 = {
"ident": ident,
"Inhalt": inhalt,
"Link": link,
"Stildatei": stildatei_value,
"Provider": provider_norm,
}
valid_rows.append(validated_row)
result_dict = {"rows": valid_rows}
return result_dict, processed_results

View File

@@ -1,7 +1,45 @@
#sn_plan41/ui/dockwidget.py
from sn_basis.ui.tabs.settings_tab import SettingsTab from sn_basis.ui.tabs.settings_tab import SettingsTab
from sn_plan41.ui.tab_a_ui import TabA from sn_plan41.ui.tab_a_ui import TabA
#from sn_plan41.ui.tabs.tab_b import TabB #from sn_plan41.ui.tabs.tab_b import TabB
from sn_basis.ui.base_dockwidget import BaseDockWidget from sn_basis.ui.base_dockwidget import BaseDockWidget
from sn_basis.functions.qt_wrapper import QTabWidget
from sn_basis.functions.message_wrapper import error
class DockWidget(BaseDockWidget): class DockWidget(BaseDockWidget):
tabs = [TabA, SettingsTab] tabs = [TabA, SettingsTab]
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
super().__init__(parent, subtitle)
# Services als Attribute speichern
self.pruefmanager = pruefmanager
self.data_grabber = data_grabber
# Tabs NACH Services initialisieren (override der Basis-Logik)
self._init_tabs_with_services()
def _init_tabs_with_services(self):
"""Tabs mit pruefmanager/data_grabber initialisieren"""
try:
# Bestehendes TabWidget löschen
self.setWidget(None)
tab_widget = QTabWidget()
for tab_class in self.tabs:
tab_instance = tab_class(self) # parent=self.dockwidget
tab_title = getattr(tab_class, "tab_title", tab_class.__name__)
tab_widget.addTab(tab_instance, tab_title)
# Services durchreichen
if hasattr(tab_instance, 'set_services'):
tab_instance.set_services(
pruefmanager=self.pruefmanager,
data_grabber=self.data_grabber
)
self.setWidget(tab_widget)
except Exception as e:
error("Services-Tabs konnten nicht initialisiert werden", str(e))

View File

@@ -4,87 +4,112 @@ sn_plan41/ui/tab_a_logic.py Fachlogik für Tab A (Daten)
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
from collections.abc import Mapping as _Mapping from collections.abc import Mapping as _Mapping
import os
from sn_basis.functions.variable_wrapper import ( # type: ignore from sn_basis.functions.qgiscore_wrapper import (
QgsVectorFileWriter,
QgsVectorLayer,
QgsProject,
)
from sn_basis.functions.variable_wrapper import (
get_variable, get_variable,
set_variable, set_variable,
) )
from sn_basis.functions.sys_wrapper import ( # type: ignore from sn_basis.functions.sys_wrapper import file_exists
file_exists, from sn_basis.functions.ly_existence_wrapper import layer_exists
write_text, from sn_basis.functions.ly_metadata_wrapper import get_layer_type
)
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) # Prüfer-Typen
from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.linkpruefer import Linkpruefer # type: ignore from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore from sn_basis.modules.stilpruefer import Stilpruefer
# Typalias für Klarheit
Row = Dict[str, Any] Row = Dict[str, Any]
DataDict = Dict[str, List[Row]] DataDict = Dict[str, List[Row]]
class TabALogic: class TabALogic:
""" """
Kapselt die komplette Logik von Tab A: Kapselt die Fachlogik von Tab A. Verfahrens-DB wird **nicht** bei Pfad-Auswahl,
- Verfahrens-Datenbank sondern erst beim ersten Layer-Schreiben angelegt (alte Logik).
- 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: 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.pruefmanager = pruefmanager
self.link_pruefer = link_pruefer self.link_pruefer = link_pruefer
self.stil_pruefer = stil_pruefer self.stil_pruefer = stil_pruefer
# ------------------------------- # -------------------------------
# Verfahrens-Datenbank # Verfahrens-Datenbank (Pfad-Management)
# ------------------------------- # -------------------------------
def load_verfahrens_db(self) -> Optional[str]: def load_verfahrens_db(self) -> Optional[str]:
""" """Lädt den gespeicherten Verfahrens-DB-Pfad (Datei muss nicht existieren)."""
Lädt die gespeicherte Verfahrens-Datenbank.
"""
path = get_variable("verfahrens_db", scope="project") path = get_variable("verfahrens_db", scope="project")
if path and file_exists(path): return path or None
return path
return None
def set_verfahrens_db(self, path: Optional[str]) -> None: def set_verfahrens_db(self, path: Optional[str]) -> None:
""" """Speichert den Verfahrens-DB-Pfad (Datei wird später angelegt)."""
Speichert oder löscht die Verfahrens-Datenbank.
"""
if path: if path:
set_variable("verfahrens_db", path, scope="project") set_variable("verfahrens_db", path, scope="project")
else: else:
set_variable("verfahrens_db", "", scope="project") set_variable("verfahrens_db", "", scope="project")
def create_new_verfahrens_db(self, path: str) -> bool: # -------------------------------
# Layer → Verfahrens-DB schreiben (alte Logik!)
# -------------------------------
def write_layer_to_verfahrens_db(
self,
source_layer: QgsVectorLayer,
zielpfad: str,
layer_name: str,
) -> bool:
""" """
Legt eine neue leere GPKG-Datei an. Schreibt einen Layer in die Verfahrens-DB.
Legt GPKG **bei Bedarf neu an** (wie puffer_setzen im alten Code).
Args:
source_layer: Layer zum Exportieren (z.B. aus DataGrabber)
zielpfad: Vom Dateiprüfer geprüfter Ziel-GPKG-Pfad
layer_name: Name des Layers in der GPKG
Returns:
True wenn erfolgreich
""" """
if not path: if not zielpfad or not source_layer or not source_layer.isValid():
return False return False
try: # Optionen wie im alten puffer_setzen
write_text(path, "") opts = QgsVectorFileWriter.SaveVectorOptions()
except Exception: opts.driverName = "GPKG"
opts.fileEncoding = "UTF-8"
opts.layerName = layer_name
# Alte Logik: bei neuem Pfad komplett neue GPKG, sonst Layer überschreiben
if not os.path.exists(zielpfad):
opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
else:
opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
transform_context = QgsProject.instance().transformContext()
error = QgsVectorFileWriter.writeAsVectorFormatV3(
source_layer,
zielpfad,
transform_context,
opts,
)
if error != QgsVectorFileWriter.NoError:
print(f"Fehler beim Schreiben nach {zielpfad}: {error}")
return False return False
self.set_verfahrens_db(path) # Pfad jetzt auch als "Verfahrens-DB" merken
self.set_verfahrens_db(zielpfad)
return True return True
# ------------------------------- # -------------------------------
@@ -92,18 +117,12 @@ class TabALogic:
# ------------------------------- # -------------------------------
def load_linkliste(self) -> Optional[str]: def load_linkliste(self) -> Optional[str]:
"""
Lädt die gespeicherte lokale Linkliste.
"""
path = get_variable("linkliste", scope="project") path = get_variable("linkliste", scope="project")
if path and file_exists(path): if path and file_exists(path):
return path return path
return None return None
def set_linkliste(self, path: Optional[str]) -> None: def set_linkliste(self, path: Optional[str]) -> None:
"""
Speichert oder löscht die lokale Linkliste.
"""
if path: if path:
set_variable("linkliste", path, scope="project") set_variable("linkliste", path, scope="project")
else: else:
@@ -114,10 +133,6 @@ class TabALogic:
# ------------------------------- # -------------------------------
def save_verfahrensgebiet_layer(self, layer) -> None: def save_verfahrensgebiet_layer(self, layer) -> None:
"""
Speichert die ID des Verfahrensgebiet-Layers.
Ungültige Layer werden ignoriert.
"""
if layer is None: if layer is None:
set_variable("verfahrensgebiet_layer", "", scope="project") set_variable("verfahrensgebiet_layer", "", scope="project")
return return
@@ -139,115 +154,12 @@ class TabALogic:
set_variable("verfahrensgebiet_layer", layer_id, scope="project") set_variable("verfahrensgebiet_layer", layer_id, scope="project")
def load_verfahrensgebiet_layer_id(self) -> Optional[str]: def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
"""
Lädt die gespeicherte Layer-ID.
"""
value = get_variable("verfahrensgebiet_layer", scope="project") value = get_variable("verfahrensgebiet_layer", scope="project")
return value or None return value or None
def is_valid_verfahrensgebiet_layer(self, layer) -> bool: def is_valid_verfahrensgebiet_layer(self, layer) -> bool:
"""
Prüft, ob ein Layer als Verfahrensgebiet geeignet ist.
"""
if not layer_exists(layer): if not layer_exists(layer):
return False return False
layer_type = get_layer_type(layer) layer_type = get_layer_type(layer)
return layer_type == "vector" 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

View File

@@ -1,4 +1,7 @@
# 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 __future__ import annotations
from typing import Optional from typing import Optional
@@ -16,77 +19,84 @@ from sn_basis.functions.qt_wrapper import (
ArrowRight, ArrowRight,
SizePolicyPreferred, SizePolicyPreferred,
SizePolicyMaximum, SizePolicyMaximum,
ComboBox, QComboBox,
) )
from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox
from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel
from sn_basis.functions.variable_wrapper import get_variable, set_variable from sn_basis.functions.variable_wrapper import get_variable, set_variable
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_plan41.ui.tab_a_logic import TabALogic # Services (werden von DockWidget injiziert)
# PrüfWorkflow / 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.Pruefmanager import Pruefmanager
from sn_basis.modules.DataGrabber import DataGrabber from sn_basis.modules.DataGrabber import DataGrabber
from sn_basis.modules.linkpruefer import Linkpruefer from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_basis.modules.stilpruefer import Stilpruefer from sn_plan41.ui.tab_a_logic import TabALogic
# Raumfilter-Optionen
# Konstanten
RAUMFILTER_VAR = "Raumfilter" RAUMFILTER_VAR = "Raumfilter"
RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne") RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne")
RAUMFILTER_DEFAULT = "Pufferlayer" RAUMFILTER_DEFAULT = "Pufferlayer"
pm = Pruefmanager(ui_modus="qgis")
lp = Linkpruefer()
sp = Stilpruefer()
class TabA(QWidget): class TabA(QWidget):
""" """
UI-Klasse für Tab A (Daten). UI-Klasse für Tab A (Daten) des Plan41-Plugins.
Diese bereinigte Version enthält ausschließlich UI-Elemente und Zuständig für:
einfache, nicht-validierende Callback-Handler. Alle fachlichen Prüfungen - Anzeige und Auswahl von Verfahrens-DB, Linklisten und Layern
und Fehlerbehandlungen werden zur Laufzeit vom Pruefmanager und den Prüfern - Steuerung der Pipeline über "Fachdaten laden"
übernommen. - Persistierung von UI-States via Projektvariablen
Services (Pruefmanager, DataGrabber) werden zur Laufzeit vom DockWidget injiziert.
Alle fachlichen Prüfungen laufen über den zentralen Pruefmanager.
""" """
tab_title = "Daten"
def __init__(self, parent=None, pruefmanager=None, link_pruefer=None, stil_pruefer=None, build_ui=True): tab_title = "Daten" #: Tab-Titel für BaseDockWidget
def __init__(self, parent: Optional[QWidget] = None):
"""
Initialisiert die UI-Struktur.
Services werden später über :meth:`set_services` injiziert.
:param parent: Parent-Widget (typischerweise DockWidget)
"""
super().__init__(parent) super().__init__(parent)
self.parent = parent
self.tab_title = "Daten"
# Logik-Adapter (TabALogic verwaltet persistente Projektvariablen) # Services (werden von DockWidget gesetzt)
self.logic = TabALogic(pruefmanager=pruefmanager, link_pruefer=link_pruefer, stil_pruefer=stil_pruefer) self.pruefmanager: Optional[Pruefmanager] = None
self.data_grabber: Optional[DataGrabber] = None
# Prüfmanager-Instanz (UI-Modus wird zur Laufzeit vom Pruefmanager gehandhabt) self.logic: Optional[TabALogic] = None
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)
# UI-State
self.verfahrens_db: Optional[str] = None self.verfahrens_db: Optional[str] = None
self.lokale_linkliste: Optional[str] = None self.lokale_linkliste: Optional[str] = None
self._pufferlayer = None
self._attributes_list = []
# UI-Widget-Referenz für Raumfilter # UI-Referenzen
self._raumfilter_combo: Optional[ComboBox] = None self._raumfilter_combo: Optional[QComboBox] = None
if build_ui:
self._build_ui() self._build_ui()
self._restore_state() self._restore_state()
# --------------------------------------------------------- def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None:
# UI-Aufbau """
# --------------------------------------------------------- Injiziert Services vom übergeordneten DockWidget.
:param pruefmanager: Zentrale Prüfmanager-Instanz
:param data_grabber: DataGrabber für Quellenprüfung/Abruf
"""
self.pruefmanager = pruefmanager
self.data_grabber = data_grabber
def _build_ui(self) -> None: def _build_ui(self) -> None:
"""Erstellt die komplette UI-Hierarchie mit allen Gruppen."""
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
main_layout.setSpacing(4) main_layout.setSpacing(4)
main_layout.setContentsMargins(4, 4, 4, 4) main_layout.setContentsMargins(4, 4, 4, 4)
# Verfahrens-Datenbank Gruppe # === VERFAHRENS-DATENBANK ===
self.group_button = QToolButton() self.group_button = QToolButton()
self.group_button.setText("Verfahrens-Datenbank") self.group_button.setText("Verfahrens-Datenbank")
self.group_button.setCheckable(True) self.group_button.setCheckable(True)
@@ -99,13 +109,11 @@ class TabA(QWidget):
self.group_content = QWidget() self.group_content = QWidget()
self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
group_layout = QVBoxLayout() group_layout = QVBoxLayout()
group_layout.setSpacing(2) group_layout.setSpacing(2)
group_layout.setContentsMargins(10, 4, 4, 4) group_layout.setContentsMargins(10, 4, 4, 4)
group_layout.addWidget(QLabel("bestehende Datei auswählen")) group_layout.addWidget(QLabel("bestehende Datei auswählen"))
self.file_widget = QgsFileWidget() self.file_widget = QgsFileWidget()
self.file_widget.setStorageMode(QgsFileWidget.GetFile) self.file_widget.setStorageMode(QgsFileWidget.GetFile)
self.file_widget.setFilter("Geopackage (*.gpkg)") self.file_widget.setFilter("Geopackage (*.gpkg)")
@@ -113,7 +121,6 @@ class TabA(QWidget):
group_layout.addWidget(self.file_widget) group_layout.addWidget(self.file_widget)
group_layout.addWidget(QLabel("-oder-")) group_layout.addWidget(QLabel("-oder-"))
self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") self.btn_new = QPushButton("Neue Verfahrens-DB anlegen")
self.btn_new.clicked.connect(self._create_new_gpkg) self.btn_new.clicked.connect(self._create_new_gpkg)
group_layout.addWidget(self.btn_new) group_layout.addWidget(self.btn_new)
@@ -121,7 +128,7 @@ class TabA(QWidget):
self.group_content.setLayout(group_layout) self.group_content.setLayout(group_layout)
main_layout.addWidget(self.group_content) main_layout.addWidget(self.group_content)
# Optionale Linkliste # === OPTIONALE LINKLISTE ===
self.optional_button = QToolButton() self.optional_button = QToolButton()
self.optional_button.setText("Optional: Lokale Linkliste") self.optional_button.setText("Optional: Lokale Linkliste")
self.optional_button.setCheckable(True) self.optional_button.setCheckable(True)
@@ -134,13 +141,11 @@ class TabA(QWidget):
self.optional_content = QWidget() self.optional_content = QWidget()
self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
optional_layout = QVBoxLayout() optional_layout = QVBoxLayout()
optional_layout.setSpacing(2) optional_layout.setSpacing(2)
optional_layout.setContentsMargins(10, 4, 4, 20) optional_layout.setContentsMargins(10, 4, 4, 20)
optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)")) optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)"))
self.linkliste_widget = QgsFileWidget() self.linkliste_widget = QgsFileWidget()
self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile)
self.linkliste_widget.setFilter("Excelliste (*.xlsx)") self.linkliste_widget.setFilter("Excelliste (*.xlsx)")
@@ -151,221 +156,200 @@ class TabA(QWidget):
self.optional_content.setVisible(False) self.optional_content.setVisible(False)
main_layout.addWidget(self.optional_content) main_layout.addWidget(self.optional_content)
# Layer-Auswahl # === LAYER-AUSWAHL + RAUMFILTER ===
layer_label = QLabel("Verfahrensgebiet-Layer auswählen") layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
main_layout.addWidget(layer_label) main_layout.addWidget(layer_label)
self.layer_combo = QgsMapLayerComboBox() self.layer_combo = QgsMapLayerComboBox()
self.layer_combo.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
self.layer_combo.layerChanged.connect(self._on_layer_changed) self.layer_combo.layerChanged.connect(self._on_layer_changed)
main_layout.addWidget(self.layer_combo) main_layout.addWidget(self.layer_combo)
# Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl)
main_layout.addWidget(QLabel("Raumfilter")) main_layout.addWidget(QLabel("Raumfilter"))
self._raumfilter_combo = ComboBox(self) self._raumfilter_combo = QComboBox(self)
# Fülle Optionen (Wrapper stellt addItems bereit) self._raumfilter_combo.addItems(RAUMFILTER_OPTIONS)
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) 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) main_layout.addWidget(self._raumfilter_combo)
# Aktion: Fachdaten laden # === PIPELINE-STEUERUNG ===
self.btn_load = QPushButton("Fachdaten laden") self.btn_pipeline = QPushButton("Fachdaten laden")
self.btn_load.clicked.connect(self._on_load_fachdaten) self.btn_pipeline.setToolTip("Starte Pipeline: Linkliste → DataGrabber → Datenschreiber → Log")
main_layout.addWidget(self.btn_load) self.btn_pipeline.clicked.connect(self._on_run_pipeline)
main_layout.addWidget(self.btn_pipeline)
main_layout.addStretch(1) main_layout.addStretch(1)
self.setLayout(main_layout) self.setLayout(main_layout)
# ---------------------------------------------------------
# State Restore (UI-Wiederherstellung ohne Prüfungen)
# ---------------------------------------------------------
def _restore_state(self) -> None: def _restore_state(self) -> None:
db = self.logic.load_verfahrens_db() """Stellt UI-State aus Projektvariablen/Persistenz wieder her."""
if db: # Verfahrens-DB
self.verfahrens_db = db
try: try:
db = get_variable("tab_a_verfahrens_db", scope="project")
if db and self.file_widget:
self.file_widget.setFilePath(db) self.file_widget.setFilePath(db)
except Exception: self.verfahrens_db = db
pass
self._update_group_color() self._update_group_color()
link = self.logic.load_linkliste()
if link:
self.lokale_linkliste = link
try:
self.linkliste_widget.setFilePath(link)
except Exception: except Exception:
pass pass
layer_id = self.logic.load_verfahrensgebiet_layer_id() # Linkliste
try:
link = get_variable("tab_a_linkliste", scope="project")
if link and self.linkliste_widget:
self.linkliste_widget.setFilePath(link)
self.lokale_linkliste = link
except Exception:
pass
# Layer
try:
layer_id = get_variable("tab_a_layer_id", scope="project")
if layer_id: if layer_id:
layer = QgsProject.instance().mapLayer(layer_id) layer = QgsProject.instance().mapLayer(layer_id)
if layer: if layer and self.layer_combo:
self.layer_combo.setLayer(layer) self.layer_combo.setLayer(layer)
self._pufferlayer = 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: except Exception:
pass pass
# --------------------------------------------------------- # Raumfilter (schon im _build_ui behandelt)
# UI-Callbacks (ohne Prüfungen / Exceptions)
# --------------------------------------------------------- # === UI CALLBACKS ===
def _toggle_group(self, checked: bool) -> None: def _toggle_group(self, checked: bool) -> None:
"""Zeigt/verbirgt Verfahrens-DB-Gruppe."""
self.group_button.setArrowType(ArrowDown if checked else ArrowRight) self.group_button.setArrowType(ArrowDown if checked else ArrowRight)
self.group_content.setVisible(checked) self.group_content.setVisible(checked)
def _toggle_optional(self, checked: bool) -> None: def _toggle_optional(self, checked: bool) -> None:
"""Zeigt/verbirgt optionale Linkliste."""
self.optional_button.setArrowType(ArrowDown if checked else ArrowRight) self.optional_button.setArrowType(ArrowDown if checked else ArrowRight)
self.optional_content.setVisible(checked) self.optional_content.setVisible(checked)
def _on_verfahrens_db_changed(self, path: str) -> None: def _on_verfahrens_db_changed(self, path: str) -> None:
"""Persistieret Verfahrens-DB-Pfad."""
self.verfahrens_db = path self.verfahrens_db = path
self.logic.set_verfahrens_db(path) set_variable("tab_a_verfahrens_db", path, scope="project")
self._update_group_color() self._update_group_color()
def _on_linkliste_changed(self, path: str) -> None: def _on_linkliste_changed(self, path: str) -> None:
"""Persistieret lokale Linkliste."""
self.lokale_linkliste = path self.lokale_linkliste = path
self.logic.set_linkliste(path) set_variable("tab_a_linkliste", path, scope="project")
def _on_layer_changed(self, layer) -> None: def _on_layer_changed(self, layer) -> None:
self.logic.save_verfahrensgebiet_layer(layer) """Persistieret Layer-Auswahl."""
self._pufferlayer = layer self._pufferlayer = layer
if layer:
set_variable("tab_a_layer_id", layer.id(), scope="project")
def _create_new_gpkg(self) -> None:
file_path, _ = QFileDialog.getSaveFileName(
self,
"Neue Verfahrens-Datenbank anlegen",
"",
"Geopackage (*.gpkg)",
)
if not file_path:
return
if not file_path.lower().endswith(".gpkg"):
file_path += ".gpkg"
# Delegation an TabALogic; TabALogic / Pruefmanager übernehmen Prüfungen
self.logic.create_new_verfahrens_db(file_path)
self.verfahrens_db = file_path
try:
self.file_widget.setFilePath(file_path)
except Exception:
pass
self._update_group_color()
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: def _on_raumfilter_changed(self, value: str) -> None:
# Persistiere Auswahl in Projekt-Variable; Prüfungen übernimmt die Laufzeitlogik """Persistieret Raumfilter-Auswahl."""
set_variable(RAUMFILTER_VAR, value, scope="project") set_variable(RAUMFILTER_VAR, value, scope="project")
# --------------------------------------------------------- def _create_new_gpkg(self) -> None:
# UI-Helfer """Delegiert GPKG-Erstellung (Prüfungen über Services)."""
# --------------------------------------------------------- file_path, _ = QFileDialog.getSaveFileName(
def _prompt_user_to_select_file(self) -> None: self, "Neue Verfahrens-Datenbank", "", "Geopackage (*.gpkg)"
fname, _ = QFileDialog.getOpenFileName(
self,
"Verfahrens-DB auswählen",
"",
"Geopackage (*.gpkg)",
) )
if fname: if file_path:
try: if not file_path.lower().endswith(".gpkg"):
self.file_widget.setFilePath(fname) file_path += ".gpkg"
except Exception: self.verfahrens_db = file_path
try: self.file_widget.setFilePath(file_path)
self.file_widget.setFileName(fname) set_variable("tab_a_verfahrens_db", file_path, scope="project")
except Exception:
self.file_widget.setProperty("filePath", fname)
self.verfahrens_db = fname
self.logic.set_verfahrens_db(fname)
self._update_group_color() self._update_group_color()
def _update_group_color(self) -> None: def _update_group_color(self) -> None:
"""Visuelles Feedback für Verfahrens-DB-Status."""
if self.verfahrens_db: if self.verfahrens_db:
self.group_button.setStyleSheet("font-weight: bold;") self.group_button.setStyleSheet("font-weight: bold; background-color: #e0f7e0;")
else: else:
self.group_button.setStyleSheet("") self.group_button.setStyleSheet("font-weight: bold;")
# === PIPELINE ===
def _on_run_pipeline(self) -> None:
"""DEBUG: Pipeline mit maximaler Ausgabe."""
print("\n" + "="*60)
print("🚀 _on_run_pipeline GESTARTET")
print("="*60)
# 🔥 DEBUG QT STATUS
from sn_basis.functions import qt_wrapper
qt_wrapper.debug_qt_status() # ← Zeigt EXAKT was läuft!
# 1. Services prüfen
print(f"pruefmanager: {self.pruefmanager is not None}")
print(f"data_grabber: {self.data_grabber is not None}")
print(f"logic: {hasattr(self, 'logic')}")
if not self.pruefmanager:
print("❌ FEHLER: self.pruefmanager fehlt!")
return
if not self.data_grabber:
print("❌ FEHLER: self.data_grabber fehlt!")
return
print("✅ Services OK")
# 2. FileWidget
source = self.file_widget.filePath()
print(f"📁 Eingabe: '{source}' (len={len(source or '')})")
# 3. Dateipruefer
print("🔍 Dateipruefer starte...")
try:
pruefer = Dateipruefer(
source,
basis_pfad="",
leereingabe_erlaubt=False,
standarddatei=None,
temporaer_erlaubt=True, # ✅ Explizit True
verfahrens_db_modus=True # ✅ Keyword-only
)
ergebnis1 = pruefer.pruefe()
print(f" → ok={ergebnis1.ok}, aktion='{ergebnis1.aktion}', kontext={ergebnis1.kontext}")
except Exception as e:
print(f"💥 Dateipruefer FEHLER: {e}")
import traceback
traceback.print_exc()
return
# 4. Pruefmanager
print("🤖 Pruefmanager starte...")
try:
ergebnis2 = self.pruefmanager.verarbeite(ergebnis1)
print(f" → ok={ergebnis2.ok}, aktion='{ergebnis2.aktion}', kontext={ergebnis2.kontext}")
except Exception as e:
print(f"💥 Pruefmanager FEHLER: {e}")
import traceback
traceback.print_exc()
return
# 5. Entscheidung
weiter = ergebnis2.ok
print(f"➡️ Weiter? {weiter} (aktion='{ergebnis2.aktion}')")
if weiter:
final_pfad = ergebnis2.kontext if ergebnis2.kontext else source
print(f"🚀 DataGrabber mit: '{final_pfad}'")
try:
self.data_grabber.run(final_pfad)
print("✅ DataGrabber aufgerufen!")
except Exception as e:
print(f"💥 DataGrabber FEHLER: {e}")
import traceback
traceback.print_exc()
else:
print("⏹️ Pipeline gestoppt (erwartet bei leerem Pfad)")
print("="*60 + "\n")
def _on_load_fachdaten(self) -> None:
"""Kompatibilitäts-Handler → neue Pipeline."""
self._on_run_pipeline()