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:
parent
1faf4e9220
commit
727d7d2976
73
app/main.py
73
app/main.py
@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user