forked from AG_QGIS/Plugin_SN_Basis
Auf Wrapper umgestellt, Prüfarchitektur QT6-kompatibel gemacht (Nicht lauffähig)
This commit is contained in:
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Attach to QGIS (Port 5678)",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "attach",
|
||||||
|
"connect": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5678
|
||||||
|
},
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis",
|
||||||
|
"remoteRoot": "C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
.vscode/settings.json
vendored
Normal file
35
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
// OSGeo Python als Interpreter (QGIS 4.0, Qt6)
|
||||||
|
"python.defaultInterpreterPath": "D:/OSGeo/apps/Python312/python.exe",
|
||||||
|
|
||||||
|
// Pylance: zusätzliche Suchpfade
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"D:/OSGeo/apps/qgis/python",
|
||||||
|
"D:/OSGeo/apps/Python312/Lib/site-packages",
|
||||||
|
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins",
|
||||||
|
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Autocomplete ebenfalls erweitern
|
||||||
|
"python.autoComplete.extraPaths": [
|
||||||
|
"D:/OSGeo/apps/qgis/python",
|
||||||
|
"D:/OSGeo/apps/Python312/Lib/site-packages",
|
||||||
|
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins",
|
||||||
|
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Pylance-Modus
|
||||||
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"python.analysis.diagnosticMode": "workspace",
|
||||||
|
|
||||||
|
// Tests aktivieren
|
||||||
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.unittestEnabled": true,
|
||||||
|
"python.testing.unittestArgs": [
|
||||||
|
"-v",
|
||||||
|
"-s",
|
||||||
|
"./test",
|
||||||
|
"-p",
|
||||||
|
"*test*.py"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
from .functions.variable_utils import get_variable
|
|
||||||
|
|
||||||
def classFactory(iface):
|
def classFactory(iface):
|
||||||
from .main import BasisPlugin
|
from .main import BasisPlugin
|
||||||
return BasisPlugin(iface)
|
return BasisPlugin(iface)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
# sn_basis/functions/messages.py
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from qgis.core import Qgis
|
|
||||||
from qgis.PyQt.QtWidgets import QWidget
|
|
||||||
from qgis.utils import iface
|
|
||||||
|
|
||||||
|
|
||||||
def push_message(
|
|
||||||
level: Qgis.MessageLevel,
|
|
||||||
title: str,
|
|
||||||
text: str,
|
|
||||||
duration: Optional[int] = 5,
|
|
||||||
parent: Optional[QWidget] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Zeigt eine Meldung in der QGIS-MessageBar.
|
|
||||||
- level: Qgis.Success | Qgis.Info | Qgis.Warning | Qgis.Critical
|
|
||||||
- title: Überschrift links (kurz halten)
|
|
||||||
- text: eigentliche Nachricht
|
|
||||||
- duration: Sekunden bis Auto-Ausblendung; None => bleibt sichtbar (mit Close-Button)
|
|
||||||
- parent: optionales Eltern-Widget (für Kontext), normalerweise nicht nötig
|
|
||||||
Rückgabe: MessageBarItem-Widget (kann später geschlossen/entfernt werden).
|
|
||||||
"""
|
|
||||||
bar = iface.messageBar()
|
|
||||||
# QGIS akzeptiert None als "sticky" Meldung
|
|
||||||
return bar.pushMessage(title, text, level=level, duration=duration)
|
|
||||||
|
|
||||||
|
|
||||||
def success(title: str, text: str, duration: int = 5):
|
|
||||||
return push_message(Qgis.Success, title, text, duration)
|
|
||||||
|
|
||||||
|
|
||||||
def info(title: str, text: str, duration: int = 5):
|
|
||||||
return push_message(Qgis.Info, title, text, duration)
|
|
||||||
|
|
||||||
|
|
||||||
def warning(title: str, text: str, duration: int = 5):
|
|
||||||
return push_message(Qgis.Warning, title, text, duration)
|
|
||||||
|
|
||||||
|
|
||||||
def error(title: str, text: str, duration: Optional[int] = 5):
|
|
||||||
# Fehler evtl. länger sichtbar lassen; setze duration=None falls gewünscht
|
|
||||||
return push_message(Qgis.Critical, title, text, duration)
|
|
||||||
880
functions/qgisqt_wrapper.py
Normal file
880
functions/qgisqt_wrapper.py
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
"""
|
||||||
|
sn_basis/functions/qgisqt_wrapper.py – zentrale QGIS/Qt-Abstraktion
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Type, Any
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def getattr_safe(obj: Any, name: str, default: Any = None) -> Any:
|
||||||
|
"""
|
||||||
|
Sichere getattr-Variante:
|
||||||
|
- fängt Exceptions beim Attributzugriff ab
|
||||||
|
- liefert default zurück, wenn Attribut fehlt oder fehlschlägt
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return getattr(obj, name)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Qt‑Symbole (werden später dynamisch importiert)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
QMessageBox: Optional[Type[Any]] = None
|
||||||
|
QFileDialog: Optional[Type[Any]] = None
|
||||||
|
QEventLoop: Optional[Type[Any]] = None
|
||||||
|
QUrl: Optional[Type[Any]] = None
|
||||||
|
QNetworkRequest: Optional[Type[Any]] = None
|
||||||
|
QNetworkReply: Optional[Type[Any]] = None
|
||||||
|
QCoreApplication: Optional[Type[Any]] = None
|
||||||
|
|
||||||
|
QWidget: Type[Any]
|
||||||
|
QGridLayout: Type[Any]
|
||||||
|
QLabel: Type[Any]
|
||||||
|
QLineEdit: Type[Any]
|
||||||
|
QGroupBox: Type[Any]
|
||||||
|
QVBoxLayout: Type[Any]
|
||||||
|
QPushButton: Type[Any]
|
||||||
|
|
||||||
|
YES: Optional[Any] = None
|
||||||
|
NO: Optional[Any] = None
|
||||||
|
CANCEL: Optional[Any] = None
|
||||||
|
ICON_QUESTION: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# QGIS‑Symbole (werden später dynamisch importiert)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
QgsProject: Optional[Type[Any]] = None
|
||||||
|
QgsVectorLayer: Optional[Type[Any]] = None
|
||||||
|
QgsNetworkAccessManager: Optional[Type[Any]] = None
|
||||||
|
Qgis: Optional[Type[Any]] = None
|
||||||
|
iface: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Qt‑Versionserkennung
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Versuch: PyQt6 importieren
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt6.QtWidgets import ( #type: ignore
|
||||||
|
QMessageBox as _QMessageBox,
|
||||||
|
QFileDialog as _QFileDialog,
|
||||||
|
QWidget as _QWidget,
|
||||||
|
QGridLayout as _QGridLayout,
|
||||||
|
QLabel as _QLabel,
|
||||||
|
QLineEdit as _QLineEdit,
|
||||||
|
QGroupBox as _QGroupBox,
|
||||||
|
QVBoxLayout as _QVBoxLayout,
|
||||||
|
QPushButton as _QPushButton,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import ( #type: ignore
|
||||||
|
Qt,
|
||||||
|
QEventLoop as _QEventLoop,
|
||||||
|
QUrl as _QUrl,
|
||||||
|
QCoreApplication as _QCoreApplication,
|
||||||
|
)
|
||||||
|
from PyQt6.QtNetwork import ( #type: ignore
|
||||||
|
QNetworkRequest as _QNetworkRequest,
|
||||||
|
QNetworkReply as _QNetworkReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
QMessageBox = _QMessageBox
|
||||||
|
QFileDialog = _QFileDialog
|
||||||
|
QEventLoop = _QEventLoop
|
||||||
|
QUrl = _QUrl
|
||||||
|
QNetworkRequest = _QNetworkRequest
|
||||||
|
QNetworkReply = _QNetworkReply
|
||||||
|
QCoreApplication = _QCoreApplication
|
||||||
|
|
||||||
|
QWidget = _QWidget
|
||||||
|
QGridLayout = _QGridLayout
|
||||||
|
QLabel = _QLabel
|
||||||
|
QLineEdit = _QLineEdit
|
||||||
|
QGroupBox = _QGroupBox
|
||||||
|
QVBoxLayout = _QVBoxLayout
|
||||||
|
QPushButton = _QPushButton
|
||||||
|
|
||||||
|
if QMessageBox is not None:
|
||||||
|
YES = QMessageBox.StandardButton.Yes
|
||||||
|
NO = QMessageBox.StandardButton.No
|
||||||
|
CANCEL = QMessageBox.StandardButton.Cancel
|
||||||
|
ICON_QUESTION = QMessageBox.Icon.Question
|
||||||
|
|
||||||
|
QT_VERSION = 6
|
||||||
|
|
||||||
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
|
return dialog.exec()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Versuch: PyQt5 importieren
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QMessageBox as _QMessageBox,
|
||||||
|
QFileDialog as _QFileDialog,
|
||||||
|
QWidget as _QWidget,
|
||||||
|
QGridLayout as _QGridLayout,
|
||||||
|
QLabel as _QLabel,
|
||||||
|
QLineEdit as _QLineEdit,
|
||||||
|
QGroupBox as _QGroupBox,
|
||||||
|
QVBoxLayout as _QVBoxLayout,
|
||||||
|
QPushButton as _QPushButton,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import (
|
||||||
|
Qt,
|
||||||
|
QEventLoop as _QEventLoop,
|
||||||
|
QUrl as _QUrl,
|
||||||
|
QCoreApplication as _QCoreApplication,
|
||||||
|
)
|
||||||
|
from PyQt5.QtNetwork import (
|
||||||
|
QNetworkRequest as _QNetworkRequest,
|
||||||
|
QNetworkReply as _QNetworkReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
QMessageBox = _QMessageBox
|
||||||
|
QFileDialog = _QFileDialog
|
||||||
|
QEventLoop = _QEventLoop
|
||||||
|
QUrl = _QUrl
|
||||||
|
QNetworkRequest = _QNetworkRequest
|
||||||
|
QNetworkReply = _QNetworkReply
|
||||||
|
QCoreApplication = _QCoreApplication
|
||||||
|
|
||||||
|
QWidget = _QWidget
|
||||||
|
QGridLayout = _QGridLayout
|
||||||
|
QLabel = _QLabel
|
||||||
|
QLineEdit = _QLineEdit
|
||||||
|
QGroupBox = _QGroupBox
|
||||||
|
QVBoxLayout = _QVBoxLayout
|
||||||
|
QPushButton = _QPushButton
|
||||||
|
|
||||||
|
if QMessageBox is not None:
|
||||||
|
YES = QMessageBox.Yes
|
||||||
|
NO = QMessageBox.No
|
||||||
|
CANCEL = QMessageBox.Cancel
|
||||||
|
ICON_QUESTION = QMessageBox.Question
|
||||||
|
|
||||||
|
QT_VERSION = 5
|
||||||
|
|
||||||
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
|
return dialog.exec_()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Mock‑Modus (kein Qt verfügbar)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
QT_VERSION = 0
|
||||||
|
|
||||||
|
class FakeEnum(int):
|
||||||
|
"""OR‑fähiger Enum‑Ersatz für Mock‑Modus."""
|
||||||
|
|
||||||
|
def __new__(cls, value: int):
|
||||||
|
return int.__new__(cls, value)
|
||||||
|
|
||||||
|
def __or__(self, other: "FakeEnum") -> "FakeEnum":
|
||||||
|
return FakeEnum(int(self) | int(other))
|
||||||
|
|
||||||
|
class _MockQMessageBox:
|
||||||
|
Yes = FakeEnum(1)
|
||||||
|
No = FakeEnum(2)
|
||||||
|
Cancel = FakeEnum(4)
|
||||||
|
Question = FakeEnum(8)
|
||||||
|
|
||||||
|
QMessageBox = _MockQMessageBox
|
||||||
|
|
||||||
|
class _MockQFileDialog:
|
||||||
|
@staticmethod
|
||||||
|
def getOpenFileName(*args, **kwargs):
|
||||||
|
return ("", "")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getSaveFileName(*args, **kwargs):
|
||||||
|
return ("", "")
|
||||||
|
|
||||||
|
QFileDialog = _MockQFileDialog
|
||||||
|
|
||||||
|
class _MockQEventLoop:
|
||||||
|
def exec(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def quit(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
QEventLoop = _MockQEventLoop
|
||||||
|
|
||||||
|
class _MockQUrl(str):
|
||||||
|
def isValid(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
QUrl = _MockQUrl
|
||||||
|
|
||||||
|
class _MockQNetworkRequest:
|
||||||
|
def __init__(self, url: Any):
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
QNetworkRequest = _MockQNetworkRequest
|
||||||
|
|
||||||
|
class _MockQNetworkReply:
|
||||||
|
class NetworkError:
|
||||||
|
NoError = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._data = b""
|
||||||
|
|
||||||
|
def error(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def errorString(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def attribute(self, *args, **kwargs) -> Any:
|
||||||
|
return 200
|
||||||
|
|
||||||
|
def readAll(self) -> bytes:
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def deleteLater(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
QNetworkReply = _MockQNetworkReply
|
||||||
|
|
||||||
|
YES = FakeEnum(1)
|
||||||
|
NO = FakeEnum(2)
|
||||||
|
CANCEL = FakeEnum(4)
|
||||||
|
ICON_QUESTION = FakeEnum(8)
|
||||||
|
|
||||||
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
|
return YES
|
||||||
|
|
||||||
|
class _MockWidget:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _MockLayout:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def addWidget(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def addLayout(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def addStretch(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setLayout(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _MockLabel:
|
||||||
|
def __init__(self, text: str = ""):
|
||||||
|
self._text = text
|
||||||
|
|
||||||
|
class _MockLineEdit:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._text = ""
|
||||||
|
|
||||||
|
def text(self) -> str:
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
def setText(self, value: str) -> None:
|
||||||
|
self._text = value
|
||||||
|
|
||||||
|
class _MockButton:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# einfache Attr für Kompatibilität mit Qt-Signal-Syntax
|
||||||
|
self.clicked = lambda *a, **k: None
|
||||||
|
|
||||||
|
def connect(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
QWidget = _MockWidget
|
||||||
|
QGridLayout = _MockLayout
|
||||||
|
QLabel = _MockLabel
|
||||||
|
QLineEdit = _MockLineEdit
|
||||||
|
QGroupBox = _MockWidget
|
||||||
|
QVBoxLayout = _MockLayout
|
||||||
|
QPushButton = _MockButton
|
||||||
|
|
||||||
|
# Kein echtes QCoreApplication im Mock
|
||||||
|
QCoreApplication = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# QGIS‑Imports
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
try:
|
||||||
|
from qgis.core import (
|
||||||
|
QgsProject as _QgsProject,
|
||||||
|
QgsVectorLayer as _QgsVectorLayer,
|
||||||
|
QgsNetworkAccessManager as _QgsNetworkAccessManager,
|
||||||
|
Qgis as _Qgis,
|
||||||
|
)
|
||||||
|
from qgis.utils import iface as _iface
|
||||||
|
|
||||||
|
QgsProject = _QgsProject
|
||||||
|
QgsVectorLayer = _QgsVectorLayer
|
||||||
|
QgsNetworkAccessManager = _QgsNetworkAccessManager
|
||||||
|
Qgis = _Qgis
|
||||||
|
iface = _iface
|
||||||
|
|
||||||
|
QGIS_AVAILABLE = True
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
QGIS_AVAILABLE = False
|
||||||
|
|
||||||
|
class _MockQgsProject:
|
||||||
|
@staticmethod
|
||||||
|
def instance() -> "_MockQgsProject":
|
||||||
|
return _MockQgsProject()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._variables = {}
|
||||||
|
|
||||||
|
def read(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
QgsProject = _MockQgsProject
|
||||||
|
|
||||||
|
class _MockQgsVectorLayer:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._valid = True
|
||||||
|
|
||||||
|
def isValid(self) -> bool:
|
||||||
|
return self._valid
|
||||||
|
|
||||||
|
def loadNamedStyle(self, path: str):
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def triggerRepaint(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
QgsVectorLayer = _MockQgsVectorLayer
|
||||||
|
|
||||||
|
class _MockQgsNetworkAccessManager:
|
||||||
|
def head(self, request: Any) -> _MockQNetworkReply:
|
||||||
|
return _MockQNetworkReply()
|
||||||
|
|
||||||
|
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
||||||
|
|
||||||
|
class _MockQgis:
|
||||||
|
class MessageLevel:
|
||||||
|
Success = 0
|
||||||
|
Info = 1
|
||||||
|
Warning = 2
|
||||||
|
Critical = 3
|
||||||
|
|
||||||
|
Qgis = _MockQgis
|
||||||
|
|
||||||
|
class FakeIface:
|
||||||
|
class FakeMessageBar:
|
||||||
|
def pushMessage(self, title, text, level=0, duration=5):
|
||||||
|
return {"title": title, "text": text, "level": level, "duration": duration}
|
||||||
|
|
||||||
|
def messageBar(self):
|
||||||
|
return self.FakeMessageBar()
|
||||||
|
|
||||||
|
def mainWindow(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
iface = FakeIface()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Message‑Funktionen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _get_message_bar():
|
||||||
|
if iface is not None:
|
||||||
|
bar_attr = getattr_safe(iface, "messageBar")
|
||||||
|
if callable(bar_attr):
|
||||||
|
try:
|
||||||
|
return bar_attr()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _MockMessageBar:
|
||||||
|
def pushMessage(self, title, text, level=0, duration=5):
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"text": text,
|
||||||
|
"level": level,
|
||||||
|
"duration": duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
return _MockMessageBar()
|
||||||
|
|
||||||
|
|
||||||
|
def push_message(level, title, text, duration=5, parent=None):
|
||||||
|
bar = _get_message_bar()
|
||||||
|
push = getattr_safe(bar, "pushMessage")
|
||||||
|
if callable(push):
|
||||||
|
return push(title, text, level=level, duration=duration)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def info(title, text, duration=5):
|
||||||
|
level = Qgis.MessageLevel.Info if Qgis is not None else 1
|
||||||
|
return push_message(level, title, text, duration)
|
||||||
|
|
||||||
|
|
||||||
|
def warning(title, text, duration=5):
|
||||||
|
level = Qgis.MessageLevel.Warning if Qgis is not None else 2
|
||||||
|
return push_message(level, title, text, duration)
|
||||||
|
|
||||||
|
|
||||||
|
def error(title, text, duration=5):
|
||||||
|
level = Qgis.MessageLevel.Critical if Qgis is not None else 3
|
||||||
|
return push_message(level, title, text, duration)
|
||||||
|
|
||||||
|
|
||||||
|
def success(title, text, duration=5):
|
||||||
|
level = Qgis.MessageLevel.Success if Qgis is not None else 0
|
||||||
|
return push_message(level, title, text, duration)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Dialog‑Interaktionen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def ask_yes_no(
|
||||||
|
title: str,
|
||||||
|
message: str,
|
||||||
|
default: bool = False,
|
||||||
|
parent: Any = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Fragt den Benutzer eine Ja/Nein‑Frage.
|
||||||
|
|
||||||
|
- In QGIS/Qt: zeigt einen QMessageBox‑Dialog
|
||||||
|
- Im Mock/Test‑Modus: gibt default zurück
|
||||||
|
"""
|
||||||
|
if QMessageBox is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
try:
|
||||||
|
buttons = YES | NO
|
||||||
|
result = QMessageBox.question(
|
||||||
|
parent,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
buttons,
|
||||||
|
YES if default else NO,
|
||||||
|
)
|
||||||
|
return result == YES
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Variablen‑Wrapper
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
try:
|
||||||
|
from qgis.core import QgsExpressionContextUtils
|
||||||
|
|
||||||
|
_HAS_QGIS_VARIABLES = True
|
||||||
|
except Exception:
|
||||||
|
_HAS_QGIS_VARIABLES = False
|
||||||
|
|
||||||
|
class _MockVariableStore:
|
||||||
|
global_vars: dict[str, str] = {}
|
||||||
|
project_vars: dict[str, str] = {}
|
||||||
|
|
||||||
|
class QgsExpressionContextUtils:
|
||||||
|
@staticmethod
|
||||||
|
def setGlobalVariable(name: str, value: str) -> None:
|
||||||
|
_MockVariableStore.global_vars[name] = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def globalScope():
|
||||||
|
class _Scope:
|
||||||
|
def variable(self, name: str) -> str:
|
||||||
|
return _MockVariableStore.global_vars.get(name, "")
|
||||||
|
|
||||||
|
return _Scope()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setProjectVariable(project: Any, name: str, value: str) -> None:
|
||||||
|
_MockVariableStore.project_vars[name] = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def projectScope(project: Any):
|
||||||
|
class _Scope:
|
||||||
|
def variable(self, name: str) -> str:
|
||||||
|
return _MockVariableStore.project_vars.get(name, "")
|
||||||
|
|
||||||
|
return _Scope()
|
||||||
|
|
||||||
|
|
||||||
|
def get_variable(key: str, scope: str = "project") -> str:
|
||||||
|
var_name = f"sn_{key}"
|
||||||
|
|
||||||
|
if scope == "project":
|
||||||
|
if QgsProject is not None:
|
||||||
|
projekt = QgsProject.instance()
|
||||||
|
else:
|
||||||
|
projekt = None # type: ignore[assignment]
|
||||||
|
return QgsExpressionContextUtils.projectScope(projekt).variable(var_name) or ""
|
||||||
|
|
||||||
|
if scope == "global":
|
||||||
|
return QgsExpressionContextUtils.globalScope().variable(var_name) or ""
|
||||||
|
|
||||||
|
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
||||||
|
|
||||||
|
|
||||||
|
def set_variable(key: str, value: str, scope: str = "project") -> None:
|
||||||
|
var_name = f"sn_{key}"
|
||||||
|
|
||||||
|
if scope == "project":
|
||||||
|
if QgsProject is not None:
|
||||||
|
projekt = QgsProject.instance()
|
||||||
|
else:
|
||||||
|
projekt = None # type: ignore[assignment]
|
||||||
|
QgsExpressionContextUtils.setProjectVariable(projekt, var_name, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if scope == "global":
|
||||||
|
QgsExpressionContextUtils.setGlobalVariable(var_name, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# syswrapper Lazy‑Import
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _sys():
|
||||||
|
from sn_basis.functions import syswrapper
|
||||||
|
return syswrapper
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Style‑Funktion
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def apply_style(layer, style_name: str) -> bool:
|
||||||
|
if layer is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_valid_attr = getattr_safe(layer, "isValid")
|
||||||
|
if not callable(is_valid_attr) or not is_valid_attr():
|
||||||
|
return False
|
||||||
|
|
||||||
|
sys = _sys()
|
||||||
|
base_dir = sys.get_plugin_root()
|
||||||
|
style_path = sys.join_path(base_dir, "styles", style_name)
|
||||||
|
|
||||||
|
if not sys.file_exists(style_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
ok, error_msg = layer.loadNamedStyle(style_path)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
trigger = getattr_safe(layer, "triggerRepaint")
|
||||||
|
if callable(trigger):
|
||||||
|
trigger()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Layer‑Wrapper
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def layer_exists(layer) -> bool:
|
||||||
|
if layer is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Mock/Wrapper-Attribut
|
||||||
|
is_valid_flag = getattr_safe(layer, "is_valid")
|
||||||
|
if is_valid_flag is not None:
|
||||||
|
try:
|
||||||
|
return bool(is_valid_flag)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_valid_attr = getattr_safe(layer, "isValid")
|
||||||
|
if callable(is_valid_attr):
|
||||||
|
return bool(is_valid_attr())
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_geometry_type(layer) -> str:
|
||||||
|
if layer is None:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
geometry_type_attr = getattr_safe(layer, "geometry_type")
|
||||||
|
if geometry_type_attr is not None:
|
||||||
|
return str(geometry_type_attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_spatial_attr = getattr_safe(layer, "isSpatial")
|
||||||
|
if callable(is_spatial_attr) and not is_spatial_attr():
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
geometry_type_qgis = getattr_safe(layer, "geometryType")
|
||||||
|
if callable(geometry_type_qgis):
|
||||||
|
gtype = geometry_type_qgis()
|
||||||
|
if gtype == 0:
|
||||||
|
return "Point"
|
||||||
|
if gtype == 1:
|
||||||
|
return "LineString"
|
||||||
|
if gtype == 2:
|
||||||
|
return "Polygon"
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
return "None"
|
||||||
|
except Exception:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_feature_count(layer) -> int:
|
||||||
|
if layer is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
feature_count_attr = getattr_safe(layer, "feature_count")
|
||||||
|
if feature_count_attr is not None:
|
||||||
|
try:
|
||||||
|
return int(feature_count_attr)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_spatial_attr = getattr_safe(layer, "isSpatial")
|
||||||
|
if callable(is_spatial_attr) and not is_spatial_attr():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
feature_count_qgis = getattr_safe(layer, "featureCount")
|
||||||
|
if callable(feature_count_qgis):
|
||||||
|
return int(feature_count_qgis())
|
||||||
|
|
||||||
|
return 0
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def is_layer_visible(layer) -> bool:
|
||||||
|
if layer is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
visible_attr = getattr_safe(layer, "visible")
|
||||||
|
if visible_attr is not None:
|
||||||
|
try:
|
||||||
|
return bool(visible_attr)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_visible_attr = getattr_safe(layer, "isVisible")
|
||||||
|
if callable(is_visible_attr):
|
||||||
|
return bool(is_visible_attr())
|
||||||
|
|
||||||
|
tree_layer_attr = getattr_safe(layer, "treeLayer")
|
||||||
|
if callable(tree_layer_attr):
|
||||||
|
node = tree_layer_attr()
|
||||||
|
else:
|
||||||
|
node = tree_layer_attr
|
||||||
|
|
||||||
|
if node is not None:
|
||||||
|
node_visible_attr = getattr_safe(node, "isVisible")
|
||||||
|
if callable(node_visible_attr):
|
||||||
|
return bool(node_visible_attr())
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_layer_visible(layer, visible: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Setzt die Sichtbarkeit eines Layers.
|
||||||
|
|
||||||
|
Unterstützt:
|
||||||
|
- Mock-/Wrapper-Attribute (layer.visible)
|
||||||
|
- QGIS-LayerTreeNode (treeLayer().setItemVisibilityChecked)
|
||||||
|
- Fallbacks ohne Exception-Wurf
|
||||||
|
|
||||||
|
Gibt True zurück, wenn die Sichtbarkeit gesetzt werden konnte.
|
||||||
|
"""
|
||||||
|
if layer is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1️⃣ Mock / Wrapper-Attribut
|
||||||
|
try:
|
||||||
|
if hasattr(layer, "visible"):
|
||||||
|
layer.visible = bool(visible)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2️⃣ QGIS: LayerTreeNode
|
||||||
|
try:
|
||||||
|
tree_layer_attr = getattr_safe(layer, "treeLayer")
|
||||||
|
node = tree_layer_attr() if callable(tree_layer_attr) else tree_layer_attr
|
||||||
|
|
||||||
|
if node is not None:
|
||||||
|
set_visible = getattr_safe(node, "setItemVisibilityChecked")
|
||||||
|
if callable(set_visible):
|
||||||
|
set_visible(bool(visible))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3️⃣ QGIS-Fallback: setVisible (selten, aber vorhanden)
|
||||||
|
try:
|
||||||
|
set_visible_attr = getattr_safe(layer, "setVisible")
|
||||||
|
if callable(set_visible_attr):
|
||||||
|
set_visible_attr(bool(visible))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_type(layer) -> str:
|
||||||
|
if layer is None:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
layer_type_attr = getattr_safe(layer, "layer_type")
|
||||||
|
if layer_type_attr is not None:
|
||||||
|
return str(layer_type_attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_spatial_attr = getattr_safe(layer, "isSpatial")
|
||||||
|
if callable(is_spatial_attr):
|
||||||
|
return "vector" if is_spatial_attr() else "table"
|
||||||
|
|
||||||
|
data_provider_attr = getattr_safe(layer, "dataProvider")
|
||||||
|
raster_type_attr = getattr_safe(layer, "rasterType")
|
||||||
|
if data_provider_attr is not None and raster_type_attr is not None:
|
||||||
|
return "raster"
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
except Exception:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_crs(layer) -> str:
|
||||||
|
if layer is None:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
crs_attr_direct = getattr_safe(layer, "crs")
|
||||||
|
if crs_attr_direct is not None and not callable(crs_attr_direct):
|
||||||
|
# direkter Attributzugriff (z. B. im Mock)
|
||||||
|
return str(crs_attr_direct)
|
||||||
|
|
||||||
|
try:
|
||||||
|
crs_callable = getattr_safe(layer, "crs")
|
||||||
|
if callable(crs_callable):
|
||||||
|
crs = crs_callable()
|
||||||
|
authid_attr = getattr_safe(crs, "authid")
|
||||||
|
if callable(authid_attr):
|
||||||
|
return authid_attr() or "None"
|
||||||
|
return "None"
|
||||||
|
except Exception:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_fields(layer) -> list[str]:
|
||||||
|
if layer is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# direkter Attributzugriff (Mock / Wrapper)
|
||||||
|
fields_attr_direct = getattr_safe(layer, "fields")
|
||||||
|
if fields_attr_direct is not None and not callable(fields_attr_direct):
|
||||||
|
try:
|
||||||
|
# direkter Iterable oder Mapping von Namen
|
||||||
|
if hasattr(fields_attr_direct, "__iter__") and not isinstance(
|
||||||
|
fields_attr_direct, (str, bytes)
|
||||||
|
):
|
||||||
|
return list(fields_attr_direct)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
fields_callable = getattr_safe(layer, "fields")
|
||||||
|
if callable(fields_callable):
|
||||||
|
fields = fields_callable()
|
||||||
|
|
||||||
|
# QGIS: QgsFields.names()
|
||||||
|
names_attr = getattr_safe(fields, "names")
|
||||||
|
if callable(names_attr):
|
||||||
|
return list(names_attr())
|
||||||
|
|
||||||
|
# Fallback: iterierbar?
|
||||||
|
if hasattr(fields, "__iter__") and not isinstance(fields, (str, bytes)):
|
||||||
|
return list(fields)
|
||||||
|
|
||||||
|
return []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_source(layer) -> str:
|
||||||
|
if layer is None:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
source_attr_direct = getattr_safe(layer, "source")
|
||||||
|
if source_attr_direct is not None and not callable(source_attr_direct):
|
||||||
|
return str(source_attr_direct)
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_callable = getattr_safe(layer, "source")
|
||||||
|
if callable(source_callable):
|
||||||
|
return source_callable() or "None"
|
||||||
|
return "None"
|
||||||
|
except Exception:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
|
def is_layer_editable(layer) -> bool:
|
||||||
|
if layer is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
editable_attr = getattr_safe(layer, "editable")
|
||||||
|
if editable_attr is not None:
|
||||||
|
try:
|
||||||
|
return bool(editable_attr)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
editable_callable = getattr_safe(layer, "isEditable")
|
||||||
|
if callable(editable_callable):
|
||||||
|
return bool(editable_callable())
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -1,37 +1,47 @@
|
|||||||
from qgis.core import QgsProject, QgsExpressionContextUtils
|
"""
|
||||||
|
sn_basis/funktions/settings_logic.py – Logik zum Lesen und Schreiben der Plugin-Einstellungen
|
||||||
|
über den zentralen qgisqt_wrapper.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sn_basis.functions.qgisqt_wrapper import (
|
||||||
|
get_variable,
|
||||||
|
set_variable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SettingsLogic:
|
class SettingsLogic:
|
||||||
def __init__(self):
|
"""
|
||||||
self.project = QgsProject.instance()
|
Verwaltet das Laden und Speichern der Plugin-Einstellungen.
|
||||||
|
Alle Variablen werden als sn_* Projektvariablen gespeichert.
|
||||||
|
"""
|
||||||
|
|
||||||
# Definition der Variablen-Namen
|
# Alle Variablen, die gespeichert werden sollen
|
||||||
self.global_vars = ["amt", "behoerde", "landkreis_user", "sachgebiet"]
|
VARIABLEN = [
|
||||||
self.project_vars = ["bezeichnung", "verfahrensnummer", "gemeinden", "landkreise_proj"]
|
"amt",
|
||||||
|
"behoerde",
|
||||||
def save(self, fields: dict):
|
"landkreis_user",
|
||||||
"""Speichert Felder als globale und projektbezogene Ausdrucksvariablen."""
|
"sachgebiet",
|
||||||
|
"bezeichnung",
|
||||||
# Globale Variablen
|
"verfahrensnummer",
|
||||||
for key in self.global_vars:
|
"gemeinden",
|
||||||
QgsExpressionContextUtils.setGlobalVariable(f"sn_{key}", fields.get(key, ""))
|
"landkreise_proj",
|
||||||
|
]
|
||||||
# Projektvariablen
|
|
||||||
for key in self.project_vars:
|
|
||||||
QgsExpressionContextUtils.setProjectVariable(self.project, f"sn_{key}", fields.get(key, ""))
|
|
||||||
|
|
||||||
print("✅ Ausdrucksvariablen gespeichert.")
|
|
||||||
|
|
||||||
def load(self) -> dict:
|
def load(self) -> dict:
|
||||||
"""Lädt Werte ausschließlich aus Ausdrucksvariablen (global + projektbezogen)."""
|
"""
|
||||||
|
Lädt alle Variablen aus dem Projekt.
|
||||||
|
Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt).
|
||||||
|
"""
|
||||||
|
daten = {}
|
||||||
|
for key in self.VARIABLEN:
|
||||||
|
daten[key] = get_variable(key, scope="project")
|
||||||
|
return daten
|
||||||
|
|
||||||
data = {}
|
def save(self, daten: dict):
|
||||||
|
"""
|
||||||
# Globale Variablen
|
Speichert alle übergebenen Variablen im Projekt.
|
||||||
for key in self.global_vars:
|
daten: dict mit key → value
|
||||||
data[key] = QgsExpressionContextUtils.globalScope().variable(f"sn_{key}") or ""
|
"""
|
||||||
|
for key, value in daten.items():
|
||||||
# Projektvariablen
|
if key in self.VARIABLEN:
|
||||||
for key in self.project_vars:
|
set_variable(key, value, scope="project")
|
||||||
data[key] = QgsExpressionContextUtils.projectScope(self.project).variable(f"sn_{key}") or ""
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# sn_basis/functions/styles.py
|
|
||||||
import os
|
|
||||||
from qgis.core import QgsVectorLayer
|
|
||||||
|
|
||||||
def apply_style(layer: QgsVectorLayer, style_name: str) -> bool:
|
|
||||||
"""
|
|
||||||
Lädt einen QML-Style aus dem styles-Ordner des Plugins und wendet ihn auf den Layer an.
|
|
||||||
style_name: Dateiname ohne Pfad, z.B. 'verfahrensgebiet.qml'
|
|
||||||
Rückgabe: True bei Erfolg, False sonst
|
|
||||||
"""
|
|
||||||
if not layer or not layer.isValid():
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Basis-Pfad: sn_basis/styles
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(__file__)) # geht von functions/ eins hoch
|
|
||||||
style_path = os.path.join(base_dir, "styles", style_name)
|
|
||||||
|
|
||||||
if not os.path.exists(style_path):
|
|
||||||
print(f"Style-Datei nicht gefunden: {style_path}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
ok, error_msg = layer.loadNamedStyle(style_path)
|
|
||||||
if not ok:
|
|
||||||
print(f"Style konnte nicht geladen werden: {error_msg}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
layer.triggerRepaint()
|
|
||||||
return True
|
|
||||||
185
functions/syswrapper.py
Normal file
185
functions/syswrapper.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
snbasis/functions/syswrapper.py – zentrale OS-/Dateisystem-Abstraktion
|
||||||
|
Robust, testfreundlich, mock-fähig.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Dateisystem‑Funktionen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def file_exists(path: str) -> bool:
|
||||||
|
"""Prüft, ob eine Datei existiert."""
|
||||||
|
try:
|
||||||
|
return os.path.exists(path)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_file(path: str) -> bool:
|
||||||
|
"""Prüft, ob ein Pfad eine Datei ist."""
|
||||||
|
try:
|
||||||
|
return os.path.isfile(path)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_dir(path: str) -> bool:
|
||||||
|
"""Prüft, ob ein Pfad ein Verzeichnis ist."""
|
||||||
|
try:
|
||||||
|
return os.path.isdir(path)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def join_path(*parts) -> str:
|
||||||
|
"""Verbindet Pfadbestandteile OS‑unabhängig."""
|
||||||
|
try:
|
||||||
|
return os.path.join(*parts)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: naive Verkettung
|
||||||
|
return "/".join(str(p) for p in parts)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Pfad‑ und Systemfunktionen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def get_temp_dir() -> str:
|
||||||
|
"""Gibt das temporäre Verzeichnis zurück."""
|
||||||
|
try:
|
||||||
|
return tempfile.gettempdir()
|
||||||
|
except Exception:
|
||||||
|
return "/tmp"
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_root() -> str:
|
||||||
|
"""
|
||||||
|
Ermittelt den Plugin‑Root‑Pfad.
|
||||||
|
Annahme: syswrapper liegt in sn_basis/funktions/
|
||||||
|
→ also zwei Ebenen hoch.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
here = pathlib.Path(__file__).resolve()
|
||||||
|
return str(here.parent.parent)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: aktuelles Arbeitsverzeichnis
|
||||||
|
return os.getcwd()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Datei‑I/O (optional, aber nützlich)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def read_file(path: str, mode="r"):
|
||||||
|
"""Liest eine Datei ein. Gibt None zurück, wenn Fehler auftreten."""
|
||||||
|
try:
|
||||||
|
with open(path, mode) as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(path: str, data, mode="w"):
|
||||||
|
"""Schreibt Daten in eine Datei. Gibt True/False zurück."""
|
||||||
|
try:
|
||||||
|
with open(path, mode) as f:
|
||||||
|
f.write(data)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Mock‑Modus (optional erweiterbar)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
class FakeFileSystem:
|
||||||
|
"""
|
||||||
|
Minimaler Mock‑Dateisystem‑Ersatz.
|
||||||
|
Wird nicht automatisch aktiviert, aber kann in Tests gepatcht werden.
|
||||||
|
"""
|
||||||
|
files = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_file(cls, path, content=""):
|
||||||
|
cls.files[path] = content
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exists(cls, path):
|
||||||
|
return path in cls.files
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read(cls, path):
|
||||||
|
return cls.files.get(path, None)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Betriebssystem‑Erkennung
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
import platform
|
||||||
|
|
||||||
|
def get_os() -> str:
|
||||||
|
"""
|
||||||
|
Gibt das Betriebssystem zurück:
|
||||||
|
- 'windows'
|
||||||
|
- 'linux'
|
||||||
|
- 'mac'
|
||||||
|
"""
|
||||||
|
system = platform.system().lower()
|
||||||
|
|
||||||
|
if "windows" in system:
|
||||||
|
return "windows"
|
||||||
|
if "darwin" in system:
|
||||||
|
return "mac"
|
||||||
|
if "linux" in system:
|
||||||
|
return "linux"
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def is_windows() -> bool:
|
||||||
|
return get_os() == "windows"
|
||||||
|
|
||||||
|
|
||||||
|
def is_linux() -> bool:
|
||||||
|
return get_os() == "linux"
|
||||||
|
|
||||||
|
|
||||||
|
def is_mac() -> bool:
|
||||||
|
return get_os() == "mac"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Pfad‑Normalisierung
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def normalize_path(path: str) -> str:
|
||||||
|
"""
|
||||||
|
Normalisiert Pfade OS‑unabhängig:
|
||||||
|
- ersetzt Backslashes durch Slashes
|
||||||
|
- entfernt doppelte Slashes
|
||||||
|
- löst relative Pfade auf
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
p = pathlib.Path(path).resolve()
|
||||||
|
return str(p)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: einfache Normalisierung
|
||||||
|
return path.replace("\\", "/").replace("//", "/")
|
||||||
|
|
||||||
|
def add_to_sys_path(path: str) -> None:
|
||||||
|
"""
|
||||||
|
Fügt einen Pfad sicher zum Python-Importpfad hinzu.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if path not in sys.path:
|
||||||
|
sys.path.insert(0, path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from qgis.core import QgsProject, QgsExpressionContextUtils
|
|
||||||
|
|
||||||
def get_variable(key: str, scope: str = "project") -> str:
|
|
||||||
"""
|
|
||||||
Liefert den Wert einer sn_* Variable zurück.
|
|
||||||
key: Name ohne Präfix, z.B. "verfahrensnummer"
|
|
||||||
scope: 'project' oder 'global'
|
|
||||||
"""
|
|
||||||
projekt = QgsProject.instance()
|
|
||||||
var_name = f"sn_{key}"
|
|
||||||
|
|
||||||
if scope == "project":
|
|
||||||
return QgsExpressionContextUtils.projectScope(projekt).variable(var_name) or ""
|
|
||||||
elif scope == "global":
|
|
||||||
return QgsExpressionContextUtils.globalScope().variable(var_name) or ""
|
|
||||||
else:
|
|
||||||
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
|
||||||
|
|
||||||
|
|
||||||
def set_variable(key: str, value: str, scope: str = "project"):
|
|
||||||
"""
|
|
||||||
Schreibt den Wert einer sn_* Variable.
|
|
||||||
key: Name ohne Präfix, z.B. "verfahrensnummer"
|
|
||||||
value: Wert, der gespeichert werden soll
|
|
||||||
scope: 'project' oder 'global'
|
|
||||||
"""
|
|
||||||
projekt = QgsProject.instance()
|
|
||||||
var_name = f"sn_{key}"
|
|
||||||
|
|
||||||
if scope == "project":
|
|
||||||
QgsExpressionContextUtils.setProjectVariable(projekt, var_name, value)
|
|
||||||
elif scope == "global":
|
|
||||||
QgsExpressionContextUtils.setGlobalVariable(var_name, value)
|
|
||||||
else:
|
|
||||||
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
|
||||||
24
main.py
24
main.py
@@ -1,12 +1,24 @@
|
|||||||
from qgis.PyQt.QtCore import QCoreApplication
|
# sn_basis/main.py
|
||||||
|
|
||||||
|
from sn_basis.functions.qgisqt_wrapper import QCoreApplication, getattr_safe
|
||||||
from qgis.utils import plugins
|
from qgis.utils import plugins
|
||||||
from sn_basis.ui.navigation import Navigation
|
from sn_basis.ui.navigation import Navigation
|
||||||
|
|
||||||
|
|
||||||
class BasisPlugin:
|
class BasisPlugin:
|
||||||
def __init__(self, iface):
|
def __init__(self, iface):
|
||||||
self.iface = iface
|
self.iface = iface
|
||||||
self.ui = None
|
self.ui = None
|
||||||
QCoreApplication.instance().aboutToQuit.connect(self.unload)
|
|
||||||
|
# QCoreApplication kann im Mock-Modus None sein
|
||||||
|
if QCoreApplication is not None:
|
||||||
|
app = getattr_safe(QCoreApplication, "instance")
|
||||||
|
if callable(app):
|
||||||
|
instance = app()
|
||||||
|
about_to_quit = getattr_safe(instance, "aboutToQuit")
|
||||||
|
connect = getattr_safe(about_to_quit, "connect")
|
||||||
|
if callable(connect):
|
||||||
|
connect(self.unload)
|
||||||
|
|
||||||
def initGui(self):
|
def initGui(self):
|
||||||
# Basis-Navigation neu aufbauen
|
# Basis-Navigation neu aufbauen
|
||||||
@@ -16,11 +28,15 @@ class BasisPlugin:
|
|||||||
for name, plugin in plugins.items():
|
for name, plugin in plugins.items():
|
||||||
if name.startswith("sn_") and name != "sn_basis":
|
if name.startswith("sn_") and name != "sn_basis":
|
||||||
try:
|
try:
|
||||||
plugin.initGui()
|
init_gui = getattr_safe(plugin, "initGui")
|
||||||
|
if callable(init_gui):
|
||||||
|
init_gui()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler beim Neuinitialisieren von {name}: {e}")
|
print(f"Fehler beim Neuinitialisieren von {name}: {e}")
|
||||||
|
|
||||||
def unload(self):
|
def unload(self):
|
||||||
if self.ui:
|
if self.ui:
|
||||||
self.ui.remove_all()
|
remove_all = getattr_safe(self.ui, "remove_all")
|
||||||
|
if callable(remove_all):
|
||||||
|
remove_all()
|
||||||
self.ui = None
|
self.ui = None
|
||||||
|
|||||||
@@ -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 sn_basis.functions.syswrapper import (
|
||||||
from enum import Enum, auto
|
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:
|
class Dateipruefer:
|
||||||
def pruefe(self, pfad: str,
|
"""
|
||||||
leer_modus: LeererPfadModus,
|
Prüft Dateieingaben und liefert ein pruef_ergebnis zurück.
|
||||||
standardname: str = None,
|
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
||||||
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
|
def __init__(
|
||||||
if not pfad or pfad.strip() == "":#wenn der angegebene Pfad leer oder ungültig ist:
|
self,
|
||||||
if leer_modus == LeererPfadModus.VERBOTEN: #wenn der Modus "verboten" vorgegeben ist, gib zurück, dass der Test fehlgeschlagen ist
|
pfad: str,
|
||||||
return DateipruefErgebnis(
|
basis_pfad: str = "",
|
||||||
erfolgreich=False,
|
leereingabe_erlaubt: bool = False,
|
||||||
fehler=["Kein Pfad angegeben."]
|
standarddatei: str | None = None,
|
||||||
)
|
temporaer_erlaubt: bool = False,
|
||||||
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...
|
self.pfad = pfad
|
||||||
return DateipruefErgebnis(
|
self.basis_pfad = basis_pfad
|
||||||
erfolgreich=False,
|
self.leereingabe_erlaubt = leereingabe_erlaubt
|
||||||
fehler=["Standardpfad oder -name fehlen."]#..gib zurück, dass der Test fehlgeschlagen ist
|
self.standarddatei = standarddatei
|
||||||
)
|
self.temporaer_erlaubt = temporaer_erlaubt
|
||||||
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...
|
# Hilfsfunktion
|
||||||
return DateipruefErgebnis(
|
# ---------------------------------------------------------
|
||||||
erfolgreich=True,#ist die Prüfung erfolgreich, aber es muss noch eine Entscheidung verlangt werden
|
|
||||||
|
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,
|
pfad=pfad,
|
||||||
entscheidung=None,
|
|
||||||
fehler=["Datei existiert bereits – Entscheidung ausstehend."]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if vorhandene_datei_entscheidung == DateiEntscheidung.ABBRECHEN:
|
# -----------------------------------------------------
|
||||||
return DateipruefErgebnis(#...der Nutzer aber abgebrochen hat...
|
# 3. Datei existiert → Erfolg
|
||||||
erfolgreich=False,#ist die Prüfung fehlgeschlagen ISSUE: ergibt das Sinn?
|
# -----------------------------------------------------
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Datei gefunden.",
|
||||||
|
aktion="ok",
|
||||||
pfad=pfad,
|
pfad=pfad,
|
||||||
fehler=["Benutzer hat abgebrochen."]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return DateipruefErgebnis(
|
# ---------------------------------------------------------
|
||||||
erfolgreich=True,
|
# Behandlung leerer Eingaben
|
||||||
pfad=pfad,
|
# ---------------------------------------------------------
|
||||||
entscheidung=vorhandene_datei_entscheidung
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Pfad gültig und Datei nicht vorhanden
|
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
|
||||||
#wenn alle Varianten NICHT zutreffen, weil ein gültiger Pfad eingegeben wurde und die Datei noch nicht vorhanden ist:
|
if self.standarddatei:
|
||||||
return DateipruefErgebnis(
|
return pruef_ergebnis(
|
||||||
erfolgreich=True,
|
ok=False,
|
||||||
pfad=pfad
|
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
|
sn_basis/modules/pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten.
|
||||||
from modules.Dateipruefer import DateiEntscheidung
|
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):
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
self.iface = iface
|
|
||||||
self.plugin_pfad = plugin_pfad
|
|
||||||
|
|
||||||
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)
|
class Pruefmanager:
|
||||||
msg.setDefaultButton(YES)
|
"""
|
||||||
|
Verarbeitet pruef_ergebnis-Objekte und steuert die Nutzerinteraktion.
|
||||||
|
"""
|
||||||
|
|
||||||
msg.button(YES).setText("Ersetzen")
|
def __init__(self, ui_modus: str = "qgis"):
|
||||||
msg.button(NO).setText("Anhängen")
|
self.ui_modus = ui_modus
|
||||||
msg.button(CANCEL).setText("Abbrechen")
|
|
||||||
|
|
||||||
result = exec_dialog(msg)
|
# ---------------------------------------------------------
|
||||||
|
# Hauptfunktion
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
if result == YES:
|
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||||
return DateiEntscheidung.ERSETZEN
|
"""
|
||||||
elif result == NO:
|
Verarbeitet ein pruef_ergebnis und führt ggf. Nutzerinteraktion durch.
|
||||||
return DateiEntscheidung.ANHAENGEN
|
Rückgabe: neues oder unverändertes pruef_ergebnis.
|
||||||
else:
|
"""
|
||||||
return DateiEntscheidung.ABBRECHEN
|
|
||||||
|
|
||||||
def frage_temporär_verwenden(self) -> bool:
|
if ergebnis.ok:
|
||||||
msg = QMessageBox()
|
return ergebnis
|
||||||
msg.setIcon(ICON_QUESTION)
|
|
||||||
msg.setWindowTitle("Temporäre Layer")
|
|
||||||
msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?")
|
|
||||||
|
|
||||||
msg.setStandardButtons(YES | NO)
|
aktion = ergebnis.aktion
|
||||||
msg.setDefaultButton(YES)
|
|
||||||
|
|
||||||
result = exec_dialog(msg)
|
# -----------------------------------------------------
|
||||||
return result == YES
|
# Allgemeine Aktionen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str:
|
if aktion == "leer":
|
||||||
pfad, _ = QFileDialog.getSaveFileName(
|
warning("Eingabe fehlt", ergebnis.meldung)
|
||||||
parent=None,
|
return ergebnis
|
||||||
caption=titel,
|
|
||||||
directory=self.plugin_pfad or "",
|
if aktion == "leereingabe_erlaubt":
|
||||||
filter=filter
|
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
|
||||||
)
|
return pruef_ergebnis(True, "Ohne Eingabe fortgefahren.", "ok", None)
|
||||||
return pfad
|
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 (
|
from sn_basis.functions.syswrapper import (
|
||||||
QEventLoop,
|
file_exists,
|
||||||
QUrl,
|
is_file,
|
||||||
QNetworkRequest,
|
join_path,
|
||||||
QNetworkReply
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from qgis.core import QgsNetworkAccessManager
|
from sn_basis.functions.qgisqt_wrapper import (
|
||||||
from modules.pruef_ergebnis import PruefErgebnis
|
network_head,
|
||||||
|
)
|
||||||
|
|
||||||
|
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
class Linkpruefer:
|
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] = {
|
def __init__(self, basis_pfad: str | None = None):
|
||||||
"REST": "REST",
|
"""
|
||||||
"WFS": "WFS",
|
basis_pfad: optionaler Basisordner für relative Pfade.
|
||||||
"WMS": "WMS",
|
"""
|
||||||
"OGR": "OGR"
|
self.basis = basis_pfad
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, link: str, anbieter: str):
|
# ---------------------------------------------------------
|
||||||
self.link = link
|
# Hilfsfunktionen
|
||||||
self.anbieter = anbieter.upper().strip() if anbieter else ""
|
# ---------------------------------------------------------
|
||||||
self.network_manager = QgsNetworkAccessManager()
|
|
||||||
|
|
||||||
def klassifiziere_anbieter(self):
|
def _pfad(self, relativer_pfad: str) -> str:
|
||||||
typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter)
|
"""Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
|
||||||
quelle = "remote" if self.link.startswith(("http://", "https://")) else "local"
|
if not self.basis:
|
||||||
return {"typ": typ, "quelle": quelle}
|
return relativer_pfad
|
||||||
|
return join_path(self.basis, relativer_pfad)
|
||||||
|
|
||||||
def pruefe_link(self):
|
def _ist_url(self, text: str) -> bool:
|
||||||
fehler = []
|
"""Einfache URL-Erkennung."""
|
||||||
warnungen = []
|
return text.startswith("http://") or text.startswith("https://")
|
||||||
|
|
||||||
if not self.link:
|
# ---------------------------------------------------------
|
||||||
fehler.append("Link fehlt.")
|
# Hauptfunktion
|
||||||
return PruefErgebnis(False, fehler=fehler, warnungen=warnungen)
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
if not self.anbieter or not self.anbieter.strip():
|
def pruefe(self, eingabe: str) -> pruef_ergebnis:
|
||||||
fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.")
|
"""
|
||||||
|
Prüft einen Link (URL oder lokalen Pfad).
|
||||||
|
Rückgabe: pruef_ergebnis
|
||||||
|
"""
|
||||||
|
|
||||||
# Remote-Links prüfen
|
if not eingabe:
|
||||||
if self.link.startswith(("http://", "https://")):
|
return pruef_ergebnis(
|
||||||
request = QNetworkRequest(QUrl(self.link))
|
ok=False,
|
||||||
reply = self.network_manager.head(request)
|
meldung="Es wurde kein Link angegeben.",
|
||||||
|
aktion="leer",
|
||||||
loop = QEventLoop()
|
pfad=None,
|
||||||
reply.finished.connect(loop.quit)
|
|
||||||
loop.exec() # Qt5/Qt6-kompatibel über qt_compat
|
|
||||||
|
|
||||||
# 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}")
|
|
||||||
|
|
||||||
reply.deleteLater()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Lokale Pfade: Plausibilitätscheck
|
|
||||||
if "." not in self.link.split("/")[-1]:
|
|
||||||
warnungen.append("Der lokale Link sieht ungewöhnlich aus.")
|
|
||||||
|
|
||||||
return PruefErgebnis(
|
|
||||||
len(fehler) == 0,
|
|
||||||
daten=self.klassifiziere_anbieter(),
|
|
||||||
fehler=fehler,
|
|
||||||
warnungen=warnungen
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def ausfuehren(self):
|
# -----------------------------------------------------
|
||||||
return self.pruefe_link()
|
# 1. Fall: URL
|
||||||
|
# -----------------------------------------------------
|
||||||
|
if self._ist_url(eingabe):
|
||||||
|
return self._pruefe_url(eingabe)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2. Fall: lokaler Pfad
|
||||||
|
# -----------------------------------------------------
|
||||||
|
return self._pruefe_dateipfad(eingabe)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# URL‑Prüfung
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _pruefe_url(self, url: str) -> pruef_ergebnis:
|
||||||
|
"""
|
||||||
|
Prüft eine URL über einen HEAD‑Request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 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
|
sn_basis/modules/pruef_ergebnis.py – Ergebnisobjekt für alle Prüfer.
|
||||||
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})")
|
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
|
sn_basis/modules/stilpruefer.py – Prüfung und Anwendung von Layerstilen.
|
||||||
from modules.pruef_ergebnis import PruefErgebnis
|
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:
|
class Stilpruefer:
|
||||||
"""
|
"""
|
||||||
Prüft, ob ein angegebener Stilpfad gültig und nutzbar ist.
|
Prüft, ob ein Stil auf einen Layer angewendet werden kann.
|
||||||
- Wenn kein Stil angegeben ist, gilt die Prüfung als erfolgreich.
|
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
||||||
- Wenn angegeben:
|
|
||||||
* Datei muss existieren
|
|
||||||
* Dateiendung muss '.qml' sein
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def pruefe(self, stilpfad: str) -> PruefErgebnis:
|
def __init__(self, layer, stil_pfad: str):
|
||||||
# kein Stil angegeben -> erfolgreich, keine Warnung
|
"""
|
||||||
if not stilpfad or stilpfad.strip() == "":
|
layer: QGIS-Layer oder Mock-Layer
|
||||||
return PruefErgebnis(
|
stil_pfad: relativer oder absoluter Pfad zum .qml-Stil
|
||||||
erfolgreich=True,
|
"""
|
||||||
daten={"stil": None},
|
self.layer = layer
|
||||||
warnungen=["Kein Stil angegeben."]
|
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 = []
|
# Fehlerfall → Nutzerinteraktion nötig
|
||||||
warnungen = []
|
return pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
# Prüfung: Datei existiert?
|
meldung=meldung,
|
||||||
if not os.path.exists(stilpfad):
|
aktion="stil_nicht_anwendbar",
|
||||||
fehler.append(f"Stildatei nicht gefunden: {stilpfad}")
|
pfad=self.stil_pfad,
|
||||||
|
|
||||||
# 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
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,49 @@
|
|||||||
#run_tests.py
|
"""
|
||||||
import sys
|
sn_basis/test/run_tests.py
|
||||||
import os
|
|
||||||
|
Zentraler Test-Runner für sn_basis.
|
||||||
|
Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig.
|
||||||
|
"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Minimaler Bootstrap, um sn_basis importierbar zu machen
|
||||||
|
TEST_DIR = os.path.dirname(__file__)
|
||||||
|
PLUGIN_ROOT = os.path.abspath(os.path.join(TEST_DIR, "..", ".."))
|
||||||
|
|
||||||
|
if PLUGIN_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, PLUGIN_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
from sn_basis.functions import syswrapper
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Bootstrap: Plugin-Root in sys.path eintragen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def bootstrap():
|
||||||
|
"""
|
||||||
|
Simuliert das QGIS-Plugin-Startverhalten:
|
||||||
|
stellt sicher, dass sn_basis importierbar ist.
|
||||||
|
"""
|
||||||
|
plugin_root = syswrapper.get_plugin_root()
|
||||||
|
syswrapper.add_to_sys_path(plugin_root)
|
||||||
|
|
||||||
|
|
||||||
|
bootstrap()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
# Farben
|
# Farben
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
RED = "\033[91m"
|
RED = "\033[91m"
|
||||||
YELLOW = "\033[93m"
|
YELLOW = "\033[93m"
|
||||||
GREEN = "\033[92m"
|
GREEN = "\033[92m"
|
||||||
@@ -13,36 +51,30 @@ CYAN = "\033[96m"
|
|||||||
MAGENTA = "\033[95m"
|
MAGENTA = "\033[95m"
|
||||||
RESET = "\033[0m"
|
RESET = "\033[0m"
|
||||||
|
|
||||||
# Globaler Testzähler
|
|
||||||
GLOBAL_TEST_COUNTER = 0
|
GLOBAL_TEST_COUNTER = 0
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Eigene TestResult-Klasse (färbt Fehler/Skipped/OK)
|
# Farbige TestResult-Klasse
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
class ColoredTestResult(unittest.TextTestResult):
|
class ColoredTestResult(unittest.TextTestResult):
|
||||||
|
|
||||||
def startTest(self, test):
|
def startTest(self, test):
|
||||||
"""Vor jedem Test eine Nummer ausgeben."""
|
|
||||||
global GLOBAL_TEST_COUNTER
|
global GLOBAL_TEST_COUNTER
|
||||||
GLOBAL_TEST_COUNTER += 1
|
GLOBAL_TEST_COUNTER += 1
|
||||||
self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n")
|
self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n")
|
||||||
super().startTest(test)
|
super().startTest(test)
|
||||||
|
|
||||||
def startTestRun(self):
|
|
||||||
"""Wird einmal zu Beginn des gesamten Testlaufs ausgeführt."""
|
|
||||||
super().startTestRun()
|
|
||||||
|
|
||||||
def startTestClass(self, test):
|
def startTestClass(self, test):
|
||||||
"""Wird aufgerufen, wenn eine neue Testklasse beginnt."""
|
|
||||||
cls = test.__class__
|
cls = test.__class__
|
||||||
file = inspect.getfile(cls)
|
file = inspect.getfile(cls)
|
||||||
filename = os.path.basename(file)
|
filename = os.path.basename(file)
|
||||||
|
|
||||||
self.stream.write(
|
self.stream.write(
|
||||||
f"\n{MAGENTA}{'='*70}\n"
|
f"\n{MAGENTA}{'=' * 70}\n"
|
||||||
f"Starte Testklasse: {filename} → {cls.__name__}\n"
|
f"Starte Testklasse: {filename} → {cls.__name__}\n"
|
||||||
f"{'='*70}{RESET}\n"
|
f"{'=' * 70}{RESET}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
def addError(self, test, err):
|
def addError(self, test, err):
|
||||||
@@ -57,31 +89,27 @@ class ColoredTestResult(unittest.TextTestResult):
|
|||||||
super().addSkip(test, reason)
|
super().addSkip(test, reason)
|
||||||
self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n")
|
self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n")
|
||||||
|
|
||||||
# unittest ruft diese Methode nicht automatisch auf → wir patchen es unten
|
|
||||||
def addSuccess(self, test):
|
def addSuccess(self, test):
|
||||||
super().addSuccess(test)
|
super().addSuccess(test)
|
||||||
self.stream.write(f"{GREEN}OK{RESET}\n")
|
self.stream.write(f"{GREEN}OK{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Eigener TestRunner, der unser ColoredTestResult nutzt
|
# Farbiger TestRunner
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
class ColoredTestRunner(unittest.TextTestRunner):
|
class ColoredTestRunner(unittest.TextTestRunner):
|
||||||
resultclass = ColoredTestResult
|
resultclass = ColoredTestResult
|
||||||
|
|
||||||
def _makeResult(self):
|
def _makeResult(self):
|
||||||
result = super()._makeResult()
|
result = super()._makeResult()
|
||||||
|
|
||||||
# Patch: unittest ruft startTestClass nicht automatisch auf
|
|
||||||
original_start_test = result.startTest
|
original_start_test = result.startTest
|
||||||
|
|
||||||
def patched_start_test(test):
|
def patched_start_test(test):
|
||||||
# Wenn neue Klasse → Kopf ausgeben
|
|
||||||
if not hasattr(result, "_last_test_class") or \
|
if not hasattr(result, "_last_test_class") or \
|
||||||
result._last_test_class != test.__class__:
|
result._last_test_class != test.__class__:
|
||||||
result.startTestClass(test)
|
result.startTestClass(test)
|
||||||
result._last_test_class = test.__class__
|
result._last_test_class = test.__class__
|
||||||
|
|
||||||
original_start_test(test)
|
original_start_test(test)
|
||||||
|
|
||||||
result.startTest = patched_start_test
|
result.startTest = patched_start_test
|
||||||
@@ -91,35 +119,28 @@ class ColoredTestRunner(unittest.TextTestRunner):
|
|||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Testlauf starten
|
# Testlauf starten
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
print("\n" + "="*70)
|
|
||||||
print(f"{CYAN}Testlauf gestartet am: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}")
|
|
||||||
print("="*70 + "\n")
|
|
||||||
|
|
||||||
# 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():
|
def main():
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(
|
||||||
|
f"{CYAN}Testlauf gestartet am: "
|
||||||
|
f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}"
|
||||||
|
)
|
||||||
|
print("=" * 70 + "\n")
|
||||||
|
|
||||||
loader = unittest.TestLoader()
|
loader = unittest.TestLoader()
|
||||||
suite = unittest.TestSuite()
|
|
||||||
|
|
||||||
test_modules = [
|
suite = loader.discover(
|
||||||
"test_dateipruefer",
|
start_dir=os.path.dirname(__file__),
|
||||||
"test_stilpruefer",
|
pattern="test_*.py"
|
||||||
"test_linkpruefer",
|
)
|
||||||
"test_qt_compat",
|
|
||||||
"test_pruefmanager",
|
|
||||||
]
|
|
||||||
|
|
||||||
for mod_name in test_modules:
|
|
||||||
mod = __import__(mod_name)
|
|
||||||
suite.addTests(loader.loadTestsFromModule(mod))
|
|
||||||
|
|
||||||
runner = ColoredTestRunner(verbosity=2)
|
runner = ColoredTestRunner(verbosity=2)
|
||||||
runner.run(suite)
|
result = runner.run(suite)
|
||||||
|
|
||||||
|
# Exit-Code für CI / Skripte
|
||||||
|
return 0 if result.wasSuccessful() else 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
|
|||||||
2
test/test_bootstrap.py
Normal file
2
test/test_bootstrap.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from sn_basis.functions import syswrapper
|
||||||
|
syswrapper.add_to_sys_path(syswrapper.get_plugin_root())
|
||||||
@@ -1,89 +1,102 @@
|
|||||||
#test_dateipruefer.py
|
# sn_basis/test/test_dateipruefer.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
from unittest.mock import patch
|
||||||
import tempfile
|
|
||||||
import sys
|
from sn_basis.modules.Dateipruefer import Dateipruefer
|
||||||
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
||||||
sys.path.insert(0, ROOT)
|
|
||||||
from modules.Dateipruefer import (
|
|
||||||
Dateipruefer,
|
|
||||||
LeererPfadModus,
|
|
||||||
DateiEntscheidung,
|
|
||||||
DateipruefErgebnis
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDateipruefer(unittest.TestCase):
|
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(
|
# 1. Leere Eingabe erlaubt
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_leereingabe_erlaubt(self):
|
||||||
|
pruefer = Dateipruefer(
|
||||||
pfad="",
|
pfad="",
|
||||||
leer_modus=LeererPfadModus.VERBOTEN
|
leereingabe_erlaubt=True
|
||||||
)
|
)
|
||||||
self.assertFalse(result.erfolgreich)
|
|
||||||
self.assertIn("Kein Pfad angegeben.", result.fehler)
|
|
||||||
|
|
||||||
def test_standardpfad_wird_verwendet(self):
|
result = pruefer.pruefe()
|
||||||
result = self.pruefer.pruefe(
|
|
||||||
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "leereingabe_erlaubt")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2. Leere Eingabe nicht erlaubt
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_leereingabe_nicht_erlaubt(self):
|
||||||
|
pruefer = Dateipruefer(
|
||||||
pfad="",
|
pfad="",
|
||||||
leer_modus=LeererPfadModus.NUTZE_STANDARD,
|
leereingabe_erlaubt=False
|
||||||
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 = pruefer.pruefe()
|
||||||
result = self.pruefer.pruefe(
|
|
||||||
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "leereingabe_nicht_erlaubt")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 3. Standarddatei vorschlagen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_standarddatei_vorschlagen(self):
|
||||||
|
pruefer = Dateipruefer(
|
||||||
pfad="",
|
pfad="",
|
||||||
leer_modus=LeererPfadModus.TEMPORAER_ERLAUBT
|
standarddatei="/tmp/std.txt"
|
||||||
)
|
)
|
||||||
self.assertTrue(result.erfolgreich)
|
|
||||||
self.assertIsNone(result.pfad)
|
|
||||||
self.assertFalse(result.temporär)
|
|
||||||
|
|
||||||
def test_existierende_datei_ohne_entscheidung(self):
|
result = pruefer.pruefe()
|
||||||
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):
|
self.assertFalse(result.ok)
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
self.assertEqual(result.aktion, "standarddatei_vorschlagen")
|
||||||
tmp_path = tmp_file.name
|
self.assertEqual(result.pfad, "/tmp/std.txt")
|
||||||
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")
|
# 4. Temporäre Datei erlaubt
|
||||||
result = self.pruefer.pruefe(
|
# -----------------------------------------------------
|
||||||
pfad=fake_path,
|
def test_temporaer_erlaubt(self):
|
||||||
leer_modus=LeererPfadModus.VERBOTEN
|
pruefer = Dateipruefer(
|
||||||
|
pfad="",
|
||||||
|
temporaer_erlaubt=True
|
||||||
)
|
)
|
||||||
self.assertTrue(result.erfolgreich)
|
|
||||||
self.assertEqual(result.pfad, fake_path)
|
result = pruefer.pruefe()
|
||||||
|
|
||||||
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "temporaer_erlaubt")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 5. Datei existiert nicht
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists", return_value=False)
|
||||||
|
def test_datei_nicht_gefunden(self, mock_exists):
|
||||||
|
pruefer = Dateipruefer(
|
||||||
|
pfad="/tmp/nichtvorhanden.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pruefer.pruefe()
|
||||||
|
|
||||||
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "datei_nicht_gefunden")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 6. Datei existiert
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
|
||||||
|
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
|
||||||
|
def test_datei_ok(self, mock_isfile, mock_exists):
|
||||||
|
pruefer = Dateipruefer(
|
||||||
|
pfad="/tmp/test.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pruefer.pruefe()
|
||||||
|
|
||||||
|
self.assertTrue(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "ok")
|
||||||
|
self.assertEqual(result.pfad, "/tmp/test.txt")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
170
test/test_layerpruefer.py
Normal file
170
test/test_layerpruefer.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# sn_basis/test/test_layerpruefer.py
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Mock-Layer für Wrapper-Tests
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class MockLayer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
exists=True,
|
||||||
|
visible=True,
|
||||||
|
layer_type="vector",
|
||||||
|
geometry_type="Polygon",
|
||||||
|
feature_count=10,
|
||||||
|
crs="EPSG:25833",
|
||||||
|
fields=None,
|
||||||
|
source="/tmp/test.shp",
|
||||||
|
editable=True,
|
||||||
|
):
|
||||||
|
self.exists = exists
|
||||||
|
self.visible = visible
|
||||||
|
self.layer_type = layer_type
|
||||||
|
self.geometry_type = geometry_type
|
||||||
|
self.feature_count = feature_count
|
||||||
|
self.crs = crs
|
||||||
|
self.fields = fields or []
|
||||||
|
self.source = source
|
||||||
|
self.editable = editable
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Wrapper-Mocks (monkeypatching)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def mock_layer_exists(layer):
|
||||||
|
return layer is not None and layer.exists
|
||||||
|
|
||||||
|
|
||||||
|
def mock_is_layer_visible(layer):
|
||||||
|
return layer.visible
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_type(layer):
|
||||||
|
return layer.layer_type
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_geometry_type(layer):
|
||||||
|
return layer.geometry_type
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_feature_count(layer):
|
||||||
|
return layer.feature_count
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_crs(layer):
|
||||||
|
return layer.crs
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_fields(layer):
|
||||||
|
return layer.fields
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_source(layer):
|
||||||
|
return layer.source
|
||||||
|
|
||||||
|
|
||||||
|
def mock_is_layer_editable(layer):
|
||||||
|
return layer.editable
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Testklasse
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class TestLayerpruefer(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Monkeypatching der Wrapper-Funktionen
|
||||||
|
import sn_basis.functions.qgisqt_wrapper as wrapper
|
||||||
|
|
||||||
|
wrapper.layer_exists = mock_layer_exists
|
||||||
|
wrapper.is_layer_visible = mock_is_layer_visible
|
||||||
|
wrapper.get_layer_type = mock_get_layer_type
|
||||||
|
wrapper.get_layer_geometry_type = mock_get_layer_geometry_type
|
||||||
|
wrapper.get_layer_feature_count = mock_get_layer_feature_count
|
||||||
|
wrapper.get_layer_crs = mock_get_layer_crs
|
||||||
|
wrapper.get_layer_fields = mock_get_layer_fields
|
||||||
|
wrapper.get_layer_source = mock_get_layer_source
|
||||||
|
wrapper.is_layer_editable = mock_is_layer_editable
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
|
def test_layer_exists(self):
|
||||||
|
layer = MockLayer(exists=False)
|
||||||
|
pruefer = Layerpruefer(layer)
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "layer_nicht_gefunden")
|
||||||
|
|
||||||
|
def test_layer_unsichtbar(self):
|
||||||
|
layer = MockLayer(visible=False)
|
||||||
|
pruefer = Layerpruefer(layer, muss_sichtbar_sein=True)
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "layer_unsichtbar")
|
||||||
|
|
||||||
|
def test_falscher_layertyp(self):
|
||||||
|
layer = MockLayer(layer_type="raster")
|
||||||
|
pruefer = Layerpruefer(layer, erwarteter_layertyp="vector")
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "falscher_layertyp")
|
||||||
|
|
||||||
|
def test_falscher_geotyp(self):
|
||||||
|
layer = MockLayer(geometry_type="Point")
|
||||||
|
pruefer = Layerpruefer(layer, erwarteter_geotyp="Polygon")
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "falscher_geotyp")
|
||||||
|
|
||||||
|
def test_layer_leer(self):
|
||||||
|
layer = MockLayer(feature_count=0)
|
||||||
|
pruefer = Layerpruefer(layer)
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "layer_leer")
|
||||||
|
|
||||||
|
def test_falsches_crs(self):
|
||||||
|
layer = MockLayer(crs="EPSG:4326")
|
||||||
|
pruefer = Layerpruefer(layer, erwartetes_crs="EPSG:25833")
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "falsches_crs")
|
||||||
|
|
||||||
|
def test_felder_fehlen(self):
|
||||||
|
layer = MockLayer(fields=["id"])
|
||||||
|
pruefer = Layerpruefer(layer, erforderliche_felder=["id", "name"])
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "felder_fehlen")
|
||||||
|
|
||||||
|
def test_datenquelle_unerwartet(self):
|
||||||
|
layer = MockLayer(source="/tmp/test.shp")
|
||||||
|
pruefer = Layerpruefer(layer, erlaubte_datenquellen=["/tmp/allowed.shp"])
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "datenquelle_unerwartet")
|
||||||
|
|
||||||
|
def test_layer_nicht_editierbar(self):
|
||||||
|
layer = MockLayer(editable=False)
|
||||||
|
pruefer = Layerpruefer(layer, muss_editierbar_sein=True)
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "layer_nicht_editierbar")
|
||||||
|
|
||||||
|
def test_layer_ok(self):
|
||||||
|
layer = MockLayer()
|
||||||
|
pruefer = Layerpruefer(layer)
|
||||||
|
ergebnis = pruefer.pruefe()
|
||||||
|
self.assertTrue(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "ok")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,77 +1,107 @@
|
|||||||
#test_linkpruefer.py
|
# sn_basis/test/test_linkpruefer.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
# QGIS-Module mocken, damit der Import funktioniert
|
from sn_basis.modules.linkpruefer import Linkpruefer
|
||||||
with patch.dict("sys.modules", {
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
"qgis": MagicMock(),
|
|
||||||
"qgis.PyQt": MagicMock(),
|
|
||||||
"qgis.PyQt.QtCore": MagicMock(),
|
|
||||||
"qgis.PyQt.QtNetwork": MagicMock(),
|
|
||||||
"qgis.core": MagicMock(),
|
|
||||||
}):
|
|
||||||
from modules.linkpruefer import Linkpruefer
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Mock-Ergebnisse für network_head()
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
class MockResponseOK:
|
||||||
|
ok = True
|
||||||
|
status = 200
|
||||||
|
error = None
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponseNotFound:
|
||||||
|
ok = False
|
||||||
|
status = 404
|
||||||
|
error = "Not Found"
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponseConnectionError:
|
||||||
|
ok = False
|
||||||
|
status = None
|
||||||
|
error = "Connection refused"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Testklasse
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
class TestLinkpruefer(unittest.TestCase):
|
class TestLinkpruefer(unittest.TestCase):
|
||||||
|
|
||||||
@patch("modules.linkpruefer.QNetworkReply")
|
# -----------------------------------------------------
|
||||||
@patch("modules.linkpruefer.QNetworkRequest")
|
# 1. Remote-Link erreichbar
|
||||||
@patch("modules.linkpruefer.QUrl")
|
# -----------------------------------------------------
|
||||||
@patch("modules.linkpruefer.QEventLoop")
|
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
|
||||||
@patch("modules.linkpruefer.QgsNetworkAccessManager")
|
def test_remote_link_ok(self, mock_head):
|
||||||
def test_remote_link_ok(
|
mock_head.return_value = MockResponseOK()
|
||||||
self, mock_manager, mock_loop, mock_url, mock_request, mock_reply
|
|
||||||
):
|
|
||||||
# Setup: simulate successful HEAD request
|
|
||||||
reply_instance = MagicMock()
|
|
||||||
reply_instance.error.return_value = mock_reply.NetworkError.NoError
|
|
||||||
reply_instance.attribute.return_value = 200
|
|
||||||
|
|
||||||
mock_manager.return_value.head.return_value = reply_instance
|
|
||||||
|
|
||||||
lp = Linkpruefer("http://example.com", "REST")
|
lp = Linkpruefer("http://example.com", "REST")
|
||||||
result = lp.pruefe_link()
|
result = lp.pruefe()
|
||||||
|
|
||||||
self.assertTrue(result.erfolgreich)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.daten["quelle"], "remote")
|
self.assertEqual(result.aktion, "ok")
|
||||||
|
|
||||||
@patch("modules.linkpruefer.QNetworkReply")
|
# -----------------------------------------------------
|
||||||
@patch("modules.linkpruefer.QNetworkRequest")
|
# 2. Remote-Link nicht erreichbar
|
||||||
@patch("modules.linkpruefer.QUrl")
|
# -----------------------------------------------------
|
||||||
@patch("modules.linkpruefer.QEventLoop")
|
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
|
||||||
@patch("modules.linkpruefer.QgsNetworkAccessManager")
|
def test_remote_link_error(self, mock_head):
|
||||||
def test_remote_link_error(
|
mock_head.return_value = MockResponseConnectionError()
|
||||||
self, mock_manager, mock_loop, mock_url, mock_request, mock_reply
|
|
||||||
):
|
|
||||||
# Fake-Reply erzeugen
|
|
||||||
reply_instance = MagicMock()
|
|
||||||
reply_instance.error.return_value = mock_reply.NetworkError.ConnectionRefusedError
|
|
||||||
reply_instance.errorString.return_value = "Connection refused"
|
|
||||||
|
|
||||||
# WICHTIG: finished-Signal simulieren
|
|
||||||
reply_instance.finished = MagicMock()
|
|
||||||
reply_instance.finished.connect = MagicMock()
|
|
||||||
|
|
||||||
# Wenn loop.exec() aufgerufen wird, rufen wir loop.quit() sofort auf
|
|
||||||
mock_loop.return_value.exec.side_effect = lambda: mock_loop.return_value.quit()
|
|
||||||
|
|
||||||
# Manager gibt unser Fake-Reply zurück
|
|
||||||
mock_manager.return_value.head.return_value = reply_instance
|
|
||||||
|
|
||||||
lp = Linkpruefer("http://example.com", "REST")
|
lp = Linkpruefer("http://example.com", "REST")
|
||||||
result = lp.pruefe_link()
|
result = lp.pruefe()
|
||||||
|
|
||||||
self.assertFalse(result.erfolgreich)
|
self.assertFalse(result.ok)
|
||||||
self.assertIn("Verbindungsfehler", result.fehler[0])
|
self.assertEqual(result.aktion, "url_nicht_erreichbar")
|
||||||
|
self.assertIn("Connection refused", result.meldung)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 3. Remote-Link 404
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
|
||||||
|
def test_remote_link_404(self, mock_head):
|
||||||
|
mock_head.return_value = MockResponseNotFound()
|
||||||
|
|
||||||
|
lp = Linkpruefer("http://example.com/missing", "REST")
|
||||||
|
result = lp.pruefe()
|
||||||
|
|
||||||
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "url_nicht_erreichbar")
|
||||||
|
self.assertIn("404", result.meldung)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 4. Lokaler Pfad existiert nicht
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists")
|
||||||
|
def test_local_link_not_found(self, mock_exists):
|
||||||
|
mock_exists.return_value = False
|
||||||
|
|
||||||
|
lp = Linkpruefer("/path/to/missing/file.shp", "OGR")
|
||||||
|
result = lp.pruefe()
|
||||||
|
|
||||||
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "pfad_nicht_gefunden")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 5. Lokaler Pfad existiert, aber ungewöhnlich
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists")
|
||||||
|
def test_local_link_warning(self, mock_exists):
|
||||||
|
mock_exists.return_value = True
|
||||||
|
|
||||||
def test_local_link_warning(self):
|
|
||||||
lp = Linkpruefer("/path/to/file_without_extension", "OGR")
|
lp = Linkpruefer("/path/to/file_without_extension", "OGR")
|
||||||
result = lp.pruefe_link()
|
result = lp.pruefe()
|
||||||
|
|
||||||
self.assertTrue(result.erfolgreich)
|
self.assertTrue(result.ok)
|
||||||
self.assertIn("ungewöhnlich", result.warnungen[0])
|
self.assertEqual(result.aktion, "ok")
|
||||||
|
self.assertIn("ungewöhnlich", result.meldung)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,86 +1,133 @@
|
|||||||
#test_pruefmanager.py
|
# sn_basis/test/test_pruefmanager.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
from unittest.mock import patch
|
||||||
import sys
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
sys.path.insert(0, ROOT)
|
|
||||||
|
|
||||||
from modules.Pruefmanager import PruefManager
|
|
||||||
from modules.Dateipruefer import DateiEntscheidung
|
|
||||||
import modules.qt_compat as qt_compat
|
|
||||||
|
|
||||||
|
|
||||||
# Skip-Decorator für Mock-Modus
|
class TestPruefmanager(unittest.TestCase):
|
||||||
def skip_if_mock(reason):
|
|
||||||
return unittest.skipIf(
|
|
||||||
qt_compat.QT_VERSION == 0,
|
|
||||||
f"{reason} — MOCK-Modus erkannt. "
|
|
||||||
"Bitte diesen Test in einer echten QGIS-Umgebung ausführen."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPruefManager(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.manager = PruefManager(plugin_pfad="/tmp")
|
self.manager = Pruefmanager()
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Tests für frage_datei_ersetzen_oder_anhaengen
|
# 1. OK-Ergebnis → keine Interaktion
|
||||||
# ---------------------------------------------------------
|
# -----------------------------------------------------
|
||||||
|
def test_ok(self):
|
||||||
|
ergebnis = pruef_ergebnis(True, "Alles gut", "ok", None)
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
self.assertTrue(entscheidung.ok)
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.YES)
|
self.assertEqual(entscheidung.aktion, "ok")
|
||||||
def test_frage_datei_ersetzen(self, mock_exec):
|
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ERSETZEN)
|
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
# -----------------------------------------------------
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.NO)
|
# 2. Leere Eingabe erlaubt → Nutzer sagt JA
|
||||||
def test_frage_datei_anhaengen(self, mock_exec):
|
# -----------------------------------------------------
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ANHAENGEN)
|
def test_leereingabe_erlaubt_ja(self, mock_ask):
|
||||||
|
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
self.assertTrue(entscheidung.ok)
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.CANCEL)
|
self.assertEqual(entscheidung.aktion, "ok")
|
||||||
def test_frage_datei_abbrechen(self, mock_exec):
|
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ABBRECHEN)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Fehlerfall: exec_dialog liefert etwas Unerwartetes
|
# 3. Leere Eingabe erlaubt → Nutzer sagt NEIN
|
||||||
# ---------------------------------------------------------
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
||||||
|
def test_leereingabe_erlaubt_nein(self, mock_ask):
|
||||||
|
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
self.assertFalse(entscheidung.ok)
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=999)
|
self.assertEqual(entscheidung.aktion, "leereingabe_erlaubt")
|
||||||
def test_frage_datei_unbekannte_antwort(self, mock_exec):
|
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ABBRECHEN)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Tests für frage_temporär_verwenden
|
# 4. Standarddatei vorschlagen → Nutzer sagt JA
|
||||||
# ---------------------------------------------------------
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
||||||
|
def test_standarddatei_vorschlagen_ja(self, mock_ask):
|
||||||
|
ergebnis = pruef_ergebnis(False, "Standarddatei verwenden?", "standarddatei_vorschlagen", "/tmp/std.txt")
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
self.assertTrue(entscheidung.ok)
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.YES)
|
self.assertEqual(entscheidung.aktion, "ok")
|
||||||
def test_frage_temporär_verwenden_ja(self, mock_exec):
|
self.assertEqual(entscheidung.pfad, "/tmp/std.txt")
|
||||||
self.assertTrue(self.manager.frage_temporär_verwenden())
|
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
# -----------------------------------------------------
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.NO)
|
# 5. Standarddatei vorschlagen → Nutzer sagt NEIN
|
||||||
def test_frage_temporär_verwenden_nein(self, mock_exec):
|
# -----------------------------------------------------
|
||||||
self.assertFalse(self.manager.frage_temporär_verwenden())
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
||||||
|
def test_standarddatei_vorschlagen_nein(self, mock_ask):
|
||||||
|
ergebnis = pruef_ergebnis(False, "Standarddatei verwenden?", "standarddatei_vorschlagen", "/tmp/std.txt")
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
self.assertFalse(entscheidung.ok)
|
||||||
# Fehlerfall: exec_dialog liefert etwas Unerwartetes
|
self.assertEqual(entscheidung.aktion, "standarddatei_vorschlagen")
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
# -----------------------------------------------------
|
||||||
@patch("modules.qt_compat.exec_dialog", return_value=None)
|
# 6. Temporäre Datei erzeugen → Nutzer sagt JA
|
||||||
def test_frage_temporär_verwenden_unbekannt(self, mock_exec):
|
# -----------------------------------------------------
|
||||||
self.assertFalse(self.manager.frage_temporär_verwenden())
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
||||||
|
def test_temporaer_erlaubt_ja(self, mock_ask):
|
||||||
|
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
|
self.assertTrue(entscheidung.ok)
|
||||||
|
self.assertEqual(entscheidung.aktion, "temporaer_erzeugen")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 7. Temporäre Datei erzeugen → Nutzer sagt NEIN
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
||||||
|
def test_temporaer_erlaubt_nein(self, mock_ask):
|
||||||
|
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
|
self.assertFalse(entscheidung.ok)
|
||||||
|
self.assertEqual(entscheidung.aktion, "temporaer_erlaubt")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 8. Layer unsichtbar → Nutzer sagt JA
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.set_layer_visible")
|
||||||
|
def test_layer_unsichtbar_ja(self, mock_set, mock_ask):
|
||||||
|
fake_layer = object()
|
||||||
|
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
||||||
|
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
|
mock_set.assert_called_once_with(fake_layer, True)
|
||||||
|
self.assertTrue(entscheidung.ok)
|
||||||
|
self.assertEqual(entscheidung.aktion, "ok")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 9. Layer unsichtbar → Nutzer sagt NEIN
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
||||||
|
def test_layer_unsichtbar_nein(self, mock_ask):
|
||||||
|
fake_layer = object()
|
||||||
|
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
||||||
|
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
|
self.assertFalse(entscheidung.ok)
|
||||||
|
self.assertEqual(entscheidung.aktion, "layer_unsichtbar")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 10. Fehlerhafte Aktion → Fallback
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.warning")
|
||||||
|
def test_unbekannte_aktion(self, mock_warn):
|
||||||
|
ergebnis = pruef_ergebnis(False, "???", "unbekannt", None)
|
||||||
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
|
mock_warn.assert_called_once()
|
||||||
|
self.assertFalse(entscheidung.ok)
|
||||||
|
self.assertEqual(entscheidung.aktion, "unbekannt")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
#test_qt_compat.py
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
import modules.qt_compat as qt_compat
|
|
||||||
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
|
||||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
||||||
sys.path.insert(0, ROOT)
|
|
||||||
|
|
||||||
def skip_if_mock(reason):
|
|
||||||
"""Decorator: überspringt Test, wenn qt_compat im Mock-Modus läuft."""
|
|
||||||
return unittest.skipIf(
|
|
||||||
qt_compat.QT_VERSION == 0,
|
|
||||||
f"{reason} — MOCK-Modus erkannt."
|
|
||||||
f"Bitte diesen Test in einer echten QGIS-Umgebung ausführen."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestQtCompat(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_exports_exist(self):
|
|
||||||
"""Prüft, ob alle erwarteten Symbole exportiert werden."""
|
|
||||||
expected = {
|
|
||||||
"QMessageBox",
|
|
||||||
"QFileDialog",
|
|
||||||
"QEventLoop",
|
|
||||||
"QUrl",
|
|
||||||
"QNetworkRequest",
|
|
||||||
"QNetworkReply",
|
|
||||||
"YES",
|
|
||||||
"NO",
|
|
||||||
"CANCEL",
|
|
||||||
"ICON_QUESTION",
|
|
||||||
"exec_dialog",
|
|
||||||
"QT_VERSION",
|
|
||||||
}
|
|
||||||
|
|
||||||
for symbol in expected:
|
|
||||||
self.assertTrue(
|
|
||||||
hasattr(qt_compat, symbol),
|
|
||||||
f"qt_compat sollte '{symbol}' exportieren"
|
|
||||||
)
|
|
||||||
|
|
||||||
@skip_if_mock("QT_VERSION kann im Mock-Modus nicht 5 oder 6 sein")
|
|
||||||
def test_qt_version_flag(self):
|
|
||||||
"""QT_VERSION muss 5 oder 6 sein."""
|
|
||||||
self.assertIn(qt_compat.QT_VERSION, (5, 6))
|
|
||||||
|
|
||||||
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
|
||||||
def test_enums_are_valid(self):
|
|
||||||
"""Prüft, ob die Enums gültige QMessageBox-Werte sind."""
|
|
||||||
|
|
||||||
msg = qt_compat.QMessageBox()
|
|
||||||
try:
|
|
||||||
msg.setStandardButtons(
|
|
||||||
qt_compat.YES |
|
|
||||||
qt_compat.NO |
|
|
||||||
qt_compat.CANCEL
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.fail(f"Qt-Enums sollten OR-kombinierbar sein, Fehler: {e}")
|
|
||||||
|
|
||||||
self.assertTrue(True)
|
|
||||||
|
|
||||||
@skip_if_mock("exec_dialog benötigt echtes Qt-Verhalten")
|
|
||||||
def test_exec_dialog_calls_correct_method(self):
|
|
||||||
"""Prüft, ob exec_dialog() die richtige Methode aufruft."""
|
|
||||||
|
|
||||||
mock_msg = MagicMock()
|
|
||||||
|
|
||||||
if qt_compat.QT_VERSION == 6:
|
|
||||||
qt_compat.exec_dialog(mock_msg)
|
|
||||||
mock_msg.exec.assert_called_once()
|
|
||||||
|
|
||||||
elif qt_compat.QT_VERSION == 5:
|
|
||||||
qt_compat.exec_dialog(mock_msg)
|
|
||||||
mock_msg.exec_.assert_called_once()
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.fail("QT_VERSION hat einen unerwarteten Wert.")
|
|
||||||
|
|
||||||
@skip_if_mock("Qt-Klassen können im Mock-Modus nicht real instanziiert werden")
|
|
||||||
def test_qt_classes_importable(self):
|
|
||||||
"""Prüft, ob die wichtigsten Qt-Klassen instanziierbar sind."""
|
|
||||||
|
|
||||||
loop = qt_compat.QEventLoop()
|
|
||||||
self.assertIsNotNone(loop)
|
|
||||||
|
|
||||||
url = qt_compat.QUrl("http://example.com")
|
|
||||||
self.assertTrue(url.isValid())
|
|
||||||
|
|
||||||
req = qt_compat.QNetworkRequest(url)
|
|
||||||
self.assertIsNotNone(req)
|
|
||||||
|
|
||||||
self.assertTrue(hasattr(qt_compat.QNetworkReply, "NetworkError"))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
60
test/test_settings_logic.py
Normal file
60
test/test_settings_logic.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# sn_basis/test/test_settings_logic.py
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from sn_basis.functions.settings_logic import SettingsLogic
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettingsLogic(unittest.TestCase):
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# Test: load() liest alle Variablen über get_variable()
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.get_variable")
|
||||||
|
def test_load(self, mock_get):
|
||||||
|
# Mock-Rückgabe für jede Variable
|
||||||
|
mock_get.side_effect = lambda key, scope="project": f"wert_{key}"
|
||||||
|
|
||||||
|
logic = SettingsLogic()
|
||||||
|
daten = logic.load()
|
||||||
|
|
||||||
|
# Alle Variablen müssen enthalten sein
|
||||||
|
for key in SettingsLogic.VARIABLEN:
|
||||||
|
self.assertIn(key, daten)
|
||||||
|
self.assertEqual(daten[key], f"wert_{key}")
|
||||||
|
|
||||||
|
# get_variable muss für jede Variable genau einmal aufgerufen werden
|
||||||
|
self.assertEqual(mock_get.call_count, len(SettingsLogic.VARIABLEN))
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# Test: save() ruft set_variable() nur für bekannte Keys auf
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.qgisqt_wrapper.set_variable")
|
||||||
|
def test_save(self, mock_set):
|
||||||
|
logic = SettingsLogic()
|
||||||
|
|
||||||
|
# Eingabedaten enthalten gültige und ungültige Keys
|
||||||
|
daten = {
|
||||||
|
"amt": "A1",
|
||||||
|
"behoerde": "B1",
|
||||||
|
"unbekannt": "IGNORIEREN",
|
||||||
|
"gemeinden": "G1",
|
||||||
|
}
|
||||||
|
|
||||||
|
logic.save(daten)
|
||||||
|
|
||||||
|
# set_variable muss nur für gültige Keys aufgerufen werden
|
||||||
|
expected_calls = 3 # amt, behoerde, gemeinden
|
||||||
|
self.assertEqual(mock_set.call_count, expected_calls)
|
||||||
|
|
||||||
|
# Prüfen, ob die richtigen Keys gespeichert wurden
|
||||||
|
saved_keys = [call.args[0] for call in mock_set.call_args_list]
|
||||||
|
self.assertIn("amt", saved_keys)
|
||||||
|
self.assertIn("behoerde", saved_keys)
|
||||||
|
self.assertIn("gemeinden", saved_keys)
|
||||||
|
self.assertNotIn("unbekannt", saved_keys)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,50 +1,79 @@
|
|||||||
#test_stilpruefer.py
|
# sn_basis/test/test_stilpruefer.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
import sys
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
|
||||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
||||||
sys.path.insert(0, ROOT)
|
|
||||||
from modules.stilpruefer import Stilpruefer
|
|
||||||
from modules.pruef_ergebnis import PruefErgebnis
|
|
||||||
class TestStilpruefer(unittest.TestCase):
|
class TestStilpruefer(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.pruefer = Stilpruefer()
|
self.pruefer = Stilpruefer()
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 1. Keine Datei angegeben
|
||||||
|
# -----------------------------------------------------
|
||||||
def test_keine_datei_angegeben(self):
|
def test_keine_datei_angegeben(self):
|
||||||
result = self.pruefer.pruefe("")
|
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):
|
self.assertTrue(result.ok)
|
||||||
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp_file:
|
self.assertEqual(result.aktion, "ok")
|
||||||
tmp_path = tmp_file.name
|
self.assertIn("Kein Stil angegeben", result.meldung)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2. Datei existiert und ist .qml
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
|
||||||
|
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
|
||||||
|
def test_datei_existiert_mit_qml(self, mock_isfile, mock_exists):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.pruefer.pruefe(tmp_path)
|
result = self.pruefer.pruefe(tmp_path)
|
||||||
self.assertTrue(result.erfolgreich)
|
|
||||||
self.assertEqual(result.daten["stil"], tmp_path)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.fehler, [])
|
self.assertEqual(result.aktion, "ok")
|
||||||
|
self.assertEqual(result.pfad, tmp_path)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
|
|
||||||
def test_datei_existiert_falsche_endung(self):
|
# -----------------------------------------------------
|
||||||
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp_file:
|
# 3. Datei existiert, aber falsche Endung
|
||||||
tmp_path = tmp_file.name
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
|
||||||
|
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
|
||||||
|
def test_datei_existiert_falsche_endung(self, mock_isfile, mock_exists):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.pruefer.pruefe(tmp_path)
|
result = self.pruefer.pruefe(tmp_path)
|
||||||
self.assertFalse(result.erfolgreich)
|
|
||||||
self.assertIn("Ungültige Dateiendung", result.fehler[0])
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "falsche_endung")
|
||||||
|
self.assertIn(".qml", result.meldung)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
|
|
||||||
def test_datei_existiert_nicht(self):
|
# -----------------------------------------------------
|
||||||
fake_path = os.path.join(tempfile.gettempdir(), "nichtvorhanden.qml")
|
# 4. Datei existiert nicht
|
||||||
|
# -----------------------------------------------------
|
||||||
|
@patch("sn_basis.functions.syswrapper.file_exists", return_value=False)
|
||||||
|
def test_datei_existiert_nicht(self, mock_exists):
|
||||||
|
fake_path = "/tmp/nichtvorhanden.qml"
|
||||||
|
|
||||||
result = self.pruefer.pruefe(fake_path)
|
result = self.pruefer.pruefe(fake_path)
|
||||||
self.assertFalse(result.erfolgreich)
|
|
||||||
self.assertIn("Stildatei nicht gefunden", result.fehler[0])
|
self.assertFalse(result.ok)
|
||||||
|
self.assertEqual(result.aktion, "datei_nicht_gefunden")
|
||||||
|
self.assertIn("nicht gefunden", result.meldung)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
164
test/test_wrapper.py
Normal file
164
test/test_wrapper.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# sn_basis/test/test_wrapper.py
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
# Wrapper importieren
|
||||||
|
import sn_basis.functions.syswrapper as syswrapper
|
||||||
|
import sn_basis.functions.qgisqt_wrapper as qgisqt
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Mock-Layer für qgisqt_wrapper
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class MockLayer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
exists=True,
|
||||||
|
visible=True,
|
||||||
|
layer_type="vector",
|
||||||
|
geometry_type="Polygon",
|
||||||
|
feature_count=10,
|
||||||
|
crs="EPSG:25833",
|
||||||
|
fields=None,
|
||||||
|
source="/tmp/test.shp",
|
||||||
|
editable=True,
|
||||||
|
):
|
||||||
|
self.exists = exists
|
||||||
|
self.visible = visible
|
||||||
|
self.layer_type = layer_type
|
||||||
|
self.geometry_type = geometry_type
|
||||||
|
self.feature_count = feature_count
|
||||||
|
self.crs = crs
|
||||||
|
self.fields = fields or []
|
||||||
|
self.source = source
|
||||||
|
self.editable = editable
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Monkeypatching für qgisqt_wrapper
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def mock_layer_exists(layer):
|
||||||
|
return layer is not None and layer.exists
|
||||||
|
|
||||||
|
|
||||||
|
def mock_is_layer_visible(layer):
|
||||||
|
return layer.visible
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_type(layer):
|
||||||
|
return layer.layer_type
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_geometry_type(layer):
|
||||||
|
return layer.geometry_type
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_feature_count(layer):
|
||||||
|
return layer.feature_count
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_crs(layer):
|
||||||
|
return layer.crs
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_fields(layer):
|
||||||
|
return layer.fields
|
||||||
|
|
||||||
|
|
||||||
|
def mock_get_layer_source(layer):
|
||||||
|
return layer.source
|
||||||
|
|
||||||
|
|
||||||
|
def mock_is_layer_editable(layer):
|
||||||
|
return layer.editable
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Testklasse
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class TestWrapper(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# qgisqt_wrapper monkeypatchen
|
||||||
|
qgisqt.layer_exists = mock_layer_exists
|
||||||
|
qgisqt.is_layer_visible = mock_is_layer_visible
|
||||||
|
qgisqt.get_layer_type = mock_get_layer_type
|
||||||
|
qgisqt.get_layer_geometry_type = mock_get_layer_geometry_type
|
||||||
|
qgisqt.get_layer_feature_count = mock_get_layer_feature_count
|
||||||
|
qgisqt.get_layer_crs = mock_get_layer_crs
|
||||||
|
qgisqt.get_layer_fields = mock_get_layer_fields
|
||||||
|
qgisqt.get_layer_source = mock_get_layer_source
|
||||||
|
qgisqt.is_layer_editable = mock_is_layer_editable
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# syswrapper Tests
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
|
def test_syswrapper_file_exists(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=True) as tmp:
|
||||||
|
self.assertTrue(syswrapper.file_exists(tmp.name))
|
||||||
|
self.assertFalse(syswrapper.file_exists("/path/does/not/exist"))
|
||||||
|
|
||||||
|
def test_syswrapper_is_file(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=True) as tmp:
|
||||||
|
self.assertTrue(syswrapper.is_file(tmp.name))
|
||||||
|
self.assertFalse(syswrapper.is_file("/path/does/not/exist"))
|
||||||
|
|
||||||
|
def test_syswrapper_join_path(self):
|
||||||
|
result = syswrapper.join_path("/tmp", "test.txt")
|
||||||
|
self.assertEqual(result, "/tmp/test.txt")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# qgisqt_wrapper Tests (Mock-Modus)
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
|
def test_qgisqt_layer_exists(self):
|
||||||
|
layer = MockLayer(exists=True)
|
||||||
|
self.assertTrue(qgisqt.layer_exists(layer))
|
||||||
|
|
||||||
|
layer = MockLayer(exists=False)
|
||||||
|
self.assertFalse(qgisqt.layer_exists(layer))
|
||||||
|
|
||||||
|
def test_qgisqt_layer_visible(self):
|
||||||
|
layer = MockLayer(visible=True)
|
||||||
|
self.assertTrue(qgisqt.is_layer_visible(layer))
|
||||||
|
|
||||||
|
layer = MockLayer(visible=False)
|
||||||
|
self.assertFalse(qgisqt.is_layer_visible(layer))
|
||||||
|
|
||||||
|
def test_qgisqt_layer_type(self):
|
||||||
|
layer = MockLayer(layer_type="vector")
|
||||||
|
self.assertEqual(qgisqt.get_layer_type(layer), "vector")
|
||||||
|
|
||||||
|
def test_qgisqt_geometry_type(self):
|
||||||
|
layer = MockLayer(geometry_type="Polygon")
|
||||||
|
self.assertEqual(qgisqt.get_layer_geometry_type(layer), "Polygon")
|
||||||
|
|
||||||
|
def test_qgisqt_feature_count(self):
|
||||||
|
layer = MockLayer(feature_count=12)
|
||||||
|
self.assertEqual(qgisqt.get_layer_feature_count(layer), 12)
|
||||||
|
|
||||||
|
def test_qgisqt_crs(self):
|
||||||
|
layer = MockLayer(crs="EPSG:4326")
|
||||||
|
self.assertEqual(qgisqt.get_layer_crs(layer), "EPSG:4326")
|
||||||
|
|
||||||
|
def test_qgisqt_fields(self):
|
||||||
|
layer = MockLayer(fields=["id", "name"])
|
||||||
|
self.assertEqual(qgisqt.get_layer_fields(layer), ["id", "name"])
|
||||||
|
|
||||||
|
def test_qgisqt_source(self):
|
||||||
|
layer = MockLayer(source="/tmp/test.shp")
|
||||||
|
self.assertEqual(qgisqt.get_layer_source(layer), "/tmp/test.shp")
|
||||||
|
|
||||||
|
def test_qgisqt_editable(self):
|
||||||
|
layer = MockLayer(editable=True)
|
||||||
|
self.assertTrue(qgisqt.is_layer_editable(layer))
|
||||||
|
|
||||||
|
layer = MockLayer(editable=False)
|
||||||
|
self.assertFalse(qgisqt.is_layer_editable(layer))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,28 +1,73 @@
|
|||||||
|
# sn_basis/ui/base_dockwidget.py
|
||||||
|
|
||||||
from qgis.PyQt.QtWidgets import QDockWidget, QTabWidget
|
from qgis.PyQt.QtWidgets import QDockWidget, QTabWidget
|
||||||
|
from sn_basis.functions.qgisqt_wrapper import warning, error
|
||||||
|
|
||||||
|
|
||||||
class BaseDockWidget(QDockWidget):
|
class BaseDockWidget(QDockWidget):
|
||||||
|
"""
|
||||||
|
Basis-Dockwidget für alle LNO-Module.
|
||||||
|
- Titel wird automatisch aus base_title + subtitle erzeugt
|
||||||
|
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
|
||||||
|
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
|
||||||
|
"""
|
||||||
|
|
||||||
base_title = "LNO Sachsen"
|
base_title = "LNO Sachsen"
|
||||||
tabs = []
|
tabs = [] # Liste von Tab-Klassen
|
||||||
action = None # Referenz auf die Toolbar-Action
|
action = None # Referenz auf die Toolbar-Action
|
||||||
|
|
||||||
def __init__(self, parent=None, subtitle=""):
|
def __init__(self, parent=None, subtitle=""):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# Titel zusammensetzen
|
# -----------------------------------------------------
|
||||||
|
# Titel setzen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
title = self.base_title if not subtitle else f"{self.base_title} | {subtitle}"
|
title = self.base_title if not subtitle else f"{self.base_title} | {subtitle}"
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
|
except Exception as e:
|
||||||
|
warning("Titel konnte nicht gesetzt werden", str(e))
|
||||||
|
|
||||||
# Dock fixieren (nur schließen erlaubt)
|
# -----------------------------------------------------
|
||||||
|
# Dock-Features
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable)
|
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable)
|
||||||
|
except Exception as e:
|
||||||
|
warning("Dock-Features konnten nicht gesetzt werden", str(e))
|
||||||
|
|
||||||
# Tabs hinzufügen
|
# -----------------------------------------------------
|
||||||
|
# Tabs erzeugen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
tab_widget = QTabWidget()
|
tab_widget = QTabWidget()
|
||||||
|
|
||||||
for tab_class in self.tabs:
|
for tab_class in self.tabs:
|
||||||
tab_widget.addTab(tab_class(), getattr(tab_class, "tab_title", tab_class.__name__))
|
try:
|
||||||
|
tab_instance = tab_class()
|
||||||
|
tab_title = getattr(tab_class, "tab_title", tab_class.__name__)
|
||||||
|
tab_widget.addTab(tab_instance, tab_title)
|
||||||
|
except Exception as e:
|
||||||
|
error("Tab konnte nicht geladen werden", f"{tab_class}: {e}")
|
||||||
|
|
||||||
self.setWidget(tab_widget)
|
self.setWidget(tab_widget)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error("Tab-Widget konnte nicht initialisiert werden", str(e))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Dock schließen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Wird aufgerufen, wenn das Dock geschlossen wird."""
|
"""
|
||||||
|
Wird aufgerufen, wenn das Dock geschlossen wird.
|
||||||
|
Setzt die zugehörige Toolbar-Action zurück.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
if self.action:
|
if self.action:
|
||||||
self.action.setChecked(False) # Toolbar-Button zurücksetzen
|
self.action.setChecked(False)
|
||||||
|
except Exception as e:
|
||||||
|
warning("Toolbar-Status konnte nicht zurückgesetzt werden", str(e))
|
||||||
|
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
|
# sn_basis/ui/dockmanager.py
|
||||||
|
|
||||||
from qgis.PyQt.QtCore import Qt
|
from qgis.PyQt.QtCore import Qt
|
||||||
from qgis.PyQt.QtWidgets import QDockWidget
|
from qgis.PyQt.QtWidgets import QDockWidget
|
||||||
from qgis.utils import iface
|
from qgis.utils import iface
|
||||||
|
|
||||||
|
from sn_basis.functions.qgisqt_wrapper import warning, error
|
||||||
|
|
||||||
|
|
||||||
class DockManager:
|
class DockManager:
|
||||||
|
"""
|
||||||
|
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
|
||||||
|
Stellt sicher, dass immer nur ein LNO-Dock gleichzeitig sichtbar ist.
|
||||||
|
"""
|
||||||
|
|
||||||
default_area = Qt.DockWidgetArea.RightDockWidgetArea
|
default_area = Qt.DockWidgetArea.RightDockWidgetArea
|
||||||
|
dock_prefix = "sn_dock_"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def show(cls, dock_widget, area=None):
|
def show(cls, dock_widget, area=None):
|
||||||
|
"""
|
||||||
|
Zeigt ein DockWidget an und entfernt vorher alle anderen
|
||||||
|
LNO-Docks (erkennbar am Prefix 'sn_dock_').
|
||||||
|
"""
|
||||||
|
if dock_widget is None:
|
||||||
|
error("Dock konnte nicht angezeigt werden", "Dock-Widget ist None.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
area = area or cls.default_area
|
area = area or cls.default_area
|
||||||
|
|
||||||
# Bestehende Plugin-Docks mit Präfix schließen
|
# Prüfen, ob das Dock einen gültigen Namen hat
|
||||||
|
if not dock_widget.objectName():
|
||||||
|
dock_widget.setObjectName(f"{cls.dock_prefix}{id(dock_widget)}")
|
||||||
|
|
||||||
|
# Bestehende Plugin-Docks schließen
|
||||||
|
try:
|
||||||
for widget in iface.mainWindow().findChildren(QDockWidget):
|
for widget in iface.mainWindow().findChildren(QDockWidget):
|
||||||
if widget is not dock_widget and widget.objectName().startswith("sn_dock_"):
|
if widget is not dock_widget and widget.objectName().startswith(cls.dock_prefix):
|
||||||
iface.removeDockWidget(widget)
|
iface.removeDockWidget(widget)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
except Exception as e:
|
||||||
|
warning("Vorherige Docks konnten nicht entfernt werden", str(e))
|
||||||
|
|
||||||
# Neues Dock anzeigen
|
# Neues Dock anzeigen
|
||||||
|
try:
|
||||||
iface.addDockWidget(area, dock_widget)
|
iface.addDockWidget(area, dock_widget)
|
||||||
dock_widget.show()
|
dock_widget.show()
|
||||||
|
except Exception as e:
|
||||||
|
error("Dock konnte nicht angezeigt werden", str(e))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error("DockManager-Fehler", str(e))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#sn_basis/ui/navigation.py
|
||||||
from qgis.PyQt.QtWidgets import QAction, QMenu, QToolBar, QActionGroup
|
from qgis.PyQt.QtWidgets import QAction, QMenu, QToolBar, QActionGroup
|
||||||
|
|
||||||
class Navigation:
|
class Navigation:
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
from qgis.PyQt.QtWidgets import (
|
# sn_basis/ui/tabs/settings_tab.py
|
||||||
|
|
||||||
|
from sn_basis.functions.qgisqt_wrapper import (
|
||||||
QWidget, QGridLayout, QLabel, QLineEdit,
|
QWidget, QGridLayout, QLabel, QLineEdit,
|
||||||
QGroupBox, QVBoxLayout, QPushButton
|
QGroupBox, QVBoxLayout, QPushButton,
|
||||||
|
info, warning, error
|
||||||
)
|
)
|
||||||
|
|
||||||
from sn_basis.functions.settings_logic import SettingsLogic
|
from sn_basis.functions.settings_logic import SettingsLogic
|
||||||
|
|
||||||
|
|
||||||
class SettingsTab(QWidget):
|
class SettingsTab(QWidget):
|
||||||
tab_title = "Projekteigenschaften" # Titel für den Tab
|
"""
|
||||||
|
Tab für benutzer- und projektspezifische Einstellungen.
|
||||||
|
Nutzt SettingsLogic für das Laden/Speichern und den Wrapper für Meldungen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tab_title = "Projekteigenschaften"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -14,13 +23,16 @@ class SettingsTab(QWidget):
|
|||||||
|
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
# Definition der Felder
|
# Definition der Felder
|
||||||
|
# -----------------------------------------------------
|
||||||
self.user_fields = {
|
self.user_fields = {
|
||||||
"amt": "Amt:",
|
"amt": "Amt:",
|
||||||
"behoerde": "Behörde:",
|
"behoerde": "Behörde:",
|
||||||
"landkreis_user": "Landkreis:",
|
"landkreis_user": "Landkreis:",
|
||||||
"sachgebiet": "Sachgebiet:"
|
"sachgebiet": "Sachgebiet:"
|
||||||
}
|
}
|
||||||
|
|
||||||
self.project_fields = {
|
self.project_fields = {
|
||||||
"bezeichnung": "Bezeichnung:",
|
"bezeichnung": "Bezeichnung:",
|
||||||
"verfahrensnummer": "Verfahrensnummer:",
|
"verfahrensnummer": "Verfahrensnummer:",
|
||||||
@@ -28,45 +40,90 @@ class SettingsTab(QWidget):
|
|||||||
"landkreise_proj": "Landkreis(e):"
|
"landkreise_proj": "Landkreis(e):"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 🟦 Benutzerspezifische Festlegungen
|
# -----------------------------------------------------
|
||||||
|
# Benutzer-Felder
|
||||||
|
# -----------------------------------------------------
|
||||||
user_group = QGroupBox("Benutzerspezifische Festlegungen")
|
user_group = QGroupBox("Benutzerspezifische Festlegungen")
|
||||||
user_layout = QGridLayout()
|
user_layout = QGridLayout()
|
||||||
self.user_inputs = {}
|
self.user_inputs = {}
|
||||||
|
|
||||||
for row, (key, label) in enumerate(self.user_fields.items()):
|
for row, (key, label) in enumerate(self.user_fields.items()):
|
||||||
self.user_inputs[key] = QLineEdit()
|
line_edit = QLineEdit()
|
||||||
|
self.user_inputs[key] = line_edit
|
||||||
user_layout.addWidget(QLabel(label), row, 0)
|
user_layout.addWidget(QLabel(label), row, 0)
|
||||||
user_layout.addWidget(self.user_inputs[key], row, 1)
|
user_layout.addWidget(line_edit, row, 1)
|
||||||
|
|
||||||
user_group.setLayout(user_layout)
|
user_group.setLayout(user_layout)
|
||||||
|
|
||||||
# 🟨 Projektspezifische Festlegungen
|
# -----------------------------------------------------
|
||||||
|
# Projekt-Felder
|
||||||
|
# -----------------------------------------------------
|
||||||
project_group = QGroupBox("Projektspezifische Festlegungen")
|
project_group = QGroupBox("Projektspezifische Festlegungen")
|
||||||
project_layout = QGridLayout()
|
project_layout = QGridLayout()
|
||||||
self.project_inputs = {}
|
self.project_inputs = {}
|
||||||
|
|
||||||
for row, (key, label) in enumerate(self.project_fields.items()):
|
for row, (key, label) in enumerate(self.project_fields.items()):
|
||||||
self.project_inputs[key] = QLineEdit()
|
line_edit = QLineEdit()
|
||||||
|
self.project_inputs[key] = line_edit
|
||||||
project_layout.addWidget(QLabel(label), row, 0)
|
project_layout.addWidget(QLabel(label), row, 0)
|
||||||
project_layout.addWidget(self.project_inputs[key], row, 1)
|
project_layout.addWidget(line_edit, row, 1)
|
||||||
|
|
||||||
project_group.setLayout(project_layout)
|
project_group.setLayout(project_layout)
|
||||||
|
|
||||||
# 🟩 Speichern-Button
|
# -----------------------------------------------------
|
||||||
|
# Speichern-Button
|
||||||
|
# -----------------------------------------------------
|
||||||
save_button = QPushButton("Speichern")
|
save_button = QPushButton("Speichern")
|
||||||
save_button.clicked.connect(self.save_data)
|
save_button.clicked.connect(self.save_data)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
# Layout zusammenfügen
|
# Layout zusammenfügen
|
||||||
|
# -----------------------------------------------------
|
||||||
main_layout.addWidget(user_group)
|
main_layout.addWidget(user_group)
|
||||||
main_layout.addWidget(project_group)
|
main_layout.addWidget(project_group)
|
||||||
main_layout.addStretch()
|
main_layout.addStretch()
|
||||||
main_layout.addWidget(save_button)
|
main_layout.addWidget(save_button)
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# Daten laden
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Speichern
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def save_data(self):
|
def save_data(self):
|
||||||
# Alle Felder zusammenführen
|
"""
|
||||||
fields = {key: widget.text() for key, widget in {**self.user_inputs, **self.project_inputs}.items()}
|
Speichert alle Eingaben über SettingsLogic.
|
||||||
|
Fehler werden über den Wrapper gemeldet.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fields = {
|
||||||
|
key: widget.text()
|
||||||
|
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
|
||||||
|
}
|
||||||
|
|
||||||
self.logic.save(fields)
|
self.logic.save(fields)
|
||||||
|
info("Gespeichert", "Die Einstellungen wurden erfolgreich gespeichert.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error("Fehler beim Speichern", str(e))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Laden
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def load_data(self):
|
def load_data(self):
|
||||||
|
"""
|
||||||
|
Lädt gespeicherte Einstellungen und füllt die Felder.
|
||||||
|
Fehler werden über den Wrapper gemeldet.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
data = self.logic.load()
|
data = self.logic.load()
|
||||||
|
|
||||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
||||||
widget.setText(data.get(key, ""))
|
widget.setText(data.get(key, ""))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
warning("Einstellungen konnten nicht geladen werden", str(e))
|
||||||
|
|||||||
Reference in New Issue
Block a user