Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23aab76e42 | ||
|
|
53a60246c8 | ||
|
|
ff5ed5d7f5 | ||
|
|
272bad3866 | ||
|
|
72844cd257 | ||
|
|
a754fcfe34 | ||
|
|
7755e3e8cb | ||
|
|
bc9bb20597 | ||
|
|
113fda7d04 | ||
|
|
8545152317 | ||
|
|
8cea9022f0 | ||
|
|
1b5a715c70 | ||
|
|
749d6d6f52 | ||
|
|
29fea7c18f | ||
|
|
d857343d36 | ||
|
|
7bcd0132e9 | ||
|
|
3489a08638 | ||
|
|
7a5ed12797 | ||
|
|
021747ae06 | ||
|
|
91bafe632f | ||
|
|
7ed22fb23e | ||
|
|
3bbb66ec32 | ||
|
|
9862386646 |
+14
-1
@@ -1,5 +1,18 @@
|
||||
- Bezeichner-Bereinigung (Layernamen) um Bindestrich ergänzt und in eine zentrale Funktion `sanitize_bezeichner` (`functions/text_wrapper.py`) ausgelagert, damit Umlaute/Sonderzeichen nicht mehr pro Aufrufstelle vergessen werden (#42)
|
||||
- Landkreis-Dropdown (statt Freitext) in den Projekteigenschaften ergänzt, inkl. Landkreiskennzahl als eigene Projektvariable
|
||||
- Landkreisliste liegt jetzt als JSON (sn_basis/data/landkreise.json) statt hartcodiert vor, damit auch andere Module und Nicht-Entwickler sie pflegen können
|
||||
- Auswahldialog für mehrere BauRaumOderBodenordnungsrecht-Treffer auf Mehrfachauswahl umgestellt (mehrere Objekte gemeinsam laden/verschmelzen)
|
||||
- Landkreisweiter Objekt-Auswahldialog jetzt als Tabelle (Spalten "Nummer"/"Name", per Spaltenkopf sortierbar, Standard aufsteigend nach Nummer) mit Suchfeld (Filter nach Nummer/Name); "Alle auswählen" berücksichtigt die Filterung; Checkbox-Text zu "Nur Objekte laden (ohne weitere Prüfungen)" geändert
|
||||
- Dialoge: Titel, Wappen-Icon, kompaktere Größe, Scrollbar-Fix und größenveränderbare Liste ergänzt
|
||||
- Die drei Verfahrensgebiet-spezifischen ALKIS-Dialoge (BROR-Auswahl, Landkreis-Objektauswahl, Landkreis-Auswahl) sind aus sn_basis nach sn_verfahrensgebiet verschoben (functions/verfahrensgebiet_dialoge.py), da sie fachlich dorthin gehören
|
||||
---
|
||||
Version 26.6.1-unstable:
|
||||
Version 26.8.1-unstable:
|
||||
-Vergleich einer CSV aus dem BI-Server mit den Beteiligten Flurstücken lt. ALKIS
|
||||
---
|
||||
Version 26.7.1-unstable:
|
||||
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
|
||||
---
|
||||
Version 26.7.1-testing:
|
||||
- VLN-API zum Laden der Plan41-Maßnahmen integriert
|
||||
---
|
||||
Version 26.5.2-testing:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"kennzahl": "11", "name": "Chemnitz, Stadt"},
|
||||
{"kennzahl": "12", "name": "Dresden, Stadt"},
|
||||
{"kennzahl": "13", "name": "Leipzig, Stadt"},
|
||||
{"kennzahl": "21", "name": "Erzgebirgskreis"},
|
||||
{"kennzahl": "22", "name": "Mittelsachsen"},
|
||||
{"kennzahl": "23", "name": "Vogtlandkreis"},
|
||||
{"kennzahl": "24", "name": "Zwickau"},
|
||||
{"kennzahl": "25", "name": "Bautzen"},
|
||||
{"kennzahl": "26", "name": "Görlitz"},
|
||||
{"kennzahl": "27", "name": "Meißen"},
|
||||
{"kennzahl": "28", "name": "Sächsische Schweiz-Osterzgebirge"},
|
||||
{"kennzahl": "29", "name": "Leipzig"},
|
||||
{"kennzahl": "30", "name": "Nordsachsen"}
|
||||
]
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -21,6 +21,8 @@ from .ly_metadata_wrapper import (
|
||||
)
|
||||
from .ly_style_wrapper import apply_style
|
||||
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden
|
||||
from .laenderkennung import get_laenderkennung, LAENDERKENNUNGEN
|
||||
from .text_wrapper import sanitize_bezeichner
|
||||
|
||||
from .message_wrapper import (
|
||||
_get_message_bar,
|
||||
|
||||
+46
-50
@@ -232,6 +232,52 @@ def ask_detailpruefung_oder_vg_laden(
|
||||
return "abbrechen"
|
||||
|
||||
|
||||
def ask_detailpruefung_oder_abbrechen(
|
||||
parent: Any,
|
||||
title: str,
|
||||
message: str,
|
||||
) -> Literal["detailpruefung", "abbrechen"]:
|
||||
"""Zeigt Dialog mit zwei Optionen nach erkannter Flächenabweichung (Shape-Workflow).
|
||||
|
||||
Optionen:
|
||||
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des Shape-VG-Umrings
|
||||
- **Abbrechen**: Vorgang beenden, Shape-Verfahrensgebiet unverändert behalten
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
title : str
|
||||
Dialog-Titel.
|
||||
message : str
|
||||
Hauptmeldung mit Flächen-Informationen.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Literal["detailpruefung", "abbrechen"]
|
||||
Genaue Entscheidung des Nutzers.
|
||||
"""
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_detailpruefung_oder_abbrechen('{title}') → abbrechen")
|
||||
return "abbrechen"
|
||||
|
||||
msg = QMessageBox(parent)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(message)
|
||||
|
||||
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
|
||||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
exec_dialog(msg)
|
||||
|
||||
clicked = msg.clickedButton()
|
||||
if clicked == detail_btn:
|
||||
return "detailpruefung"
|
||||
else:
|
||||
return "abbrechen"
|
||||
|
||||
|
||||
class ProgressDialog:
|
||||
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||||
self.total = max(total, 1)
|
||||
@@ -344,53 +390,3 @@ class ProgressDialog:
|
||||
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
||||
return ProgressDialog(total, title, label)
|
||||
|
||||
|
||||
def ask_bror_kombination(
|
||||
parent: Any,
|
||||
kombinationen: list[tuple[str, str]],
|
||||
) -> Optional[tuple[str, str]]:
|
||||
"""Zeigt Dialog zur Auswahl einer BROR-Kombination (adv_name + bezeichnung).
|
||||
|
||||
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
|
||||
``adv_name`` und ``bezeichnung`` zurückliefert, sodass der Nutzer
|
||||
entscheiden muss, welche Objekte in die Pipeline einfließen sollen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
kombinationen :
|
||||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[tuple[str, str]]
|
||||
Das gewählte ``(adv_name, bezeichnung)``-Tupel oder ``None`` bei
|
||||
Abbruch bzw. leerer Eingabe.
|
||||
"""
|
||||
if not kombinationen:
|
||||
return None
|
||||
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_bror_kombination → {kombinationen[0]}")
|
||||
return kombinationen[0]
|
||||
|
||||
items = [f"{adv} | {bez}" for adv, bez in kombinationen]
|
||||
item, ok = QInputDialog.getItem(
|
||||
parent,
|
||||
"BROR-Objekt auswählen",
|
||||
"Mehrere BauRaumOderBodenordnungsrecht-Objekte gefunden.\n"
|
||||
"Bitte wählen Sie die gewünschte Kombination\n"
|
||||
"aus ADV-Name und Bezeichnung:",
|
||||
items,
|
||||
0,
|
||||
False,
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
try:
|
||||
idx = items.index(item)
|
||||
return kombinationen[idx]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
sn_basis/functions/laenderkennung.py – ALKIS-Länderkennung je Plugin-Präfix.
|
||||
|
||||
AGLB-länderspezifische Funktionen (z.B. Flurstückskennzeichen-Bildung) sollen
|
||||
nicht hart auf Sachsen ("14") verdrahtet sein, sondern die Länderkennung über
|
||||
dieses generische Mapping ermitteln. Für neue Bundesländer genügt ein weiterer
|
||||
Eintrag in ``LAENDERKENNUNGEN`` – die aufrufende Logik bleibt unverändert.
|
||||
"""
|
||||
|
||||
#: Mapping von sn_*-Plugin-Präfix (ohne Unterstrich) auf die zweistellige
|
||||
#: ALKIS-Länderkennung. Aktuell nur Sachsen befüllt; weitere Bundesländer
|
||||
#: können hier ergänzt werden, sobald Schwesterplugins dafür existieren.
|
||||
LAENDERKENNUNGEN: dict[str, str] = {
|
||||
"sn": "14", # Sachsen
|
||||
}
|
||||
|
||||
|
||||
def get_laenderkennung(plugin_praefix: str | None = None) -> str:
|
||||
"""Ermittelt die ALKIS-Länderkennung für einen Plugin-Präfix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plugin_praefix : str, optional
|
||||
Plugin-Präfix ohne Unterstrich, z.B. ``"sn"``. Wird kein Präfix
|
||||
übergeben, wird er aus dem Package-Namen des aufrufenden Moduls
|
||||
abgeleitet (``__name__`` des Aufrufers, erstes ``_``-Segment).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Zweistellige Länderkennung, z.B. ``"14"``.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
Wenn für den ermittelten Präfix keine Länderkennung hinterlegt ist.
|
||||
"""
|
||||
if not plugin_praefix:
|
||||
import inspect
|
||||
|
||||
frame = inspect.stack()[1]
|
||||
caller_module = frame.frame.f_globals.get("__name__", "")
|
||||
plugin_praefix = caller_module.split(".")[0].split("_")[0]
|
||||
|
||||
plugin_praefix = plugin_praefix.lower()
|
||||
if plugin_praefix not in LAENDERKENNUNGEN:
|
||||
raise KeyError(
|
||||
f"Keine Länderkennung für Plugin-Präfix '{plugin_praefix}' hinterlegt. "
|
||||
f"Bekannte Präfixe: {sorted(LAENDERKENNUNGEN)}"
|
||||
)
|
||||
return LAENDERKENNUNGEN[plugin_praefix]
|
||||
@@ -0,0 +1,47 @@
|
||||
"""sn_basis/functions/landkreise.py – Sächsische Landkreise/Helfer mit Kennzahl.
|
||||
|
||||
Daten liegen in sn_basis/data/landkreise.json (auch von anderen sn_*-Modulen
|
||||
lesbar, z.B. per ``from sn_basis.functions.landkreise import LANDKREISE``).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
try:
|
||||
from qgis.core import Qgis, QgsMessageLog
|
||||
except Exception: # Mock-Modus / außerhalb QGIS
|
||||
Qgis = None
|
||||
QgsMessageLog = None
|
||||
|
||||
# .../sn_basis/data/landkreise.json (eine Ebene über dieser Datei, dann data/)
|
||||
_DATA_DATEI = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "landkreise.json")
|
||||
|
||||
|
||||
def _lade_landkreise() -> "list[tuple[str, str]]":
|
||||
try:
|
||||
with open(_DATA_DATEI, "r", encoding="utf-8") as f:
|
||||
rohdaten = json.load(f)
|
||||
return [(eintrag["kennzahl"], eintrag["name"]) for eintrag in rohdaten]
|
||||
except Exception as exc:
|
||||
if QgsMessageLog is not None:
|
||||
QgsMessageLog.logMessage(
|
||||
f"landkreise.json konnte nicht geladen werden ({_DATA_DATEI}): {exc}",
|
||||
"sn_basis",
|
||||
Qgis.Warning,
|
||||
)
|
||||
else:
|
||||
print(f"⚠️ landkreise.json konnte nicht geladen werden ({_DATA_DATEI}): {exc}")
|
||||
return []
|
||||
|
||||
|
||||
#: Liste von (Landkreiskennzahl, Landkreis/Helfer)-Tupeln, in Datei-Reihenfolge.
|
||||
LANDKREISE: "list[tuple[str, str]]" = _lade_landkreise()
|
||||
|
||||
#: Kennzahl -> Name
|
||||
KENNZAHL_ZU_NAME: "dict[str, str]" = dict(LANDKREISE)
|
||||
|
||||
#: Name -> Kennzahl
|
||||
NAME_ZU_KENNZAHL: "dict[str, str]" = {name: kennzahl for kennzahl, name in LANDKREISE}
|
||||
|
||||
#: Sentinel-"Kennzahl" für die Auswahl "Alle Landkreise" (kein Landkreis-Filter).
|
||||
ALLE_LANDKREISE = "ALLE"
|
||||
+193
-10
@@ -46,6 +46,15 @@ QCheckBox: Type[Any] = object
|
||||
QHBoxLayout: Type[Any] = object
|
||||
QFont: Type[Any] = object
|
||||
QSettings: Type[Any] = object
|
||||
QDialog: Type[Any] = object
|
||||
QDialogButtonBox: Type[Any] = object
|
||||
QListWidget: Type[Any] = object
|
||||
QListWidgetItem: Type[Any] = object
|
||||
QAbstractItemView: Type[Any] = object
|
||||
QTableWidget: Type[Any] = object
|
||||
QTableWidgetItem: Type[Any] = object
|
||||
QTableWidgetSelectionRange: Type[Any] = object
|
||||
QIcon: Type[Any] = object
|
||||
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
@@ -92,8 +101,16 @@ try:
|
||||
QComboBox as _QComboBox,
|
||||
QCheckBox as _QCheckBox,
|
||||
QHBoxLayout as _QHBoxLayout,
|
||||
QDialog as _QDialog,
|
||||
QDialogButtonBox as _QDialogButtonBox,
|
||||
QListWidget as _QListWidget,
|
||||
QListWidgetItem as _QListWidgetItem,
|
||||
QAbstractItemView as _QAbstractItemView,
|
||||
QTableWidget as _QTableWidget,
|
||||
QTableWidgetItem as _QTableWidgetItem,
|
||||
QTableWidgetSelectionRange as _QTableWidgetSelectionRange,
|
||||
)
|
||||
from qgis.PyQt.QtGui import QFont as _QFont
|
||||
from qgis.PyQt.QtGui import QFont as _QFont, QIcon as _QIcon
|
||||
from qgis.PyQt.QtCore import (
|
||||
QEventLoop as _QEventLoop,
|
||||
QTimer as _QTimer,
|
||||
@@ -143,6 +160,15 @@ try:
|
||||
QHBoxLayout = _QHBoxLayout
|
||||
QFont = _QFont
|
||||
QSettings = _QSettings
|
||||
QDialog = _QDialog
|
||||
QDialogButtonBox = _QDialogButtonBox
|
||||
QListWidget = _QListWidget
|
||||
QListWidgetItem = _QListWidgetItem
|
||||
QAbstractItemView = _QAbstractItemView
|
||||
QTableWidget = _QTableWidget
|
||||
QTableWidgetItem = _QTableWidgetItem
|
||||
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
|
||||
QIcon = _QIcon
|
||||
# ✅ QT6 ENUMS
|
||||
YES = QMessageBox.StandardButton.Yes
|
||||
NO = QMessageBox.StandardButton.No
|
||||
@@ -163,7 +189,12 @@ try:
|
||||
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
|
||||
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
|
||||
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
|
||||
|
||||
DialogAccepted = QDialog.DialogCode.Accepted
|
||||
ExtendedSelection = QAbstractItemView.SelectionMode.ExtendedSelection
|
||||
SelectRows = QAbstractItemView.SelectionBehavior.SelectRows
|
||||
NoEditTriggers = QAbstractItemView.EditTrigger.NoEditTriggers
|
||||
AscendingOrder = Qt.SortOrder.AscendingOrder
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
return dialog.exec()
|
||||
|
||||
@@ -194,8 +225,16 @@ except (ImportError, AttributeError):
|
||||
QComboBox as _QComboBox,
|
||||
QCheckBox as _QCheckBox,
|
||||
QHBoxLayout as _QHBoxLayout,
|
||||
QDialog as _QDialog,
|
||||
QDialogButtonBox as _QDialogButtonBox,
|
||||
QListWidget as _QListWidget,
|
||||
QListWidgetItem as _QListWidgetItem,
|
||||
QAbstractItemView as _QAbstractItemView,
|
||||
QTableWidget as _QTableWidget,
|
||||
QTableWidgetItem as _QTableWidgetItem,
|
||||
QTableWidgetSelectionRange as _QTableWidgetSelectionRange,
|
||||
)
|
||||
from PyQt5.QtGui import QFont as _QFont
|
||||
from PyQt5.QtGui import QFont as _QFont, QIcon as _QIcon
|
||||
from PyQt5.QtCore import (
|
||||
QEventLoop as _QEventLoop,
|
||||
QTimer as _QTimer,
|
||||
@@ -243,7 +282,16 @@ except (ImportError, AttributeError):
|
||||
QHBoxLayout= _QHBoxLayout
|
||||
QFont = _QFont
|
||||
QSettings = _QSettings
|
||||
|
||||
QDialog = _QDialog
|
||||
QDialogButtonBox = _QDialogButtonBox
|
||||
QListWidget = _QListWidget
|
||||
QListWidgetItem = _QListWidgetItem
|
||||
QAbstractItemView = _QAbstractItemView
|
||||
QTableWidget = _QTableWidget
|
||||
QTableWidgetItem = _QTableWidgetItem
|
||||
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
|
||||
QIcon = _QIcon
|
||||
|
||||
# ✅ PYQT5 ENUMS
|
||||
YES = QMessageBox.Yes
|
||||
NO = QMessageBox.No
|
||||
@@ -265,7 +313,12 @@ except (ImportError, AttributeError):
|
||||
DockWidgetClosable = QDockWidget.DockWidgetClosable
|
||||
DockAreaLeft = Qt.LeftDockWidgetArea
|
||||
DockAreaRight = Qt.RightDockWidgetArea
|
||||
|
||||
DialogAccepted = QDialog.Accepted
|
||||
ExtendedSelection = QAbstractItemView.ExtendedSelection
|
||||
SelectRows = QAbstractItemView.SelectRows
|
||||
NoEditTriggers = QAbstractItemView.NoEditTriggers
|
||||
AscendingOrder = Qt.AscendingOrder
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
return dialog.exec_()
|
||||
|
||||
@@ -319,6 +372,10 @@ except (ImportError, AttributeError):
|
||||
def getText(parent, title, label, mode=None, text=""):
|
||||
return text, True
|
||||
|
||||
@staticmethod
|
||||
def getItem(parent, title, label, items, current=0, editable=False):
|
||||
return (items[current] if items else ""), True
|
||||
|
||||
QInputDialog = _MockQInputDialog
|
||||
|
||||
class _MockQEventLoop:
|
||||
@@ -482,13 +539,19 @@ except (ImportError, AttributeError):
|
||||
|
||||
class _MockComboBox:
|
||||
def __init__(self, parent=None):
|
||||
self._items = []
|
||||
self._items = [] # Liste von (text, userData)
|
||||
self._index = -1
|
||||
self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
|
||||
def addItem(self, text: str) -> None: self._items.append(text)
|
||||
def addItem(self, text: str, userData=None) -> None:
|
||||
self._items.append((text, userData))
|
||||
if self._index < 0:
|
||||
self._index = 0
|
||||
def addItems(self, items): [self.addItem(it) for it in items]
|
||||
def findText(self, text: str) -> int:
|
||||
return self._items.index(text) if text in self._items else -1
|
||||
def findText(self, text: str) -> int:
|
||||
for i, (t, _) in enumerate(self._items):
|
||||
if t == text:
|
||||
return i
|
||||
return -1
|
||||
def setCurrentIndex(self, idx: int) -> None:
|
||||
if 0 <= idx < len(self._items):
|
||||
self._index = idx
|
||||
@@ -497,7 +560,11 @@ except (ImportError, AttributeError):
|
||||
idx = self.findText(text)
|
||||
if idx >= 0: self.setCurrentIndex(idx)
|
||||
def currentText(self) -> str:
|
||||
return self._items[self._index] if 0 <= self._index < len(self._items) else ""
|
||||
return self._items[self._index][0] if 0 <= self._index < len(self._items) else ""
|
||||
def currentData(self):
|
||||
return self._items[self._index][1] if 0 <= self._index < len(self._items) else None
|
||||
def count(self) -> int:
|
||||
return len(self._items)
|
||||
|
||||
QComboBox = _MockComboBox
|
||||
|
||||
@@ -605,6 +672,122 @@ except (ImportError, AttributeError):
|
||||
self._checked = checked
|
||||
|
||||
QCheckBox = _MockQCheckBox
|
||||
|
||||
DialogAccepted = 1
|
||||
ExtendedSelection = 3
|
||||
SelectRows = 1
|
||||
NoEditTriggers = 0
|
||||
AscendingOrder = 0
|
||||
|
||||
class _MockQDialog(_MockWidget):
|
||||
DialogCode = type("DialogCode", (), {"Accepted": DialogAccepted, "Rejected": 0})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._layout = None
|
||||
def setWindowTitle(self, title: str) -> None: pass
|
||||
def setWindowIcon(self, icon) -> None: pass
|
||||
def exec(self) -> int: return DialogAccepted
|
||||
def exec_(self) -> int: return DialogAccepted
|
||||
def accept(self) -> None: pass
|
||||
def reject(self) -> None: pass
|
||||
def resize(self, *args, **kwargs) -> None: pass
|
||||
def adjustSize(self) -> None: pass
|
||||
def sizeHint(self):
|
||||
return type("Size", (), {"width": lambda self=None: 200, "height": lambda self=None: 100})()
|
||||
|
||||
QDialog = _MockQDialog
|
||||
|
||||
class _MockQDialogButtonBox(_MockWidget):
|
||||
StandardButton = type("StandardButton", (), {"Ok": 1, "Cancel": 2})
|
||||
Ok = 1
|
||||
Cancel = 2
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.accepted = type('Signal', (), {'connect': lambda s, cb: None})()
|
||||
self.rejected = type('Signal', (), {'connect': lambda s, cb: None})()
|
||||
|
||||
QDialogButtonBox = _MockQDialogButtonBox
|
||||
|
||||
class _MockQListWidgetItem:
|
||||
def __init__(self, text: str = ""):
|
||||
self._text = text
|
||||
self._selected = False
|
||||
def setSelected(self, value: bool) -> None: self._selected = value
|
||||
def isSelected(self) -> bool: return self._selected
|
||||
|
||||
QListWidgetItem = _MockQListWidgetItem
|
||||
|
||||
class _MockQListWidget(_MockWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._items = []
|
||||
def addItem(self, item) -> None: self._items.append(item)
|
||||
def item(self, index: int): return self._items[index] if 0 <= index < len(self._items) else None
|
||||
def count(self) -> int: return len(self._items)
|
||||
def row(self, item) -> int: return self._items.index(item) if item in self._items else -1
|
||||
def selectedItems(self): return [i for i in self._items if i.isSelected()]
|
||||
def setSelectionMode(self, *args, **kwargs) -> None: pass
|
||||
def sizeHintForRow(self, index: int) -> int: return 20
|
||||
def setFixedHeight(self, height: int) -> None: pass
|
||||
def frameWidth(self) -> int: return 1
|
||||
|
||||
QListWidget = _MockQListWidget
|
||||
|
||||
class _MockQAbstractItemView:
|
||||
SelectionMode = type("SelectionMode", (), {"ExtendedSelection": ExtendedSelection})
|
||||
|
||||
QAbstractItemView = _MockQAbstractItemView
|
||||
|
||||
class _MockQTableWidgetItem:
|
||||
def __init__(self, text: str = ""):
|
||||
self._text = text
|
||||
def text(self) -> str: return self._text
|
||||
def setText(self, text: str) -> None: self._text = text
|
||||
|
||||
QTableWidgetItem = _MockQTableWidgetItem
|
||||
|
||||
class _MockQTableWidget(_MockWidget):
|
||||
def __init__(self, rows: int = 0, cols: int = 0, *args, **kwargs):
|
||||
self._items = {}
|
||||
self._row_count = rows
|
||||
self._col_count = cols
|
||||
self._hidden_rows = set()
|
||||
def setItem(self, row: int, col: int, item) -> None: self._items[(row, col)] = item
|
||||
def item(self, row: int, col: int): return self._items.get((row, col))
|
||||
def rowCount(self) -> int: return self._row_count
|
||||
def setRowCount(self, rows: int) -> None: self._row_count = rows
|
||||
def setHorizontalHeaderLabels(self, labels) -> None: pass
|
||||
def setSelectionMode(self, *args, **kwargs) -> None: pass
|
||||
def setSelectionBehavior(self, *args, **kwargs) -> None: pass
|
||||
def setEditTriggers(self, *args, **kwargs) -> None: pass
|
||||
def setSortingEnabled(self, *args, **kwargs) -> None: pass
|
||||
def setRowHidden(self, row: int, hidden: bool) -> None:
|
||||
if hidden: self._hidden_rows.add(row)
|
||||
else: self._hidden_rows.discard(row)
|
||||
def isRowHidden(self, row: int) -> bool: return row in self._hidden_rows
|
||||
def selectRow(self, row: int) -> None: pass
|
||||
def selectedItems(self): return []
|
||||
def setRangeSelected(self, *args, **kwargs) -> None: pass
|
||||
def sortByColumn(self, *args, **kwargs) -> None: pass
|
||||
def resizeColumnToContents(self, col: int) -> None: pass
|
||||
def setColumnWidth(self, col: int, width: int) -> None: pass
|
||||
def columnWidth(self, col: int) -> int: return 100
|
||||
def columnCount(self) -> int: return self._col_count
|
||||
def verticalHeader(self): return _MockWidget()
|
||||
def horizontalHeader(self): return _MockWidget()
|
||||
def frameWidth(self) -> int: return 1
|
||||
|
||||
QTableWidget = _MockQTableWidget
|
||||
|
||||
class _MockQTableWidgetSelectionRange:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
|
||||
QTableWidgetSelectionRange = _MockQTableWidgetSelectionRange
|
||||
|
||||
class _MockQIcon:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
|
||||
QIcon = _MockQIcon
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
return YES
|
||||
# --------------------------- TEST ---------------------------
|
||||
|
||||
@@ -20,6 +20,7 @@ class SettingsLogic:
|
||||
"amt",
|
||||
"behoerde",
|
||||
"landkreis_user",
|
||||
"landkreis_kennzahl",
|
||||
"sachgebiet",
|
||||
"bezeichnung",
|
||||
"name",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
sn_basis/functions/text_wrapper.py – Bezeichner-Hilfsfunktionen.
|
||||
|
||||
Zentrale Stelle, um Freitext (z. B. ein Thema oder ein Schriftfeldinhalt) in einen
|
||||
technischen Bezeichner (Layername, Dateiname) umzuwandeln. Nicht pro Aufrufstelle
|
||||
ein eigenes Regex schreiben – Umlaute, Unterstrich und Bindestrich werden hier an
|
||||
einer Stelle gepflegt (siehe docs/wissen/fallstricke.md, Issue Plugin_SN_Basis #42).
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# Buchstaben, Ziffern, deutsche Umlaute/ß, Unterstrich und Bindestrich gelten immer
|
||||
# als zulässig. Wurde in der Vergangenheit wiederholt vergessen, wenn ein neues
|
||||
# Regex direkt im Aufrufer entstand statt diese Funktion zu nutzen.
|
||||
_BEZEICHNER_ERLAUBT = r"A-Za-z0-9_\-ÄäÖöÜüß"
|
||||
|
||||
|
||||
def sanitize_bezeichner(text: str, ersatz: str = "_") -> str:
|
||||
"""Wandelt `text` in einen technischen Bezeichner um.
|
||||
|
||||
Alle Zeichen außerhalb von Buchstaben/Ziffern/Umlauten/ß/Unterstrich/Bindestrich
|
||||
werden durch `ersatz` ersetzt, anschließend werden führende und folgende
|
||||
Vorkommen von `ersatz` entfernt. Gedacht für Layer- und Dateinamen – nicht für
|
||||
Freitext in Schriftfeldern oder der Legende, wo auch andere Zeichen sichtbar
|
||||
bleiben sollen.
|
||||
"""
|
||||
bereinigt = re.sub(rf"[^{_BEZEICHNER_ERLAUBT}]+", ersatz, text)
|
||||
return bereinigt.strip(ersatz)
|
||||
@@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version=26.7.2-testing
|
||||
name=LNO Sachsen | Plugin Basisfunktionen
|
||||
description=Plugin mit Basisfunktionen
|
||||
author=Daniel Helbig, Michael Otto
|
||||
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
|
||||
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
|
||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/testing/
|
||||
QGISMaximumVersion=4.99
|
||||
qgisMinimumVersion=3.40
|
||||
experimental=true
|
||||
deprecated=false
|
||||
supportsQt6=true
|
||||
+13
-2
@@ -30,7 +30,6 @@ from sn_basis.modules.linkpruefer import Linkpruefer
|
||||
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||
from sn_basis.modules.excel_importer import ExcelImporter
|
||||
from sn_plan41.modules.listenauswerter import Listenauswerter
|
||||
|
||||
|
||||
SourceType = Literal["service", "database", "excel", "unknown"]
|
||||
@@ -286,7 +285,19 @@ class DataGrabber:
|
||||
}
|
||||
source_dict["rows"].append(result_row)
|
||||
|
||||
# Validierung über Listenauswerter
|
||||
# Validierung über Listenauswerter – Import erst hier (statt am Modul-
|
||||
# anfang), da DataGrabber generisch für alle sn_*-Plugins nutzbar sein
|
||||
# soll und nicht bei jedem Aufruf zwingend sn_plan41 installiert ist;
|
||||
# nur die Excel-Quellenverarbeitung braucht Listenauswerter.
|
||||
try:
|
||||
from sn_plan41.modules.listenauswerter import Listenauswerter
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Die Verarbeitung von Excel-Quellen benötigt das Plugin "
|
||||
"sn_plan41 (sn_plan41.modules.listenauswerter.Listenauswerter), "
|
||||
"das hier nicht installiert ist."
|
||||
) from exc
|
||||
|
||||
listenauswerter = Listenauswerter(self.pruefmanager, self.stil_pruefer or Stilpruefer())
|
||||
validated, validation_results = listenauswerter.validate_rows(source_dict)
|
||||
results.extend(validation_results)
|
||||
|
||||
+55
-12
@@ -19,7 +19,7 @@ Designprinzipien
|
||||
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
||||
|
||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
|
||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, quote
|
||||
import json
|
||||
import time
|
||||
|
||||
@@ -132,6 +132,7 @@ class Datenabruf:
|
||||
# außer bei WMS (WMS bleibt unverändert).
|
||||
use_bbox = (raumfilter != "ohne") and (str(provider).upper() != "WMS")
|
||||
url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None)
|
||||
print(f"[Datenabruf] URL für {thema!r} ({provider}): {url}")
|
||||
|
||||
# 2b) Fachdaten abrufen
|
||||
features, error_msg = self._fetch_features(
|
||||
@@ -309,8 +310,11 @@ class Datenabruf:
|
||||
if callable(extent_to_bbox):
|
||||
bbox_str = extent_to_bbox(extent_obj)
|
||||
else:
|
||||
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
||||
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||
# Fallback: xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
||||
# QgsRectangle verwendet xMinimum()/yMinimum()/xMaximum()/yMaximum()
|
||||
if hasattr(extent_obj, "xMinimum") and callable(getattr(extent_obj, "xMinimum")):
|
||||
bbox_str = f"{extent_obj.xMinimum()},{extent_obj.yMinimum()},{extent_obj.xMaximum()},{extent_obj.yMaximum()}"
|
||||
elif hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||
bbox_str = f"{extent_obj.xmin()},{extent_obj.ymin()},{extent_obj.xmax()},{extent_obj.ymax()}"
|
||||
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
|
||||
bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}"
|
||||
@@ -325,12 +329,26 @@ class Datenabruf:
|
||||
if provider_norm == "WFS":
|
||||
query_params.setdefault("service", "WFS")
|
||||
query_params.setdefault("request", "GetFeature")
|
||||
query_params.setdefault("outputFormat", "application/json")
|
||||
if bbox_str:
|
||||
query_params.setdefault("BBOX", bbox_str)
|
||||
query_params.setdefault("VERSION", "2.0.0")
|
||||
# WFS 2.0: 'typename' → 'TYPENAMES'
|
||||
if "typename" in query_params and "TYPENAMES" not in query_params:
|
||||
query_params["TYPENAMES"] = query_params.pop("typename")
|
||||
# resultType=hits: nur Anzahl vom Server abfragen, kein GML-Download
|
||||
query_params.setdefault("resultType", "hits")
|
||||
# CRS in OGC-URN-Format (wie QGIS Datenquellenverwaltung)
|
||||
srsname_urn: Optional[str] = None
|
||||
if crs_authid:
|
||||
query_params.setdefault("SRSNAME", crs_authid)
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
if crs_authid.upper().startswith("EPSG:"):
|
||||
srsname_urn = f"urn:ogc:def:crs:EPSG::{crs_authid.split(':', 1)[1]}"
|
||||
else:
|
||||
srsname_urn = crs_authid
|
||||
if bbox_str:
|
||||
# 5. Komponente: CRS-URN anhängen (Standard für WFS 2.0)
|
||||
bbox_with_crs = f"{bbox_str},{srsname_urn}" if srsname_urn else bbox_str
|
||||
query_params.setdefault("BBOX", bbox_with_crs)
|
||||
if srsname_urn:
|
||||
query_params.setdefault("SRSNAME", srsname_urn)
|
||||
new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/')
|
||||
rebuilt = parsed._replace(query=new_query)
|
||||
return urlunparse(rebuilt)
|
||||
|
||||
@@ -389,14 +407,14 @@ class Datenabruf:
|
||||
query_params.setdefault("inSR", srid)
|
||||
query_params.setdefault("outSR", srid)
|
||||
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/')
|
||||
rebuilt = parsed_rest._replace(query=new_query)
|
||||
return urlunparse(rebuilt)
|
||||
|
||||
# Default: generischer bbox-Parameter (nur wenn vorhanden)
|
||||
if bbox_str:
|
||||
query_params.setdefault("bbox", bbox_str)
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/')
|
||||
rebuilt = parsed._replace(query=new_query)
|
||||
return urlunparse(rebuilt)
|
||||
|
||||
@@ -573,7 +591,33 @@ class Datenabruf:
|
||||
# Sonst: gib das gesamte JSON als einzelnes Objekt zurück
|
||||
return [parsed], None
|
||||
except json.JSONDecodeError:
|
||||
# Nicht-JSON-Antwort (z. B. GML). Wenn QGIS verfügbar, versuche GML via temporärer Datei + OGR
|
||||
# Nicht-JSON-Antwort (GML/XML). Für WFS resultType=hits: Anzahl aus XML lesen.
|
||||
if prov == "WFS":
|
||||
try:
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(response_text)
|
||||
# WFS 2.0: numberOfMatched; WFS 1.x: numberOfFeatures
|
||||
n_str = (
|
||||
root.get("numberOfMatched")
|
||||
or root.get("numberMatched")
|
||||
or root.get("numberOfFeatures")
|
||||
or root.get("numberOfReturned")
|
||||
)
|
||||
if n_str is not None:
|
||||
try:
|
||||
count = int(n_str)
|
||||
return [{}] * count, None
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# Fallback: wfs:member-Elemente zählen
|
||||
members = [
|
||||
c for c in root
|
||||
if c.tag.lower().endswith(("member", "featuremember"))
|
||||
]
|
||||
return [{}] * len(members), None
|
||||
except Exception as exc:
|
||||
return [], f"WFS-XML-Antwort konnte nicht geparst werden: {exc}"
|
||||
# Nicht-WFS, nicht-JSON: GML via temporärer Datei + OGR
|
||||
if getattr(qgiscore, "QGIS_AVAILABLE", False):
|
||||
try:
|
||||
import tempfile
|
||||
@@ -588,5 +632,4 @@ class Datenabruf:
|
||||
return [], "GML-Antwort konnte nicht als Layer geladen werden"
|
||||
except Exception as exc:
|
||||
return [], f"Fehler beim Parsen von GML: {exc}"
|
||||
# Wenn alles fehlschlägt:
|
||||
return [], "Antwort konnte nicht als JSON oder GML geparst werden"
|
||||
|
||||
@@ -30,13 +30,13 @@ from __future__ import annotations
|
||||
from typing import Any, Dict, List, Optional
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import sqlite3
|
||||
|
||||
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
||||
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
|
||||
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
|
||||
from sn_basis.functions.text_wrapper import sanitize_bezeichner
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class Datenschreiber:
|
||||
continue
|
||||
|
||||
layer_name_raw = thema or str(ident)
|
||||
layer_name = re.sub(r"[^A-Za-z0-9_ÄäÖöÜüß]+", "_", layer_name_raw).strip("_")
|
||||
layer_name = sanitize_bezeichner(layer_name_raw)
|
||||
if not layer_name:
|
||||
layer_name = f"layer_{ident}"
|
||||
|
||||
|
||||
+27
-1
@@ -122,7 +122,33 @@ class LayerLoader:
|
||||
|
||||
try:
|
||||
if provider_lower == "wfs":
|
||||
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||
raw_link = link.strip()
|
||||
if raw_link.lower().startswith("url="):
|
||||
raw_link = raw_link[4:]
|
||||
# QGIS WFS-Provider erwartet die Basis-URL und 'typename' als
|
||||
# separaten URI-Parameter. GetFeature-spezifische Parameter
|
||||
# (request, outputFormat, resultType, BBOX, …) werden entfernt.
|
||||
try:
|
||||
from urllib.parse import urlparse as _up, parse_qsl as _pqsl, urlunparse as _uu, urlencode as _ue
|
||||
_parsed = _up(raw_link)
|
||||
_params = dict(_pqsl(_parsed.query, keep_blank_values=True))
|
||||
_typename = (
|
||||
_params.pop("typename", None)
|
||||
or _params.pop("TYPENAME", None)
|
||||
or _params.pop("TYPENAMES", None)
|
||||
)
|
||||
for _k in ("request", "REQUEST", "outputFormat", "OUTPUTFORMAT",
|
||||
"resultType", "RESULTTYPE", "BBOX", "bbox",
|
||||
"STARTINDEX", "COUNT"):
|
||||
_params.pop(_k, None)
|
||||
_params.setdefault("service", "WFS")
|
||||
_params.setdefault("version", "2.0.0")
|
||||
_base_url = _uu(_parsed._replace(query=_ue(_params)))
|
||||
uri = f"url={_base_url}"
|
||||
if _typename:
|
||||
uri += f" typename={_typename}"
|
||||
except Exception:
|
||||
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||
layer = QgsVectorLayer(uri, thema, "WFS")
|
||||
elif provider_lower == "wms":
|
||||
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||
|
||||
@@ -45,7 +45,7 @@ MM = QgsUnitTypes.LayoutMillimeters
|
||||
|
||||
|
||||
class PrintLayout:
|
||||
"""Erzeugt ein QGIS-Einzelblatt-Layout fuer den Druck."""
|
||||
"""Erzeugt ein QGIS-Einzelblatt-Layout für den Druck."""
|
||||
|
||||
_FRAME_STROKE_WIDTH_MM = 0.5
|
||||
_LEGEND_WIDTH_MM = 180.0
|
||||
@@ -340,7 +340,7 @@ class PrintLayout:
|
||||
("[% @sn_landkreise_proj %]", 16.0, -48.0, 171.0, 4.0, "Format_1", "Landkreis"),
|
||||
("[% @sn_kartenname %]", 16.0, -32.0, 137.0, 12.0, "Format_3", "Kartenname"),
|
||||
("Gefertigt: ", 16.0, -23.0, 81.0, 4.0, "Format_1", "Gefertigt"),
|
||||
("Geprueft: ", 101.0, -23.0, 81.0, 4.0, "Format_1", "Geprüft"),
|
||||
("Geprüft: ", 101.0, -23.0, 81.0, 4.0, "Format_1", "Geprüft"),
|
||||
(
|
||||
"Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
|
||||
16.0,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung fuer Baufreigabe-Vorlagen.
|
||||
sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung für Baufreigabe-Vorlagen.
|
||||
|
||||
Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengroessen im Querformat,
|
||||
ein 120 mm breites Kartenschild (statt 180 mm) und erbenerben die Standard-Logik
|
||||
fuer Hauptkarte, Quellenangabe und Legende vom PrintLayout.
|
||||
Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengrößen im Querformat,
|
||||
ein 120 mm breites Kartenschild (statt 180 mm) und erben die Standard-Logik
|
||||
für Hauptkarte, Quellenangabe und Legende vom PrintLayout.
|
||||
|
||||
Atlas-Integration erfolgt nach Layouterstellung durch externe Konfiguration
|
||||
in der PrintLogic.
|
||||
@@ -32,7 +32,7 @@ MM = QgsUnitTypes.LayoutMillimeters
|
||||
|
||||
|
||||
class PrintLayoutBaufreigabe(PrintLayout):
|
||||
"""Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Groesse und 80mm Kartenschild."""
|
||||
"""Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Größe und 80mm Kartenschild."""
|
||||
|
||||
_FRAME_STROKE_WIDTH_MM = 0.5
|
||||
_KARTENSCHILD_WIDTH_MM = 80.0
|
||||
@@ -41,7 +41,7 @@ class PrintLayoutBaufreigabe(PrintLayout):
|
||||
_GRID_INTERVAL_MAP_UNITS = 500.0
|
||||
|
||||
# DIN-Formate: nur A3 und A4 im Querformat
|
||||
_DIN_A3_LANDSCAPE = (420, 297) # Breite x Hoehe (Querformat)
|
||||
_DIN_A3_LANDSCAPE = (420, 297) # Breite x Höhe (Querformat)
|
||||
_DIN_A4_LANDSCAPE = (297, 210) # Querformat
|
||||
|
||||
_TEXT_FORMAT_TEMPLATES = {
|
||||
@@ -99,25 +99,25 @@ class PrintLayoutBaufreigabe(PrintLayout):
|
||||
fit_extent_to_map: bool = True,
|
||||
atlas_driven: bool = True,
|
||||
) -> Any:
|
||||
"""Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Groesse.
|
||||
"""Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Größe.
|
||||
|
||||
Args:
|
||||
name: Name des Layouts
|
||||
din_format: "DIN A3" oder "DIN A4"
|
||||
map_width_mm: Breite der Hauptkarte
|
||||
map_height_mm: Hoehe der Hauptkarte
|
||||
map_height_mm: Höhe der Hauptkarte
|
||||
extent: QgsRectangle der Kartenausdehnung
|
||||
plotmassstab: Plotmassstab (z.B. 5000 fuer 1:5000)
|
||||
plotmassstab: Plotmaßstab (z.B. 5000 für 1:5000)
|
||||
thema: Name des Visibility-Presets (optional)
|
||||
fit_extent_to_map: Extent bei Bedarf auf das Karten-Seitenverhaeltnis erweitern
|
||||
fit_extent_to_map: Extent bei Bedarf auf das Karten-Seitenverhältnis erweitern
|
||||
|
||||
Returns:
|
||||
QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet)
|
||||
QgsPrintLayout objektInstance (wird im Layout-Dialog geöffnet)
|
||||
|
||||
Raises:
|
||||
ValueError: Falls Name bereits existiert oder Format ungueltig ist
|
||||
ValueError: Falls Name bereits existiert oder Format ungültig ist
|
||||
"""
|
||||
# Seitenformat auswaehlen (nur A3/A4 im Querformat)
|
||||
# Seitenformat auswählen (nur A3/A4 im Querformat)
|
||||
if din_format == "DIN A3":
|
||||
page_width_mm, page_height_mm = self._DIN_A3_LANDSCAPE
|
||||
elif din_format == "DIN A4":
|
||||
@@ -266,9 +266,9 @@ class PrintLayoutBaufreigabe(PrintLayout):
|
||||
map_top_mm: float,
|
||||
map_height_mm: float,
|
||||
) -> None:
|
||||
"""Erstellt Legende, Quellenangabe und Kartenschild fuer Baufreigabe-Layout.
|
||||
"""Erstellt Legende, Quellenangabe und Kartenschild für Baufreigabe-Layout.
|
||||
|
||||
Angepasst fuer 80mm Kartenschild statt 180mm.
|
||||
Angepasst für 80mm Kartenschild statt 180mm.
|
||||
"""
|
||||
if self._current_layout is None:
|
||||
raise RuntimeError("Kein Layout aktiv.")
|
||||
@@ -279,7 +279,7 @@ class PrintLayoutBaufreigabe(PrintLayout):
|
||||
kartenfenster_untere_rechte_ecke_mm[1] + rel_y_mm,
|
||||
)
|
||||
|
||||
# Legende: 80mm Breite, hoehe adaptive wie Standard
|
||||
# Legende: 80mm Breite, Höhe adaptiv wie Standard
|
||||
legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0
|
||||
legende_height_mm = max(map_height_mm - 122.0, 20.0)
|
||||
legende_y = map_top_mm
|
||||
|
||||
+33
-33
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
sn_basis/modules/print_logic.py - Fachlogik fuer den Druck-Tab (nur Einzelblatt).
|
||||
sn_basis/modules/print_logic.py - Fachlogik für den Druck-Tab (nur Einzelblatt).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,7 +16,7 @@ from sn_basis.modules.print_layout_baufreigabe import PrintLayoutBaufreigabe
|
||||
|
||||
|
||||
class AtlasConfig(TypedDict, total=False):
|
||||
"""Strukturierte Konfiguration fuer Atlas-Optionen.
|
||||
"""Strukturierte Konfiguration für Atlas-Optionen.
|
||||
|
||||
Alle Felder sind optional (total=False) und haben vordefinierte Standartwerte.
|
||||
"""
|
||||
@@ -113,7 +113,7 @@ STANDARD_A4_LANDSCAPE_H_MM = 210.0
|
||||
|
||||
|
||||
class PrintLogic:
|
||||
"""Kapselt die Fachlogik fuer den Druck-Tab (ohne Atlas)."""
|
||||
"""Kapselt die Fachlogik für den Druck-Tab (ohne Atlas)."""
|
||||
|
||||
def __init__(self, pruefmanager: Pruefmanager) -> None:
|
||||
self.pruefmanager = pruefmanager
|
||||
@@ -125,8 +125,8 @@ class PrintLogic:
|
||||
|
||||
def _show_target_size_too_small(self) -> dict:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Zielgroesse zu klein",
|
||||
"Die gewaehlte Zielgroesse reicht fuer den Einzelblatt-Druck nicht aus.",
|
||||
"Zielgröße zu klein",
|
||||
"Die gewählte Zielgröße reicht für den Einzelblatt-Druck nicht aus.",
|
||||
)
|
||||
return self._failed_result()
|
||||
|
||||
@@ -348,13 +348,13 @@ class PrintLogic:
|
||||
if int(cast(Any, selected_count())) == 0:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"Auf dem Layer sind keine Features ausgewaehlt.",
|
||||
"Auf dem Layer sind keine Features ausgewählt.",
|
||||
)
|
||||
return None, ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Bestehenden Atlaslayer loeschen (optional Wiederverwedung)
|
||||
# Bestehenden Atlaslayer löschen (optional Wiederverwendung)
|
||||
existing_atlas = self._get_existing_layer_by_name("Atlaslayer")
|
||||
if existing_atlas is not None:
|
||||
try:
|
||||
@@ -367,7 +367,7 @@ class PrintLogic:
|
||||
if qgs_vector_layer is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"QgsVectorLayer ist nicht verfuegbar.",
|
||||
"QgsVectorLayer ist nicht verfügbar.",
|
||||
)
|
||||
return None, ""
|
||||
|
||||
@@ -392,7 +392,7 @@ class PrintLogic:
|
||||
)
|
||||
return None, ""
|
||||
|
||||
# Ausgewaehlte Features kopieren
|
||||
# Ausgewählte Features kopieren
|
||||
try:
|
||||
get_selected = getattr(source_layer, "selectedFeatures", None)
|
||||
if not callable(get_selected):
|
||||
@@ -400,15 +400,15 @@ class PrintLogic:
|
||||
|
||||
selected_features = list(cast(Any, get_selected()))
|
||||
if not selected_features:
|
||||
raise Exception("Keine Features ausgewaehlt")
|
||||
raise Exception("Keine Features ausgewählt")
|
||||
|
||||
qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature")
|
||||
if qgs_feature is None:
|
||||
raise Exception("QgsFeature Symbol nicht verfuegbar")
|
||||
raise Exception("QgsFeature Symbol nicht verfügbar")
|
||||
|
||||
data_provider = getattr(atlas_layer, "dataProvider", lambda: None)()
|
||||
if data_provider is None:
|
||||
raise Exception("dataProvider nicht verfuegbar")
|
||||
raise Exception("dataProvider nicht verfügbar")
|
||||
|
||||
new_features = []
|
||||
for source_feature in selected_features:
|
||||
@@ -470,7 +470,7 @@ class PrintLogic:
|
||||
layout_name: str = "",
|
||||
atlas_config: AtlasConfig | None = None,
|
||||
) -> bool:
|
||||
"""Konfiguriert den Atlas fuer ein Baufreigabe-Layout mit erweiterten Optionen.
|
||||
"""Konfiguriert den Atlas für ein Baufreigabe-Layout mit erweiterten Optionen.
|
||||
|
||||
Wird nach erfolgreicher Baufreigabe-Layout-Erstellung aufgerufen.
|
||||
Setzt den Coverage-Layer (aus sn_abdeckungslayer Variable) und konfiguriert den Atlas
|
||||
@@ -483,7 +483,7 @@ class PrintLogic:
|
||||
project = QgsProject.instance()
|
||||
layout_manager = getattr(project, "layoutManager", lambda: None)()
|
||||
if layout_manager is None:
|
||||
self.pruefmanager.zeige_hinweis("Atlas konfigurieren", "Layout Manager nicht verfuegbar.")
|
||||
self.pruefmanager.zeige_hinweis("Atlas konfigurieren", "Layout Manager nicht verfügbar.")
|
||||
return False
|
||||
|
||||
if not layout_name:
|
||||
@@ -672,7 +672,7 @@ class PrintLogic:
|
||||
if map_canvas is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Druckrechteck",
|
||||
"Das Kartenfenster ist nicht verfuegbar.",
|
||||
"Das Kartenfenster ist nicht verfügbar.",
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -684,7 +684,7 @@ class PrintLogic:
|
||||
if None in (qgs_geometry, qgs_rectangle, qgs_wkb_types, qgs_map_tool_emit_point, qgs_rubber_band):
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Druckrechteck",
|
||||
"Die Rechteck-Erfassung ist in dieser QGIS-Umgebung nicht verfuegbar.",
|
||||
"Die Rechteck-Erfassung ist in dieser QGIS-Umgebung nicht verfügbar.",
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -736,13 +736,13 @@ class PrintLogic:
|
||||
if width <= 0 or height <= 0:
|
||||
logic.pruefmanager.zeige_hinweis(
|
||||
"Druckrechteck",
|
||||
"Das gezeichnete Rechteck ist ungueltig.",
|
||||
"Das gezeichnete Rechteck ist ungültig.",
|
||||
)
|
||||
else:
|
||||
if logic._store_druckrechteck(layer, rectangle):
|
||||
logic.pruefmanager.zeige_hinweis(
|
||||
"Druckrechteck",
|
||||
"Das Druckrechteck wurde uebernommen.",
|
||||
"Das Druckrechteck wurde übernommen.",
|
||||
)
|
||||
try:
|
||||
self.rubber_band.reset(polygon_geometry)
|
||||
@@ -920,7 +920,7 @@ class PrintLogic:
|
||||
if extent is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Druckbereich",
|
||||
"Das aktuelle Kartenfenster liefert keine gueltige Ausdehnung.",
|
||||
"Das aktuelle Kartenfenster liefert keine gültige Ausdehnung.",
|
||||
)
|
||||
return None, None
|
||||
return extent, None
|
||||
@@ -931,7 +931,7 @@ class PrintLogic:
|
||||
if extent is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Druckbereich",
|
||||
"Auf dem aktuellen Layer sind keine gueltigen Objekte ausgewaehlt.",
|
||||
"Auf dem aktuellen Layer sind keine gültigen Objekte ausgewählt.",
|
||||
)
|
||||
return None, None
|
||||
return self._buffer_extent(extent, plotmassstab), None
|
||||
@@ -948,7 +948,7 @@ class PrintLogic:
|
||||
if not ergebnis.ok:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Verfahrensgebiets-Layer angeben",
|
||||
"Kein gueltiger Verfahrensgebiets-Layer gefunden. Bitte Layerauswahl im jeweiligen Plugin setzen.",
|
||||
"Kein gültiger Verfahrensgebiets-Layer gefunden. Bitte Layerauswahl im jeweiligen Plugin setzen.",
|
||||
)
|
||||
return None, None
|
||||
|
||||
@@ -1001,12 +1001,12 @@ class PrintLogic:
|
||||
required_map_height_mm: float,
|
||||
max_format: str = "DIN A3",
|
||||
) -> tuple[str, float, float, float, float, bool]:
|
||||
"""Waehlt fuer Baufreigabe ein Blatt im Querformat.
|
||||
"""Wählt für Baufreigabe ein Blatt im Querformat.
|
||||
|
||||
Args:
|
||||
required_map_width_mm: benoetigte Breite der Hauptkarte in mm
|
||||
required_map_height_mm: benoetigte Hoehe der Hauptkarte in mm
|
||||
max_format: maximale Blattgroesse, standardmaessig DIN A3
|
||||
required_map_width_mm: benötigte Breite der Hauptkarte in mm
|
||||
required_map_height_mm: benötigte Höhe der Hauptkarte in mm
|
||||
max_format: maximale Blattgröße, standardmäßig DIN A3
|
||||
|
||||
Returns:
|
||||
(din_format, page_width_mm, page_height_mm, map_w_mm, map_h_mm, fallback_applied)
|
||||
@@ -1029,8 +1029,8 @@ class PrintLogic:
|
||||
if map_w >= required_map_width_mm and map_h >= required_map_height_mm:
|
||||
return din_format, page_w, page_h, map_w, map_h, False
|
||||
|
||||
# Wenn der gewuenschte Ausschnitt nicht passt, verwende das maximal erlaubte Blatt
|
||||
# und erweitere die Hauptkarte auf die verfuegbare Kartenflaeche (zentriert per fit_extent_to_map).
|
||||
# Wenn der gewünschte Ausschnitt nicht passt, verwende das maximal erlaubte Blatt
|
||||
# und erweitere die Hauptkarte auf die verfügbare Kartenfläche (zentriert per fit_extent_to_map).
|
||||
din_format, page_w, page_h = limited_candidates[-1]
|
||||
map_w = max(page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM, 50.0)
|
||||
map_h = max(page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM, 50.0)
|
||||
@@ -1059,7 +1059,7 @@ class PrintLogic:
|
||||
return map_width_mm + fixed_width_mm, map_height_mm + fixed_height_mm
|
||||
|
||||
def _get_required_map_dimensions_mm(self, extent: Any, plotmassstab: float) -> tuple[float, float]:
|
||||
"""Berechnet die benoetigte Kartenfenstergroesse in mm fuer Extent und Massstab."""
|
||||
"""Berechnet die benötigte Kartenfenstergröße in mm für Extent und Maßstab."""
|
||||
if extent is None or not isinstance(plotmassstab, (int, float)) or plotmassstab <= 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
@@ -1077,7 +1077,7 @@ class PrintLogic:
|
||||
required_map_height_mm: float,
|
||||
max_format: str,
|
||||
) -> tuple[float, float, float, float] | None:
|
||||
"""Waehlt das kleinste passende DIN-Blatt fuer das Standardlayout."""
|
||||
"""Wählt das kleinste passende DIN-Blatt für das Standardlayout."""
|
||||
try:
|
||||
max_index = DIN_AUTO_REIHENFOLGE.index(max_format)
|
||||
except ValueError:
|
||||
@@ -1170,7 +1170,7 @@ class PrintLogic:
|
||||
resolved_layer = layer or self._resolve_layer_from_project()
|
||||
|
||||
if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE):
|
||||
self.pruefmanager.zeige_hinweis("Kartennamen waehlen", "Kartennamen waehlen")
|
||||
self.pruefmanager.zeige_hinweis("Kartennamen wählen", "Kartennamen wählen")
|
||||
return self._failed_result()
|
||||
|
||||
if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER:
|
||||
@@ -1191,7 +1191,7 @@ class PrintLogic:
|
||||
massstab_zahl = float(PLOTMASSSTAB_BY_AUSWAHL.get(massstab_auswahl, 0))
|
||||
|
||||
if massstab_zahl <= 0:
|
||||
self.pruefmanager.zeige_hinweis("Massstab fehlt", "Kein gueltiger Massstab angegeben.")
|
||||
self.pruefmanager.zeige_hinweis("Maßstab fehlt", "Kein gültiger Maßstab angegeben.")
|
||||
return self._failed_result()
|
||||
|
||||
extent, layer_for_extent = self._resolve_extent(resolved_layer, druckbereich_auswahl, massstab_zahl)
|
||||
@@ -1262,7 +1262,7 @@ class PrintLogic:
|
||||
|
||||
vorlage_name = (vorlage_name or "").strip() or kartenname
|
||||
|
||||
# Atlas-Status vorab bestimmen, damit er sowohl an create_ als auch configure_ uebergeben wird.
|
||||
# Atlas-Status vorab bestimmen, damit er sowohl an create_ als auch configure_ übergeben wird.
|
||||
atlas_raw_bf = get_variable(ATLAS_ERZEUGEN_UI_VAR, scope="project")
|
||||
atlas_erzeugen_bf = self._is_enabled_flag(atlas_raw_bf)
|
||||
|
||||
@@ -1283,7 +1283,7 @@ class PrintLogic:
|
||||
atlas_driven=atlas_erzeugen_bf,
|
||||
)
|
||||
else:
|
||||
# Standard-Layout: dynamische Groessenanpassung
|
||||
# Standard-Layout: dynamische Größenanpassung
|
||||
PrintLayout().create_single_page_layout(
|
||||
name=vorlage_name,
|
||||
page_width_mm=auto_page_w,
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestDataGrabber(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_detects_excel(self, mock_dateipruefer):
|
||||
"""Prüft, dass eine erreichbare .xlsx-Datei als Quelltyp 'excel' erkannt wird."""
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
@@ -33,6 +34,7 @@ class TestDataGrabber(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_returns_unknown_filetype(self, mock_dateipruefer):
|
||||
"""Prüft, dass eine erreichbare Datei mit unbekannter Endung als 'unbekannter_dateityp' eingestuft wird."""
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
@@ -48,6 +50,7 @@ class TestDataGrabber(unittest.TestCase):
|
||||
@patch("sn_basis.modules.DataGrabber.Linkpruefer")
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_detects_service_when_file_check_fails(self, mock_dateipruefer, mock_linkpruefer):
|
||||
"""Prüft, dass eine Quelle als 'dienst' erkannt wird, wenn die Dateiprüfung fehlschlägt, die URL aber erreichbar ist."""
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="missing",
|
||||
@@ -70,6 +73,7 @@ class TestDataGrabber(unittest.TestCase):
|
||||
@patch("sn_basis.modules.DataGrabber.Linkpruefer")
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_returns_unbekannte_quelle_on_full_failure(self, mock_dateipruefer, mock_linkpruefer):
|
||||
"""Prüft, dass eine Quelle als 'unbekannte_quelle' eingestuft wird, wenn weder Datei- noch Linkprüfung erfolgreich sind."""
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="missing",
|
||||
@@ -92,6 +96,7 @@ class TestDataGrabber(unittest.TestCase):
|
||||
@patch.object(DataGrabber, "analyze_source_type")
|
||||
@patch.object(DataGrabber, "_process_excel_source")
|
||||
def test_run_returns_success_summary_with_valid_rows(self, mock_process_excel, mock_analyze):
|
||||
"""Prüft, dass run() bei erfolgreichem Excel-Import eine Erfolgsmeldung mit den geladenen Zeilen liefert."""
|
||||
mock_analyze.return_value = (
|
||||
"excel",
|
||||
pruef_ergebnis(ok=True, meldung="ok", aktion="ok", kontext=None),
|
||||
@@ -110,6 +115,7 @@ class TestDataGrabber(unittest.TestCase):
|
||||
|
||||
@patch.object(DataGrabber, "analyze_source_type")
|
||||
def test_run_returns_failure_summary_without_rows(self, mock_analyze):
|
||||
"""Prüft, dass run() bei nicht erkennbarer Quelle eine Fehlermeldung ohne Zeilen liefert."""
|
||||
mock_analyze.return_value = (
|
||||
"unbekannte_quelle",
|
||||
pruef_ergebnis(ok=False, meldung="bad", aktion="unbekannte_quelle", kontext=None),
|
||||
|
||||
@@ -13,6 +13,7 @@ class TestDateipruefer(unittest.TestCase):
|
||||
# 1. Leere Eingabe erlaubt
|
||||
# -----------------------------------------------------
|
||||
def test_leereingabe_erlaubt(self):
|
||||
"""Prüft, dass eine leere Pfadangabe bei leereingabe_erlaubt=True als 'leereingabe_erlaubt' ohne Kontext gemeldet wird."""
|
||||
pruefer = Dateipruefer(
|
||||
pfad="",
|
||||
leereingabe_erlaubt=True
|
||||
@@ -28,6 +29,7 @@ class TestDateipruefer(unittest.TestCase):
|
||||
# 2. Leere Eingabe nicht erlaubt
|
||||
# -----------------------------------------------------
|
||||
def test_leereingabe_nicht_erlaubt(self):
|
||||
"""Prüft, dass eine leere Pfadangabe ohne leereingabe_erlaubt als 'leereingabe_nicht_erlaubt' abgelehnt wird."""
|
||||
pruefer = Dateipruefer(
|
||||
pfad="",
|
||||
leereingabe_erlaubt=False
|
||||
@@ -43,6 +45,7 @@ class TestDateipruefer(unittest.TestCase):
|
||||
# 3. Standarddatei vorschlagen
|
||||
# -----------------------------------------------------
|
||||
def test_standarddatei_vorschlagen(self):
|
||||
"""Prüft, dass bei leerer Pfadangabe mit gesetzter Standarddatei 'standarddatei_vorschlagen' mit dem Standardpfad als Kontext geliefert wird."""
|
||||
pruefer = Dateipruefer(
|
||||
pfad="",
|
||||
standarddatei="/tmp/std.txt"
|
||||
@@ -58,6 +61,7 @@ class TestDateipruefer(unittest.TestCase):
|
||||
# 4. Temporäre Datei erlaubt
|
||||
# -----------------------------------------------------
|
||||
def test_temporaer_erlaubt(self):
|
||||
"""Prüft, dass eine leere Pfadangabe bei temporaer_erlaubt=True als 'temporaer_erlaubt' ohne Kontext gemeldet wird."""
|
||||
pruefer = Dateipruefer(
|
||||
pfad="",
|
||||
temporaer_erlaubt=True
|
||||
@@ -74,6 +78,7 @@ class TestDateipruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=False)
|
||||
def test_datei_nicht_gefunden(self, mock_exists):
|
||||
"""Prüft, dass ein nicht existierender Dateipfad als 'datei_nicht_gefunden' mit dem Pfad als Kontext abgelehnt wird."""
|
||||
pruefer = Dateipruefer(
|
||||
pfad="/tmp/nichtvorhanden.txt"
|
||||
)
|
||||
@@ -89,6 +94,7 @@ class TestDateipruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=True)
|
||||
def test_datei_ok(self, mock_exists):
|
||||
"""Prüft, dass ein existierender Dateipfad als 'ok' mit dem Pfad als Kontext bestätigt wird."""
|
||||
pruefer = Dateipruefer(
|
||||
pfad="/tmp/test.txt"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""sn_basis/tests/test_laenderkennung.py
|
||||
|
||||
Unit-Tests für :mod:`sn_basis.functions.laenderkennung`.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from sn_basis.functions.laenderkennung import get_laenderkennung, LAENDERKENNUNGEN
|
||||
|
||||
|
||||
class TestGetLaenderkennung(unittest.TestCase):
|
||||
def test_bekannter_praefix_gibt_kennung_zurueck(self):
|
||||
"""Prüft, dass der Präfix 'sn' die ALKIS-Länderkennung '14' für Sachsen liefert."""
|
||||
self.assertEqual(get_laenderkennung("sn"), "14")
|
||||
|
||||
def test_praefix_gross_klein_unabhaengig(self):
|
||||
"""Prüft, dass die Präfixsuche groß-/kleinschreibungsunabhängig ist."""
|
||||
self.assertEqual(get_laenderkennung("SN"), "14")
|
||||
|
||||
def test_unbekannter_praefix_wirft_keyerror(self):
|
||||
"""Prüft, dass ein unbekannter Präfix einen KeyError auslöst."""
|
||||
with self.assertRaises(KeyError):
|
||||
get_laenderkennung("xx")
|
||||
|
||||
def test_ohne_argument_wird_aus_aufrufendem_modul_abgeleitet(self):
|
||||
"""Prüft, dass ohne Argument der Präfix aus dem Modulnamen des Aufrufers abgeleitet wird."""
|
||||
# __name__ dieses Testmoduls beginnt mit "sn_basis" -> Präfix "sn"
|
||||
self.assertEqual(get_laenderkennung(), "14")
|
||||
|
||||
def test_mapping_enthaelt_sachsen(self):
|
||||
"""Prüft, dass die Mapping-Tabelle LAENDERKENNUNGEN einen Eintrag für Sachsen ('sn' -> '14') enthält."""
|
||||
self.assertEqual(LAENDERKENNUNGEN.get("sn"), "14")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -97,6 +97,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
|
||||
def test_layer_exists(self):
|
||||
"""Prüft, dass ein nicht vorhandener Layer als 'layer_nicht_gefunden' abgelehnt wird."""
|
||||
layer = MockLayer(exists=False)
|
||||
pruefer = Layerpruefer(layer)
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -104,6 +105,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "layer_nicht_gefunden")
|
||||
|
||||
def test_layer_unsichtbar(self):
|
||||
"""Prüft, dass ein unsichtbarer Layer bei geforderter Sichtbarkeit als 'layer_unsichtbar' abgelehnt wird."""
|
||||
layer = MockLayer(visible=False)
|
||||
pruefer = Layerpruefer(layer, muss_sichtbar_sein=True)
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -111,6 +113,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "layer_unsichtbar")
|
||||
|
||||
def test_falscher_layertyp(self):
|
||||
"""Prüft, dass ein Layer mit abweichendem Layertyp (Raster statt Vektor) als 'falscher_layertyp' abgelehnt wird."""
|
||||
layer = MockLayer(layer_type="raster")
|
||||
pruefer = Layerpruefer(layer, erwarteter_layertyp="vector")
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -118,6 +121,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "falscher_layertyp")
|
||||
|
||||
def test_falscher_geotyp(self):
|
||||
"""Prüft, dass ein Layer mit abweichendem Geometrietyp (Punkt statt Polygon) als 'falscher_geotyp' abgelehnt wird."""
|
||||
layer = MockLayer(geometry_type="Point")
|
||||
pruefer = Layerpruefer(layer, erwarteter_geotyp="Polygon")
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -125,6 +129,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "falscher_geotyp")
|
||||
|
||||
def test_layer_leer(self):
|
||||
"""Prüft, dass ein Layer ohne Features als 'layer_leer' abgelehnt wird."""
|
||||
layer = MockLayer(feature_count=0)
|
||||
pruefer = Layerpruefer(layer)
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -132,6 +137,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "layer_leer")
|
||||
|
||||
def test_falsches_crs(self):
|
||||
"""Prüft, dass ein Layer mit abweichendem Koordinatenbezugssystem als 'falsches_crs' abgelehnt wird."""
|
||||
layer = MockLayer(crs="EPSG:4326")
|
||||
pruefer = Layerpruefer(layer, erwartetes_crs="EPSG:25833")
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -139,6 +145,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "falsches_crs")
|
||||
|
||||
def test_felder_fehlen(self):
|
||||
"""Prüft, dass ein Layer mit fehlenden Pflichtfeldern als 'felder_fehlen' abgelehnt wird."""
|
||||
layer = MockLayer(fields=["id"])
|
||||
pruefer = Layerpruefer(layer, erforderliche_felder=["id", "name"])
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -146,6 +153,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "felder_fehlen")
|
||||
|
||||
def test_datenquelle_unerwartet(self):
|
||||
"""Prüft, dass eine Datenquelle außerhalb der erlaubten Liste als 'datenquelle_unerwartet' abgelehnt wird."""
|
||||
layer = MockLayer(source="/tmp/test.shp")
|
||||
pruefer = Layerpruefer(layer, erlaubte_datenquellen=["/tmp/allowed.shp"])
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -153,6 +161,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "datenquelle_unerwartet")
|
||||
|
||||
def test_layer_nicht_editierbar(self):
|
||||
"""Prüft, dass ein nicht editierbarer Layer bei geforderter Editierbarkeit als 'layer_nicht_editierbar' abgelehnt wird."""
|
||||
layer = MockLayer(editable=False)
|
||||
pruefer = Layerpruefer(layer, muss_editierbar_sein=True)
|
||||
ergebnis = pruefer.pruefe()
|
||||
@@ -160,6 +169,7 @@ class TestLayerpruefer(unittest.TestCase):
|
||||
self.assertEqual(ergebnis.aktion, "layer_nicht_editierbar")
|
||||
|
||||
def test_layer_ok(self):
|
||||
"""Prüft, dass ein Layer, der alle Anforderungen erfüllt, als 'ok' bestätigt wird."""
|
||||
layer = MockLayer()
|
||||
pruefer = Layerpruefer(layer)
|
||||
ergebnis = pruefer.pruefe()
|
||||
|
||||
@@ -15,6 +15,7 @@ class TestLinkpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.linkpruefer.network_head")
|
||||
def test_remote_link_ok(self, mock_head):
|
||||
"""Prüft, dass eine erreichbare Remote-URL (HTTP HEAD ohne Fehler) als 'ok' bestätigt wird."""
|
||||
mock_head.return_value = NetworkReply(error=0)
|
||||
|
||||
lp = Linkpruefer()
|
||||
@@ -29,6 +30,7 @@ class TestLinkpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.linkpruefer.network_head")
|
||||
def test_remote_link_error(self, mock_head):
|
||||
"""Prüft, dass eine Remote-URL mit Fehlerantwort als 'url_nicht_erreichbar' abgelehnt wird."""
|
||||
mock_head.return_value = NetworkReply(error=1)
|
||||
|
||||
lp = Linkpruefer()
|
||||
@@ -43,6 +45,7 @@ class TestLinkpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.linkpruefer.network_head", return_value=None)
|
||||
def test_remote_link_network_error(self, mock_head):
|
||||
"""Prüft, dass eine ausbleibende Netzwerkantwort (None) als 'netzwerkfehler' gemeldet wird."""
|
||||
lp = Linkpruefer()
|
||||
result = lp.pruefe("http://example.com")
|
||||
|
||||
@@ -55,6 +58,7 @@ class TestLinkpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=False)
|
||||
def test_local_link_not_found(self, mock_exists):
|
||||
"""Prüft, dass ein nicht existierender lokaler Pfad als 'pfad_nicht_gefunden' abgelehnt wird."""
|
||||
lp = Linkpruefer()
|
||||
result = lp.pruefe("/path/to/missing/file.shp")
|
||||
|
||||
@@ -67,6 +71,7 @@ class TestLinkpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=True)
|
||||
def test_local_link_ok(self, mock_exists):
|
||||
"""Prüft, dass ein existierender lokaler Pfad als 'ok' bestätigt wird."""
|
||||
lp = Linkpruefer()
|
||||
result = lp.pruefe("/path/to/file.shp")
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class _FakeLegendForResize:
|
||||
class TestPrintLayout(unittest.TestCase):
|
||||
|
||||
def test_create_legend_item_links_map_and_enables_map_filtering(self):
|
||||
"""Prüft, dass eine erzeugte Legende mit der übergebenen Karte verknüpft ist und automatisch nach deren Sichtbarkeit gefiltert wird."""
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
layout = QgsPrintLayout(project)
|
||||
@@ -74,6 +75,7 @@ class TestPrintLayout(unittest.TestCase):
|
||||
self.assertTrue(legend.legend_filter_by_map_enabled)
|
||||
|
||||
def test_create_legend_item_does_not_copy_project_layers_into_legend_model(self):
|
||||
"""Prüft, dass eine neu erzeugte Legende keine Projekt-Layer in ihr Legendenmodell übernimmt (leere Root-Gruppe)."""
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
layout = QgsPrintLayout(project)
|
||||
@@ -91,6 +93,7 @@ class TestPrintLayout(unittest.TestCase):
|
||||
self.assertEqual(root_group.layers, [])
|
||||
|
||||
def test_configure_legend_size_uses_content_adaptive_size(self):
|
||||
"""Prüft, dass die Legendengröße inhaltsadaptiv bestimmt wird (kein manuelles Resize) und die ermittelte Höhe zurückgegeben wird."""
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
legend = _FakeLegendForResize()
|
||||
@@ -107,6 +110,7 @@ class TestPrintLayout(unittest.TestCase):
|
||||
self.assertEqual(height, 33.0)
|
||||
|
||||
def test_configure_hauptkarte_grids_uses_wrapper_constants(self):
|
||||
"""Prüft, dass das Gitternetz der Hauptkarte mit den erwarteten Stil-, Rahmen- und Beschriftungskonstanten (nur rechts/unten beschriftet, Schriftgröße und Hintergrund) konfiguriert wird."""
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
layout = QgsPrintLayout(project)
|
||||
@@ -153,6 +157,7 @@ class TestPrintLayout(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.modules.print_layout.open_layout_designer")
|
||||
def test_create_single_page_layout_keeps_legend_linked_to_hauptkarte_and_theme(self, _mock_open_designer):
|
||||
"""Prüft, dass ein einseitiges Layout die Hauptkarte auf das übergebene Kartenthema festlegt und die Legende mit dieser Karte verknüpft bleibt."""
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.logic = PrintLogic(pruefmanager=MagicMock())
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_prefers_a4_for_small_extent(self):
|
||||
"""Prüft, dass für einen kleinen Kartenausschnitt (Baufreigabe) das Format DIN A4 ohne Fallback gewählt wird."""
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(80.0, 120.0)
|
||||
|
||||
self.assertEqual(din_format, "DIN A4")
|
||||
@@ -46,6 +47,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertFalse(fallback)
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_escalates_to_a3(self):
|
||||
"""Prüft, dass ein größerer Kartenausschnitt (Baufreigabe) auf DIN A3 eskaliert wird."""
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(200.0, 180.0)
|
||||
|
||||
self.assertEqual(din_format, "DIN A3")
|
||||
@@ -54,6 +56,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertFalse(fallback)
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_respects_a4_as_maximum_with_fallback(self):
|
||||
"""Prüft, dass bei auf DIN A4 begrenztem Höchstformat ein eigentlich A3-großer Ausschnitt mit Fallback-Kennzeichnung auf A4 reduziert wird."""
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(
|
||||
200.0,
|
||||
180.0,
|
||||
@@ -66,6 +69,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertTrue(fallback)
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_falls_back_to_a3_for_oversize_extent(self):
|
||||
"""Prüft, dass ein Ausschnitt, der auch DIN A3 übersteigt, mit Fallback-Kennzeichnung auf DIN A3 begrenzt wird."""
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(400.0, 300.0)
|
||||
|
||||
self.assertEqual(din_format, "DIN A3")
|
||||
@@ -74,21 +78,25 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertTrue(fallback)
|
||||
|
||||
def test_select_standard_page_layout_uses_a4_as_minimum_sheet(self):
|
||||
"""Prüft, dass für einen kleinen Ausschnitt trotz Höchstgrenze DIN A3 mindestens DIN A4 als Blattgröße gewählt wird."""
|
||||
selected = self.logic._select_standard_page_layout(60.0, 100.0, max_format="DIN A3")
|
||||
|
||||
self.assertEqual(selected, (297.0, 210.0, 87.0, 190.0))
|
||||
|
||||
def test_select_standard_page_layout_limits_hauptkarte_to_a3(self):
|
||||
"""Prüft, dass ein die Fläche voll ausnutzender Ausschnitt auf DIN A3 als größtes zulässiges Format begrenzt wird."""
|
||||
selected = self.logic._select_standard_page_layout(120.0, 240.0, max_format="DIN A3")
|
||||
|
||||
self.assertEqual(selected, (420.0, 297.0, 210.0, 277.0))
|
||||
|
||||
def test_select_standard_page_layout_returns_none_if_a3_is_too_small(self):
|
||||
"""Prüft, dass kein Layout gewählt wird (None), wenn der Ausschnitt selbst DIN A3 als Höchstformat übersteigt."""
|
||||
selected = self.logic._select_standard_page_layout(260.0, 280.0, max_format="DIN A3")
|
||||
|
||||
self.assertIsNone(selected)
|
||||
|
||||
def test_get_standard_layout_dimensions_uses_a4_and_forces_fit_for_small_extent(self):
|
||||
"""Prüft, dass für einen kleinen Ausschnitt DIN A4 gewählt und das Einpassen (force_fit) erzwungen wird."""
|
||||
page_w, page_h, map_w, map_h, force_fit = self.logic._get_standard_layout_dimensions(
|
||||
60.0,
|
||||
100.0,
|
||||
@@ -101,6 +109,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertTrue(force_fit)
|
||||
|
||||
def test_get_standard_layout_dimensions_caps_endlosrolle_width_at_2000_mm(self):
|
||||
"""Prüft, dass bei aktiviertem Formfaktor (Endlosrolle) die Seitenbreite auf die maximale Rollenbreite (2000 mm) begrenzt wird."""
|
||||
page_w, page_h, map_w, map_h, force_fit = self.logic._get_standard_layout_dimensions(
|
||||
2500.0,
|
||||
100.0,
|
||||
@@ -114,6 +123,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertTrue(force_fit)
|
||||
|
||||
def test_apply_standard_minimum_a4_fallback_for_small_page(self):
|
||||
"""Prüft, dass eine zu kleine Seite auf die A4-Mindestgröße angehoben und als erzwungen (forced) markiert wird."""
|
||||
page_w, page_h, map_w, map_h, forced = self.logic._apply_standard_minimum_a4_fallback(
|
||||
250.0,
|
||||
180.0,
|
||||
@@ -126,6 +136,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertEqual((map_w, map_h), (87.0, 190.0))
|
||||
|
||||
def test_apply_standard_minimum_a4_fallback_keeps_large_page(self):
|
||||
"""Prüft, dass eine bereits ausreichend große Seite (A3) unverändert bleibt und nicht als erzwungen markiert wird."""
|
||||
page_w, page_h, map_w, map_h, forced = self.logic._apply_standard_minimum_a4_fallback(
|
||||
420.0,
|
||||
297.0,
|
||||
@@ -138,23 +149,27 @@ class TestPrintLogic(unittest.TestCase):
|
||||
self.assertEqual((map_w, map_h), (210.0, 277.0))
|
||||
|
||||
def test_does_plot_fit_page_true_for_standard_a4(self):
|
||||
"""Prüft, dass ein Kartenobjekt, das in eine DIN A4-Seite passt, als passend erkannt wird."""
|
||||
fits = self.logic._does_plot_fit_page(297.0, 210.0, 87.0, 190.0, 210.0, 20.0)
|
||||
|
||||
self.assertTrue(fits)
|
||||
|
||||
def test_does_plot_fit_page_false_if_objects_exceed_page(self):
|
||||
"""Prüft, dass ein Kartenobjekt, das die Seitenbreite überschreitet, als nicht passend erkannt wird."""
|
||||
fits = self.logic._does_plot_fit_page(297.0, 210.0, 120.0, 190.0, 210.0, 20.0)
|
||||
|
||||
self.assertFalse(fits)
|
||||
|
||||
@patch("sn_basis.modules.print_logic.set_variable")
|
||||
def test_set_druckbereich_accepts_wie_kartenfenster(self, mock_set_variable):
|
||||
"""Prüft, dass die Auswahl 'wie Kartenfenster' als Projektvariable 'druckbereich' gespeichert wird."""
|
||||
self.logic.set_druckbereich_for_auswahl(DRUCKBEREICH_WIE_KARTENFENSTER)
|
||||
|
||||
mock_set_variable.assert_called_once_with("druckbereich", DRUCKBEREICH_WIE_KARTENFENSTER, scope="project")
|
||||
|
||||
@patch("sn_basis.modules.print_logic.iface")
|
||||
def test_get_canvas_extent_returns_valid_extent(self, mock_iface):
|
||||
"""Prüft, dass der aktuelle Kartenausschnitt des Kartenfensters unverändert zurückgegeben wird."""
|
||||
extent = _FakeExtent(120.0, 90.0)
|
||||
mock_iface.mapCanvas.return_value = _FakeCanvas(extent)
|
||||
|
||||
@@ -165,6 +180,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
@patch.object(PrintLogic, "_buffer_extent")
|
||||
@patch.object(PrintLogic, "_get_canvas_extent")
|
||||
def test_resolve_extent_uses_canvas_extent_without_buffer(self, mock_get_canvas_extent, mock_buffer_extent):
|
||||
"""Prüft, dass bei Druckbereich 'wie Kartenfenster' der Kartenausschnitt ohne Puffer und ohne verknüpften Layer übernommen wird."""
|
||||
extent = _FakeExtent(120.0, 90.0)
|
||||
mock_get_canvas_extent.return_value = extent
|
||||
|
||||
@@ -177,6 +193,7 @@ class TestPrintLogic(unittest.TestCase):
|
||||
@patch.object(PrintLogic, "_buffer_extent")
|
||||
@patch.object(PrintLogic, "_get_druckrechteck_extent")
|
||||
def test_resolve_extent_uses_rectangle_extent_without_buffer(self, mock_get_rect_extent, mock_buffer_extent):
|
||||
"""Prüft, dass bei Druckbereich 'Rechteck' der gezeichnete Ausschnitt ohne Puffer und ohne verknüpften Layer übernommen wird."""
|
||||
extent = _FakeExtent(120.0, 90.0)
|
||||
mock_get_rect_extent.return_value = extent
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# 1. OK-Ergebnis → keine Interaktion
|
||||
# -----------------------------------------------------
|
||||
def test_ok(self):
|
||||
"""Prüft, dass ein bereits erfolgreiches Prüfergebnis ohne Nutzerinteraktion als 'ok' durchgereicht wird."""
|
||||
ergebnis = pruef_ergebnis(True, "Alles gut", "ok", None)
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -27,6 +28,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||
def test_leereingabe_erlaubt_ja(self, mock_ask):
|
||||
"""Prüft, dass bei erlaubter leerer Eingabe eine Ja-Antwort des Nutzers die Entscheidung 'ok' liefert."""
|
||||
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -38,6 +40,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||
def test_leereingabe_erlaubt_nein(self, mock_ask):
|
||||
"""Prüft, dass bei erlaubter leerer Eingabe eine Nein-Antwort die Aktion 'leereingabe_erlaubt' als abgelehnt bestehen lässt."""
|
||||
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -49,6 +52,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||
def test_standarddatei_vorschlagen_ja(self, mock_ask):
|
||||
"""Prüft, dass eine Ja-Antwort auf den Vorschlag der Standarddatei die Entscheidung 'ok' mit dem Standardpfad als Kontext liefert."""
|
||||
ergebnis = pruef_ergebnis(
|
||||
False,
|
||||
"Standarddatei verwenden?",
|
||||
@@ -67,6 +71,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||
def test_standarddatei_vorschlagen_nein(self, mock_ask):
|
||||
"""Prüft, dass eine Nein-Antwort auf den Vorschlag der Standarddatei die Aktion 'standarddatei_vorschlagen' als abgelehnt bestehen lässt."""
|
||||
ergebnis = pruef_ergebnis(
|
||||
False,
|
||||
"Standarddatei verwenden?",
|
||||
@@ -84,6 +89,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||
def test_temporaer_erlaubt_ja(self, mock_ask):
|
||||
"""Prüft, dass eine Ja-Antwort auf die Rückfrage nach einer temporären Datei die Aktion 'temporaer_erzeugen' auslöst."""
|
||||
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -95,6 +101,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||
def test_temporaer_erlaubt_nein(self, mock_ask):
|
||||
"""Prüft, dass eine Nein-Antwort auf die Rückfrage nach einer temporären Datei die Aktion 'temporaer_erlaubt' als abgelehnt bestehen lässt."""
|
||||
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -107,6 +114,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||
@patch("sn_basis.modules.Pruefmanager.set_layer_visible")
|
||||
def test_layer_unsichtbar_ja(self, mock_set, mock_ask):
|
||||
"""Prüft, dass eine Ja-Antwort bei unsichtbarem Layer den Layer sichtbar schaltet und die Entscheidung 'ok' liefert."""
|
||||
fake_layer = object()
|
||||
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
||||
|
||||
@@ -121,6 +129,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||
def test_layer_unsichtbar_nein(self, mock_ask):
|
||||
"""Prüft, dass eine Nein-Antwort bei unsichtbarem Layer die Aktion 'layer_unsichtbar' als abgelehnt bestehen lässt."""
|
||||
fake_layer = object()
|
||||
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
||||
|
||||
@@ -134,6 +143,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.Pruefmanager.warning")
|
||||
def test_unbekannte_aktion(self, mock_warn):
|
||||
"""Prüft, dass eine unbekannte Aktion eine Warnung auslöst und unverarbeitet als Fehlschlag zurückgegeben wird."""
|
||||
ergebnis = pruef_ergebnis(False, "???", "unbekannt", None)
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -143,6 +153,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="overwrite")
|
||||
def test_datei_existiert_overwrite(self, _mock_choice):
|
||||
"""Prüft, dass die Nutzerwahl 'überschreiben' bei bestehender Datei zur Aktion 'datei_existiert_ueberschreiben' führt."""
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -152,6 +163,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="append")
|
||||
def test_datei_existiert_append(self, _mock_choice):
|
||||
"""Prüft, dass die Nutzerwahl 'anhängen' bei bestehender Datei zur Aktion 'datei_existiert_anhaengen' führt."""
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -160,6 +172,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="cancel")
|
||||
def test_datei_existiert_cancel(self, _mock_choice):
|
||||
"""Prüft, dass die Nutzerwahl 'abbrechen' bei bestehender Datei zur Aktion 'datei_existiert_ueberspringen' führt."""
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
@@ -167,6 +180,7 @@ class TestPruefmanager(unittest.TestCase):
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_ueberspringen")
|
||||
|
||||
def test_datei_existiert_in_non_qgis_mode_returns_input(self):
|
||||
"""Prüft, dass außerhalb des QGIS-Modus bei bestehender Datei keine Rückfrage erfolgt, sondern die ursprüngliche Aktion unentschieden zurückgegeben wird."""
|
||||
manager = Pruefmanager(ui_modus="test")
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class TestSettingsLogic(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.functions.settings_logic.get_variable")
|
||||
def test_load(self, mock_get):
|
||||
"""Prüft, dass load() jede bekannte Projektvariable genau einmal über get_variable() liest."""
|
||||
# Mock-Rückgabe für jede Variable
|
||||
mock_get.side_effect = lambda key, scope="project": f"wert_{key}"
|
||||
|
||||
@@ -32,6 +33,7 @@ class TestSettingsLogic(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.functions.settings_logic.set_variable")
|
||||
def test_save(self, mock_set):
|
||||
"""Prüft, dass save() nur bekannte Variablen-Keys speichert und unbekannte Keys ignoriert."""
|
||||
logic = SettingsLogic()
|
||||
|
||||
# Eingabedaten enthalten gültige und ungültige Keys
|
||||
@@ -57,6 +59,7 @@ class TestSettingsLogic(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.functions.settings_logic.get_variable")
|
||||
def test_load_handles_partial_values_with_empty_fallback(self, mock_get):
|
||||
"""Prüft, dass load() bei fehlenden Werten auf einen leeren String zurückfällt statt einen Fehler zu werfen."""
|
||||
values = {
|
||||
"amt": "A1",
|
||||
"behoerde": "",
|
||||
@@ -74,6 +77,7 @@ class TestSettingsLogic(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.functions.settings_logic.set_variable")
|
||||
def test_save_empty_string_value_is_persisted(self, mock_set):
|
||||
"""Prüft, dass save() einen leeren String als gültigen Wert speichert, statt ihn wie einen fehlenden Key zu behandeln."""
|
||||
logic = SettingsLogic()
|
||||
|
||||
logic.save({"amt": "", "behoerde": "B2"})
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestStilpruefer(unittest.TestCase):
|
||||
# 1. Keine Datei angegeben
|
||||
# -----------------------------------------------------
|
||||
def test_keine_datei_angegeben(self):
|
||||
"""Prüft, dass eine leere Pfadangabe als 'ok' ohne Kontext akzeptiert wird (Stil ist optional)."""
|
||||
result = self.pruefer.pruefe("")
|
||||
|
||||
self.assertTrue(result.ok)
|
||||
@@ -30,6 +31,7 @@ class TestStilpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
|
||||
def test_datei_existiert_mit_qml(self, mock_exists):
|
||||
"""Prüft, dass eine existierende .qml-Datei als 'stil_anwendbar' bestätigt wird."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
@@ -48,6 +50,7 @@ class TestStilpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
|
||||
def test_datei_existiert_falsche_endung(self, mock_exists):
|
||||
"""Prüft, dass eine existierende Datei ohne .qml-Endung als 'falsche_endung' abgelehnt wird."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
@@ -67,6 +70,7 @@ class TestStilpruefer(unittest.TestCase):
|
||||
# -----------------------------------------------------
|
||||
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=False)
|
||||
def test_datei_existiert_nicht(self, mock_exists):
|
||||
"""Prüft, dass ein nicht existierender .qml-Pfad als 'datei_nicht_gefunden' abgelehnt wird."""
|
||||
fake_path = "/tmp/nichtvorhanden.qml"
|
||||
|
||||
result = self.pruefer.pruefe(fake_path)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""sn_basis/tests/test_text_wrapper.py
|
||||
|
||||
Unit-Tests für :mod:`sn_basis.functions.text_wrapper`.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from sn_basis.functions.text_wrapper import sanitize_bezeichner
|
||||
|
||||
|
||||
class TestSanitizeBezeichner(unittest.TestCase):
|
||||
def test_umlaute_bleiben_erhalten(self):
|
||||
"""Prüft, dass Umlaute und ß nicht durch den Ersatz entfernt werden."""
|
||||
self.assertEqual(sanitize_bezeichner("Grünfläche"), "Grünfläche")
|
||||
self.assertEqual(sanitize_bezeichner("Straße"), "Straße")
|
||||
|
||||
def test_bindestrich_bleibt_erhalten(self):
|
||||
"""Prüft, dass ein Bindestrich im Bezeichner erhalten bleibt (Issue #42)."""
|
||||
self.assertEqual(sanitize_bezeichner("Wege-Plan"), "Wege-Plan")
|
||||
|
||||
def test_unterstrich_bleibt_erhalten(self):
|
||||
"""Prüft, dass ein Unterstrich im Bezeichner erhalten bleibt."""
|
||||
self.assertEqual(sanitize_bezeichner("thema_flur_1"), "thema_flur_1")
|
||||
|
||||
def test_leerzeichen_wird_durch_ersatz_zusammengefasst(self):
|
||||
"""Prüft, dass Leerzeichen (kein zulässiges Zeichen) zu einem Ersatzzeichen werden."""
|
||||
self.assertEqual(sanitize_bezeichner("Flurstücke Gemarkung"), "Flurstücke_Gemarkung")
|
||||
|
||||
def test_fuehrender_und_folgender_ersatz_wird_entfernt(self):
|
||||
"""Prüft, dass ein Ersatzzeichen am Anfang oder Ende des Ergebnisses entfernt wird."""
|
||||
self.assertEqual(sanitize_bezeichner("!Thema!"), "Thema")
|
||||
|
||||
def test_eigenes_ersatzzeichen_wird_verwendet(self):
|
||||
"""Prüft, dass ein abweichendes Ersatzzeichen anstelle von Unterstrich genutzt wird."""
|
||||
self.assertEqual(sanitize_bezeichner("a b", ersatz="-"), "a-b")
|
||||
|
||||
def test_nur_unzulaessige_zeichen_ergibt_leeren_string(self):
|
||||
"""Prüft, dass ein Bezeichner ganz aus unzulässigen Zeichen zu einem leeren String wird."""
|
||||
self.assertEqual(sanitize_bezeichner("!!!"), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,6 +23,7 @@ class TestVariableWrapper(unittest.TestCase):
|
||||
mock_set_project_variable,
|
||||
mock_project_instance,
|
||||
):
|
||||
"""Prüft, dass set_variable() im Projekt-Scope nicht schreibt, wenn der neue Wert dem bestehenden entspricht."""
|
||||
mock_project_instance.return_value = object()
|
||||
mock_project_scope.return_value = _Scope("layer_1")
|
||||
|
||||
@@ -39,6 +40,7 @@ class TestVariableWrapper(unittest.TestCase):
|
||||
mock_set_project_variable,
|
||||
mock_project_instance,
|
||||
):
|
||||
"""Prüft, dass set_variable() im Projekt-Scope bei geändertem Wert mit dem Präfix 'sn_' geschrieben wird."""
|
||||
fake_project = object()
|
||||
mock_project_instance.return_value = fake_project
|
||||
mock_project_scope.return_value = _Scope("old_value")
|
||||
@@ -53,12 +55,14 @@ class TestVariableWrapper(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.setGlobalVariable")
|
||||
def test_set_variable_global_writes_prefixed_name(self, mock_set_global):
|
||||
"""Prüft, dass set_variable() im globalen Scope den Variablennamen mit 'sn_' vorangestellt schreibt."""
|
||||
variable_wrapper.set_variable("verfahrensnummer", "123", scope="global")
|
||||
|
||||
mock_set_global.assert_called_once_with("sn_verfahrensnummer", "123")
|
||||
|
||||
@patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.globalScope")
|
||||
def test_get_variable_global_returns_empty_string_fallback(self, mock_global_scope):
|
||||
"""Prüft, dass get_variable() im globalen Scope für eine nicht gesetzte Variable einen leeren String liefert."""
|
||||
mock_global_scope.return_value = _Scope("")
|
||||
|
||||
value = variable_wrapper.get_variable("nicht_vorhanden", scope="global")
|
||||
@@ -66,10 +70,12 @@ class TestVariableWrapper(unittest.TestCase):
|
||||
self.assertEqual(value, "")
|
||||
|
||||
def test_get_variable_invalid_scope_raises(self):
|
||||
"""Prüft, dass get_variable() bei einem unbekannten Scope-Namen einen ValueError auslöst."""
|
||||
with self.assertRaises(ValueError):
|
||||
variable_wrapper.get_variable("foo", scope="invalid")
|
||||
|
||||
def test_set_variable_invalid_scope_raises(self):
|
||||
"""Prüft, dass set_variable() bei einem unbekannten Scope-Namen einen ValueError auslöst."""
|
||||
with self.assertRaises(ValueError):
|
||||
variable_wrapper.set_variable("foo", "bar", scope="invalid")
|
||||
|
||||
|
||||
@@ -16,18 +16,21 @@ class TestVerfahrensgebietManager(unittest.TestCase):
|
||||
|
||||
@patch("sn_basis.functions.verfahrensgebiet_manager.get_variable", return_value="layer_123")
|
||||
def test_get_verfahrensgebiet_layer_id_returns_value(self, _mock_get_variable):
|
||||
"""Prüft, dass get_verfahrensgebiet_layer_id() die in der Projektvariable gespeicherte Layer-ID liefert."""
|
||||
result = verfahrensgebiet_manager.get_verfahrensgebiet_layer_id()
|
||||
|
||||
self.assertEqual(result, "layer_123")
|
||||
|
||||
@patch("sn_basis.functions.verfahrensgebiet_manager.get_variable", return_value="")
|
||||
def test_get_verfahrensgebiet_layer_id_returns_none_for_empty_value(self, _mock_get_variable):
|
||||
"""Prüft, dass get_verfahrensgebiet_layer_id() bei leerer Projektvariable None statt eines leeren Strings liefert."""
|
||||
result = verfahrensgebiet_manager.get_verfahrensgebiet_layer_id()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("sn_basis.functions.verfahrensgebiet_manager.set_variable")
|
||||
def test_set_verfahrensgebiet_layer_id_writes_project_variable(self, mock_set_variable):
|
||||
"""Prüft, dass set_verfahrensgebiet_layer_id() die Layer-ID als Projektvariable 'verfahrensgebiet_layer' speichert."""
|
||||
verfahrensgebiet_manager.set_verfahrensgebiet_layer_id("layer_abc")
|
||||
|
||||
mock_set_variable.assert_called_once_with("verfahrensgebiet_layer", "layer_abc", scope="project")
|
||||
@@ -43,6 +46,7 @@ class TestVerfahrensgebietManager(unittest.TestCase):
|
||||
mock_project_instance,
|
||||
mock_find_layer,
|
||||
):
|
||||
"""Prüft, dass ohne gefundenen Verfahrensgebiet-Layer keine Schreibaktion erfolgt, wenn die Variable bereits leer ist."""
|
||||
mock_find_layer.return_value = (None, None)
|
||||
mock_project_instance.return_value = object()
|
||||
mock_project_scope.return_value = _Scope("")
|
||||
@@ -55,6 +59,7 @@ class TestVerfahrensgebietManager(unittest.TestCase):
|
||||
@patch("sn_basis.functions.verfahrensgebiet_manager._find_verfahrensgebiet_layer")
|
||||
@patch("sn_basis.functions.verfahrensgebiet_manager.set_variable")
|
||||
def test_update_with_layer_writes_variable(self, mock_set_variable, mock_find_layer):
|
||||
"""Prüft, dass ein gefundener Verfahrensgebiet-Layer seine ID als Projektvariable speichert."""
|
||||
mock_layer = object()
|
||||
mock_find_layer.return_value = (mock_layer, "layer_99")
|
||||
|
||||
|
||||
+11
-11
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
sn_basis/ui/tabs/print_tab.py - Zentraler Druck-Tab fuer sn-Plugins.
|
||||
sn_basis/ui/tabs/print_tab.py - Zentraler Druck-Tab für sn-Plugins.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -41,7 +41,7 @@ from sn_basis.modules.print_logic import (
|
||||
|
||||
|
||||
KARTENNAME_VAR = "tab_b_kartenname"
|
||||
KARTENNAME_PLACEHOLDER = "Kartenname waehlen"
|
||||
KARTENNAME_PLACEHOLDER = "Kartenname wählen"
|
||||
KARTENNAME_38 = "§38"
|
||||
KARTENNAME_41 = "§41"
|
||||
KARTENNAME_BAUFREIGABE = "Anlage zur Baufreigabe"
|
||||
@@ -59,7 +59,7 @@ DRUCKBEREICH_OPTIONEN = [
|
||||
|
||||
|
||||
class PrintTab(QWidget):
|
||||
"""UI-Klasse fuer den zentralen Druck-Tab."""
|
||||
"""UI-Klasse für den zentralen Druck-Tab."""
|
||||
|
||||
tab_title = "Druck"
|
||||
|
||||
@@ -130,7 +130,7 @@ class PrintTab(QWidget):
|
||||
self._kartenname_combo = kartenname_combo
|
||||
main_layout.addWidget(kartenname_combo)
|
||||
|
||||
massstab_label = QLabel("Massstab")
|
||||
massstab_label = QLabel("Maßstab")
|
||||
massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
||||
main_layout.addWidget(massstab_label)
|
||||
|
||||
@@ -152,7 +152,7 @@ class PrintTab(QWidget):
|
||||
self._thema_combo = thema_combo
|
||||
main_layout.addWidget(thema_combo)
|
||||
|
||||
zielgroesse_label = QLabel("max. Blattgroesse")
|
||||
zielgroesse_label = QLabel("max. Blattgröße")
|
||||
zielgroesse_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
||||
main_layout.addWidget(zielgroesse_label)
|
||||
|
||||
@@ -610,7 +610,7 @@ class PrintTab(QWidget):
|
||||
return None
|
||||
|
||||
def _on_atlas_layer_changed(self, layer_or_name: Any) -> None:
|
||||
"""Speichert den ausgewaehlten Atlas-Layer als Layer-ID in Projektvariablen."""
|
||||
"""Speichert den ausgewählten Atlas-Layer als Layer-ID in Projektvariablen."""
|
||||
if layer_or_name is None or layer_or_name == "":
|
||||
set_variable("sn_abdeckungslayer", "", scope="project")
|
||||
return
|
||||
@@ -639,10 +639,10 @@ class PrintTab(QWidget):
|
||||
pass
|
||||
|
||||
def _on_auswahl_as_atlas_clicked(self) -> None:
|
||||
"""Handler fuer Button 'Auswahl als Atlas anlegen'.
|
||||
|
||||
"""Handler für Button 'Auswahl als Atlas anlegen'.
|
||||
|
||||
Erstellt einen neuen Layer 'Atlaslayer' aus der aktuellen Feature-Auswahl
|
||||
und traegt ihn ins Atlas-Dropdown ein.
|
||||
und trägt ihn ins Atlas-Dropdown ein.
|
||||
"""
|
||||
if not self.logic or not self.pruefmanager:
|
||||
return
|
||||
@@ -673,11 +673,11 @@ class PrintTab(QWidget):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback nur fuer Mock/klassische ComboBox
|
||||
# Fallback nur für Mock/klassische ComboBox
|
||||
self._populate_atlas_layer_combo()
|
||||
|
||||
def _populate_atlas_layer_combo(self) -> None:
|
||||
"""Fallback-Befuellung, falls kein echtes QgsMapLayerComboBox Verhalten verfuegbar ist."""
|
||||
"""Fallback-Befüllung, falls kein echtes QgsMapLayerComboBox Verhalten verfügbar ist."""
|
||||
if not self._atlas_layer_combo:
|
||||
return
|
||||
|
||||
|
||||
+42
-11
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
sn_basis/ui/tabs/settings_tab.py – Einstellungen-Tab für Projekteinformationen.
|
||||
sn_basis/ui/tabs/settings_tab.py – Einstellungen-Tab für Projektinformationen.
|
||||
Verwaltet benutzerspezifische und projektspezifische Einstellungen.
|
||||
"""
|
||||
|
||||
@@ -8,28 +8,33 @@ from sn_basis.functions.qt_wrapper import (
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
QVBoxLayout,
|
||||
QPushButton,
|
||||
)
|
||||
from sn_basis.functions.settings_logic import SettingsLogic
|
||||
from sn_basis.functions.landkreise import LANDKREISE
|
||||
|
||||
#: Feldschlüssel, die als Landkreis-Dropdown (statt Freitext) dargestellt werden.
|
||||
_LANDKREIS_DROPDOWN_FELDER = {"landkreis_user"}
|
||||
|
||||
|
||||
class SettingsTab(QWidget):
|
||||
"""
|
||||
Tab-Widget für die Verwaltung von Projekt- und Benutzereinstellungen.
|
||||
|
||||
|
||||
Attributes:
|
||||
tab_title: Anzeigetitel des Tabs ("Projekteigenschaften")
|
||||
logic: SettingsLogic-Instanz für persistente Einstellungen
|
||||
"""
|
||||
|
||||
|
||||
tab_title = "Projekteigenschaften"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Initialisiert den Settings-Tab mit Eingabefeldern.
|
||||
|
||||
|
||||
Args:
|
||||
parent: Übergeordnetes Widget (Standard: None)
|
||||
"""
|
||||
@@ -64,7 +69,7 @@ class SettingsTab(QWidget):
|
||||
self.user_inputs = {}
|
||||
|
||||
for row, (key, label) in enumerate(self.user_fields.items()):
|
||||
input_widget = QLineEdit()
|
||||
input_widget = self._build_input(key)
|
||||
self.user_inputs[key] = input_widget
|
||||
|
||||
user_layout.addWidget(QLabel(label), row, 0)
|
||||
@@ -80,7 +85,7 @@ class SettingsTab(QWidget):
|
||||
self.project_inputs = {}
|
||||
|
||||
for row, (key, label) in enumerate(self.project_fields.items()):
|
||||
input_widget = QLineEdit()
|
||||
input_widget = self._build_input(key)
|
||||
self.project_inputs[key] = input_widget
|
||||
|
||||
project_layout.addWidget(QLabel(label), row, 0)
|
||||
@@ -107,14 +112,35 @@ class SettingsTab(QWidget):
|
||||
# Daten laden
|
||||
self.load_data()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Eingabewidget je Feld erzeugen
|
||||
# ---------------------------------------------------------
|
||||
def _build_input(self, key: str):
|
||||
if key in _LANDKREIS_DROPDOWN_FELDER:
|
||||
combo = QComboBox()
|
||||
combo.addItem("", None) # Platzhalter: keine Vorbelegung bei leerem Projekt
|
||||
for kennzahl, name in LANDKREISE:
|
||||
combo.addItem(name, kennzahl)
|
||||
return combo
|
||||
return QLineEdit()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Speichern
|
||||
# ---------------------------------------------------------
|
||||
def save_data(self):
|
||||
fields = {
|
||||
key: widget.text()
|
||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
|
||||
}
|
||||
fields = {}
|
||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
||||
if isinstance(widget, QComboBox):
|
||||
fields[key] = widget.currentText()
|
||||
else:
|
||||
fields[key] = widget.text()
|
||||
|
||||
# Landkreiskennzahl aus der Dropdown-Auswahl zusätzlich als eigene
|
||||
# Variable bereitstellen (siehe landkreise.py).
|
||||
landkreis_widget = self.user_inputs.get("landkreis_user")
|
||||
if isinstance(landkreis_widget, QComboBox):
|
||||
fields["landkreis_kennzahl"] = landkreis_widget.currentData() or ""
|
||||
|
||||
self.logic.save(fields)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
@@ -123,4 +149,9 @@ class SettingsTab(QWidget):
|
||||
def load_data(self):
|
||||
data = self.logic.load()
|
||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
||||
widget.setText(data.get(key, ""))
|
||||
if isinstance(widget, QComboBox):
|
||||
gespeicherter_name = data.get(key, "")
|
||||
index = widget.findText(gespeicherter_name)
|
||||
widget.setCurrentIndex(index if index >= 0 else 0)
|
||||
else:
|
||||
widget.setText(data.get(key, ""))
|
||||
|
||||
Reference in New Issue
Block a user