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:
@@ -1,3 +1,5 @@
|
||||
- Neu: Anzeige für den Eigentümer im Tab „Eigentümer“. „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, sonst als normales Fenster. Die Sitzung wird von Hand begonnen und beendet; „Anzeige leeren“ entfernt die Daten, ohne das Fenster zu schließen, damit der nächste Eigentümer sie nicht sieht (#56)
|
||||
- Neu: Angezeigte Daten lassen sich mit „Korrigieren …“ ändern. 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 (#56)
|
||||
- Neu: Eigentümersuche im Tab „Eigentümer“. Den Serienbriefexport (CSV) von Hand als Layer laden, als Eigentümerdatei wählen, Name und/oder Vorname eingeben: Die Trefferliste zeigt Name, Anschrift, E-Mail, den Wert von PER_RECHTSPERSON (0, 1, 5 oder 9, noch nicht entschlüsselt) und das Kennzeichen „verstorben“. Gleiche Personen mit gleicher Anschrift werden zusammengefasst, Groß-/Kleinschreibung und Umlautschreibweise spielen keine Rolle (#56)
|
||||
- Intern: Die Auswahltabelle aus „Verfahrensgebiete aus Landkreis laden“ (Suchfeld, „Alle auswählen“, Mehrfachauswahl) ist als gemeinsame Funktion herausgelöst und wird von der Eigentümer-Trefferliste mitgenutzt; der Landkreis-Dialog verhält sich unverändert (#56)
|
||||
- Neu: Tab „Eigentümer“ mit Flurstückssuche. Gemarkung (Name oder Nummer) und Flurstücksnummer (z. B. 562 oder 12/3) eingeben, die Treffer werden im Layer „Flurstücke“ ausgewählt und angezoomt. Ohne Nenner werden alle Flurstücke mit diesem Zähler gefunden. Fehlt der Layer „Flurstücke“, weist ein Dialog darauf hin, zuerst Verfahrensgebiet und Flurstücke aus ALKIS zu laden (#56)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""sn_verfahrensgebiet/functions/eigentuemer_korrektur.py – Korrekturen beim Datenabgleich (#56).
|
||||
|
||||
Der Bearbeiter korrigiert im Plugin Name, Anschrift oder E-Mail eines
|
||||
angezeigten Eigentümers. Die Eigentümerdatei (Serienbriefexport) bleibt
|
||||
unverändert; jede Änderung wird als eigene Zeile an eine Korrektur-CSV
|
||||
angehängt, standardmäßig ``<Eigentümerdatei>_korrekturen.csv`` im selben Ordner.
|
||||
|
||||
Spalten der Korrektur-CSV (UTF-8 mit BOM, Komma, wie der Export):
|
||||
``Zeitpunkt, EIG_KONTO, PER_ID, Feld, Alter Wert, Neuer Wert``. ``Feld`` ist
|
||||
der Spaltenname aus dem Export. Gehören mehrere Konten oder Personen-IDs zu
|
||||
einem Eintrag, stehen sie durch Leerzeichen getrennt in einer Zelle.
|
||||
|
||||
**Datenschutz:** Die Korrektur-CSV enthält Personendaten. Sie liegt neben
|
||||
dem Export und ist genauso zu schützen wie dieser. Das Modul schreibt
|
||||
nichts auf die Konsole und zeigt keine Dialoge.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer, SPALTEN
|
||||
|
||||
#: Korrigierbare Felder: (Schlüssel, Beschriftung). Der Spaltenname kommt aus SPALTEN.
|
||||
FELDER: List[Tuple[str, str]] = [
|
||||
("anrede", "Anrede"),
|
||||
("titel", "Titel"),
|
||||
("vorname", "Vorname"),
|
||||
("nachname", "Name"),
|
||||
("strasse", "Straße"),
|
||||
("plz", "PLZ"),
|
||||
("wohnort", "Wohnort"),
|
||||
("ortsteil", "Ortsteil"),
|
||||
("email", "E-Mail"),
|
||||
]
|
||||
KOPFZEILE = ["Zeitpunkt", "EIG_KONTO", "PER_ID", "Feld", "Alter Wert", "Neuer Wert"]
|
||||
ENDUNG_KORREKTUR = "_korrekturen.csv"
|
||||
|
||||
Aenderung = Tuple[str, str, str] # (Schlüssel, alter Wert, neuer Wert)
|
||||
|
||||
|
||||
def feldwerte(eigentuemer: Eigentuemer) -> Dict[str, str]:
|
||||
"""Die korrigierbaren Werte eines Eintrags; mehrere E-Mails durch ``, `` getrennt."""
|
||||
werte = {schluessel: getattr(eigentuemer, schluessel, "") for schluessel, _ in FELDER if schluessel != "email"}
|
||||
werte["email"] = ", ".join(eigentuemer.emails)
|
||||
return werte
|
||||
|
||||
|
||||
def ermittle_aenderungen(eigentuemer: Eigentuemer, neue_werte: Dict[str, str]) -> List[Aenderung]:
|
||||
"""Vergleicht die Eingaben mit dem Eintrag; Leerraum am Rand und doppelte Leerzeichen zählen nicht."""
|
||||
alt = feldwerte(eigentuemer)
|
||||
aenderungen: List[Aenderung] = []
|
||||
for schluessel, _ in FELDER:
|
||||
if schluessel not in neue_werte:
|
||||
continue
|
||||
neu = " ".join(str(neue_werte[schluessel] or "").split())
|
||||
if neu != alt[schluessel]:
|
||||
aenderungen.append((schluessel, alt[schluessel], neu))
|
||||
return aenderungen
|
||||
|
||||
|
||||
def wende_an(eigentuemer: Eigentuemer, aenderungen: List[Aenderung]) -> Eigentuemer:
|
||||
"""Liefert den Eintrag mit den neuen Werten (für die Anzeige)."""
|
||||
felder = {}
|
||||
for schluessel, _alt, neu in aenderungen:
|
||||
if schluessel == "email":
|
||||
felder["emails"] = tuple(t.strip() for t in neu.split(",") if t.strip())
|
||||
else:
|
||||
felder[schluessel] = neu
|
||||
return replace(eigentuemer, **felder)
|
||||
|
||||
|
||||
def korrektur_pfad(layer_quelle: str) -> Optional[str]:
|
||||
"""Vorgeschlagene Korrektur-CSV neben der Eigentümerdatei, oder ``None``.
|
||||
|
||||
Versteht die Quellangaben des Textdatei-Providers
|
||||
(``file:///D:/ordner/export.csv?type=csv&…``) und des OGR-Providers
|
||||
(``D:/ordner/export.csv|layername=export``).
|
||||
"""
|
||||
quelle = (layer_quelle or "").strip()
|
||||
if not quelle:
|
||||
return None
|
||||
if quelle.lower().startswith("file:"):
|
||||
pfad = unquote(urlparse(quelle).path)
|
||||
if re.match(r"^/[A-Za-z]:", pfad):
|
||||
pfad = pfad[1:]
|
||||
else:
|
||||
pfad = quelle.split("|", 1)[0]
|
||||
stamm, endung = os.path.splitext(pfad)
|
||||
if endung.lower() != ".csv":
|
||||
return None
|
||||
return stamm + ENDUNG_KORREKTUR
|
||||
|
||||
|
||||
def schreibe_korrekturen(
|
||||
pfad: str,
|
||||
eigentuemer: Eigentuemer,
|
||||
aenderungen: List[Aenderung],
|
||||
zeitpunkt: Optional[datetime] = None,
|
||||
) -> int:
|
||||
"""Hängt die Änderungen an die Korrektur-CSV an und gibt die Zahl der Zeilen zurück.
|
||||
|
||||
Legt die Datei mit Kopfzeile an, wenn sie fehlt oder leer ist.
|
||||
|
||||
Raises
|
||||
------
|
||||
OSError
|
||||
Die Datei lässt sich nicht schreiben.
|
||||
"""
|
||||
if not aenderungen:
|
||||
return 0
|
||||
stempel = (zeitpunkt or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
|
||||
konten = " ".join(eigentuemer.konten)
|
||||
per_ids = " ".join(eigentuemer.per_ids)
|
||||
neu_anlegen = not os.path.exists(pfad) or os.path.getsize(pfad) == 0
|
||||
with open(pfad, "a", encoding="utf-8-sig" if neu_anlegen else "utf-8", newline="") as datei:
|
||||
schreiber = csv.writer(datei, delimiter=",", lineterminator="\r\n")
|
||||
if neu_anlegen:
|
||||
schreiber.writerow(KOPFZEILE)
|
||||
for schluessel, alt, neu in aenderungen:
|
||||
schreiber.writerow([stempel, konten, per_ids, SPALTEN[schluessel], alt, neu])
|
||||
return len(aenderungen)
|
||||
@@ -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("<b>Muster</b> & 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()
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""sn_verfahrensgebiet/ui/eigentuemer_anzeige.py – Anzeigefenster für den Eigentümer (#56).
|
||||
|
||||
Das Fenster ist für einen zweiten Bildschirm gedacht, den nur der Eigentümer
|
||||
sieht. Es zeigt ausschließlich Name, Anschrift und E-Mail der vom Bearbeiter
|
||||
gewählten Einträge – keine Konten, keine Kennzeichen, keine anderen Treffer.
|
||||
Ohne Inhalt zeigt es nur einen neutralen Platzhalter.
|
||||
|
||||
Liegt das Fenster auf einem anderen Bildschirm als QGIS, öffnet es im Vollbild
|
||||
ohne Rahmen; der Eigentümer kann es nicht versehentlich schließen oder
|
||||
verschieben. Auf dem QGIS-Bildschirm (z. B. am Notebook ohne zweiten Monitor)
|
||||
öffnet es als normales Fenster.
|
||||
"""
|
||||
|
||||
import html
|
||||
from typing import Callable, List, Optional, Sequence
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QLabel, QComboBox, QCoreApplication, Qt
|
||||
from sn_basis.functions.qgisui_wrapper import get_main_window
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
|
||||
PLATZHALTER = "Datenabgleich"
|
||||
OHNE_EMAIL = "keine angegeben"
|
||||
|
||||
|
||||
def _ortsteil(text: str) -> str:
|
||||
return text if not text or text.upper().startswith("OT") else f"OT {text}"
|
||||
|
||||
|
||||
def anzeige_html(eintraege: Sequence[Eigentuemer]) -> str:
|
||||
"""HTML für das Anzeigefenster; alle Werte sind maskiert.
|
||||
|
||||
Je Eintrag eine Tabelle mit Name, Anschrift (mehrzeilig) und E-Mail. Eine
|
||||
fehlende E-Mail wird ausdrücklich genannt, damit der Eigentümer sie ergänzen
|
||||
kann. Ohne Einträge nur der Platzhalter.
|
||||
"""
|
||||
if not eintraege:
|
||||
return f"<p style='color:#888888;'>{html.escape(PLATZHALTER)}</p>"
|
||||
|
||||
bloecke = []
|
||||
for e in eintraege:
|
||||
name = " ".join(t for t in (e.anrede, e.titel, e.vorname, e.nachname) if t)
|
||||
ort = " ".join(t for t in (e.plz, e.wohnort) if t)
|
||||
anschrift = "<br/>".join(html.escape(t) for t in (e.strasse, ort, _ortsteil(e.ortsteil)) if t)
|
||||
email = html.escape(", ".join(e.emails)) if e.emails else f"<i>{OHNE_EMAIL}</i>"
|
||||
bloecke.append(
|
||||
"<table cellpadding='6'>"
|
||||
f"<tr><td style='color:#666666;'>Name</td><td><b>{html.escape(name)}</b></td></tr>"
|
||||
f"<tr><td style='color:#666666;' valign='top'>Anschrift</td><td>{anschrift}</td></tr>"
|
||||
f"<tr><td style='color:#666666;'>E-Mail</td><td>{email}</td></tr>"
|
||||
"</table>"
|
||||
)
|
||||
return "<hr/>".join(bloecke)
|
||||
|
||||
|
||||
class EigentuemerAnzeige(QWidget):
|
||||
"""Eigenes Fenster (nicht im Dock) mit großer Schrift für den Eigentümer.
|
||||
|
||||
``beim_schliessen`` wird aufgerufen, wenn das Fenster von außen geschlossen
|
||||
wird (z. B. Alt+F4), damit der Tab die Sitzung als beendet führt.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
self.beim_schliessen: Optional[Callable[[], None]] = None
|
||||
self.setWindowTitle(PLATZHALTER)
|
||||
self.setStyleSheet("background-color: white; color: black;")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(60, 50, 60, 50)
|
||||
self._text = QLabel()
|
||||
self._text.setTextFormat(Qt.TextFormat.RichText)
|
||||
self._text.setWordWrap(True)
|
||||
self._text.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
|
||||
self._text.setStyleSheet("font-size: 26pt;")
|
||||
layout.addWidget(self._text)
|
||||
self.leeren()
|
||||
|
||||
def zeige(self, eintraege: Sequence[Eigentuemer]) -> None:
|
||||
"""Ersetzt den Inhalt durch die Einträge."""
|
||||
self._text.setText(anzeige_html(eintraege))
|
||||
|
||||
def leeren(self) -> None:
|
||||
"""Zeigt nur noch den neutralen Platzhalter."""
|
||||
self._text.setText(anzeige_html([]))
|
||||
|
||||
def closeEvent(self, event): # noqa: N802 (Qt-Name)
|
||||
rueckruf, self.beim_schliessen = self.beim_schliessen, None
|
||||
if rueckruf is not None:
|
||||
rueckruf()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bildschirme
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def bildschirme() -> List[object]:
|
||||
"""Alle angeschlossenen Bildschirme (``QScreen``); leer ohne Qt-Anwendung."""
|
||||
try:
|
||||
anwendung = QCoreApplication.instance()
|
||||
return list(anwendung.screens()) if anwendung is not None else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def qgis_bildschirm() -> Optional[object]:
|
||||
"""Der Bildschirm mit dem QGIS-Hauptfenster, oder ``None``."""
|
||||
try:
|
||||
return get_main_window().screen()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def bildschirm_text(bildschirm, nummer: int) -> str:
|
||||
"""Eintrag für die Auswahlliste, z. B. ``Bildschirm 2 (1920 × 1080)``."""
|
||||
try:
|
||||
geo = bildschirm.geometry()
|
||||
return f"Bildschirm {nummer} ({geo.width()} × {geo.height()})"
|
||||
except Exception:
|
||||
return f"Bildschirm {nummer}"
|
||||
|
||||
|
||||
def standard_index(alle: Sequence[object], qgis: Optional[object]) -> int:
|
||||
"""Index des ersten Bildschirms, der nicht der QGIS-Bildschirm ist; sonst 0."""
|
||||
for index, bildschirm in enumerate(alle):
|
||||
if bildschirm is not qgis:
|
||||
return index
|
||||
return 0
|
||||
|
||||
|
||||
def zeige_auf_bildschirm(fenster, bildschirm, vollbild: bool) -> None:
|
||||
"""Öffnet ``fenster`` auf ``bildschirm`` – im Vollbild oder mittig als normales Fenster."""
|
||||
if bildschirm is None:
|
||||
fenster.show()
|
||||
return
|
||||
fenster.winId() # erzeugt das native Fenster, damit es einem Bildschirm zugeordnet werden kann
|
||||
griff = fenster.windowHandle()
|
||||
if griff is not None:
|
||||
griff.setScreen(bildschirm)
|
||||
if vollbild:
|
||||
fenster.setGeometry(bildschirm.geometry())
|
||||
fenster.showFullScreen()
|
||||
else:
|
||||
frei = bildschirm.availableGeometry()
|
||||
breite, hoehe = min(900, frei.width()), min(650, frei.height())
|
||||
fenster.setGeometry(frei.x() + (frei.width() - breite) // 2,
|
||||
frei.y() + (frei.height() - hoehe) // 2, breite, hoehe)
|
||||
fenster.showNormal()
|
||||
fenster.raise_()
|
||||
|
||||
|
||||
class BildschirmAuswahl(QComboBox):
|
||||
"""Auswahl des Bildschirms für das Anzeigefenster; wird beim Aufklappen aufgefrischt.
|
||||
|
||||
Vorgabe ist der erste Bildschirm, auf dem QGIS nicht liegt.
|
||||
"""
|
||||
|
||||
def aktualisieren(self) -> None:
|
||||
bisher = self.currentData()
|
||||
alle = bildschirme()
|
||||
self.clear()
|
||||
for nummer, bildschirm in enumerate(alle, start=1):
|
||||
self.addItem(bildschirm_text(bildschirm, nummer), bildschirm.name())
|
||||
index = self.findData(bisher) if bisher else -1
|
||||
self.setCurrentIndex(index if index >= 0 else standard_index(alle, qgis_bildschirm()))
|
||||
|
||||
def showPopup(self) -> None:
|
||||
self.aktualisieren()
|
||||
super().showPopup()
|
||||
|
||||
def gewaehlter_bildschirm(self) -> Optional[object]:
|
||||
"""Der gewählte ``QScreen`` oder ``None``, wenn es ihn nicht (mehr) gibt."""
|
||||
name = self.currentData()
|
||||
for bildschirm in bildschirme():
|
||||
if bildschirm.name() == name:
|
||||
return bildschirm
|
||||
return None
|
||||
@@ -0,0 +1,79 @@
|
||||
"""sn_verfahrensgebiet/ui/eigentuemer_korrektur_dialog.py – Korrekturdialog für den Bearbeiter (#56).
|
||||
|
||||
Zeigt die Felder eines angezeigten Eintrags zum Bearbeiten. Sind mehrere
|
||||
Einträge angezeigt, wählt der Bearbeiter oben, welchen er korrigiert. Der
|
||||
Dialog speichert nichts selbst; das übernimmt der Tab über
|
||||
:mod:`sn_verfahrensgebiet.functions.eigentuemer_korrektur`.
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Sequence, Tuple
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QT_VERSION, exec_dialog, DialogAccepted,
|
||||
QDialog, QDialogButtonBox, QVBoxLayout, QGridLayout, QLabel, QLineEdit, QComboBox,
|
||||
)
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_korrektur import FELDER, feldwerte
|
||||
|
||||
|
||||
def frage_korrektur(
|
||||
parent,
|
||||
eintraege: Sequence[Eigentuemer],
|
||||
) -> Optional[Tuple[int, Dict[str, str]]]:
|
||||
"""Fragt die korrigierten Werte ab.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[Tuple[int, Dict[str, str]]]
|
||||
Index des gewählten Eintrags und die Werte aller Felder, oder ``None``
|
||||
bei Abbruch (und im Mock-Modus).
|
||||
"""
|
||||
if not eintraege or QT_VERSION == 0:
|
||||
return None
|
||||
|
||||
dialog = QDialog(parent)
|
||||
dialog.setWindowTitle("Daten korrigieren")
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
auswahl = QComboBox()
|
||||
for eintrag in eintraege:
|
||||
auswahl.addItem(f"{eintrag.name} – {eintrag.anschrift}")
|
||||
if len(eintraege) > 1:
|
||||
layout.addWidget(QLabel("Eintrag:"))
|
||||
layout.addWidget(auswahl)
|
||||
|
||||
raster = QGridLayout()
|
||||
felder: Dict[str, QLineEdit] = {}
|
||||
for zeile, (schluessel, beschriftung) in enumerate(FELDER):
|
||||
raster.addWidget(QLabel(f"{beschriftung}:"), zeile, 0)
|
||||
feld = QLineEdit()
|
||||
raster.addWidget(feld, zeile, 1)
|
||||
felder[schluessel] = feld
|
||||
layout.addLayout(raster)
|
||||
|
||||
def _fuellen(index: int) -> None:
|
||||
if 0 <= index < len(eintraege):
|
||||
for schluessel, wert in feldwerte(eintraege[index]).items():
|
||||
felder[schluessel].setText(wert)
|
||||
|
||||
auswahl.currentIndexChanged.connect(_fuellen)
|
||||
_fuellen(0)
|
||||
|
||||
hinweis = QLabel(
|
||||
"<small>Die Eigentümerdatei bleibt unverändert. Jede Änderung wird in der "
|
||||
"Korrektur-CSV festgehalten und sofort im Anzeigefenster gezeigt. "
|
||||
"Mehrere E-Mail-Adressen mit Komma trennen.</small>"
|
||||
)
|
||||
hinweis.setWordWrap(True)
|
||||
layout.addWidget(hinweis)
|
||||
|
||||
knoepfe = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
|
||||
knoepfe.accepted.connect(dialog.accept)
|
||||
knoepfe.rejected.connect(dialog.reject)
|
||||
layout.addWidget(knoepfe)
|
||||
dialog.resize(520, dialog.sizeHint().height())
|
||||
|
||||
if exec_dialog(dialog) != DialogAccepted:
|
||||
return None
|
||||
return auswahl.currentIndex(), {schluessel: feld.text() for schluessel, feld in felder.items()}
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
Nicht modales Fenster für den Bearbeiter. Es zeigt die zusammengefassten
|
||||
Treffer mit dem Wert von ``PER_RECHTSPERSON`` (noch nicht entschlüsselt) und dem
|
||||
Kennzeichen „verstorben“ und erlaubt die Mehrfachauswahl. Die Tabelle kommt aus
|
||||
Kennzeichen „verstorben“ und erlaubt die Mehrfachauswahl. „Daten anzeigen“
|
||||
übergibt die Auswahl an das Anzeigefenster für den Eigentümer. Die Tabelle kommt aus
|
||||
:func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.baue_auswahltabelle`,
|
||||
dieselbe wie bei „Verfahrensgebiete aus Landkreis laden“.
|
||||
"""
|
||||
|
||||
from typing import List, Sequence, Tuple
|
||||
from typing import Callable, List, Optional, Sequence, Tuple
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
|
||||
@@ -34,9 +35,15 @@ def trefferzeile(eigentuemer: Eigentuemer) -> Tuple[str, str, str, str, str]:
|
||||
class EigentuemerTrefferFenster(QDialog):
|
||||
"""Trefferliste der Eigentümersuche; bleibt offen, während weitergesucht wird."""
|
||||
|
||||
def __init__(self, treffer: Sequence[Eigentuemer], parent=None):
|
||||
def __init__(
|
||||
self,
|
||||
treffer: Sequence[Eigentuemer],
|
||||
parent=None,
|
||||
beim_anzeigen: Optional[Callable[[List[Eigentuemer]], None]] = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.treffer: List[Eigentuemer] = list(treffer)
|
||||
self.beim_anzeigen = beim_anzeigen
|
||||
self.setWindowTitle("Eigentümersuche – Treffer")
|
||||
self.setModal(False)
|
||||
|
||||
@@ -55,6 +62,10 @@ class EigentuemerTrefferFenster(QDialog):
|
||||
self.tabelle.selectRow(0)
|
||||
|
||||
knoepfe = QHBoxLayout()
|
||||
self.btn_anzeigen = QPushButton("Daten anzeigen")
|
||||
self.btn_anzeigen.setToolTip("Zeigt die ausgewählten Einträge im Fenster für den Eigentümer.")
|
||||
self.btn_anzeigen.clicked.connect(self._on_anzeigen)
|
||||
knoepfe.addWidget(self.btn_anzeigen)
|
||||
knoepfe.addStretch()
|
||||
self.btn_schliessen = QPushButton("Schließen")
|
||||
self.btn_schliessen.clicked.connect(self.close)
|
||||
@@ -66,3 +77,7 @@ class EigentuemerTrefferFenster(QDialog):
|
||||
def ausgewaehlte_eigentuemer(self) -> List[Eigentuemer]:
|
||||
"""Die in der Tabelle ausgewählten Treffer, in der Reihenfolge der Trefferliste."""
|
||||
return [self.treffer[i] for i in ausgewaehlte_indizes(self.tabelle)]
|
||||
|
||||
def _on_anzeigen(self) -> None:
|
||||
if self.beim_anzeigen is not None:
|
||||
self.beim_anzeigen(self.ausgewaehlte_eigentuemer())
|
||||
|
||||
+202
-2
@@ -8,10 +8,19 @@ Zwei Bereiche:
|
||||
- **Eigentümersuche:** Eigentümerdatei (CSV-Layer) wählen, Name und/oder Vorname
|
||||
eingeben, „Suchen“ öffnet die Trefferliste.
|
||||
Logik: :mod:`sn_verfahrensgebiet.functions.eigentuemer_suche`.
|
||||
- **Anzeige für den Eigentümer:** Bildschirm wählen, Sitzung beginnen und
|
||||
beenden, Anzeige leeren, angezeigte Daten korrigieren. „Daten anzeigen“ in
|
||||
der Trefferliste füllt das Anzeigefenster (und beginnt bei Bedarf die
|
||||
Sitzung). Korrekturen landen in einer Korrektur-CSV neben der
|
||||
Eigentümerdatei (:mod:`sn_verfahrensgebiet.functions.eigentuemer_korrektur`).
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QComboBox, Qt,
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QComboBox,
|
||||
QFileDialog, Qt,
|
||||
)
|
||||
from sn_basis.functions.qgisui_wrapper import iface
|
||||
from sn_basis.functions.dialog_wrapper import show_info_dialog
|
||||
@@ -28,10 +37,24 @@ from sn_verfahrensgebiet.functions.eigentuemer_suche import (
|
||||
suche_eigentuemer,
|
||||
EigentuemerSucheFehler,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_korrektur import (
|
||||
ermittle_aenderungen,
|
||||
korrektur_pfad,
|
||||
schreibe_korrekturen,
|
||||
wende_an,
|
||||
)
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_treffer import EigentuemerTrefferFenster
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_anzeige import (
|
||||
BildschirmAuswahl,
|
||||
EigentuemerAnzeige,
|
||||
qgis_bildschirm,
|
||||
zeige_auf_bildschirm,
|
||||
)
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_korrektur_dialog import frage_korrektur
|
||||
|
||||
_TITEL_SUCHE = "Flurstückssuche"
|
||||
_TITEL_EIGENTUEMER = "Eigentümersuche"
|
||||
_TITEL_ANZEIGE = "Anzeige für den Eigentümer"
|
||||
|
||||
|
||||
class EigentuemerLayerAuswahl(QComboBox):
|
||||
@@ -131,8 +154,53 @@ class EigentuemerTab(QWidget):
|
||||
self.nachname_edit.returnPressed.connect(self._on_eigentuemer_suchen)
|
||||
self.vorname_edit.returnPressed.connect(self._on_eigentuemer_suchen)
|
||||
|
||||
titel_anzeige = QLabel(f"<b>{_TITEL_ANZEIGE}</b>")
|
||||
titel_anzeige.setStyleSheet("margin-top: 12px;")
|
||||
layout.addWidget(titel_anzeige)
|
||||
|
||||
zeile_bildschirm = QHBoxLayout()
|
||||
zeile_bildschirm.addWidget(QLabel("Bildschirm:"))
|
||||
self.bildschirm_combo = BildschirmAuswahl()
|
||||
self.bildschirm_combo.setToolTip(
|
||||
"Bildschirm, den der Eigentümer sieht. Auf einem anderen Bildschirm als QGIS "
|
||||
"öffnet die Anzeige im Vollbild."
|
||||
)
|
||||
zeile_bildschirm.addWidget(self.bildschirm_combo, 1)
|
||||
layout.addLayout(zeile_bildschirm)
|
||||
|
||||
zeile_sitzung = QHBoxLayout()
|
||||
self.btn_sitzung = QPushButton("Sitzung beginnen")
|
||||
self.btn_anzeige_leeren = QPushButton("Anzeige leeren")
|
||||
self.btn_anzeige_leeren.setToolTip(
|
||||
"Entfernt die Daten aus dem Anzeigefenster, ohne es zu schließen – "
|
||||
"vor dem nächsten Eigentümer."
|
||||
)
|
||||
self.btn_korrigieren = QPushButton("Korrigieren …")
|
||||
self.btn_korrigieren.setToolTip(
|
||||
"Angezeigte Daten korrigieren; die Änderung wird in der Korrektur-CSV festgehalten."
|
||||
)
|
||||
for knopf in (self.btn_sitzung, self.btn_anzeige_leeren, self.btn_korrigieren):
|
||||
zeile_sitzung.addWidget(knopf)
|
||||
layout.addLayout(zeile_sitzung)
|
||||
|
||||
self.sitzung_label = QLabel()
|
||||
self.sitzung_label.setWordWrap(True)
|
||||
layout.addWidget(self.sitzung_label)
|
||||
|
||||
self.btn_sitzung.clicked.connect(self._on_sitzung_umschalten)
|
||||
self.btn_anzeige_leeren.clicked.connect(self._on_anzeige_leeren)
|
||||
self.btn_korrigieren.clicked.connect(self._on_korrigieren)
|
||||
|
||||
self._trefferfenster = None
|
||||
self._treffer_layer_id = None
|
||||
self._anzeige = None
|
||||
self._sitzung_seit = None
|
||||
self._angezeigt = []
|
||||
self._anzeige_layer_id = None
|
||||
self._korrektur_pfade = {}
|
||||
self.eigentuemer_layer_combo.aktualisieren()
|
||||
self.bildschirm_combo.aktualisieren()
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@@ -200,6 +268,7 @@ class EigentuemerTab(QWidget):
|
||||
if not treffer:
|
||||
info(_TITEL_EIGENTUEMER, "Keine passenden Einträge gefunden.")
|
||||
return
|
||||
self._treffer_layer_id = layer.id()
|
||||
self._zeige_treffer(treffer)
|
||||
|
||||
def _zeige_treffer(self, treffer) -> None:
|
||||
@@ -207,5 +276,136 @@ class EigentuemerTab(QWidget):
|
||||
alt = getattr(self, "_trefferfenster", None)
|
||||
if alt is not None:
|
||||
alt.close()
|
||||
self._trefferfenster = EigentuemerTrefferFenster(treffer, self)
|
||||
self._trefferfenster = EigentuemerTrefferFenster(
|
||||
treffer, self, beim_anzeigen=self._daten_anzeigen
|
||||
)
|
||||
self._trefferfenster.show()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Anzeige für den Eigentümer
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_sitzung_umschalten(self) -> None:
|
||||
if self._anzeige is None:
|
||||
self._sitzung_beginnen()
|
||||
else:
|
||||
self._sitzung_beenden()
|
||||
|
||||
def _sitzung_beginnen(self) -> None:
|
||||
"""Öffnet das Anzeigefenster leer auf dem gewählten Bildschirm."""
|
||||
bildschirm = self.bildschirm_combo.gewaehlter_bildschirm()
|
||||
anzeige = EigentuemerAnzeige(self)
|
||||
anzeige.beim_schliessen = self._anzeige_wurde_geschlossen
|
||||
vollbild = bildschirm is not None and bildschirm is not qgis_bildschirm()
|
||||
zeige_auf_bildschirm(anzeige, bildschirm, vollbild)
|
||||
self._anzeige = anzeige
|
||||
self._sitzung_seit = datetime.now()
|
||||
self._angezeigt = []
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _sitzung_beenden(self) -> None:
|
||||
"""Schließt das Anzeigefenster; die Korrektur-CSV bleibt, wie sie ist."""
|
||||
anzeige = self._anzeige
|
||||
self._anzeige_wurde_geschlossen()
|
||||
if anzeige is not None:
|
||||
anzeige.beim_schliessen = None
|
||||
anzeige.close()
|
||||
|
||||
def _anzeige_wurde_geschlossen(self) -> None:
|
||||
self._anzeige = None
|
||||
self._sitzung_seit = None
|
||||
self._angezeigt = []
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _on_anzeige_leeren(self) -> None:
|
||||
"""Entfernt die Daten aus der Anzeige, das Fenster bleibt offen."""
|
||||
if self._anzeige is not None:
|
||||
self._anzeige.leeren()
|
||||
self._angezeigt = []
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _daten_anzeigen(self, eintraege) -> None:
|
||||
"""Rückruf der Trefferliste: zeigt die gewählten Einträge, beginnt bei Bedarf die Sitzung."""
|
||||
if not eintraege:
|
||||
warning(_TITEL_ANZEIGE, "Bitte in der Trefferliste mindestens einen Eintrag auswählen.")
|
||||
return
|
||||
if self._anzeige is None:
|
||||
self._sitzung_beginnen()
|
||||
self._angezeigt = list(eintraege)
|
||||
self._anzeige_layer_id = self._treffer_layer_id
|
||||
self._anzeige.zeige(self._angezeigt)
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _aktualisiere_sitzungsstatus(self) -> None:
|
||||
"""Knopftexte und Statuszeile; ohne Namen, damit der Tab nichts preisgibt."""
|
||||
aktiv = self._anzeige is not None
|
||||
self.btn_sitzung.setText("Sitzung beenden" if aktiv else "Sitzung beginnen")
|
||||
self.btn_anzeige_leeren.setEnabled(aktiv and bool(self._angezeigt))
|
||||
self.btn_korrigieren.setEnabled(aktiv and bool(self._angezeigt))
|
||||
if not aktiv:
|
||||
text = "Keine Sitzung. „Daten anzeigen“ in der Trefferliste beginnt sie automatisch."
|
||||
else:
|
||||
anzahl = len(self._angezeigt)
|
||||
inhalt = "leer" if not anzahl else ("1 Eintrag" if anzahl == 1 else f"{anzahl} Einträge")
|
||||
text = f"Sitzung seit {self._sitzung_seit:%H:%M} Uhr – Anzeige: {inhalt}"
|
||||
self.sitzung_label.setText(text)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Korrekturen
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_korrigieren(self) -> None:
|
||||
"""Fragt Korrekturen ab, hält sie in der Korrektur-CSV fest und zeigt sie sofort an."""
|
||||
if not self._angezeigt:
|
||||
warning(_TITEL_ANZEIGE, "Es werden keine Daten angezeigt.")
|
||||
return
|
||||
ergebnis = frage_korrektur(self, self._angezeigt)
|
||||
if ergebnis is None:
|
||||
return
|
||||
index, werte = ergebnis
|
||||
eintrag = self._angezeigt[index]
|
||||
aenderungen = ermittle_aenderungen(eintrag, werte)
|
||||
if not aenderungen:
|
||||
info(_TITEL_ANZEIGE, "Keine Änderung.")
|
||||
return
|
||||
|
||||
pfad = self._korrektur_datei()
|
||||
if not pfad:
|
||||
return
|
||||
try:
|
||||
schreibe_korrekturen(pfad, eintrag, aenderungen)
|
||||
except OSError as exc:
|
||||
self._korrektur_pfade.pop(self._anzeige_layer_id, None)
|
||||
warning(_TITEL_ANZEIGE, f"Die Korrektur-CSV konnte nicht geschrieben werden: {exc.strerror or exc}")
|
||||
return
|
||||
|
||||
self._angezeigt[index] = wende_an(eintrag, aenderungen)
|
||||
if self._anzeige is not None:
|
||||
self._anzeige.zeige(self._angezeigt)
|
||||
anzahl = len(aenderungen)
|
||||
text = "1 Änderung" if anzahl == 1 else f"{anzahl} Änderungen"
|
||||
success(_TITEL_ANZEIGE, f"{text} in „{os.path.basename(pfad)}“ festgehalten.")
|
||||
|
||||
def _korrektur_datei(self):
|
||||
"""Pfad der Korrektur-CSV für die angezeigte Eigentümerdatei.
|
||||
|
||||
Vorgabe ist ``<Eigentümerdatei>_korrekturen.csv`` daneben. Lässt sich
|
||||
daraus kein beschreibbarer Ordner ableiten, wird gefragt. Die Wahl gilt
|
||||
für die Eigentümerdatei, solange der Tab besteht.
|
||||
"""
|
||||
layer_id = self._anzeige_layer_id
|
||||
if layer_id in self._korrektur_pfade:
|
||||
return self._korrektur_pfade[layer_id]
|
||||
layer = hole_layer(layer_id)
|
||||
vorschlag = korrektur_pfad(layer.source()) if layer is not None else None
|
||||
ordner = os.path.dirname(vorschlag) if vorschlag else ""
|
||||
if vorschlag and os.path.isdir(ordner) and os.access(ordner, os.W_OK):
|
||||
pfad = vorschlag
|
||||
else:
|
||||
pfad, _ = QFileDialog.getSaveFileName(
|
||||
self, "Korrektur-CSV wählen", vorschlag or os.path.expanduser("~"), "CSV-Dateien (*.csv)"
|
||||
)
|
||||
if not pfad:
|
||||
return None
|
||||
self._korrektur_pfade[layer_id] = pfad
|
||||
return pfad
|
||||
|
||||
Reference in New Issue
Block a user