forked from AG_QGIS/Plugin_SN_Basis
Auf Wrapper umgestellt, Prüfarchitektur QT6-kompatibel gemacht (Nicht lauffähig)
This commit is contained in:
@@ -1,100 +1,126 @@
|
||||
#Modul zur Prüfung und zum Exception Handling für Dateieingaben
|
||||
#Dateipruefer.py
|
||||
"""
|
||||
sn_basis/modulesdateipruefer.py – Prüfung von Dateieingaben für das Plugin.
|
||||
Verwendet syswrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
|
||||
"""
|
||||
|
||||
import os
|
||||
from enum import Enum, auto
|
||||
from sn_basis.functions.syswrapper import (
|
||||
file_exists,
|
||||
is_file,
|
||||
join_path,
|
||||
)
|
||||
|
||||
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# ENUMS
|
||||
# -------------------------------
|
||||
class LeererPfadModus(Enum):#legt die modi fest, die für Dateipfade möglich sind
|
||||
VERBOTEN = auto() #ein leeres Eingabefeld stellt einen Fehler dar
|
||||
NUTZE_STANDARD = auto() #ein leeres Eingabefeld fordert zur Entscheidung auf: nutze Standard oder brich ab
|
||||
TEMPORAER_ERLAUBT = auto() #ein leeres Eingabefeld fordert zur Entscheidung auf: arbeite temporär oder brich ab.
|
||||
|
||||
|
||||
class DateiEntscheidung(Enum):#legt die Modi fest, wie mit bestehenden Dateien umgegangen werden soll (hat das das QGSFile-Objekt schon selbst?)
|
||||
ERSETZEN = auto()#Ergebnis der Nutzerentscheidung: bestehende Datei ersetzen
|
||||
ANHAENGEN = auto()#Ergebnis der Nutzerentscheidung: an bestehende Datei anhängen
|
||||
ABBRECHEN = auto()#bricht den Vorgang ab. (muss das eine definierte Option sein? oder geht das auch mit einem normalen Abbruch-Button)
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# RÜCKGABEOBJEKT
|
||||
# -------------------------------
|
||||
#Das Dateiprüfergebnis wird an den Prüfmanager übergeben. Alle GUI-Abfragen werden im Prüfmanager behandelt.
|
||||
class DateipruefErgebnis:
|
||||
#Definition der Parameter und Festlegung auf den Parametertyp,bzw den Standardwert
|
||||
def __init__(self, erfolgreich: bool, pfad: str = None, temporär: bool = False,
|
||||
entscheidung: DateiEntscheidung = None, fehler: list = None):
|
||||
self.erfolgreich = erfolgreich
|
||||
self.pfad = pfad
|
||||
self.temporär = temporär
|
||||
self.entscheidung = entscheidung
|
||||
self.fehler = fehler or []
|
||||
|
||||
def __repr__(self):
|
||||
return (f"DateipruefErgebnis(erfolgreich={self.erfolgreich}, "
|
||||
f"pfad={repr(self.pfad)}, temporär={self.temporär}, "
|
||||
f"entscheidung={repr(self.entscheidung)}, fehler={repr(self.fehler)})")
|
||||
|
||||
# -------------------------------
|
||||
# DATEIPRÜFER
|
||||
# -------------------------------
|
||||
class Dateipruefer:
|
||||
def pruefe(self, pfad: str,
|
||||
leer_modus: LeererPfadModus,
|
||||
standardname: str = None,
|
||||
plugin_pfad: str = None,
|
||||
vorhandene_datei_entscheidung: DateiEntscheidung = None) -> DateipruefErgebnis: #Rückgabetypannotation; "Die Funktion "pruefe" gibt ein Objekt vom Typ "DateipruefErgebnis" zurück
|
||||
"""
|
||||
Prüft Dateieingaben und liefert ein pruef_ergebnis zurück.
|
||||
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
||||
"""
|
||||
|
||||
# 1. Prüfe, ob das Eingabefeld leer ist
|
||||
if not pfad or pfad.strip() == "":#wenn der angegebene Pfad leer oder ungültig ist:
|
||||
if leer_modus == LeererPfadModus.VERBOTEN: #wenn der Modus "verboten" vorgegeben ist, gib zurück, dass der Test fehlgeschlagen ist
|
||||
return DateipruefErgebnis(
|
||||
erfolgreich=False,
|
||||
fehler=["Kein Pfad angegeben."]
|
||||
)
|
||||
elif leer_modus == LeererPfadModus.NUTZE_STANDARD:#wenn der Modus "Nutze_Standard" vorgegeben ist...
|
||||
if not plugin_pfad or not standardname:#wenn kein gültiger Pluginpfad angegeben ist oder die Standarddatei fehlt...
|
||||
return DateipruefErgebnis(
|
||||
erfolgreich=False,
|
||||
fehler=["Standardpfad oder -name fehlen."]#..gib zurück, dass der Test fehlgeschlagen ist
|
||||
)
|
||||
pfad = os.path.join(plugin_pfad, standardname)#...wenn es Standarddatei und Pluginpfad gibt...setze sie zum Pfad zusammen...
|
||||
elif leer_modus == LeererPfadModus.TEMPORAER_ERLAUBT:#wenn der Modus "temporär" vorgegeben ist,...
|
||||
return DateipruefErgebnis(#...gib zurück, dass das Prüfergebnis erfolgreich ist (Entscheidung, ob temporör gearbeitet werden soll oder nicht, kommt woanders)
|
||||
erfolgreich=True,
|
||||
pfad=None
|
||||
)
|
||||
def __init__(
|
||||
self,
|
||||
pfad: str,
|
||||
basis_pfad: str = "",
|
||||
leereingabe_erlaubt: bool = False,
|
||||
standarddatei: str | None = None,
|
||||
temporaer_erlaubt: bool = False,
|
||||
):
|
||||
self.pfad = pfad
|
||||
self.basis_pfad = basis_pfad
|
||||
self.leereingabe_erlaubt = leereingabe_erlaubt
|
||||
self.standarddatei = standarddatei
|
||||
self.temporaer_erlaubt = temporaer_erlaubt
|
||||
|
||||
# 2. Existiert die Datei bereits?
|
||||
if os.path.exists(pfad):#wenn die Datei vorhanden ist...
|
||||
if not vorhandene_datei_entscheidung:#aber noch keine Entscheidung getroffen ist...
|
||||
return DateipruefErgebnis(
|
||||
erfolgreich=True,#ist die Prüfung erfolgreich, aber es muss noch eine Entscheidung verlangt werden
|
||||
pfad=pfad,
|
||||
entscheidung=None,
|
||||
fehler=["Datei existiert bereits – Entscheidung ausstehend."]
|
||||
)
|
||||
|
||||
if vorhandene_datei_entscheidung == DateiEntscheidung.ABBRECHEN:
|
||||
return DateipruefErgebnis(#...der Nutzer aber abgebrochen hat...
|
||||
erfolgreich=False,#ist die Prüfung fehlgeschlagen ISSUE: ergibt das Sinn?
|
||||
pfad=pfad,
|
||||
fehler=["Benutzer hat abgebrochen."]
|
||||
)
|
||||
# ---------------------------------------------------------
|
||||
# Hilfsfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
return DateipruefErgebnis(
|
||||
erfolgreich=True,
|
||||
def _pfad(self, relativer_pfad: str) -> str:
|
||||
"""Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis."""
|
||||
return join_path(self.basis_pfad, relativer_pfad)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Hauptfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def pruefe(self) -> pruef_ergebnis:
|
||||
"""
|
||||
Prüft eine Dateieingabe und liefert ein pruef_ergebnis zurück.
|
||||
Der Pruefmanager entscheidet später, wie der Nutzer gefragt wird.
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 1. Fall: Eingabe ist leer
|
||||
# -----------------------------------------------------
|
||||
if not self.pfad:
|
||||
return self._handle_leere_eingabe()
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2. Fall: Eingabe ist nicht leer → Datei prüfen
|
||||
# -----------------------------------------------------
|
||||
pfad = self._pfad(self.pfad)
|
||||
|
||||
if not file_exists(pfad) or not is_file(pfad):
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.",
|
||||
aktion="datei_nicht_gefunden",
|
||||
pfad=pfad,
|
||||
entscheidung=vorhandene_datei_entscheidung
|
||||
)
|
||||
|
||||
# 3. Pfad gültig und Datei nicht vorhanden
|
||||
#wenn alle Varianten NICHT zutreffen, weil ein gültiger Pfad eingegeben wurde und die Datei noch nicht vorhanden ist:
|
||||
return DateipruefErgebnis(
|
||||
erfolgreich=True,
|
||||
pfad=pfad
|
||||
# -----------------------------------------------------
|
||||
# 3. Datei existiert → Erfolg
|
||||
# -----------------------------------------------------
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="Datei gefunden.",
|
||||
aktion="ok",
|
||||
pfad=pfad,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Behandlung leerer Eingaben
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _handle_leere_eingabe(self) -> pruef_ergebnis:
|
||||
"""
|
||||
Liefert ein pruef_ergebnis für den Fall, dass das Dateifeld leer ist.
|
||||
Der Pruefmanager fragt später den Nutzer.
|
||||
"""
|
||||
|
||||
# 1. Leereingabe erlaubt → Nutzer fragen, ob das beabsichtigt war
|
||||
if self.leereingabe_erlaubt:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?",
|
||||
aktion="leereingabe_erlaubt",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
|
||||
if self.standarddatei:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Es wurde keine Datei angegeben. Soll die Standarddatei '{self.standarddatei}' verwendet werden?",
|
||||
aktion="standarddatei_vorschlagen",
|
||||
pfad=self._pfad(self.standarddatei),
|
||||
)
|
||||
|
||||
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
|
||||
if self.temporaer_erlaubt:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Es wurde keine Datei angegeben. Soll eine temporäre Datei erzeugt werden?",
|
||||
aktion="temporaer_erlaubt",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# 4. Leereingabe nicht erlaubt → Fehler
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Es wurde keine Datei angegeben.",
|
||||
aktion="leereingabe_nicht_erlaubt",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
@@ -1,52 +1,138 @@
|
||||
#Pruefmanager.py
|
||||
from modules.qt_compat import QMessageBox, QFileDialog, YES, NO, CANCEL, ICON_QUESTION, exec_dialog
|
||||
from modules.Dateipruefer import DateiEntscheidung
|
||||
"""
|
||||
sn_basis/modules/pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten.
|
||||
Steuert die Nutzerinteraktion über qgisqt_wrapper.
|
||||
"""
|
||||
|
||||
class PruefManager:
|
||||
from sn_basis.functions.qgisqt_wrapper import (
|
||||
ask_yes_no,
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
set_layer_visible, # optional, falls implementiert
|
||||
)
|
||||
|
||||
def __init__(self, iface=None, plugin_pfad=None):
|
||||
self.iface = iface
|
||||
self.plugin_pfad = plugin_pfad
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
def frage_datei_ersetzen_oder_anhaengen(self, pfad: str) -> DateiEntscheidung:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle("Datei existiert")
|
||||
msg.setText(f"Die Datei '{pfad}' existiert bereits.\nWas möchtest du tun?")
|
||||
|
||||
msg.setStandardButtons(YES | NO | CANCEL)
|
||||
msg.setDefaultButton(YES)
|
||||
class Pruefmanager:
|
||||
"""
|
||||
Verarbeitet pruef_ergebnis-Objekte und steuert die Nutzerinteraktion.
|
||||
"""
|
||||
|
||||
msg.button(YES).setText("Ersetzen")
|
||||
msg.button(NO).setText("Anhängen")
|
||||
msg.button(CANCEL).setText("Abbrechen")
|
||||
def __init__(self, ui_modus: str = "qgis"):
|
||||
self.ui_modus = ui_modus
|
||||
|
||||
result = exec_dialog(msg)
|
||||
# ---------------------------------------------------------
|
||||
# Hauptfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
if result == YES:
|
||||
return DateiEntscheidung.ERSETZEN
|
||||
elif result == NO:
|
||||
return DateiEntscheidung.ANHAENGEN
|
||||
else:
|
||||
return DateiEntscheidung.ABBRECHEN
|
||||
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||
"""
|
||||
Verarbeitet ein pruef_ergebnis und führt ggf. Nutzerinteraktion durch.
|
||||
Rückgabe: neues oder unverändertes pruef_ergebnis.
|
||||
"""
|
||||
|
||||
def frage_temporär_verwenden(self) -> bool:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle("Temporäre Layer")
|
||||
msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?")
|
||||
if ergebnis.ok:
|
||||
return ergebnis
|
||||
|
||||
msg.setStandardButtons(YES | NO)
|
||||
msg.setDefaultButton(YES)
|
||||
aktion = ergebnis.aktion
|
||||
|
||||
result = exec_dialog(msg)
|
||||
return result == YES
|
||||
# -----------------------------------------------------
|
||||
# Allgemeine Aktionen
|
||||
# -----------------------------------------------------
|
||||
|
||||
def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str:
|
||||
pfad, _ = QFileDialog.getSaveFileName(
|
||||
parent=None,
|
||||
caption=titel,
|
||||
directory=self.plugin_pfad or "",
|
||||
filter=filter
|
||||
)
|
||||
return pfad
|
||||
if aktion == "leer":
|
||||
warning("Eingabe fehlt", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "leereingabe_erlaubt":
|
||||
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
|
||||
return pruef_ergebnis(True, "Ohne Eingabe fortgefahren.", "ok", None)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "leereingabe_nicht_erlaubt":
|
||||
warning("Eingabe erforderlich", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "standarddatei_vorschlagen":
|
||||
if ask_yes_no("Standarddatei verwenden", ergebnis.meldung):
|
||||
return pruef_ergebnis(True, "Standarddatei wird verwendet.", "ok", ergebnis.pfad)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "temporaer_erlaubt":
|
||||
if ask_yes_no("Temporäre Datei erzeugen", ergebnis.meldung):
|
||||
return pruef_ergebnis(True, "Temporäre Datei soll erzeugt werden.", "temporaer_erzeugen", None)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "datei_nicht_gefunden":
|
||||
warning("Datei nicht gefunden", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "kein_dateipfad":
|
||||
warning("Ungültiger Pfad", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "pfad_nicht_gefunden":
|
||||
warning("Pfad nicht gefunden", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "url_nicht_erreichbar":
|
||||
warning("URL nicht erreichbar", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "netzwerkfehler":
|
||||
error("Netzwerkfehler", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Layer-Aktionen
|
||||
# -----------------------------------------------------
|
||||
|
||||
if aktion == "layer_nicht_gefunden":
|
||||
error("Layer fehlt", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "layer_unsichtbar":
|
||||
if ask_yes_no("Layer einblenden", ergebnis.meldung):
|
||||
# Falls set_layer_visible implementiert ist
|
||||
try:
|
||||
set_layer_visible(ergebnis.pfad, True)
|
||||
except Exception:
|
||||
pass
|
||||
return pruef_ergebnis(True, "Layer wurde eingeblendet.", "ok", ergebnis.pfad)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "falscher_geotyp":
|
||||
warning("Falscher Geometrietyp", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "layer_leer":
|
||||
warning("Layer enthält keine Objekte", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "falscher_layertyp":
|
||||
warning("Falscher Layertyp", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "falsches_crs":
|
||||
warning("Falsches CRS", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "felder_fehlen":
|
||||
warning("Fehlende Felder", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "datenquelle_unerwartet":
|
||||
warning("Unerwartete Datenquelle", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
if aktion == "layer_nicht_editierbar":
|
||||
warning("Layer nicht editierbar", ergebnis.meldung)
|
||||
return ergebnis
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Fallback
|
||||
# -----------------------------------------------------
|
||||
|
||||
warning("Unbekannte Aktion", f"Unbekannte Aktion: {aktion}")
|
||||
return ergebnis
|
||||
|
||||
170
modules/layerpruefer.py
Normal file
170
modules/layerpruefer.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
sn_basis/modules/layerpruefer.py – Prüfung von QGIS-Layern.
|
||||
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
|
||||
"""
|
||||
|
||||
from sn_basis.functions.qgisqt_wrapper import (
|
||||
layer_exists,
|
||||
get_layer_geometry_type,
|
||||
get_layer_feature_count,
|
||||
is_layer_visible,
|
||||
get_layer_type,
|
||||
get_layer_crs,
|
||||
get_layer_fields,
|
||||
get_layer_source,
|
||||
is_layer_editable,
|
||||
)
|
||||
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
class Layerpruefer:
|
||||
"""
|
||||
Prüft Layer auf Existenz, Sichtbarkeit, Geometrietyp, Objektanzahl,
|
||||
Layertyp, CRS, Felder, Datenquelle und Editierbarkeit.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
layer,
|
||||
erwarteter_geotyp: str | None = None,
|
||||
muss_sichtbar_sein: bool = False,
|
||||
erwarteter_layertyp: str | None = None,
|
||||
erwartetes_crs: str | None = None,
|
||||
erforderliche_felder: list[str] | None = None,
|
||||
erlaubte_datenquellen: list[str] | None = None,
|
||||
muss_editierbar_sein: bool = False,
|
||||
):
|
||||
self.layer = layer
|
||||
self.erwarteter_geotyp = erwarteter_geotyp
|
||||
self.muss_sichtbar_sein = muss_sichtbar_sein
|
||||
self.erwarteter_layertyp = erwarteter_layertyp
|
||||
self.erwartetes_crs = erwartetes_crs
|
||||
self.erforderliche_felder = erforderliche_felder or []
|
||||
self.erlaubte_datenquellen = erlaubte_datenquellen or []
|
||||
self.muss_editierbar_sein = muss_editierbar_sein
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Hauptfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def pruefe(self) -> pruef_ergebnis:
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 1. Existenz
|
||||
# -----------------------------------------------------
|
||||
if not layer_exists(self.layer):
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Der Layer existiert nicht oder wurde nicht geladen.",
|
||||
aktion="layer_nicht_gefunden", # type: ignore
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2. Sichtbarkeit
|
||||
# -----------------------------------------------------
|
||||
sichtbar = is_layer_visible(self.layer)
|
||||
if self.muss_sichtbar_sein and not sichtbar:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?",
|
||||
aktion="layer_unsichtbar", # type: ignore
|
||||
pfad=self.layer, # Layerobjekt wird übergeben
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 3. Layertyp
|
||||
# -----------------------------------------------------
|
||||
layertyp = get_layer_type(self.layer)
|
||||
if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Layer hat den Typ '{layertyp}', erwartet wurde '{self.erwarteter_layertyp}'.",
|
||||
aktion="falscher_layertyp",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 4. Geometrietyp
|
||||
# -----------------------------------------------------
|
||||
geotyp = get_layer_geometry_type(self.layer)
|
||||
if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Layer hat den Geometrietyp '{geotyp}', erwartet wurde '{self.erwarteter_geotyp}'.",
|
||||
aktion="falscher_geotyp",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 5. Featureanzahl
|
||||
# -----------------------------------------------------
|
||||
anzahl = get_layer_feature_count(self.layer)
|
||||
if anzahl == 0:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Der Layer enthält keine Objekte.",
|
||||
aktion="layer_leer",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 6. CRS
|
||||
# -----------------------------------------------------
|
||||
crs = get_layer_crs(self.layer)
|
||||
if self.erwartetes_crs and crs != self.erwartetes_crs:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Layer hat das CRS '{crs}', erwartet wurde '{self.erwartetes_crs}'.",
|
||||
aktion="falsches_crs",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 7. Felder
|
||||
# -----------------------------------------------------
|
||||
felder = get_layer_fields(self.layer)
|
||||
fehlende = [f for f in self.erforderliche_felder if f not in felder]
|
||||
|
||||
if fehlende:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Layer enthält nicht alle erforderlichen Felder: {', '.join(fehlende)}",
|
||||
aktion="felder_fehlen",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 8. Datenquelle
|
||||
# -----------------------------------------------------
|
||||
quelle = get_layer_source(self.layer)
|
||||
if self.erlaubte_datenquellen and quelle not in self.erlaubte_datenquellen:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.",
|
||||
aktion="datenquelle_unerwartet",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 9. Editierbarkeit
|
||||
# -----------------------------------------------------
|
||||
editable = is_layer_editable(self.layer)
|
||||
if self.muss_editierbar_sein and not editable:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Der Layer ist nicht editierbar.",
|
||||
aktion="layer_nicht_editierbar",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 10. Alles OK
|
||||
# -----------------------------------------------------
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="Layerprüfung erfolgreich.",
|
||||
aktion="ok",
|
||||
pfad=None,
|
||||
)
|
||||
@@ -1,77 +1,141 @@
|
||||
# Linkpruefer.py – Qt5/Qt6-kompatibel über qt_compat
|
||||
"""
|
||||
sn_basis/modules/linkpruefer.py – Prüfung von URLs und lokalen Links.
|
||||
Verwendet syswrapper und qgisqt_wrapper.
|
||||
Gibt pruef_ergebnis an den Pruefmanager zurück.
|
||||
"""
|
||||
|
||||
from modules.qt_compat import (
|
||||
QEventLoop,
|
||||
QUrl,
|
||||
QNetworkRequest,
|
||||
QNetworkReply
|
||||
from sn_basis.functions.syswrapper import (
|
||||
file_exists,
|
||||
is_file,
|
||||
join_path,
|
||||
)
|
||||
|
||||
from qgis.core import QgsNetworkAccessManager
|
||||
from modules.pruef_ergebnis import PruefErgebnis
|
||||
from sn_basis.functions.qgisqt_wrapper import (
|
||||
network_head,
|
||||
)
|
||||
|
||||
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
||||
|
||||
|
||||
class Linkpruefer:
|
||||
"""Prüft den Link mit QgsNetworkAccessManager und klassifiziert Anbieter nach Attribut."""
|
||||
"""
|
||||
Prüft URLs und lokale Pfade.
|
||||
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
||||
"""
|
||||
|
||||
ANBIETER_TYPEN: dict[str, str] = {
|
||||
"REST": "REST",
|
||||
"WFS": "WFS",
|
||||
"WMS": "WMS",
|
||||
"OGR": "OGR"
|
||||
}
|
||||
def __init__(self, basis_pfad: str | None = None):
|
||||
"""
|
||||
basis_pfad: optionaler Basisordner für relative Pfade.
|
||||
"""
|
||||
self.basis = basis_pfad
|
||||
|
||||
def __init__(self, link: str, anbieter: str):
|
||||
self.link = link
|
||||
self.anbieter = anbieter.upper().strip() if anbieter else ""
|
||||
self.network_manager = QgsNetworkAccessManager()
|
||||
# ---------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def klassifiziere_anbieter(self):
|
||||
typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter)
|
||||
quelle = "remote" if self.link.startswith(("http://", "https://")) else "local"
|
||||
return {"typ": typ, "quelle": quelle}
|
||||
def _pfad(self, relativer_pfad: str) -> str:
|
||||
"""Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
|
||||
if not self.basis:
|
||||
return relativer_pfad
|
||||
return join_path(self.basis, relativer_pfad)
|
||||
|
||||
def pruefe_link(self):
|
||||
fehler = []
|
||||
warnungen = []
|
||||
def _ist_url(self, text: str) -> bool:
|
||||
"""Einfache URL-Erkennung."""
|
||||
return text.startswith("http://") or text.startswith("https://")
|
||||
|
||||
if not self.link:
|
||||
fehler.append("Link fehlt.")
|
||||
return PruefErgebnis(False, fehler=fehler, warnungen=warnungen)
|
||||
# ---------------------------------------------------------
|
||||
# Hauptfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
if not self.anbieter or not self.anbieter.strip():
|
||||
fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.")
|
||||
def pruefe(self, eingabe: str) -> pruef_ergebnis:
|
||||
"""
|
||||
Prüft einen Link (URL oder lokalen Pfad).
|
||||
Rückgabe: pruef_ergebnis
|
||||
"""
|
||||
|
||||
# Remote-Links prüfen
|
||||
if self.link.startswith(("http://", "https://")):
|
||||
request = QNetworkRequest(QUrl(self.link))
|
||||
reply = self.network_manager.head(request)
|
||||
if not eingabe:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Es wurde kein Link angegeben.",
|
||||
aktion="leer",
|
||||
pfad=None,
|
||||
)
|
||||
|
||||
loop = QEventLoop()
|
||||
reply.finished.connect(loop.quit)
|
||||
loop.exec() # Qt5/Qt6-kompatibel über qt_compat
|
||||
# -----------------------------------------------------
|
||||
# 1. Fall: URL
|
||||
# -----------------------------------------------------
|
||||
if self._ist_url(eingabe):
|
||||
return self._pruefe_url(eingabe)
|
||||
|
||||
# Fehlerprüfung Qt5/Qt6-kompatibel
|
||||
if reply.error() != QNetworkReply.NetworkError.NoError:
|
||||
fehler.append(f"Verbindungsfehler: {reply.errorString()}")
|
||||
else:
|
||||
status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status is None or status < 200 or status >= 400:
|
||||
fehler.append(f"Link nicht erreichbar: HTTP {status}")
|
||||
# -----------------------------------------------------
|
||||
# 2. Fall: lokaler Pfad
|
||||
# -----------------------------------------------------
|
||||
return self._pruefe_dateipfad(eingabe)
|
||||
|
||||
reply.deleteLater()
|
||||
# ---------------------------------------------------------
|
||||
# URL‑Prüfung
|
||||
# ---------------------------------------------------------
|
||||
|
||||
else:
|
||||
# Lokale Pfade: Plausibilitätscheck
|
||||
if "." not in self.link.split("/")[-1]:
|
||||
warnungen.append("Der lokale Link sieht ungewöhnlich aus.")
|
||||
def _pruefe_url(self, url: str) -> pruef_ergebnis:
|
||||
"""
|
||||
Prüft eine URL über einen HEAD‑Request.
|
||||
"""
|
||||
|
||||
return PruefErgebnis(
|
||||
len(fehler) == 0,
|
||||
daten=self.klassifiziere_anbieter(),
|
||||
fehler=fehler,
|
||||
warnungen=warnungen
|
||||
reply = network_head(url)
|
||||
|
||||
if reply is None:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Die URL '{url}' konnte nicht geprüft werden.",
|
||||
aktion="netzwerkfehler",
|
||||
pfad=url,
|
||||
)
|
||||
|
||||
if reply.error != 0:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Die URL '{url}' ist nicht erreichbar.",
|
||||
aktion="url_nicht_erreichbar",
|
||||
pfad=url,
|
||||
)
|
||||
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="URL ist erreichbar.",
|
||||
aktion="ok",
|
||||
pfad=url,
|
||||
)
|
||||
|
||||
def ausfuehren(self):
|
||||
return self.pruefe_link()
|
||||
# ---------------------------------------------------------
|
||||
# Lokale Datei‑/Pfadprüfung
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _pruefe_dateipfad(self, eingabe: str) -> pruef_ergebnis:
|
||||
"""
|
||||
Prüft einen lokalen Pfad.
|
||||
"""
|
||||
|
||||
pfad = self._pfad(eingabe)
|
||||
|
||||
if not file_exists(pfad):
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.",
|
||||
aktion="pfad_nicht_gefunden",
|
||||
pfad=pfad,
|
||||
)
|
||||
|
||||
if not is_file(pfad):
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Pfad '{eingabe}' ist keine Datei.",
|
||||
aktion="kein_dateipfad",
|
||||
pfad=pfad,
|
||||
)
|
||||
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="Dateipfad ist gültig.",
|
||||
aktion="ok",
|
||||
pfad=pfad,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,58 @@
|
||||
#pruef_ergebnis.py
|
||||
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
|
||||
class PruefErgebnis:
|
||||
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
|
||||
self.erfolgreich = erfolgreich
|
||||
self.daten = daten or {}
|
||||
self.fehler = fehler or []
|
||||
self.warnungen = warnungen or []
|
||||
"""
|
||||
sn_basis/modules/pruef_ergebnis.py – Ergebnisobjekt für alle Prüfer.
|
||||
|
||||
def __repr__(self):
|
||||
return (f"PruefErgebnis(erfolgreich={self.erfolgreich}, "
|
||||
f"daten={self.daten}, fehler={self.fehler}, warnungen={self.warnungen})")
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Literal
|
||||
|
||||
|
||||
# Alle möglichen Aktionen, die ein Prüfer auslösen kann.
|
||||
# Erweiterbar ohne Umbau der Klasse.
|
||||
PruefAktion = Literal[
|
||||
"ok",
|
||||
"leer",
|
||||
"leereingabe_erlaubt",
|
||||
"leereingabe_nicht_erlaubt",
|
||||
"standarddatei_vorschlagen",
|
||||
"temporaer_erlaubt",
|
||||
"datei_nicht_gefunden",
|
||||
"kein_dateipfad",
|
||||
"pfad_nicht_gefunden",
|
||||
"url_nicht_erreichbar",
|
||||
"netzwerkfehler",
|
||||
"falscher_layertyp",
|
||||
"falscher_geotyp",
|
||||
"layer_leer",
|
||||
"falsches_crs",
|
||||
"felder_fehlen",
|
||||
"datenquelle_unerwartet",
|
||||
"layer_nicht_editierbar",
|
||||
"temporaer_erzeugen",
|
||||
"stil_nicht_anwendbar",
|
||||
"layer_unsichtbar",
|
||||
"unbekannt",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class pruef_ergebnis:
|
||||
"""
|
||||
Reines Datenobjekt, das das Ergebnis einer Prüfung beschreibt.
|
||||
|
||||
ok: True → Prüfung erfolgreich
|
||||
False → Nutzerinteraktion oder Fehler nötig
|
||||
|
||||
meldung: Text, der dem Nutzer angezeigt werden soll
|
||||
|
||||
aktion: Maschinenlesbarer Code, der dem Pruefmanager sagt,
|
||||
wie er weiter verfahren soll
|
||||
|
||||
pfad: Optionaler Pfad oder URL, die geprüft wurde oder
|
||||
verwendet werden soll
|
||||
"""
|
||||
|
||||
ok: bool
|
||||
meldung: str
|
||||
aktion: PruefAktion
|
||||
pfad: Optional[str] = None
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
qt_compat.py – Einheitliche Qt-Kompatibilitätsschicht für QGIS-Plugins.
|
||||
|
||||
Ziele:
|
||||
- PyQt6 bevorzugt
|
||||
- Fallback auf PyQt5
|
||||
- Mock-Modus, wenn kein Qt verfügbar ist (z. B. in Unittests)
|
||||
- OR-fähige Fake-Enums im Mock-Modus
|
||||
"""
|
||||
|
||||
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: PyQt6 importieren
|
||||
# ---------------------------------------------------------
|
||||
try:
|
||||
from PyQt6.QtWidgets import QMessageBox, QFileDialog
|
||||
from PyQt6.QtCore import Qt, QEventLoop, QUrl
|
||||
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
YES = QMessageBox.StandardButton.Yes
|
||||
NO = QMessageBox.StandardButton.No
|
||||
CANCEL = QMessageBox.StandardButton.Cancel
|
||||
ICON_QUESTION = QMessageBox.Icon.Question
|
||||
|
||||
QT_VERSION = 6
|
||||
|
||||
def exec_dialog(dialog):
|
||||
"""Einheitliche Ausführung eines Dialogs."""
|
||||
return dialog.exec()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: PyQt5 importieren
|
||||
# ---------------------------------------------------------
|
||||
except Exception:
|
||||
try:
|
||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
||||
from PyQt5.QtCore import Qt, QEventLoop, QUrl
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
YES = QMessageBox.Yes
|
||||
NO = QMessageBox.No
|
||||
CANCEL = QMessageBox.Cancel
|
||||
ICON_QUESTION = QMessageBox.Question
|
||||
|
||||
QT_VERSION = 5
|
||||
|
||||
def exec_dialog(dialog):
|
||||
return dialog.exec_()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Mock-Modus (kein Qt verfügbar)
|
||||
# ---------------------------------------------------------
|
||||
except Exception:
|
||||
QT_VERSION = 0
|
||||
|
||||
class FakeEnum(int):
|
||||
"""Ein OR-fähiger Enum-Ersatz für den Mock-Modus."""
|
||||
def __or__(self, other):
|
||||
return FakeEnum(int(self) | int(other))
|
||||
|
||||
class QMessageBox:
|
||||
Yes = FakeEnum(1)
|
||||
No = FakeEnum(2)
|
||||
Cancel = FakeEnum(4)
|
||||
Question = FakeEnum(8)
|
||||
|
||||
class QFileDialog:
|
||||
"""Minimaler Mock für QFileDialog."""
|
||||
@staticmethod
|
||||
def getOpenFileName(*args, **kwargs):
|
||||
return ("", "") # kein Dateipfad
|
||||
|
||||
YES = QMessageBox.Yes
|
||||
NO = QMessageBox.No
|
||||
CANCEL = QMessageBox.Cancel
|
||||
ICON_QUESTION = QMessageBox.Question
|
||||
|
||||
def exec_dialog(dialog):
|
||||
"""Mock-Ausführung: gibt YES zurück, außer Tests patchen es."""
|
||||
return YES
|
||||
# -------------------------
|
||||
# Mock Netzwerk-Klassen
|
||||
# -------------------------
|
||||
class QEventLoop:
|
||||
def exec(self):
|
||||
return 0
|
||||
|
||||
def quit(self):
|
||||
pass
|
||||
|
||||
class QUrl(str):
|
||||
pass
|
||||
|
||||
class QNetworkRequest:
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
class QNetworkReply:
|
||||
def __init__(self):
|
||||
self._data = b""
|
||||
|
||||
def readAll(self):
|
||||
return self._data
|
||||
|
||||
def error(self):
|
||||
return 0
|
||||
|
||||
def exec_dialog(dialog):
|
||||
return YES
|
||||
|
||||
@@ -1,46 +1,59 @@
|
||||
#stilpruefer.py
|
||||
import os
|
||||
from modules.pruef_ergebnis import PruefErgebnis
|
||||
"""
|
||||
sn_basis/modules/stilpruefer.py – Prüfung und Anwendung von Layerstilen.
|
||||
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
|
||||
"""
|
||||
|
||||
from sn_basis.functions.qgisqt_wrapper import (
|
||||
apply_style,
|
||||
)
|
||||
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
class Stilpruefer:
|
||||
"""
|
||||
Prüft, ob ein angegebener Stilpfad gültig und nutzbar ist.
|
||||
- Wenn kein Stil angegeben ist, gilt die Prüfung als erfolgreich.
|
||||
- Wenn angegeben:
|
||||
* Datei muss existieren
|
||||
* Dateiendung muss '.qml' sein
|
||||
Prüft, ob ein Stil auf einen Layer angewendet werden kann.
|
||||
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
||||
"""
|
||||
|
||||
def pruefe(self, stilpfad: str) -> PruefErgebnis:
|
||||
# kein Stil angegeben -> erfolgreich, keine Warnung
|
||||
if not stilpfad or stilpfad.strip() == "":
|
||||
return PruefErgebnis(
|
||||
erfolgreich=True,
|
||||
daten={"stil": None},
|
||||
warnungen=["Kein Stil angegeben."]
|
||||
def __init__(self, layer, stil_pfad: str):
|
||||
"""
|
||||
layer: QGIS-Layer oder Mock-Layer
|
||||
stil_pfad: relativer oder absoluter Pfad zum .qml-Stil
|
||||
"""
|
||||
self.layer = layer
|
||||
self.stil_pfad = stil_pfad
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Hauptfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def pruefe(self) -> pruef_ergebnis:
|
||||
"""
|
||||
Versucht, den Stil anzuwenden.
|
||||
Rückgabe: pruef_ergebnis
|
||||
"""
|
||||
|
||||
# Wrapper übernimmt:
|
||||
# - Pfadberechnung
|
||||
# - Existenzprüfung
|
||||
# - loadNamedStyle
|
||||
# - Fehlerbehandlung
|
||||
# - Mock-Modus
|
||||
erfolg, meldung = apply_style(self.layer, self.stil_pfad)
|
||||
|
||||
if erfolg:
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung=f"Stil erfolgreich angewendet: {self.stil_pfad}",
|
||||
aktion="ok",
|
||||
pfad=self.stil_pfad,
|
||||
)
|
||||
|
||||
fehler = []
|
||||
warnungen = []
|
||||
|
||||
# Prüfung: Datei existiert?
|
||||
if not os.path.exists(stilpfad):
|
||||
fehler.append(f"Stildatei nicht gefunden: {stilpfad}")
|
||||
|
||||
# Prüfung: Endung .qml?
|
||||
elif not stilpfad.lower().endswith(".qml"):
|
||||
fehler.append(f"Ungültige Dateiendung für Stil: {stilpfad}")
|
||||
else:
|
||||
# Hinweis: alle Checks bestanden
|
||||
return PruefErgebnis(
|
||||
erfolgreich=True,
|
||||
daten={"stil": stilpfad}
|
||||
)
|
||||
|
||||
return PruefErgebnis(
|
||||
erfolgreich=False if fehler else True,
|
||||
daten={"stil": stilpfad},
|
||||
fehler=fehler,
|
||||
warnungen=warnungen
|
||||
# Fehlerfall → Nutzerinteraktion nötig
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=meldung,
|
||||
aktion="stil_nicht_anwendbar",
|
||||
pfad=self.stil_pfad,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user