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")
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: