diff --git a/assets/Dateipruefer_flowchart.svg b/assets/Dateipruefer_flowchart.svg new file mode 100644 index 0000000..b9abfe8 --- /dev/null +++ b/assets/Dateipruefer_flowchart.svg @@ -0,0 +1,102 @@ +JaVERBOTENNUTZE_STANDARDohne plugin_pfad/standardNUTZE_STANDARDmit plugin_pfad+standardTEMPORAER_ERLAUBTNeinJaNeinJa=ABBRECHENJa=ERSETZEN/ANHAENGENNeinStart: Eingabe prüfenPfad leer?leer_modusReturn: erfolgreich=FalseFehler: 'Kein Pfad angegeben'Return: erfolgreich=FalseFehler: 'Standardpfad/-name fehlen'Setze Pfad=plugin_pfad+standardReturn: erfolgreich=Truetemporär=TrueDatei existiert?vorhandene_datei_entscheidung gesetzt?Return: erfolgreich=Trueentscheidung=NoneFehler: 'Datei existiert bereits – Entscheidung ausstehend'Return: erfolgreich=FalseFehler: 'Benutzer hat abgebrochen'Return: erfolgreich=Trueentscheidung=...Return: erfolgreich=Truepfad=... \ No newline at end of file diff --git a/assets/Linkpruefer_flowchart.svg b/assets/Linkpruefer_flowchart.svg new file mode 100644 index 0000000..7d791de --- /dev/null +++ b/assets/Linkpruefer_flowchart.svg @@ -0,0 +1,3 @@ + + +NeinJaJaNeinJaJaNeinNeinJaNeinStart LinkprüfungIst Link vorhanden?Fehler: Link fehltPrüfergebnis: Fehler zurückgebenIst Link Remote http/https?HEAD-Anfrage mit QgsNetworkAccessManagerAntwort erhalten?Fehler: VerbindungsfehlerPrüfergebnis: Fehler zurückgebenHTTP-Statuscode < 200 oder ≥ 400?Fehler: Link nicht erreichbarAnbieter klassifizierenPrüfergebnis zurückgebenPlausibilitätscheck für lokalen LinkLink sieht ungewöhnlich aus?Warnung ausgeben \ No newline at end of file diff --git a/assets/Objektstruktur.txt b/assets/Objektstruktur.txt new file mode 100644 index 0000000..df3e392 --- /dev/null +++ b/assets/Objektstruktur.txt @@ -0,0 +1,122 @@ ++ PluginController + └─ GUIManager + └─ PrüfManager (koordiniert alle Prüfer) + ├─ Dateiprüfer + ├─ Linklistenprüfer + │ └─ Zeilenprüfer[n] + │ ├─ Linkprüfer + │ └─ Stilprüfer + └─ LayerLoader + └─ Logger + +Plan41_plugin/ +│ +├── plugin/ # Plugin-Code +│ ├── main_plugin.py # PluginController +│ ├── dock_widget.py # GUIManager +│ ├── pruefer/ +│ │ ├── dateipruefer.py +│ │ ├── linklistenpruefer.py +│ │ ├── zeilenpruefer.py +│ │ ├── linkpruefer.py +│ │ └── stilpruefer.py +│ ├── loader.py +│ └── logger.py +│ +├── tests/ # Unit-Tests +│ ├── __init__.py +│ ├── test_dateipruefer.py +│ ├── test_linklistenpruefer.py +│ ├── test_zeilenpruefer.py +│ ├── test_linkpruefer.py +│ ├── test_stilpruefer.py +│ ├── test_logger.py +│ └── run_tests.py # zentraler Test-Runner +│ +├── requirements.txt +└── README.md + + ++------------------------------------+ +| PluginController | ++------------------------------------+ +| - Dock_widget: GUIManager | +| - pruef_manager: PruefManager | +| - loader: LayerLoader | +| - logger: Logger | ++------------------------------------+ +| + start(): void | ++------------------------------------+ + ++------------------------------------+ +| GUIManager | ++------------------------------------+ +| - dialog: QWidget | ++------------------------------------+ +| + getParameter(): dict | ++------------------------------------+ + ++------------------------------------+ +| PruefManager | ++------------------------------------+ +| - dateipruefer: Dateipruefer | +| - linklistenpruefer: Linklisten... | ++------------------------------------+ +| + pruefe_alle(parameter): list | ++------------------------------------+ + ++------------------------------------+ +| Dateipruefer | ++------------------------------------+ +| + pruefe(pfad: str): PruefErgebnis | ++------------------------------------+ + ++------------------------------------+ +| Linklistenpruefer | ++------------------------------------+ +| + pruefe(pfad: str): list[Zeile] | ++------------------------------------+ + ++------------------------------------+ +| Zeilenpruefer | ++------------------------------------+ +| - linkpruefer: Linkpruefer | +| - stilpruefer: Stilpruefer | ++------------------------------------+ +| + pruefe(zeile: str): LayerAuftrag | ++------------------------------------+ + ++------------------------------------+ +| Linkpruefer | ++------------------------------------+ +| + pruefe(link: str): PruefErgebnis | ++------------------------------------+ + ++------------------------------------+ +| Stilpruefer | ++------------------------------------+ +| + pruefe(stilname: str): Pruef... | ++------------------------------------+ + ++------------------------------------+ +| LayerLoader | ++------------------------------------+ +| + lade(layer_auftrag): void | ++------------------------------------+ + ++------------------------------------+ +| Logger | ++------------------------------------+ +| + schreibe(msg: str): void | +| + exportiere(): file | ++------------------------------------+ + ++------------------------------------+ +| PruefErgebnis | ++------------------------------------+ +| - erfolgreich: bool | +| - daten: dict | +| - fehler: list[str] | +| - warnungen: list[str] | ++------------------------------------+ + diff --git a/assets/Pluginkonzept.md b/assets/Pluginkonzept.md new file mode 100644 index 0000000..fd3ff8a --- /dev/null +++ b/assets/Pluginkonzept.md @@ -0,0 +1,22 @@ +**Pluginkonzept** +Das Plugin ist grundsätzlich als modulares System gedacht. Komponenten sollen sowohl im Plugin selbst, aber auch in anderen Anwendungen verbaut werden können. +Die Module sind als Python-Objekte angelegt. +Alle Fallunterscheidungen, Exception-Management und Fehlerbehandlung sind in die "Prüfer" ausgelagert. +Der "Prüfmanager" übernimmt dabei die Interaktion mit dem Anwender, um Abfragen oder Fallunterscheidungen durchzuführen, die nicht anhand des Codes erfolgen können. +Alle Prüfer geben ein Objekt "Prüfergebnis" zurück, das das Ergebnis der Fallunterscheidung, Exceptions und Fehlermeldungen enthält. Die Prüfer haben selbst keine UI-Elemente. + +| Modul | Aufgabe | Beschreibung | +|-------------------|---------------------------------------|--------------| +|PruefManager | Nutzerabfragen, Ergebnisanpassung | Der Pruefmanager wertet das Ergebnis vom Typ "PruefErgebnis" aus. Sind Entscheidungen erforderlich, fragt er den Anwender und passt das PruefErgebnis entsprechend an, bzw gibt Fehler aus| +|Dateipruefer | Auswertung der Eingaben in Dateiauswahlfeldern | Der Dateipruefer prüft die Eingaben in Dateifeldern. Dabei kann bei jeder Prüfung vorgegeben werden, ob leere Eingabefelder zulässig sind, und ob sie, wenn sie leer sind, eine Standarddatei aufrufen oder temporäre Layer erzeugen. In jedem Fall wird der Nutzer zur Entscheidung aufgefordert, ob das leere Feld beabsichtigt ist, oder ein Bedienfehler| +|Linklistenpruefer | Spezialprüfer für die Linkliste aus dem Plan41-Plugin | Damit die beiden Objekte Stilpruefer und Linkpruefer auch unabhängig voneinander verwendet werden können, fasst der Linklistenpruefer die Ergebnisse zusammen und ergänzt eine Prüfung zur Kartenreihenfolge/Layerreihenfolge| +|Linkpruefer | prüft die Quelle eines angegebenen Links technisch und entscheidet die technischen Parameter nach Typ und Quellort | Enthält eine Fallunterscheidung für lokale und remote-Quellen, sowie für unterschiedliche Datenanbieter. Der Linkpruefer gibt Fehler und Exceptions zurück, wenn die Quelle fehlerhaft oder nicht erreichbar ist.| +|Stilpruefer | Prüft verschiedene Stilquellen | Der Stilpruefer prüft .qml und eingebettete Stile und gibt Warnungen zurück, bzw. Exceptions, um Nutzerentscheidungen auszulösen| + +Jedes Modul hat seinen eigenen Unittest. Die Tests werden im Unterordner "Test" zusammengefasst und können gesammelt über die "run_tests.py" aufgerufen werden. + +Jedes Modul wird durch ein Mermaid-ClassDiagram beschrieben. Die Entscheidungen und Exceptions, sowie die behandelten Fehler werden visuell aufbereitet. + +Zur Verarbeitung werden alle Nutzerinteraktionen und Angaben zunächst in den zuständigen Prüfer übergeben. Wenn vorhanden, mit den erforderlichen Parametern. Das Ergebnis wird zur Auswertung an den Pruefmanager übergeben. Dieser bereitet das Ergebnis auf, behandelt alle Exceptions und Anwenderentscheidungen und gibt die Daten mit den richtigen Parametern zur Weiterverarbeitung an die eigentliche Funktion. + + diff --git a/assets/Stilpruefer_flowchart.svg b/assets/Stilpruefer_flowchart.svg new file mode 100644 index 0000000..dafc0d0 --- /dev/null +++ b/assets/Stilpruefer_flowchart.svg @@ -0,0 +1 @@ +NeinJaNeinJaNeinJaInput StilpfadStilpfad vorhanden?Ergebnis: Erfolg, Stil=None, Warnung 'Kein Stil angegeben'Datei existiert?Ergebnis: Fehler 'Stildatei nicht gefunden'Endet mit .qml?Ergebnis: Fehler 'Ungültige Dateiendung'Ergebnis: Erfolg, Stil=pfad \ No newline at end of file diff --git a/assets/UML_Struktur.png b/assets/UML_Struktur.png new file mode 100644 index 0000000..70fea30 Binary files /dev/null and b/assets/UML_Struktur.png differ diff --git a/modules/Dateipruefer.py b/modules/Dateipruefer.py new file mode 100644 index 0000000..166eccc --- /dev/null +++ b/modules/Dateipruefer.py @@ -0,0 +1,97 @@ +import os +from enum import Enum, auto + + +# ------------------------------- +# 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 + + # 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 + ) + + # 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."] + ) + + return DateipruefErgebnis( + erfolgreich=True, + 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 + ) diff --git a/modules/Pruefmanager.py b/modules/Pruefmanager.py new file mode 100644 index 0000000..e3b97ce --- /dev/null +++ b/modules/Pruefmanager.py @@ -0,0 +1,51 @@ +from PyQt5.QtWidgets import QMessageBox, QFileDialog +from Dateipruefer import DateiEntscheidung + +class PruefManager: + + def __init__(self, iface=None, plugin_pfad=None): + self.iface = iface + self.plugin_pfad = plugin_pfad + + def frage_datei_ersetzen_oder_anhaengen(self, pfad: str) -> DateiEntscheidung: + """Fragt den Nutzer, ob die vorhandene Datei ersetzt, angehängt oder abgebrochen werden soll.""" + msg = QMessageBox() + msg.setIcon(QMessageBox.Question) + msg.setWindowTitle("Datei existiert") + msg.setText(f"Die Datei '{pfad}' existiert bereits.\nWas möchtest du tun?") + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) + msg.setDefaultButton(QMessageBox.Yes) + msg.button(QMessageBox.Yes).setText("Ersetzen") + msg.button(QMessageBox.No).setText("Anhängen") + msg.button(QMessageBox.Cancel).setText("Abbrechen") + + result = msg.exec_() + + if result == QMessageBox.Yes: + return DateiEntscheidung.ERSETZEN + elif result == QMessageBox.No: + return DateiEntscheidung.ANHAENGEN + else: + return DateiEntscheidung.ABBRECHEN + + def frage_temporär_verwenden(self) -> bool: + """Fragt den Nutzer, ob mit temporären Layern gearbeitet werden soll.""" + msg = QMessageBox() + msg.setIcon(QMessageBox.Question) + msg.setWindowTitle("Temporäre Layer") + msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?") + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msg.setDefaultButton(QMessageBox.Yes) + + result = msg.exec_() + return result == QMessageBox.Yes + + def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str: + """Öffnet einen QFileDialog zur Dateiauswahl.""" + pfad, _ = QFileDialog.getSaveFileName( + parent=None, + caption=titel, + directory=self.plugin_pfad or "", + filter=filter + ) + return pfad diff --git a/modules/__init__py b/modules/__init__py new file mode 100644 index 0000000..e69de29 diff --git a/modules/linkpruefer.py b/modules/linkpruefer.py new file mode 100644 index 0000000..b840a25 --- /dev/null +++ b/modules/linkpruefer.py @@ -0,0 +1,94 @@ +# Importiert den Event-Loop und URL-Objekte aus der PyQt-Bibliothek von QGIS +from qgis.PyQt.QtCore import QEventLoop, QUrl +# Importiert den NetworkAccessManager aus dem QGIS Core-Modul +from qgis.core import QgsNetworkAccessManager +# Importiert das QNetworkRequest-Objekt für HTTP-Anfragen +from qgis.PyQt.QtNetwork import QNetworkRequest +# Importiert die Klasse für das Ergebnisobjekt der Prüfung +from pruef_ergebnis import PruefErgebnis + +# Definiert die Klasse zum Prüfen von Links +class Linkpruefer: + """Prüft den Link mit QgsNetworkAccessManager und klassifiziert Anbieter nach Attribut.""" + + # Statische Zuordnung möglicher Anbietertypen als Konstanten + ANBIETER_TYPEN: dict[str, str] = { + "REST": "REST", + "WFS": "WFS", + "WMS": "WMS", + "OGR": "OGR" + } + + # Konstruktor zum Initialisieren der Instanz + def __init__(self, link: str, anbieter: str): + # Speichert den übergebenen Link als Instanzvariable + self.link = link + + # Speichert den Anbietertyp, bereinigt und in Großbuchstaben (auch wenn leer oder None) + self.anbieter = anbieter.upper().strip() if anbieter else "" + # Erstellt einen neuen NetworkAccessManager für Netzwerkverbindungen + self.network_manager = QgsNetworkAccessManager() + + # Methode zur Klassifizierung des Anbieters und der Quelle + def klassifiziere_anbieter(self): + # Bestimmt den Typ auf Basis der vorgegebenen Konstante oder nimmt den Rohwert + typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter) + # Unterscheidet zwischen "remote" (http/https) oder "local" (Dateipfad) + quelle = "remote" if self.link.startswith(("http://", "https://")) else "local" + # Gibt Typ und Quelle als Dictionary zurück + return { + "typ": typ, + "quelle": quelle + } + + + # Prüft die Erreichbarkeit und Plausibilität des Links + def pruefe_link(self): + # Initialisiert Listen für Fehler und Warnungen + fehler = [] + warnungen = [] + + # Prüft, ob ein Link übergeben wurde + if not self.link: + fehler.append("Link fehlt.") + return PruefErgebnis(False, fehler=fehler, warnungen=warnungen) + + # Prüft, ob ein Anbieter angegeben ist + if not self.anbieter or not self.anbieter.strip(): + fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.") + + # Prüfung für Remote-Links (http/https) + if self.link.startswith(("http://", "https://")): + # Erstellt eine HTTP-Anfrage mit dem Link + request = QNetworkRequest(QUrl(self.link)) + # Startet eine HEAD-Anfrage über den NetworkManager + reply = self.network_manager.head(request) + + # Wartet synchron auf die Netzwerkanwort (Event Loop) + loop = QEventLoop() + reply.finished.connect(loop.quit) + loop.exec_() + + # Prüft auf Netzwerkfehler + if reply.error(): + fehler.append(f"Verbindungsfehler: {reply.errorString()}") + else: + # Holt den HTTP-Statuscode aus der Antwort + status = reply.attribute(reply.HttpStatusCodeAttribute) + # Prüft, ob der Status außerhalb des Erfolgsbereichs liegt + if status is None or status < 200 or status >= 400: + fehler.append(f"Link nicht erreichbar: HTTP {status}") + # Räumt die Antwort auf (Vermeidung von Speicherlecks) + reply.deleteLater() + else: + # Plausibilitäts-Check für lokale Links (Dateien), prüft auf Dateiendung + if "." not in self.link.split("/")[-1]: + warnungen.append("Der lokale Link sieht ungewöhnlich aus.") + + # Gibt das Ergebnisobjekt mit allen gesammelten Informationen zurück + return PruefErgebnis(len(fehler) == 0, daten=self.klassifiziere_anbieter(), fehler=fehler, warnungen=warnungen) + + # Führt die Linkprüfung als externe Methode aus + def ausfuehren(self): + # Gibt das Ergebnis der Prüf-Methode zurück + return self.pruefe_link() diff --git a/modules/pruef_ergebnis b/modules/pruef_ergebnis new file mode 100644 index 0000000..4f9b719 --- /dev/null +++ b/modules/pruef_ergebnis @@ -0,0 +1,11 @@ +# 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 [] + + def __repr__(self): + return (f"PruefErgebnis(erfolgreich={self.erfolgreich}, " + f"daten={self.daten}, fehler={self.fehler}, warnungen={self.warnungen})") diff --git a/modules/pruef_ergebnis.py b/modules/pruef_ergebnis.py new file mode 100644 index 0000000..4f9b719 --- /dev/null +++ b/modules/pruef_ergebnis.py @@ -0,0 +1,11 @@ +# 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 [] + + def __repr__(self): + return (f"PruefErgebnis(erfolgreich={self.erfolgreich}, " + f"daten={self.daten}, fehler={self.fehler}, warnungen={self.warnungen})") diff --git a/modules/stilpruefer.py b/modules/stilpruefer.py new file mode 100644 index 0000000..1ac65b1 --- /dev/null +++ b/modules/stilpruefer.py @@ -0,0 +1,45 @@ +import os +from pruef_ergebnis import PruefErgebnis + + +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 + """ + + 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."] + ) + + 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 + ) diff --git a/styles/GIS_63000F_Objekt_Denkmalschutz.qml b/styles/GIS_63000F_Objekt_Denkmalschutz.qml new file mode 100644 index 0000000..06bb9e5 --- /dev/null +++ b/styles/GIS_63000F_Objekt_Denkmalschutz.qml @@ -0,0 +1,609 @@ + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "gml_id" + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "gml_id" + + 2 + diff --git a/styles/GIS_Biotope_F.qml b/styles/GIS_Biotope_F.qml new file mode 100644 index 0000000..ed06272 --- /dev/null +++ b/styles/GIS_Biotope_F.qml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml b/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml new file mode 100644 index 0000000..5e40734 --- /dev/null +++ b/styles/GIS_Flst_Beschriftung_ALKIS_NAS.qml @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/styles/GIS_LfULG_LSG.qml b/styles/GIS_LfULG_LSG.qml new file mode 100644 index 0000000..28082ba --- /dev/null +++ b/styles/GIS_LfULG_LSG.qml @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/styles/verfahrensgebiet.qml b/styles/verfahrensgebiet.qml index 5504107..474e368 100644 --- a/styles/verfahrensgebiet.qml +++ b/styles/verfahrensgebiet.qml @@ -1,25 +1,83 @@ - - - 1 - 1 - 1 - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -32,285 +90,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 1 - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . - - 0 - . - - 0 - generatedlayout - - - - - - - - - - - - - - - - - - - - - - COALESCE( "name", '<NULL>' ) - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 2 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..324c4b2 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +#Testordner \ No newline at end of file diff --git a/test/run_tests.py b/test/run_tests.py new file mode 100644 index 0000000..6f94a3a --- /dev/null +++ b/test/run_tests.py @@ -0,0 +1,29 @@ +import sys +import os +import unittest + +# Projekt-Root dem Suchpfad hinzufügen +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if project_root not in sys.path: + sys.path.insert(0, project_root) + +def main(): + loader = unittest.TestLoader() + suite = unittest.TestSuite() + + test_modules = [ + "test_dateipruefer", + "test_stilpruefer", + "test_linkpruefer", + # "test_pruefmanager" enthält QGIS-spezifische Funktionen + ] + + for mod_name in test_modules: + mod = __import__(mod_name) + suite.addTests(loader.loadTestsFromModule(mod)) + + runner = unittest.TextTestRunner(verbosity=2) + runner.run(suite) + +if __name__ == "__main__": + main() diff --git a/test/start_osgeo4w_qgis.bat b/test/start_osgeo4w_qgis.bat new file mode 100644 index 0000000..a4b0c23 --- /dev/null +++ b/test/start_osgeo4w_qgis.bat @@ -0,0 +1,9 @@ +@echo off +SET OSGEO4W_ROOT=D:\QGISQT5 +call %OSGEO4W_ROOT%\bin\o4w_env.bat +set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis +set PYTHONPATH=%QGIS_PREFIX_PATH%\python;%PYTHONPATH% +set PATH=%OSGEO4W_ROOT%\bin;%QGIS_PREFIX_PATH%\bin;%PATH% + +REM Neue Eingabeaufforderung starten und Python-Skript ausführen +start cmd /k "python run_tests.py" diff --git a/test/test_dateipruefer.py b/test/test_dateipruefer.py new file mode 100644 index 0000000..6f8ff7d --- /dev/null +++ b/test/test_dateipruefer.py @@ -0,0 +1,88 @@ +import unittest +import os +import tempfile +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from Dateipruefer import ( + Dateipruefer, + LeererPfadModus, + DateiEntscheidung, + DateipruefErgebnis +) + + +class TestDateipruefer(unittest.TestCase): + def setUp(self): + self.pruefer = Dateipruefer() + self.plugin_pfad = tempfile.gettempdir() + self.standardname = "test_standard.gpkg" + + def test_verbotener_leerer_pfad(self): + result = self.pruefer.pruefe( + pfad="", + leer_modus=LeererPfadModus.VERBOTEN + ) + self.assertFalse(result.erfolgreich) + self.assertIn("Kein Pfad angegeben.", result.fehler) + + def test_standardpfad_wird_verwendet(self): + result = self.pruefer.pruefe( + pfad="", + leer_modus=LeererPfadModus.NUTZE_STANDARD, + standardname=self.standardname, + plugin_pfad=self.plugin_pfad + ) + self.assertTrue(result.erfolgreich) + expected_path = os.path.join(self.plugin_pfad, self.standardname) + self.assertEqual(result.pfad, expected_path) + + def test_temporärer_layer_wird_erkannt(self): + result = self.pruefer.pruefe( + pfad="", + leer_modus=LeererPfadModus.TEMPORAER_ERLAUBT + ) + self.assertTrue(result.erfolgreich) + self.assertIsNone(result.pfad) + self.assertFalse(result.temporär) + + def test_existierende_datei_ohne_entscheidung(self): + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_path = tmp_file.name + try: + result = self.pruefer.pruefe( + pfad=tmp_path, + leer_modus=LeererPfadModus.VERBOTEN + ) + self.assertTrue(result.erfolgreich) # neu: jetzt True, nicht False + self.assertIn("Datei existiert bereits – Entscheidung ausstehend.", result.fehler) + self.assertIsNone(result.entscheidung) + finally: + os.remove(tmp_path) + + def test_existierende_datei_mit_entscheidung_ersetzen(self): + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_path = tmp_file.name + try: + result = self.pruefer.pruefe( + pfad=tmp_path, + leer_modus=LeererPfadModus.VERBOTEN, + vorhandene_datei_entscheidung=DateiEntscheidung.ERSETZEN + ) + self.assertTrue(result.erfolgreich) + self.assertEqual(result.entscheidung, DateiEntscheidung.ERSETZEN) + finally: + os.remove(tmp_path) + + def test_datei_nicht_existiert(self): + fake_path = os.path.join(self.plugin_pfad, "nicht_existierend.gpkg") + result = self.pruefer.pruefe( + pfad=fake_path, + leer_modus=LeererPfadModus.VERBOTEN + ) + self.assertTrue(result.erfolgreich) + self.assertEqual(result.pfad, fake_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_linkpruefer.py b/test/test_linkpruefer.py new file mode 100644 index 0000000..35baeb3 --- /dev/null +++ b/test/test_linkpruefer.py @@ -0,0 +1,125 @@ +# test/test_linkpruefer.py + +import unittest +import sys +from unittest.mock import patch +from qgis.PyQt.QtCore import QCoreApplication, QTimer +from qgis.PyQt.QtNetwork import QNetworkRequest + +from linkpruefer import Linkpruefer + +# Stelle sicher, dass eine Qt-App existiert +app = QCoreApplication.instance() +if app is None: + app = QCoreApplication(sys.argv) + + +class DummyReply: + """Fake-Reply, um Netzwerkabfragen zu simulieren""" + HttpStatusCodeAttribute = QNetworkRequest.HttpStatusCodeAttribute + + def __init__(self, error, status_code): + self._error = error + self._status_code = status_code + self.finished = self # Fake-Signal + + def connect(self, slot): + # sorgt dafür, dass loop.quit() nach Start von loop.exec_() ausgeführt wird + QTimer.singleShot(0, slot) + + def error(self): + return self._error + + def errorString(self): + return "Simulierter Fehler" if self._error != 0 else "" + + def attribute(self, attr): + if attr == self.HttpStatusCodeAttribute: + return self._status_code + return None + + def deleteLater(self): + # kein echtes QObject → wir tun einfach nichts + pass + + +class TestLinkpruefer(unittest.TestCase): + """Tests für alle Funktionen des Linkpruefer""" + + # ---------------------------- + # Remote-Tests mit DummyReply + # ---------------------------- + @patch('linkpruefer.QgsNetworkAccessManager.head') + def test_remote_link_success(self, mock_head): + mock_head.return_value = DummyReply(0, 200) + + checker = Linkpruefer("https://example.com/service", "REST") + result = checker.ausfuehren() + + self.assertTrue(result.erfolgreich) + self.assertEqual(result.daten['typ'], 'REST') + self.assertEqual(result.daten['quelle'], 'remote') + self.assertEqual(result.fehler, []) + self.assertEqual(result.warnungen, []) + + @patch('linkpruefer.QgsNetworkAccessManager.head') + def test_remote_link_failure(self, mock_head): + mock_head.return_value = DummyReply(1, 404) + + checker = Linkpruefer("https://example.com/kaputt", "WMS") + result = checker.ausfuehren() + + self.assertFalse(result.erfolgreich) + self.assertIn("Verbindungsfehler: Simulierter Fehler", result.fehler) + + # ---------------------------- + # Klassifizierungstests + # ---------------------------- + def test_klassifiziere_anbieter_remote(self): + checker = Linkpruefer("https://beispiel.de", "wms") + daten = checker.klassifiziere_anbieter() + self.assertEqual(daten["typ"], "WMS") + self.assertEqual(daten["quelle"], "remote") + + def test_klassifiziere_anbieter_local(self): + checker = Linkpruefer("C:/daten/test.shp", "ogr") + daten = checker.klassifiziere_anbieter() + self.assertEqual(daten["typ"], "OGR") + self.assertEqual(daten["quelle"], "local") + + # ---------------------------- + # Lokale Links + # ---------------------------- + def test_pruefe_link_local_ok(self): + checker = Linkpruefer("C:/daten/test.shp", "OGR") + result = checker.pruefe_link() + self.assertTrue(result.erfolgreich) + self.assertEqual(result.warnungen, []) + + def test_pruefe_link_local_warnung(self): + checker = Linkpruefer("C:/daten/ordner/", "OGR") + result = checker.pruefe_link() + self.assertTrue(result.erfolgreich) + self.assertIn("ungewöhnlich", result.warnungen[0]) + + # ---------------------------- + # Sonderfall: leerer Link + # ---------------------------- + def test_pruefe_link_empty(self): + checker = Linkpruefer("", "REST") + result = checker.pruefe_link() + self.assertFalse(result.erfolgreich) + self.assertIn("Link fehlt.", result.fehler) + + # ---------------------------- + # leerer Anbieter + # ---------------------------- + def test_pruefe_link_leerer_anbieter(self): + checker = Linkpruefer("https://example.com/service", "") + result = checker.pruefe_link() + self.assertFalse(result.erfolgreich) + self.assertIn("Anbieter muss gesetzt werden und darf nicht leer sein.", result.fehler) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_pruefmanager.py b/test/test_pruefmanager.py new file mode 100644 index 0000000..a33d4e5 --- /dev/null +++ b/test/test_pruefmanager.py @@ -0,0 +1,36 @@ +import unittest +import os +from unittest.mock import patch +from pruefmanager import PruefManager +from Dateipruefer import DateiEntscheidung +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +class TestPruefManager(unittest.TestCase): + + def setUp(self): + self.manager = PruefManager(plugin_pfad="/tmp") + + @patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.Yes) + def test_frage_datei_ersetzen(self, mock_msgbox): + entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg") + self.assertEqual(entscheidung, DateiEntscheidung.ERSETZEN) + + @patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.No) + def test_frage_datei_anhaengen(self, mock_msgbox): + entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg") + self.assertEqual(entscheidung, DateiEntscheidung.ANHAENGEN) + + @patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.Cancel) + def test_frage_datei_abbrechen(self, mock_msgbox): + entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg") + self.assertEqual(entscheidung, DateiEntscheidung.ABBRECHEN) + + @patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.Yes) + def test_frage_temporär_verwenden_ja(self, mock_msgbox): + self.assertTrue(self.manager.frage_temporär_verwenden()) + + @patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.No) + def test_frage_temporär_verwenden_nein(self, mock_msgbox): + self.assertFalse(self.manager.frage_temporär_verwenden()) diff --git a/test/test_stilpruefer.py b/test/test_stilpruefer.py new file mode 100644 index 0000000..ea37db7 --- /dev/null +++ b/test/test_stilpruefer.py @@ -0,0 +1,47 @@ +import unittest +import tempfile +import os +from stilpruefer import Stilpruefer +from pruef_ergebnis import PruefErgebnis + + +class TestStilpruefer(unittest.TestCase): + def setUp(self): + self.pruefer = Stilpruefer() + + def test_keine_datei_angegeben(self): + result = self.pruefer.pruefe("") + self.assertTrue(result.erfolgreich) + self.assertIn("Kein Stil angegeben.", result.warnungen) + self.assertIsNone(result.daten["stil"]) + + def test_datei_existiert_mit_qml(self): + with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp_file: + tmp_path = tmp_file.name + try: + result = self.pruefer.pruefe(tmp_path) + self.assertTrue(result.erfolgreich) + self.assertEqual(result.daten["stil"], tmp_path) + self.assertEqual(result.fehler, []) + finally: + os.remove(tmp_path) + + def test_datei_existiert_falsche_endung(self): + with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp_file: + tmp_path = tmp_file.name + try: + result = self.pruefer.pruefe(tmp_path) + self.assertFalse(result.erfolgreich) + self.assertIn("Ungültige Dateiendung", result.fehler[0]) + finally: + os.remove(tmp_path) + + def test_datei_existiert_nicht(self): + fake_path = os.path.join(tempfile.gettempdir(), "nichtvorhanden.qml") + result = self.pruefer.pruefe(fake_path) + self.assertFalse(result.erfolgreich) + self.assertIn("Stildatei nicht gefunden", result.fehler[0]) + + +if __name__ == "__main__": + unittest.main()
Ja
VERBOTEN
NUTZE_STANDARDohne plugin_pfad/standard
NUTZE_STANDARDmit plugin_pfad+standard
TEMPORAER_ERLAUBT
Nein
Ja=ABBRECHEN
Ja=ERSETZEN/ANHAENGEN
Start: Eingabe prüfen
Pfad leer?
leer_modus
Return: erfolgreich=FalseFehler: 'Kein Pfad angegeben'
Return: erfolgreich=FalseFehler: 'Standardpfad/-name fehlen'
Setze Pfad=plugin_pfad+standard
Return: erfolgreich=Truetemporär=True
Datei existiert?
vorhandene_datei_entscheidung gesetzt?
Return: erfolgreich=Trueentscheidung=NoneFehler: 'Datei existiert bereits – Entscheidung ausstehend'
Return: erfolgreich=FalseFehler: 'Benutzer hat abgebrochen'
Return: erfolgreich=Trueentscheidung=...
Return: erfolgreich=Truepfad=...
Start Linkprüfung
Ist Link vorhanden?
Fehler: Link fehlt
Prüfergebnis: Fehler zurückgeben
Ist Link Remote http/https?
HEAD-Anfrage mit QgsNetworkAccessManager
Antwort erhalten?
Fehler: Verbindungsfehler
HTTP-Statuscode < 200 oder ≥ 400?
Fehler: Link nicht erreichbar
Anbieter klassifizieren
Prüfergebnis zurückgeben
Plausibilitätscheck für lokalen Link
Link sieht ungewöhnlich aus?
Warnung ausgeben