Generate a PDF certificate and email it to the learner on issuance#706
Generate a PDF certificate and email it to the learner on issuance#706payamnj wants to merge 2 commits into
Conversation
Certificates were previously only viewable as an in-browser page after a learner submitted the "enter your name" form. Add a PDF generation path (WeasyPrint, optional "certificates" extra) that's shared between: - A background job (send_certificate_pdfs) that emails the PDF, deferred via a swappable TaskQueueProtocol[Certificate] queue (CERTIFICATE_PDF_QUEUE setting), mirroring the existing newsletter sendout queue architecture. - An on-demand download endpoint at personalised/certificate/<certificate_number>/download/. Certificate gains pdf_email_status/pdf_email_retry_count/pdf_email_sent_at to track delivery, with retries up to CERTIFICATES.MAX_RETRIES (default 3). A data migration marks all pre-existing certificates as already-sent so upgrading doesn't retroactively email a PDF for every certificate ever issued — only certificates issued after the upgrade are queued. Closes #705 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Closing for now — pausing this ticket to reconsider the PDF-generation approach. Why: WeasyPrint requires native system libraries (Pango/Cairo/GObject/HarfBuzz) that pip does not install. Running the background job locally surfaced this directly: ``` For a `pip install`-able library, requiring Homebrew/apt system packages just to render a certificate is too much friction, and we also want the PDF to look exactly like the certificate the learner actually sees (`Certificate.jsx`, rendered client-side with CSS gradients/blend-modes/flexbox) — which no server-side redraw (WeasyPrint's simplified HTML/CSS template, or a from-scratch reportlab/xhtml2pdf recreation) can achieve without duplicating that design and accepting a visual gap. Full decision history and the direction being considered next are written up on #705. The branch (`feature/certificate-pdf-email`) is left in place, unmerged, for reference if we return to the WeasyPrint approach later. |
Summary
Certificates were previously only viewable as an in-browser page after a learner submitted the "enter your name" form — no PDF existed, nothing was emailed. This adds a shared PDF-generation path used by both an automated email delivery job and an on-demand download endpoint.
services/certificate_pdf_service.py:generate_certificate_pdf(certificate) -> bytes, rendering a newcertificates/certificate_pdf.htmltemplate via WeasyPrint (A4 landscape), with the organization logo and a verification QR code embedded as base64 data URIs (no network/storage dependency at render time). WeasyPrint is an optional dependency (pip install django-email-learning[certificates]), since it pulls in system libraries (Pango/Cairo/HarfBuzz) that not every installation needs.send_certificate_pdfs), deferred via a swappableTaskQueueProtocol[Certificate]queue, mirroring the existing newsletter sendout architecture exactly:DatabaseCertificatePdfQueue(default) claims certificates withpdf_email_status=PENDINGviaselect_for_update(skip_locked=True).resolve_queue()helper, keyed by a newCERTIFICATE_PDF_QUEUEsetting — swap in your own queue (Pub/Sub, SQS, etc.) by implementing the protocol and pointing the setting at it, exactly likeSENDOUT_QUEUE.CERTIFICATES.MAX_RETRIES(default 3) on failure, then marks the certificateFAILED.send_certificate_pdfs) and HTTP job-trigger endpoint (/api/jobs/send_certificate_pdfs/), matching every other job in this codebase.GET .../personalised/certificate/<certificate_number>/download/, reusing the samegenerate_certificate_pdf()function (regenerated fresh each request, not cached) so there's one source of truth for what a certificate PDF looks like, not two divergent rendering paths.pdf_email_status/pdf_email_retry_count/pdf_email_sent_atto track delivery state, mirroringSendoutDelivery.Upgrade safety
A data migration (
0003_certificate_pdf_email_status) marks all pre-existing certificates as already-sent at migration time, so upgrading does not retroactively email a PDF for every certificate ever issued — only certificates issued after the upgrade get queued for a PDF email. New certificates default toPENDINGvia the model field default, so the trigger is implicit:Certificate.objects.get_or_create(...)inSubmitCertificateFormView.postalready sets it correctly, no extra code needed there.Design notes
generate_certificate_pdf's actualHTML(...).write_pdf()call is isolated into a small_render_html_to_pdf()function so it can be mocked in tests without requiring those system libraries in CI. Real end-to-end PDF rendering should be verified in an environment with WeasyPrint's system dependencies installed.CertificateView.get()into a shared_lookup_certificate()helper, reused by the new download view.Closes #705
Test plan
pytest -q— 796 passed, 81.45% coveragemypy— no issues across 168 source filesruff check/ruff format— cleanpython manage.py makemigrations --check— no driftbandit— no new findings (2 pre-existing low-severity findings, unrelated to this change)CertificatePDF-status model defaults,generate_certificate_pdf(),DatabaseCertificatePdfQueueclaiming/batching, the background job's success/retry/permanent-failure paths, the job-trigger HTTP endpoint and management command, and the download view (success, not-found, invalid format)🤖 Generated with Claude Code