From 79e7937d511d26cb7fc2ab5eb9c6f6d66aa09a55 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Thu, 7 May 2026 13:48:19 +0200 Subject: [PATCH] feat: PDF-Generierung auf v3-Layout, Werkstatt-Link im Admin, Share nur bei Login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei Aufgaben in einem Schwung: 1. Werkstatt-Link im Admin admin_stand bekommt eine Sektion 'Design-Werkstaetten' mit Link auf /v2/scorecard-werkstatt — damit Admins den Live-Editor finden ohne die URL kennen zu muessen. 2. Share-Block nur fuer angemeldete User Der ganze Share-Block (Kopieren, Threads, Mastodon, LinkedIn, Instagram, E-Mail, Scorecard, Stock-Bild) bekommt id=v2-share-block und wird per initAuth() display:none/block geschaltet — analog zum Comment-Form. Default im Markup: display:none, damit Gaeste ihn nicht waehrend des Auth-Roundtrips kurz sehen. Funktioniert in v2 und v3 (gleicher JS-Handler via super-Inheritance). 3. PDF-Layout = v3-Layout Neues Template v3/pdf/antrag_pdf.html (single column, A4) reused die Visuallogik aus der Online-Detailseite: - Score-Hero-Block mit Farb-Tint - Matrix 5×5 mit Achsen-Labels (Werte oben, Berührungsgruppen links) - Programm-Treue pro Fraktion mit Begruendung + Zitaten + Fallback- Hinweis bei fehlenden Zitaten - Verbesserungsvorschlaege mit Redline-Format - Abstimmungsergebnis (best-effort via get_plenum_votes) inkl. Konsistenz-Hinweis Online-spezifisches gestrichen: Merken-Button, Vote-treffend, Share, Kommentare, News-Box, Reanalyze, Historie, Modals. NEU im PDF: 'Schwerpunkte erklaert'-Sektion direkt unter der Matrix. Listet die Top-4 positiven und Top-4 negativen Matrix-Felder mit ihrem LLM-generierten label + aspect — Ersatz fuer den interaktiven Klick, der im PDF nicht funktioniert. report.generate_html_report_v3() neu, generate_pdf_report() ruft diese statt der alten Inline-HTML-Variante. Alte generate_html_report bleibt als Fallback erhalten. WeasyPrint rendert mit @page A4, Footer mit Drucksache + Branding + Seitenzahl 'Seite X von Y'. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/report.py | 99 ++- app/templates/v2/screens/admin_stand.html | 10 + app/templates/v2/screens/antrag_detail.html | 20 +- app/templates/v3/pdf/antrag_pdf.html | 756 ++++++++++++++++++++ app/templates/v3/screens/antrag_detail.html | 4 +- 5 files changed, 877 insertions(+), 12 deletions(-) create mode 100644 app/templates/v3/pdf/antrag_pdf.html diff --git a/app/report.py b/app/report.py index 72a3587..b8d3570 100644 --- a/app/report.py +++ b/app/report.py @@ -14,11 +14,21 @@ from html import escape as _e from pathlib import Path from typing import Optional +from jinja2 import Environment, FileSystemLoader, select_autoescape + logger = logging.getLogger(__name__) from .models import Assessment, MATRIX_LABELS, EMPFEHLUNG_CONFIG from .bundeslaender import BUNDESLAENDER +# Eigene Jinja-Env fuer PDF-Templates (separat von Starlette templates, +# weil report.py auch von Hintergrund-Jobs ohne FastAPI-Request laufen muss). +_TEMPLATE_DIR = Path(__file__).parent / "templates" +_pdf_jinja = Environment( + loader=FileSystemLoader(str(_TEMPLATE_DIR)), + autoescape=select_autoescape(["html"]), +) + # ECOnGOOD Colors COLORS = { "darkgray": "#5a5a5a", @@ -512,6 +522,90 @@ async def generate_html_report( output_path.write_text(html) +async def generate_html_report_v3( + assessment: Assessment, + output_path: Path, + bundesland: Optional[str] = None, +) -> None: + """Render Antrags-PDF im neuen v3-Layout (single column, A4 portrait). + + Reuses die v3-Layout-Logik (Score-Hero, Matrix mit Achsen-Labels, + Programm-Treue, Verbesserungen) und ergaenzt sie um die im PDF + notwendigen Adaptionen: + + - Kein interaktiver Matrix-Klick → "Schwerpunkte erklaert"-Sektion + listet die Top-3 positiven und Top-3 negativen Felder mit ihren + LLM-generierten label/aspect-Texten unter der Matrix. + - Plenum-Votes werden best-effort geladen, inkl. Konsistenz-Hinweis + (Mehrheit deckt sich / gegen GWOE-Empfehlung). + - Online-Elemente (Share, Vote-treffend, Kommentare, News, Modals) + sind im Template gar nicht erst angelegt. + + Template: app/templates/v3/pdf/antrag_pdf.html + """ + matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix} + + # Schwerpunkt-Felder mit Erklaerung: Top + Bottom Ratings. + sorted_matrix = sorted( + assessment.gwoe_matrix, key=lambda e: e.rating, reverse=True + ) + matrix_top = [ + {"field": e.field, "label": e.label, "aspect": e.aspect, "rating": e.rating} + for e in sorted_matrix if e.rating > 0 + ][:4] + matrix_bottom = [ + {"field": e.field, "label": e.label, "aspect": e.aspect, "rating": e.rating} + for e in sorted(sorted_matrix, key=lambda e: e.rating) if e.rating < 0 + ][:4] + + # Score-Color (gleich wie Scorecard) + s = assessment.gwoe_score + if s >= 8: score_color = "#1a7f37" + elif s >= 5: score_color = "#bf6c10" + else: score_color = "#9a2a2a" + + parlament_name = "" + if bundesland and bundesland in BUNDESLAENDER: + parlament_name = BUNDESLAENDER[bundesland].parlament_name + + # Plenum-Votes best-effort (Hintergrund-Job kann ohne DB-Pfad laufen, + # in dem Fall einfach keine Votes anzeigen). + plenum_votes: list[dict] = [] + konsistenz_state: Optional[str] = None + konsistenz_decisive: Optional[str] = None + try: + from .database import get_plenum_votes + plenum_votes = await get_plenum_votes( + bundesland or "NRW", assessment.drucksache, + ) + if plenum_votes: + from .marker import consistency_state, decisive_outcome + konsistenz_state = consistency_state( + assessment.empfehlung.value, plenum_votes, + ) + konsistenz_decisive = decisive_outcome(plenum_votes) + except Exception as exc: + logger.warning( + "Plenum-Votes fuer PDF nicht ladbar (drucksache=%s): %s", + assessment.drucksache, exc, + ) + + template = _pdf_jinja.get_template("v3/pdf/antrag_pdf.html") + html = template.render( + assessment=assessment, + matrix_lookup=matrix_lookup, + matrix_top=matrix_top, + matrix_bottom=matrix_bottom, + score_color=score_color, + parlament_name=parlament_name, + bundesland=bundesland or "", + plenum_votes=plenum_votes, + konsistenz_state=konsistenz_state, + konsistenz_decisive=konsistenz_decisive, + ) + output_path.write_text(html) + + async def generate_pdf_report( assessment: Assessment, output_path: Path, @@ -535,9 +629,10 @@ async def generate_pdf_report( ``bundesland`` is forwarded to ``generate_html_report`` so the source parlament name appears in the report header. """ - # Step 1 — render the report itself + # Step 1 — render the report itself, neues v3-Layout (single column, + # Score-Hero, Matrix mit Achsen-Labels, Schwerpunkte-erklaert). html_path = output_path.with_suffix('.tmp.html') - await generate_html_report(assessment, html_path, bundesland=bundesland) + await generate_html_report_v3(assessment, html_path, bundesland=bundesland) try: from weasyprint import HTML diff --git a/app/templates/v2/screens/admin_stand.html b/app/templates/v2/screens/admin_stand.html index 3a908fd..c3fcb98 100644 --- a/app/templates/v2/screens/admin_stand.html +++ b/app/templates/v2/screens/admin_stand.html @@ -180,6 +180,16 @@

Lade aktuelles Beispiel …

+ +

Design-Werkstätten

+

+ Live-Editoren für Layout-Iteration ohne Server-Redeploy: +

+ {% endblock %} diff --git a/app/templates/v2/screens/antrag_detail.html b/app/templates/v2/screens/antrag_detail.html index bd6138a..ec0a649 100644 --- a/app/templates/v2/screens/antrag_detail.html +++ b/app/templates/v2/screens/antrag_detail.html @@ -484,8 +484,9 @@ - {# ── Share-Block (analog v1) ───────────────────────────────────── #} -
+ {# ── Share-Block (analog v1) — nur fuer angemeldete User sichtbar, + wird in initAuth() ein-/ausgeblendet (display:none default). ── #} + - {# Teilen #} -
+ {# Teilen — nur fuer angemeldete User; initAuth() blendet via #v2-share-block ein. #} +