"""Attendee profile models for conference registration.
Provides an abstract base for projects that need custom attendee profile fields,
and a concrete ``Attendee`` model that links users to conferences with check-in
tracking and access codes.
"""
import secrets
import string
from django.conf import settings
from django.db import models
[docs]
def generate_access_code(*, max_retries: int = 10) -> str:
"""Generate an 8-character uppercase alphanumeric access code.
Retries on collision up to ``max_retries`` times. The keyspace is
36^8 (~2.8 trillion), so collisions are extremely unlikely.
Args:
max_retries: Maximum retry attempts on collision.
Returns:
A unique random string of 8 uppercase letters and digits.
Raises:
RuntimeError: If a unique code cannot be generated after retries.
"""
from django_program.registration.attendee import Attendee # noqa: PLC0415
alphabet = string.ascii_uppercase + string.digits
for _ in range(max_retries):
code = "".join(secrets.choice(alphabet) for _ in range(8))
if not Attendee.objects.filter(access_code=code).exists():
return code
msg = f"Failed to generate unique access code after {max_retries} attempts"
raise RuntimeError(msg)
[docs]
class AttendeeProfileBase(models.Model):
"""Abstract base for attendee profiles.
Projects extend this with custom fields (e.g. dietary restrictions,
t-shirt size) by subclassing and pointing ``DJANGO_PROGRAM.attendee_profile_model``
at the concrete subclass.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="%(class)s",
)
access_code = models.CharField(max_length=20, unique=True, editable=False)
completed_registration = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
[docs]
def save(self, *args: object, **kwargs: object) -> None: # pragma: no cover — abstract base
"""Auto-generate access code on first save if not already set."""
if not self.access_code:
self.access_code = generate_access_code()
super().save(*args, **kwargs)
[docs]
class Attendee(models.Model):
"""Links a user to a conference with registration state and check-in tracking.
Unlike ``AttendeeProfileBase``, this uses a ``ForeignKey`` to the user
because a single user can attend multiple conferences. The ``order`` field
is also a ``ForeignKey`` (not OneToOne) so that replacement/upgrade orders
can update the link without constraint violations.
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="attendees",
)
conference = models.ForeignKey(
"program_conference.Conference",
on_delete=models.CASCADE,
related_name="attendees",
)
order = models.ForeignKey(
"program_registration.Order",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="attendees",
)
access_code = models.CharField(max_length=20, unique=True, editable=False)
checked_in_at = models.DateTimeField(null=True, blank=True)
completed_registration = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = [("user", "conference")]
def __str__(self) -> str:
return f"{self.user} @ {self.conference}"
[docs]
def save(self, *args: object, **kwargs: object) -> None:
"""Auto-generate access code on first save if not already set."""
if not self.access_code:
self.access_code = generate_access_code()
super().save(*args, **kwargs)