5 Commits

6 changed files with 494 additions and 236 deletions

View File

@@ -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

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,3 +0,0 @@
{
"extraPaths": ["."]
}

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

@@ -2,83 +2,127 @@
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
from collections.abc import Mapping as _Mapping
import os
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
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.stilpruefer import Stilpruefer
Row = Dict[str, Any]
DataDict = Dict[str, List[Row]]
class TabALogic:
"""
Kapselt die komplette Logik von Tab A:
- Verfahrens-Datenbank
- optionale Linkliste
- Verfahrensgebiet-Layer
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:
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
# -------------------------------
# Lokale Linkliste
# -------------------------------
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:
@@ -89,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
@@ -113,18 +153,11 @@ 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

View File

@@ -2,9 +2,11 @@
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 ( # type: ignore
from sn_basis.functions.qt_wrapper import (
QWidget,
QVBoxLayout,
QLabel,
@@ -12,90 +14,106 @@ from sn_basis.functions.qt_wrapper import ( # type: ignore
QToolButton,
QFileDialog,
QMessageBox,
QTabWidget,
ToolButtonTextBesideIcon,
ArrowDown,
ArrowRight,
SizePolicyPreferred,
SizePolicyMaximum,
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_basis.functions.qgisui_wrapper import ( # type: ignore
QgsFileWidget,
QgsMapLayerComboBox,
add_dock_widget,
)
from sn_basis.functions.qgiscore_wrapper import ( # type: ignore
QgsProject,
QgsMapLayerProxyModel,
)
from sn_basis.functions.message_wrapper import ( # type: ignore
info,
warning,
error,
)
from sn_basis.functions.dialog_wrapper import ask_yes_no # type: ignore
from sn_basis.functions.sys_wrapper import file_exists # type: ignore
# Services (werden von DockWidget injiziert)
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.DataGrabber import DataGrabber
from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_plan41.ui.tab_a_logic import TabALogic
from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore
# Konstanten
RAUMFILTER_VAR = "Raumfilter"
RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne")
RAUMFILTER_DEFAULT = "Pufferlayer"
class TabA(QWidget):
"""
UI-Klasse für Tab A (Daten).
Enthält ausschließlich UI-Code und delegiert Logik an TabALogic.
UI-Klasse für Tab A (Daten) des Plan41-Plugins.
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" #: Tab-Titel für BaseDockWidget
def __init__(self, parent: Optional[QWidget] = None):
"""
Initialisiert die UI-Struktur.
def __init__(self, parent=None, build_ui: bool=True):
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"
self.logic = TabALogic()
# 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-Referenzen
self._raumfilter_combo: Optional[QComboBox] = None
if build_ui:
self._build_ui()
self._restore_state()
# ---------------------------------------------------------
# UI-Aufbau
# ---------------------------------------------------------
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
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
# -------------------------------
# === VERFAHRENS-DATENBANK ===
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)
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)")
@@ -103,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)
@@ -111,30 +128,24 @@ 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)
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)
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)")
@@ -145,164 +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)
main_layout.addWidget(QLabel("Raumfilter"))
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)
# === 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
# ---------------------------------------------------------
def _restore_state(self) -> None:
db = self.logic.load_verfahrens_db()
if db:
self.verfahrens_db = db
"""Stellt UI-State aus Projektvariablen/Persistenz wieder her."""
# Verfahrens-DB
try:
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
link = self.logic.load_linkliste()
if link:
self.lokale_linkliste = link
# 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_id = self.logic.load_verfahrensgebiet_layer_id()
# Layer
try:
layer_id = get_variable("tab_a_layer_id", scope="project")
if layer_id:
layer = QgsProject.instance().mapLayer(layer_id)
if layer:
if layer and self.layer_combo:
self.layer_combo.setLayer(layer)
self._pufferlayer = layer
except Exception:
pass
# ---------------------------------------------------------
# UI-Callbacks
# ---------------------------------------------------------
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
)
# 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):
"""
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:
"""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:
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
"""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:
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
"""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 _on_raumfilter_changed(self, value: str) -> None:
"""Persistieret Raumfilter-Auswahl."""
set_variable(RAUMFILTER_VAR, value, scope="project")
def _create_new_gpkg(self) -> None:
"""Delegiert GPKG-Erstellung (Prüfungen über Services)."""
file_path, _ = QFileDialog.getSaveFileName(
self,
"Neue Verfahrens-Datenbank anlegen",
"",
"Geopackage (*.gpkg)",
self, "Neue Verfahrens-Datenbank", "", "Geopackage (*.gpkg)"
)
if not file_path:
return
if file_path:
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
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()
info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}")
# ---------------------------------------------------------
# UI-Helfer
# ---------------------------------------------------------
def _update_group_color(self) -> None:
"""Visuelles Feedback für Verfahrens-DB-Status."""
if self.verfahrens_db:
self.group_button.setStyleSheet("font-weight: bold; background-color: #e0f7e0;")
else:
self.group_button.setStyleSheet("font-weight: bold;")
def _update_group_color(self):
"""
Aktualisiert die Darstellung der Gruppenüberschrift.
"""
if not hasattr(self, "group_button"):
# === 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
if self.verfahrens_db:
self.group_button.setStyleSheet("font-weight: bold;")
else:
self.group_button.setStyleSheet("")
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()