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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy