Topbar: - height: 32px (statt auto), line-height: 1, alle children max 24px - Topbar-Icons explizit auf 12x12 (statt 14) - selects/buttons/a mit fester Hoehe 22px, padding 2px 6px Landtag-Suche: - search_landtag filtert jetzt Drucksachen aus, deren Titel typische Frage-Praefixe haben (Welche/Wie viele/Wann/Was/Hat/Ist/...) oder mit '?' enden — bei NRW-OPAL liefert der Adapter alle als 'sonstige', daher Title-Heuristik. Server-side, damit alle Adapter profitieren. - Neuer Helper drucksache_typen.likely_kleine_anfrage_titel() Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""Drucksache-Typ-Normalisierung (#127).
|
|
|
|
Jeder Landtag hat eigene Bezeichnungen für Dokumenttypen. Dieses Modul
|
|
normalisiert sie auf einheitliche Kategorien und bestimmt ob eine
|
|
Drucksache abstimmbar ist (= GWÖ-Bewertung sinnvoll).
|
|
"""
|
|
|
|
# Normierte Kategorien
|
|
ANTRAG = "antrag"
|
|
GESETZENTWURF = "gesetzentwurf"
|
|
AENDERUNGSANTRAG = "aenderungsantrag"
|
|
DRINGLICHKEITSANTRAG = "dringlichkeitsantrag"
|
|
ENTSCHLIESSUNGSANTRAG = "entschliessungsantrag"
|
|
BESCHLUSSEMPFEHLUNG = "beschlussempfehlung"
|
|
KLEINE_ANFRAGE = "kleine_anfrage"
|
|
GROSSE_ANFRAGE = "grosse_anfrage"
|
|
UNTERRICHTUNG = "unterrichtung"
|
|
PETITION = "petition"
|
|
WAHLVORSCHLAG = "wahlvorschlag"
|
|
BERICHT = "bericht"
|
|
SONSTIGE = "sonstige"
|
|
|
|
ABSTIMMBARE_TYPEN = {
|
|
ANTRAG,
|
|
GESETZENTWURF,
|
|
AENDERUNGSANTRAG,
|
|
DRINGLICHKEITSANTRAG,
|
|
ENTSCHLIESSUNGSANTRAG,
|
|
}
|
|
|
|
# Übersetzungstabelle: Original-Typ (lowercase) → normierter Typ.
|
|
# Keys werden case-insensitive + substring-matched.
|
|
# Reihenfolge: spezifischere zuerst (z.B. "kleine anfrage" vor "anfrage").
|
|
_TYP_MAP = [
|
|
# Abstimmbar
|
|
("gesetzentwurf", GESETZENTWURF),
|
|
("änderungsantrag", AENDERUNGSANTRAG),
|
|
("aenderungsantrag", AENDERUNGSANTRAG),
|
|
("dringlichkeitsantrag", DRINGLICHKEITSANTRAG),
|
|
("entschließungsantrag", ENTSCHLIESSUNGSANTRAG),
|
|
("entschliessungsantrag", ENTSCHLIESSUNGSANTRAG),
|
|
("antrag gemäß", ANTRAG),
|
|
("antrag", ANTRAG),
|
|
# Nicht abstimmbar
|
|
("kleine anfrage", KLEINE_ANFRAGE),
|
|
("große anfrage", GROSSE_ANFRAGE),
|
|
("grosse anfrage", GROSSE_ANFRAGE),
|
|
("anfrage", KLEINE_ANFRAGE),
|
|
("beschlussempfehlung", BESCHLUSSEMPFEHLUNG),
|
|
("unterrichtung", UNTERRICHTUNG),
|
|
("bericht", BERICHT),
|
|
("mitteilung", UNTERRICHTUNG),
|
|
("vorlage", UNTERRICHTUNG),
|
|
("petition", PETITION),
|
|
("wahlvorschlag", WAHLVORSCHLAG),
|
|
("stellungnahme", SONSTIGE),
|
|
("drucksache", SONSTIGE),
|
|
]
|
|
|
|
|
|
def normalize_typ(original: str) -> str:
|
|
"""Normalisiert einen BL-spezifischen Typ-String auf eine Kategorie.
|
|
|
|
Case-insensitiv, Substring-Match, spezifischere Patterns zuerst.
|
|
"""
|
|
if not original:
|
|
return SONSTIGE
|
|
low = original.lower().strip()
|
|
for pattern, norm in _TYP_MAP:
|
|
if pattern in low:
|
|
return norm
|
|
return SONSTIGE
|
|
|
|
|
|
def ist_abstimmbar(typ_normiert: str) -> bool:
|
|
"""Prüft ob ein normierter Typ zur Abstimmung steht.
|
|
|
|
``sonstige`` wird durchgelassen (benefit of the doubt) — wenn der
|
|
Adapter den Typ nicht bestimmen kann (z.B. NRW liefert nur
|
|
"Drucksache"), wird der echte Check erst beim Analysieren gemacht
|
|
(aus dem Dokument-Text).
|
|
"""
|
|
return typ_normiert in ABSTIMMBARE_TYPEN or typ_normiert == SONSTIGE
|
|
|
|
|
|
def ist_abstimmbar_original(original: str) -> bool:
|
|
"""Convenience: prüft direkt am Original-Typ-String."""
|
|
return ist_abstimmbar(normalize_typ(original))
|
|
|
|
|
|
# Frage-Präfixe die typisch für Kleine Anfragen sind. Wird genutzt wenn der
|
|
# Adapter (z.B. NRW) den Typ nur als "Drucksache" liefert — wir versuchen
|
|
# anhand des Titels eine bessere Klassifikation, damit Search-Ergebnisse
|
|
# nicht voll mit nicht-abstimmbaren Anfragen sind.
|
|
_FRAGE_PRAEFIXE = (
|
|
"welche ", "wie viele ", "wieviel", "wie viel ", "wie hoch ", "wie ",
|
|
"wann ", "warum ", "weshalb ", "wo ", "wer ", "wie steht ", "wie weit ",
|
|
"ist es ", "ist der ", "ist die ", "ist das ", "sind ",
|
|
"trifft es ", "kann ", "wird ", "wieso ", "was ",
|
|
"hat ", "hat der ", "hat die ", "hat das ",
|
|
"haben ", "war ", "waren ",
|
|
)
|
|
|
|
|
|
def likely_kleine_anfrage_titel(title: str) -> bool:
|
|
"""Heuristik: erkennt Kleine Anfragen am Titel-Format.
|
|
|
|
Wenn der Titel mit einem typischen Frage-Präfix beginnt oder mit "?" endet,
|
|
behandeln wir die Drucksache als Kleine Anfrage. NRW-OPAL klassifiziert
|
|
alle Drucksachen als "Drucksache" → ohne diese Heuristik landen Anfragen
|
|
in den Search-Ergebnissen, was den User verwirrt (#149 Folge).
|
|
|
|
Args:
|
|
title: Drucksachen-Titel inkl. evtl. Nummer-Präfix wie "1Welche...".
|
|
|
|
Returns:
|
|
True wenn der Titel wie eine Kleine Anfrage aussieht.
|
|
"""
|
|
if not title:
|
|
return False
|
|
t = title.strip()
|
|
# Manche Adapter prefixen mit Nummerierung wie "1Welche..." — strippen
|
|
while t and (t[0].isdigit() or t[0] in " .-"):
|
|
t = t[1:]
|
|
t_low = t.lower()
|
|
if t_low.startswith(_FRAGE_PRAEFIXE):
|
|
return True
|
|
if t.rstrip().endswith("?"):
|
|
return True
|
|
return False
|