Zweite Anhaengen-Pipeline (Gemarkungen-Button) auf Dedup umgestellt (#28)

handle_flurst_aus_gemarkungen in working_tab.py hatte eine eigene,
von flurstueck_alkis() unabhaengige Anhaengen-Logik, die
lade_flurstuecke_per_gemarkung direkt aufrief und ungefiltert
anhaengte. Nutzt jetzt dieselbe flurstueckskennzeichen-Pruefung wie
der Fix im vorigen Commit; dafuer vorhandene_kennzeichen() in
flurstueck_alkis.py oeffentlich gemacht.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 09:17:30 +02:00
co-authored by Claude Sonnet 5
parent 821bb0640e
commit 003ad1957b
4 changed files with 155 additions and 5 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
- Bugfix: Flurstücke aus ALKIS an bestehenden Layer anhängen überspringt jetzt bereits vorhandene Flurstücke (Abgleich über flurstueckskennzeichen), statt sie doppelt zu laden (#28) - Bugfix: Flurstücke aus ALKIS an bestehenden Layer anhängen überspringt jetzt bereits vorhandene Flurstücke (Abgleich über flurstueckskennzeichen), statt sie doppelt zu laden – betrifft sowohl den Dialog in "Flurstücke laden" als auch den Button "aus gewählten Gemarkungen laden" (#28)
- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix) - Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix)
- Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt - Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt
- Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte) - Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte)
+2 -2
View File
@@ -75,7 +75,7 @@ def _check_existing_flurstueck_layer(tab_widget):
return None, LoadStatus.NONE return None, LoadStatus.NONE
def _vorhandene_kennzeichen(layer) -> set: def vorhandene_kennzeichen(layer) -> set:
"""Liefert die ``flurstueckskennzeichen``-Werte, die ``layer`` bereits enthält. """Liefert die ``flurstueckskennzeichen``-Werte, die ``layer`` bereits enthält.
Fehlt das Feld, wird eine leere Menge zurückgegeben (kein Abgleich möglich). Fehlt das Feld, wird eine leere Menge zurückgegeben (kein Abgleich möglich).
@@ -272,7 +272,7 @@ def flurstueck_alkis(
alle_feats = list(layer.getFeatures()) alle_feats = list(layer.getFeatures())
neue_feats = alle_feats neue_feats = alle_feats
if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0: if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0:
vorhandene = _vorhandene_kennzeichen(append_target) vorhandene = vorhandene_kennzeichen(append_target)
neue_feats = [ neue_feats = [
f for f in alle_feats f for f in alle_feats
if str(f[FELD_FLURSTUECKSKENNZEICHEN] or "").strip() not in vorhandene if str(f[FELD_FLURSTUECKSKENNZEICHEN] or "").strip() not in vorhandene
+137
View File
@@ -15,6 +15,7 @@ if "qgis" not in sys.modules:
sys.modules["qgis.utils"] = qgis_utils sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import FLST_LAYER_NAME, FELD_FLURSTUECKSKENNZEICHEN
class _DummyLayer: class _DummyLayer:
@@ -81,6 +82,92 @@ class _DummyCombo:
self.layer = layer self.layer = layer
class _DummyGemFeature:
def __init__(self, schluessel, name):
self._schluessel = schluessel
self._name = name
def __getitem__(self, key):
if key == "schluessel":
return self._schluessel
if key == "ave_name":
return self._name
return None
class _DummyGemarkungenLayer:
def __init__(self, selected):
self._selected = list(selected)
def selectedFeatureCount(self):
return len(self._selected)
def selectedFeatures(self):
return list(self._selected)
class _DummyFlstFeatureWT:
def __init__(self, kennzeichen):
self._kennzeichen = kennzeichen
def __getitem__(self, key):
if key == FELD_FLURSTUECKSKENNZEICHEN:
return self._kennzeichen
return None
class _DummyFlstFieldsWT:
def indexFromName(self, name):
return 0 if name == FELD_FLURSTUECKSKENNZEICHEN else -1
class _DummyTempLayer:
"""Rückgabe von lade_flurstuecke_per_gemarkung in Tests."""
def __init__(self, features):
self._features = list(features)
def getFeatures(self):
return list(self._features)
def fields(self):
return _DummyFlstFieldsWT()
def isValid(self):
return True
def featureCount(self):
return len(self._features)
class _DummyExistingFlstLayer:
"""Bereits im Projekt vorhandener Flurstück-Layer, zeichnet addFeatures() auf."""
def __init__(self, features):
self._features = list(features)
self.added = None
def getFeatures(self):
return list(self._features)
def fields(self):
return _DummyFlstFieldsWT()
def dataProvider(self):
layer = self
class _Provider:
def addFeatures(self, feats):
layer.added = list(feats)
layer._features.extend(feats)
return True
return _Provider()
def updateExtents(self):
pass
class TestWorkingTab(unittest.TestCase): class TestWorkingTab(unittest.TestCase):
def _make_tab(self): def _make_tab(self):
@@ -96,6 +183,7 @@ class TestWorkingTab(unittest.TestCase):
tab.haken_flurst = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None) tab.haken_flurst = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.haken_gem = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None) tab.haken_gem = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.haken_grenz = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None) tab.haken_grenz = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.btn_flurst_aus_gemarkungen = _DummyButton()
tab.setze_haken = lambda *_args, **_kwargs: None tab.setze_haken = lambda *_args, **_kwargs: None
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben # Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
tab.pruefmanager = types.SimpleNamespace( tab.pruefmanager = types.SimpleNamespace(
@@ -175,6 +263,55 @@ class TestWorkingTab(unittest.TestCase):
mock_set_layer_id.assert_called_once_with("") mock_set_layer_id.assert_called_once_with("")
# ------------------------------------------------------------------
# handle_flurst_aus_gemarkungen
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.lade_flurstuecke_per_gemarkung")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_flurst_aus_gemarkungen_anhaengen_ueberspringt_vorhandene(
self, mock_iface, mock_project_instance, mock_msgbox_cls, mock_lade, mock_success
):
"""Anhängen über den Gemarkungen-Button überspringt bereits vorhandene flurstueckskennzeichen (#28)."""
tab = self._make_tab()
mock_iface.mapCanvas.return_value = _DummyCanvas()
gem_feat = _DummyGemFeature("110001", "Musterstadt")
gem_layer = _DummyGemarkungenLayer([gem_feat])
bestehend = _DummyExistingFlstLayer([_DummyFlstFeatureWT("110001_0001__00001")])
def map_layers_by_name(name):
if name == "Gemarkungen":
return [gem_layer]
if name == FLST_LAYER_NAME:
return [bestehend]
return []
project = types.SimpleNamespace(mapLayersByName=map_layers_by_name)
mock_project_instance.return_value = project
btn_anhaengen = object()
msg_instance = mock_msgbox_cls.return_value
msg_instance.addButton.side_effect = (
lambda text, role: btn_anhaengen if text == "Anhängen" else object()
)
msg_instance.clickedButton.return_value = btn_anhaengen
neu_geladen = _DummyTempLayer([
_DummyFlstFeatureWT("110001_0001__00001"), # bereits vorhanden
_DummyFlstFeatureWT("110001_0002__00007"), # neu
])
mock_lade.return_value = neu_geladen
tab.handle_flurst_aus_gemarkungen()
self.assertEqual(len(bestehend.added), 1)
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
mock_success.assert_called_once()
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# handle_grenz_alkis # handle_grenz_alkis
# ------------------------------------------------------------------ # ------------------------------------------------------------------
+15 -2
View File
@@ -18,8 +18,10 @@ from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import ( from sn_verfahrensgebiet.functions.flurstueck_alkis import (
flurstueck_alkis, flurstueck_alkis,
lade_flurstuecke_per_gemarkung, lade_flurstuecke_per_gemarkung,
vorhandene_kennzeichen,
ALKIS_SF_URL, ALKIS_SF_URL,
FLST_LAYER_NAME, FLST_LAYER_NAME,
FELD_FLURSTUECKSKENNZEICHEN,
) )
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import ( from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
verfahrensgebiet_alkis_komplett, verfahrensgebiet_alkis_komplett,
@@ -517,11 +519,22 @@ class WorkingTab(QWidget):
duration=0, duration=0,
) )
return return
existing_flst[0].dataProvider().addFeatures(list(temp_layer.getFeatures())) neue_feats = list(temp_layer.getFeatures())
if temp_layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0:
vorhandene = vorhandene_kennzeichen(existing_flst[0])
neue_feats = [
f for f in neue_feats
if str(f[FELD_FLURSTUECKSKENNZEICHEN] or "").strip() not in vorhandene
]
existing_flst[0].dataProvider().addFeatures(neue_feats)
existing_flst[0].updateExtents() existing_flst[0].updateExtents()
iface.mapCanvas().refresh() iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True) self.setze_haken(self.haken_flurst, True)
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5) success(
"Flurstücke",
f"{len(neue_feats)} neue Features wurden an den vorhandenen Layer angehängt.",
duration=5,
)
return return
# else: Überschreiben → fall-through zum flurstueck_alkis(ohne_dialog=True)-Aufruf # else: Überschreiben → fall-through zum flurstueck_alkis(ohne_dialog=True)-Aufruf