{{ assessment.title|truncate(80, end="…") }}
+ + {% if assessment.antrag_zusammenfassung %} +{{ assessment.antrag_zusammenfassung|truncate(180, end="…") }}
+ {% endif %} + +diff --git a/app/main.py b/app/main.py index dc0031f..e632882 100644 --- a/app/main.py +++ b/app/main.py @@ -3540,7 +3540,111 @@ async def scorecard_template( } width, height = dimensions.get(format, dimensions["og"]) - response = templates.TemplateResponse("v2/screens/scorecard.html", { + # ─── Cloud-Design (portrait) braucht aggregierte Hilfsdaten ────── + # Chips: Top-3 positiv bewerteter Felder mit Code + Wert-Kurzname + Symbol + werte_kurz = { + "1": "Würde", + "2": "Solidarität", + "3": "Nachhaltigkeit", + "4": "Soz. Gerechtigkeit", + "5": "Transparenz", + } + def _sym(r: int) -> str: + if r >= 4: return "++" + if r >= 1: return "+" + if r == 0: return "○" + if r <= -4: return "−−" + return "−" + matrix_chips = [] + sorted_matrix = sorted(assessment.gwoe_matrix, key=lambda e: e.rating, reverse=True) + for e in sorted_matrix: + if e.rating <= 0: + break + col = e.field[1:] if len(e.field) >= 2 else "" + matrix_chips.append({ + "code": e.field, + "label_short": werte_kurz.get(col, ""), + "symbol": _sym(e.rating), + }) + if len(matrix_chips) >= 3: + break + + # Wahlperiode aus datum + bundesland (best-effort) + wahlperiode = "" + try: + from .wahlperioden import wahlperiode_for + wahlperiode = wahlperiode_for(row.get("datum", ""), bundesland) or "" + except Exception: + pass + + # Fraktions-Bars: pro WP-Score-Eintrag eine Bar mit Vote-Label. + # Plenum-Votes best-effort laden (nur erstes Vote-Ergebnis fuer Beschluss). + plenum_votes_list: list[dict] = [] + try: + from .database import get_plenum_votes as _gpv + plenum_votes_list = await _gpv(bundesland or "NRW", drucksache) + except Exception: + pass + + vote_lookup: dict[str, str] = {} + if plenum_votes_list: + v0 = plenum_votes_list[0] + for f in (v0.get("fraktionen_ja") or []): vote_lookup[str(f).upper()] = "ja" + for f in (v0.get("fraktionen_nein") or []): vote_lookup[str(f).upper()] = "nein" + for f in (v0.get("fraktionen_enthaltung") or []): vote_lookup[str(f).upper()] = "enth" + + fraktionen_bars = [] + for fs in (assessment.wahlprogramm_scores or [])[:5]: + wp_score = (fs.wahlprogramm.score if fs.wahlprogramm else 0) or 0 + vote = vote_lookup.get(str(fs.fraktion).upper()) + if vote == "ja": + vote_label, vote_class = "Ja ✓", "ja" + elif vote == "nein": + vote_label, vote_class = "Nein ✗", "nein" + elif vote == "enth": + vote_label, vote_class = "Enth.", "enth" + else: + vote_label, vote_class = "—", "unbekannt" + fraktionen_bars.append({ + "name": fs.fraktion, + "bar_pct": int(min(100, max(0, wp_score * 10))), + "score_text": f"{int(round(wp_score))}/10", + "vote_label": vote_label, + "vote_class": vote_class, + "weak": wp_score < 5 or vote == "nein", + }) + + # Beschluss-Bar + beschluss = None + if plenum_votes_list: + v0 = plenum_votes_list[0] + ergebnis = (v0.get("ergebnis") or "").strip().lower() + n_ja = len(v0.get("fraktionen_ja") or []) + n_nein = len(v0.get("fraktionen_nein") or []) + is_positive = ergebnis in ("angenommen", "bestaetigt", "bestätigt") + ergebnis_text = (v0.get("ergebnis") or "").capitalize() or ("Angenommen" if is_positive else "Abgelehnt") + if v0.get("einstimmig"): + mehrheit_text = "einstimmig" + elif n_ja or n_nein: + mehrheit_text = f"{n_ja}:{n_nein} Fraktionen" + else: + mehrheit_text = "" + beschluss = { + "is_positive": is_positive, + "text": f"{ergebnis_text}" + (f" — {mehrheit_text}" if mehrheit_text else ""), + } + + score_color_band = "good" if score >= 7 else "mid" if score >= 4 else "low" + + # Template-Wahl: portrait nutzt das Cloud-Design (eigene Datei), + # square/og bleiben beim alten Layout. + template_name = ( + "v2/screens/scorecard_portrait.html" + if format == "portrait" + else "v2/screens/scorecard.html" + ) + + response = templates.TemplateResponse(template_name, { "request": request, "assessment": assessment, "bundesland": bundesland, @@ -3548,6 +3652,13 @@ async def scorecard_template( "fraktionen": fraktionen[:4], "datum": (row.get("datum") or "")[:10], "score_color": score_color, + "score_color_band": score_color_band, + "matrix_chips": matrix_chips, + "fraktionen_bars": fraktionen_bars, + "fraktionen_count": max(1, len(fraktionen_bars)), + "beschluss": beschluss, + "antrag_typ": (row.get("typ") or "Antrag"), + "wahlperiode": wahlperiode, "width": width, "height": height, }) @@ -3592,17 +3703,102 @@ async def _render_scorecard_pdf( elif score >= 5: score_color = "#bf6c10" else: score_color = "#9a2a2a" - template = templates.env.get_template("v2/screens/scorecard.html") - html_content = template.render( - request=None, - assessment=assessment, - bundesland=bundesland, - matrix_lookup=matrix_lookup, - fraktionen=fraktionen[:4], - datum=(row.get("datum") or "")[:10], - score_color=score_color, - width=width, height=height, - ) + # Portrait nutzt das Cloud-Design-Template; square/og das alte. + # Fuer Portrait brauchen wir die gleichen Aggregat-Daten wie der + # HTML-Render (Chips, Fraktions-Bars, Beschluss). + if format == "portrait": + werte_kurz = { + "1": "Würde", "2": "Solidarität", "3": "Nachhaltigkeit", + "4": "Soz. Gerechtigkeit", "5": "Transparenz", + } + def _sym(r: int) -> str: + if r >= 4: return "++" + if r >= 1: return "+" + if r == 0: return "○" + if r <= -4: return "−−" + return "−" + matrix_chips = [] + for e in sorted(assessment.gwoe_matrix, key=lambda x: x.rating, reverse=True): + if e.rating <= 0: break + col = e.field[1:] if len(e.field) >= 2 else "" + matrix_chips.append({ + "code": e.field, + "label_short": werte_kurz.get(col, ""), + "symbol": _sym(e.rating), + }) + if len(matrix_chips) >= 3: break + try: + from .wahlperioden import wahlperiode_for + wahlperiode = wahlperiode_for(row.get("datum", ""), bundesland) or "" + except Exception: + wahlperiode = "" + plenum_votes_list: list[dict] = [] + try: + from .database import get_plenum_votes as _gpv + plenum_votes_list = await _gpv(bundesland or "NRW", drucksache) + except Exception: + pass + vote_lookup: dict[str, str] = {} + if plenum_votes_list: + v0 = plenum_votes_list[0] + for f in (v0.get("fraktionen_ja") or []): vote_lookup[str(f).upper()] = "ja" + for f in (v0.get("fraktionen_nein") or []): vote_lookup[str(f).upper()] = "nein" + for f in (v0.get("fraktionen_enthaltung") or []): vote_lookup[str(f).upper()] = "enth" + fraktionen_bars = [] + for fs in (assessment.wahlprogramm_scores or [])[:5]: + wp_score = (fs.wahlprogramm.score if fs.wahlprogramm else 0) or 0 + vote = vote_lookup.get(str(fs.fraktion).upper()) + if vote == "ja": vote_label, vote_class = "Ja ✓", "ja" + elif vote == "nein": vote_label, vote_class = "Nein ✗", "nein" + elif vote == "enth": vote_label, vote_class = "Enth.", "enth" + else: vote_label, vote_class = "—", "unbekannt" + fraktionen_bars.append({ + "name": fs.fraktion, + "bar_pct": int(min(100, max(0, wp_score * 10))), + "score_text": f"{int(round(wp_score))}/10", + "vote_label": vote_label, "vote_class": vote_class, + "weak": wp_score < 5 or vote == "nein", + }) + beschluss = None + if plenum_votes_list: + v0 = plenum_votes_list[0] + ergebnis = (v0.get("ergebnis") or "").strip().lower() + n_ja = len(v0.get("fraktionen_ja") or []) + n_nein = len(v0.get("fraktionen_nein") or []) + is_positive = ergebnis in ("angenommen", "bestaetigt", "bestätigt") + ergebnis_text = (v0.get("ergebnis") or "").capitalize() or ("Angenommen" if is_positive else "Abgelehnt") + mehrheit_text = "einstimmig" if v0.get("einstimmig") else (f"{n_ja}:{n_nein} Fraktionen" if (n_ja or n_nein) else "") + beschluss = { + "is_positive": is_positive, + "text": ergebnis_text + (f" — {mehrheit_text}" if mehrheit_text else ""), + } + score_color_band = "good" if score >= 7 else "mid" if score >= 4 else "low" + template = templates.env.get_template("v2/screens/scorecard_portrait.html") + html_content = template.render( + request=None, + assessment=assessment, + bundesland=bundesland, + matrix_chips=matrix_chips, + fraktionen_bars=fraktionen_bars, + fraktionen_count=max(1, len(fraktionen_bars)), + beschluss=beschluss, + antrag_typ=(row.get("typ") or "Antrag"), + wahlperiode=wahlperiode, + score_color_band=score_color_band, + width=width, height=height, + ) + else: + template = templates.env.get_template("v2/screens/scorecard.html") + html_content = template.render( + request=None, + assessment=assessment, + bundesland=bundesland, + matrix_lookup=matrix_lookup, + fraktionen=fraktionen[:4], + datum=(row.get("datum") or "")[:10], + score_color=score_color, + width=width, height=height, + ) # `size: NNNpt` → PDF-Page hat exakt N×M Punkte. PyMuPDF rendert # bei zoom=1 dann 1 PDF-Punkt = 1 PNG-Pixel. CSS-Pixel werden diff --git a/app/templates/v2/screens/scorecard_portrait.html b/app/templates/v2/screens/scorecard_portrait.html new file mode 100644 index 0000000..043f5ab --- /dev/null +++ b/app/templates/v2/screens/scorecard_portrait.html @@ -0,0 +1,230 @@ +{# scorecard_portrait.html — 1080×1350 Hochkant-Scorecard (Instagram 4:5) + + Designvorgabe: Claude Design, ZIP "GWÖ Antrag Score Card.zip" vom 2026-05-07. + Übernommen 1:1 in HTML/CSS, Jinja-Variablen ersetzen die Beispiel-Inhalte. + + Komprimierung gegenüber der Profi-Detail-Ansicht (siehe Spec-Sheet): + - 5×5-Matrix → Top-3 Schwerpunkt-Chips + - Zitate, Verbesserungen, Kommentare, News alle raus + - Stärken/Schwächen-Fließtext → 1-Satz-Zusammenfassung + - Fraktions-Bewertung als Balken-Grid (5 Spalten) statt langer Liste + - Beschluss als invertierte schwarze Bar am Ende +#} + + +
+ +{{ assessment.antrag_zusammenfassung|truncate(180, end="…") }}
+ {% endif %} + +