from django.db import models from django.conf import settings from events.models import Event class AdSurface(models.Model): """ A display surface where ad placements can appear. e.g. HOME_FEATURED_CAROUSEL, HOME_TOP_EVENTS, CATEGORY_FEATURED """ LAYOUT_CHOICES = [ ('carousel', 'Carousel'), ('grid', 'Grid'), ('list', 'List'), ] SORT_CHOICES = [ ('rank', 'By Rank'), ('date', 'By Date'), ('popularity', 'By Popularity'), ] key = models.CharField(max_length=50, unique=True, db_index=True) name = models.CharField(max_length=100) description = models.TextField(blank=True, default='') max_slots = models.IntegerField(default=8) layout_type = models.CharField(max_length=20, choices=LAYOUT_CHOICES, default='carousel') sort_behavior = models.CharField(max_length=20, choices=SORT_CHOICES, default='rank') is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['name'] verbose_name = 'Ad Surface' verbose_name_plural = 'Ad Surfaces' def __str__(self): return f"{self.name} ({self.key})" @property def active_count(self): return self.placements.filter(status__in=['ACTIVE', 'SCHEDULED']).count() class AdPlacement(models.Model): """ A single placement of an event on a surface. Supports scheduling, ranking, scope-based targeting, and lifecycle status. """ STATUS_CHOICES = [ ('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('SCHEDULED', 'Scheduled'), ('EXPIRED', 'Expired'), ('DISABLED', 'Disabled'), ] PRIORITY_CHOICES = [ ('SPONSORED', 'Sponsored'), ('MANUAL', 'Manual'), ('ALGO', 'Algorithm'), ] SCOPE_CHOICES = [ ('GLOBAL', 'Global — shown to all users'), ('LOCAL', 'Local — shown to nearby users (50 km radius)'), ] surface = models.ForeignKey( AdSurface, on_delete=models.CASCADE, related_name='placements', ) event = models.ForeignKey( Event, on_delete=models.CASCADE, related_name='ad_placements', ) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='DRAFT', db_index=True) priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='MANUAL') scope = models.CharField(max_length=10, choices=SCOPE_CHOICES, default='GLOBAL', db_index=True) rank = models.IntegerField(default=0, help_text='Lower rank = higher position') start_at = models.DateTimeField(null=True, blank=True, help_text='When this placement becomes active') end_at = models.DateTimeField(null=True, blank=True, help_text='When this placement expires') boost_label = models.CharField( max_length=50, blank=True, default='', help_text='Display label e.g. "Featured", "Top Pick", "Sponsored"', ) notes = models.TextField(blank=True, default='') created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_placements', ) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_placements', ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['rank', '-created_at'] verbose_name = 'Ad Placement' verbose_name_plural = 'Ad Placements' constraints = [ models.UniqueConstraint( fields=['surface', 'event'], condition=models.Q(status__in=['DRAFT', 'ACTIVE', 'SCHEDULED']), name='unique_active_placement_per_surface', ), ] def __str__(self): return f"{self.event.name} on {self.surface.key} [{self.status}]"