Source code for django_program.registration.letter

"""Visa invitation letter request model for conference attendees.

Tracks the lifecycle of invitation letter requests from submission through
review, PDF generation, and delivery. Used by attendees who need a formal
invitation letter for visa applications.
"""

from django.conf import settings
from django.db import models


[docs] class LetterRequest(models.Model): """A request for a visa invitation letter from a conference attendee. Captures passport and travel details needed to produce a formal letter for embassy submission. Progresses through a review workflow from ``SUBMITTED`` to ``SENT`` (or ``REJECTED``). """
[docs] class Status(models.TextChoices): """Workflow states for a letter request.""" SUBMITTED = "submitted", "Submitted" UNDER_REVIEW = "under_review", "Under Review" APPROVED = "approved", "Approved" GENERATED = "generated", "Generated" SENT = "sent", "Sent" REJECTED = "rejected", "Rejected"
ALLOWED_TRANSITIONS: dict[str, set[str]] = { Status.SUBMITTED: {Status.UNDER_REVIEW, Status.APPROVED, Status.REJECTED}, Status.UNDER_REVIEW: {Status.APPROVED, Status.REJECTED}, Status.APPROVED: {Status.GENERATED}, Status.GENERATED: {Status.SENT}, } conference = models.ForeignKey( "program_conference.Conference", on_delete=models.CASCADE, related_name="letter_requests", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="letter_requests", ) attendee = models.ForeignKey( "program_registration.Attendee", on_delete=models.SET_NULL, null=True, blank=True, related_name="letter_requests", ) passport_name = models.CharField(max_length=300) passport_number = models.CharField(max_length=50) nationality = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) travel_from = models.DateField() travel_until = models.DateField() destination_address = models.TextField() embassy_name = models.CharField(max_length=300, blank=True, default="") status = models.CharField( max_length=20, choices=Status.choices, default=Status.SUBMITTED, ) rejection_reason = models.TextField(blank=True, default="") generated_pdf = models.FileField(upload_to="letters/", blank=True, null=True) reviewed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="reviewed_letter_requests", ) reviewed_at = models.DateTimeField(null=True, blank=True) sent_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-created_at"] unique_together = [("user", "conference")] def __str__(self) -> str: return f"{self.passport_name}{self.conference} ({self.get_status_display()})"
[docs] def transition_to(self, new_status: str) -> None: """Transition this request to a new workflow status. Validates that the transition is allowed before applying it. Args: new_status: The target status value (one of ``Status`` choices). Raises: ValueError: If the transition from the current status to ``new_status`` is not permitted. """ allowed = self.ALLOWED_TRANSITIONS.get(str(self.status), set()) if new_status not in allowed: msg = ( f"Cannot transition from '{self.get_status_display()}' to '{new_status}'. Allowed: {allowed or 'none'}" ) raise ValueError(msg) self.status = new_status