[32788f42] feat(list-generator): create React app and FastAPI backend for PDF list generation

This commit is contained in:
2026-03-18 19:20:59 +00:00
parent 16cd760dac
commit 21c8ff66fd
30 changed files with 4525 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info fonts-liberation fontconfig && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

View File

@@ -0,0 +1,19 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import generate
app = FastAPI(title="List Generator API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(generate.router, prefix="/api")
@app.get("/health")
def health_check():
return {"status": "ok"}

View File

@@ -0,0 +1,46 @@
from fastapi import APIRouter, File, UploadFile, Form, HTTPException
from fastapi.responses import FileResponse
from typing import Optional
import os
import shutil
import tempfile
from ..services.pdf_generator import generate_school_pdf
router = APIRouter()
@router.post("/generate-list")
async def generate_list(
institution: str = Form(...),
date_info: str = Form(...),
list_type: str = Form("k"),
students_file: UploadFile = File(...),
families_file: Optional[UploadFile] = File(None)
):
try:
with tempfile.TemporaryDirectory() as temp_dir:
students_path = os.path.join(temp_dir, "students.csv")
with open(students_path, "wb") as buffer:
shutil.copyfileobj(students_file.file, buffer)
families_path = None
if families_file:
families_path = os.path.join(temp_dir, "families.csv")
with open(families_path, "wb") as buffer:
shutil.copyfileobj(families_file.file, buffer)
pdf_path = generate_school_pdf(
institution=institution,
date_info=date_info,
list_type=list_type,
students_csv_path=students_path,
families_csv_path=families_path,
output_dir=temp_dir
)
if not pdf_path or not os.path.exists(pdf_path):
raise HTTPException(status_code=500, detail="PDF generation failed")
with open(pdf_path, "rb") as f:
pdf_bytes = f.read()
final_output_path = os.path.join("/tmp", os.path.basename(pdf_path))
with open(final_output_path, "wb") as f:
f.write(pdf_bytes)
return FileResponse(path=final_output_path, filename=os.path.basename(pdf_path), media_type="application/pdf")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,45 @@
import pandas as pd
import os
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
import datetime
def generate_school_pdf(institution: str, date_info: str, list_type: str, students_csv_path: str, families_csv_path: str = None, output_dir: str = "/tmp") -> str:
# Read CSV data
df = pd.read_csv(students_csv_path, sep=";")
# Clean column names
df.columns = df.columns.str.strip()
# Group by class
grouped = df.groupby("Klasse")
class_data = []
for class_name, group in grouped:
students = group.to_dict("records")
class_data.append({"name": class_name, "students": students})
# Prepare summary data
class_counts = [{"name": c, "count": len(g)} for c, g in grouped]
total_students = sum(c["count"] for c in class_counts)
# Setup Jinja2 environment
template_dir = os.path.join(os.path.dirname(__file__), "..", "templates")
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("school_list.html")
# Render HTML
current_time = datetime.datetime.now().strftime("%d.%m.%Y %H:%M Uhr")
html_out = template.render(
institution=institution,
date_info=date_info,
class_counts=class_counts,
total_students=total_students,
class_data=class_data,
current_time=current_time
)
# Generate PDF
output_filename = f"Listen_{institution.replace( , _)}_{list_type}_{datetime.datetime.now().strftime(%Y-%m-%d_%H-%M)}.pdf"
output_path = os.path.join(output_dir, output_filename)
HTML(string=html_out).write_pdf(output_path)
return output_path

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
@page { size: A4 portrait; margin: 20mm; }
body { font-family: sans-serif; font-size: 11pt; }
.header { margin-bottom: 20px; }
.institution-name { font-weight: bold; font-size: 14pt; }
.date-info { font-size: 12pt; }
.summary { margin-top: 30px; }
.summary h2 { font-size: 12pt; font-weight: normal; margin-bottom: 10px; }
.summary-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
.summary-table td { padding: 4px 0; }
.summary-total { margin-top: 10px; border-top: 1px solid black; padding-top: 10px; font-weight: bold; }
.class-section { page-break-before: always; }
.student-table { width: 100%; border-collapse: collapse; margin-top: 30px; }
.student-table th { text-align: left; border-bottom: 1px solid black; padding-bottom: 5px; font-weight: normal; }
.student-table td { padding: 5px 0; }
.class-summary { margin-top: 30px; font-weight: bold; }
.class-note { margin-top: 20px; font-size: 10pt; }
.footer { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 10pt; }
.footer-left { text-align: left; }
.footer-right { text-align: right; white-space: pre-wrap; }
</style></head><body>
<div class="header"><div class="institution-name">{{ institution }}</div><div class="date-info">{{ date_info }}</div></div>
<div class="summary"><h2>Übersicht der Anmeldungen:</h2><table class="summary-table">
{% for count in class_counts %}
<tr><td style="width: 50%;">Klasse {{ count.name }}</td><td>{{ count.count }} Anmeldungen</td></tr>
{% endfor %}
</table><div class="summary-total">Gesamt: {{ total_students }} Anmeldungen</div></div>
{% for class_info in class_data %}
<div class="class-section">
<div class="header"><div class="institution-name">{{ institution }}</div><div class="date-info">{{ date_info }}</div></div>
<table class="student-table"><thead><tr><th style="width: 40%">Nachname</th><th style="width: 40%">Vorname</th><th style="width: 20%">Klasse</th></tr></thead><tbody>
{% for student in class_info.students %}
<tr><td>{{ student.Nachname }}</td><td>{{ student.Vorname }}</td><td>{{ student.Klasse }}</td></tr>
{% endfor %}
</tbody></table>
<div class="class-summary">{{ class_info.students|length }} angemeldete Kinder</div>
<div class="class-note">Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden<br>Schüler an die Anmeldung erinnern.</div>
</div>
{% endfor %}
<div class="footer"><div class="footer-left">Stand {{ current_time }}</div><div class="footer-right">Kinderfotos Erding\nGartenstr. 10 85445 Oberding\nwww.kinderfotos-erding.de\n08122-8470867</div></div>
</body></html>

View File

@@ -0,0 +1,6 @@
fastapi[all]==0.111.0
uvicorn==0.30.1
pandas==2.2.2
python-multipart==0.0.9
weasyprint==62.1
jinja2==3.1.4

View File

@@ -0,0 +1,3 @@
import pandas as pd
df = pd.DataFrame({'Nachname': ['Müller', 'Schmidt'], 'Vorname': ['Max', 'Anna'], 'Klasse': ['1a', '1b']})
df.to_csv('test.csv', sep=';', index=False)