import uuid from django.contrib.gis.db import models from django.conf import settings class Challenge(models.Model): class Status(models.TextChoices): ACTIVE = "active", "Active" CLOSED = "closed", "Closed" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) creator = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="created_challenges" ) title = models.CharField(max_length=255) description = models.TextField(blank=True, default="") status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE, db_index=True) # The polygon users must be physically inside to submit a recording. # Drawn by the creator on the Cesium map. region = models.PolygonField(geography=True) # Derived centroid — stored separately for cheap proximity queries and map pin placement. # Set automatically from `region` on save; not user-supplied. region_centroid = models.PointField(geography=True) # Optional cap on accepted submissions (null = unlimited) max_submissions = models.PositiveIntegerField(null=True, blank=True) # Denormalised count of published splats linked to this challenge. # Incremented by a Celery task when a splat transitions to is_published=True. # Avoids a COUNT(*) on every map tile request. submission_count = models.PositiveIntegerField(default=0) expires_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): # Keep centroid in sync with region automatically if self.region: self.region_centroid = self.region.centroid super().save(*args, **kwargs) def __str__(self): return self.title class Meta: db_table = "challenges" indexes = [ models.Index(fields=["status", "created_at"]), ] class ChallengeParticipant(models.Model): """ A user who has bookmarked / accepted a challenge. Used to target FCM notifications (e.g. new submission, challenge expiring soon). Participation is recorded automatically when a user submits a splat for a challenge, or manually when they tap "I'll do this" in the app. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) challenge = models.ForeignKey(Challenge, on_delete=models.CASCADE, related_name="participants") user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="challenge_participations" ) # The splat this user submitted for the challenge, if any submitted_splat = models.OneToOneField( "splats.Splat", null=True, blank=True, on_delete=models.SET_NULL, related_name="challenge_participation", ) joined_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.user_id} → {self.challenge_id}" class Meta: db_table = "challenge_participants" unique_together = [("challenge", "user")] indexes = [ models.Index(fields=["user", "joined_at"]), ]