angefangen, datagrabber anzulegen (nicht lauffähig)

This commit is contained in:
2026-02-13 21:38:25 +01:00
parent e153a45ffa
commit 93b17e154c
3 changed files with 327 additions and 137 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

View File

@@ -2,18 +2,30 @@
sn_plan41/ui/tab_a_logic.py Fachlogik für Tab A (Daten)
"""
from typing import Optional
from __future__ import annotations
from sn_basis.functions.variable_wrapper import (#type: ignore
from typing import Any, Dict, List, Optional, Tuple
from collections.abc import Mapping as _Mapping
from sn_basis.functions.variable_wrapper import ( # type: ignore
get_variable,
set_variable,
)
from sn_basis.functions.sys_wrapper import (#type:ignore
from sn_basis.functions.sys_wrapper import ( # type: ignore
file_exists,
write_text,
)
from sn_basis.functions.ly_existence_wrapper import layer_exists#type:ignore
from sn_basis.functions.ly_metadata_wrapper import get_layer_type#type:ignore
from sn_basis.functions.ly_existence_wrapper import layer_exists # type: ignore
from sn_basis.functions.ly_metadata_wrapper import get_layer_type # type: ignore
# Prüfer-Typen (werden als Instanzen erwartet)
from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore
from sn_basis.modules.linkpruefer import Linkpruefer # type: ignore
from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore
# Typalias für Klarheit
Row = Dict[str, Any]
DataDict = Dict[str, List[Row]]
class TabALogic:
@@ -22,8 +34,22 @@ class TabALogic:
- Verfahrens-Datenbank
- optionale Linkliste
- Verfahrensgebiet-Layer
Diese Klasse erwartet beim Erzeugen Instanzen von Pruefmanager, Linkpruefer
und Stilpruefer. Die validate_and_filter_rows-Methode verwendet diese
Instanzen über self.
"""
def __init__(self, pruefmanager: Pruefmanager, link_pruefer: Linkpruefer, stil_pruefer: Stilpruefer) -> None:
"""
:param pruefmanager: Instanz des Pruefmanagers (für verarbeite/report)
:param link_pruefer: Instanz des Linkpruefers (mit methode pruefe)
:param stil_pruefer: Instanz des Stilpruefers (mit methode pruefe)
"""
self.pruefmanager = pruefmanager
self.link_pruefer = link_pruefer
self.stil_pruefer = stil_pruefer
# -------------------------------
# Verfahrens-Datenbank
# -------------------------------
@@ -61,7 +87,6 @@ class TabALogic:
self.set_verfahrens_db(path)
return True
# -------------------------------
# Lokale Linkliste
# -------------------------------
@@ -113,7 +138,6 @@ class TabALogic:
set_variable("verfahrensgebiet_layer", layer_id, scope="project")
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
"""
Lädt die gespeicherte Layer-ID.
@@ -130,3 +154,100 @@ class TabALogic:
layer_type = get_layer_type(layer)
return layer_type == "vector"
# -------------------------------
# Validierung und Filterung von data_dict
# -------------------------------
def validate_and_filter_rows(self, data_dict: DataDict) -> Tuple[DataDict, List[Any]]:
"""
Validiert und filtert die Zeilen aus `data_dict`.
Erwartete Struktur von `data_dict`: {'rows': [ {attr}, ... ]}.
Für jede Zeile werden die folgenden Attribute gelesen:
ident = attr['ident'] (Pflicht)
thema = attr['Inhalt'] (optional)
url = attr['Link'] (Pflicht)
stildatei = attr['Stildatei'] (optional)
provider = attr['Provider'] (Pflicht, wird uppercased)
Verhalten
- Pflichtfelder (ident, Link, Provider) müssen vorhanden und nicht-leer sein,
sonst wird die Zeile verworfen.
- Wenn Link nicht leer ist, wird self.link_pruefer.pruefe(url) aufgerufen.
- Ist das Ergebnis ok: Zeile wird behalten.
- Ist das Ergebnis nicht ok: Zeile wird verworfen; das verarbeitete
pruef_ergebnis wird gesammelt.
- Wenn Stildatei nicht leer ist, wird self.stil_pruefer.pruefe(stildatei) aufgerufen.
- Ist das Ergebnis ok: der Wert bleibt erhalten.
- Ist das Ergebnis nicht ok: das Feld `Stildatei` wird in der zurückgegebenen
Zeile auf None gesetzt; das verarbeitete pruef_ergebnis wird gesammelt.
- Alle pruef_ergebnis-Objekte werden an self.pruefmanager.verarbeite(...) übergeben.
Die verarbeiteten Ergebnisse werden in der Rückgabe-Liste gesammelt.
Rückgabe
- (valid_data_dict, processed_results)
valid_data_dict: {'rows': [valid_row1, valid_row2, ...]}
processed_results: Liste der vom Pruefmanager verarbeiteten pruef_ergebnis-Objekte
"""
processed_results: List[Any] = []
valid_rows: List[Row] = []
# Grundstruktur prüfen
if not isinstance(data_dict, dict):
return {"rows": []}, processed_results
rows = data_dict.get("rows", [])
if not isinstance(rows, (list, tuple)):
return {"rows": []}, processed_results
for raw in rows:
# Sicherstellen, dass raw ein Mapping ist
if not isinstance(raw, _Mapping):
continue
ident = raw.get("ident")
inhalt = raw.get("Inhalt")
link = raw.get("Link")
stildatei = raw.get("Stildatei")
provider = raw.get("Provider")
# Pflichtfelder prüfen
if not ident or not link or not provider:
continue
# Provider normalisieren
provider_norm = str(provider).upper()
# Link prüfen
pe_link = self.link_pruefer.pruefe(link)
processed_link = self.pruefmanager.verarbeite(pe_link)
if not getattr(processed_link, "ok", False):
processed_results.append(processed_link)
continue # Zeile verwerfen
# Stil prüfen (falls vorhanden)
if stildatei:
pe_stil = self.stil_pruefer.pruefe(stildatei)
processed_stil = self.pruefmanager.verarbeite(pe_stil)
if not getattr(processed_stil, "ok", False):
processed_results.append(processed_stil)
stildatei_value: Optional[str] = None
else:
stildatei_value = stildatei
else:
stildatei_value = None
# Validierte Zeile zusammenbauen
validated_row: Row = {
"ident": ident,
"Inhalt": inhalt,
"Link": link,
"Stildatei": stildatei_value,
"Provider": provider_norm,
}
valid_rows.append(validated_row)
result_dict: DataDict = {"rows": valid_rows}
return result_dict, processed_results

View File

@@ -1,10 +1,9 @@
"""
sn_plan41/ui/tab_a_ui.py UI für Tab A (Daten)
"""
# sn_plan41/ui/tab_a_ui.py UI für Tab A (Daten)
from __future__ import annotations
from typing import Optional
from sn_basis.functions.qt_wrapper import (
from sn_basis.functions.qt_wrapper import (
QWidget,
QVBoxLayout,
QLabel,
@@ -12,52 +11,69 @@ from sn_basis.functions.qt_wrapper import (
QToolButton,
QFileDialog,
QMessageBox,
QTabWidget,
ToolButtonTextBesideIcon,
ArrowDown,
ToolButtonTextBesideIcon,
ArrowDown,
ArrowRight,
SizePolicyPreferred,
SizePolicyMaximum,
ComboBox,
)
from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox
from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel
from sn_basis.functions.variable_wrapper import get_variable, set_variable
)
from sn_basis.functions.qgisui_wrapper import (
QgsFileWidget,
QgsMapLayerComboBox,
add_dock_widget,
)
from sn_basis.functions.qgiscore_wrapper import (
QgsProject,
QgsMapLayerProxyModel,
)
from sn_basis.functions.message_wrapper import (
info,
warning,
error,
)
from sn_basis.functions.dialog_wrapper import ask_yes_no
from sn_basis.functions.sys_wrapper import file_exists
from sn_plan41.ui.tab_a_logic import TabALogic
from sn_plan41.ui.tab_a_logic import TabALogic
# Prü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.DataGrabber import DataGrabber
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.stilpruefer import Stilpruefer
# Raumfilter-Optionen
RAUMFILTER_VAR = "Raumfilter"
RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne")
RAUMFILTER_DEFAULT = "Pufferlayer"
pm = Pruefmanager(ui_modus="qgis")
lp = Linkpruefer()
sp = Stilpruefer()
class TabA(QWidget):
"""
UI-Klasse für Tab A (Daten).
Enthält ausschließlich UI-Code und delegiert Logik an TabALogic.
Diese bereinigte Version enthält ausschließlich UI-Elemente und
einfache, nicht-validierende Callback-Handler. Alle fachlichen Prüfungen
und Fehlerbehandlungen werden zur Laufzeit vom Pruefmanager und den Prüfern
übernommen.
"""
tab_title = "Daten"
def __init__(self, parent=None, build_ui: bool=True):
def __init__(self, parent=None, pruefmanager=None, link_pruefer=None, stil_pruefer=None, build_ui=True):
super().__init__(parent)
self.parent=parent
self.tab_title="Daten"
self.parent = parent
self.tab_title = "Daten"
self.logic = TabALogic()
# Logik-Adapter (TabALogic verwaltet persistente Projektvariablen)
self.logic = TabALogic(pruefmanager=pruefmanager, link_pruefer=link_pruefer, stil_pruefer=stil_pruefer)
# Prüfmanager-Instanz (UI-Modus wird zur Laufzeit vom Pruefmanager gehandhabt)
self.pruefmanager = Pruefmanager(ui_modus="qgis")
# DataGrabber-Instanz (synchroner Aufruf; Prüfungen übernimmt Pruefmanager/Pruefer)
self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager)
# Platzhalter, die vom Plugin oder Nutzer gesetzt werden können
self._attributes_list = [] # optionale Attributliste (z. B. Excel-Import)
self._pufferlayer = None # optionaler Layer (Verfahrensgebiet)
self.verfahrens_db: Optional[str] = None
self.lokale_linkliste: Optional[str] = None
# UI-Widget-Referenz für Raumfilter
self._raumfilter_combo: Optional[ComboBox] = None
if build_ui:
self._build_ui()
self._restore_state()
@@ -65,23 +81,18 @@ class TabA(QWidget):
# ---------------------------------------------------------
# UI-Aufbau
# ---------------------------------------------------------
def _build_ui(self) -> None:
main_layout = QVBoxLayout()
main_layout.setSpacing(4)
main_layout.setContentsMargins(4, 4, 4, 4)
# -------------------------------
# Verfahrens-Datenbank
# -------------------------------
# Verfahrens-Datenbank Gruppe
self.group_button = QToolButton()
self.group_button.setText("Verfahrens-Datenbank")
self.group_button.setCheckable(True)
self.group_button.setChecked(True)
self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon)
self.group_button.setArrowType(ArrowDown)
self.group_button.setStyleSheet("font-weight: bold;")
self.group_button.toggled.connect(self._toggle_group)
main_layout.addWidget(self.group_button)
@@ -89,7 +100,6 @@ class TabA(QWidget):
self.group_content = QWidget()
self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
group_layout = QVBoxLayout()
group_layout.setSpacing(2)
group_layout.setContentsMargins(10, 4, 4, 4)
@@ -111,17 +121,13 @@ class TabA(QWidget):
self.group_content.setLayout(group_layout)
main_layout.addWidget(self.group_content)
# -------------------------------
# Optionale Linkliste
# -------------------------------
self.optional_button = QToolButton()
self.optional_button.setText("Optional: Lokale Linkliste")
self.optional_button.setCheckable(True)
self.optional_button.setChecked(False)
self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon)
self.optional_button.setArrowType(ArrowRight)
self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;")
self.optional_button.toggled.connect(self._toggle_optional)
main_layout.addWidget(self.optional_button)
@@ -145,10 +151,7 @@ class TabA(QWidget):
self.optional_content.setVisible(False)
main_layout.addWidget(self.optional_content)
# -------------------------------
# Layer-Auswahl
# -------------------------------
layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
main_layout.addWidget(layer_label)
@@ -159,24 +162,83 @@ class TabA(QWidget):
self.layer_combo.layerChanged.connect(self._on_layer_changed)
main_layout.addWidget(self.layer_combo)
# Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl)
main_layout.addWidget(QLabel("Raumfilter"))
self._raumfilter_combo = ComboBox(self)
# Fülle Optionen (Wrapper stellt addItems bereit)
try:
self._raumfilter_combo.addItems(list(RAUMFILTER_OPTIONS))
except Exception:
# fallback: iterativ hinzufügen, falls Wrapper andere API hat
for opt in RAUMFILTER_OPTIONS:
if hasattr(self._raumfilter_combo, "addItem"):
self._raumfilter_combo.addItem(opt)
# Initialisiere Auswahl aus Projekt-Variable oder Default
stored = get_variable(RAUMFILTER_VAR, scope="project")
if isinstance(stored, str) and stored in RAUMFILTER_OPTIONS:
try:
self._raumfilter_combo.setCurrentText(stored)
except Exception:
try:
idx = self._raumfilter_combo.findText(stored)
if idx is not None and idx >= 0:
self._raumfilter_combo.setCurrentIndex(idx)
except Exception:
pass
else:
try:
self._raumfilter_combo.setCurrentText(RAUMFILTER_DEFAULT)
except Exception:
try:
idx = self._raumfilter_combo.findText(RAUMFILTER_DEFAULT)
if idx is not None and idx >= 0:
self._raumfilter_combo.setCurrentIndex(idx)
except Exception:
pass
# persistiere Default, falls noch kein Wert gesetzt
if not stored:
set_variable(RAUMFILTER_VAR, RAUMFILTER_DEFAULT, scope="project")
# Signal: bei Änderung Variable setzen
try:
self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed)
except Exception:
try:
self._raumfilter_combo.current_text_changed.connect(self._on_raumfilter_changed)
except Exception:
pass
main_layout.addWidget(self._raumfilter_combo)
# Aktion: Fachdaten laden
self.btn_load = QPushButton("Fachdaten laden")
self.btn_load.clicked.connect(self._on_load_fachdaten)
main_layout.addWidget(self.btn_load)
main_layout.addStretch(1)
self.setLayout(main_layout)
# ---------------------------------------------------------
# State Restore
# State Restore (UI-Wiederherstellung ohne Prüfungen)
# ---------------------------------------------------------
def _restore_state(self) -> None:
db = self.logic.load_verfahrens_db()
if db:
self.verfahrens_db = db
self.file_widget.setFilePath(db)
try:
self.file_widget.setFilePath(db)
except Exception:
pass
self._update_group_color()
link = self.logic.load_linkliste()
if link:
self.lokale_linkliste = link
self.linkliste_widget.setFilePath(link)
try:
self.linkliste_widget.setFilePath(link)
except Exception:
pass
layer_id = self.logic.load_verfahrensgebiet_layer_id()
if layer_id:
@@ -184,78 +246,42 @@ class TabA(QWidget):
if layer:
self.layer_combo.setLayer(layer)
# Raumfilter aus Variable wiederherstellen (falls Combo existiert)
try:
stored = get_variable(RAUMFILTER_VAR, scope="project")
if stored and self._raumfilter_combo is not None:
try:
self._raumfilter_combo.setCurrentText(stored)
except Exception:
idx = self._raumfilter_combo.findText(stored)
if idx is not None and idx >= 0:
self._raumfilter_combo.setCurrentIndex(idx)
except Exception:
pass
# ---------------------------------------------------------
# UI-Callbacks
# UI-Callbacks (ohne Prüfungen / Exceptions)
# ---------------------------------------------------------
def _toggle_group(self, checked: bool):
"""
Klappt den Gruppenbereich ein oder aus.
"""
if not hasattr(self, "group_button"):
return
self.group_button.setArrowType(
ArrowDown if checked else ArrowRight
)
def _toggle_group(self, checked: bool) -> None:
self.group_button.setArrowType(ArrowDown if checked else ArrowRight)
self.group_content.setVisible(checked)
def _toggle_optional(self, checked: bool):
"""
Klappt den optionalen Bereich ein oder aus.
"""
if not hasattr(self, "optional_button"):
return
self.group_button.setArrowType(
ArrowDown if checked else ArrowRight
)
def _toggle_optional(self, checked: bool) -> None:
self.optional_button.setArrowType(ArrowDown if checked else ArrowRight)
self.optional_content.setVisible(checked)
def _on_verfahrens_db_changed(self, path: str) -> None:
if not path:
self.verfahrens_db = None
self.logic.set_verfahrens_db(None)
self._update_group_color()
return
if not path.lower().endswith(".gpkg"):
path += ".gpkg"
self.file_widget.setFilePath(path)
if not file_exists(path):
warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}")
self.file_widget.setFilePath("")
return
self.verfahrens_db = path
self.logic.set_verfahrens_db(path)
self._update_group_color()
def _on_linkliste_changed(self, path: str) -> None:
if not path:
self.lokale_linkliste = None
self.logic.set_linkliste(None)
return
if not path.lower().endswith(".xlsx"):
path += ".xlsx"
self.linkliste_widget.setFilePath(path)
if not file_exists(path):
warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}")
self.linkliste_widget.setFilePath("")
return
self.lokale_linkliste = path
self.logic.set_linkliste(path)
def _on_layer_changed(self, layer) -> None:
self.logic.save_verfahrensgebiet_layer(layer)
self._pufferlayer = layer
def _create_new_gpkg(self) -> None:
file_path, _ = QFileDialog.getSaveFileName(
@@ -264,45 +290,82 @@ class TabA(QWidget):
"",
"Geopackage (*.gpkg)",
)
if not file_path:
return
if not file_path.lower().endswith(".gpkg"):
file_path += ".gpkg"
if file_exists(file_path):
overwrite = ask_yes_no(
"Datei existiert bereits",
f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?",
default=False,
parent=self,
)
if not overwrite:
return
if not self.logic.create_new_verfahrens_db(file_path):
error("Fehler", "Die Datei konnte nicht angelegt werden.")
return
# Delegation an TabALogic; TabALogic / Pruefmanager übernehmen Prüfungen
self.logic.create_new_verfahrens_db(file_path)
self.verfahrens_db = file_path
self.file_widget.setFilePath(file_path)
try:
self.file_widget.setFilePath(file_path)
except Exception:
pass
self._update_group_color()
info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}")
def _on_load_fachdaten(self) -> None:
"""
Platzhalter-Handler für 'Fachdaten laden'.
Keine Prüfungen oder Exception-Handling hier. Die fachliche Prüfung
und Fehlerbehandlung erfolgen zur Laufzeit durch den Pruefmanager und
die Prüfer, die vom DataGrabber verwendet werden.
"""
pfad = self.file_widget.filePath()
# Dateipruefer wird zur Laufzeit verwendet; hier nur der Aufruf
pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True)
ergebnis = pruefer.pruefe()
ergebnis = self.pruefmanager.verarbeite(ergebnis)
zielpfad = None
if ergebnis.kontext is not None:
try:
zielpfad = str(ergebnis.kontext)
except Exception:
zielpfad = ergebnis.kontext
self.data_grabber.run(
attributes_list=self._attributes_list,
pufferlayer=self._pufferlayer,
zielpfad=zielpfad,
temporaer=(ergebnis.aktion == "temporaer_erzeugen"),
temporaer_erlaubt=True,
)
# ---------------------------------------------------------
# Raumfilter Callback
# ---------------------------------------------------------
def _on_raumfilter_changed(self, value: str) -> None:
# Persistiere Auswahl in Projekt-Variable; Prüfungen übernimmt die Laufzeitlogik
set_variable(RAUMFILTER_VAR, value, scope="project")
# ---------------------------------------------------------
# UI-Helfer
# ---------------------------------------------------------
def _prompt_user_to_select_file(self) -> None:
fname, _ = QFileDialog.getOpenFileName(
self,
"Verfahrens-DB auswählen",
"",
"Geopackage (*.gpkg)",
)
if fname:
try:
self.file_widget.setFilePath(fname)
except Exception:
try:
self.file_widget.setFileName(fname)
except Exception:
self.file_widget.setProperty("filePath", fname)
self.verfahrens_db = fname
self.logic.set_verfahrens_db(fname)
self._update_group_color()
def _update_group_color(self):
"""
Aktualisiert die Darstellung der Gruppenüberschrift.
"""
if not hasattr(self, "group_button"):
return
def _update_group_color(self) -> None:
if self.verfahrens_db:
self.group_button.setStyleSheet("font-weight: bold;")
else:
self.group_button.setStyleSheet("")