Skip to content

Commit f01f67f

Browse files
Add management command to create sponsor vouchers for PyCon 2023 (#2233)
* ignore Makefile .state folder * add test and docker_shell command to Makefile * Add command to create pycon vouchers for sponsors * Update sponsors/management/commands/create_pycon_vouchers_for_sponsors.py * Update sponsors/management/commands/create_pycon_vouchers_for_sponsors.py Co-authored-by: Ee Durbin <ewdurbin@gmail.com>
1 parent afe3cdb commit f01f67f

File tree

4 files changed

+194
-0
lines changed

4 files changed

+194
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ __pycache__
2525
.env
2626
.DS_Store
2727
.envrc
28+
.state/

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,9 @@ shell: .state/db-initialized
5050
clean:
5151
docker-compose down -v
5252
rm -f .state/docker-build-web .state/db-initialized .state/db-migrated
53+
54+
test: .state/db-initialized
55+
docker-compose run --rm web ./manage.py test
56+
57+
docker_shell: .state/db-initialized
58+
docker-compose run --rm web /bin/bash
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import os
2+
from hashlib import sha1
3+
from calendar import timegm
4+
from datetime import datetime
5+
import sys
6+
from urllib.parse import urlencode
7+
8+
import requests
9+
from requests.exceptions import RequestException
10+
11+
from django.db.models import Q
12+
from django.conf import settings
13+
from django.core.management import BaseCommand
14+
15+
from sponsors.models import (
16+
SponsorBenefit,
17+
BenefitFeature,
18+
ProvidedTextAsset,
19+
TieredBenefit,
20+
)
21+
22+
BENEFITS = {
23+
121: {
24+
"internal_name": "full_conference_passes_2023_code",
25+
"voucher_type": "SPNS_COMP_",
26+
},
27+
139: {
28+
"internal_name": "expo_hall_only_passes_2023_code",
29+
"voucher_type": "SPNS_EXPO_COMP_",
30+
},
31+
148: {
32+
"internal_name": "additional_full_conference_passes_2023_code",
33+
"voucher_type": "SPNS_EXPO_DISC_",
34+
},
35+
166: {
36+
"internal_name": "online_only_conference_passes_2023_code",
37+
"voucher_type": "SPNS_ONLINE_COMP_",
38+
},
39+
}
40+
41+
42+
def api_call(uri, query):
43+
method = "GET"
44+
body = ""
45+
46+
timestamp = timegm(datetime.utcnow().timetuple())
47+
base_string = "".join(
48+
(
49+
settings.PYCON_API_SECRET,
50+
str(timestamp),
51+
method.upper(),
52+
f"{uri}?{urlencode(query)}",
53+
body,
54+
)
55+
)
56+
57+
headers = {
58+
"X-API-Key": str(settings.PYCON_API_KEY),
59+
"X-API-Signature": str(sha1(base_string.encode("utf-8")).hexdigest()),
60+
"X-API-Timestamp": str(timestamp),
61+
}
62+
scheme = "http" if settings.DEBUG else "https"
63+
url = f"{scheme}://{settings.PYCON_API_HOST}{uri}"
64+
try:
65+
return requests.get(url, headers=headers, params=query).json()
66+
except RequestException:
67+
raise
68+
69+
70+
def generate_voucher_codes(year):
71+
for benefit_id, code in BENEFITS.items():
72+
for sponsorbenefit in (
73+
SponsorBenefit.objects.filter(sponsorship_benefit_id=benefit_id)
74+
.filter(sponsorship__status="finalized")
75+
.all()
76+
):
77+
try:
78+
quantity = BenefitFeature.objects.instance_of(TieredBenefit).get(
79+
sponsor_benefit=sponsorbenefit
80+
)
81+
except BenefitFeature.DoesNotExist:
82+
print(
83+
f"No quantity found for {sponsorbenefit.sponsorship.sponsor.name} and {code['internal_name']}"
84+
)
85+
continue
86+
try:
87+
asset = ProvidedTextAsset.objects.filter(
88+
sponsor_benefit=sponsorbenefit
89+
).get(internal_name=code["internal_name"])
90+
except ProvidedTextAsset.DoesNotExist:
91+
print(
92+
f"No provided asset found for {sponsorbenefit.sponsorship.sponsor.name} with internal name {code['internal_name']}"
93+
)
94+
continue
95+
96+
result = api_call(
97+
f"/{year}/api/vouchers/",
98+
query={
99+
"voucher_type": code["voucher_type"],
100+
"quantity": quantity.quantity,
101+
"sponsor_name": sponsorbenefit.sponsorship.sponsor.name,
102+
},
103+
)
104+
if result["code"] == 200:
105+
print(
106+
f"Fullfilling {code['internal_name']} for {sponsorbenefit.sponsorship.sponsor.name}: {quantity.quantity}"
107+
)
108+
promo_code = result["data"]["promo_code"]
109+
asset.value = promo_code
110+
asset.save()
111+
else:
112+
print(
113+
f"Error from PyCon when fullfilling {code['internal_name']} for {sponsorbenefit.sponsorship.sponsor.name}: {result}"
114+
)
115+
print(f"Done!")
116+
117+
118+
class Command(BaseCommand):
119+
"""
120+
Create Contract objects for existing approved Sponsorships.
121+
122+
Run this command as a initial data migration or to make sure
123+
all approved Sponsorships do have associated Contract objects.
124+
"""
125+
126+
help = "Create Contract objects for existing approved Sponsorships."
127+
128+
def add_arguments(self, parser):
129+
parser.add_argument("year")
130+
131+
def handle(self, **options):
132+
year = options["year"]
133+
generate_voucher_codes(year)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from django.test import TestCase
2+
3+
from model_bakery import baker
4+
5+
from unittest import mock
6+
7+
from sponsors.models import ProvidedTextAssetConfiguration, ProvidedTextAsset
8+
from sponsors.models.enums import AssetsRelatedTo
9+
10+
from sponsors.management.commands.create_pycon_vouchers_for_sponsors import (
11+
generate_voucher_codes,
12+
BENEFITS,
13+
)
14+
15+
16+
class CreatePyConVouchersForSponsorsTestCase(TestCase):
17+
@mock.patch(
18+
"sponsors.management.commands.create_pycon_vouchers_for_sponsors.api_call",
19+
return_value={"code": 200, "data": {"promo_code": "test-promo-code"}},
20+
)
21+
def test_generate_voucher_codes(self, mock_api_call):
22+
for benefit_id, code in BENEFITS.items():
23+
sponsor = baker.make("sponsors.Sponsor", name="Foo")
24+
sponsorship = baker.make(
25+
"sponsors.Sponsorship", status="finalized", sponsor=sponsor
26+
)
27+
sponsorship_benefit = baker.make(
28+
"sponsors.SponsorshipBenefit", id=benefit_id
29+
)
30+
sponsor_benefit = baker.make(
31+
"sponsors.SponsorBenefit",
32+
id=benefit_id,
33+
sponsorship=sponsorship,
34+
sponsorship_benefit=sponsorship_benefit,
35+
)
36+
quantity = baker.make(
37+
"sponsors.TieredBenefit",
38+
sponsor_benefit=sponsor_benefit,
39+
)
40+
config = baker.make(
41+
ProvidedTextAssetConfiguration,
42+
related_to=AssetsRelatedTo.SPONSORSHIP.value,
43+
_fill_optional=True,
44+
internal_name=code["internal_name"],
45+
)
46+
asset = config.create_benefit_feature(sponsor_benefit=sponsor_benefit)
47+
48+
generate_voucher_codes(2020)
49+
50+
for benefit_id, code in BENEFITS.items():
51+
asset = ProvidedTextAsset.objects.get(
52+
sponsor_benefit__id=benefit_id, internal_name=code["internal_name"]
53+
)
54+
self.assertEqual(asset.value, "test-promo-code")

0 commit comments

Comments
 (0)