diff --git a/.gitignore b/.gitignore index 60836490f..954ff2401 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ __pycache__ .env .DS_Store .envrc +.state/ diff --git a/Makefile b/Makefile index 0b190f249..dc296feb4 100644 --- a/Makefile +++ b/Makefile @@ -50,3 +50,9 @@ shell: .state/db-initialized clean: docker-compose down -v rm -f .state/docker-build-web .state/db-initialized .state/db-migrated + +test: .state/db-initialized + docker-compose run --rm web ./manage.py test + +docker_shell: .state/db-initialized + docker-compose run --rm web /bin/bash diff --git a/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py b/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py new file mode 100644 index 000000000..173ec31b9 --- /dev/null +++ b/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py @@ -0,0 +1,133 @@ +import os +from hashlib import sha1 +from calendar import timegm +from datetime import datetime +import sys +from urllib.parse import urlencode + +import requests +from requests.exceptions import RequestException + +from django.db.models import Q +from django.conf import settings +from django.core.management import BaseCommand + +from sponsors.models import ( + SponsorBenefit, + BenefitFeature, + ProvidedTextAsset, + TieredBenefit, +) + +BENEFITS = { + 121: { + "internal_name": "full_conference_passes_2023_code", + "voucher_type": "SPNS_COMP_", + }, + 139: { + "internal_name": "expo_hall_only_passes_2023_code", + "voucher_type": "SPNS_EXPO_COMP_", + }, + 148: { + "internal_name": "additional_full_conference_passes_2023_code", + "voucher_type": "SPNS_EXPO_DISC_", + }, + 166: { + "internal_name": "online_only_conference_passes_2023_code", + "voucher_type": "SPNS_ONLINE_COMP_", + }, +} + + +def api_call(uri, query): + method = "GET" + body = "" + + timestamp = timegm(datetime.utcnow().timetuple()) + base_string = "".join( + ( + settings.PYCON_API_SECRET, + str(timestamp), + method.upper(), + f"{uri}?{urlencode(query)}", + body, + ) + ) + + headers = { + "X-API-Key": str(settings.PYCON_API_KEY), + "X-API-Signature": str(sha1(base_string.encode("utf-8")).hexdigest()), + "X-API-Timestamp": str(timestamp), + } + scheme = "http" if settings.DEBUG else "https" + url = f"{scheme}://{settings.PYCON_API_HOST}{uri}" + try: + return requests.get(url, headers=headers, params=query).json() + except RequestException: + raise + + +def generate_voucher_codes(year): + for benefit_id, code in BENEFITS.items(): + for sponsorbenefit in ( + SponsorBenefit.objects.filter(sponsorship_benefit_id=benefit_id) + .filter(sponsorship__status="finalized") + .all() + ): + try: + quantity = BenefitFeature.objects.instance_of(TieredBenefit).get( + sponsor_benefit=sponsorbenefit + ) + except BenefitFeature.DoesNotExist: + print( + f"No quantity found for {sponsorbenefit.sponsorship.sponsor.name} and {code['internal_name']}" + ) + continue + try: + asset = ProvidedTextAsset.objects.filter( + sponsor_benefit=sponsorbenefit + ).get(internal_name=code["internal_name"]) + except ProvidedTextAsset.DoesNotExist: + print( + f"No provided asset found for {sponsorbenefit.sponsorship.sponsor.name} with internal name {code['internal_name']}" + ) + continue + + result = api_call( + f"/{year}/api/vouchers/", + query={ + "voucher_type": code["voucher_type"], + "quantity": quantity.quantity, + "sponsor_name": sponsorbenefit.sponsorship.sponsor.name, + }, + ) + if result["code"] == 200: + print( + f"Fullfilling {code['internal_name']} for {sponsorbenefit.sponsorship.sponsor.name}: {quantity.quantity}" + ) + promo_code = result["data"]["promo_code"] + asset.value = promo_code + asset.save() + else: + print( + f"Error from PyCon when fullfilling {code['internal_name']} for {sponsorbenefit.sponsorship.sponsor.name}: {result}" + ) + print(f"Done!") + + +class Command(BaseCommand): + """ + Create Contract objects for existing approved Sponsorships. + + Run this command as a initial data migration or to make sure + all approved Sponsorships do have associated Contract objects. + """ + + help = "Create Contract objects for existing approved Sponsorships." + + def add_arguments(self, parser): + parser.add_argument("year") + + def handle(self, **options): + year = options["year"] + generate_voucher_codes(year) diff --git a/sponsors/tests/test_management_command.py b/sponsors/tests/test_management_command.py new file mode 100644 index 000000000..100daad2a --- /dev/null +++ b/sponsors/tests/test_management_command.py @@ -0,0 +1,54 @@ +from django.test import TestCase + +from model_bakery import baker + +from unittest import mock + +from sponsors.models import ProvidedTextAssetConfiguration, ProvidedTextAsset +from sponsors.models.enums import AssetsRelatedTo + +from sponsors.management.commands.create_pycon_vouchers_for_sponsors import ( + generate_voucher_codes, + BENEFITS, +) + + +class CreatePyConVouchersForSponsorsTestCase(TestCase): + @mock.patch( + "sponsors.management.commands.create_pycon_vouchers_for_sponsors.api_call", + return_value={"code": 200, "data": {"promo_code": "test-promo-code"}}, + ) + def test_generate_voucher_codes(self, mock_api_call): + for benefit_id, code in BENEFITS.items(): + sponsor = baker.make("sponsors.Sponsor", name="Foo") + sponsorship = baker.make( + "sponsors.Sponsorship", status="finalized", sponsor=sponsor + ) + sponsorship_benefit = baker.make( + "sponsors.SponsorshipBenefit", id=benefit_id + ) + sponsor_benefit = baker.make( + "sponsors.SponsorBenefit", + id=benefit_id, + sponsorship=sponsorship, + sponsorship_benefit=sponsorship_benefit, + ) + quantity = baker.make( + "sponsors.TieredBenefit", + sponsor_benefit=sponsor_benefit, + ) + config = baker.make( + ProvidedTextAssetConfiguration, + related_to=AssetsRelatedTo.SPONSORSHIP.value, + _fill_optional=True, + internal_name=code["internal_name"], + ) + asset = config.create_benefit_feature(sponsor_benefit=sponsor_benefit) + + generate_voucher_codes(2020) + + for benefit_id, code in BENEFITS.items(): + asset = ProvidedTextAsset.objects.get( + sponsor_benefit__id=benefit_id, internal_name=code["internal_name"] + ) + self.assertEqual(asset.value, "test-promo-code")