gwoe-antragspruefer/app/marker.py

104 lines
3.7 KiB
Python
Raw Normal View History

"""Detail-Marker fuer Stimmverhalten × GWÖ.
Zwei Funktionen, die im Antrag-Detail-Template aus Jinja heraus
aufgerufen werden:
- ``heuchelei_score``: liefert den Wahlprogramm-Score einer Fraktion,
wenn er ``threshold`` ist und damit den -Indikator triggert.
- ``decisive_outcome``: bestimmt das erste definitive" Outcome aus
einer Liste von Plenum-Votes (Überweisung/zurückgezogen/sammel
übersprungen, ``angenommen|abgelehnt|bestätigt`` priorisiert).
- ``consistency_state``: vergleicht GWÖ-Empfehlung mit dem definitiven
Outcome und liefert ``"conflict"``, ``"aligned"`` oder ``None``.
Die Logik soll testbar sein, ohne Jinja zu rendern. Daher reine
Funktionen ohne Seiteneffekte.
Siehe ADR 0010 (Stimmverhalten × GWÖ-Aggregat) für den Kontext.
"""
from __future__ import annotations
from typing import Iterable, Optional
DECISIVE_OUTCOMES = frozenset({"angenommen", "abgelehnt", "bestätigt"})
def heuchelei_score(
fraktion: str,
fraktions_scores: Optional[Iterable[dict]],
threshold: float = 7.0,
) -> Optional[float]:
"""Wahlprogramm-Score einer Fraktion, falls ≥ threshold; sonst None.
``fraktions_scores`` ist die Liste, die ``_row_to_detail`` ans
Template hängt jeder Eintrag hat ``fraktion`` (str) und
``wahlprogramm.score`` (float|int).
Wenn die Fraktion nicht gefunden wird oder der Score unter dem
Schwellwert liegt, wird ``None`` zurückgegeben das Template
rendert dann keinen -Marker.
"""
if not fraktions_scores:
return None
for fs in fraktions_scores:
if fs.get("fraktion") == fraktion:
wp = fs.get("wahlprogramm") or {}
score = wp.get("score")
if score is None:
return None
try:
score_f = float(score)
except (TypeError, ValueError):
return None
return score_f if score_f >= threshold else None
return None
def decisive_outcome(plenum_votes: Optional[Iterable[dict]]) -> Optional[str]:
"""Erstes definitives Outcome aus einer Liste von Plenum-Votes.
Bei mehreren Votes (Überweisung Endabstimmung) wird das erste
Vote zurückgegeben, dessen ``ergebnis`` (case-insensitiv) in
``DECISIVE_OUTCOMES`` liegt. ``None`` wenn keines vorhanden.
"""
if not plenum_votes:
return None
for v in plenum_votes:
ergebnis = (v.get("ergebnis") or "").lower()
if ergebnis in DECISIVE_OUTCOMES:
return ergebnis
return None
def consistency_state(
verdict_title: Optional[str],
plenum_votes: Optional[Iterable[dict]],
) -> Optional[str]:
"""Vergleich GWÖ-Empfehlung × tatsächlicher Beschluss.
Liefert:
- ``"conflict"`` Empfehlung unterstützen/befürworten" und
Beschluss abgelehnt", ODER Empfehlung „ablehnen" und Beschluss
angenommen/bestätigt".
- ``"aligned"`` Empfehlung und Beschluss decken sich.
- ``None`` keine eindeutige Aussage möglich (Beschluss
überwiesen", oder Empfehlung ambivalent wie „Mit Vorbehalt",
oder fehlende Daten).
"""
if not verdict_title or not plenum_votes:
return None
outcome = decisive_outcome(plenum_votes)
if outcome is None:
return None
text = verdict_title.lower()
rec_supports = ("unterstützen" in text) or ("befürworten" in text)
rec_rejects = "ablehnen" in text
out_passed = outcome in ("angenommen", "bestätigt")
out_failed = outcome == "abgelehnt"
if (rec_supports and out_failed) or (rec_rejects and out_passed):
return "conflict"
if (rec_supports and out_passed) or (rec_rejects and out_failed):
return "aligned"
return None