feat(#6 Phase 11): Bundesrats-Drucksachen erkennen + markieren + ablehnen
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) <noreply@anthropic.com>
This commit is contained in:
parent
03948038c4
commit
5667259bff
@ -128,3 +128,25 @@ def likely_kleine_anfrage_titel(title: str) -> bool:
|
|||||||
if t.rstrip().endswith("?"):
|
if t.rstrip().endswith("?"):
|
||||||
return True
|
return True
|
||||||
return False
|
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()))
|
||||||
|
|||||||
16
app/main.py
16
app/main.py
@ -1416,6 +1416,8 @@ async def search_landtag(
|
|||||||
"bundesland": bundesland,
|
"bundesland": bundesland,
|
||||||
"typ": doc.typ,
|
"typ": doc.typ,
|
||||||
"typ_normiert": doc.typ_normiert,
|
"typ_normiert": doc.typ_normiert,
|
||||||
|
"is_bundesrat": doc.is_bundesrat,
|
||||||
|
"urheber_bundeslaender": doc.urheber_bundeslaender,
|
||||||
"gwoeScore": None,
|
"gwoeScore": None,
|
||||||
"status": "unchecked",
|
"status": "unchecked",
|
||||||
})
|
})
|
||||||
@ -1572,6 +1574,20 @@ async def analyze_drucksache(
|
|||||||
# Get document metadata
|
# Get document metadata
|
||||||
doc = await adapter.get_document(drucksache)
|
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.
|
# #127: Typ-Check — nur abstimmbare Drucksachen analysieren.
|
||||||
# Falls der Adapter den Typ nicht richtig setzt (NRW: "Drucksache"),
|
# Falls der Adapter den Typ nicht richtig setzt (NRW: "Drucksache"),
|
||||||
# versuche den Typ aus dem Dokument-Text zu erkennen.
|
# versuche den Typ aus dem Dokument-Text zu erkennen.
|
||||||
|
|||||||
@ -23,11 +23,18 @@ class Drucksache:
|
|||||||
bundesland: str
|
bundesland: str
|
||||||
typ: str = "Antrag" # Original-Typ vom Landtag (z.B. "Kleine Anfrage", "Gesetzentwurf")
|
typ: str = "Antrag" # Original-Typ vom Landtag (z.B. "Kleine Anfrage", "Gesetzentwurf")
|
||||||
typ_normiert: str = "" # Normierter Typ (wird automatisch gesetzt)
|
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):
|
def __post_init__(self):
|
||||||
from .drucksache_typen import normalize_typ
|
from .drucksache_typen import normalize_typ
|
||||||
if not self.typ_normiert:
|
if not self.typ_normiert:
|
||||||
self.typ_normiert = normalize_typ(self.typ)
|
self.typ_normiert = normalize_typ(self.typ)
|
||||||
|
if self.urheber_bundeslaender is None:
|
||||||
|
self.urheber_bundeslaender = []
|
||||||
|
|
||||||
|
|
||||||
class ParlamentAdapter(ABC):
|
class ParlamentAdapter(ABC):
|
||||||
@ -2928,15 +2935,30 @@ class BundestagAdapter(ParlamentAdapter):
|
|||||||
if not pdf_url:
|
if not pdf_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Fraktionen aus urheber-Liste extrahieren. DIP listet sie als
|
# Bundesrats-Drucksachen erkennen am DIP-Feld `herausgeber: 'BR'`.
|
||||||
# "Fraktion der AfD" o.ä. — extract_fraktionen kennt das Pattern
|
# Antragsteller sind dort Bundesländer (z.B. Sachsen, Hessen),
|
||||||
# bereits aus den Landtags-Adaptern.
|
# nicht Bundestags-Fraktionen.
|
||||||
|
is_bundesrat = (doc.get("herausgeber") or "").upper() == "BR"
|
||||||
|
|
||||||
urheber_strs: list[str] = []
|
urheber_strs: list[str] = []
|
||||||
|
urheber_bundeslaender: list[str] = []
|
||||||
for u in (doc.get("urheber") or []):
|
for u in (doc.get("urheber") or []):
|
||||||
if isinstance(u, dict):
|
if isinstance(u, dict):
|
||||||
urheber_strs.append(u.get("titel") or u.get("bezeichnung") or "")
|
urheber_strs.append(u.get("titel") or u.get("bezeichnung") or "")
|
||||||
urheber_combined = ", ".join(filter(None, urheber_strs))
|
# Bei Bundesrat: bezeichnung ist der BL-Code (SN, HE, NW, ...)
|
||||||
fraktionen = extract_fraktionen(urheber_combined, bundesland=self.bundesland)
|
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(
|
return Drucksache(
|
||||||
drucksache=nummer,
|
drucksache=nummer,
|
||||||
@ -2946,6 +2968,8 @@ class BundestagAdapter(ParlamentAdapter):
|
|||||||
link=pdf_url,
|
link=pdf_url,
|
||||||
bundesland=self.bundesland,
|
bundesland=self.bundesland,
|
||||||
typ=doc.get("drucksachetyp", "Antrag"),
|
typ=doc.get("drucksachetyp", "Antrag"),
|
||||||
|
is_bundesrat=is_bundesrat,
|
||||||
|
urheber_bundeslaender=urheber_bundeslaender,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _fetch_page(
|
async def _fetch_page(
|
||||||
|
|||||||
@ -273,29 +273,41 @@ function renderRow(item, bl) {
|
|||||||
var url = item.url || item.link || '';
|
var url = item.url || item.link || '';
|
||||||
var done = lsCheckedIds.has(ds);
|
var done = lsCheckedIds.has(ds);
|
||||||
var fraktionen = Array.isArray(item.fraktionen) ? item.fraktionen : [];
|
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
|
var titleHtml = url
|
||||||
? '<a href="' + escHtml(url) + '" target="_blank" rel="noopener">' + title + '</a>'
|
? '<a href="' + escHtml(url) + '" target="_blank" rel="noopener">' + title + '</a>'
|
||||||
: title;
|
: title;
|
||||||
|
|
||||||
var fraktionenHtml = fraktionen.length
|
var fraktionenHtml = '';
|
||||||
? '<div class="ls-fraktionen">'
|
if (isBundesrat) {
|
||||||
|
var blsList = bundeslaender.length
|
||||||
|
? bundeslaender.map(function(c){ return escHtml(c); }).join(', ')
|
||||||
|
: '—';
|
||||||
|
fraktionenHtml = '<div class="ls-fraktionen" style="opacity:0.85;">'
|
||||||
|
+ '<span class="ls-fraktion" style="background:rgba(247,148,29,0.18);color:#bf6c10;border-color:rgba(247,148,29,0.4);" title="Bundesrats-Drucksache: Antragsteller sind Bundesländer, nicht Fraktionen. Wird derzeit nicht für die GWÖ-Bewertung unterstützt.">Bundesrat · ' + blsList + '</span>'
|
||||||
|
+ '</div>';
|
||||||
|
} else if (fraktionen.length) {
|
||||||
|
fraktionenHtml = '<div class="ls-fraktionen">'
|
||||||
+ fraktionen.map(function (f) {
|
+ fraktionen.map(function (f) {
|
||||||
return '<span class="ls-fraktion">' + escHtml(f) + '</span>';
|
return '<span class="ls-fraktion">' + escHtml(f) + '</span>';
|
||||||
}).join('')
|
}).join('')
|
||||||
+ '</div>'
|
+ '</div>';
|
||||||
: '';
|
}
|
||||||
|
|
||||||
var actionHtml;
|
var actionHtml;
|
||||||
if (done) {
|
if (done) {
|
||||||
actionHtml = '<span class="ls-badge-done">Bewertet → <a href="/antrag/' + encodeURIComponent(ds) + '" style="color:inherit;">Ansehen</a></span>';
|
actionHtml = '<span class="ls-badge-done">Bewertet → <a href="/antrag/' + encodeURIComponent(ds) + '" style="color:inherit;">Ansehen</a></span>';
|
||||||
|
} else if (isBundesrat) {
|
||||||
|
actionHtml = '<span style="font-family:var(--font-mono);font-size:10px;opacity:0.55;" title="Bundesrats-Drucksachen werden derzeit nicht analysiert.">nicht unterstützt</span>';
|
||||||
} else if (lsIsAuth) {
|
} else if (lsIsAuth) {
|
||||||
actionHtml = '<button class="ls-btn-analyse" onclick="lsAnalyse(this,\'' + escAttr(ds) + '\',\'' + escAttr(bl) + '\')">Analysieren</button>';
|
actionHtml = '<button class="ls-btn-analyse" onclick="lsAnalyse(this,\'' + escAttr(ds) + '\',\'' + escAttr(bl) + '\')">Analysieren</button>';
|
||||||
} else {
|
} else {
|
||||||
actionHtml = '<span style="font-family:var(--font-mono);font-size:10px;opacity:0.5;">Anmeldung nötig</span>';
|
actionHtml = '<span style="font-family:var(--font-mono);font-size:10px;opacity:0.5;">Anmeldung nötig</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<div class="ls-row">'
|
return '<div class="ls-row"' + (isBundesrat ? ' style="opacity:0.7;"' : '') + '>'
|
||||||
+ '<div class="ls-drucksache">' + escHtml(ds) + '</div>'
|
+ '<div class="ls-drucksache">' + escHtml(ds) + '</div>'
|
||||||
+ '<div class="ls-main">'
|
+ '<div class="ls-main">'
|
||||||
+ '<div class="ls-title">' + titleHtml + '</div>'
|
+ '<div class="ls-title">' + titleHtml + '</div>'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user