From c7eab5a695634009b44f221bed741b0413c17c83 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Sat, 9 May 2026 02:21:12 +0200 Subject: [PATCH] feat(pdf): Heuchelei-/Opportunismus-Marker im Vote-Block (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web-Detail zeigt diese Marker bereits — pro NEIN-Fraktion einen ⚠ wenn der eigene Wahlprogramm-Score ≥ 7/10 ist (Heuchelei: stimmt gegen die eigenen Versprechen), pro JA-Fraktion einen ! wenn der Wahlprogramm- Score < 3/10 (Opportunismus: stimmt zu obwohl Antrag inhaltlich nicht zum eigenen Programm passt). Im PDF fehlten sie bisher. Daten-Pfad: report.py rechnet die Marker einmal vor (heuchelei_score / opportunismus_score aus app/marker.py, gefüttert mit umgemappten fraktions_scores aus assessment.wahlprogramm_scores) und reicht zwei Maps fraktion → score ans Template. Template macht nur noch Lookup: ``opportunismus_by_fraktion.get(f)`` neben jeder JA-Fraktion, ``heuchelei_by_fraktion.get(f)`` neben jeder NEIN-Fraktion. Plus kompakte Legende unter dem Vote-Block, falls überhaupt Marker vorkommen. Stimmverhalten und Programm-Treue-Begründungen sind im PDF schon da (verifiziert bei der Code-Inspektion). Damit ist die "PDF auf Augenhöhe mit Web-Detail"-Liste aus #175 bis auf News-Match abgehakt; News-Match explizit out-of-scope nach User-Entscheidung. --- app/report.py | 35 ++++++++++++++++++++++++++++ app/templates/v3/pdf/antrag_pdf.html | 25 ++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/report.py b/app/report.py index b8d3570..6dc140f 100644 --- a/app/report.py +++ b/app/report.py @@ -590,6 +590,39 @@ async def generate_html_report_v3( assessment.drucksache, exc, ) + # Pre-compute Heuchelei-/Opportunismus-Marker pro Fraktion. + # Jinja-Globals sind im Web ``heuchelei_score`` und ``opportunismus_score``; + # sie erwarten Dict-Listen (fraktions_scores aus _row_to_detail). Das + # Pydantic-Assessment.wahlprogramm_scores hat dieselben Daten, aber als + # Pydantic-Objekte. Wir mappen einmal um und rechnen die Marker für + # alle abstimmenden Fraktionen, damit das Template nur noch Lookup + # statt Logik macht. + fraktions_scores_dict = [] + for fs in (assessment.wahlprogramm_scores or []): + fraktions_scores_dict.append({ + "fraktion": fs.fraktion, + "wahlprogramm": {"score": fs.wahlprogramm.score if fs.wahlprogramm else None}, + }) + + from .marker import heuchelei_score as _h, opportunismus_score as _o + heuchelei_by_fraktion: dict[str, float] = {} + opportunismus_by_fraktion: dict[str, float] = {} + if plenum_votes and fraktions_scores_dict: + seen: set[str] = set() + for v in plenum_votes: + for f in (v.get("fraktionen_nein") or []): + if f in seen: + continue + hv = _h(f, fraktions_scores_dict) + if hv is not None: + heuchelei_by_fraktion[f] = hv + for f in (v.get("fraktionen_ja") or []): + if f in seen: + continue + ov = _o(f, fraktions_scores_dict) + if ov is not None: + opportunismus_by_fraktion[f] = ov + template = _pdf_jinja.get_template("v3/pdf/antrag_pdf.html") html = template.render( assessment=assessment, @@ -602,6 +635,8 @@ async def generate_html_report_v3( plenum_votes=plenum_votes, konsistenz_state=konsistenz_state, konsistenz_decisive=konsistenz_decisive, + heuchelei_by_fraktion=heuchelei_by_fraktion, + opportunismus_by_fraktion=opportunismus_by_fraktion, ) output_path.write_text(html) diff --git a/app/templates/v3/pdf/antrag_pdf.html b/app/templates/v3/pdf/antrag_pdf.html index 4a9229b..5597a9e 100644 --- a/app/templates/v3/pdf/antrag_pdf.html +++ b/app/templates/v3/pdf/antrag_pdf.html @@ -504,6 +504,20 @@ background: rgba(45, 164, 78, 0.07); border-left-color: #2da44e; } + .pdf-marker { + font-weight: 700; + font-size: 9pt; + margin-left: 2pt; + } + .pdf-marker.heuchelei { color: #cf222e; } + .pdf-marker.opportunismus { color: #bf6c10; font-style: italic; } + .pdf-marker-legend { + font-size: 7.5pt; + color: #57606a; + margin-top: 4pt; + padding-top: 3pt; + border-top: 1pt dashed #d0d7de; + } @@ -754,12 +768,12 @@
{% if v.fraktionen_ja %}
Ja: - {% for f in v.fraktionen_ja %}{{ f }}{% endfor %} + {% for f in v.fraktionen_ja %}{{ f }}{% if opportunismus_by_fraktion.get(f) is not none %}!{% endif %}{% endfor %}
{% endif %} {% if v.fraktionen_nein %}
Nein: - {% for f in v.fraktionen_nein %}{{ f }}{% endfor %} + {% for f in v.fraktionen_nein %}{{ f }}{% if heuchelei_by_fraktion.get(f) is not none %}{% endif %}{% endfor %}
{% endif %} {% if v.fraktionen_enthaltung %} @@ -768,6 +782,13 @@
{% endif %} + {% if heuchelei_by_fraktion or opportunismus_by_fraktion %} +
+ {% if heuchelei_by_fraktion %} Heuchelei (Nein trotz Wahlprogramm-Match ≥7/10){% endif %} + {% if heuchelei_by_fraktion and opportunismus_by_fraktion %} · {% endif %} + {% if opportunismus_by_fraktion %}! Opportunismus (Ja trotz Wahlprogramm-Mismatch <3/10){% endif %} +
+ {% endif %} {% endfor %}