feat(ad_control): new AdSurface + AdPlacement module for placement-based featured/top events
- New ad_control Django app: AdSurface + AdPlacement models with GLOBAL/LOCAL scope - Admin CRUD API at /api/v1/ad-control/ (JWT-protected): surfaces, placements, picker events - Placement lifecycle: DRAFT → ACTIVE|SCHEDULED → EXPIRED|DISABLED - LOCAL scope: Haversine ≤ 50km from event lat/lng (fixed radius, no config needed) - Consumer APIs: /api/events/featured-events/ and /api/events/top-events/ rewritten to use placement-based queries (same URL paths + response shape — no breaking changes) - Seed command: seed_surfaces --migrate converts existing is_featured/is_top_event booleans - mount: admin_api/urls.py → ad-control/, mobile_api/urls.py → replaced consumer views - settings.py: added ad_control to INSTALLED_APPS
This commit is contained in:
0
ad_control/management/__init__.py
Normal file
0
ad_control/management/__init__.py
Normal file
0
ad_control/management/commands/__init__.py
Normal file
0
ad_control/management/commands/__init__.py
Normal file
100
ad_control/management/commands/seed_surfaces.py
Normal file
100
ad_control/management/commands/seed_surfaces.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Seed the default AdSurface records and migrate existing boolean flags to placements.
|
||||
|
||||
Usage:
|
||||
python manage.py seed_surfaces # seed surfaces only
|
||||
python manage.py seed_surfaces --migrate # also migrate is_featured / is_top_event to placements
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from ad_control.models import AdSurface, AdPlacement
|
||||
from events.models import Event
|
||||
|
||||
|
||||
SURFACES = [
|
||||
{
|
||||
'key': 'HOME_FEATURED_CAROUSEL',
|
||||
'name': 'Featured Carousel',
|
||||
'description': 'Homepage hero carousel — high-impact banner-style placement.',
|
||||
'max_slots': 8,
|
||||
'layout_type': 'carousel',
|
||||
'sort_behavior': 'rank',
|
||||
},
|
||||
{
|
||||
'key': 'HOME_TOP_EVENTS',
|
||||
'name': 'Top Events',
|
||||
'description': 'Homepage "Top Events" grid section below the hero.',
|
||||
'max_slots': 10,
|
||||
'layout_type': 'grid',
|
||||
'sort_behavior': 'rank',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seed default ad surfaces and optionally migrate boolean flags to placements.'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--migrate',
|
||||
action='store_true',
|
||||
help='Also migrate existing is_featured / is_top_event flags to AdPlacement rows.',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# --- Seed surfaces ---
|
||||
for s in SURFACES:
|
||||
obj, created = AdSurface.objects.update_or_create(
|
||||
key=s['key'],
|
||||
defaults=s,
|
||||
)
|
||||
status = 'CREATED' if created else 'EXISTS'
|
||||
self.stdout.write(f" [{status}] {obj.key} — {obj.name}")
|
||||
|
||||
# --- Migrate boolean flags ---
|
||||
if options['migrate']:
|
||||
self.stdout.write('\nMigrating boolean flags to placements...')
|
||||
|
||||
featured_surface = AdSurface.objects.get(key='HOME_FEATURED_CAROUSEL')
|
||||
top_surface = AdSurface.objects.get(key='HOME_TOP_EVENTS')
|
||||
|
||||
featured_events = Event.objects.filter(is_featured=True)
|
||||
top_events = Event.objects.filter(is_top_event=True)
|
||||
|
||||
created_count = 0
|
||||
|
||||
for rank, event in enumerate(featured_events, start=1):
|
||||
_, created = AdPlacement.objects.get_or_create(
|
||||
surface=featured_surface,
|
||||
event=event,
|
||||
defaults={
|
||||
'status': 'ACTIVE',
|
||||
'priority': 'MANUAL',
|
||||
'scope': 'GLOBAL',
|
||||
'rank': rank,
|
||||
'boost_label': 'Featured',
|
||||
},
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
|
||||
for rank, event in enumerate(top_events, start=1):
|
||||
_, created = AdPlacement.objects.get_or_create(
|
||||
surface=top_surface,
|
||||
event=event,
|
||||
defaults={
|
||||
'status': 'ACTIVE',
|
||||
'priority': 'MANUAL',
|
||||
'scope': 'GLOBAL',
|
||||
'rank': rank,
|
||||
'boost_label': 'Top Event',
|
||||
},
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' Migrated {created_count} placements '
|
||||
f'({featured_events.count()} featured, {top_events.count()} top events).'
|
||||
))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\nDone.'))
|
||||
Reference in New Issue
Block a user