Files
training-software/certificates/models.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

48 lines
1.7 KiB
Python

import uuid
from django.conf import settings
from django.db import models
class CertificateStatus(models.TextChoices):
PENDING = "pending", "Pending"
RENDERING = "rendering", "Rendering"
COMPLETED = "completed", "Completed"
FAILED = "failed", "Failed"
class Certificate(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="certificates"
)
course = models.ForeignKey(
"courses.Course", on_delete=models.CASCADE, related_name="certificates"
)
training_record = models.OneToOneField(
"training.TrainingRecord",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="certificate",
)
serial_number = models.CharField(max_length=50, unique=True, db_index=True)
issued_at = models.DateTimeField(null=True, blank=True)
status = models.CharField(
max_length=20, choices=CertificateStatus.choices, default=CertificateStatus.PENDING
)
pdf_path = models.CharField(max_length=1000, blank=True)
pdf_hash = models.CharField(max_length=128, blank=True) # SHA-256 hex
verification_url = models.CharField(max_length=500, blank=True)
render_attempts = models.PositiveSmallIntegerField(default=0)
render_error = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "certificates_certificate"
ordering = ["-issued_at"]
def __str__(self):
return f"Certificate {self.serial_number} for {self.user_id}"