Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f01f67f

Browse filesBrowse 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
Copy full SHA for f01f67f

File tree

4 files changed

+194
-0
lines changed
Filter options

4 files changed

+194
-0
lines changed

‎.gitignore

Copy file name to clipboardExpand all lines: .gitignore
+1Lines 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

Copy file name to clipboardExpand all lines: Makefile
+6Lines 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
+133Lines 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)
+54Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.