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>
This commit is contained in:
2026-10-06 13:27:15 +02:00
co-authored by Claude Opus 5.5
parent d88efac0ec
commit e547cb90c5
9 changed files with 1063 additions and 13 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Tests für das Anzeigefenster des Eigentümer-Datenabgleichs (#56).
Alle Namen und Anschriften sind erfunden.
"""
import types
import unittest
from unittest.mock import MagicMock
from sn_verfahrensgebiet.ui import eigentuemer_anzeige as ea
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
ANNA = Eigentuemer(anrede="Frau", titel="Dr.", vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1",
plz="01234", wohnort="Musterstadt", ortsteil="Unterdorf", emails=("anna@example.org",),
verstorben=True, rechtsperson=("0",), per_ids=("4711",), konten=("98765",))
def _rechteck(x, y, b, h):
return types.SimpleNamespace(x=lambda: x, y=lambda: y, width=lambda: b, height=lambda: h)
class TestAnzeigetext(unittest.TestCase):
def test_name_anschrift_email(self):
"""Prüft, dass die Anzeige Name mit Anrede und Titel, die mehrzeilige Anschrift mit Ortsteil und die E-Mail zeigt."""
text = ea.anzeige_html([ANNA])
self.assertIn("Frau Dr. Anna Beispiel", text)
self.assertIn("Hauptstraße 1<br/>01234 Musterstadt<br/>OT Unterdorf", text)
self.assertIn("anna@example.org", text)
def test_keine_internen_angaben(self):
"""Prüft, dass der Eigentümer weder Konten, Personen-IDs, Rechtsperson-Wert noch das Kennzeichen „verstorben“ sieht."""
text = ea.anzeige_html([ANNA])
for intern in ("98765", "4711", "verstorben", "Rechtsperson"):
self.assertNotIn(intern, text)
def test_fehlende_email_wird_genannt(self):
"""Prüft, dass eine fehlende E-Mail ausdrücklich als „keine angegeben“ erscheint."""
self.assertIn(ea.OHNE_EMAIL, ea.anzeige_html([Eigentuemer(nachname="Muster")]))
def test_werte_werden_maskiert(self):
"""Prüft, dass Sonderzeichen aus den Daten maskiert werden und kein HTML einschleusen."""
text = ea.anzeige_html([Eigentuemer(nachname="<b>Muster</b> & Co")])
self.assertIn("&lt;b&gt;Muster&lt;/b&gt; &amp; Co", text)
self.assertNotIn("<b>Muster", text)
def test_mehrere_eintraege_getrennt(self):
"""Prüft, dass mehrere Einträge untereinander, durch eine Linie getrennt, erscheinen."""
text = ea.anzeige_html([ANNA, Eigentuemer(vorname="Bernd", nachname="Muster")])
self.assertEqual(text.count("<hr/>"), 1)
self.assertIn("Bernd Muster", text)
def test_leer_nur_platzhalter(self):
"""Prüft, dass die leere Anzeige nur den neutralen Platzhalter zeigt."""
text = ea.anzeige_html([])
self.assertIn(ea.PLATZHALTER, text)
self.assertNotIn("Name", text)
class TestBildschirme(unittest.TestCase):
def test_vorgabe_ist_nicht_der_qgis_bildschirm(self):
"""Prüft, dass als Vorgabe der erste Bildschirm ohne QGIS gewählt wird, bei nur einem Bildschirm dieser."""
a, b, c = object(), object(), object()
self.assertEqual(ea.standard_index([a, b, c], a), 1)
self.assertEqual(ea.standard_index([a, b], b), 0)
self.assertEqual(ea.standard_index([a], a), 0)
self.assertEqual(ea.standard_index([], None), 0)
def test_bildschirm_text(self):
"""Prüft, dass ein Bildschirm mit Nummer und Auflösung beschriftet wird."""
bildschirm = types.SimpleNamespace(geometry=lambda: _rechteck(0, 0, 1920, 1080))
self.assertEqual(ea.bildschirm_text(bildschirm, 2), "Bildschirm 2 (1920 × 1080)")
self.assertEqual(ea.bildschirm_text(object(), 3), "Bildschirm 3")
def test_ohne_qt_anwendung_keine_bildschirme(self):
"""Prüft, dass ohne Qt-Anwendung (Mock-Modus) eine leere Bildschirmliste kommt statt eines Fehlers."""
self.assertEqual(ea.bildschirme(), [])
def test_vollbild_auf_gewaehltem_bildschirm(self):
"""Prüft, dass das Fenster im Vollbild dem gewählten Bildschirm zugeordnet und auf dessen Fläche gelegt wird."""
geo = _rechteck(1920, 0, 1920, 1080)
bildschirm = types.SimpleNamespace(geometry=lambda: geo)
fenster = MagicMock()
ea.zeige_auf_bildschirm(fenster, bildschirm, vollbild=True)
fenster.windowHandle.return_value.setScreen.assert_called_once_with(bildschirm)
fenster.setGeometry.assert_called_once_with(geo)
fenster.showFullScreen.assert_called_once()
fenster.showNormal.assert_not_called()
def test_normales_fenster_mittig(self):
"""Prüft, dass auf dem QGIS-Bildschirm ein normales Fenster mittig im freien Bereich öffnet."""
bildschirm = types.SimpleNamespace(availableGeometry=lambda: _rechteck(0, 40, 1600, 1000))
fenster = MagicMock()
ea.zeige_auf_bildschirm(fenster, bildschirm, vollbild=False)
fenster.setGeometry.assert_called_once_with(350, 215, 900, 650)
fenster.showNormal.assert_called_once()
fenster.showFullScreen.assert_not_called()
def test_ohne_bildschirm_einfach_zeigen(self):
"""Prüft, dass ohne bekannten Bildschirm das Fenster einfach gezeigt wird."""
fenster = MagicMock()
ea.zeige_auf_bildschirm(fenster, None, vollbild=True)
fenster.show.assert_called_once()
fenster.showFullScreen.assert_not_called()
if __name__ == "__main__":
unittest.main()
+134
View File
@@ -0,0 +1,134 @@
"""Tests für die Korrekturen beim Eigentümer-Datenabgleich (#56).
Alle Namen und Anschriften sind erfunden.
"""
import contextlib
import csv
import io
import os
import tempfile
import unittest
from datetime import datetime
from sn_verfahrensgebiet.functions import eigentuemer_korrektur as ek
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
ANNA = Eigentuemer(anrede="Frau", vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1", plz="01234",
wohnort="Musterstadt", emails=("anna@example.org",), per_ids=("7",), konten=("100", "200"))
class TestAenderungen(unittest.TestCase):
def test_feldwerte(self):
"""Prüft, dass alle korrigierbaren Felder vorbelegt werden, mehrere E-Mails durch Komma getrennt."""
werte = ek.feldwerte(Eigentuemer(nachname="Beispiel", emails=("a@example.org", "b@example.org")))
self.assertEqual(set(werte), {k for k, _ in ek.FELDER})
self.assertEqual(werte["email"], "a@example.org, b@example.org")
self.assertEqual(werte["nachname"], "Beispiel")
def test_nur_echte_aenderungen(self):
"""Prüft, dass nur geänderte Felder zählen und Leerraum am Rand oder doppelte Leerzeichen keine Änderung sind."""
werte = ek.feldwerte(ANNA)
werte.update(strasse=" Hauptstraße 1 ", wohnort="Neustadt", email="anna.neu@example.org")
self.assertEqual(ek.ermittle_aenderungen(ANNA, werte), [
("wohnort", "Musterstadt", "Neustadt"),
("email", "anna@example.org", "anna.neu@example.org"),
])
self.assertEqual(ek.ermittle_aenderungen(ANNA, ek.feldwerte(ANNA)), [])
def test_feld_leeren_ist_aenderung(self):
"""Prüft, dass das Leeren eines Feldes als Änderung zählt."""
self.assertEqual(ek.ermittle_aenderungen(ANNA, {"email": ""}), [("email", "anna@example.org", "")])
def test_wende_an(self):
"""Prüft, dass die Änderungen im angezeigten Eintrag landen und E-Mails wieder getrennt werden; Konten bleiben."""
neu = ek.wende_an(ANNA, [("wohnort", "Musterstadt", "Neustadt"),
("email", "", "a@example.org, b@example.org")])
self.assertEqual(neu.wohnort, "Neustadt")
self.assertEqual(neu.emails, ("a@example.org", "b@example.org"))
self.assertEqual(neu.konten, ANNA.konten)
self.assertEqual(ANNA.wohnort, "Musterstadt")
class TestKorrekturPfad(unittest.TestCase):
def test_textdatei_provider(self):
"""Prüft, dass aus der Quelle eines Textdatei-Layers (file-URI, Leerzeichen kodiert) der Pfad neben der CSV wird."""
quelle = "file:///D:/Verfahren/Eigent%C3%BCmer%20Export.csv?type=csv&detectTypes=yes&geomType=none"
self.assertEqual(ek.korrektur_pfad(quelle), "D:/Verfahren/Eigentümer Export_korrekturen.csv")
self.assertEqual(ek.korrektur_pfad("file:///home/test/export.CSV?type=csv"),
"/home/test/export_korrekturen.csv")
def test_ogr_provider(self):
"""Prüft, dass auch eine OGR-Quelle mit Layernamen den Pfad neben der CSV ergibt."""
self.assertEqual(ek.korrektur_pfad("D:/Verfahren/export.csv|layername=export"),
"D:/Verfahren/export_korrekturen.csv")
def test_keine_csv(self):
"""Prüft, dass für Quellen ohne CSV-Datei kein Pfad vorgeschlagen wird."""
self.assertIsNone(ek.korrektur_pfad(""))
self.assertIsNone(ek.korrektur_pfad("D:/Verfahren/daten.gpkg|layername=export"))
self.assertIsNone(ek.korrektur_pfad("memory?geometry=None"))
class TestSchreiben(unittest.TestCase):
def setUp(self):
self._ordner = tempfile.TemporaryDirectory()
self.pfad = os.path.join(self._ordner.name, "export_korrekturen.csv")
def tearDown(self):
self._ordner.cleanup()
def _zeilen(self):
with open(self.pfad, encoding="utf-8-sig", newline="") as datei:
return list(csv.reader(datei))
def test_neue_datei_mit_kopfzeile_und_bom(self):
"""Prüft, dass eine neue Korrektur-CSV mit BOM, Kopfzeile und je Änderung einer Zeile mit Exportspaltennamen entsteht."""
anzahl = ek.schreibe_korrekturen(self.pfad, ANNA, [
("strasse", "Hauptstraße 1", "Am Anger 3, Haus B"),
("email", "anna@example.org", ""),
], zeitpunkt=datetime(2026, 10, 6, 14, 30, 5))
self.assertEqual(anzahl, 2)
with open(self.pfad, "rb") as datei:
self.assertTrue(datei.read().startswith(b"\xef\xbb\xbf"))
self.assertEqual(self._zeilen(), [
ek.KOPFZEILE,
["2026-10-06 14:30:05", "100 200", "7", "STRASSE", "Hauptstraße 1", "Am Anger 3, Haus B"],
["2026-10-06 14:30:05", "100 200", "7", "Email", "anna@example.org", ""],
])
def test_anhaengen_ohne_zweite_kopfzeile(self):
"""Prüft, dass weitere Korrekturen angehängt werden, ohne Kopfzeile oder BOM zu wiederholen."""
ek.schreibe_korrekturen(self.pfad, ANNA, [("wohnort", "Musterstadt", "Neustadt")])
ek.schreibe_korrekturen(self.pfad, ANNA, [("plz", "01234", "01235")])
zeilen = self._zeilen()
self.assertEqual(len(zeilen), 3)
self.assertEqual(zeilen[0], ek.KOPFZEILE)
self.assertEqual([z[3] for z in zeilen[1:]], ["WOHNORT", "PLZ"])
with open(self.pfad, "rb") as datei:
self.assertEqual(datei.read().count(b"\xef\xbb\xbf"), 1)
def test_ohne_aenderung_keine_datei(self):
"""Prüft, dass ohne Änderung keine Datei angelegt wird."""
self.assertEqual(ek.schreibe_korrekturen(self.pfad, ANNA, []), 0)
self.assertFalse(os.path.exists(self.pfad))
def test_schreibfehler_wird_weitergereicht(self):
"""Prüft, dass ein nicht schreibbarer Pfad als OSError beim Aufrufer ankommt."""
with self.assertRaises(OSError):
ek.schreibe_korrekturen(os.path.join(self._ordner.name, "fehlt", "x.csv"), ANNA,
[("wohnort", "Musterstadt", "Neustadt")])
def test_schreibt_nichts_auf_die_konsole(self):
"""Prüft, dass das Festhalten von Korrekturen keine Personendaten auf die Konsole schreibt (Datenschutz)."""
ausgabe = io.StringIO()
with contextlib.redirect_stdout(ausgabe), contextlib.redirect_stderr(ausgabe):
ek.schreibe_korrekturen(self.pfad, ANNA, [("wohnort", "Musterstadt", "Neustadt")])
self.assertEqual(ausgabe.getvalue(), "")
if __name__ == "__main__":
unittest.main()
+213 -8
View File
@@ -1,11 +1,14 @@
"""Tests für den Tab „Eigentümer“ – Flurstückssuche und Eigentümersuche (#56).
"""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:
@@ -114,7 +117,8 @@ 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:
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()
@@ -123,31 +127,37 @@ class TestEigentuemerSuchen(unittest.TestCase):
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")):
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:
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()
"""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:
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.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):
@@ -169,6 +179,201 @@ class TestTrefferliste(unittest.TestCase):
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):