Spaces:
Sleeping
Sleeping
Apiarist Dev
fix: PDF notes wrap inside table; hide trend chart until 2+ inspections (no empty box)
b378fd6 | """ | |
| PDF report generator for an apiary's weekly inspections. | |
| Pulls inspection rows from SQLite (db.py) and renders a clean one-page | |
| report per hive plus a summary page. Uses reportlab, pure Python, no | |
| system deps, ships fine on HF Spaces. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from io import BytesIO | |
| from pathlib import Path | |
| from reportlab.lib import colors | |
| from reportlab.lib.pagesizes import LETTER | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch | |
| from reportlab.platypus import ( | |
| SimpleDocTemplate, | |
| Paragraph, | |
| Spacer, | |
| Table, | |
| TableStyle, | |
| PageBreak, | |
| ) | |
| import db | |
| def _styles(): | |
| base = getSampleStyleSheet() | |
| base.add( | |
| ParagraphStyle( | |
| name="ApiaristTitle", | |
| parent=base["Heading1"], | |
| fontSize=22, | |
| textColor=colors.HexColor("#f4a300"), | |
| spaceAfter=12, | |
| ) | |
| ) | |
| base.add( | |
| ParagraphStyle( | |
| name="ApiaristH2", | |
| parent=base["Heading2"], | |
| fontSize=14, | |
| textColor=colors.HexColor("#1a1410"), | |
| spaceBefore=10, | |
| spaceAfter=6, | |
| ) | |
| ) | |
| base.add( | |
| ParagraphStyle( | |
| name="ApiaristSmall", | |
| parent=base["BodyText"], | |
| fontSize=9, | |
| textColor=colors.HexColor("#555"), | |
| ) | |
| ) | |
| return base | |
| def _date(epoch: float | None) -> str: | |
| if not epoch: | |
| return "-" | |
| return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch)) | |
| def _hive_table(inspections: list[dict]) -> Table: | |
| # Wrap the long Notes text in a Paragraph so it wraps inside the column | |
| # instead of overflowing the table edge. | |
| cell_style = ParagraphStyle( | |
| name="cell", fontSize=8, leading=10, | |
| textColor=colors.HexColor("#1a1410"), | |
| ) | |
| hdr = [ | |
| Paragraph(f"<b>{h}</b>", ParagraphStyle("h", fontSize=9, textColor=colors.HexColor("#1a1410"))) | |
| for h in ["Date", "Queen?", "Mites", "Swarm?", "Health", "Notes"] | |
| ] | |
| rows = [hdr] | |
| for i in inspections: | |
| rows.append( | |
| [ | |
| Paragraph(_date(i["created_at"]), cell_style), | |
| "Y" if i["queen_detected"] else "N", | |
| str(i["varroa_mites_visible"] or 0), | |
| "Y" if i["swarm_cells_detected"] else "N", | |
| i["frame_health"] or "-", | |
| Paragraph((i["notes"] or "")[:160], cell_style), | |
| ] | |
| ) | |
| t = Table(rows, repeatRows=1, colWidths=[1.05 * inch] + [0.6 * inch] * 4 + [2.9 * inch]) | |
| t.setStyle( | |
| TableStyle( | |
| [ | |
| ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#f4a300")), | |
| ("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#1a1410")), | |
| ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), | |
| ("FONTSIZE", (0, 0), (-1, -1), 9), | |
| ("BOTTOMPADDING", (0, 0), (-1, 0), 6), | |
| ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f7f1e0")]), | |
| ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#cccccc")), | |
| ("VALIGN", (0, 0), (-1, -1), "TOP"), | |
| ] | |
| ) | |
| ) | |
| return t | |
| def generate_report() -> bytes: | |
| """Build a PDF report covering every hive in the registry. Returns bytes.""" | |
| hives = db.list_hives() | |
| styles = _styles() | |
| buf = BytesIO() | |
| doc = SimpleDocTemplate( | |
| buf, | |
| pagesize=LETTER, | |
| rightMargin=0.6 * inch, | |
| leftMargin=0.6 * inch, | |
| topMargin=0.6 * inch, | |
| bottomMargin=0.6 * inch, | |
| title="Apiarist Inspection Report", | |
| author="Apiarist", | |
| ) | |
| story = [] | |
| story.append(Paragraph(" Apiarist Inspection Report", styles["ApiaristTitle"])) | |
| story.append( | |
| Paragraph( | |
| f"Generated {time.strftime('%Y-%m-%d %H:%M')}", | |
| styles["ApiaristSmall"], | |
| ) | |
| ) | |
| story.append(Spacer(1, 0.2 * inch)) | |
| total_inspections = sum((h.get("inspection_count") or 0) for h in hives) | |
| summary_lines = [ | |
| f"<b>Total hives:</b> {len(hives)}", | |
| f"<b>Total inspections:</b> {total_inspections}", | |
| ] | |
| for line in summary_lines: | |
| story.append(Paragraph(line, styles["BodyText"])) | |
| story.append(Spacer(1, 0.3 * inch)) | |
| if not hives: | |
| story.append( | |
| Paragraph( | |
| "No hives have been registered yet. Add one in the Hives tab " | |
| "and inspect a frame to populate this report.", | |
| styles["BodyText"], | |
| ) | |
| ) | |
| doc.build(story) | |
| return buf.getvalue() | |
| for idx, h in enumerate(hives): | |
| story.append(Paragraph(h["name"], styles["ApiaristH2"])) | |
| meta_bits = [] | |
| if h.get("location"): | |
| meta_bits.append(f"Location: {h['location']}") | |
| if h.get("queen_marker"): | |
| meta_bits.append(f"Queen marker: {h['queen_marker']}") | |
| meta_bits.append(f"Inspections: {h.get('inspection_count') or 0}") | |
| meta_bits.append(f"Last inspected: {_date(h.get('last_inspected'))}") | |
| story.append(Paragraph(" • ".join(meta_bits), styles["ApiaristSmall"])) | |
| story.append(Spacer(1, 0.1 * inch)) | |
| inspections = db.get_inspections_for_hive(h["id"], limit=20) | |
| if inspections: | |
| story.append(_hive_table(inspections)) | |
| else: | |
| story.append( | |
| Paragraph("<i>No inspections recorded.</i>", styles["BodyText"]) | |
| ) | |
| if idx != len(hives) - 1: | |
| story.append(PageBreak()) | |
| doc.build(story) | |
| return buf.getvalue() | |
| def save_report(path: Path) -> Path: | |
| pdf_bytes = generate_report() | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_bytes(pdf_bytes) | |
| return path | |