[32788f42] feat(list-generator): create React app and FastAPI backend for PDF list generation
This commit is contained in:
0
list-generator/backend/app/__init__.py
Normal file
0
list-generator/backend/app/__init__.py
Normal file
19
list-generator/backend/app/main.py
Normal file
19
list-generator/backend/app/main.py
Normal 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"}
|
||||
0
list-generator/backend/app/routers/__init__.py
Normal file
0
list-generator/backend/app/routers/__init__.py
Normal file
46
list-generator/backend/app/routers/generate.py
Normal file
46
list-generator/backend/app/routers/generate.py
Normal 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))
|
||||
0
list-generator/backend/app/services/__init__.py
Normal file
0
list-generator/backend/app/services/__init__.py
Normal file
45
list-generator/backend/app/services/pdf_generator.py
Normal file
45
list-generator/backend/app/services/pdf_generator.py
Normal 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
|
||||
42
list-generator/backend/app/templates/school_list.html
Normal file
42
list-generator/backend/app/templates/school_list.html
Normal 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>
|
||||
Reference in New Issue
Block a user