Skip to content

Commit fdd22f9

Browse files
authored
Merge pull request #85 from sousa-dev/cursor/transit-ads-premium-gating-cbb2
feat(transit): serve platform=all ads to specific platforms + admin bulk retag
2 parents bba2820 + 9b3527f commit fdd22f9

3 files changed

Lines changed: 118 additions & 6 deletions

File tree

src/transit/admin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,12 @@ class AdAdmin(IslandScopedAdmin):
9999
list_filter = ('island', 'platform', 'status')
100100
search_fields = ('entity', 'description')
101101
date_hierarchy = 'start'
102+
actions = ('set_platform_to_all',)
103+
104+
@admin.action(description='Set platform to all (selected)')
105+
def set_platform_to_all(self, request, queryset):
106+
updated = queryset.update(platform='all')
107+
self.message_user(
108+
request,
109+
f'{updated} ad{"s" if updated != 1 else ""} set to platform=all.',
110+
)

src/transit/services/ads.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from datetime import datetime
66
from difflib import SequenceMatcher
77

8+
from django.db.models import Q
89
from django.utils import timezone
910

1011
from transit.models import Ad, Stop, StopGroup
@@ -62,7 +63,9 @@ def select_ad(*, advertise_on: str, platform: str, now_ts: float | None = None)
6263

6364
ads = Ad.objects.filter(status='active', start__lte=ad_dt, end__gte=ad_dt)
6465
if platform != 'all':
65-
ads = ads.filter(platform=platform)
66+
# A specific client platform (e.g. ios/android) is eligible for both its
67+
# own targeted campaigns and any cross-platform `platform='all'` campaign.
68+
ads = ads.filter(Q(platform=platform) | Q(platform='all'))
6669

6770
if advertise_on in ('home', 'all'):
6871
if advertise_on != 'all':
@@ -77,11 +80,10 @@ def select_ad(*, advertise_on: str, platform: str, now_ts: float | None = None)
7780

7881
ad = ads.order_by('?').first()
7982
if ad is None:
80-
ad = (
81-
Ad.objects.filter(platform=platform, status='default')
82-
.order_by('?')
83-
.first()
84-
)
83+
default_qs = Ad.objects.filter(status='default')
84+
if platform != 'all':
85+
default_qs = default_qs.filter(Q(platform=platform) | Q(platform='all'))
86+
ad = default_qs.order_by('?').first()
8587
if ad is None and platform not in ('all', ''):
8688
# Dev sqlite often has android-only defaults; serve any default rather than 404
8789
ad = Ad.objects.filter(status='default').order_by('?').first()

src/transit/tests/test_ads.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Tests for ad selection platform filtering and the admin bulk retag action."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import timedelta
6+
7+
from django.contrib.admin.sites import AdminSite
8+
from django.test import RequestFactory, TestCase
9+
from django.utils import timezone
10+
11+
from tenancy.services import for_island
12+
from transit.admin import AdAdmin
13+
from transit.models import Ad
14+
from transit.services.ads import select_ad
15+
from transit.tests.fixtures import ensure_transit_fixtures
16+
17+
18+
class _DummyMessages:
19+
def __init__(self):
20+
self.messages = []
21+
22+
def add(self, level, message, extra_tags=''):
23+
self.messages.append(message)
24+
25+
26+
class SelectAdPlatformTests(TestCase):
27+
def setUp(self):
28+
self.island, _, _ = ensure_transit_fixtures()
29+
self.now = timezone.now()
30+
31+
def _make_ad(self, *, platform: str, status: str = 'active', advertise_on: str = 'home') -> Ad:
32+
return Ad.objects.create(
33+
island=self.island,
34+
entity=f'{platform}-{status}',
35+
media='https://example.com/banner.png',
36+
start=self.now - timedelta(days=1),
37+
end=self.now + timedelta(days=1),
38+
advertise_on=advertise_on,
39+
platform=platform,
40+
status=status,
41+
)
42+
43+
def test_platform_all_active_returned_for_ios(self):
44+
ad = self._make_ad(platform='all')
45+
with for_island(self.island):
46+
selected = select_ad(advertise_on='home', platform='ios')
47+
self.assertEqual(selected, ad)
48+
49+
def test_platform_web_not_returned_for_ios(self):
50+
self._make_ad(platform='web')
51+
with for_island(self.island):
52+
selected = select_ad(advertise_on='home', platform='ios')
53+
self.assertIsNone(selected)
54+
55+
def test_platform_web_default_fallback_for_ios(self):
56+
default = self._make_ad(platform='all', status='default')
57+
self._make_ad(platform='web')
58+
with for_island(self.island):
59+
selected = select_ad(advertise_on='home', platform='ios')
60+
self.assertEqual(selected, default)
61+
62+
def test_ios_targeted_ad_returned_for_ios(self):
63+
ad = self._make_ad(platform='ios')
64+
with for_island(self.island):
65+
selected = select_ad(advertise_on='home', platform='ios')
66+
self.assertEqual(selected, ad)
67+
68+
69+
class AdAdminBulkActionTests(TestCase):
70+
def setUp(self):
71+
self.island, _, _ = ensure_transit_fixtures()
72+
self.now = timezone.now()
73+
self.admin = AdAdmin(Ad, AdminSite())
74+
self.factory = RequestFactory()
75+
76+
def _make_ad(self, platform: str) -> Ad:
77+
return Ad.objects.create(
78+
island=self.island,
79+
entity=f'ad-{platform}',
80+
media='https://example.com/banner.png',
81+
start=self.now - timedelta(days=1),
82+
end=self.now + timedelta(days=1),
83+
advertise_on='home',
84+
platform=platform,
85+
status='active',
86+
)
87+
88+
def test_set_platform_to_all_updates_selected(self):
89+
a = self._make_ad('web')
90+
b = self._make_ad('android')
91+
request = self.factory.post('/admin/')
92+
request._messages = _DummyMessages()
93+
94+
queryset = Ad.objects.filter(id__in=[a.id, b.id])
95+
self.admin.set_platform_to_all(request, queryset)
96+
97+
a.refresh_from_db()
98+
b.refresh_from_db()
99+
self.assertEqual(a.platform, 'all')
100+
self.assertEqual(b.platform, 'all')
101+
self.assertEqual(request._messages.messages, ['2 ads set to platform=all.'])

0 commit comments

Comments
 (0)