Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91bafe632f | ||
|
|
7ed22fb23e | ||
|
|
3bbb66ec32 | ||
|
|
9258f03705 | ||
|
|
9862386646 | ||
|
|
706de43512 | ||
|
|
68ce088192 | ||
|
|
9dc15b5b92 | ||
|
|
319d425670 | ||
|
|
970343cd98 | ||
|
|
3d9bfdb316 | ||
|
|
15e48f58af | ||
|
|
933b3f9cb6 | ||
|
|
f6b62395dd | ||
|
|
025a5f5e13 | ||
|
|
e196a4896f | ||
|
|
63a2e8d63a | ||
|
|
79b827c009 | ||
|
|
0e695ba595 | ||
|
|
e11f84e89b | ||
|
|
171ebcbe4f | ||
|
|
b7dfac68f1 | ||
|
|
62e9bcee1e | ||
|
|
e9d9a2294d | ||
|
|
c8fde119b4 | ||
|
|
692f690fea | ||
|
|
94b957bb73 | ||
|
|
00023c632f | ||
|
|
fba89478a0 | ||
|
|
3c1d8fc4e5 | ||
|
|
44eee228a8 | ||
|
|
34e2ff4f66 | ||
|
|
91669b34d6 | ||
|
|
d2035139d9 | ||
|
|
30bfcc597d | ||
|
|
88761eab22 | ||
|
|
d762f2d94f | ||
|
|
9f7e81e8b2 | ||
|
|
943aab7509 | ||
|
|
5b4cbc3127 | ||
|
|
f0944aab99 |
@@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version=
|
||||
name=LNO Sachsen | Plugin Basisfunktionen
|
||||
description=Plugin mit Basisfunktionen
|
||||
author=Daniel Helbig, Michael Otto
|
||||
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
|
||||
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
|
||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/unstable/
|
||||
QGISMaximumVersion=4.99
|
||||
qgisMinimumVersion=3.40
|
||||
experimental=true
|
||||
deprecated=false
|
||||
supportsQt6=true
|
||||
+16
@@ -1,3 +1,19 @@
|
||||
"""
|
||||
sn_basis – QGIS-Plugin für Basis-Verwaltung und zentrale Koordination.
|
||||
Orchestriert die Navigation und initialisiert abhängige sn_-Plugins.
|
||||
"""
|
||||
|
||||
|
||||
def classFactory(iface):
|
||||
"""
|
||||
Factory-Funktion für QGIS.
|
||||
Wird von QGIS aufgerufen, um die Plugin-Instanz zu erstellen.
|
||||
|
||||
Args:
|
||||
iface: QGIS-Schnittstellenobjekt
|
||||
|
||||
Returns:
|
||||
BasisPlugin: Instanziiertes Plugin
|
||||
"""
|
||||
from .main import BasisPlugin
|
||||
return BasisPlugin(iface)
|
||||
|
||||
+22
-1
@@ -1,5 +1,26 @@
|
||||
Version 26.7.1-unstable:
|
||||
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
|
||||
---
|
||||
Version 26.4.2-testing:
|
||||
Version 26.7.1-testing:
|
||||
- VLN-API zum Laden der Plan41-Maßnahmen integriert
|
||||
---
|
||||
Version 26.5.2-testing:
|
||||
---
|
||||
Version 26.5.1-testing:
|
||||
---
|
||||
Version 26.5.1-unstable:
|
||||
---
|
||||
Version 26.4.8-unstable:
|
||||
---
|
||||
Version 26.4.7-unstable:
|
||||
---
|
||||
Version 26.4.6-unstable:
|
||||
---
|
||||
Version 26.4.5-unstable:
|
||||
---
|
||||
Version 26.4.4-unstable:
|
||||
---
|
||||
Version 26.4.3-unstable:
|
||||
---
|
||||
Version 26.04.1-unstable:
|
||||
---
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
sn_basis/functions – Wrapper und Hilfsfunktionen für QGIS-Kompatibilität.
|
||||
Bietet abstrahierte Schnittstellen für Qt, QGIS und OS-Operationen.
|
||||
"""
|
||||
|
||||
from .ly_existence_wrapper import layer_exists
|
||||
from .ly_geometry_wrapper import (
|
||||
get_layer_geometry_type,
|
||||
@@ -15,7 +20,7 @@ from .ly_metadata_wrapper import (
|
||||
is_layer_editable,
|
||||
)
|
||||
from .ly_style_wrapper import apply_style
|
||||
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom
|
||||
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden
|
||||
|
||||
from .message_wrapper import (
|
||||
_get_message_bar,
|
||||
|
||||
@@ -128,6 +128,156 @@ def ask_overwrite_append_cancel_custom(
|
||||
return "cancel"
|
||||
|
||||
|
||||
def ask_ergaenzen_entfernen_ersetzen_abbrechen(
|
||||
parent: Any,
|
||||
title: str,
|
||||
message: str,
|
||||
) -> Literal["ergaenzen", "entfernen", "ersetzen", "abbrechen"]:
|
||||
"""Zeigt Dialog mit vier Optionen für Verfahrensgebiet-Editoperationen.
|
||||
|
||||
Optionen:
|
||||
- **Ergänzen**: ausgewählte Flurstücke zum Verfahrensgebiet hinzufügen
|
||||
- **Entfernen**: ausgewählte Flurstücke aus dem Verfahrensgebiet entfernen
|
||||
- **Ersetzen**: Verfahrensgebiet vollständig durch Auswahl ersetzen
|
||||
- **Abbrechen**: Vorgang beenden ohne Änderung
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
title : str
|
||||
Dialog-Titel.
|
||||
message : str
|
||||
Hauptmeldung mit Erklärung.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Literal["ergaenzen", "entfernen", "ersetzen", "abbrechen"]
|
||||
Genaue Entscheidung des Nutzers.
|
||||
"""
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_ergaenzen_entfernen_ersetzen_abbrechen('{title}') → abbrechen")
|
||||
return "abbrechen"
|
||||
|
||||
msg = QMessageBox(parent)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(message)
|
||||
|
||||
ergaenzen_btn = msg.addButton("Ergänzen", QMessageBox.ButtonRole.ActionRole)
|
||||
entfernen_btn = msg.addButton("Entfernen", QMessageBox.ButtonRole.ActionRole)
|
||||
ersetzen_btn = msg.addButton("Ersetzen", QMessageBox.ButtonRole.AcceptRole)
|
||||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
exec_dialog(msg)
|
||||
|
||||
clicked = msg.clickedButton()
|
||||
if clicked == ergaenzen_btn:
|
||||
return "ergaenzen"
|
||||
elif clicked == entfernen_btn:
|
||||
return "entfernen"
|
||||
elif clicked == ersetzen_btn:
|
||||
return "ersetzen"
|
||||
else:
|
||||
return "abbrechen"
|
||||
|
||||
|
||||
def ask_detailpruefung_oder_vg_laden(
|
||||
parent: Any,
|
||||
title: str,
|
||||
message: str,
|
||||
) -> Literal["detailpruefung", "als_vg_laden", "abbrechen"]:
|
||||
"""Zeigt Dialog mit drei Optionen nach erkannter Flächenabweichung.
|
||||
|
||||
Optionen:
|
||||
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des BROR-Umrings
|
||||
- **Bodenordnungsobjekt als VG laden**: BROR direkt als Verfahrensgebiet übernehmen
|
||||
- **Abbrechen**: Vorgang beenden ohne weitere Aktion
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
title : str
|
||||
Dialog-Titel.
|
||||
message : str
|
||||
Hauptmeldung mit Flächen-Informationen.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Literal["detailpruefung", "als_vg_laden", "abbrechen"]
|
||||
Genaue Entscheidung des Nutzers.
|
||||
"""
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_detailpruefung_oder_vg_laden('{title}') → abbrechen")
|
||||
return "abbrechen"
|
||||
|
||||
msg = QMessageBox(parent)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(message)
|
||||
|
||||
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
|
||||
vg_btn = msg.addButton("Bodenordnungsobjekt als Verfahrensgebiet laden", QMessageBox.ButtonRole.ActionRole)
|
||||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
exec_dialog(msg)
|
||||
|
||||
clicked = msg.clickedButton()
|
||||
if clicked == detail_btn:
|
||||
return "detailpruefung"
|
||||
elif clicked == vg_btn:
|
||||
return "als_vg_laden"
|
||||
else:
|
||||
return "abbrechen"
|
||||
|
||||
|
||||
def ask_detailpruefung_oder_abbrechen(
|
||||
parent: Any,
|
||||
title: str,
|
||||
message: str,
|
||||
) -> Literal["detailpruefung", "abbrechen"]:
|
||||
"""Zeigt Dialog mit zwei Optionen nach erkannter Flächenabweichung (Shape-Workflow).
|
||||
|
||||
Optionen:
|
||||
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des Shape-VG-Umrings
|
||||
- **Abbrechen**: Vorgang beenden, Shape-Verfahrensgebiet unverändert behalten
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
title : str
|
||||
Dialog-Titel.
|
||||
message : str
|
||||
Hauptmeldung mit Flächen-Informationen.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Literal["detailpruefung", "abbrechen"]
|
||||
Genaue Entscheidung des Nutzers.
|
||||
"""
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_detailpruefung_oder_abbrechen('{title}') → abbrechen")
|
||||
return "abbrechen"
|
||||
|
||||
msg = QMessageBox(parent)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(message)
|
||||
|
||||
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
|
||||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
exec_dialog(msg)
|
||||
|
||||
clicked = msg.clickedButton()
|
||||
if clicked == detail_btn:
|
||||
return "detailpruefung"
|
||||
else:
|
||||
return "abbrechen"
|
||||
|
||||
|
||||
class ProgressDialog:
|
||||
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||||
self.total = max(total, 1)
|
||||
@@ -220,7 +370,73 @@ class ProgressDialog:
|
||||
if self._dlg is not None:
|
||||
self._dlg.close()
|
||||
|
||||
def raise_to_front(self) -> None:
|
||||
"""Bringt den Fortschrittsdialog in den Vordergrund.
|
||||
|
||||
Wird aufgerufen, nachdem ein modaler Subdialog (z. B. Auswahlbox)
|
||||
geschlossen wurde und der Fortschrittsdialog wieder sichtbar sein soll.
|
||||
"""
|
||||
if QT_VERSION == 0:
|
||||
return
|
||||
if self._dlg is not None:
|
||||
try:
|
||||
self._dlg.raise_()
|
||||
self._dlg.activateWindow()
|
||||
QCoreApplication.processEvents()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
||||
return ProgressDialog(total, title, label)
|
||||
|
||||
|
||||
def ask_bror_kombination(
|
||||
parent: Any,
|
||||
kombinationen: list[tuple[str, str]],
|
||||
) -> Optional[tuple[str, str]]:
|
||||
"""Zeigt Dialog zur Auswahl einer BROR-Kombination (adv_name + bezeichnung).
|
||||
|
||||
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
|
||||
``adv_name`` und ``bezeichnung`` zurückliefert, sodass der Nutzer
|
||||
entscheiden muss, welche Objekte in die Pipeline einfließen sollen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
kombinationen :
|
||||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[tuple[str, str]]
|
||||
Das gewählte ``(adv_name, bezeichnung)``-Tupel oder ``None`` bei
|
||||
Abbruch bzw. leerer Eingabe.
|
||||
"""
|
||||
if not kombinationen:
|
||||
return None
|
||||
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_bror_kombination → {kombinationen[0]}")
|
||||
return kombinationen[0]
|
||||
|
||||
items = [f"{adv} | {bez}" for adv, bez in kombinationen]
|
||||
item, ok = QInputDialog.getItem(
|
||||
parent,
|
||||
"BROR-Objekt auswählen",
|
||||
"Mehrere BauRaumOderBodenordnungsrecht-Objekte gefunden.\n"
|
||||
"Bitte wählen Sie die gewünschte Kombination\n"
|
||||
"aus ADV-Name und Bezeichnung:",
|
||||
items,
|
||||
0,
|
||||
False,
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
try:
|
||||
idx = items.index(item)
|
||||
return kombinationen[idx]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -5,6 +5,25 @@ from sn_basis.functions.sys_wrapper import get_plugin_root, join_path
|
||||
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||
from typing import Optional
|
||||
|
||||
def apply_style_from_path(layer, style_path: str) -> bool:
|
||||
"""
|
||||
Wendet einen Layerstil anhand eines vollständigen Pfades an.
|
||||
|
||||
Im Gegensatz zu :func:`apply_style` wird der Pfad extern bereitgestellt;
|
||||
es findet keine eigene Stilprüfung statt.
|
||||
"""
|
||||
if not layer_exists(layer):
|
||||
return False
|
||||
try:
|
||||
ok, _ = layer.loadNamedStyle(str(style_path))
|
||||
if ok:
|
||||
getattr(layer, "triggerRepaint", lambda: None)()
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def apply_style(layer, style_name: str) -> bool:
|
||||
"""
|
||||
Wendet einen Layerstil an, sofern er gültig ist.
|
||||
|
||||
@@ -24,6 +24,7 @@ QgsFeature: Type[Any]
|
||||
QgsField: Type[Any]
|
||||
QgsGeometry: Type[Any]
|
||||
QgsFeatureRequest: Type[Any]
|
||||
QgsRectangle: Type[Any]
|
||||
QgsCoordinateTransform: Type[Any]
|
||||
QgsCoordinateReferenceSystem: Type[Any]
|
||||
QgsPrintLayout: Type[Any]
|
||||
@@ -37,6 +38,12 @@ QgsLayoutPoint: Type[Any]
|
||||
QgsLayoutSize: Type[Any]
|
||||
QgsUnitTypes: Type[Any]
|
||||
QgsLayoutItem: Type[Any]
|
||||
QgsBlockingNetworkRequest: Type[Any]
|
||||
QgsJsonExporter: Type[Any]
|
||||
QgsJsonUtils: Type[Any]
|
||||
GEOM_POINT: Any = None
|
||||
GEOM_LINE: Any = None
|
||||
GEOM_POLYGON: Any = None
|
||||
|
||||
MAP_GRID_STYLE_MARKERS: Any = None
|
||||
MAP_GRID_STYLE_FRAME_ANNOTATIONS: Any = None
|
||||
@@ -76,6 +83,7 @@ try:
|
||||
QgsField as _QgsField,
|
||||
QgsGeometry as _QgsGeometry,
|
||||
QgsFeatureRequest as _QgsFeatureRequest,
|
||||
QgsRectangle as _QgsRectangle,
|
||||
QgsCoordinateTransform as _QgsCoordinateTransform,
|
||||
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
|
||||
QgsPrintLayout as _QgsPrintLayout,
|
||||
@@ -89,6 +97,9 @@ try:
|
||||
QgsLayoutSize as _QgsLayoutSize,
|
||||
QgsUnitTypes as _QgsUnitTypes,
|
||||
QgsLayoutItem as _QgsLayoutItem,
|
||||
QgsBlockingNetworkRequest as _QgsBlockingNetworkRequest,
|
||||
QgsJsonExporter as _QgsJsonExporter,
|
||||
QgsJsonUtils as _QgsJsonUtils,
|
||||
)
|
||||
|
||||
QgsProject = _QgsProject
|
||||
@@ -102,6 +113,7 @@ try:
|
||||
QgsField = _QgsField
|
||||
QgsGeometry = _QgsGeometry
|
||||
QgsFeatureRequest = _QgsFeatureRequest
|
||||
QgsRectangle = _QgsRectangle
|
||||
QgsCoordinateTransform = _QgsCoordinateTransform
|
||||
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
|
||||
QgsPrintLayout = _QgsPrintLayout
|
||||
@@ -115,6 +127,32 @@ try:
|
||||
QgsLayoutSize = _QgsLayoutSize
|
||||
QgsUnitTypes = _QgsUnitTypes
|
||||
QgsLayoutItem = _QgsLayoutItem
|
||||
QgsBlockingNetworkRequest = _QgsBlockingNetworkRequest
|
||||
QgsJsonExporter = _QgsJsonExporter
|
||||
QgsJsonUtils = _QgsJsonUtils
|
||||
|
||||
# Geometrietyp-Konstanten (QGIS 4 / QGIS 3.30+ vs. ältere QGIS-3)
|
||||
def _resolve_geom_type(*paths):
|
||||
for obj, *attrs in paths:
|
||||
val = obj
|
||||
for attr in attrs:
|
||||
val = getattr(val, attr, None)
|
||||
if val is None:
|
||||
break
|
||||
else:
|
||||
if val is not None:
|
||||
return val
|
||||
return None
|
||||
|
||||
GEOM_POINT = _resolve_geom_type(
|
||||
(_Qgis, "GeometryType", "Point"),
|
||||
) or getattr(__import__("qgis.core", fromlist=["QgsWkbTypes"]), "QgsWkbTypes", type("_", (), {"PointGeometry": 0})).PointGeometry
|
||||
GEOM_LINE = _resolve_geom_type(
|
||||
(_Qgis, "GeometryType", "Line"),
|
||||
) or getattr(__import__("qgis.core", fromlist=["QgsWkbTypes"]), "QgsWkbTypes", type("_", (), {"LineGeometry": 1})).LineGeometry
|
||||
GEOM_POLYGON = _resolve_geom_type(
|
||||
(_Qgis, "GeometryType", "Polygon"),
|
||||
) or getattr(__import__("qgis.core", fromlist=["QgsWkbTypes"]), "QgsWkbTypes", type("_", (), {"PolygonGeometry": 2})).PolygonGeometry
|
||||
|
||||
def _resolve_qgis_enum(*paths: tuple[Any, ...]) -> Any:
|
||||
for path in paths:
|
||||
@@ -774,6 +812,60 @@ except Exception:
|
||||
|
||||
QgsVectorFileWriter = _MockQgsVectorFileWriter
|
||||
|
||||
# Geometrietyp-Konstanten (Mock-Werte entsprechen QgsWkbTypes)
|
||||
GEOM_POINT = 0
|
||||
GEOM_LINE = 1
|
||||
GEOM_POLYGON = 2
|
||||
|
||||
class _MockQgsBlockingNetworkRequest:
|
||||
NoError = 0
|
||||
|
||||
def __init__(self):
|
||||
self._reply = None
|
||||
|
||||
def get(self, request: Any) -> int:
|
||||
return self.NoError
|
||||
|
||||
def post(self, request: Any, data: bytes) -> int:
|
||||
return self.NoError
|
||||
|
||||
def put(self, request: Any, data: bytes) -> int:
|
||||
return self.NoError
|
||||
|
||||
def reply(self) -> Any:
|
||||
return self._reply
|
||||
|
||||
def errorMessage(self) -> str:
|
||||
return ""
|
||||
|
||||
QgsBlockingNetworkRequest = _MockQgsBlockingNetworkRequest
|
||||
|
||||
class _MockQgsJsonUtils:
|
||||
@staticmethod
|
||||
def stringToFields(text: str) -> Any:
|
||||
return type("_Fields", (), {"toList": lambda self: []})()
|
||||
|
||||
@staticmethod
|
||||
def stringToFeatureList(text: str, fields: Any) -> list:
|
||||
return []
|
||||
|
||||
QgsJsonUtils = _MockQgsJsonUtils
|
||||
|
||||
class _MockQgsJsonExporter:
|
||||
def __init__(self, layer: Any = None):
|
||||
self._layer = layer
|
||||
|
||||
def setSourceCrs(self, crs: Any) -> None:
|
||||
pass
|
||||
|
||||
def setTransformGeometries(self, transform: bool) -> None:
|
||||
pass
|
||||
|
||||
def exportFeatures(self, features: list) -> str:
|
||||
return '{"type":"FeatureCollection","features":[]}'
|
||||
|
||||
QgsJsonExporter = _MockQgsJsonExporter
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Netzwerk
|
||||
# ---------------------------------------------------------
|
||||
@@ -951,3 +1043,79 @@ def layer_exists_in_gpkg(gpkg_path: str, layer_name: str) -> bool:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# QGIS-3/4-kompatible Geometrie-Hilfsfunktionen
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def polygon_zu_linie(geom: Any) -> Optional[Any]:
|
||||
"""Konvertiert ein Polygon/MultiPolygon zur Umringslinie (QGIS 3 + 4).
|
||||
|
||||
In QGIS 3.x existiert ``QgsGeometry.boundary()``, in QGIS 4.x wurde sie
|
||||
entfernt. Diese Funktion kapselt die Versionsunterschiede und stellt dem
|
||||
Fachplugin eine stabile API bereit.
|
||||
|
||||
Strategie (dreistufig):
|
||||
1. ``geom.boundary()`` – funktioniert in QGIS 3.x
|
||||
2. ``geom.convertToType(1, False)`` – ``1`` = LineGeometry-Wert,
|
||||
identisch in ``QgsWkbTypes.LineGeometry`` (3.x) und
|
||||
``Qgis.GeometryType.Line`` (4.x)
|
||||
3. Manuelle Ring-Extraktion via ``asPolygon()`` / ``asMultiPolygon()``
|
||||
+ ``QgsGeometry.fromPolylineXY()``
|
||||
|
||||
Parameters
|
||||
----------
|
||||
geom :
|
||||
Polygon- oder MultiPolygon-``QgsGeometry``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[QgsGeometry]
|
||||
Liniengeometrie (Umring) oder ``None`` bei Fehler / leerer Eingabe.
|
||||
"""
|
||||
if geom is None:
|
||||
return None
|
||||
try:
|
||||
if geom.isEmpty():
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Stufe 1: QGIS 3.x – .boundary() existiert und liefert alle Ringe korrekt
|
||||
if hasattr(geom, "boundary"):
|
||||
try:
|
||||
result = geom.boundary()
|
||||
if result is not None and not result.isEmpty():
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stufe 2: Manuelle Ring-Extraktion (primärer QGIS-4.x-Pfad).
|
||||
# convertToType(1, False) würde bei MultiPolygon nur den ersten Außenring
|
||||
# zurückgeben und alle weiteren Polygone sowie Innenringe verwerfen → daher
|
||||
# direkt die vollständige manuelle Extraktion als nächste Stufe.
|
||||
lines = []
|
||||
try:
|
||||
if geom.isMultipart():
|
||||
for polygon in geom.asMultiPolygon():
|
||||
for ring in polygon:
|
||||
lines.append(QgsGeometry.fromPolylineXY(ring))
|
||||
else:
|
||||
for ring in geom.asPolygon():
|
||||
lines.append(QgsGeometry.fromPolylineXY(ring))
|
||||
except Exception:
|
||||
pass
|
||||
if lines:
|
||||
return QgsGeometry.unaryUnion(lines)
|
||||
|
||||
# Stufe 3: Letzter Fallback – convertToType mit destMultipart=True, damit
|
||||
# zumindest alle Außenringe eines MultiPolygons erfasst werden.
|
||||
try:
|
||||
result = geom.convertToType(1, True)
|
||||
if result is not None and not result.isEmpty():
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
+32
-2
@@ -45,6 +45,7 @@ QComboBox: Type[Any] = object
|
||||
QCheckBox: Type[Any] = object
|
||||
QHBoxLayout: Type[Any] = object
|
||||
QFont: Type[Any] = object
|
||||
QSettings: Type[Any] = object
|
||||
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
@@ -99,7 +100,8 @@ try:
|
||||
QUrl as _QUrl,
|
||||
QCoreApplication as _QCoreApplication,
|
||||
Qt as _Qt,
|
||||
QVariant as _QVariant
|
||||
QVariant as _QVariant,
|
||||
QSettings as _QSettings,
|
||||
)
|
||||
from qgis.PyQt.QtNetwork import (
|
||||
QNetworkRequest as _QNetworkRequest,
|
||||
@@ -140,6 +142,7 @@ try:
|
||||
QVariant = _QVariant
|
||||
QHBoxLayout = _QHBoxLayout
|
||||
QFont = _QFont
|
||||
QSettings = _QSettings
|
||||
# ✅ QT6 ENUMS
|
||||
YES = QMessageBox.StandardButton.Yes
|
||||
NO = QMessageBox.StandardButton.No
|
||||
@@ -199,7 +202,8 @@ except (ImportError, AttributeError):
|
||||
QUrl as _QUrl,
|
||||
QCoreApplication as _QCoreApplication,
|
||||
Qt as _Qt,
|
||||
QVariant as _QVariant
|
||||
QVariant as _QVariant,
|
||||
QSettings as _QSettings,
|
||||
)
|
||||
from PyQt5.QtNetwork import (
|
||||
QNetworkRequest as _QNetworkRequest,
|
||||
@@ -238,6 +242,7 @@ except (ImportError, AttributeError):
|
||||
QVariant = _QVariant
|
||||
QHBoxLayout= _QHBoxLayout
|
||||
QFont = _QFont
|
||||
QSettings = _QSettings
|
||||
|
||||
# ✅ PYQT5 ENUMS
|
||||
YES = QMessageBox.Yes
|
||||
@@ -560,6 +565,31 @@ except (ImportError, AttributeError):
|
||||
pass
|
||||
QHBoxLayout = _MockQHBoxLayout
|
||||
|
||||
class _MockQSettings:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._data: dict = {}
|
||||
self._group = ""
|
||||
|
||||
def beginGroup(self, group: str) -> None:
|
||||
self._group = group
|
||||
|
||||
def endGroup(self) -> None:
|
||||
self._group = ""
|
||||
|
||||
def setValue(self, key: str, value: Any) -> None:
|
||||
full = "%s/%s" % (self._group, key) if self._group else key
|
||||
self._data[full] = value
|
||||
|
||||
def value(self, key: str, default: Any = "") -> Any:
|
||||
full = "%s/%s" % (self._group, key) if self._group else key
|
||||
return self._data.get(full, default)
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
full = "%s/%s" % (self._group, key) if self._group else key
|
||||
self._data.pop(full, None)
|
||||
|
||||
QSettings = _MockQSettings
|
||||
|
||||
class _MockQCheckBox:
|
||||
def __init__(self, text: str = "", *args, **kwargs):
|
||||
self._text = text
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
mermaid´´´
|
||||
flowchart TD
|
||||
A[Projekt]
|
||||
|
||||
subgraph children[ ]
|
||||
direction TB
|
||||
B[src]
|
||||
C[docs]
|
||||
D[README.md]
|
||||
end
|
||||
|
||||
A --> B
|
||||
A --> C
|
||||
A --> D
|
||||
@@ -171,3 +171,46 @@ def setup_verfahrensgebiet_listener() -> None:
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def teardown_verfahrensgebiet_listener() -> None:
|
||||
"""
|
||||
Trennt globale Event-Listener vom QgsProject-Singleton.
|
||||
|
||||
Diese Funktion ist idempotent und kann mehrfach aufgerufen werden.
|
||||
"""
|
||||
global _listener_installed
|
||||
|
||||
if not _listener_installed:
|
||||
return
|
||||
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
if project is None:
|
||||
return
|
||||
|
||||
try:
|
||||
project.layersAdded.disconnect(_on_layers_added)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
project.layerRemoved.disconnect(_on_layer_removed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
||||
signal = getattr(project, signal_name, None)
|
||||
if signal is None:
|
||||
continue
|
||||
try:
|
||||
signal.disconnect(_on_project_changed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except RuntimeError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_listener_installed = False
|
||||
|
||||
@@ -4,7 +4,10 @@ from qgis.utils import plugins
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QCoreApplication
|
||||
from sn_basis.functions.sys_wrapper import getattr_safe
|
||||
from sn_basis.functions.verfahrensgebiet_manager import setup_verfahrensgebiet_listener
|
||||
from sn_basis.functions.verfahrensgebiet_manager import (
|
||||
setup_verfahrensgebiet_listener,
|
||||
teardown_verfahrensgebiet_listener,
|
||||
)
|
||||
from sn_basis.ui.navigation import Navigation
|
||||
|
||||
|
||||
@@ -53,6 +56,8 @@ class BasisPlugin:
|
||||
"""
|
||||
Räumt UI-Komponenten sauber auf.
|
||||
"""
|
||||
teardown_verfahrensgebiet_listener()
|
||||
|
||||
if self.ui:
|
||||
self.ui.remove_all()
|
||||
self.ui = None
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
[general]
|
||||
version=26.4.2-testing
|
||||
version=26.7.2-testing
|
||||
name=LNO Sachsen | Plugin Basisfunktionen
|
||||
description=Plugin mit Basisfunktionen
|
||||
author=Daniel Helbig, Michael Otto
|
||||
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
|
||||
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
|
||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/testing/
|
||||
|
||||
QGISMaximumVersion=4.99
|
||||
qgisMinimumVersion=3.40
|
||||
experimental=true
|
||||
deprecated=false
|
||||
|
||||
@@ -1 +1,40 @@
|
||||
#Datenbankpruefer.py
|
||||
"""
|
||||
sn_basis/modules/Datenbankpruefer.py – Prüfung von Datenbanken.
|
||||
Validiert Datenbank-Verbindungen und -Strukturen.
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
class Datenbankpruefer:
|
||||
"""
|
||||
Prüft Datenbankverbindungen und Datenbankstrukturen auf Gültigkeit.
|
||||
Gibt pruef_ergebnis für die Weiterverarbeitung durch Pruefmanager zurück.
|
||||
"""
|
||||
|
||||
def __init__(self, pfad: Optional[str] = None) -> None:
|
||||
"""
|
||||
Initialisiert den Datenbankprüfer.
|
||||
|
||||
Args:
|
||||
pfad: Optional Pfad zur Datenbankdatei (z.B. .gpkg, .sqlite)
|
||||
"""
|
||||
self.pfad = pfad
|
||||
|
||||
def pruefe(self) -> pruef_ergebnis:
|
||||
"""
|
||||
Prüft die Datenbank auf Gültigkeit und Verbindbarkeit.
|
||||
|
||||
Returns:
|
||||
pruef_ergebnis: Ergebnis der Prüfung mit ok-Flag und Aktion
|
||||
"""
|
||||
if not self.pfad:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Kein Datenbankpfad angegeben.",
|
||||
aktion="kein_dateipfad",
|
||||
kontext=None,
|
||||
)
|
||||
# Implementierung folgt
|
||||
return pruef_ergebnis(ok=True, meldung="Datenbank OK", aktion="ok", kontext=self.pfad)
|
||||
@@ -209,7 +209,7 @@ class Datenschreiber:
|
||||
continue
|
||||
|
||||
layer_name_raw = thema or str(ident)
|
||||
layer_name = re.sub(r"[^A-Za-z0-9_]+", "_", layer_name_raw).strip("_")
|
||||
layer_name = re.sub(r"[^A-Za-z0-9_ÄäÖöÜüß]+", "_", layer_name_raw).strip("_")
|
||||
if not layer_name:
|
||||
layer_name = f"layer_{ident}"
|
||||
|
||||
|
||||
+48
-1
@@ -5,7 +5,7 @@ sn_basis/modules/Pruefmanager.py
|
||||
from __future__ import annotations
|
||||
from typing import Optional, Any
|
||||
|
||||
from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom
|
||||
from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen
|
||||
from sn_basis.functions.ly_visibility_wrapper import set_layer_visible
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||
|
||||
@@ -204,6 +204,10 @@ class Pruefmanager:
|
||||
if ergebnis.aktion == "datei_existiert":
|
||||
return self._handle_datei_existiert(ergebnis)
|
||||
|
||||
# 3. Verfahrensgebiet-Editentscheidung
|
||||
if ergebnis.aktion == "verfahrensgebiet_layer_existiert":
|
||||
return self._handle_verfahrensgebiet_layer_existiert(ergebnis)
|
||||
|
||||
# 3. Basis interaktive Aktionen
|
||||
interactive_actions = {
|
||||
"leereingabe_erlaubt",
|
||||
@@ -223,6 +227,49 @@ class Pruefmanager:
|
||||
)
|
||||
return ergebnis
|
||||
|
||||
def _handle_verfahrensgebiet_layer_existiert(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||
"""Fragt Nutzer, was mit dem bestehenden Verfahrensgebiet-Layer geschehen soll.
|
||||
|
||||
Zeigt einen Dialog mit vier Optionen:
|
||||
- **Ergänzen**: neue Flurstücke hinzufügen
|
||||
- **Entfernen**: ausgewählte Flurstücke entfernen
|
||||
- **Ersetzen**: Verfahrensgebiet vollständig ersetzen
|
||||
- **Abbrechen**: Vorgang beenden
|
||||
|
||||
Returns
|
||||
-------
|
||||
pruef_ergebnis
|
||||
ok=True mit aktion ``verfahrensgebiet_ergaenzen`` / ``verfahrensgebiet_entfernen`` /
|
||||
``verfahrensgebiet_ersetzen``, oder ok=False mit ``verfahrensgebiet_abbrechen``.
|
||||
"""
|
||||
if self.ui_modus != "qgis":
|
||||
return ergebnis
|
||||
|
||||
titel = "Verfahrensgebiet bereits vorhanden"
|
||||
meldung = (
|
||||
ergebnis.meldung
|
||||
or "Ein Verfahrensgebiet-Layer existiert bereits.\nWas soll geschehen?"
|
||||
)
|
||||
|
||||
entscheidung = ask_ergaenzen_entfernen_ersetzen_abbrechen(
|
||||
parent=self.parent,
|
||||
title=titel,
|
||||
message=meldung,
|
||||
)
|
||||
|
||||
aktion_map = {
|
||||
"ergaenzen": "verfahrensgebiet_ergaenzen",
|
||||
"entfernen": "verfahrensgebiet_entfernen",
|
||||
"ersetzen": "verfahrensgebiet_ersetzen",
|
||||
"abbrechen": "verfahrensgebiet_abbrechen",
|
||||
}
|
||||
|
||||
return pruef_ergebnis(
|
||||
ok=(entscheidung != "abbrechen"),
|
||||
aktion=aktion_map[entscheidung],
|
||||
kontext=ergebnis.kontext,
|
||||
)
|
||||
|
||||
def _ask_use_or_replace_pufferlayer(self) -> str:
|
||||
"""
|
||||
Fragt den Nutzer, ob ein vorhandener Pufferlayer verwendet
|
||||
|
||||
@@ -74,6 +74,13 @@ PruefAktion = Literal[
|
||||
"stil_not_implemented",
|
||||
"datei_unbekannt",
|
||||
"needs_user_action",
|
||||
|
||||
# Verfahrensgebiet-Editoperationen
|
||||
"verfahrensgebiet_layer_existiert", # bestehender Layer → Nutzerentscheidung
|
||||
"verfahrensgebiet_ergaenzen", # Flurstücke hinzufügen
|
||||
"verfahrensgebiet_entfernen", # Flurstücke entfernen
|
||||
"verfahrensgebiet_ersetzen", # Verfahrensgebiet vollständig ersetzen
|
||||
"verfahrensgebiet_abbrechen", # Nutzer hat abgebrochen
|
||||
]
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -1 +1,4 @@
|
||||
#Testordner
|
||||
"""
|
||||
sn_basis/tests – Test-Suite für das sn_basis Plugin.
|
||||
Enthält Unit-Tests und Integrationstests für alle Module.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import unittest
|
||||
from typing import Any, cast
|
||||
from unittest.mock import patch
|
||||
|
||||
from sn_basis.modules.DataGrabber import DataGrabber
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
class _DummyPruefmanager:
|
||||
def verarbeite(self, ergebnis: Any):
|
||||
return ergebnis
|
||||
|
||||
|
||||
class TestDataGrabber(unittest.TestCase):
|
||||
|
||||
def _make_grabber(self) -> DataGrabber:
|
||||
return DataGrabber(pruefmanager=cast(Any, _DummyPruefmanager()))
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_detects_excel(self, mock_dateipruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
aktion="ok",
|
||||
kontext="C:/tmp/list.xlsx",
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, result = grabber.analyze_source_type("C:/tmp/list.xlsx")
|
||||
|
||||
self.assertEqual(source_type, "excel")
|
||||
self.assertTrue(result.ok)
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_returns_unknown_filetype(self, mock_dateipruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
aktion="ok",
|
||||
kontext="C:/tmp/file.txt",
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, _result = grabber.analyze_source_type("C:/tmp/file.txt")
|
||||
|
||||
self.assertEqual(source_type, "unbekannter_dateityp")
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Linkpruefer")
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_detects_service_when_file_check_fails(self, mock_dateipruefer, mock_linkpruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="missing",
|
||||
aktion="datei_nicht_gefunden",
|
||||
kontext=None,
|
||||
)
|
||||
mock_linkpruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
aktion="url_ok",
|
||||
kontext="https://example.test",
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, result = grabber.analyze_source_type("https://example.test")
|
||||
|
||||
self.assertEqual(source_type, "dienst")
|
||||
self.assertTrue(result.ok)
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Linkpruefer")
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_returns_unbekannte_quelle_on_full_failure(self, mock_dateipruefer, mock_linkpruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="missing",
|
||||
aktion="datei_nicht_gefunden",
|
||||
kontext=None,
|
||||
)
|
||||
mock_linkpruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="bad",
|
||||
aktion="url_nicht_erreichbar",
|
||||
kontext=None,
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, result = grabber.analyze_source_type("broken-input")
|
||||
|
||||
self.assertEqual(source_type, "unbekannte_quelle")
|
||||
self.assertFalse(result.ok)
|
||||
|
||||
@patch.object(DataGrabber, "analyze_source_type")
|
||||
@patch.object(DataGrabber, "_process_excel_source")
|
||||
def test_run_returns_success_summary_with_valid_rows(self, mock_process_excel, mock_analyze):
|
||||
mock_analyze.return_value = (
|
||||
"excel",
|
||||
pruef_ergebnis(ok=True, meldung="ok", aktion="ok", kontext=None),
|
||||
)
|
||||
mock_process_excel.return_value = (
|
||||
{"rows": [{"ident": "A1"}]},
|
||||
[],
|
||||
)
|
||||
|
||||
grabber = self._make_grabber()
|
||||
source_dict, summary = grabber.run("C:/tmp/list.xlsx")
|
||||
|
||||
self.assertEqual(len(source_dict["rows"]), 1)
|
||||
self.assertTrue(summary.ok)
|
||||
self.assertEqual(summary.aktion, "ok")
|
||||
|
||||
@patch.object(DataGrabber, "analyze_source_type")
|
||||
def test_run_returns_failure_summary_without_rows(self, mock_analyze):
|
||||
mock_analyze.return_value = (
|
||||
"unbekannte_quelle",
|
||||
pruef_ergebnis(ok=False, meldung="bad", aktion="unbekannte_quelle", kontext=None),
|
||||
)
|
||||
|
||||
grabber = self._make_grabber()
|
||||
source_dict, summary = grabber.run("???")
|
||||
|
||||
self.assertEqual(source_dict, {})
|
||||
self.assertFalse(summary.ok)
|
||||
self.assertEqual(summary.aktion, "keine_validen_eintraege")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -141,6 +141,40 @@ class TestPruefmanager(unittest.TestCase):
|
||||
self.assertFalse(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "unbekannt")
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="overwrite")
|
||||
def test_datei_existiert_overwrite(self, _mock_choice):
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertTrue(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_ueberschreiben")
|
||||
self.assertEqual(entscheidung.kontext, "C:/tmp/v.gpkg")
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="append")
|
||||
def test_datei_existiert_append(self, _mock_choice):
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertTrue(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_anhaengen")
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="cancel")
|
||||
def test_datei_existiert_cancel(self, _mock_choice):
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertTrue(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_ueberspringen")
|
||||
|
||||
def test_datei_existiert_in_non_qgis_mode_returns_input(self):
|
||||
manager = Pruefmanager(ui_modus="test")
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
|
||||
entscheidung = manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertFalse(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -55,6 +55,32 @@ class TestSettingsLogic(unittest.TestCase):
|
||||
self.assertIn("gemeinden", saved_keys)
|
||||
self.assertNotIn("unbekannt", saved_keys)
|
||||
|
||||
@patch("sn_basis.functions.settings_logic.get_variable")
|
||||
def test_load_handles_partial_values_with_empty_fallback(self, mock_get):
|
||||
values = {
|
||||
"amt": "A1",
|
||||
"behoerde": "",
|
||||
"landkreis_user": "L1",
|
||||
}
|
||||
mock_get.side_effect = lambda key, scope="project": values.get(key, "")
|
||||
|
||||
logic = SettingsLogic()
|
||||
daten = logic.load()
|
||||
|
||||
self.assertEqual(daten["amt"], "A1")
|
||||
self.assertEqual(daten["behoerde"], "")
|
||||
self.assertEqual(daten["landkreis_user"], "L1")
|
||||
self.assertEqual(daten["sachgebiet"], "")
|
||||
|
||||
@patch("sn_basis.functions.settings_logic.set_variable")
|
||||
def test_save_empty_string_value_is_persisted(self, mock_set):
|
||||
logic = SettingsLogic()
|
||||
|
||||
logic.save({"amt": "", "behoerde": "B2"})
|
||||
|
||||
mock_set.assert_any_call("amt", "", scope="project")
|
||||
mock_set.assert_any_call("behoerde", "B2", scope="project")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -51,6 +51,28 @@ class TestVariableWrapper(unittest.TestCase):
|
||||
"new_value",
|
||||
)
|
||||
|
||||
@patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.setGlobalVariable")
|
||||
def test_set_variable_global_writes_prefixed_name(self, mock_set_global):
|
||||
variable_wrapper.set_variable("verfahrensnummer", "123", scope="global")
|
||||
|
||||
mock_set_global.assert_called_once_with("sn_verfahrensnummer", "123")
|
||||
|
||||
@patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.globalScope")
|
||||
def test_get_variable_global_returns_empty_string_fallback(self, mock_global_scope):
|
||||
mock_global_scope.return_value = _Scope("")
|
||||
|
||||
value = variable_wrapper.get_variable("nicht_vorhanden", scope="global")
|
||||
|
||||
self.assertEqual(value, "")
|
||||
|
||||
def test_get_variable_invalid_scope_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
variable_wrapper.get_variable("foo", scope="invalid")
|
||||
|
||||
def test_set_variable_invalid_scope_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
variable_wrapper.set_variable("foo", "bar", scope="invalid")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
sn_basis/ui – Benutzeroberflächen-Module für sn_basis.
|
||||
Enthält Dock-Manager, Navigation und einzelne Tabs.
|
||||
"""
|
||||
|
||||
@@ -99,6 +99,20 @@ class BaseDockWidget(QDockWidget):
|
||||
Wird aufgerufen, wenn das Dock geschlossen wird.
|
||||
Setzt die zugehörige Toolbar-Action zurück.
|
||||
"""
|
||||
try:
|
||||
tab_widget = self.widget()
|
||||
if tab_widget and hasattr(tab_widget, "count") and hasattr(tab_widget, "widget"):
|
||||
for index in range(tab_widget.count()):
|
||||
tab = tab_widget.widget(index)
|
||||
cleanup = getattr(tab, "cleanup", None)
|
||||
if callable(cleanup):
|
||||
try:
|
||||
cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self.action:
|
||||
self.action.setChecked(False)
|
||||
|
||||
+8
-3
@@ -1,4 +1,9 @@
|
||||
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||
"""
|
||||
sn_basis/ui/tabs – Tab-Widgets für die sn_basis Benutzeroberfläche.
|
||||
Stellt print_tab und settings_tab bereit.
|
||||
"""
|
||||
|
||||
__all__ = ["PrintTab", "SettingsTab"]
|
||||
# from sn_basis.ui.tabs.print_tab import PrintTab
|
||||
# from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||
|
||||
# __all__ = ["PrintTab", "SettingsTab"]
|
||||
|
||||
+35
-3
@@ -65,9 +65,11 @@ class PrintTab(QWidget):
|
||||
|
||||
def __init__(self, parent: Optional[Any] = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.pruefmanager: Optional[Pruefmanager] = None
|
||||
self.logic: Optional[PrintLogic] = None
|
||||
self.logic = PrintLogic(self.pruefmanager)
|
||||
|
||||
|
||||
#self.logic: Optional[PrintLogic] = None
|
||||
self._kartenname_combo: Optional[Any] = None
|
||||
self._massstab_combo: Optional[Any] = None
|
||||
self._thema_combo: Optional[Any] = None
|
||||
@@ -436,8 +438,38 @@ class PrintTab(QWidget):
|
||||
druckbereich_auswahl=self._druckbereich_combo.currentText() if self._druckbereich_combo else DRUCKBEREICH_VERFAHRENSGEBIET,
|
||||
)
|
||||
|
||||
def _disconnect_project_signals(self) -> None:
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if project is None:
|
||||
return
|
||||
|
||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
||||
signal = getattr(project, signal_name, None)
|
||||
if signal is None:
|
||||
continue
|
||||
try:
|
||||
signal.disconnect(self._on_project_changed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Trennt Signalverbindungen vor dem Schliessen des Tabs."""
|
||||
self._disconnect_theme_collection_signals()
|
||||
self._disconnect_project_signals()
|
||||
|
||||
def _connect_project_signals(self) -> None:
|
||||
project = QgsProject.instance()
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if project is None:
|
||||
return
|
||||
|
||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
||||
signal = getattr(project, signal_name, None)
|
||||
if signal is None:
|
||||
|
||||
+19
-1
@@ -1,4 +1,8 @@
|
||||
#sn_basis/ui/tabs/settings_tab.py
|
||||
"""
|
||||
sn_basis/ui/tabs/settings_tab.py – Einstellungen-Tab für Projekteinformationen.
|
||||
Verwaltet benutzerspezifische und projektspezifische Einstellungen.
|
||||
"""
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QWidget,
|
||||
QGridLayout,
|
||||
@@ -12,9 +16,23 @@ from sn_basis.functions.settings_logic import SettingsLogic
|
||||
|
||||
|
||||
class SettingsTab(QWidget):
|
||||
"""
|
||||
Tab-Widget für die Verwaltung von Projekt- und Benutzereinstellungen.
|
||||
|
||||
Attributes:
|
||||
tab_title: Anzeigetitel des Tabs ("Projekteigenschaften")
|
||||
logic: SettingsLogic-Instanz für persistente Einstellungen
|
||||
"""
|
||||
|
||||
tab_title = "Projekteigenschaften"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Initialisiert den Settings-Tab mit Eingabefeldern.
|
||||
|
||||
Args:
|
||||
parent: Übergeordnetes Widget (Standard: None)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.logic = SettingsLogic()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user