From 5667259bff46ac5ed57b4c503a73e8e2cf3e46b8 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Wed, 6 May 2026 23:29:12 +0200 Subject: [PATCH] feat(#6 Phase 11): Bundesrats-Drucksachen erkennen + markieren + ablehnen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DIP-Drucksachen mit `herausgeber: 'BR'` (Bundesrat) haben Bundesländer als Antragsteller (z.B. SN, HE) statt Fraktionen. Variante b — explizite Behandlung statt nur ausschließen: - Drucksache-dataclass: neue Felder `is_bundesrat: bool`, `urheber_bundeslaender: list[str]`. Existierende Pfade unberührt. - BundestagAdapter._doc_to_drucksache: liest herausgeber + urheber-Liste, setzt Bundesländer-Codes (bezeichnung wie "SN") in urheber_bundeslaender. fraktionen bleibt leer fuer BR — verhindert dass Stimmverhalten-Aggregate verwirrt werden. - /api/search-landtag liefert is_bundesrat + urheber_bundeslaender im Response. - /api/analyze-drucksache (POST) lehnt BR-Drucksachen mit HTTP 400 + klarer Meldung ab statt crashen. - v2-Search-UI rendert grayen Bundesrat-Sticker mit BL-Codes statt Fraktionen, "Analysieren"-Button durch "nicht unterstuetzt" ersetzt. is_bundesrat_drucksache() in drucksache_typen.py als Format-Helper (N/M/JJ-Pattern) bleibt fuer Cases wo nur die Drucksache-ID ohne Adapter-Metadaten verfuegbar ist. Refs: #6 Co-Authored-By: Claude Opus 4.7 (1M context) --- app/drucksache_typen.py | 22 +++++++++++++ app/main.py | 16 ++++++++++ app/parlamente.py | 34 ++++++++++++++++++--- app/templates/v2/screens/landtag_suche.html | 22 ++++++++++--- 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/app/drucksache_typen.py b/app/drucksache_typen.py index dc9f3fe..f2ec87d 100644 --- a/app/drucksache_typen.py +++ b/app/drucksache_typen.py @@ -128,3 +128,25 @@ def likely_kleine_anfrage_titel(title: str) -> bool: if t.rstrip().endswith("?"): return True return False + + +# Bundesrats-Drucksachen-Format erkennen. +# Bundestag: "21/9954" (Wahlperiode/Nummer, 2 Komponenten). +# Bundesrat: "186/3/26", "186/2/26" (Nummer/Sub/Jahr, 3 Komponenten). +import re as _re + +_BUNDESRAT_PATTERN = _re.compile(r"^\d+/\d+/\d+$") + + +def is_bundesrat_drucksache(drucksache: str) -> bool: + """Erkennt Bundesrats-Drucksachen am 3-Komponenten-Format ``N/M/JJ``. + + Diese Drucksachen finden sich über die DIP-API (Bundestags-Backend + indexiert sie mit), haben aber **keine Fraktionen** als Antragsteller — + sondern Bundesländer. Der GWÖ-Analyzer erwartet Fraktionen und schlägt + bei Bundesrats-Drucksachen fehl. Folge: separate Behandlung im Such- + + Analyse-Pfad. + """ + if not drucksache: + return False + return bool(_BUNDESRAT_PATTERN.match(drucksache.strip())) diff --git a/app/main.py b/app/main.py index ca4fb7a..394f8d0 100644 --- a/app/main.py +++ b/app/main.py @@ -1416,6 +1416,8 @@ async def search_landtag( "bundesland": bundesland, "typ": doc.typ, "typ_normiert": doc.typ_normiert, + "is_bundesrat": doc.is_bundesrat, + "urheber_bundeslaender": doc.urheber_bundeslaender, "gwoeScore": None, "status": "unchecked", }) @@ -1572,6 +1574,20 @@ async def analyze_drucksache( # Get document metadata doc = await adapter.get_document(drucksache) + # Bundesrats-Drucksachen ablehnen — Antragsteller sind Bundesländer, + # GWÖ-Wahlprogramm-Bewertung greift nicht (kein Partei-Bezug). + if doc and getattr(doc, "is_bundesrat", False): + bls = ", ".join(doc.urheber_bundeslaender or []) or "—" + raise HTTPException( + status_code=400, + detail=( + f"Bundesrats-Drucksache (Antragsteller: {bls}) wird derzeit " + f"nicht unterstützt. Die GWÖ-Bewertung erwartet eine " + f"antragstellende Fraktion, Bundesländer-Anträge aus dem " + f"Bundesrat haben aber keine Wahlprogramm-Verbindung." + ), + ) + # #127: Typ-Check — nur abstimmbare Drucksachen analysieren. # Falls der Adapter den Typ nicht richtig setzt (NRW: "Drucksache"), # versuche den Typ aus dem Dokument-Text zu erkennen. diff --git a/app/parlamente.py b/app/parlamente.py index 2f92544..e6c4535 100644 --- a/app/parlamente.py +++ b/app/parlamente.py @@ -23,11 +23,18 @@ class Drucksache: bundesland: str typ: str = "Antrag" # Original-Typ vom Landtag (z.B. "Kleine Anfrage", "Gesetzentwurf") typ_normiert: str = "" # Normierter Typ (wird automatisch gesetzt) + # Bundesrats-Drucksachen (DIP herausgeber=BR): Antragsteller sind + # Bundesländer (z.B. ['SN','HE']) statt Fraktionen. fraktionen bleibt + # leer, damit Stimmverhalten-Aggregate sich nicht verwirren. + is_bundesrat: bool = False + urheber_bundeslaender: list[str] = None # type: ignore[assignment] def __post_init__(self): from .drucksache_typen import normalize_typ if not self.typ_normiert: self.typ_normiert = normalize_typ(self.typ) + if self.urheber_bundeslaender is None: + self.urheber_bundeslaender = [] class ParlamentAdapter(ABC): @@ -2928,15 +2935,30 @@ class BundestagAdapter(ParlamentAdapter): if not pdf_url: return None - # Fraktionen aus urheber-Liste extrahieren. DIP listet sie als - # "Fraktion der AfD" o.ä. — extract_fraktionen kennt das Pattern - # bereits aus den Landtags-Adaptern. + # Bundesrats-Drucksachen erkennen am DIP-Feld `herausgeber: 'BR'`. + # Antragsteller sind dort Bundesländer (z.B. Sachsen, Hessen), + # nicht Bundestags-Fraktionen. + is_bundesrat = (doc.get("herausgeber") or "").upper() == "BR" + urheber_strs: list[str] = [] + urheber_bundeslaender: list[str] = [] for u in (doc.get("urheber") or []): if isinstance(u, dict): urheber_strs.append(u.get("titel") or u.get("bezeichnung") or "") - urheber_combined = ", ".join(filter(None, urheber_strs)) - fraktionen = extract_fraktionen(urheber_combined, bundesland=self.bundesland) + # Bei Bundesrat: bezeichnung ist der BL-Code (SN, HE, NW, ...) + if is_bundesrat: + code = (u.get("bezeichnung") or "").strip().upper() + if code and len(code) <= 4: + urheber_bundeslaender.append(code) + + if is_bundesrat: + # Fraktionen leer lassen — Bundesländer-Antragsteller sind + # in urheber_bundeslaender. Stimmverhalten-Aggregate ignorieren + # diese Drucksachen automatisch (kein fraktionen-Match). + fraktionen: list[str] = [] + else: + urheber_combined = ", ".join(filter(None, urheber_strs)) + fraktionen = extract_fraktionen(urheber_combined, bundesland=self.bundesland) return Drucksache( drucksache=nummer, @@ -2946,6 +2968,8 @@ class BundestagAdapter(ParlamentAdapter): link=pdf_url, bundesland=self.bundesland, typ=doc.get("drucksachetyp", "Antrag"), + is_bundesrat=is_bundesrat, + urheber_bundeslaender=urheber_bundeslaender, ) async def _fetch_page( diff --git a/app/templates/v2/screens/landtag_suche.html b/app/templates/v2/screens/landtag_suche.html index bb39666..994d5ed 100644 --- a/app/templates/v2/screens/landtag_suche.html +++ b/app/templates/v2/screens/landtag_suche.html @@ -273,29 +273,41 @@ function renderRow(item, bl) { var url = item.url || item.link || ''; var done = lsCheckedIds.has(ds); var fraktionen = Array.isArray(item.fraktionen) ? item.fraktionen : []; + var isBundesrat = !!item.is_bundesrat; + var bundeslaender = Array.isArray(item.urheber_bundeslaender) ? item.urheber_bundeslaender : []; var titleHtml = url ? '' + title + '' : title; - var fraktionenHtml = fraktionen.length - ? '
' + var fraktionenHtml = ''; + if (isBundesrat) { + var blsList = bundeslaender.length + ? bundeslaender.map(function(c){ return escHtml(c); }).join(', ') + : '—'; + fraktionenHtml = '
' + + 'Bundesrat · ' + blsList + '' + + '
'; + } else if (fraktionen.length) { + fraktionenHtml = '
' + fraktionen.map(function (f) { return '' + escHtml(f) + ''; }).join('') - + '
' - : ''; + + '
'; + } var actionHtml; if (done) { actionHtml = 'Bewertet → Ansehen'; + } else if (isBundesrat) { + actionHtml = 'nicht unterstützt'; } else if (lsIsAuth) { actionHtml = ''; } else { actionHtml = 'Anmeldung nötig'; } - return '
' + return '
' + '
' + escHtml(ds) + '
' + '
' + '
' + titleHtml + '
'