forked from AG_QGIS/Plugin_SN_Plan41
auf Wrapper umgestellt, tests ergänzt
This commit is contained in:
21
main.py
21
main.py
@@ -1,6 +1,8 @@
|
|||||||
|
# sn_plan41/main.py
|
||||||
|
|
||||||
from qgis.utils import plugins
|
from qgis.utils import plugins
|
||||||
from sn_basis.ui.dockmanager import DockManager
|
from sn_basis.ui.dockmanager import DockManager
|
||||||
from .ui.dockwidget import DockWidget
|
from sn_plan41.ui.dockwidget import DockWidget
|
||||||
|
|
||||||
|
|
||||||
class Plan41:
|
class Plan41:
|
||||||
@@ -15,14 +17,17 @@ class Plan41:
|
|||||||
|
|
||||||
def initGui(self):
|
def initGui(self):
|
||||||
basis = plugins.get("sn_basis")
|
basis = plugins.get("sn_basis")
|
||||||
if basis and basis.ui:
|
if not basis or not getattr(basis, "ui", None):
|
||||||
|
return
|
||||||
|
|
||||||
self.action = basis.ui.add_action(
|
self.action = basis.ui.add_action(
|
||||||
self.plugin_name,
|
self.plugin_name,
|
||||||
self.run,
|
self.run,
|
||||||
tooltip=f"Öffnet {self.plugin_name}",
|
tooltip=f"Öffnet {self.plugin_name}",
|
||||||
priority=20
|
priority=20,
|
||||||
)
|
)
|
||||||
basis.ui.finalize_menu_and_toolbar()
|
basis.ui.finalize_menu_and_toolbar()
|
||||||
|
print("Plan41/sn_Basis:initGui called")
|
||||||
|
|
||||||
def unload(self):
|
def unload(self):
|
||||||
if self.dockwidget:
|
if self.dockwidget:
|
||||||
@@ -32,13 +37,15 @@ class Plan41:
|
|||||||
|
|
||||||
if self.action:
|
if self.action:
|
||||||
basis = plugins.get("sn_basis")
|
basis = plugins.get("sn_basis")
|
||||||
if basis and basis.ui:
|
if basis and getattr(basis, "ui", None):
|
||||||
# Action aus Menü und Toolbar entfernen
|
|
||||||
basis.ui.remove_action(self.action)
|
basis.ui.remove_action(self.action)
|
||||||
self.action = None
|
self.action = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.dockwidget = DockWidget(self.iface.mainWindow(), subtitle=self.plugin_name)
|
self.dockwidget = DockWidget(
|
||||||
|
self.iface.mainWindow(),
|
||||||
|
subtitle=self.plugin_name,
|
||||||
|
)
|
||||||
self.dockwidget.setObjectName(self.dock_name)
|
self.dockwidget.setObjectName(self.dock_name)
|
||||||
|
|
||||||
# Action-Referenz im Dock speichern
|
# Action-Referenz im Dock speichern
|
||||||
@@ -48,5 +55,5 @@ class Plan41:
|
|||||||
|
|
||||||
# Toolbar-Button als aktiv markieren
|
# Toolbar-Button als aktiv markieren
|
||||||
basis = plugins.get("sn_basis")
|
basis = plugins.get("sn_basis")
|
||||||
if basis and basis.ui:
|
if basis and getattr(basis, "ui", None):
|
||||||
basis.ui.set_active_plugin(self.action)
|
basis.ui.set_active_plugin(self.action)
|
||||||
|
|||||||
3
pyrightconfig.json
Normal file
3
pyrightconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extraPaths": ["."]
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Plugin-Hauptverzeichnis ermitteln
|
|
||||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
# Plugin-Ordner in den Python-Pfad aufnehmen
|
|
||||||
sys.path.insert(0, BASE_DIR)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
# Testverzeichnis
|
|
||||||
test_dir = os.path.join(BASE_DIR, "tests")
|
|
||||||
|
|
||||||
# Test-Suite automatisch finden
|
|
||||||
suite = unittest.defaultTestLoader.discover(test_dir)
|
|
||||||
|
|
||||||
# Runner starten
|
|
||||||
runner = unittest.TextTestRunner(verbosity=2)
|
|
||||||
result = runner.run(suite)
|
|
||||||
|
|
||||||
# Exit-Code setzen (für CI oder Skripte nützlich)
|
|
||||||
sys.exit(not result.wasSuccessful())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run()
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Plugin-Ordner in den Python-Pfad aufnehmen
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
||||||
|
|
||||||
from qgis.PyQt.QtWidgets import QApplication
|
|
||||||
from qgis.core import QgsProject, QgsVectorLayer
|
|
||||||
|
|
||||||
from sn_plan41.ui.tabs import tab_a
|
|
||||||
|
|
||||||
|
|
||||||
class TestTabA(unittest.TestCase):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
"""Qt-Anwendung initialisieren."""
|
|
||||||
cls.app = QApplication([])
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Vor jedem Test: Projektvariablen löschen und TabA neu erzeugen."""
|
|
||||||
self.project = QgsProject.instance()
|
|
||||||
self.project.setCustomVariables({})
|
|
||||||
|
|
||||||
# TabA erzeugen
|
|
||||||
self.tab = TabA()
|
|
||||||
|
|
||||||
# Temporäre Testdateien
|
|
||||||
self.tmp_dir = tempfile.gettempdir()
|
|
||||||
self.test_db = os.path.join(self.tmp_dir, "test_db.gpkg")
|
|
||||||
self.test_link = os.path.join(self.tmp_dir, "test_link.xlsx")
|
|
||||||
|
|
||||||
# Dummy-Dateien anlegen
|
|
||||||
with open(self.test_db, "w") as f:
|
|
||||||
f.write("")
|
|
||||||
with open(self.test_link, "w") as f:
|
|
||||||
f.write("")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Verfahrens-DB speichern & wiederherstellen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def test_save_and_restore_verfahrens_db(self):
|
|
||||||
self.tab.on_file_changed(self.test_db)
|
|
||||||
|
|
||||||
vars = self.project.customVariables()
|
|
||||||
self.assertEqual(vars.get("sn_verfahrens_db"), self.test_db)
|
|
||||||
|
|
||||||
tab2 = TabA()
|
|
||||||
self.assertEqual(tab2.verfahrens_db, self.test_db)
|
|
||||||
self.assertEqual(tab2.file_widget.filePath(), self.test_db)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Verfahrens-DB löschen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def test_delete_verfahrens_db(self):
|
|
||||||
self.tab.on_file_changed(self.test_db)
|
|
||||||
self.tab.on_file_changed("")
|
|
||||||
|
|
||||||
vars = self.project.customVariables()
|
|
||||||
self.assertNotIn("sn_verfahrens_db", vars)
|
|
||||||
self.assertIsNone(self.tab.verfahrens_db)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Linkliste speichern & löschen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def test_save_and_delete_linkliste(self):
|
|
||||||
self.tab.on_linkliste_changed(self.test_link)
|
|
||||||
|
|
||||||
vars = self.project.customVariables()
|
|
||||||
self.assertEqual(vars.get("sn_linkliste"), self.test_link)
|
|
||||||
|
|
||||||
self.tab.on_linkliste_changed("")
|
|
||||||
|
|
||||||
vars = self.project.customVariables()
|
|
||||||
self.assertNotIn("sn_linkliste", vars)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Layer-Vorauswahl
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def test_preselect_verfahrensgebiet_layer(self):
|
|
||||||
vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory")
|
|
||||||
other_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "AndereDaten", "memory")
|
|
||||||
|
|
||||||
self.project.addMapLayer(other_layer)
|
|
||||||
self.project.addMapLayer(vg_layer)
|
|
||||||
|
|
||||||
tab2 = TabA()
|
|
||||||
|
|
||||||
selected = tab2.layer_combo.currentLayer()
|
|
||||||
self.assertIsNotNone(selected)
|
|
||||||
self.assertEqual(selected.name(), "Verfahrensgebiet")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Gespeicherter Layer wird wiederhergestellt
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def test_restore_saved_layer(self):
|
|
||||||
vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory")
|
|
||||||
self.project.addMapLayer(vg_layer)
|
|
||||||
|
|
||||||
vars = {"sn_verfahrensgebiet_layer": vg_layer.id()}
|
|
||||||
self.project.setCustomVariables(vars)
|
|
||||||
|
|
||||||
tab2 = TabA()
|
|
||||||
|
|
||||||
selected = tab2.layer_combo.currentLayer()
|
|
||||||
self.assertEqual(selected.id(), vg_layer.id())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
148
tests/run_tests.py
Normal file
148
tests/run_tests.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
sn_plan41/test/run_tests.py
|
||||||
|
|
||||||
|
Zentraler Test-Runner für sn_plan41.
|
||||||
|
Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import datetime
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Plugin-Roots bestimmen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
THIS_FILE = Path(__file__).resolve()
|
||||||
|
|
||||||
|
# .../plugins/sn_plan41
|
||||||
|
SN_PLAN41_ROOT = THIS_FILE.parents[1]
|
||||||
|
# .../plugins/sn_basis
|
||||||
|
SN_BASIS_ROOT = SN_PLAN41_ROOT.parent / "sn_basis"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# sys.path Bootstrap
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
for path in (SN_PLAN41_ROOT, SN_BASIS_ROOT):
|
||||||
|
path_str = str(path)
|
||||||
|
if path_str not in sys.path:
|
||||||
|
sys.path.insert(0, path_str)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Farben
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
RED = "\033[91m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
MAGENTA = "\033[95m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
GLOBAL_TEST_COUNTER = 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Farbige TestResult-Klasse
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
class ColoredTestResult(unittest.TextTestResult):
|
||||||
|
|
||||||
|
_last_test_class = None
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
global GLOBAL_TEST_COUNTER
|
||||||
|
GLOBAL_TEST_COUNTER += 1
|
||||||
|
self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n")
|
||||||
|
super().startTest(test)
|
||||||
|
|
||||||
|
def startTestClass(self, test):
|
||||||
|
cls = test.__class__
|
||||||
|
file = inspect.getfile(cls)
|
||||||
|
filename = os.path.basename(file)
|
||||||
|
|
||||||
|
self.stream.write(
|
||||||
|
f"\n{MAGENTA}{'=' * 70}\n"
|
||||||
|
f"Starte Testklasse: {filename} → {cls.__name__}\n"
|
||||||
|
f"{'=' * 70}{RESET}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def addError(self, test, err):
|
||||||
|
super().addError(test, err)
|
||||||
|
self.stream.write(f"{RED}ERROR{RESET}\n")
|
||||||
|
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
super().addFailure(test, err)
|
||||||
|
self.stream.write(f"{RED}FAILURE{RESET}\n")
|
||||||
|
|
||||||
|
def addSkip(self, test, reason):
|
||||||
|
super().addSkip(test, reason)
|
||||||
|
self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n")
|
||||||
|
|
||||||
|
def addSuccess(self, test):
|
||||||
|
super().addSuccess(test)
|
||||||
|
self.stream.write(f"{GREEN}OK{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Farbiger TestRunner
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
class ColoredTestRunner(unittest.TextTestRunner):
|
||||||
|
|
||||||
|
def _makeResult(self):
|
||||||
|
result = ColoredTestResult(
|
||||||
|
self.stream,
|
||||||
|
self.descriptions,
|
||||||
|
self.verbosity,
|
||||||
|
)
|
||||||
|
|
||||||
|
original_start_test = result.startTest
|
||||||
|
|
||||||
|
def patched_start_test(test):
|
||||||
|
if result._last_test_class != test.__class__:
|
||||||
|
result.startTestClass(test)
|
||||||
|
result._last_test_class = test.__class__
|
||||||
|
original_start_test(test)
|
||||||
|
|
||||||
|
result.startTest = patched_start_test
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Testlauf starten
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(
|
||||||
|
f"{CYAN}Testlauf gestartet am: "
|
||||||
|
f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}"
|
||||||
|
)
|
||||||
|
print("=" * 70 + "\n")
|
||||||
|
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
|
||||||
|
TEST_ROOT = SN_PLAN41_ROOT / "tests"
|
||||||
|
|
||||||
|
suite = loader.discover(
|
||||||
|
start_dir=str(TEST_ROOT),
|
||||||
|
pattern="test_*.py",
|
||||||
|
top_level_dir=str(SN_PLAN41_ROOT.parent),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
runner = ColoredTestRunner(verbosity=2)
|
||||||
|
result = runner.run(suite)
|
||||||
|
|
||||||
|
return 0 if result.wasSuccessful() else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
153
tests/test_tab_a_logic.py
Normal file
153
tests/test_tab_a_logic.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore
|
||||||
|
from sn_basis.functions.variable_wrapper import get_variable # type: ignore
|
||||||
|
from sn_basis.functions.sys_wrapper import file_exists # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TestTabALogic(unittest.TestCase):
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 1. Verfahrens-DB setzen und laden
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_verfahrens_db_set_and_load(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
db_path = Path(tmp) / "test.gpkg"
|
||||||
|
db_path.write_text("")
|
||||||
|
|
||||||
|
logic.set_verfahrens_db(str(db_path))
|
||||||
|
|
||||||
|
stored = get_variable("verfahrens_db", scope="project")
|
||||||
|
self.assertEqual(stored, str(db_path))
|
||||||
|
|
||||||
|
loaded = logic.load_verfahrens_db()
|
||||||
|
self.assertEqual(loaded, str(db_path))
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2. Verfahrens-DB löschen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_verfahrens_db_clear(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
logic.set_verfahrens_db(None)
|
||||||
|
|
||||||
|
stored = get_variable("verfahrens_db", scope="project")
|
||||||
|
self.assertEqual(stored, "")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 3. Neue Verfahrens-DB anlegen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_create_new_verfahrens_db(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
db_path = Path(tmp) / "neu.gpkg"
|
||||||
|
|
||||||
|
result = logic.create_new_verfahrens_db(str(db_path))
|
||||||
|
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.assertTrue(file_exists(db_path))
|
||||||
|
|
||||||
|
stored = get_variable("verfahrens_db", scope="project")
|
||||||
|
self.assertEqual(stored, str(db_path))
|
||||||
|
|
||||||
|
def test_create_new_verfahrens_db_with_none_path(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
result = logic.create_new_verfahrens_db(None)
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 4. Linkliste setzen und laden
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_linkliste_set_and_load(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
link_path = Path(tmp) / "links.xlsx"
|
||||||
|
link_path.write_text("dummy")
|
||||||
|
|
||||||
|
logic.set_linkliste(str(link_path))
|
||||||
|
|
||||||
|
stored = get_variable("linkliste", scope="project")
|
||||||
|
self.assertEqual(stored, str(link_path))
|
||||||
|
|
||||||
|
loaded = logic.load_linkliste()
|
||||||
|
self.assertEqual(loaded, str(link_path))
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 5. Linkliste löschen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_linkliste_clear(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
logic.set_linkliste(None)
|
||||||
|
|
||||||
|
stored = get_variable("linkliste", scope="project")
|
||||||
|
self.assertEqual(stored, "")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 6. Layer-ID speichern
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_verfahrensgebiet_layer_id_storage(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
class MockLayer:
|
||||||
|
def id(self):
|
||||||
|
return "layer-123"
|
||||||
|
|
||||||
|
logic.save_verfahrensgebiet_layer(MockLayer())
|
||||||
|
|
||||||
|
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
||||||
|
self.assertEqual(stored, "layer-123")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 7. Ungültiger Layer wird ignoriert
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_invalid_layer_is_rejected(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
class InvalidLayer:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logic.save_verfahrensgebiet_layer(InvalidLayer())
|
||||||
|
|
||||||
|
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
||||||
|
self.assertEqual(stored, "")
|
||||||
|
#-----------------------------------------------------
|
||||||
|
# 8. Layer-ID wirft Exception
|
||||||
|
#----------------------------------------------------
|
||||||
|
def test_layer_id_raises_exception(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
class BadLayer:
|
||||||
|
def id(self):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
logic.save_verfahrensgebiet_layer(BadLayer())
|
||||||
|
|
||||||
|
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
||||||
|
self.assertEqual(stored, "")
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 11. Layer ID wird leer zurückgegeben
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_layer_id_returns_empty(self):
|
||||||
|
logic = TabALogic()
|
||||||
|
|
||||||
|
class EmptyLayer:
|
||||||
|
def id(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
logic.save_verfahrensgebiet_layer(EmptyLayer())
|
||||||
|
|
||||||
|
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
||||||
|
self.assertEqual(stored, "")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
57
tests/test_tab_a_ui.py
Normal file
57
tests/test_tab_a_ui.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
Smoke-Tests für TabA UI (sn_plan41/ui/tab_a_ui.py)
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
- UI kann erstellt werden
|
||||||
|
- Callbacks crashen nicht
|
||||||
|
- Keine Qt-Verhaltensprüfung
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sn_plan41.ui.tab_a_ui import TabA #type:ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TestTabAUI(unittest.TestCase):
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 1. UI kann erstellt werden
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_tab_a_ui_can_be_created(self):
|
||||||
|
tab = TabA(parent=None,build_ui=False)
|
||||||
|
|
||||||
|
self.assertIsNotNone(tab)
|
||||||
|
self.assertEqual(tab.tab_title, "Daten")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2. Toggle-Callbacks crashen nicht
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_tab_a_toggle_callbacks_do_not_crash(self):
|
||||||
|
tab = TabA(parent=None,build_ui=False)
|
||||||
|
|
||||||
|
tab._toggle_group(True)
|
||||||
|
tab._toggle_group(False)
|
||||||
|
|
||||||
|
tab._toggle_optional(True)
|
||||||
|
tab._toggle_optional(False)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 3. Datei-Callbacks akzeptieren leere Eingaben
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_tab_a_file_callbacks_accept_empty_input(self):
|
||||||
|
tab = TabA(parent=None,build_ui=False)
|
||||||
|
|
||||||
|
tab._on_verfahrens_db_changed("")
|
||||||
|
tab._on_linkliste_changed("")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 4. Layer-Callback akzeptiert None
|
||||||
|
# -----------------------------------------------------
|
||||||
|
def test_tab_a_layer_callback_accepts_none(self):
|
||||||
|
tab = TabA(parent=None,build_ui=False)
|
||||||
|
|
||||||
|
tab._on_layer_changed(None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||||
from sn_plan41.ui.tabs.tab_a import TabA
|
from sn_plan41.ui.tab_a_ui import TabA
|
||||||
from sn_plan41.ui.tabs.tab_b import TabB
|
#from sn_plan41.ui.tabs.tab_b import TabB
|
||||||
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
||||||
|
|
||||||
class DockWidget(BaseDockWidget):
|
class DockWidget(BaseDockWidget):
|
||||||
tabs = [TabA, TabB, SettingsTab]
|
tabs = [TabA, SettingsTab]
|
||||||
|
|||||||
132
ui/tab_a_logic.py
Normal file
132
ui/tab_a_logic.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
sn_plan41/ui/tab_a_logic.py – Fachlogik für Tab A (Daten)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sn_basis.functions.variable_wrapper import (#type: ignore
|
||||||
|
get_variable,
|
||||||
|
set_variable,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.sys_wrapper import (#type:ignore
|
||||||
|
file_exists,
|
||||||
|
write_text,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.ly_existence_wrapper import layer_exists#type:ignore
|
||||||
|
from sn_basis.functions.ly_metadata_wrapper import get_layer_type#type:ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TabALogic:
|
||||||
|
"""
|
||||||
|
Kapselt die komplette Logik von Tab A:
|
||||||
|
- Verfahrens-Datenbank
|
||||||
|
- optionale Linkliste
|
||||||
|
- Verfahrensgebiet-Layer
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Verfahrens-Datenbank
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
def load_verfahrens_db(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Lädt die gespeicherte Verfahrens-Datenbank.
|
||||||
|
"""
|
||||||
|
path = get_variable("verfahrens_db", scope="project")
|
||||||
|
if path and file_exists(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_verfahrens_db(self, path: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Speichert oder löscht die Verfahrens-Datenbank.
|
||||||
|
"""
|
||||||
|
if path:
|
||||||
|
set_variable("verfahrens_db", path, scope="project")
|
||||||
|
else:
|
||||||
|
set_variable("verfahrens_db", "", scope="project")
|
||||||
|
|
||||||
|
def create_new_verfahrens_db(self, path: str) -> bool:
|
||||||
|
"""
|
||||||
|
Legt eine neue leere GPKG-Datei an.
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
write_text(path, "")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.set_verfahrens_db(path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Lokale Linkliste
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
def load_linkliste(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Lädt die gespeicherte lokale Linkliste.
|
||||||
|
"""
|
||||||
|
path = get_variable("linkliste", scope="project")
|
||||||
|
if path and file_exists(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_linkliste(self, path: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Speichert oder löscht die lokale Linkliste.
|
||||||
|
"""
|
||||||
|
if path:
|
||||||
|
set_variable("linkliste", path, scope="project")
|
||||||
|
else:
|
||||||
|
set_variable("linkliste", "", scope="project")
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Verfahrensgebiet-Layer
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
def save_verfahrensgebiet_layer(self, layer) -> None:
|
||||||
|
"""
|
||||||
|
Speichert die ID des Verfahrensgebiet-Layers.
|
||||||
|
Ungültige Layer werden ignoriert.
|
||||||
|
"""
|
||||||
|
if layer is None:
|
||||||
|
set_variable("verfahrensgebiet_layer", "", scope="project")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not hasattr(layer, "id") or not callable(layer.id):
|
||||||
|
set_variable("verfahrensgebiet_layer", "", scope="project")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer_id = layer.id()
|
||||||
|
except Exception:
|
||||||
|
set_variable("verfahrensgebiet_layer", "", scope="project")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not layer_id:
|
||||||
|
set_variable("verfahrensgebiet_layer", "", scope="project")
|
||||||
|
return
|
||||||
|
|
||||||
|
set_variable("verfahrensgebiet_layer", layer_id, scope="project")
|
||||||
|
|
||||||
|
|
||||||
|
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Lädt die gespeicherte Layer-ID.
|
||||||
|
"""
|
||||||
|
value = get_variable("verfahrensgebiet_layer", scope="project")
|
||||||
|
return value or None
|
||||||
|
|
||||||
|
def is_valid_verfahrensgebiet_layer(self, layer) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob ein Layer als Verfahrensgebiet geeignet ist.
|
||||||
|
"""
|
||||||
|
if not layer_exists(layer):
|
||||||
|
return False
|
||||||
|
|
||||||
|
layer_type = get_layer_type(layer)
|
||||||
|
return layer_type == "vector"
|
||||||
308
ui/tab_a_ui.py
Normal file
308
ui/tab_a_ui.py
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"""
|
||||||
|
sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sn_basis.functions.qt_wrapper import ( # type: ignore
|
||||||
|
QWidget,
|
||||||
|
QVBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QToolButton,
|
||||||
|
QFileDialog,
|
||||||
|
QMessageBox,
|
||||||
|
QTabWidget,
|
||||||
|
ToolButtonTextBesideIcon,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowRight,
|
||||||
|
SizePolicyPreferred,
|
||||||
|
SizePolicyMaximum,
|
||||||
|
|
||||||
|
)
|
||||||
|
from sn_basis.functions.qgisui_wrapper import ( # type: ignore
|
||||||
|
QgsFileWidget,
|
||||||
|
QgsMapLayerComboBox,
|
||||||
|
add_dock_widget,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import ( # type: ignore
|
||||||
|
QgsProject,
|
||||||
|
QgsMapLayerProxyModel,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.message_wrapper import ( # type: ignore
|
||||||
|
info,
|
||||||
|
warning,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.dialog_wrapper import ask_yes_no # type: ignore
|
||||||
|
from sn_basis.functions.sys_wrapper import file_exists # type: ignore
|
||||||
|
|
||||||
|
from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TabA(QWidget):
|
||||||
|
"""
|
||||||
|
UI-Klasse für Tab A (Daten).
|
||||||
|
Enthält ausschließlich UI-Code und delegiert Logik an TabALogic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, parent=None, build_ui: bool=True):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent=parent
|
||||||
|
self.tab_title="Daten"
|
||||||
|
|
||||||
|
self.logic = TabALogic()
|
||||||
|
|
||||||
|
self.verfahrens_db: Optional[str] = None
|
||||||
|
self.lokale_linkliste: Optional[str] = None
|
||||||
|
|
||||||
|
if build_ui:
|
||||||
|
self._build_ui()
|
||||||
|
self._restore_state()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# UI-Aufbau
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
main_layout.setSpacing(4)
|
||||||
|
main_layout.setContentsMargins(4, 4, 4, 4)
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Verfahrens-Datenbank
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
self.group_button = QToolButton()
|
||||||
|
self.group_button.setText("Verfahrens-Datenbank")
|
||||||
|
self.group_button.setCheckable(True)
|
||||||
|
self.group_button.setChecked(True)
|
||||||
|
self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon)
|
||||||
|
self.group_button.setArrowType(ArrowDown)
|
||||||
|
|
||||||
|
self.group_button.setStyleSheet("font-weight: bold;")
|
||||||
|
self.group_button.toggled.connect(self._toggle_group)
|
||||||
|
main_layout.addWidget(self.group_button)
|
||||||
|
|
||||||
|
self.group_content = QWidget()
|
||||||
|
self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
|
||||||
|
|
||||||
|
|
||||||
|
group_layout = QVBoxLayout()
|
||||||
|
group_layout.setSpacing(2)
|
||||||
|
group_layout.setContentsMargins(10, 4, 4, 4)
|
||||||
|
|
||||||
|
group_layout.addWidget(QLabel("bestehende Datei auswählen"))
|
||||||
|
|
||||||
|
self.file_widget = QgsFileWidget()
|
||||||
|
self.file_widget.setStorageMode(QgsFileWidget.GetFile)
|
||||||
|
self.file_widget.setFilter("Geopackage (*.gpkg)")
|
||||||
|
self.file_widget.fileChanged.connect(self._on_verfahrens_db_changed)
|
||||||
|
group_layout.addWidget(self.file_widget)
|
||||||
|
|
||||||
|
group_layout.addWidget(QLabel("-oder-"))
|
||||||
|
|
||||||
|
self.btn_new = QPushButton("Neue Verfahrens-DB anlegen")
|
||||||
|
self.btn_new.clicked.connect(self._create_new_gpkg)
|
||||||
|
group_layout.addWidget(self.btn_new)
|
||||||
|
|
||||||
|
self.group_content.setLayout(group_layout)
|
||||||
|
main_layout.addWidget(self.group_content)
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Optionale Linkliste
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
self.optional_button = QToolButton()
|
||||||
|
self.optional_button.setText("Optional: Lokale Linkliste")
|
||||||
|
self.optional_button.setCheckable(True)
|
||||||
|
self.optional_button.setChecked(False)
|
||||||
|
self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon)
|
||||||
|
self.optional_button.setArrowType(ArrowRight)
|
||||||
|
|
||||||
|
self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
||||||
|
self.optional_button.toggled.connect(self._toggle_optional)
|
||||||
|
main_layout.addWidget(self.optional_button)
|
||||||
|
|
||||||
|
self.optional_content = QWidget()
|
||||||
|
self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
|
||||||
|
|
||||||
|
optional_layout = QVBoxLayout()
|
||||||
|
optional_layout.setSpacing(2)
|
||||||
|
optional_layout.setContentsMargins(10, 4, 4, 20)
|
||||||
|
|
||||||
|
optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)"))
|
||||||
|
|
||||||
|
self.linkliste_widget = QgsFileWidget()
|
||||||
|
self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile)
|
||||||
|
self.linkliste_widget.setFilter("Excelliste (*.xlsx)")
|
||||||
|
self.linkliste_widget.fileChanged.connect(self._on_linkliste_changed)
|
||||||
|
optional_layout.addWidget(self.linkliste_widget)
|
||||||
|
|
||||||
|
self.optional_content.setLayout(optional_layout)
|
||||||
|
self.optional_content.setVisible(False)
|
||||||
|
main_layout.addWidget(self.optional_content)
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Layer-Auswahl
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
|
||||||
|
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
||||||
|
main_layout.addWidget(layer_label)
|
||||||
|
|
||||||
|
self.layer_combo = QgsMapLayerComboBox()
|
||||||
|
self.layer_combo.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
|
||||||
|
self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
|
||||||
|
self.layer_combo.layerChanged.connect(self._on_layer_changed)
|
||||||
|
main_layout.addWidget(self.layer_combo)
|
||||||
|
|
||||||
|
main_layout.addStretch(1)
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# State Restore
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _restore_state(self) -> None:
|
||||||
|
db = self.logic.load_verfahrens_db()
|
||||||
|
if db:
|
||||||
|
self.verfahrens_db = db
|
||||||
|
self.file_widget.setFilePath(db)
|
||||||
|
self._update_group_color()
|
||||||
|
|
||||||
|
link = self.logic.load_linkliste()
|
||||||
|
if link:
|
||||||
|
self.lokale_linkliste = link
|
||||||
|
self.linkliste_widget.setFilePath(link)
|
||||||
|
|
||||||
|
layer_id = self.logic.load_verfahrensgebiet_layer_id()
|
||||||
|
if layer_id:
|
||||||
|
layer = QgsProject.instance().mapLayer(layer_id)
|
||||||
|
if layer:
|
||||||
|
self.layer_combo.setLayer(layer)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# UI-Callbacks
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _toggle_group(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Klappt den Gruppenbereich ein oder aus.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "group_button"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.group_button.setArrowType(
|
||||||
|
ArrowDown if checked else ArrowRight
|
||||||
|
)
|
||||||
|
|
||||||
|
self.group_content.setVisible(checked)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_optional(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Klappt den optionalen Bereich ein oder aus.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "optional_button"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.group_button.setArrowType(
|
||||||
|
ArrowDown if checked else ArrowRight
|
||||||
|
)
|
||||||
|
|
||||||
|
self.optional_content.setVisible(checked)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_verfahrens_db_changed(self, path: str) -> None:
|
||||||
|
if not path:
|
||||||
|
self.verfahrens_db = None
|
||||||
|
self.logic.set_verfahrens_db(None)
|
||||||
|
self._update_group_color()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not path.lower().endswith(".gpkg"):
|
||||||
|
path += ".gpkg"
|
||||||
|
self.file_widget.setFilePath(path)
|
||||||
|
|
||||||
|
if not file_exists(path):
|
||||||
|
warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}")
|
||||||
|
self.file_widget.setFilePath("")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.verfahrens_db = path
|
||||||
|
self.logic.set_verfahrens_db(path)
|
||||||
|
self._update_group_color()
|
||||||
|
|
||||||
|
def _on_linkliste_changed(self, path: str) -> None:
|
||||||
|
if not path:
|
||||||
|
self.lokale_linkliste = None
|
||||||
|
self.logic.set_linkliste(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not path.lower().endswith(".xlsx"):
|
||||||
|
path += ".xlsx"
|
||||||
|
self.linkliste_widget.setFilePath(path)
|
||||||
|
|
||||||
|
if not file_exists(path):
|
||||||
|
warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}")
|
||||||
|
self.linkliste_widget.setFilePath("")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.lokale_linkliste = path
|
||||||
|
self.logic.set_linkliste(path)
|
||||||
|
|
||||||
|
def _on_layer_changed(self, layer) -> None:
|
||||||
|
self.logic.save_verfahrensgebiet_layer(layer)
|
||||||
|
|
||||||
|
def _create_new_gpkg(self) -> None:
|
||||||
|
file_path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self,
|
||||||
|
"Neue Verfahrens-Datenbank anlegen",
|
||||||
|
"",
|
||||||
|
"Geopackage (*.gpkg)",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not file_path.lower().endswith(".gpkg"):
|
||||||
|
file_path += ".gpkg"
|
||||||
|
|
||||||
|
if file_exists(file_path):
|
||||||
|
overwrite = ask_yes_no(
|
||||||
|
"Datei existiert bereits",
|
||||||
|
f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?",
|
||||||
|
default=False,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
if not overwrite:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.logic.create_new_verfahrens_db(file_path):
|
||||||
|
error("Fehler", "Die Datei konnte nicht angelegt werden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.verfahrens_db = file_path
|
||||||
|
self.file_widget.setFilePath(file_path)
|
||||||
|
self._update_group_color()
|
||||||
|
info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# UI-Helfer
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _update_group_color(self):
|
||||||
|
"""
|
||||||
|
Aktualisiert die Darstellung der Gruppenüberschrift.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "group_button"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.verfahrens_db:
|
||||||
|
self.group_button.setStyleSheet("font-weight: bold;")
|
||||||
|
else:
|
||||||
|
self.group_button.setStyleSheet("")
|
||||||
|
|
||||||
316
ui/tabs/tab_a.py
316
ui/tabs/tab_a.py
@@ -1,316 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from qgis.PyQt.QtWidgets import (
|
|
||||||
QWidget, QVBoxLayout, QLabel, QMessageBox, QPushButton,
|
|
||||||
QFileDialog, QToolButton, QSizePolicy
|
|
||||||
)
|
|
||||||
from qgis.PyQt.QtCore import Qt
|
|
||||||
from qgis.gui import QgsFileWidget, QgsMapLayerComboBox
|
|
||||||
from qgis.core import QgsProject, QgsMapLayerProxyModel
|
|
||||||
|
|
||||||
|
|
||||||
class TabA(QWidget):
|
|
||||||
tab_title = "Daten"
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Variablen initialisieren
|
|
||||||
self.verfahrens_db = None
|
|
||||||
self.lokale_linkliste = None
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Hauptlayout
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
main_layout = QVBoxLayout()
|
|
||||||
main_layout.setSpacing(4)
|
|
||||||
main_layout.setContentsMargins(4, 4, 4, 4)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# COLLAPSIBLE GRUPPE: Verfahrens-Datenbank
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
self.group_button = QToolButton()
|
|
||||||
self.group_button.setText("Verfahrens-Datenbank")
|
|
||||||
self.group_button.setCheckable(True)
|
|
||||||
self.group_button.setChecked(True)
|
|
||||||
self.group_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
||||||
self.group_button.setArrowType(Qt.DownArrow)
|
|
||||||
self.group_button.setStyleSheet("font-weight: bold;")
|
|
||||||
self.group_button.toggled.connect(self.toggle_group)
|
|
||||||
main_layout.addWidget(self.group_button, 0)
|
|
||||||
|
|
||||||
# Inhalt der Gruppe
|
|
||||||
self.group_content = QWidget()
|
|
||||||
self.group_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
||||||
|
|
||||||
group_layout = QVBoxLayout()
|
|
||||||
group_layout.setSpacing(2)
|
|
||||||
group_layout.setContentsMargins(10, 4, 4, 4)
|
|
||||||
|
|
||||||
# Hinweis
|
|
||||||
hinweis1 = QLabel("bestehende Datei auswählen")
|
|
||||||
group_layout.addWidget(hinweis1)
|
|
||||||
|
|
||||||
# Datei-Auswahl
|
|
||||||
self.file_widget = QgsFileWidget()
|
|
||||||
self.file_widget.setStorageMode(QgsFileWidget.GetFile)
|
|
||||||
self.file_widget.setFilter("Geopackage (*.gpkg)")
|
|
||||||
self.file_widget.fileChanged.connect(self.on_file_changed)
|
|
||||||
group_layout.addWidget(self.file_widget)
|
|
||||||
|
|
||||||
# Hinweis "-oder-"
|
|
||||||
hinweis2 = QLabel("-oder-")
|
|
||||||
group_layout.addWidget(hinweis2)
|
|
||||||
|
|
||||||
# Button: Neue Datei
|
|
||||||
self.btn_new = QPushButton("Neue Verfahrens-DB anlegen")
|
|
||||||
self.btn_new.clicked.connect(self.create_new_gpkg)
|
|
||||||
group_layout.addWidget(self.btn_new)
|
|
||||||
|
|
||||||
self.group_content.setLayout(group_layout)
|
|
||||||
main_layout.addWidget(self.group_content, 0)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# COLLAPSIBLE Optional-Bereich
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
self.optional_button = QToolButton()
|
|
||||||
self.optional_button.setText("Optional: Lokale Linkliste")
|
|
||||||
self.optional_button.setCheckable(True)
|
|
||||||
self.optional_button.setChecked(False)
|
|
||||||
self.optional_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
||||||
self.optional_button.setArrowType(Qt.RightArrow)
|
|
||||||
self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
self.optional_button.toggled.connect(self.toggle_optional)
|
|
||||||
main_layout.addWidget(self.optional_button, 0)
|
|
||||||
|
|
||||||
# Inhalt optional
|
|
||||||
self.optional_content = QWidget()
|
|
||||||
self.optional_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
||||||
|
|
||||||
optional_layout = QVBoxLayout()
|
|
||||||
optional_layout.setSpacing(2)
|
|
||||||
optional_layout.setContentsMargins(10, 4, 4, 20)
|
|
||||||
|
|
||||||
# Hinweistext
|
|
||||||
optional_hint = QLabel("(frei lassen für globale Linkliste)")
|
|
||||||
optional_layout.addWidget(optional_hint)
|
|
||||||
|
|
||||||
# Datei-Auswahl für Linkliste
|
|
||||||
self.linkliste_widget = QgsFileWidget()
|
|
||||||
self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile)
|
|
||||||
self.linkliste_widget.setFilter("Excelliste (*.xlsx)")
|
|
||||||
self.linkliste_widget.fileChanged.connect(self.on_linkliste_changed)
|
|
||||||
optional_layout.addWidget(self.linkliste_widget)
|
|
||||||
main_layout.addWidget(self.optional_content, 0)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Layer-Auswahlfeld
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
|
|
||||||
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
main_layout.addWidget(layer_label)
|
|
||||||
|
|
||||||
self.layer_combo = QgsMapLayerComboBox()
|
|
||||||
self.layer_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
||||||
|
|
||||||
# ✅ QGIS 3.22–3.46 kompatibel
|
|
||||||
self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
|
|
||||||
|
|
||||||
# Layerwechsel speichern
|
|
||||||
self.layer_combo.layerChanged.connect(self.on_layer_changed)
|
|
||||||
|
|
||||||
main_layout.addWidget(self.layer_combo)
|
|
||||||
|
|
||||||
self.optional_content.setLayout(optional_layout)
|
|
||||||
self.optional_content.setVisible(False)
|
|
||||||
|
|
||||||
|
|
||||||
# Spacer
|
|
||||||
main_layout.addStretch(1)
|
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
|
||||||
|
|
||||||
# ✅ gespeicherte Werte wiederherstellen (jetzt existieren die Widgets!)
|
|
||||||
self.restore_saved_values()
|
|
||||||
|
|
||||||
# ✅ Layer-Vorauswahl durchführen
|
|
||||||
self.preselect_verfahrensgebiet_layer()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Collapsible Gruppe ein-/ausblenden
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def toggle_group(self, checked):
|
|
||||||
self.group_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
|
|
||||||
self.group_content.setVisible(checked)
|
|
||||||
|
|
||||||
def toggle_optional(self, checked):
|
|
||||||
self.optional_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
|
|
||||||
self.optional_content.setVisible(checked)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Datei-Auswahl: Verfahrens-DB
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def on_file_changed(self, path: str):
|
|
||||||
if not path:
|
|
||||||
self.verfahrens_db = None
|
|
||||||
|
|
||||||
# ✅ Projektvariable löschen
|
|
||||||
vars = QgsProject.instance().customVariables()
|
|
||||||
if "sn_verfahrens_db" in vars:
|
|
||||||
del vars["sn_verfahrens_db"]
|
|
||||||
QgsProject.instance().setCustomVariables(vars)
|
|
||||||
|
|
||||||
self.update_group_button_color()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not path.lower().endswith(".gpkg"):
|
|
||||||
path += ".gpkg"
|
|
||||||
self.file_widget.setFilePath(path)
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
self.verfahrens_db = path
|
|
||||||
else:
|
|
||||||
self.verfahrens_db = None
|
|
||||||
QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}")
|
|
||||||
self.file_widget.setFilePath("")
|
|
||||||
|
|
||||||
# ✅ speichern
|
|
||||||
vars = QgsProject.instance().customVariables()
|
|
||||||
vars["sn_verfahrens_db"] = self.verfahrens_db
|
|
||||||
QgsProject.instance().setCustomVariables(vars)
|
|
||||||
|
|
||||||
self.update_group_button_color()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Datei-Auswahl: Lokale Linkliste
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def on_linkliste_changed(self, path: str):
|
|
||||||
if not path:
|
|
||||||
self.lokale_linkliste = None
|
|
||||||
|
|
||||||
vars = QgsProject.instance().customVariables()
|
|
||||||
if "sn_linkliste" in vars:
|
|
||||||
del vars["sn_linkliste"]
|
|
||||||
QgsProject.instance().setCustomVariables(vars)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
if not path.lower().endswith(".xlsx"):
|
|
||||||
path += ".xlsx"
|
|
||||||
self.linkliste_widget.setFilePath(path)
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
self.lokale_linkliste = path
|
|
||||||
else:
|
|
||||||
self.lokale_linkliste = None
|
|
||||||
QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}")
|
|
||||||
self.linkliste_widget.setFilePath("")
|
|
||||||
|
|
||||||
# ✅ speichern
|
|
||||||
vars = QgsProject.instance().customVariables()
|
|
||||||
vars["sn_linkliste"] = self.lokale_linkliste
|
|
||||||
QgsProject.instance().setCustomVariables(vars)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Layer-Auswahl speichern
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def on_layer_changed(self, layer):
|
|
||||||
if layer:
|
|
||||||
vars = QgsProject.instance().customVariables()
|
|
||||||
vars["sn_verfahrensgebiet_layer"] = layer.id()
|
|
||||||
QgsProject.instance().setCustomVariables(vars)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Button-Farbe aktualisieren
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def update_group_button_color(self):
|
|
||||||
if self.verfahrens_db:
|
|
||||||
self.group_button.setStyleSheet("font-weight: bold; background-color: #c4f7c4;")
|
|
||||||
else:
|
|
||||||
self.group_button.setStyleSheet("font-weight: bold;")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Vorauswahl des Layers "Verfahrensgebiet"
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def preselect_verfahrensgebiet_layer(self):
|
|
||||||
project = QgsProject.instance()
|
|
||||||
|
|
||||||
# ✅ zuerst gespeicherten Layer wiederherstellen
|
|
||||||
saved_layer_id = project.customVariables().get("sn_verfahrensgebiet_layer", None)
|
|
||||||
if saved_layer_id:
|
|
||||||
layer = project.mapLayer(saved_layer_id)
|
|
||||||
if layer:
|
|
||||||
self.layer_combo.setLayer(layer)
|
|
||||||
return
|
|
||||||
|
|
||||||
# ✅ sonst nach Namen suchen
|
|
||||||
for layer in project.mapLayers().values():
|
|
||||||
if "verfahrensgebiet" in layer.name().lower():
|
|
||||||
self.layer_combo.setLayer(layer)
|
|
||||||
return
|
|
||||||
|
|
||||||
# ✅ Fallback: erster Layer
|
|
||||||
if self.layer_combo.count() > 0:
|
|
||||||
self.layer_combo.setCurrentIndex(0)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Werte wiederherstellen
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def restore_saved_values(self):
|
|
||||||
project = QgsProject.instance()
|
|
||||||
vars = project.customVariables()
|
|
||||||
|
|
||||||
# ✅ Verfahrens-DB
|
|
||||||
saved_db = vars.get("sn_verfahrens_db", None)
|
|
||||||
if saved_db and os.path.exists(saved_db):
|
|
||||||
self.verfahrens_db = saved_db
|
|
||||||
self.file_widget.setFilePath(saved_db)
|
|
||||||
self.update_group_button_color()
|
|
||||||
|
|
||||||
# ✅ Linkliste
|
|
||||||
saved_link = vars.get("sn_linkliste", None)
|
|
||||||
if saved_link and os.path.exists(saved_link):
|
|
||||||
self.lokale_linkliste = saved_link
|
|
||||||
self.linkliste_widget.setFilePath(saved_link)
|
|
||||||
def create_new_gpkg(self):
|
|
||||||
"""Öffnet einen Save-Dialog und legt eine neue GPKG-Datei an."""
|
|
||||||
file_path, _ = QFileDialog.getSaveFileName(
|
|
||||||
self,
|
|
||||||
"Neue Verfahrens-Datenbank anlegen",
|
|
||||||
"",
|
|
||||||
"Geopackage (*.gpkg);;Alle Dateien (*)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
return # Abbruch
|
|
||||||
|
|
||||||
# Automatisch .gpkg anhängen
|
|
||||||
if not file_path.lower().endswith(".gpkg"):
|
|
||||||
file_path += ".gpkg"
|
|
||||||
|
|
||||||
# Existiert Datei bereits?
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
overwrite = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
"Datei existiert bereits",
|
|
||||||
f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?",
|
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
|
||||||
QMessageBox.No
|
|
||||||
)
|
|
||||||
if overwrite != QMessageBox.Yes:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Datei anlegen
|
|
||||||
try:
|
|
||||||
open(file_path, "w").close()
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Die Datei konnte nicht angelegt werden:\n{e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Datei übernehmen
|
|
||||||
self.verfahrens_db = file_path
|
|
||||||
self.file_widget.setFilePath(file_path)
|
|
||||||
self.update_group_button_color()
|
|
||||||
|
|
||||||
QMessageBox.information(self, "Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}")
|
|
||||||
Reference in New Issue
Block a user