104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
|
|
"""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
|