- 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>
48 lines
1.7 KiB
Python
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}"
|