Files
training-software/certificates/tasks.py
Paperclip CTO b3a7537364
Some checks failed
CI / Tests (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / OpenAPI Schema (push) Has been cancelled
feat(TRA-242): async certificate generation pipeline with hash-based verification
- Certificate model: serial_number (unique), status (pending/rendering/completed/failed),
  pdf_path, pdf_hash (SHA-256), verification_url, render_attempts, training_record FK
- renderer.py: LaTeX template → pdflatex subprocess → PDF; _escape_latex for XSS safety;
  compute_pdf_hash for immutable verification metadata
- services.py: issue_certificate() generates serial, creates record, enqueues Celery task
- tasks.py: render_certificate_task (bind=True, autoretry 3x with 60s backoff);
  sets RENDERING → COMPLETED with hash; FAILED with error on all retries exhausted
- API: /certificates/ (own list), /certificates/{id}/ (own detail)
- 15 unit + integration tests: hash consistency, LaTeX escaping, mocked render, retry behavior

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-07 09:22:56 +02:00

57 lines
2.0 KiB
Python

from celery import shared_task
@shared_task(
name="certificates.render_certificate",
bind=True,
autoretry_for=(Exception,),
retry_kwargs={"max_retries": 3, "countdown": 60},
)
def render_certificate_task(self, certificate_id: str):
"""
Async task: render a certificate PDF, compute its hash, and persist the result.
Retried up to 3 times on failure with 60s delay.
"""
from django.utils.timezone import now
from .models import Certificate, CertificateStatus
from .renderer import RenderError, compute_pdf_hash, render_certificate_pdf
from .services import get_pdf_storage_path
try:
cert = Certificate.objects.select_related("user", "course").get(pk=certificate_id)
except Certificate.DoesNotExist:
return {"error": f"Certificate {certificate_id} not found"}
cert.status = CertificateStatus.RENDERING
cert.render_attempts += 1
cert.save(update_fields=["status", "render_attempts", "updated_at"])
context = {
"recipient_name": getattr(cert.user, "display_name", "") or cert.user.email,
"course_title": cert.course.title,
"issued_date": now().strftime("%B %d, %Y"),
"serial_number": cert.serial_number,
"verification_url": cert.verification_url,
}
output_path = get_pdf_storage_path(cert)
try:
render_certificate_pdf(context, output_path)
pdf_hash = compute_pdf_hash(output_path)
except RenderError as exc:
cert.status = CertificateStatus.FAILED
cert.render_error = str(exc)
cert.save(update_fields=["status", "render_error", "updated_at"])
raise # triggers Celery retry
cert.status = CertificateStatus.COMPLETED
cert.pdf_path = output_path
cert.pdf_hash = pdf_hash
cert.issued_at = now()
cert.render_error = ""
cert.save(update_fields=["status", "pdf_path", "pdf_hash", "issued_at", "render_error", "updated_at"])
return {"certificate_id": certificate_id, "serial": cert.serial_number, "hash": pdf_hash}