Eigentümersuche mit Trefferliste im Tab „Eigentümer“ (#56)
Eigentümerdatei (CSV-Layer mit Nachname/Vorname) wählen, Name und/oder Vorname eingeben: Eine nicht modale Trefferliste zeigt Name, Anschrift, E-Mail und die Kennzeichen „verstorben“/„Rechtsperson“. Gleiche Personen mit gleicher Anschrift werden zusammengefasst; Groß-/Kleinschreibung und Umlautschreibweise spielen keine Rolle. Von QGIS als Zahl gelesene PLZ bekommen ihre führende Null zurück. Meldungen enthalten keine Namen. Die Auswahltabelle aus „Verfahrensgebiete aus Landkreis laden“ ist als baue_auswahltabelle herausgelöst und wird von der Trefferliste mitgenutzt. Zweiter Schritt von #56; Anzeigefenster und Korrekturen folgen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
"""Tests für die Eigentümersuche im Serienbriefexport (#56).
|
||||
|
||||
Alle Namen und Anschriften sind erfunden.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from sn_verfahrensgebiet.functions import eigentuemer_suche as es
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
|
||||
KOPF = [
|
||||
"EIG_KONTO", "PER_ID", "PER_VERSTORBEN", "PER_NR", "Anrede", "Titel", "Vorname", "Nachname",
|
||||
"OT", "STRASSE", "PLZ", "WOHNORT", "Email", "PER_KONTOINHABER", "PER_RECHTSPERSON",
|
||||
"EIG_GEMARKUNG", "EIG_BLATT", "LISTE_ZGBBL", "ANZAHL_PERSONEN", "EIG_ID",
|
||||
]
|
||||
|
||||
|
||||
class _Null:
|
||||
"""Nachbildung von QGIS-NULL."""
|
||||
|
||||
def isNull(self):
|
||||
return True
|
||||
|
||||
|
||||
class _Feature(dict):
|
||||
def __init__(self, fid, werte):
|
||||
super().__init__(werte)
|
||||
self._fid = fid
|
||||
|
||||
def id(self):
|
||||
return self._fid
|
||||
|
||||
|
||||
class _Layer:
|
||||
def __init__(self, zeilen, kopf=KOPF, name="Serienbrief", layer_id="csv1"):
|
||||
self._kopf = list(kopf)
|
||||
self._name = name
|
||||
self._id = layer_id
|
||||
self._features = [
|
||||
_Feature(i + 1, {spalte: zeile.get(spalte.lstrip(""), "") for spalte in self._kopf})
|
||||
for i, zeile in enumerate(zeilen)
|
||||
]
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def fields(self):
|
||||
return types.SimpleNamespace(names=lambda: list(self._kopf))
|
||||
|
||||
def getFeatures(self):
|
||||
return iter(self._features)
|
||||
|
||||
|
||||
def _zeile(nachname, vorname, strasse="Hauptstraße 1", plz="01234", ort="Musterstadt", **weitere):
|
||||
zeile = {"Nachname": nachname, "Vorname": vorname, "STRASSE": strasse, "PLZ": plz, "WOHNORT": ort}
|
||||
zeile.update(weitere)
|
||||
return zeile
|
||||
|
||||
|
||||
class TestLayererkennung(unittest.TestCase):
|
||||
|
||||
def test_layer_mit_name_und_vorname_ist_eigentuemerlayer(self):
|
||||
"""Prüft, dass ein Layer mit den Spalten Nachname und Vorname als Eigentümerdatei erkannt wird, auch mit BOM und anderer Schreibweise."""
|
||||
self.assertTrue(es.ist_eigentuemerlayer(_Layer([])))
|
||||
self.assertTrue(es.ist_eigentuemerlayer(_Layer([], kopf=["NACHNAME", "vorname"])))
|
||||
self.assertFalse(es.ist_eigentuemerlayer(_Layer([], kopf=["flurstueckskennzeichen"])))
|
||||
self.assertFalse(es.ist_eigentuemerlayer(object()))
|
||||
|
||||
def test_eigentuemerlayer_im_projekt(self):
|
||||
"""Prüft, dass aus dem Projekt nur passende Layer kommen, nach Namen sortiert."""
|
||||
b = _Layer([], name="b-Export", layer_id="b")
|
||||
a = _Layer([], name="A-Export", layer_id="a")
|
||||
flst = _Layer([], kopf=["flurstueckskennzeichen"], name="Flurstücke", layer_id="f")
|
||||
projekt = types.SimpleNamespace(mapLayers=lambda: {"b": b, "f": flst, "a": a})
|
||||
with patch.object(es, "QgsProject", types.SimpleNamespace(instance=lambda: projekt)):
|
||||
self.assertEqual(es.eigentuemerlayer_im_projekt(), [a, b])
|
||||
|
||||
|
||||
class TestSuche(unittest.TestCase):
|
||||
|
||||
def test_ohne_eingabe_fehler(self):
|
||||
"""Prüft, dass ohne Name und Vorname nicht die ganze Datei gelistet, sondern um eine Eingabe gebeten wird."""
|
||||
with self.assertRaisesRegex(es.EigentuemerSucheFehler, "Namen"):
|
||||
es.suche_eigentuemer(_Layer([_zeile("Beispiel", "Anna")]), " ", "")
|
||||
|
||||
def test_layer_ohne_pflichtspalten(self):
|
||||
"""Prüft, dass ein Layer ohne Nachname/Vorname mit Hinweis auf den Serienbriefexport abgelehnt wird."""
|
||||
with self.assertRaisesRegex(es.EigentuemerSucheFehler, "Serienbriefexport"):
|
||||
es.suche_eigentuemer(_Layer([], kopf=["flurstueckskennzeichen"]), "Beispiel", "")
|
||||
|
||||
def test_teilstring_ohne_gross_kleinschreibung(self):
|
||||
"""Prüft, dass Name und Vorname als Teilstring ohne Rücksicht auf Groß-/Kleinschreibung gesucht werden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna"),
|
||||
_zeile("Beispiel-Muster", "Bernd"),
|
||||
_zeile("Anders", "Anna"),
|
||||
])
|
||||
self.assertEqual([e.nachname for e in es.suche_eigentuemer(layer, "beispiel", "")],
|
||||
["Beispiel", "Beispiel-Muster"])
|
||||
self.assertEqual([e.nachname for e in es.suche_eigentuemer(layer, "", "ANNA")], ["Anders", "Beispiel"])
|
||||
self.assertEqual([e.vorname for e in es.suche_eigentuemer(layer, "beisp", "an")], ["Anna"])
|
||||
|
||||
def test_umlaute_und_eszett(self):
|
||||
"""Prüft, dass „Müller“ auch „Mueller“ findet und „Strauss“ auch „Strauß“."""
|
||||
layer = _Layer([_zeile("Mueller", "Jörg"), _zeile("Strauß", "Erika", ort="Anderswo")])
|
||||
self.assertEqual(len(es.suche_eigentuemer(layer, "Müller", "Joerg")), 1)
|
||||
self.assertEqual(len(es.suche_eigentuemer(layer, "Strauss", "")), 1)
|
||||
|
||||
def test_gleiche_person_gleiche_anschrift_zusammengefasst(self):
|
||||
"""Prüft, dass Zeilen mit gleichem Namen und gleicher Anschrift ein Treffer werden und Konten, Personen-IDs und E-Mails gesammelt werden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", EIG_KONTO="100", PER_ID="7", Email="anna@example.org"),
|
||||
_zeile("beispiel ", " Anna", strasse="Hauptstraße 1", EIG_KONTO="200", PER_ID="7",
|
||||
Titel="Dr.", Email="anna@example.org"),
|
||||
_zeile("Beispiel", "Anna", EIG_KONTO="300", PER_ID="8", Email="a.b@example.org"),
|
||||
])
|
||||
treffer = es.suche_eigentuemer(layer, "Beispiel", "Anna")
|
||||
self.assertEqual(len(treffer), 1)
|
||||
e = treffer[0]
|
||||
self.assertEqual(e.konten, ("100", "200", "300"))
|
||||
self.assertEqual(e.per_ids, ("7", "8"))
|
||||
self.assertEqual(e.emails, ("anna@example.org", "a.b@example.org"))
|
||||
self.assertEqual(e.feature_ids, (1, 2, 3))
|
||||
self.assertEqual(e.titel, "Dr.")
|
||||
|
||||
def test_andere_anschrift_eigener_treffer(self):
|
||||
"""Prüft, dass dieselbe Person mit anderer Anschrift ein eigener Treffer bleibt, sortiert nach Wohnort."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", ort="Zettlitz"),
|
||||
_zeile("Beispiel", "Anna", ort="Altdorf", plz="09999"),
|
||||
])
|
||||
treffer = es.suche_eigentuemer(layer, "Beispiel", "Anna")
|
||||
self.assertEqual([e.wohnort for e in treffer], ["Altdorf", "Zettlitz"])
|
||||
|
||||
def test_kennzeichen_verstorben_und_rechtsperson(self):
|
||||
"""Prüft, dass „verstorben“ und „Rechtsperson“ aus üblichen Ja-Werten erkannt werden und Nein-Werte, Leeres und NULL nicht zählen."""
|
||||
ja = ["J", "ja", "1", "true", "X"]
|
||||
nein = ["", "0", "N", "nein", "false", None, _Null()]
|
||||
zeilen = [_zeile(f"Ja{i}", "Test", PER_VERSTORBEN=w, PER_RECHTSPERSON=w) for i, w in enumerate(ja)]
|
||||
zeilen += [_zeile(f"Nein{i}", "Test", PER_VERSTORBEN=w, PER_RECHTSPERSON=w) for i, w in enumerate(nein)]
|
||||
for e in es.suche_eigentuemer(_Layer(zeilen), "", "Test"):
|
||||
erwartet = e.nachname.startswith("Ja")
|
||||
with self.subTest(name=e.nachname):
|
||||
self.assertEqual(e.verstorben, erwartet)
|
||||
self.assertEqual(e.rechtsperson, erwartet)
|
||||
self.assertEqual(e.hinweise, "verstorben, Rechtsperson" if erwartet else "")
|
||||
|
||||
def test_plz_mit_fuehrender_null(self):
|
||||
"""Prüft, dass eine von QGIS als Zahl gelesene PLZ wieder fünfstellig mit führender Null erscheint und Zeilen dadurch zusammenfinden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", plz=1067),
|
||||
_zeile("Beispiel", "Anna", plz="01067"),
|
||||
_zeile("Muster", "Bernd", plz="1067.0"),
|
||||
])
|
||||
treffer = es.suche_eigentuemer(layer, "e", "")
|
||||
self.assertEqual([(e.nachname, e.plz) for e in treffer], [("Beispiel", "01067"), ("Muster", "01067")])
|
||||
|
||||
def test_suche_gibt_nichts_aus(self):
|
||||
"""Prüft, dass die Suche keine Personendaten auf die Konsole schreibt (Datenschutz)."""
|
||||
ausgabe = io.StringIO()
|
||||
with contextlib.redirect_stdout(ausgabe), contextlib.redirect_stderr(ausgabe):
|
||||
es.suche_eigentuemer(_Layer([_zeile("Beispiel", "Anna")]), "Beispiel", "")
|
||||
self.assertEqual(ausgabe.getvalue(), "")
|
||||
|
||||
|
||||
class TestAnzeigeformen(unittest.TestCase):
|
||||
|
||||
def test_name_und_anschrift(self):
|
||||
"""Prüft, dass Name mit Titel und einzeilige Anschrift mit Ortsteil lesbar zusammengesetzt werden."""
|
||||
e = Eigentuemer(titel="Dr.", vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1",
|
||||
plz="01234", wohnort="Musterstadt", ortsteil="Unterdorf")
|
||||
self.assertEqual(e.name, "Beispiel, Dr. Anna")
|
||||
self.assertEqual(e.anschrift, "Hauptstraße 1, 01234 Musterstadt, OT Unterdorf")
|
||||
self.assertEqual(Eigentuemer(nachname="Beispiel", ortsteil="OT Unterdorf").anschrift, "OT Unterdorf")
|
||||
self.assertEqual(Eigentuemer(nachname="Gemeinde Musterstadt").name, "Gemeinde Musterstadt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Tests für den Tab „Eigentümer“ – Flurstückssuche (#56)."""
|
||||
"""Tests für den Tab „Eigentümer“ – Flurstückssuche und Eigentümersuche (#56).
|
||||
|
||||
Alle Namen und Anschriften sind erfunden.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
@@ -21,6 +24,9 @@ from sn_verfahrensgebiet.functions.flurstueck_suche import (
|
||||
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"):
|
||||
@@ -86,6 +92,83 @@ class TestFlurstueckSuchen(unittest.TestCase):
|
||||
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)
|
||||
|
||||
|
||||
@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 erscheinen, eine offene alte Liste geschlossen wird und die Eingaben an die Suche gehen."""
|
||||
layer = object()
|
||||
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)
|
||||
fenster.return_value.show.assert_called_once()
|
||||
self.assertIs(tab._trefferfenster, fenster.return_value)
|
||||
|
||||
|
||||
class TestTrefferliste(unittest.TestCase):
|
||||
|
||||
def test_trefferzeile_mit_hinweis(self):
|
||||
"""Prüft, dass die Trefferliste Name, Anschrift, E-Mail und die Kennzeichen „verstorben“/„Rechtsperson“ zeigt."""
|
||||
self.assertEqual(
|
||||
trefferzeile(_ANNA),
|
||||
("Beispiel, Anna", "Hauptstraße 1, 01234 Musterstadt", "anna@example.org", "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)
|
||||
|
||||
|
||||
class TestDockWidgetTabs(unittest.TestCase):
|
||||
|
||||
def test_tab_ist_im_dockwidget_eingetragen(self):
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Tests für die gemeinsame Auswahltabelle in verfahrensgebiet_dialoge (#56)."""
|
||||
|
||||
import types
|
||||
import unittest
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import (
|
||||
ausgewaehlte_indizes,
|
||||
zeilen_sichtbar,
|
||||
)
|
||||
|
||||
|
||||
class _Item:
|
||||
def __init__(self, row, index):
|
||||
self._row = row
|
||||
self._index = index
|
||||
|
||||
def row(self):
|
||||
return self._row
|
||||
|
||||
def data(self, _rolle):
|
||||
return self._index
|
||||
|
||||
|
||||
class TestAuswahltabelle(unittest.TestCase):
|
||||
|
||||
def test_suchfilter_ueber_alle_spalten(self):
|
||||
"""Prüft, dass der Suchfilter Teilstrings in jeder Spalte ohne Groß-/Kleinschreibung findet und leer alles zeigt."""
|
||||
zeilen = [["14612", "Verfahren Nord"], ["14613", "Süd"], ["14700", None]]
|
||||
self.assertEqual(zeilen_sichtbar(zeilen, "nord"), [True, False, False])
|
||||
self.assertEqual(zeilen_sichtbar(zeilen, " 146 "), [True, True, False])
|
||||
self.assertEqual(zeilen_sichtbar(zeilen, ""), [True, True, True])
|
||||
|
||||
def test_auswahl_nach_umsortieren(self):
|
||||
"""Prüft, dass die Auswahl über den gespeicherten Index auf die ursprünglichen Zeilen zurückgeführt wird, auch wenn die Tabelle umsortiert ist."""
|
||||
# Tabelle zeigt Zeile 0 → Index 2, Zeile 1 → Index 0; beide Spalten je Zeile ausgewählt
|
||||
tabelle = types.SimpleNamespace(
|
||||
selectedItems=lambda: [_Item(0, 2), _Item(0, 2), _Item(1, 0), _Item(1, 0)],
|
||||
item=lambda row, col: _Item(row, {0: 2, 1: 0}[row]),
|
||||
)
|
||||
self.assertEqual(ausgewaehlte_indizes(tabelle), [0, 2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user