feat: Scorecard Portrait — Claude-Design 1:1 übernommen (1080×1350)
User hat eine Design-Vorgabe als ZIP geliefert (GWÖ Antrag Score Card.zip, Claude Design Output, Stand 2026-05-07). Komplett anderes Konzept als mein vorheriges Layout — nicht eine 5×5-Matrix als Datenviz, sondern eine editorial-magazin-mässige Card mit klaren Modulen. Übernommen 1:1: - Inter + JetBrains Mono via Google Fonts - 3-Zonen-Grid: Header 88px / Body / Footer 96px - Paper-BG #f5f1ea, Ink #1a1a1a, GWÖ-Grün gedeckt #0a5d3f - Header-Strip: Brand-Dot + 'GWÖ-Antragsprüfer' / 'Matrix 2.0' / 'NRW · WP18' - Topline: Drs.-ID + Antragsteller-Pill (Partei-Farbpunkt) - H1 Antragstitel 78pt, weight 800, line-height 0.95 - Lede: 1-Satz-Zusammenfassung, max 180 Zeichen - Score-Block: 320px breite gruene Kachel mit 9.0/10 + 'Empfehlung' kicker + Verdict-Text + 3 Schwerpunkt-Chips (Code + Wert-kurz + Symbol) - Fraktions-Grid: bis zu 5 Spalten, pro Fraktion Name + WP-Bar + Score + Vote-Label (Ja ✓ / Nein ✗); weak-Klasse (rot) bei Score<5 oder Nein - Decision-Bar: invertiert schwarz, Beschluss-Text mit ✓/✗-Akzent - Footer: URL + /antrag/-Pfad + CC BY 4.0 + QR-Pattern Datenaggregation in main.py erweitert: - matrix_chips: Top-3 positive Felder (rating > 0) mit Code+Wert+Symbol - fraktionen_bars: aus wahlprogramm_scores + plenum_votes-Lookup (WP-Bar + Vote-Label pro Fraktion) - beschluss: aus erstem plenum_vote (ergebnis + Mehrheits-Verhältnis) - wahlperiode: via wahlperioden.wahlperiode_for(datum, BL) Routing: - /v2/scorecard?format=portrait → scorecard_portrait.html (NEU) - /v2/scorecard?format=square|og → scorecard.html (alt, unveraendert) - /api/assessment/scorecard.png?format=portrait nutzt ebenfalls das neue Portrait-Template fuer die PNG-Generierung. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79e7937d51
commit
1350fc7f52
220
app/main.py
220
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
|
||||
|
||||
230
app/templates/v2/screens/scorecard_portrait.html
Normal file
230
app/templates/v2/screens/scorecard_portrait.html
Normal file
@ -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
|
||||
#}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>GWÖ-Scorecard – Drs. {{ assessment.drucksache }}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--ink:#1a1a1a;
|
||||
--paper:#f5f1ea;
|
||||
--rule:#1a1a1a;
|
||||
--muted:#6b6660;
|
||||
--accent:#0a5d3f;
|
||||
--accent-warn:#b04a2f;
|
||||
--grid:#d9d2c5;
|
||||
--score-bg: {% if score_color_band == 'good' %}#0a5d3f{% elif score_color_band == 'mid' %}#6b6660{% else %}#b04a2f{% endif %};
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;}
|
||||
body{
|
||||
width:{{ width }}px;height:{{ height }}px;overflow:hidden;
|
||||
}
|
||||
|
||||
.card{
|
||||
width:1080px;height:1350px;
|
||||
background:var(--paper);color:var(--ink);
|
||||
position:relative;overflow:hidden;
|
||||
display:grid;grid-template-rows: 88px 1fr 96px;
|
||||
}
|
||||
|
||||
/* Kopfleiste */
|
||||
.head{
|
||||
border-bottom:2px solid var(--rule);
|
||||
padding:0 56px;
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
font-family:'JetBrains Mono',monospace;
|
||||
font-size:18px;font-weight:500;letter-spacing:.04em;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.head .brand{display:flex;align-items:center;gap:14px}
|
||||
.head .dot{width:14px;height:14px;background:var(--accent);border-radius:50%}
|
||||
.head .meta{display:flex;gap:24px;color:var(--muted)}
|
||||
|
||||
/* Body */
|
||||
.body{padding:40px 56px 32px;display:flex;flex-direction:column;gap:28px;min-height:0}
|
||||
|
||||
.topline{display:flex;justify-content:space-between;align-items:flex-start;gap:32px}
|
||||
.ids{font-family:'JetBrains Mono',monospace;font-size:20px;color:var(--muted);letter-spacing:.06em}
|
||||
.ids strong{color:var(--ink);font-weight:600}
|
||||
.party-pill{
|
||||
display:inline-flex;align-items:center;gap:10px;
|
||||
padding:8px 18px;border:2px solid var(--rule);border-radius:999px;
|
||||
font-weight:700;font-size:20px;letter-spacing:.04em;
|
||||
}
|
||||
.party-pill::before{content:"";width:12px;height:12px;border-radius:50%;background:#d44}
|
||||
|
||||
h1.title{
|
||||
font-family:'Inter',sans-serif;
|
||||
font-weight:800;font-size:78px;line-height:.95;
|
||||
letter-spacing:-.025em;text-wrap:balance;
|
||||
}
|
||||
|
||||
.lede{
|
||||
font-size:26px;line-height:1.35;color:var(--ink);
|
||||
max-width:880px;text-wrap:pretty;
|
||||
}
|
||||
|
||||
.scorewrap{
|
||||
display:grid;grid-template-columns: 320px 1fr;gap:32px;align-items:stretch;
|
||||
border-top:2px solid var(--rule);border-bottom:2px solid var(--rule);
|
||||
padding:24px 0;
|
||||
}
|
||||
.score{
|
||||
background:var(--score-bg);color:var(--paper);
|
||||
display:flex;flex-direction:column;justify-content:center;align-items:center;
|
||||
padding:18px;border-radius:8px;
|
||||
}
|
||||
.score .num{font-size:120px;font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
|
||||
.score .num small{font-size:42px;opacity:.7;font-weight:600}
|
||||
.score .label{font-family:'JetBrains Mono',monospace;font-size:15px;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;opacity:.85}
|
||||
|
||||
.verdict{display:flex;flex-direction:column;justify-content:center;gap:14px}
|
||||
.verdict .kicker{font-family:'JetBrains Mono',monospace;font-size:15px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}
|
||||
.verdict .vtext{font-size:34px;font-weight:700;line-height:1.1;letter-spacing:-.01em}
|
||||
.verdict .fields{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
|
||||
.field-chip{
|
||||
font-family:'JetBrains Mono',monospace;font-size:16px;font-weight:600;
|
||||
border:1.5px solid var(--rule);padding:4px 10px;border-radius:4px;background:transparent;
|
||||
}
|
||||
|
||||
/* Fraktionen Grid */
|
||||
.fractions{display:grid;grid-template-columns:repeat({{ fraktionen_count }},1fr);gap:0;border:1.5px solid var(--rule)}
|
||||
.frac{
|
||||
padding:16px 14px;border-right:1.5px solid var(--rule);
|
||||
display:flex;flex-direction:column;gap:8px;align-items:center;text-align:center;
|
||||
background:var(--paper);
|
||||
}
|
||||
.frac:last-child{border-right:none}
|
||||
.frac .name{font-family:'JetBrains Mono',monospace;font-size:17px;font-weight:700;letter-spacing:.05em}
|
||||
.frac .bar{height:8px;width:100%;background:#e6dfd1;border-radius:2px;overflow:hidden}
|
||||
.frac .bar > span{display:block;height:100%;background:var(--accent)}
|
||||
.frac.weak .bar > span{background:var(--accent-warn)}
|
||||
.frac .val{font-family:'JetBrains Mono',monospace;font-size:18px;font-weight:600}
|
||||
.frac .vote{font-size:14px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
|
||||
.vote.ja{color:var(--accent)}
|
||||
.vote.nein{color:var(--accent-warn)}
|
||||
.vote.enth{color:var(--muted)}
|
||||
.vote.unbekannt{color:var(--muted);opacity:.6}
|
||||
|
||||
/* Beschluss */
|
||||
.decision{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:18px 22px;background:var(--ink);color:var(--paper);
|
||||
border-radius:6px;
|
||||
}
|
||||
.decision .left{font-family:'JetBrains Mono',monospace;font-size:16px;letter-spacing:.1em;text-transform:uppercase;opacity:.7}
|
||||
.decision .right{font-size:24px;font-weight:700}
|
||||
.decision .right .x{color:#ff6a4a;margin-right:10px}
|
||||
.decision .right .ok{color:#7fd9a8;margin-right:10px}
|
||||
|
||||
/* Footer */
|
||||
.foot{
|
||||
border-top:2px solid var(--rule);
|
||||
padding:0 56px;
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
font-family:'JetBrains Mono',monospace;font-size:16px;letter-spacing:.04em;
|
||||
color:var(--muted);
|
||||
}
|
||||
.foot .url{color:var(--ink);font-weight:600}
|
||||
.foot .qr{width:64px;height:64px;background:
|
||||
repeating-conic-gradient(var(--ink) 0 25%, var(--paper) 0 50%) 50%/16px 16px;
|
||||
border:2px solid var(--ink);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<header class="head">
|
||||
<div class="brand"><span class="dot"></span>GWÖ-Antragsprüfer</div>
|
||||
<div class="meta">
|
||||
<span>Matrix 2.0</span>
|
||||
<span>{{ bundesland }}{% if wahlperiode %} · WP{{ wahlperiode }}{% endif %}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="body">
|
||||
|
||||
<div class="topline">
|
||||
<div class="ids">
|
||||
<strong>Drs. {{ assessment.drucksache }}</strong>
|
||||
{% if antrag_typ %} · {{ antrag_typ }}{% endif %}
|
||||
{% if assessment.datum %} · eingebracht {{ assessment.datum }}{% endif %}
|
||||
</div>
|
||||
{% if assessment.fraktionen %}
|
||||
<div class="party-pill">{{ assessment.fraktionen[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h1 class="title">{{ assessment.title|truncate(80, end="…") }}</h1>
|
||||
|
||||
{% if assessment.antrag_zusammenfassung %}
|
||||
<p class="lede">{{ assessment.antrag_zusammenfassung|truncate(180, end="…") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="scorewrap">
|
||||
<div class="score">
|
||||
<div class="num">{{ "%.1f"|format(assessment.gwoe_score) }}<small>/10</small></div>
|
||||
<div class="label">GWÖ-Score</div>
|
||||
</div>
|
||||
<div class="verdict">
|
||||
<div class="kicker">Empfehlung</div>
|
||||
<div class="vtext">{{ assessment.empfehlung.value }}</div>
|
||||
{% if matrix_chips %}
|
||||
<div class="fields">
|
||||
{% for chip in matrix_chips %}
|
||||
<span class="field-chip">{{ chip.code }} {{ chip.label_short }} {{ chip.symbol }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if fraktionen_bars %}
|
||||
<div class="fractions">
|
||||
{% for f in fraktionen_bars %}
|
||||
<div class="frac{% if f.weak %} weak{% endif %}">
|
||||
<div class="name">{{ f.name }}</div>
|
||||
<div class="bar"><span style="width:{{ f.bar_pct }}%"></span></div>
|
||||
<div class="val">{{ f.score_text }}</div>
|
||||
<div class="vote {{ f.vote_class }}">{{ f.vote_label }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if beschluss %}
|
||||
<div class="decision">
|
||||
<div class="left">Beschluss Plenum</div>
|
||||
<div class="right">
|
||||
<span class="{{ 'ok' if beschluss.is_positive else 'x' }}">{{ '✓' if beschluss.is_positive else '✗' }}</span>
|
||||
{{ beschluss.text }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="foot">
|
||||
<div><span class="url">gwoe.toppyr.de</span> · /antrag/{{ assessment.drucksache }}</div>
|
||||
<div>CC BY 4.0</div>
|
||||
<div class="qr" aria-hidden="true"></div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user