- 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
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""
|
|
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.'))
|