Add support for PDF view
This commit is contained in:
@@ -6,7 +6,7 @@ from functools import partial
|
||||
from typing import Annotated
|
||||
|
||||
import starlette.status as status
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
@@ -21,6 +21,7 @@ from meal_manager.models import (
|
||||
Subscription,
|
||||
TeamRegistration,
|
||||
)
|
||||
from meal_manager.pdf import build_dinner_overview_pdf
|
||||
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
@@ -332,3 +333,21 @@ async def delete_team_registration(
|
||||
session.delete(session.scalars(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
@app.get("/event/{event_id}/pdf")
|
||||
def get_event_attendance_pdf(event_id: int, session: SessionDep):
|
||||
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
pdf_buffer = build_dinner_overview_pdf(event)
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": f"inline; filename=attendance_event_{event_id}.pdf"
|
||||
}
|
||||
|
||||
return Response(
|
||||
content=pdf_buffer.getvalue(),
|
||||
media_type="application/pdf",
|
||||
headers=headers
|
||||
)
|
||||
101
src/meal_manager/pdf.py
Normal file
101
src/meal_manager/pdf.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4, portrait
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||||
|
||||
from meal_manager.models import Event
|
||||
|
||||
|
||||
def build_dinner_overview_pdf(event: Event) -> BytesIO:
|
||||
"""Build a PDF with an overview of the event's attendance."""
|
||||
# Create an in-memory PDF
|
||||
buffer = BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=portrait(A4), topMargin=30, bottomMargin=30, leftMargin=40, rightMargin=40)
|
||||
styles = getSampleStyleSheet()
|
||||
elements = []
|
||||
|
||||
# Title
|
||||
title_style = styles["Title"]
|
||||
title_style.fontSize = 16
|
||||
title_style.spaceAfter = 20
|
||||
elements.append(Paragraph(f"Anwesenheitsliste – {event.title} ({event.event_time.date().strftime('%d.%m.%y')})",
|
||||
title_style))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Team overview section
|
||||
elements.append(Paragraph("Dienste", styles["Heading2"]))
|
||||
elements.append(Spacer(1, 12))
|
||||
|
||||
team_data = [["Team", "Personen"]]
|
||||
team_types = {
|
||||
"Kochen": [r for r in event.team if r.work_type == "cooking"],
|
||||
"Abwaschen": [r for r in event.team if r.work_type == "dishes"],
|
||||
"Tische decken": [r for r in event.team if r.work_type == "tables"]
|
||||
}
|
||||
|
||||
for team_name, registrations in team_types.items():
|
||||
members = ", ".join(r.person_name for r in registrations)
|
||||
team_data.append([team_name, members])
|
||||
|
||||
team_table = Table(team_data, repeatRows=1, colWidths=[100, '*'])
|
||||
team_table.setStyle(TableStyle([
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor('#E8E8E8')),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor('#A0A0A0')),
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 8),
|
||||
]))
|
||||
|
||||
elements.append(team_table)
|
||||
elements.append(Spacer(1, 25))
|
||||
|
||||
sum_adults = 0
|
||||
sum_children = 0
|
||||
sum_small_children = 0
|
||||
for r in event.registrations:
|
||||
sum_adults += r.num_adult_meals
|
||||
sum_children += r.num_children_meals
|
||||
sum_small_children += r.num_small_children_meals
|
||||
|
||||
# Attendance section
|
||||
elements.append(Paragraph("Teilnehmende", styles["Heading2"]))
|
||||
elements.append(Paragraph(f"Gesamt: {sum_adults} Erwachsene, {sum_children} Kinder, {sum_small_children} Kleinkinder"))
|
||||
elements.append(Spacer(1, 12))
|
||||
|
||||
# Table header
|
||||
data = [["Haushalt", "Erwachsene", "Kinder >7", "Kinder <7", "Kommentar", "Anwesend?"]]
|
||||
|
||||
# Table rows
|
||||
for r in event.registrations:
|
||||
data.append([
|
||||
r.household.name,
|
||||
r.num_adult_meals,
|
||||
r.num_children_meals,
|
||||
r.num_small_children_meals,
|
||||
Paragraph(r.comment),
|
||||
""
|
||||
])
|
||||
for _ in range(5):
|
||||
data.append([""] * 6)
|
||||
|
||||
# Create table
|
||||
table = Table(data, repeatRows=1, colWidths=[120, 70, 60, 60, '*', 65])
|
||||
table.setStyle(TableStyle([
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor('#E8E8E8')),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor('#A0A0A0')),
|
||||
("ALIGN", (1, 1), (-2, -1), "CENTER"),
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 8),
|
||||
]))
|
||||
|
||||
elements.append(table)
|
||||
doc.build(elements)
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
@@ -34,6 +34,9 @@
|
||||
<i class="bi bi-book"></i> Original Rezept ansehen
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/event/{{event.id}}/pdf" class="btn btn-secondary mb-2 w-100" target="_blank">
|
||||
<i class="bi bi-printer m-2"></i> Druckansicht
|
||||
</a>
|
||||
{% if user and user.admin %}
|
||||
<button type="button" class="btn btn-danger mb-2 w-100" data-bs-toggle="modal" data-bs-target="#deleteEvent">
|
||||
Event Löschen
|
||||
|
||||
Reference in New Issue
Block a user