fix(#179): Scorecard via WeasyPrint-PDF (Container hat kein Chromium)

Container hat keine Playwright/Chromium-Installation — PNG-Render
deferred. WeasyPrint-PDF mit exaktem Seiten-Setup (1200x630pt fuer
og, 1080x1080pt fuer square) liefert jetzt /api/assessment/scorecard.pdf.

Folge-Issue fuer PNG: Chromium ins Image bauen oder pypdfium2/pdf2image
fuer pdf→png-Konvertierung ergaenzen.
This commit is contained in:
Dotty Dotter 2026-05-06 23:46:01 +02:00
parent 1faf4e9220
commit 727d7d2976

View File

@ -3413,50 +3413,67 @@ async def scorecard_template(
}) })
@app.get("/api/assessment/scorecard.png") @app.get("/api/assessment/scorecard.pdf")
async def api_scorecard_png( async def api_scorecard_pdf(
drucksache: str, bundesland: str = "NRW", format: str = "og", drucksache: str, bundesland: str = "NRW", format: str = "og",
): ):
"""Liefert die Scorecard als PNG via Playwright-Render (#179). """Liefert die Scorecard als PDF via WeasyPrint (#179).
`format=og` 1200×630, `format=square` 1080×1080. `format=og` 1200×630pt, `format=square` 1080×1080pt.
PNG-Render via Playwright wird in einem Folge-Issue ergänzt
(Container hat heute kein Chromium installiert).
""" """
from fastapi.responses import Response as _Response
from weasyprint import HTML, CSS
drucksache = validate_drucksache(drucksache) drucksache = validate_drucksache(drucksache)
row = await get_assessment(drucksache) row = await get_assessment(drucksache)
if not row: if not row:
raise HTTPException(status_code=404, detail="Antrag nicht gefunden") raise HTTPException(status_code=404, detail="Antrag nicht gefunden")
if format not in ("og", "square"): if format not in ("og", "square"):
raise HTTPException(status_code=400, detail="format muss 'og' oder 'square' sein") raise HTTPException(status_code=400, detail="format muss 'og' oder 'square' sein")
width, height = ((1200, 630) if format == "og" else (1080, 1080)) width, height = ((1200, 630) if format == "og" else (1080, 1080))
try: # HTML aus dem internen Template ziehen (gleiche Logik wie /v2/scorecard).
from playwright.sync_api import sync_playwright from .models import Assessment
import urllib.parse as _up assessment = Assessment.model_validate(row)
url = ( matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix}
f"http://127.0.0.1:{settings.port}/v2/scorecard" fraktionen = list(row.get("fraktionen", []) or [])
f"?drucksache={_up.quote(drucksache, safe='')}" if isinstance(fraktionen, str):
f"&bundesland={_up.quote(bundesland)}" try:
f"&format={format}" import json as _json
) fraktionen = _json.loads(fraktionen) or []
with sync_playwright() as pw: except Exception:
browser = pw.chromium.launch(args=["--no-sandbox"]) fraktionen = []
page = browser.new_page(viewport={"width": width, "height": height})
page.goto(url, wait_until="networkidle", timeout=15000)
png = page.screenshot(
clip={"x": 0, "y": 0, "width": width, "height": height}, type="png",
)
browser.close()
except Exception as e:
logger.exception("Scorecard-Render fehlgeschlagen")
raise HTTPException(status_code=500, detail=f"Render-Fehler: {e}")
score = assessment.gwoe_score
if score >= 8: score_color = "#1a7f37"
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,
)
# PDF mit exakt einer Seite in der Zielgrösse
page_size_css = f"@page {{ size: {width}px {height}px; margin: 0; }}"
pdf = HTML(string=html_content).write_pdf(stylesheets=[CSS(string=page_size_css)])
safe_name = drucksache.replace("/", "-") safe_name = drucksache.replace("/", "-")
return Response( return _Response(
content=png, media_type="image/png", content=pdf, media_type="application/pdf",
headers={ headers={
"Content-Disposition": ( "Content-Disposition": (
f'inline; filename="scorecard-{safe_name}-{format}.png"' f'inline; filename="scorecard-{safe_name}-{format}.pdf"'
), ),
"Cache-Control": "public, max-age=600", "Cache-Control": "public, max-age=600",
}, },