diff --git a/changelog.txt b/changelog.txt index 41aaad1..1011734 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- Bezeichner-Bereinigung (Layernamen) um Bindestrich ergänzt und in eine zentrale Funktion `sanitize_bezeichner` (`functions/text_wrapper.py`) ausgelagert, damit Umlaute/Sonderzeichen nicht mehr pro Aufrufstelle vergessen werden (#42) - Landkreis-Dropdown (statt Freitext) in den Projekteigenschaften ergänzt, inkl. Landkreiskennzahl als eigene Projektvariable - Landkreisliste liegt jetzt als JSON (sn_basis/data/landkreise.json) statt hartcodiert vor, damit auch andere Module und Nicht-Entwickler sie pflegen können - Auswahldialog für mehrere BauRaumOderBodenordnungsrecht-Treffer auf Mehrfachauswahl umgestellt (mehrere Objekte gemeinsam laden/verschmelzen) diff --git a/functions/__init__.py b/functions/__init__.py index 485334a..9180e6a 100644 --- a/functions/__init__.py +++ b/functions/__init__.py @@ -22,6 +22,7 @@ from .ly_metadata_wrapper import ( from .ly_style_wrapper import apply_style from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden from .laenderkennung import get_laenderkennung, LAENDERKENNUNGEN +from .text_wrapper import sanitize_bezeichner from .message_wrapper import ( _get_message_bar, diff --git a/functions/text_wrapper.py b/functions/text_wrapper.py new file mode 100644 index 0000000..03e93a4 --- /dev/null +++ b/functions/text_wrapper.py @@ -0,0 +1,28 @@ +""" +sn_basis/functions/text_wrapper.py – Bezeichner-Hilfsfunktionen. + +Zentrale Stelle, um Freitext (z. B. ein Thema oder ein Schriftfeldinhalt) in einen +technischen Bezeichner (Layername, Dateiname) umzuwandeln. Nicht pro Aufrufstelle +ein eigenes Regex schreiben – Umlaute, Unterstrich und Bindestrich werden hier an +einer Stelle gepflegt (siehe docs/wissen/fallstricke.md, Issue Plugin_SN_Basis #42). +""" + +import re + +# Buchstaben, Ziffern, deutsche Umlaute/ß, Unterstrich und Bindestrich gelten immer +# als zulässig. Wurde in der Vergangenheit wiederholt vergessen, wenn ein neues +# Regex direkt im Aufrufer entstand statt diese Funktion zu nutzen. +_BEZEICHNER_ERLAUBT = r"A-Za-z0-9_\-ÄäÖöÜüß" + + +def sanitize_bezeichner(text: str, ersatz: str = "_") -> str: + """Wandelt `text` in einen technischen Bezeichner um. + + Alle Zeichen außerhalb von Buchstaben/Ziffern/Umlauten/ß/Unterstrich/Bindestrich + werden durch `ersatz` ersetzt, anschließend werden führende und folgende + Vorkommen von `ersatz` entfernt. Gedacht für Layer- und Dateinamen – nicht für + Freitext in Schriftfeldern oder der Legende, wo auch andere Zeichen sichtbar + bleiben sollen. + """ + bereinigt = re.sub(rf"[^{_BEZEICHNER_ERLAUBT}]+", ersatz, text) + return bereinigt.strip(ersatz) diff --git a/modules/Datenschreiber.py b/modules/Datenschreiber.py index aa91308..b25551b 100644 --- a/modules/Datenschreiber.py +++ b/modules/Datenschreiber.py @@ -30,13 +30,13 @@ from __future__ import annotations from typing import Any, Dict, List, Optional import os import json -import re import datetime import sqlite3 from sn_basis.functions import qgiscore_wrapper as qgiscore from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists +from sn_basis.functions.text_wrapper import sanitize_bezeichner from sn_basis.modules.pruef_ergebnis import pruef_ergebnis @@ -209,7 +209,7 @@ class Datenschreiber: continue layer_name_raw = thema or str(ident) - layer_name = re.sub(r"[^A-Za-z0-9_ÄäÖöÜüß]+", "_", layer_name_raw).strip("_") + layer_name = sanitize_bezeichner(layer_name_raw) if not layer_name: layer_name = f"layer_{ident}" diff --git a/tests/test_text_wrapper.py b/tests/test_text_wrapper.py new file mode 100644 index 0000000..ed917da --- /dev/null +++ b/tests/test_text_wrapper.py @@ -0,0 +1,43 @@ +"""sn_basis/tests/test_text_wrapper.py + +Unit-Tests für :mod:`sn_basis.functions.text_wrapper`. +""" + +import unittest + +from sn_basis.functions.text_wrapper import sanitize_bezeichner + + +class TestSanitizeBezeichner(unittest.TestCase): + def test_umlaute_bleiben_erhalten(self): + """Prüft, dass Umlaute und ß nicht durch den Ersatz entfernt werden.""" + self.assertEqual(sanitize_bezeichner("Grünfläche"), "Grünfläche") + self.assertEqual(sanitize_bezeichner("Straße"), "Straße") + + def test_bindestrich_bleibt_erhalten(self): + """Prüft, dass ein Bindestrich im Bezeichner erhalten bleibt (Issue #42).""" + self.assertEqual(sanitize_bezeichner("Wege-Plan"), "Wege-Plan") + + def test_unterstrich_bleibt_erhalten(self): + """Prüft, dass ein Unterstrich im Bezeichner erhalten bleibt.""" + self.assertEqual(sanitize_bezeichner("thema_flur_1"), "thema_flur_1") + + def test_leerzeichen_wird_durch_ersatz_zusammengefasst(self): + """Prüft, dass Leerzeichen (kein zulässiges Zeichen) zu einem Ersatzzeichen werden.""" + self.assertEqual(sanitize_bezeichner("Flurstücke Gemarkung"), "Flurstücke_Gemarkung") + + def test_fuehrender_und_folgender_ersatz_wird_entfernt(self): + """Prüft, dass ein Ersatzzeichen am Anfang oder Ende des Ergebnisses entfernt wird.""" + self.assertEqual(sanitize_bezeichner("!Thema!"), "Thema") + + def test_eigenes_ersatzzeichen_wird_verwendet(self): + """Prüft, dass ein abweichendes Ersatzzeichen anstelle von Unterstrich genutzt wird.""" + self.assertEqual(sanitize_bezeichner("a b", ersatz="-"), "a-b") + + def test_nur_unzulaessige_zeichen_ergibt_leeren_string(self): + """Prüft, dass ein Bezeichner ganz aus unzulässigen Zeichen zu einem leeren String wird.""" + self.assertEqual(sanitize_bezeichner("!!!"), "") + + +if __name__ == "__main__": + unittest.main()