Fassung 0.7.0: Landkreis-Linkliste, oFB liest nur, Untergruppen, 403 meldet nicht ab

- Auswahl über GET /tgen/auswahl (Manager seit 06.10.2026): oben die eigenen Landkreise
  („… — Linkliste des Landkreises“), dann die Verfahren; als oFB die Verfahren des Landkreises
  mit „(nur lesen)“. Älterer Server ohne die Route: Rückfall auf /tgen.
- Landkreis gewählt: „Liste holen“ lädt die Linkliste des ganzen Landkreises; „Layer laden“
  erklärt, dass es für einen Landkreis keine Verfahrenslayer gibt.
- 401 und 403 getrennt: nur 401 (AuthError) meldet ab, 403 (KeinZugriff) zeigt eine Meldung.
  Bis 0.6.0 verwarf jedes 403 den gespeicherten API-Key.
- OGC-Sammlungen mit vln:schreibbar: false werden schreibgeschützt geladen; Meldungen für Stil
  und Verkehrszeichen nennen „nur lesen“ als Grund.
- Linkliste: Gruppe › Untergruppe › Layer im Ebenenbaum (linkliste.zielgruppe), Untergruppe
  auch in der Dienstliste.
- README: Abschnitt „Geodienste aus der Linkliste“; Tests ohne Netz:
  tools/headless_linkliste_test.py (neu), tools/headless_panel_test.py erweitert — beide grün
  mit QGIS 4.2 offscreen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
KAI
2026-10-06 22:44:36 +02:00
co-authored by Claude Opus 5.5
parent c84bd6e378
commit a56bf7c397
10 changed files with 331 additions and 69 deletions
+75 -24
View File
@@ -1,8 +1,4 @@
"""HTTP-Client für die VLN-Manager-API (Karten-Plugin map.php + Stammdaten).
API-Vertrag laut Doku im Wissensdatenbank-Vault
("API.md", "API – Karten (map.php).md", "API – Schnittstellen (Referenz).md",
Stand 2026-06-08):
"""HTTP-Client für die VLN-Manager-API.
Basis-URL: https://api.flurneuordnung-sachsen.de/v2
@@ -11,15 +7,15 @@ Stand 2026-06-08):
Antwort: {"data": {"userauth": "<url_token>", "id": ...}}
Das userauth-Token ist der API-Key (= PERSON.url_token).
GET /tgen -> Teilnehmergemeinschaften (Verfahren)
GET /tgen/auswahl -> {"verfahren": [{vkz, name, zugriff}], "aemter": [{vkz, name}]}
Eigene Verfahren ("voll") und — als oFB — die Verfahren der eigenen Landkreise
("lesen"), dazu die Landkreise selbst (Manager seit 06.10.2026; Rückfall: GET /tgen).
GET /ogc/collections?vkz=… -> Sammlungen (s. ogc_layers.py), ``vln:schreibbar`` je Sammlung
GET /linkliste?vkz=… -> Geodienste eines Verfahrens oder Landkreises (s. linkliste.py)
GET /maps/<layer>/{vkz} -> GeoJSON FeatureCollection (EPSG:25833)
PUT /maps/<layer>/{vkz} <- GeoJSON (ersetzt den Layer dieser VKZ)
Antwort: {"data": {"vkz", "layer", "art", "speicher_id"}, "status": "ok"}
Layer: umringe, p41, st, kas, we
Auth nach Login über den Header "X-API-Key".
Fehlerformat: RFC 7807 (application/problem+json).
Auth über den Header "X-API-Key". Fehlerformat: RFC 7807 (application/problem+json).
401 = nicht (mehr) angemeldet (AuthError), 403 = angemeldet, aber nicht berechtigt
(KeinZugriff) — nur 401 führt zum Abmelden.
Es wird QgsBlockingNetworkRequest verwendet, damit Proxy- und
Zertifikatseinstellungen aus QGIS automatisch greifen.
@@ -36,11 +32,38 @@ DEFAULT_BASE_URL = "https://api.flurneuordnung-sachsen.de/v2"
class ApiError(Exception):
"""Fehler bei der Kommunikation mit der Karten-API."""
"""Fehler bei der Kommunikation mit der Karten-API.
:ivar status: HTTP-Status, wenn der Server geantwortet hat (sonst ``None``).
"""
def __init__(self, nachricht="", status=None):
super().__init__(nachricht)
self.status = status
class AuthError(ApiError):
"""API-Key fehlt, ist abgelaufen oder hat keine Berechtigung (401/403)."""
"""Nicht (mehr) angemeldet: API-Key fehlt oder ist abgelaufen (401) — dann neu anmelden."""
class KeinZugriff(ApiError):
"""Angemeldet, aber für diese Aktion nicht berechtigt (403).
Kein Grund abzumelden: eine oFB liest z. B. die Verfahren ihres Landkreises, darf dort aber
nicht schreiben. Bis 0.6.0 meldete jedes 403 ab und verwarf den gespeicherten API-Key.
"""
def fehler_fuer_status(status, text):
"""Die passende Ausnahme für einen HTTP-Fehlerstatus.
:returns: ``AuthError`` (401), ``KeinZugriff`` (403), sonst ``ApiError`` — mit ``status``.
"""
if status == 401:
return AuthError(text, status)
if status == 403:
return KeinZugriff(text, status)
return ApiError(text, status)
class KartenApiClient:
@@ -83,9 +106,34 @@ class KartenApiClient:
# Verfahren (Teilnehmergemeinschaften)
# ------------------------------------------------------------------
def get_auswahl(self):
"""Verfahren und Landkreise zur Auswahl — ``GET /tgen/auswahl``.
:returns: ``{"verfahren": [{vkz, name, zugriff}], "aemter": [{vkz, name}]}`` mit ``vkz``
als Text und ``zugriff`` ``"voll"`` oder ``"lesen"`` (eine oFB liest die Verfahren
ihres Landkreises). Ein älterer Server kennt die Route nicht (400/404) — dann
Rückfall auf :meth:`get_verfahren`, alles ``"voll"``, keine Landkreise.
"""
try:
data = self._request("GET", "/tgen/auswahl")
except ApiError as exc:
if getattr(exc, "status", None) in (400, 404):
return {"verfahren": [dict(v, zugriff="voll") for v in self.get_verfahren()],
"aemter": []}
raise
if not isinstance(data, dict) or not isinstance(data.get("verfahren"), list):
raise ApiError("Antwort von /tgen/auswahl hat ein unerwartetes Format: %s"
% json.dumps(data)[:200])
verfahren = [{"vkz": str(v.get("vkz")), "name": str(v.get("name") or ""),
"zugriff": "lesen" if v.get("zugriff") == "lesen" else "voll"}
for v in data["verfahren"] if isinstance(v, dict) and v.get("vkz")]
aemter = [{"vkz": str(a.get("vkz")), "name": str(a.get("name") or "")}
for a in (data.get("aemter") or []) if isinstance(a, dict) and a.get("vkz")]
return {"verfahren": verfahren, "aemter": aemter}
def get_verfahren(self):
"""Liste der Verfahren (TGs) als [{"vkz": ..., "name": ...}, ...],
sortiert nach VKZ. Quelle: GET /tgen."""
sortiert nach VKZ. Quelle: GET /tgen (Rückfall für ältere Server)."""
data = self._request("GET", "/tgen")
rows = None
@@ -210,19 +258,22 @@ class KartenApiClient:
status = int(status) if status is not None else None
if status in (401, 403):
raise AuthError(
raise fehler_fuer_status(
status,
"Keine Berechtigung für %s %s (HTTP %s)%s"
% (method, path, status, self._error_detail(content))
)
if error != QgsBlockingNetworkRequest.NoError:
raise ApiError(
"%s %s fehlgeschlagen: %s%s"
% (method, path, blocking.errorMessage(), self._error_detail(content))
% (method, path, status, self._error_detail(content)),
)
if status is not None and status >= 400:
raise ApiError(
"%s %s lieferte HTTP %s%s"
% (method, path, status, self._error_detail(content))
% (method, path, status, self._error_detail(content)),
status,
)
if error != QgsBlockingNetworkRequest.NoError:
raise ApiError(
"%s %s fehlgeschlagen: %s%s"
% (method, path, blocking.errorMessage(), self._error_detail(content)),
status,
)
if not content:
+32 -11
View File
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
"""Linkliste der Geodienste — die Dienste eines Verfahrens als QGIS-Layer.
Die Liste kommt vom VLN-Manager (``GET /v2/linkliste?vkz=…``) und ist dort vierstufig gepflegt:
landesweit, Landkreis, Team und Verfahren erben aufeinander. Das Plugin bekommt bereits die
**aufgelöste** Liste — es muss nichts zusammenrechnen, sondern nur laden. Je Eintrag steht dabei,
woher er stammt (``herkunft``), damit man in QGIS sieht, ob ein Dienst landesweit gilt oder nur
für dieses eine Verfahren eingetragen wurde.
Die Liste kommt vom VLN-Manager (``GET /v2/linkliste?vkz=…``, ``vkz`` eines Verfahrens oder
— seit 0.7.0 — eines Landkreises). Gepflegt wird sie landesweit und vom Landkreis (oFB); Team
und Teilnehmergemeinschaft sehen sie nur (Erik, 06.10.2026) — an- und abgehakt wird hier im
Plugin. Das Plugin bekommt bereits die **aufgelöste** Liste — es muss nichts zusammenrechnen,
sondern nur laden. Je Eintrag steht dabei, woher er stammt (``herkunft``), und — wenn gesetzt —
seine ``untergruppe``: der Ebenenbaum wird dann Gruppe › Untergruppe › Layer.
Vier Anbieter kommen vor, jeder braucht in QGIS eine andere Datenquelle:
@@ -155,6 +156,29 @@ def layer_bauen(eintrag, zeit_ms=20000):
return None
def zielgruppe(oben, knoten, gruppe_name, untergruppe=None):
"""Die Ebenengruppe für einen Dienst: Gruppe, darunter — falls gesetzt — die Untergruppe.
Bestehende Gruppen werden wiederverwendet (``findGroup``), damit ein zweiter Ladelauf keine
doppelten Knoten anlegt. Ohne Gruppe gilt „Sonstiges“, wie bisher.
:param oben: ``QgsLayerTreeGroup`` „Geodienste <vkz> <name>“.
:param knoten: Zwischenspeicher ``{(gruppe, untergruppe): QgsLayerTreeGroup}`` des Ladelaufs.
:returns: die Gruppe, in die der Layer gehört.
"""
gruppe_name = str(gruppe_name or "Sonstiges")
untergruppe = str(untergruppe or "").strip()
if (gruppe_name, "") not in knoten:
knoten[(gruppe_name, "")] = oben.findGroup(gruppe_name) or oben.addGroup(gruppe_name)
gruppe = knoten[(gruppe_name, "")]
if not untergruppe:
return gruppe
if (gruppe_name, untergruppe) not in knoten:
knoten[(gruppe_name, untergruppe)] = (gruppe.findGroup(untergruppe)
or gruppe.addGroup(untergruppe))
return knoten[(gruppe_name, untergruppe)]
def dienste_hinzufuegen(iface, base_url, vkz, vkz_name, dienste, api_key=None, nur_ids=None):
"""Die Dienste als Layer in die Gruppe „Geodienste“ hängen — **schrittweise**.
@@ -171,7 +195,7 @@ def dienste_hinzufuegen(iface, base_url, vkz, vkz_name, dienste, api_key=None, n
wurzel = projekt.layerTreeRoot()
oben_name = ("%s %s %s" % (GRUPPE, vkz, vkz_name or "")).strip()
oben = wurzel.findGroup(oben_name) or wurzel.insertGroup(0, oben_name)
untergruppen = {}
knoten = {}
balken = QProgressBar()
balken.setMaximum(max(1, len(auswahl)))
@@ -196,16 +220,13 @@ def dienste_hinzufuegen(iface, base_url, vkz, vkz_name, dienste, api_key=None, n
return
eintrag = auswahl[zustand["i"]]
zustand["i"] += 1
gruppe_name = str(eintrag.get("gruppe_name") or "Sonstiges")
if gruppe_name not in untergruppen:
untergruppen[gruppe_name] = (oben.findGroup(gruppe_name)
or oben.addGroup(gruppe_name))
ziel = zielgruppe(oben, knoten, eintrag.get("gruppe_name"), eintrag.get("untergruppe"))
layer = layer_bauen(eintrag)
if layer is not None and layer.isValid():
if eintrag.get("stil"):
stil_anwenden(layer, base_url, eintrag.get("id"), api_key)
projekt.addMapLayer(layer, False)
untergruppen[gruppe_name].addLayer(layer)
ziel.addLayer(layer)
zustand["geladen"] += 1
else:
zustand["fehler"].append(str(eintrag.get("name") or "?"))
+3 -2
View File
@@ -5,7 +5,7 @@ qgisMaximumVersion=4.99
supportsQt6=True
description=Karten des VLN Managers in QGIS: OGC API Features, alle Layer editierbar — von der Vorerhebung über das Maßnahmenblatt bis zu Widmungen und Verkehrszeichen.
about=Anmeldung mit der Manager-Kennung (der API-Key wird gemerkt), Verfahrenslayer über die OGC API laden und bearbeiten (jede Änderung geht einzeln zurück), Geodienste aus der Linkliste, Stile vom Server. Verkehrszeichen und Widmungen lassen sich wie im Web setzen: Schild aus der Palette wählen, erster Klick Standort, zweiter Klick Blickrichtung; die Schildgrafiken kommen von der API und liegen danach lokal.
version=0.6.0
version=0.7.0
author=Erik
email=phpwelt@gmail.com
tracker=https://entwicklung.flurneuordnung-sachsen.de/VLN_SN/API_Karte_QGISDemo/issues
@@ -15,5 +15,6 @@ category=Web
icon=
experimental=True
deprecated=False
changelog=0.6.0 Widmungen und Verkehrszeichen: eigene Layergruppe, Schildgrafiken vom Server (lokal abgelegt), Palette mit Suche, Werkzeuge „Schild setzen" und „Zusatzzeichen"; Seitenleiste rollt statt das QGIS-Fenster zu strecken (Issue #1).
changelog=0.7.0 Linkliste mit Untergruppen (Gruppe › Untergruppe › Layer) und für den ganzen Landkreis; Auswahl über /tgen/auswahl: als oFB die Verfahren des Landkreises „nur lesen“, Layer dort schreibgeschützt; ein 403 meldet nicht mehr ab.
0.6.0 Widmungen und Verkehrszeichen: eigene Layergruppe, Schildgrafiken vom Server (lokal abgelegt), Palette mit Suche, Werkzeuge „Schild setzen" und „Zusatzzeichen"; Seitenleiste rollt statt das QGIS-Fenster zu strecken (Issue #1).
0.5.0 Geodienste aus der Linkliste, Stile speichern, Seitenleiste statt Werkzeugleiste.
+12 -4
View File
@@ -14,7 +14,9 @@ Dieses Modul zeigt den Weg aus PyQGIS:
``X-API-Key`` mitschickt. Das Passwort landet nie in QGIS. Alternative ohne Plugin:
Methode ``Basic`` mit dem Manager-Login (E-Mail + Passwort) — die API nimmt beides.
2. **Sammlungen** des Verfahrens holen (``GET /ogc/collections?vkz=…``) — die API listet nur, was
der Nutzer sehen darf (Vorsitz/Kassenbetreuer/Stellvertreter der TG; Verband alles).
der Nutzer sehen darf (Vorsitz/Kassenbetreuer/Stellvertreter der TG; Verband alles; seit
06.10.2026 auch die oFB des Landkreises, aber nur lesend). ``vln:schreibbar: false`` an einer
Sammlung heißt: der Layer wird schreibgeschützt geladen.
3. Je Sammlung einen ``QgsVectorLayer(uri, titel, "OAPIF")`` in einer Layer-Gruppe anlegen.
Sammlungs-IDs: ``<layer>-<vkz>`` (umringe, p41, st, kas, we, ngg, biotop = jüngster Stand wie
@@ -158,7 +160,8 @@ def stil_speichern(layer, base_url, cid, api_key=None, authcfg=None, vorlage=Fal
"""Den aktuellen Layer-Stil als QML an den Server schicken (PUT …/style.qml).
:param vorlage: als Vorlage der Art speichern — gilt dann fuer alle Verfahren.
:returns: ``(True, Text)`` oder ``(False, Meldung)``; 403 heisst: Gruppe MODUL_KARTENSTIL fehlt.
:returns: ``(True, Text)`` oder ``(False, Meldung)``; 403 heisst: im Verfahren nur lesend
(oFB) oder — bei einer Vorlage — die Gruppe MODUL_KARTENSTIL fehlt.
"""
pfad = os.path.join(tempfile.gettempdir(), "vln_upload_%s.qml" % cid.replace("*", "_"))
meldung = layer.saveNamedStyle(pfad)
@@ -180,8 +183,9 @@ def stil_speichern(layer, base_url, cid, api_key=None, authcfg=None, vorlage=Fal
if code == 200:
return True, "%d Zeichen gespeichert%s" % (len(rumpf), " (Vorlage der Art)" if vorlage else "")
if code == 403:
return False, ("Keine Berechtigung: Stile darf nur speichern, wer in der Gruppe "
"MODUL_KARTENSTIL ist (oder Verbandsmitarbeiter).")
return False, ("Keine Berechtigung: den Stil eines Verfahrens speichert nur, wer dort "
"schreiben darf (nicht bei „nur lesen“); eine Vorlage für alle Verfahren "
"nur die Gruppe MODUL_KARTENSTIL oder der Verband.")
if code in (401, None):
raise AuthError("Anmeldung abgelaufen")
return False, "Server antwortete mit HTTP %s: %s" % (code, bytes(antwort.content())[:200].decode("utf-8", "replace"))
@@ -345,6 +349,10 @@ def layer_hinzufuegen(iface, base_url, vkz, vkz_name, collections, authcfg=None,
layer = QgsVectorLayer(layer_uri(base_url, cid, authcfg, coll.get("storageCrs"), api_key),
titel, "OAPIF")
if layer.isValid():
if coll.get("vln:schreibbar") is False:
# Nur lesen (oFB des Landkreises, Manager seit 06.10.2026): QGIS soll kein
# Bearbeiten anbieten, das erst beim Speichern am Server scheitert.
layer.setReadOnly(True)
# Verkehrszeichen brauchen ihre Grafiken, BEVOR der Stil greift — sonst zeigt der
# Ausdruck auf eine Datei, die es lokal nicht gibt, und QGIS malt ein Fragezeichen.
if cid.startswith(("widmung-", "blattzeichen-")) and client is not None:
+42 -15
View File
@@ -34,6 +34,18 @@ PANEL_TITEL = "VLN Karten"
PANEL_OBJEKT = "VlnKartenPanel"
def ist_landkreis(vkz):
"""Steht ``vkz`` für einen Landkreis (oFB, 10–100) statt für ein Verfahren?
Landkreise stehen seit 0.7.0 oben in der Auswahl — gewählt lädt man dort nur die
Linkliste des Landkreises, keine Verfahrenslayer.
"""
try:
return 10 <= int(vkz) <= 100
except (TypeError, ValueError):
return False
def _klein(widget):
"""Hinweistexte eine Stufe kleiner und gedämpft — wie in QGIS-Panels üblich.
@@ -237,15 +249,17 @@ class VlnPanel(QDockWidget):
knopf.blockSignals(False)
def _bau_dienste(self):
"""Die Linkliste des gewählten Verfahrens — vierstufig gepflegt, hier nur geladen.
"""Die Linkliste des gewählten Verfahrens oder Landkreises — gepflegt landesweit und vom
Landkreis, hier nur geladen.
Die Liste kommt fertig aufgelöst vom Server (landesweit, Landkreis, Team, Verfahren);
je Zeile steht die Herkunft dabei, damit man sieht, wo ein Dienst herkommt.
Die Liste kommt fertig aufgelöst vom Server; je Zeile stehen Gruppe, Untergruppe und
Herkunft dabei, damit man sieht, wo ein Dienst herkommt.
"""
kasten = QGroupBox("Geodienste (Linkliste)")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
lage.addWidget(_klein(QLabel("WMS-, WFS- und ArcGIS-Dienste für dieses Verfahren.")))
lage.addWidget(_klein(QLabel("WMS-, WFS- und ArcGIS-Dienste für dieses Verfahren "
"oder den gewählten Landkreis.")))
self.dienste_liste = QListWidget()
# Bewusst niedrig: die Mindesthoehe aller Kaesten zusammen ist die Mindesthoehe des
# Docks (s. Rollflaeche im Konstruktor). 140 px hier machten die Seitenleiste auf
@@ -276,11 +290,14 @@ class VlnPanel(QDockWidget):
self.dienste_liste.item(i).setCheckState(ziel)
def set_dienste(self, dienste):
"""Die geholte Liste anzeigen: Name, Gruppe und Herkunft, alle zunächst angehakt."""
"""Die geholte Liste anzeigen: Name, Gruppe › Untergruppe und Herkunft, alle zunächst
angehakt."""
self.dienste_liste.clear()
for d in dienste or []:
text = "%s · %s · %s" % (d.get("name"), d.get("gruppe_name") or "",
d.get("herkunft_text") or "")
gruppe = d.get("gruppe_name") or ""
if d.get("untergruppe"):
gruppe = "%s › %s" % (gruppe, d.get("untergruppe"))
text = "%s · %s · %s" % (d.get("name"), gruppe, d.get("herkunft_text") or "")
if d.get("stil"):
text += " · mit Stil"
eintrag = QListWidgetItem(text)
@@ -304,8 +321,8 @@ class VlnPanel(QDockWidget):
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
self.stil_knopf = QPushButton("Stil des aktiven Layers speichern")
self.stil_knopf.setToolTip("Den Stil dieses Verfahrens darf jeder speichern, der das "
"Verfahren sieht")
self.stil_knopf.setToolTip("Den Stil dieses Verfahrens darf speichern, wer im Verfahren "
"schreiben darf (nicht bei „nur lesen“)")
self.stil_knopf.clicked.connect(lambda: self._ruf("save_style"))
self.vorlage_haken = QCheckBox("als Vorlage für alle Verfahren")
self.vorlage_haken.setToolTip("Gilt dann für diese Art in jedem Verfahren, z. B. für "
@@ -344,12 +361,22 @@ class VlnPanel(QDockWidget):
self.dienste_liste.clear()
self._stumm = False
def set_verfahren(self, verfahren):
"""``verfahren``: Liste von ``{"vkz":…, "name":…}`` aus der API."""
self._verfahren = [
("%s — %s" % (v["vkz"], v["name"]) if v.get("name") else str(v["vkz"]), v["vkz"])
for v in verfahren
]
def set_verfahren(self, verfahren, aemter=()):
"""Die Auswahl füllen.
:param verfahren: ``[{"vkz", "name", "zugriff"}]`` aus der API; ``zugriff == "lesen"``
bekommt den Zusatz „nur lesen“ (oFB: Verfahren des eigenen Landkreises).
:param aemter: ``[{"vkz", "name"}]`` — die eigenen Landkreise stehen oben; gewählt lädt
man dort nur die Linkliste des Landkreises.
"""
eintraege = [("%s — Linkliste des Landkreises" % (a.get("name") or a["vkz"]), a["vkz"])
for a in aemter]
for v in verfahren:
text = "%s — %s" % (v["vkz"], v["name"]) if v.get("name") else str(v["vkz"])
if v.get("zugriff") == "lesen":
text += " (nur lesen)"
eintraege.append((text, v["vkz"]))
self._verfahren = eintraege
self._liste_fuellen()
def set_status(self, text):
+23 -9
View File
@@ -17,7 +17,7 @@ from qgis.PyQt.QtWidgets import QAction, QDialog, QMessageBox
from qgis.core import Qgis, QgsDataSourceUri, QgsProject
from .api_client import ApiError, AuthError, KartenApiClient
from .panel import VlnPanel
from .panel import VlnPanel, ist_landkreis
from .login_dialog import LoginDialog, SETTINGS_GROUP
from . import linkliste
from . import ogc_layers
@@ -143,9 +143,10 @@ class VlnKartenPlugin:
self.populate_verfahren()
def populate_verfahren(self):
"""Füllt die Auswahlliste mit allen Verfahren aus GET /tgen."""
"""Füllt die Auswahlliste aus ``GET /tgen/auswahl``: eigene Verfahren, als oFB die
Verfahren des Landkreises (nur lesen) und die Landkreise selbst (für die Linkliste)."""
try:
verfahren = self.client.get_verfahren()
auswahl = self.client.get_auswahl()
except AuthError:
self._handle_session_expired()
return
@@ -159,8 +160,15 @@ class VlnKartenPlugin:
return
self.panel.set_angemeldet(True, self.client.mail or "API-Key")
self.panel.set_verfahren(verfahren)
self.panel.set_status("%d Verfahren geladen." % len(verfahren))
verfahren, aemter = auswahl["verfahren"], auswahl["aemter"]
self.panel.set_verfahren(verfahren, aemter)
text = "%d Verfahren geladen" % len(verfahren)
lesend = sum(1 for v in verfahren if v.get("zugriff") == "lesen")
if lesend:
text += " (davon %d nur lesen)" % lesend
if aemter:
text += ", %d Landkreis(e) für die Linkliste" % len(aemter)
self.panel.set_status(text + ".")
self.restore_project_selection()
def _handle_session_expired(self):
@@ -229,6 +237,12 @@ class VlnKartenPlugin:
if not vkz:
self._show_error("Kein Verfahren", "Bitte zuerst ein Verfahren auswählen.")
return
if ist_landkreis(vkz):
self._show_error("Landkreis gewählt",
"Für einen Landkreis gibt es keine Verfahrenslayer — bitte ein "
"Verfahren wählen. Die Linkliste des Landkreises holen Sie unter "
"„Geodienste“.")
return
authcfg = ogc_layers.authcfg_sicherstellen(self.client.api_key, SETTINGS_GROUP)
if not authcfg:
self.iface.messageBar().pushMessage(
@@ -363,11 +377,11 @@ class VlnKartenPlugin:
self.panel.set_status("%d von %d Layern gespeichert." % (len(gespeichert), len(offen)))
def run_dienste_holen(self):
"""Die Linkliste des gewählten Verfahrens holen und in der Seitenleiste anzeigen.
"""Die Linkliste des gewählten Verfahrens oder Landkreises holen und anzeigen.
Der Server liefert sie bereits aufgelöst: landesweit, Landkreis, Team und Verfahren
sind zusammengeführt, geerbte Einträge tragen ihre Herkunft. Das Plugin rechnet nichts
nach, es zeigt und lädt.
Der Server liefert sie bereits aufgelöst (landesweit und Landkreis), geerbte Einträge
tragen ihre Herkunft. Das Plugin rechnet nichts nach, es zeigt und lädt. Ein 403 meldet
nicht ab (seit 0.7.0), sondern zeigt nur, dass hier keine Berechtigung besteht.
"""
if not self._ensure_logged_in():
return
+2 -2
View File
@@ -226,7 +226,7 @@ class SchildSetzen(QgsMapTool):
if layer is None:
return
if not layer.isEditable() and not layer.startEditing():
self._melden("Der Layer lässt sich nicht bearbeiten.", True)
self._melden("Der Layer lässt sich nicht bearbeiten — bei „nur lesen“ fehlt das Schreibrecht im Verfahren.", True)
return
namen = [f.name() for f in layer.fields()]
f = QgsFeature(layer.fields())
@@ -293,7 +293,7 @@ class ZusatzAnhaengen(QgsMapTool):
self._melden("Schon vorhanden oder schon zwei Zusatzzeichen — nichts geändert.", True)
return
if not layer.isEditable() and not layer.startEditing():
self._melden("Der Layer lässt sich nicht bearbeiten.", True)
self._melden("Der Layer lässt sich nicht bearbeiten — bei „nur lesen“ fehlt das Schreibrecht im Verfahren.", True)
return
layer.changeAttributeValue(treffer.id(), layer.fields().indexOf("zusatz"),
", ".join(neu))