Files
Plugin_SN_Verfahrensgebiet/tests/test_eigentuemer_tab.py
T
DanielandClaude Opus 5.5 e547cb90c5 Anzeige für den Eigentümer und Korrekturen im Tab „Eigentümer“ (#56)
„Daten anzeigen“ in der Trefferliste zeigt Name, Anschrift und E-Mail der
gewählten Einträge in einem eigenen Fenster: auf einem zweiten Bildschirm
im Vollbild, auf dem QGIS-Bildschirm als normales Fenster. Die Sitzung
wird von Hand begonnen und beendet; „Anzeige leeren“ entfernt die Daten,
ohne das Fenster zu schließen. Konten, IDs und Kennzeichen erscheinen dort
nicht, alle Werte werden maskiert.

„Korrigieren …“ ändert angezeigte Daten. Die Eigentümerdatei bleibt
unverändert; jede Änderung wird mit Zeitpunkt, Konto, Personen-ID, altem
und neuem Wert an <Eigentümerdatei>_korrekturen.csv angehängt und sofort
angezeigt. Ohne beschreibbaren Ordner wird nach dem Speicherort gefragt.

In test_eigentuemer_tab.py sind die beim letzten Mal zusammengezogenen
with-Zeilen wieder umbrochen; die Trefferliste bekommt jetzt den
Anzeige-Rückruf, der Test dazu ist angepasst.

Dritter und letzter Schritt von #56.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:27:15 +02:00

391 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests für den Tab „Eigentümer“ – Flurstückssuche, Eigentümersuche, Anzeige und Korrektur (#56).
Alle Namen und Anschriften sind erfunden.
"""
import os
import sys
import tempfile
import types
import unittest
from datetime import datetime
from unittest.mock import MagicMock, patch
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.ui.tabs import eigentuemer_tab as et
from sn_verfahrensgebiet.ui.tabs.eigentuemer_tab import EigentuemerTab
from sn_verfahrensgebiet.functions.flurstueck_suche import (
FlurstueckSucheFehler,
KeinFlurstueckslayer,
SuchErgebnis,
)
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer, EigentuemerSucheFehler
from sn_verfahrensgebiet.ui import eigentuemer_treffer as treffer_modul
from sn_verfahrensgebiet.ui.eigentuemer_treffer import EigentuemerTrefferFenster, trefferzeile
def _tab(gemarkung="0268", flurstueck="562"):
tab = EigentuemerTab.__new__(EigentuemerTab)
tab.gemarkung_edit = types.SimpleNamespace(text=lambda: gemarkung)
tab.flurstueck_edit = types.SimpleNamespace(text=lambda: flurstueck)
return tab
@patch.object(et, "success")
@patch.object(et, "info")
@patch.object(et, "warning")
@patch.object(et, "show_info_dialog")
class TestFlurstueckSuchen(unittest.TestCase):
def test_ohne_flurstueckslayer_erscheint_hinweisdialog(self, dialog, warnung, hinweis, erfolg):
"""Prüft, dass ohne Layer „Flurstücke“ ein Dialog zum Laden von Verfahrensgebiet und Flurstücken erscheint."""
with patch.object(et, "flurstueck_suchen", side_effect=KeinFlurstueckslayer()):
_tab()._on_flurstueck_suchen()
dialog.assert_called_once()
self.assertIn("Flurstücke aus ALKIS laden", dialog.call_args[0][1])
warnung.assert_not_called()
erfolg.assert_not_called()
def test_eingabefehler_als_warnung(self, dialog, warnung, hinweis, erfolg):
"""Prüft, dass eine ungültige Eingabe als Warnung in der Meldungsleiste erscheint, nicht als Dialog."""
with patch.object(et, "flurstueck_suchen", side_effect=FlurstueckSucheFehler("falsch")):
_tab()._on_flurstueck_suchen()
warnung.assert_called_once_with("Flurstückssuche", "falsch")
dialog.assert_not_called()
def test_kein_treffer(self, dialog, warnung, hinweis, erfolg):
"""Prüft, dass ohne Treffer ein Hinweis erscheint und keine Auswahl gesetzt wird."""
layer = MagicMock()
with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [])):
_tab(flurstueck="999")._on_flurstueck_suchen()
hinweis.assert_called_once()
self.assertIn("999", hinweis.call_args[0][1])
layer.selectByIds.assert_not_called()
def test_treffer_werden_ausgewaehlt_und_angezoomt(self, dialog, warnung, hinweis, erfolg):
"""Prüft, dass Treffer im Flurstückslayer ausgewählt, angezoomt und gemeldet werden."""
layer = MagicMock()
layer.name.return_value = "Flurstücke"
canvas = MagicMock()
with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [1, 2])) as suche, \
patch.object(et, "iface", types.SimpleNamespace(mapCanvas=lambda: canvas)):
_tab(gemarkung=" Beispielhausen ", flurstueck=" 562 ")._on_flurstueck_suchen()
suche.assert_called_once_with("Beispielhausen", "562")
layer.selectByIds.assert_called_once_with([1, 2])
canvas.zoomToSelected.assert_called_once_with(layer)
canvas.flashFeatureIds.assert_called_once_with(layer, [1, 2])
erfolg.assert_called_once()
self.assertIn("2 Flurstücke", erfolg.call_args[0][1])
def test_ohne_kartenansicht_bleibt_auswahl(self, dialog, warnung, hinweis, erfolg):
"""Prüft, dass die Auswahl auch ohne Kartenansicht gesetzt und gemeldet wird."""
layer = MagicMock()
with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [7])), \
patch.object(et, "iface", types.SimpleNamespace()):
_tab()._on_flurstueck_suchen()
layer.selectByIds.assert_called_once_with([7])
self.assertIn("1 Flurstück", erfolg.call_args[0][1])
def _eig_tab(nachname="Beispiel", vorname="", layer_id="csv1"):
tab = EigentuemerTab.__new__(EigentuemerTab)
tab.nachname_edit = types.SimpleNamespace(text=lambda: nachname)
tab.vorname_edit = types.SimpleNamespace(text=lambda: vorname)
tab.eigentuemer_layer_combo = MagicMock()
tab.eigentuemer_layer_combo.currentData.return_value = layer_id
tab._trefferfenster = None
return tab
_ANNA = Eigentuemer(vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1", plz="01234",
wohnort="Musterstadt", emails=("anna@example.org",), verstorben=True,
rechtsperson=("0",))
@patch.object(et, "info")
@patch.object(et, "warning")
class TestEigentuemerSuchen(unittest.TestCase):
def test_ohne_eigentuemerdatei_warnung(self, warnung, hinweis):
"""Prüft, dass ohne gewählte Eigentümerdatei die Liste aufgefrischt und dann auf das Laden der CSV hingewiesen wird."""
tab = _eig_tab(layer_id=None)
with patch.object(et, "hole_layer", return_value=None), \
patch.object(et, "suche_eigentuemer") as suche:
tab._on_eigentuemer_suchen()
tab.eigentuemer_layer_combo.aktualisieren.assert_called_once()
warnung.assert_called_once()
self.assertIn("CSV", warnung.call_args[0][1])
suche.assert_not_called()
def test_suchfehler_als_warnung(self, warnung, hinweis):
"""Prüft, dass ein Fehler der Suche (z. B. leere Eingabe) als Warnung erscheint."""
with patch.object(et, "hole_layer", return_value=object()), \
patch.object(et, "suche_eigentuemer", side_effect=EigentuemerSucheFehler("Bitte eingeben")):
_eig_tab()._on_eigentuemer_suchen()
warnung.assert_called_once_with("Eigentümersuche", "Bitte eingeben")
def test_kein_treffer_ohne_namen_in_meldung(self, warnung, hinweis):
"""Prüft, dass ohne Treffer ein Hinweis erscheint, der den gesuchten Namen nicht wiederholt (Datenschutz)."""
with patch.object(et, "hole_layer", return_value=object()), \
patch.object(et, "suche_eigentuemer", return_value=[]), \
patch.object(et, "EigentuemerTrefferFenster") as fenster:
_eig_tab(nachname="Beispiel")._on_eigentuemer_suchen()
hinweis.assert_called_once()
self.assertNotIn("Beispiel", hinweis.call_args[0][1])
fenster.assert_not_called()
def test_treffer_oeffnen_trefferliste_und_ersetzen_die_alte(self, warnung, hinweis):
"""Prüft, dass Treffer in einer neuen Trefferliste mit Anzeige-Rückruf erscheinen, eine offene alte Liste geschlossen wird und die Eingaben an die Suche gehen."""
layer = MagicMock()
layer.id.return_value = "csv1"
tab = _eig_tab(nachname="Beisp", vorname="An")
altes_fenster = MagicMock()
tab._trefferfenster = altes_fenster
with patch.object(et, "hole_layer", return_value=layer), \
patch.object(et, "suche_eigentuemer", return_value=[_ANNA]) as suche, patch.object(et, "EigentuemerTrefferFenster") as fenster:
tab._on_eigentuemer_suchen()
suche.assert_called_once_with(layer, "Beisp", "An")
altes_fenster.close.assert_called_once()
fenster.assert_called_once_with([_ANNA], tab, beim_anzeigen=tab._daten_anzeigen)
fenster.return_value.show.assert_called_once()
self.assertIs(tab._trefferfenster, fenster.return_value)
self.assertEqual(tab._treffer_layer_id, "csv1")
class TestTrefferliste(unittest.TestCase):
def test_trefferzeile_mit_hinweis(self):
"""Prüft, dass die Trefferliste Name, Anschrift, E-Mail, den Rohwert von PER_RECHTSPERSON und das Kennzeichen „verstorben“ zeigt."""
self.assertEqual(
trefferzeile(_ANNA),
("Beispiel, Anna", "Hauptstraße 1, 01234 Musterstadt", "anna@example.org", "0", "verstorben"),
)
def test_ausgewaehlte_eigentuemer(self):
"""Prüft, dass das Trefferfenster die in der Tabelle gewählten Einträge zurückgibt."""
bernd = Eigentuemer(vorname="Bernd", nachname="Muster")
fenster = EigentuemerTrefferFenster.__new__(EigentuemerTrefferFenster)
fenster.treffer = [_ANNA, bernd]
fenster.tabelle = object()
with patch.object(treffer_modul, "ausgewaehlte_indizes", return_value=[1]) as indizes:
self.assertEqual(fenster.ausgewaehlte_eigentuemer(), [bernd])
indizes.assert_called_once_with(fenster.tabelle)
def test_daten_anzeigen_gibt_auswahl_weiter(self):
"""Prüft, dass „Daten anzeigen“ die Auswahl der Trefferliste an den Tab übergibt."""
fenster = EigentuemerTrefferFenster.__new__(EigentuemerTrefferFenster)
fenster.treffer = [_ANNA]
fenster.tabelle = object()
fenster.beim_anzeigen = MagicMock()
with patch.object(treffer_modul, "ausgewaehlte_indizes", return_value=[0]):
fenster._on_anzeigen()
fenster.beim_anzeigen.assert_called_once_with([_ANNA])
def _anz_tab(angezeigt=None, anzeige=None, layer_id="csv1"):
tab = EigentuemerTab.__new__(EigentuemerTab)
for name in ("btn_sitzung", "btn_anzeige_leeren", "btn_korrigieren", "sitzung_label", "bildschirm_combo"):
setattr(tab, name, MagicMock())
tab._anzeige = anzeige
tab._sitzung_seit = datetime(2026, 10, 6, 9, 5) if anzeige is not None else None
tab._angezeigt = list(angezeigt or [])
tab._treffer_layer_id = layer_id
tab._anzeige_layer_id = layer_id
tab._korrektur_pfade = {}
return tab
_BERND = Eigentuemer(vorname="Bernd", nachname="Muster", konten=("300",), per_ids=("8",))
@patch.object(et, "zeige_auf_bildschirm")
@patch.object(et, "qgis_bildschirm")
@patch.object(et, "EigentuemerAnzeige")
class TestSitzung(unittest.TestCase):
def test_sitzung_beginnen_im_vollbild_auf_zweitem_bildschirm(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass „Sitzung beginnen“ ein leeres Anzeigefenster auf dem gewählten Bildschirm öffnet, im Vollbild, wenn QGIS woanders liegt."""
tab = _anz_tab()
zweiter = object()
tab.bildschirm_combo.gewaehlter_bildschirm.return_value = zweiter
qgis.return_value = object()
tab._on_sitzung_umschalten()
anzeige_klasse.assert_called_once_with(tab)
zeige.assert_called_once_with(anzeige_klasse.return_value, zweiter, True)
self.assertIs(tab._anzeige, anzeige_klasse.return_value)
self.assertEqual(tab._angezeigt, [])
tab.btn_sitzung.setText.assert_called_with("Sitzung beenden")
self.assertIn("leer", tab.sitzung_label.setText.call_args[0][0])
def test_auf_qgis_bildschirm_kein_vollbild(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass das Anzeigefenster auf dem QGIS-Bildschirm als normales Fenster öffnet."""
tab = _anz_tab()
einziger = object()
tab.bildschirm_combo.gewaehlter_bildschirm.return_value = einziger
qgis.return_value = einziger
tab._sitzung_beginnen()
self.assertFalse(zeige.call_args[0][2])
def test_daten_anzeigen_beginnt_sitzung_bei_bedarf(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass „Daten anzeigen“ ohne laufende Sitzung diese beginnt und die Einträge zeigt; die Statuszeile nennt nur die Anzahl."""
tab = _anz_tab()
tab._daten_anzeigen([_ANNA, _BERND])
anzeige_klasse.return_value.zeige.assert_called_once_with([_ANNA, _BERND])
self.assertEqual(tab._angezeigt, [_ANNA, _BERND])
status = tab.sitzung_label.setText.call_args[0][0]
self.assertIn("2 Einträge", status)
self.assertNotIn("Beispiel", status)
def test_daten_anzeigen_ohne_auswahl(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass „Daten anzeigen“ ohne Auswahl nur einen Hinweis gibt und keine Sitzung beginnt."""
tab = _anz_tab()
with patch.object(et, "warning") as warnung:
tab._daten_anzeigen([])
warnung.assert_called_once()
anzeige_klasse.assert_not_called()
def test_anzeige_leeren_laesst_fenster_offen(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass „Anzeige leeren“ die Daten entfernt, das Fenster aber nicht schließt."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
tab._on_anzeige_leeren()
anzeige.leeren.assert_called_once()
anzeige.close.assert_not_called()
self.assertEqual(tab._angezeigt, [])
self.assertIs(tab._anzeige, anzeige)
tab.btn_anzeige_leeren.setEnabled.assert_called_with(False)
def test_sitzung_beenden_schliesst_fenster(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass „Sitzung beenden“ das Anzeigefenster schließt und die angezeigten Daten vergisst."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
tab._on_sitzung_umschalten()
anzeige.close.assert_called_once()
self.assertIsNone(anzeige.beim_schliessen)
self.assertIsNone(tab._anzeige)
self.assertEqual(tab._angezeigt, [])
tab.btn_sitzung.setText.assert_called_with("Sitzung beginnen")
def test_von_aussen_geschlossen_beendet_sitzung(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass ein von außen geschlossenes Anzeigefenster (z. B. Alt+F4) die Sitzung im Tab beendet."""
tab = _anz_tab()
tab._sitzung_beginnen()
rueckruf = anzeige_klasse.return_value.beim_schliessen
rueckruf()
self.assertIsNone(tab._anzeige)
@patch.object(et, "success")
@patch.object(et, "info")
@patch.object(et, "warning")
class TestKorrektur(unittest.TestCase):
def test_korrektur_wird_festgehalten_und_angezeigt(self, warnung, hinweis, erfolg):
"""Prüft, dass eine Korrektur in die Korrektur-CSV geschrieben, sofort angezeigt und ohne Namen gemeldet wird."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA, _BERND], anzeige=anzeige)
tab._korrektur_pfade["csv1"] = "D:/Verfahren/export_korrekturen.csv"
with patch.object(et, "frage_korrektur", return_value=(1, {"wohnort": "Neustadt"})), \
patch.object(et, "schreibe_korrekturen") as schreiben:
tab._on_korrigieren()
schreiben.assert_called_once_with("D:/Verfahren/export_korrekturen.csv", _BERND,
[("wohnort", "", "Neustadt")])
self.assertEqual(tab._angezeigt[1].wohnort, "Neustadt")
self.assertIs(tab._angezeigt[0], _ANNA)
anzeige.zeige.assert_called_once_with(tab._angezeigt)
meldung = erfolg.call_args[0][1]
self.assertIn("export_korrekturen.csv", meldung)
self.assertNotIn("Muster", meldung)
def test_ohne_aenderung_nichts_schreiben(self, warnung, hinweis, erfolg):
"""Prüft, dass ein unverändert gespeicherter Dialog nichts schreibt."""
tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock())
with patch.object(et, "frage_korrektur", return_value=(0, {"nachname": "Beispiel"})), \
patch.object(et, "schreibe_korrekturen") as schreiben:
tab._on_korrigieren()
schreiben.assert_not_called()
hinweis.assert_called_once()
def test_abbruch_nichts_schreiben(self, warnung, hinweis, erfolg):
"""Prüft, dass ein abgebrochener Korrekturdialog nichts schreibt."""
tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock())
with patch.object(et, "frage_korrektur", return_value=None), \
patch.object(et, "schreibe_korrekturen") as schreiben:
tab._on_korrigieren()
schreiben.assert_not_called()
def test_schreibfehler_warnung_und_anzeige_unveraendert(self, warnung, hinweis, erfolg):
"""Prüft, dass bei einem Schreibfehler gewarnt, die Anzeige nicht geändert und der Pfad beim nächsten Mal neu bestimmt wird."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
tab._korrektur_pfade["csv1"] = "X:/gesperrt.csv"
with patch.object(et, "frage_korrektur", return_value=(0, {"wohnort": "Neustadt"})), \
patch.object(et, "schreibe_korrekturen", side_effect=PermissionError(13, "Zugriff verweigert")):
tab._on_korrigieren()
warnung.assert_called_once()
anzeige.zeige.assert_not_called()
self.assertIs(tab._angezeigt[0], _ANNA)
self.assertNotIn("csv1", tab._korrektur_pfade)
def test_ohne_anzeige_keine_korrektur(self, warnung, hinweis, erfolg):
"""Prüft, dass ohne angezeigte Daten kein Korrekturdialog erscheint."""
tab = _anz_tab()
with patch.object(et, "frage_korrektur") as dialog:
tab._on_korrigieren()
dialog.assert_not_called()
warnung.assert_called_once()
class TestKorrekturDatei(unittest.TestCase):
def test_vorschlag_neben_der_eigentuemerdatei(self):
"""Prüft, dass die Korrektur-CSV ohne Rückfrage neben der Eigentümerdatei liegt, wenn der Ordner beschreibbar ist, und gemerkt wird."""
with tempfile.TemporaryDirectory() as ordner:
quelle = "file:///" + os.path.join(ordner, "export.csv").replace(os.sep, "/") + "?type=csv"
layer = types.SimpleNamespace(source=lambda: quelle)
tab = _anz_tab()
with patch.object(et, "hole_layer", return_value=layer), \
patch.object(et.QFileDialog, "getSaveFileName") as dialog:
pfad = tab._korrektur_datei()
dialog.assert_not_called()
self.assertEqual(os.path.normcase(os.path.normpath(pfad)),
os.path.normcase(os.path.join(ordner, "export_korrekturen.csv")))
self.assertEqual(tab._korrektur_pfade["csv1"], pfad)
def test_ohne_csv_quelle_wird_gefragt(self):
"""Prüft, dass ohne verwertbare CSV-Quelle nach dem Speicherort gefragt wird; Abbrechen liefert keinen Pfad."""
tab = _anz_tab()
layer = types.SimpleNamespace(source=lambda: "memory?geometry=None")
with patch.object(et, "hole_layer", return_value=layer), \
patch.object(et.QFileDialog, "getSaveFileName", return_value=("", "")) as dialog:
self.assertIsNone(tab._korrektur_datei())
dialog.assert_called_once()
self.assertEqual(tab._korrektur_pfade, {})
with patch.object(et, "hole_layer", return_value=layer), \
patch.object(et.QFileDialog, "getSaveFileName", return_value=("D:/k.csv", "")):
self.assertEqual(tab._korrektur_datei(), "D:/k.csv")
class TestDockWidgetTabs(unittest.TestCase):
def test_tab_ist_im_dockwidget_eingetragen(self):
"""Prüft, dass der Tab „Eigentümer“ im DockWidget direkt nach dem Bearbeitungs-Tab steht."""
from sn_verfahrensgebiet.ui.dockwidget import DockWidget
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
self.assertEqual(DockWidget.tabs[:2], [WorkingTab, EigentuemerTab])
self.assertEqual(EigentuemerTab.tab_title, "Eigentümer")
if __name__ == "__main__":
unittest.main()