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_plan41.ui.tab_a_ui import TabA
#from sn_plan41.ui.tabs.tab_b import TabB
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):
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 typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
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,
set_variable,
)
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.sys_wrapper import file_exists
from sn_basis.functions.ly_existence_wrapper import layer_exists
from sn_basis.functions.ly_metadata_wrapper import get_layer_type
# 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
# Prüfer-Typen
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.stilpruefer import Stilpruefer
# Typalias für Klarheit
Row = Dict[str, Any]
DataDict = Dict[str, List[Row]]
class TabALogic:
"""
Kapselt die komplette Logik von Tab A:
- 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.
Kapselt die Fachlogik von Tab A. Verfahrens-DB wird **nicht** bei Pfad-Auswahl,
sondern erst beim ersten Layer-Schreiben angelegt (alte Logik).
"""
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
# Verfahrens-Datenbank (Pfad-Management)
# -------------------------------
def load_verfahrens_db(self) -> Optional[str]:
"""
Lädt die gespeicherte Verfahrens-Datenbank.
"""
"""Lädt den gespeicherten Verfahrens-DB-Pfad (Datei muss nicht existieren)."""
path = get_variable("verfahrens_db", scope="project")
if path and file_exists(path):
return path
return None
return path or None
def set_verfahrens_db(self, path: Optional[str]) -> None:
"""
Speichert oder löscht die Verfahrens-Datenbank.
"""
"""Speichert den Verfahrens-DB-Pfad (Datei wird später angelegt)."""
if path:
set_variable("verfahrens_db", path, scope="project")
else:
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
try:
write_text(path, "")
except Exception:
# Optionen wie im alten puffer_setzen
opts = QgsVectorFileWriter.SaveVectorOptions()
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
self.set_verfahrens_db(path)
# Pfad jetzt auch als "Verfahrens-DB" merken
self.set_verfahrens_db(zielpfad)
return True
# -------------------------------
@@ -92,18 +117,12 @@ class TabALogic:
# -------------------------------
def load_linkliste(self) -> Optional[str]:
"""
Lädt die gespeicherte lokale Linkliste.
"""
path = get_variable("linkliste", scope="project")
if path and file_exists(path):
return path
return None
def set_linkliste(self, path: Optional[str]) -> None:
"""
Speichert oder löscht die lokale Linkliste.
"""
if path:
set_variable("linkliste", path, scope="project")
else:
@@ -114,10 +133,6 @@ class TabALogic:
# -------------------------------
def save_verfahrensgebiet_layer(self, layer) -> None:
"""
Speichert die ID des Verfahrensgebiet-Layers.
Ungültige Layer werden ignoriert.
"""
if layer is None:
set_variable("verfahrensgebiet_layer", "", scope="project")
return
@@ -139,115 +154,12 @@ class TabALogic:
set_variable("verfahrensgebiet_layer", layer_id, scope="project")
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
"""
Lädt die gespeicherte Layer-ID.
"""
value = get_variable("verfahrensgebiet_layer", scope="project")
return value or None
def is_valid_verfahrensgebiet_layer(self, layer) -> bool:
"""
Prüft, ob ein Layer als Verfahrensgebiet geeignet ist.
"""
if not layer_exists(layer):
return False
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

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 typing import Optional
@@ -16,77 +19,84 @@ from sn_basis.functions.qt_wrapper import (
ArrowRight,
SizePolicyPreferred,
SizePolicyMaximum,
ComboBox,
QComboBox,
)
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.modules.pruef_ergebnis import pruef_ergebnis
from sn_plan41.ui.tab_a_logic import TabALogic
# PrüfWorkflow / DataGrabber (werden zur Laufzeit vom Pruefmanager/Pruefern verwendet)
from sn_basis.modules.Dateipruefer import Dateipruefer
# Services (werden von DockWidget injiziert)
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
from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_plan41.ui.tab_a_logic import TabALogic
# Konstanten
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).
UI-Klasse für Tab A (Daten) des Plan41-Plugins.
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.
Zuständig für:
- Anzeige und Auswahl von Verfahrens-DB, Linklisten und Layern
- Steuerung der Pipeline über "Fachdaten laden"
- 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)
self.parent = parent
self.tab_title = "Daten"
# 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)
# Services (werden von DockWidget gesetzt)
self.pruefmanager: Optional[Pruefmanager] = None
self.data_grabber: Optional[DataGrabber] = None
self.logic: Optional[TabALogic] = None
# UI-State
self.verfahrens_db: Optional[str] = None
self.lokale_linkliste: Optional[str] = None
self._pufferlayer = None
self._attributes_list = []
# UI-Widget-Referenz für Raumfilter
self._raumfilter_combo: Optional[ComboBox] = None
# UI-Referenzen
self._raumfilter_combo: Optional[QComboBox] = None
if build_ui:
self._build_ui()
self._restore_state()
self._build_ui()
self._restore_state()
def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None:
"""
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
# ---------------------------------------------------------
# UI-Aufbau
# ---------------------------------------------------------
def _build_ui(self) -> None:
"""Erstellt die komplette UI-Hierarchie mit allen Gruppen."""
main_layout = QVBoxLayout()
main_layout.setSpacing(4)
main_layout.setContentsMargins(4, 4, 4, 4)
# Verfahrens-Datenbank Gruppe
# === VERFAHRENS-DATENBANK ===
self.group_button = QToolButton()
self.group_button.setText("Verfahrens-Datenbank")
self.group_button.setCheckable(True)
@@ -99,13 +109,11 @@ 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)
group_layout.addWidget(QLabel("bestehende Datei auswählen"))
self.file_widget = QgsFileWidget()
self.file_widget.setStorageMode(QgsFileWidget.GetFile)
self.file_widget.setFilter("Geopackage (*.gpkg)")
@@ -113,7 +121,6 @@ class TabA(QWidget):
group_layout.addWidget(self.file_widget)
group_layout.addWidget(QLabel("-oder-"))
self.btn_new = QPushButton("Neue Verfahrens-DB anlegen")
self.btn_new.clicked.connect(self._create_new_gpkg)
group_layout.addWidget(self.btn_new)
@@ -121,7 +128,7 @@ class TabA(QWidget):
self.group_content.setLayout(group_layout)
main_layout.addWidget(self.group_content)
# Optionale Linkliste
# === OPTIONALE LINKLISTE ===
self.optional_button = QToolButton()
self.optional_button.setText("Optional: Lokale Linkliste")
self.optional_button.setCheckable(True)
@@ -134,13 +141,11 @@ class TabA(QWidget):
self.optional_content = QWidget()
self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
optional_layout = QVBoxLayout()
optional_layout.setSpacing(2)
optional_layout.setContentsMargins(10, 4, 4, 20)
optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)"))
self.linkliste_widget = QgsFileWidget()
self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile)
self.linkliste_widget.setFilter("Excelliste (*.xlsx)")
@@ -151,221 +156,200 @@ class TabA(QWidget):
self.optional_content.setVisible(False)
main_layout.addWidget(self.optional_content)
# Layer-Auswahl
# === LAYER-AUSWAHL + RAUMFILTER ===
layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
main_layout.addWidget(layer_label)
self.layer_combo = QgsMapLayerComboBox()
self.layer_combo.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
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
self._raumfilter_combo = QComboBox(self)
self._raumfilter_combo.addItems(RAUMFILTER_OPTIONS)
self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed)
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)
# === PIPELINE-STEUERUNG ===
self.btn_pipeline = QPushButton("Fachdaten laden")
self.btn_pipeline.setToolTip("Starte Pipeline: Linkliste → DataGrabber → Datenschreiber → Log")
self.btn_pipeline.clicked.connect(self._on_run_pipeline)
main_layout.addWidget(self.btn_pipeline)
main_layout.addStretch(1)
self.setLayout(main_layout)
# ---------------------------------------------------------
# State Restore (UI-Wiederherstellung ohne Prüfungen)
# ---------------------------------------------------------
def _restore_state(self) -> None:
db = self.logic.load_verfahrens_db()
if db:
self.verfahrens_db = 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
try:
self.linkliste_widget.setFilePath(link)
except Exception:
pass
layer_id = self.logic.load_verfahrensgebiet_layer_id()
if layer_id:
layer = QgsProject.instance().mapLayer(layer_id)
if layer:
self.layer_combo.setLayer(layer)
# Raumfilter aus Variable wiederherstellen (falls Combo existiert)
"""Stellt UI-State aus Projektvariablen/Persistenz wieder her."""
# Verfahrens-DB
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)
db = get_variable("tab_a_verfahrens_db", scope="project")
if db and self.file_widget:
self.file_widget.setFilePath(db)
self.verfahrens_db = db
self._update_group_color()
except Exception:
pass
# ---------------------------------------------------------
# UI-Callbacks (ohne Prüfungen / Exceptions)
# ---------------------------------------------------------
# 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:
layer = QgsProject.instance().mapLayer(layer_id)
if layer and self.layer_combo:
self.layer_combo.setLayer(layer)
self._pufferlayer = layer
except Exception:
pass
# Raumfilter (schon im _build_ui behandelt)
# === UI CALLBACKS ===
def _toggle_group(self, checked: bool) -> None:
"""Zeigt/verbirgt Verfahrens-DB-Gruppe."""
self.group_button.setArrowType(ArrowDown if checked else ArrowRight)
self.group_content.setVisible(checked)
def _toggle_optional(self, checked: bool) -> None:
"""Zeigt/verbirgt optionale Linkliste."""
self.optional_button.setArrowType(ArrowDown if checked else ArrowRight)
self.optional_content.setVisible(checked)
def _on_verfahrens_db_changed(self, path: str) -> None:
"""Persistieret Verfahrens-DB-Pfad."""
self.verfahrens_db = path
self.logic.set_verfahrens_db(path)
set_variable("tab_a_verfahrens_db", path, scope="project")
self._update_group_color()
def _on_linkliste_changed(self, path: str) -> None:
"""Persistieret lokale Linkliste."""
self.lokale_linkliste = path
self.logic.set_linkliste(path)
set_variable("tab_a_linkliste", path, scope="project")
def _on_layer_changed(self, layer) -> None:
self.logic.save_verfahrensgebiet_layer(layer)
"""Persistieret Layer-Auswahl."""
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:
# Persistiere Auswahl in Projekt-Variable; Prüfungen übernimmt die Laufzeitlogik
"""Persistieret Raumfilter-Auswahl."""
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)",
def _create_new_gpkg(self) -> None:
"""Delegiert GPKG-Erstellung (Prüfungen über Services)."""
file_path, _ = QFileDialog.getSaveFileName(
self, "Neue Verfahrens-Datenbank", "", "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)
if file_path:
if not file_path.lower().endswith(".gpkg"):
file_path += ".gpkg"
self.verfahrens_db = file_path
self.file_widget.setFilePath(file_path)
set_variable("tab_a_verfahrens_db", file_path, scope="project")
self._update_group_color()
def _update_group_color(self) -> None:
"""Visuelles Feedback für Verfahrens-DB-Status."""
if self.verfahrens_db:
self.group_button.setStyleSheet("font-weight: bold;")
self.group_button.setStyleSheet("font-weight: bold; background-color: #e0f7e0;")
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()