Skip to content

Commit 3a42133

Browse files
authored
Add filters to logo placement API (#1984)
* Remove authentication that is already defined at base settings * Refactor tests to configure sponsorship applications during set up * Add publisher and logo place filters via querystring at logo placement API * Centralizes sponsor slug definition at pythondorg This change is also present at #1981, but since pypi integration also needs it, I decided to duplicate it to not have the integration between services pending on an open PR.
1 parent c307536 commit 3a42133

File tree

3 files changed

+92
-11
lines changed

3 files changed

+92
-11
lines changed

sponsors/api.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
from rest_framework.views import APIView
88
from rest_framework.response import Response
99
from sponsors.models import BenefitFeature, LogoPlacement, Sponsorship
10+
from sponsors.models.enums import PublisherChoices, LogoPlacementChoices
1011

1112

1213
class LogoPlacementSerializer(serializers.Serializer):
1314
publisher = serializers.CharField()
1415
flight = serializers.CharField()
1516
sponsor = serializers.CharField()
17+
sponsor_slug = serializers.CharField()
1618
description = serializers.CharField()
1719
logo = serializers.URLField()
1820
start_date = serializers.DateField()
@@ -22,6 +24,33 @@ class LogoPlacementSerializer(serializers.Serializer):
2224
level_order = serializers.IntegerField()
2325

2426

27+
class FilterLogoPlacementsSerializer(serializers.Serializer):
28+
publisher = serializers.ChoiceField(
29+
choices=[(c.value, c.name.replace("_", " ").title()) for c in PublisherChoices],
30+
required=False,
31+
)
32+
flight = serializers.ChoiceField(
33+
choices=[(c.value, c.name.replace("_", " ").title()) for c in LogoPlacementChoices],
34+
required=False,
35+
)
36+
37+
@property
38+
def by_publisher(self):
39+
return self.validated_data.get("publisher")
40+
41+
@property
42+
def by_flight(self):
43+
return self.validated_data.get("flight")
44+
45+
def skip_logo(self, logo):
46+
if self.by_publisher and self.by_publisher != logo.publisher:
47+
return True
48+
if self.by_flight and self.by_flight != logo.logo_place:
49+
return True
50+
else:
51+
return False
52+
53+
2554
class SponsorPublisherPermission(permissions.BasePermission):
2655
message = 'Must have publisher permission.'
2756

@@ -33,18 +62,21 @@ def has_permission(self, request, view):
3362

3463

3564
class LogoPlacementeAPIList(APIView):
36-
authentication_classes = [TokenAuthentication]
3765
permission_classes = [SponsorPublisherPermission]
3866
serializer_class = LogoPlacementSerializer
3967

4068
def get(self, request, *args, **kwargs):
4169
placements = []
70+
logo_filters = FilterLogoPlacementsSerializer(data=request.GET)
71+
if not logo_filters.is_valid():
72+
return Response(logo_filters.errors, status=400)
4273

4374
sponsorships = Sponsorship.objects.enabled().with_logo_placement()
4475
for sponsorship in sponsorships.select_related("sponsor").iterator():
4576
sponsor = sponsorship.sponsor
4677
base_data = {
4778
"sponsor": sponsor.name,
79+
"sponsor_slug": sponsor.slug,
4880
"level_name": sponsorship.level_name,
4981
"level_order": sponsorship.package.order,
5082
"description": sponsor.description,
@@ -55,7 +87,8 @@ def get(self, request, *args, **kwargs):
5587
}
5688

5789
benefits = BenefitFeature.objects.filter(sponsor_benefit__sponsorship_id=sponsorship.pk)
58-
for logo in benefits.instance_of(LogoPlacement):
90+
logos = [l for l in benefits.instance_of(LogoPlacement) if not logo_filters.skip_logo(l)]
91+
for logo in logos:
5992
placement = base_data.copy()
6093
placement["publisher"] = logo.publisher
6194
placement["flight"] = logo.logo_place

sponsors/models/sponsors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from allauth.account.models import EmailAddress
55
from django.conf import settings
66
from django.db import models
7+
from django.template.defaultfilters import slugify
78
from django.urls import reverse
89
from django_countries.fields import CountryField
910
from ordered_model.models import OrderedModel
@@ -102,6 +103,10 @@ def primary_contact(self):
102103
except SponsorContact.DoesNotExist:
103104
return None
104105

106+
@property
107+
def slug(self):
108+
return slugify(self.name)
109+
105110
@property
106111
def admin_url(self):
107112
return reverse("admin:sponsors_sponsor_change", args=[self.pk])

sponsors/tests/test_api.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from urllib.parse import urlencode
2+
13
from django.contrib.auth.models import Permission
24
from django.urls import reverse_lazy
35
from django.utils.text import slugify
@@ -20,6 +22,15 @@ def setUp(self):
2022
self.authorization = f'Token {token.key}'
2123
self.sponsors = baker.make(Sponsor, _create_files=True, _quantity=3)
2224

25+
sponsorships = baker.make_recipe("sponsors.tests.finalized_sponsorship", sponsor=iter(self.sponsors),
26+
_quantity=3)
27+
self.sp1, self.sp2, self.sp3 = sponsorships
28+
baker.make_recipe("sponsors.tests.logo_at_download_feature", sponsor_benefit__sponsorship=self.sp1)
29+
baker.make_recipe("sponsors.tests.logo_at_sponsors_feature", sponsor_benefit__sponsorship=self.sp1)
30+
baker.make_recipe("sponsors.tests.logo_at_sponsors_feature", sponsor_benefit__sponsorship=self.sp2)
31+
baker.make_recipe("sponsors.tests.logo_at_pypi_feature", sponsor_benefit__sponsorship=self.sp3,
32+
link_to_sponsors_page=True, describe_as_sponsor=True)
33+
2334
def tearDown(self):
2435
for sponsor in Sponsor.objects.all():
2536
if sponsor.web_logo:
@@ -28,12 +39,6 @@ def tearDown(self):
2839
sponsor.print_logo.delete()
2940

3041
def test_list_logo_placement_as_expected(self):
31-
sp1, sp2, sp3 = baker.make_recipe("sponsors.tests.finalized_sponsorship", sponsor=iter(self.sponsors), _quantity=3)
32-
baker.make_recipe("sponsors.tests.logo_at_download_feature", sponsor_benefit__sponsorship=sp1)
33-
baker.make_recipe("sponsors.tests.logo_at_sponsors_feature", sponsor_benefit__sponsorship=sp1)
34-
baker.make_recipe("sponsors.tests.logo_at_sponsors_feature", sponsor_benefit__sponsorship=sp2)
35-
baker.make_recipe("sponsors.tests.logo_at_pypi_feature", sponsor_benefit__sponsorship=sp3, link_to_sponsors_page=True, describe_as_sponsor=True)
36-
3742
response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization)
3843
data = response.json()
3944

@@ -50,15 +55,15 @@ def test_list_logo_placement_as_expected(self):
5055
[p for p in data if p["publisher"] == PublisherChoices.FOUNDATION.value][0]['sponsor_url']
5156
)
5257
self.assertEqual(
53-
f"http://testserver/psf/sponsors/#{slugify(sp3.sponsor.name)}",
58+
f"http://testserver/psf/sponsors/#{slugify(self.sp3.sponsor.name)}",
5459
[p for p in data if p["publisher"] == PublisherChoices.PYPI.value][0]['sponsor_url']
5560
)
5661
self.assertCountEqual(
57-
[sp1.sponsor.description, sp1.sponsor.description, sp2.sponsor.description],
62+
[self.sp1.sponsor.description, self.sp1.sponsor.description, self.sp2.sponsor.description],
5863
[p['description'] for p in data if p["publisher"] == PublisherChoices.FOUNDATION.value]
5964
)
6065
self.assertEqual(
61-
[f"{sp3.sponsor.name} is a {sp3.level_name} sponsor of the Python Software Foundation."],
66+
[f"{self.sp3.sponsor.name} is a {self.sp3.level_name} sponsor of the Python Software Foundation."],
6267
[p['description'] for p in data if p["publisher"] == PublisherChoices.PYPI.value]
6368
)
6469

@@ -86,3 +91,41 @@ def test_user_must_have_required_permission(self):
8691
self.user.user_permissions.remove(self.permission)
8792
response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization)
8893
self.assertEqual(403, response.status_code)
94+
95+
def test_filter_sponsorship_by_publisher(self):
96+
querystring = urlencode({
97+
"publisher": PublisherChoices.PYPI.value,
98+
})
99+
url = f"{self.url}?{querystring}"
100+
response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization)
101+
data = response.json()
102+
103+
self.assertEqual(200, response.status_code)
104+
self.assertEqual(1, len(data))
105+
self.assertEqual(self.sp3.sponsor.name, data[0]["sponsor"])
106+
107+
def test_filter_sponsorship_by_flight(self):
108+
querystring = urlencode({
109+
"flight": LogoPlacementChoices.SIDEBAR.value,
110+
})
111+
url = f"{self.url}?{querystring}"
112+
response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization)
113+
data = response.json()
114+
115+
self.assertEqual(200, response.status_code)
116+
self.assertEqual(1, len(data))
117+
self.assertEqual(self.sp3.sponsor.name, data[0]["sponsor"])
118+
self.assertEqual(self.sp3.sponsor.slug, data[0]["sponsor_slug"])
119+
120+
def test_bad_request_for_invalid_filters(self):
121+
querystring = urlencode({
122+
"flight": "invalid-flight",
123+
"publisher": "invalid-publisher"
124+
})
125+
url = f"{self.url}?{querystring}"
126+
response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization)
127+
data = response.json()
128+
129+
self.assertEqual(400, response.status_code)
130+
self.assertIn("flight", data)
131+
self.assertIn("publisher", data)

0 commit comments

Comments
 (0)