diff --git a/changelog.txt b/changelog.txt index 5d6ccae..18460cb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- Intern: Der Button "aus gewählten Gemarkungen laden" nutzt jetzt dieselbe Ladelogik wie "Flurstücke laden" (flurstueck_alkis) statt einer eigenen, parallelen Implementierung – konsolidiert drei bisher getrennte Code-Pfade zum Flurstücke-Laden auf einen (#47) - 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) - Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt diff --git a/functions/flurstueck_alkis.py b/functions/flurstueck_alkis.py index 1064fdf..d16fd97 100644 --- a/functions/flurstueck_alkis.py +++ b/functions/flurstueck_alkis.py @@ -75,7 +75,7 @@ def _check_existing_flurstueck_layer(tab_widget): return None, LoadStatus.NONE -def vorhandene_kennzeichen(layer) -> set: +def _vorhandene_kennzeichen(layer) -> set: """Liefert die ``flurstueckskennzeichen``-Werte, die ``layer`` bereits enthält. Fehlt das Feld, wird eine leere Menge zurückgegeben (kein Abgleich möglich). @@ -171,6 +171,7 @@ def flurstueck_alkis( vorgabe_namen: Optional[List[str]] = None, bbox=None, ohne_dialog: bool = False, + ohne_schluessel_abfrage: bool = False, ) -> Tuple[Optional[QgsVectorLayer], LoadStatus]: """Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln. @@ -184,17 +185,27 @@ def flurstueck_alkis( Eltern-Widget für Dialoge. vorgabe_schluessel : list of str, optional Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard - angezeigt; bei ``ohne_dialog=True`` direkt verwendet. + angezeigt; bei ``ohne_dialog=True`` oder ``ohne_schluessel_abfrage=True`` + direkt verwendet. vorgabe_namen : list of str, optional Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext. bbox : Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt für Rückwärtskompatibilität in der Signatur erhalten. ohne_dialog : bool, optional - ``True``: Überspringt die Gemarkungsschlüssel-Nutzerabfrage und verwendet - ``vorgabe_schluessel`` direkt. Vorhandene Layer werden ohne Rückfrage - ersetzt. Gedacht für die automatische Pipeline - (``verfahrensgebiet_alkis_komplett``). Standard: ``False``. + ``True``: Überspringt sowohl die Gemarkungsschlüssel-Nutzerabfrage (wie + ``ohne_schluessel_abfrage``) als auch die Rückfrage bei einem bereits + vorhandenen Layer – dieser wird dann immer ersetzt. Gedacht für die + automatischen Pipelines (``verfahrensgebiet_alkis_komplett``, + ``verfahrensgebiet_shape_komplett``). Standard: ``False``. + ohne_schluessel_abfrage : bool, optional + ``True``: Überspringt nur die Gemarkungsschlüssel-Nutzerabfrage und + verwendet ``vorgabe_schluessel`` direkt; die Rückfrage bei einem + bereits vorhandenen Layer (Überschreiben/Anhängen/Abbrechen, inkl. + Dedup beim Anhängen) bleibt aktiv. Für Aufrufer, die die Schlüssel + bereits anderweitig ermittelt haben (z. B. aus einer Layer-Auswahl), + aber trotzdem interaktiv mit einem bestehenden Layer umgehen wollen. + Wirkungslos, wenn ``ohne_dialog=True`` gesetzt ist. Standard: ``False``. Returns ------- @@ -220,8 +231,8 @@ def flurstueck_alkis( append_target = existing # --- Schritt 2: Gemarkungsschlüssel bestimmen ------------------------- - if ohne_dialog: - # Pipeline-Modus: vorgabe_schluessel direkt verwenden, kein Dialog + if ohne_dialog or ohne_schluessel_abfrage: + # vorgabe_schluessel direkt verwenden, kein ask_text-Dialog if not vorgabe_schluessel: return None, LoadStatus.NONE schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()] @@ -272,7 +283,7 @@ def flurstueck_alkis( alle_feats = list(layer.getFeatures()) neue_feats = alle_feats if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0: - vorhandene = vorhandene_kennzeichen(append_target) + vorhandene = _vorhandene_kennzeichen(append_target) neue_feats = [ f for f in alle_feats if str(f[FELD_FLURSTUECKSKENNZEICHEN] or "").strip() not in vorhandene diff --git a/tests/test_flurstueck_alkis.py b/tests/test_flurstueck_alkis.py index 57640b8..7e018a2 100644 --- a/tests/test_flurstueck_alkis.py +++ b/tests/test_flurstueck_alkis.py @@ -311,6 +311,28 @@ class TestFlurstueckAlkis(unittest.TestCase): self.assertIsNotNone(layer) self.assertEqual(status, LoadStatus.FIRST) + # ------------------------------------------------------------------ # + # 10. ohne_schluessel_abfrage: vorgabe_schluessel direkt, kein ask_text (#47) + # ------------------------------------------------------------------ # + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") + def test_ohne_schluessel_abfrage_ueberspringt_ask_text( + self, mock_ask_text, mock_project, mock_lade + ): + """ohne_schluessel_abfrage=True verwendet vorgabe_schluessel direkt, ohne ask_text aufzurufen.""" + mock_project.return_value = _DummyProject() + geladen = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")]) + mock_lade.return_value = geladen + + layer, status = flurstueck_alkis( + None, vorgabe_schluessel=["1234", "5678"], ohne_schluessel_abfrage=True + ) + + mock_ask_text.assert_not_called() + self.assertIs(layer, geladen) + self.assertEqual(status, LoadStatus.FIRST) + mock_lade.assert_called_once_with(["1234", "5678"]) # --------------------------------------------------------------------------- @@ -422,6 +444,28 @@ class TestFlurstueckAlkisAnhaengenDedup(unittest.TestCase): self.assertEqual(status, LoadStatus.APPEND) self.assertEqual(len(bestehend.added), 2) + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") + def test_ohne_schluessel_abfrage_append_dedup(self, mock_ask_text, mock_check, mock_lade): + """ohne_schluessel_abfrage=True: Anhängen dedupliziert trotzdem, ohne nach Schlüsseln zu fragen (#47).""" + bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")]) + neu_geladen = _DummyFlstLayer([ + _DummyFlstFeature("110001_0001__00001"), # bereits vorhanden + _DummyFlstFeature("110001_0002__00007"), # neu + ]) + mock_check.return_value = (bestehend, LoadStatus.APPEND) + mock_lade.return_value = neu_geladen + + layer, status = flurstueck_alkis( + None, vorgabe_schluessel=["0001", "0002"], ohne_schluessel_abfrage=True + ) + + mock_ask_text.assert_not_called() + self.assertEqual(status, LoadStatus.APPEND) + self.assertEqual(len(bestehend.added), 1) + self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_working_tab.py b/tests/test_working_tab.py index d69566a..54f5524 100644 --- a/tests/test_working_tab.py +++ b/tests/test_working_tab.py @@ -15,7 +15,6 @@ if "qgis" not in sys.modules: sys.modules["qgis.utils"] = qgis_utils from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus -from sn_verfahrensgebiet.functions.flurstueck_alkis import FLST_LAYER_NAME, FELD_FLURSTUECKSKENNZEICHEN class _DummyLayer: @@ -106,68 +105,6 @@ class _DummyGemarkungenLayer: 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): def _make_tab(self): @@ -264,54 +201,82 @@ class TestWorkingTab(unittest.TestCase): mock_set_layer_id.assert_called_once_with("") # ------------------------------------------------------------------ - # handle_flurst_aus_gemarkungen + # handle_flurst_alkis / handle_flurst_aus_gemarkungen (#47: eine gemeinsame + # Ladelogik in flurstueck_alkis(), hier wird nur noch die Anbindung geprüft) # ------------------------------------------------------------------ @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.apply_style") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis") @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 + def test_handle_flurst_alkis_first_load_uebernimmt_layer( + self, mock_iface, mock_project_instance, mock_flurstueck_alkis, mock_apply_style, mock_success ): - """Anhängen über den Gemarkungen-Button überspringt bereits vorhandene flurstueckskennzeichen (#28).""" + """Erstmaliges Laden über 'Flurstücke laden' fügt den Layer dem Projekt hinzu.""" tab = self._make_tab() mock_iface.mapCanvas.return_value = _DummyCanvas() + project = _DummyProject() + mock_project_instance.return_value = project + layer = _DummyLayer(valid=True, layer_id="flst_first") + mock_flurstueck_alkis.return_value = (layer, LoadStatus.FIRST) + tab.handle_flurst_alkis() + + mock_flurstueck_alkis.assert_called_once_with(tab) + self.assertIn(layer, project.added) + mock_success.assert_called_once() + + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.success") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface") + def test_handle_flurst_aus_gemarkungen_ruft_flurstueck_alkis_ohne_schluessel_abfrage( + self, mock_iface, mock_project_instance, mock_flurstueck_alkis, mock_success + ): + """Der Gemarkungen-Auswahl-Button ruft dieselbe Ladefunktion mit ohne_schluessel_abfrage=True auf (#47).""" + 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() + project = types.SimpleNamespace( + mapLayersByName=lambda name: [gem_layer] if name == "Gemarkungen" else [] ) - 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 + mock_project_instance.return_value = project + layer = _DummyLayer(valid=True, layer_id="flst_append") + mock_flurstueck_alkis.return_value = (layer, LoadStatus.APPEND) tab.handle_flurst_aus_gemarkungen() - self.assertEqual(len(bestehend.added), 1) - self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007") + mock_flurstueck_alkis.assert_called_once() + _args, kwargs = mock_flurstueck_alkis.call_args + self.assertEqual(kwargs.get("vorgabe_schluessel"), ["0001"]) + self.assertEqual(kwargs.get("vorgabe_namen"), ["Musterstadt"]) + self.assertTrue(kwargs.get("ohne_schluessel_abfrage")) mock_success.assert_called_once() + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.error") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.success") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance") + def test_handle_flurst_aus_gemarkungen_abbruch_zeigt_keine_zusaetzliche_meldung( + self, mock_project_instance, mock_flurstueck_alkis, mock_success, mock_error + ): + """Bricht flurstueck_alkis() ab (None, NONE), zeigt der Button keine eigene Meldung – flurstueck_alkis() hat das schon getan (#47).""" + tab = self._make_tab() + gem_feat = _DummyGemFeature("110001", "Musterstadt") + gem_layer = _DummyGemarkungenLayer([gem_feat]) + project = types.SimpleNamespace( + mapLayersByName=lambda name: [gem_layer] if name == "Gemarkungen" else [] + ) + mock_project_instance.return_value = project + mock_flurstueck_alkis.return_value = (None, LoadStatus.NONE) + + tab.handle_flurst_aus_gemarkungen() + + mock_success.assert_not_called() + mock_error.assert_not_called() + # ------------------------------------------------------------------ # handle_grenz_alkis # ------------------------------------------------------------------ diff --git a/ui/tabs/working_tab.py b/ui/tabs/working_tab.py index d3e4382..4f322eb 100644 --- a/ui/tabs/working_tab.py +++ b/ui/tabs/working_tab.py @@ -15,14 +15,7 @@ from sn_basis.modules.pruef_ergebnis import pruef_ergebnis from qgis.utils import iface from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus -from sn_verfahrensgebiet.functions.flurstueck_alkis import ( - flurstueck_alkis, - lade_flurstuecke_per_gemarkung, - vorhandene_kennzeichen, - ALKIS_SF_URL, - FLST_LAYER_NAME, - FELD_FLURSTUECKSKENNZEICHEN, -) +from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import ( verfahrensgebiet_alkis_komplett, BROR_LAYER_NAME, @@ -394,6 +387,25 @@ class WorkingTab(QWidget): # Flurstücke-Handler # ------------------------------------------------------------------ + def _uebernimm_flurstuecke_ergebnis(self, layer, status) -> None: + """Übernimmt das Ergebnis von flurstueck_alkis() ins Projekt (FIRST/RELOAD/APPEND/KEEP).""" + if status in (LoadStatus.FIRST, LoadStatus.RELOAD): + QgsProject.instance().addMapLayer(layer) + apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") + iface.mapCanvas().refresh() + self.setze_haken(self.haken_flurst, True) + msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen" + success("Flurstücke", f"Layer wurde {msg}.", duration=5) + + elif status == LoadStatus.KEEP: + self.setze_haken(self.haken_flurst, True) + info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4) + + elif status == LoadStatus.APPEND: + self.setze_haken(self.haken_flurst, True) + iface.mapCanvas().refresh() + success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5) + def handle_flurst_alkis(self) -> None: """Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln.""" self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden") @@ -401,25 +413,7 @@ class WorkingTab(QWidget): layer, status = flurstueck_alkis(self) if not layer or not layer.isValid(): return - - if status in (LoadStatus.FIRST, LoadStatus.RELOAD): - QgsProject.instance().addMapLayer(layer) - # iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert - apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") - iface.mapCanvas().refresh() - self.setze_haken(self.haken_flurst, True) - msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen" - success("Flurstücke", f"Layer wurde {msg}.", duration=5) - - elif status == LoadStatus.KEEP: - self.setze_haken(self.haken_flurst, True) - info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4) - - elif status == LoadStatus.APPEND: - self.setze_haken(self.haken_flurst, True) - iface.mapCanvas().refresh() - success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5) - + self._uebernimm_flurstuecke_ergebnis(layer, status) finally: self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden") @@ -493,79 +487,20 @@ class WorkingTab(QWidget): ) return - # Prüfen ob bereits ein Flurstücke-Layer vorhanden → 3-Knopf-Dialog - existing_flst = project.mapLayersByName(FLST_LAYER_NAME) - if existing_flst: - msg_box = QMessageBox(self) - msg_box.setWindowTitle("Layer bereits vorhanden") - msg_box.setText( - f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?" - ) - btn_ueberschreiben = msg_box.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole) - btn_anhaengen = msg_box.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole) - btn_abbrechen = msg_box.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole) - msg_box.setDefaultButton(btn_abbrechen) - msg_box.exec() - clicked = msg_box.clickedButton() - if clicked is btn_abbrechen: - return - if clicked is btn_anhaengen: - temp_layer = lade_flurstuecke_per_gemarkung(vorgabe_schluessel) - if not temp_layer or not temp_layer.isValid() or temp_layer.featureCount() == 0: - error( - "Flurstücke", - f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) " - f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.", - duration=0, - ) - return - 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() - iface.mapCanvas().refresh() - self.setze_haken(self.haken_flurst, True) - success( - "Flurstücke", - f"{len(neue_feats)} neue Features wurden an den vorhandenen Layer angehängt.", - duration=5, - ) - return - # else: Überschreiben → fall-through zum flurstueck_alkis(ohne_dialog=True)-Aufruf - - # Flurstücke direkt laden (kein Dialog) + # Flurstücke laden – Schlüssel bereits bekannt, aber bei bestehendem Layer + # trotzdem interaktiv nachfragen (Überschreiben/Anhängen/Abbrechen, inkl. Dedup) layer, status = flurstueck_alkis( self, vorgabe_schluessel=vorgabe_schluessel, vorgabe_namen=vorgabe_namen, - ohne_dialog=True, + ohne_schluessel_abfrage=True, ) if not layer or not layer.isValid(): - error( - "Flurstücke", - f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) " - f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.", - duration=0, - ) + # flurstueck_alkis() hat bei Abbruch/Fehler bereits selbst eine Meldung gezeigt. return - if status in (LoadStatus.FIRST, LoadStatus.RELOAD): - project.addMapLayer(layer) - apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") - iface.mapCanvas().refresh() - self.setze_haken(self.haken_flurst, True) - msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen" - success("Flurstücke", f"Layer wurde {msg}.", duration=5) - - elif status == LoadStatus.KEEP: - self.setze_haken(self.haken_flurst, True) - info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4) + self._uebernimm_flurstuecke_ergebnis(layer, status) finally: self._set_busy(self.btn_flurst_aus_gemarkungen, False, "aus gewählten Gemarkungen laden")