add PDF export (FR10)

pdf_export.py builds a PDF report (summary stats, AGP chart, hypo/hyper
event summaries) via fpdf2 + matplotlib rather than plotly's kaleido, to
avoid bundling a Chromium build into the image just for this. The Export
page lets the user pick which sections to include and a date range, then
download the result to send to the hospital team.

This completes all ten functional requirements in
docs/requirements-reporting.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-07-26 21:10:46 +02:00
co-authored by Claude Sonnet 5
parent 9929f44daa
commit fe6a471f17
4 changed files with 229 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
from datetime import date
import pandas as pd
from pdf_export import SECTION_AGP, SECTION_EVENTS, SECTION_SUMMARY, build_report_pdf
def _sample_glucose():
timestamps = pd.date_range("2026-01-01", periods=48, freq="30min")
return pd.DataFrame({"timestamp": timestamps, "mmol": [6.0 + (i % 5) for i in range(48)]})
def test_build_report_pdf_produces_valid_pdf_bytes():
glucose = _sample_glucose()
events_summary = {
"Hypo Level 1": {"count": 2, "avg_duration_minutes": 20.0, "events_per_week": 1.0},
}
pdf_bytes = build_report_pdf(
[SECTION_SUMMARY, SECTION_AGP, SECTION_EVENTS],
glucose,
events_summary,
date(2026, 1, 1),
date(2026, 1, 2),
)
assert isinstance(pdf_bytes, bytes)
assert pdf_bytes.startswith(b"%PDF")
assert len(pdf_bytes) > 500
def test_build_report_pdf_handles_empty_glucose():
empty = pd.DataFrame(columns=["timestamp", "mmol"])
pdf_bytes = build_report_pdf(
[SECTION_SUMMARY, SECTION_AGP], empty, {}, date(2026, 1, 1), date(2026, 1, 2)
)
assert pdf_bytes.startswith(b"%PDF")
def test_build_report_pdf_handles_no_sections_selected():
glucose = _sample_glucose()
pdf_bytes = build_report_pdf([], glucose, {}, date(2026, 1, 1), date(2026, 1, 2))
assert pdf_bytes.startswith(b"%PDF")