[#41] Allow users to download the user/planng guide for a payment

- backend changes to support physical and digital checkouts
This commit is contained in:
Matthew Northcott 2023-04-20 14:59:00 +12:00
parent d09e6f3914
commit 13c8436d98
11 changed files with 255 additions and 179 deletions

View file

@ -36,12 +36,12 @@ class ActivationKeySetAdmin(admin.ModelAdmin):
class ActivationKeyAdmin(admin.ModelAdmin): class ActivationKeyAdmin(admin.ModelAdmin):
list_display = ['key', 'creation_date', 'key_set', 'remaining_activations'] list_display = ['key', 'creation_date', 'key_set', 'activations', 'remaining_activations']
list_filter = ['creation_date', 'key_set'] list_filter = ['creation_date', 'key_set']
class QuestionnaireAdmin(admin.ModelAdmin): class QuestionnaireAdmin(admin.ModelAdmin):
list_display = ['address_display', 'location_display', 'soil_variant', 'ecological_district_display', 'habitat', 'zone', 'key_set_display'] list_display = ['address_display', 'location_display', 'soil_variant', 'ecological_district_display', 'habitat', 'zone', 'key_set_display', 'creation_date']
list_filter = [ActivationKeySetFilter] list_filter = [ActivationKeySetFilter]
actions = ['export'] actions = ['export']
@ -134,3 +134,5 @@ admin.site.register(models.ActivationKey, ActivationKeyAdmin)
admin.site.register(models.ActivationKeySet, ActivationKeySetAdmin) admin.site.register(models.ActivationKeySet, ActivationKeySetAdmin)
admin.site.register(models.Questionnaire, QuestionnaireAdmin) admin.site.register(models.Questionnaire, QuestionnaireAdmin)
admin.site.register(models.Export, ExportAdmin) admin.site.register(models.Export, ExportAdmin)
admin.site.register(models.Customer)
admin.site.register(models.CustomerAddress)

View file

@ -1,64 +1,7 @@
import json from .models import Plant, EcologicalRegion, EcologicalDistrictLayer, ChristchurchRegion, SoilOrder, SoilVariant, ActivationKey, Questionnaire
from django.http import Http404
from django.db.models import Q
from .models import Plant, EcologicalRegion, EcologicalDistrictLayer, ChristchurchRegion, SoilOrder, SoilVariant
from .wms_utils import get_point_from_coordinates from .wms_utils import get_point_from_coordinates
def coordinate_filter(request, queryset, ignore_soil_order=False):
coordinates = request.query_params.get('coordinates')
if coordinates is not None:
pnt = get_point_from_coordinates(coordinates)
filtered_regions = EcologicalRegion.objects.filter(
ecologicaldistrictlayer__geom__intersects=pnt).values_list('id', flat=True)
filtered_soil_orders = SoilOrder.objects.filter(
soillayer__geom__intersects=pnt).values_list('id', flat=True)
# Filter by ecological regions and soil orders
if ignore_soil_order:
return queryset.filter(ecological_regions__in=filtered_regions).distinct()
else:
return queryset.filter(
Q(ecological_regions__in=filtered_regions) &
Q(soil_order__in=filtered_soil_orders)).distinct()
return queryset
def soil_variant_filter(request, queryset):
soil_variant = request.query_params.get('soilVariant')
if soil_variant in {"D", "W", "M"}:
soil_variant_ids = SoilVariant.objects.filter(Q(name__startswith=soil_variant) | Q(
name__startswith="M")).values_list('id', flat=True).distinct()
return queryset.filter(soil_variants__in=soil_variant_ids).distinct()
return queryset
def zone_filter(zone_json, queryset):
return queryset.filter(zones__id__contains=zone_json['id']).distinct()
def soil_order_coordinate_filter(coordinates):
pnt = get_point_from_coordinates(coordinates)
try:
return SoilOrder.objects.filter(soillayer__geom__intersects=pnt)
except SoilOrder.DoesNotExist:
raise Http404(f"Soil Order cannot be found for point {pnt}")
def ecological_district_coordinate_filter(coordinates):
pnt = get_point_from_coordinates(coordinates)
try:
return EcologicalDistrictLayer.objects.filter(geom__intersects=pnt)
except EcologicalDistrictLayer.DoesNotExist:
raise Http404(
f"Ecological district layer cannot be found for point {pnt}")
def is_in_auckland(coordinates): def is_in_auckland(coordinates):
pnt = get_point_from_coordinates(coordinates) pnt = get_point_from_coordinates(coordinates)
eco_district = EcologicalDistrictLayer.objects.filter(geom__intersects=pnt).first() eco_district = EcologicalDistrictLayer.objects.filter(geom__intersects=pnt).first()
@ -68,22 +11,31 @@ def is_in_auckland(coordinates):
def is_in_christchurch(coordinates): def is_in_christchurch(coordinates):
pnt = get_point_from_coordinates(coordinates) pnt = get_point_from_coordinates(coordinates)
in_chch = ChristchurchRegion.objects.filter(geom__intersects=pnt).first() in_chch = ChristchurchRegion.objects.filter(geom__intersects=pnt).first()
return in_chch is not None; return in_chch is not None
def get_filtered_plants(request): def get_filtered_plants(request):
filtered_plants = Plant.objects.all() try:
ak = ActivationKey.objects.get(key=request.query_params['key'])
except (KeyError, ActivationKey.DoesNotExist):
raise ValueError("Invalid key")
zone = request.query_params.get('zone') if not (q := Questionnaire.objects.filter(key=ak).order_by("-creation_date").first()):
if zone != None: return Plant.objects.none()
zone_json = json.loads(zone)
filtered_plants = zone_filter(zone_json, filtered_plants)
if not zone_json['ignore_location_filter']: regions = EcologicalRegion.objects.filter(ecologicaldistrictlayer__geom__intersects=q.location).distinct()
filtered_plants = coordinate_filter( soils = (
request, filtered_plants, ignore_soil_order=zone_json['ignore_soil_order_filter']) SoilVariant.objects.filter(name="Mesic") |
else: SoilVariant.objects.filter(name__istartswith=q.soil_variant)
filtered_plants = coordinate_filter(request, filtered_plants) ).distinct()
filtered_plants = soil_variant_filter(request, filtered_plants) qs = Plant.objects.filter(
zones__in=[q.zone],
ecological_regions__in=regions,
soil_variants__in=soils,
)
return filtered_plants if not q.zone.ignore_soil_order_filter:
orders = SoilOrder.objects.filter(soillayer__geom__intersects=q.location).distinct()
qs = qs.filter(soil_order__in=orders).distinct()
return qs.distinct()

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.17 on 2023-04-20 02:57
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('api', '0016_alter_activationkey_key_set'),
]
operations = [
migrations.CreateModel(
name='CustomerAddress',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('city', models.CharField(max_length=100)),
('line1', models.CharField(max_length=255)),
('line2', models.CharField(blank=True, max_length=255, null=True)),
('postal_code', models.SmallIntegerField()),
],
),
migrations.AddField(
model_name='activationkey',
name='activations',
field=models.SmallIntegerField(default=0),
),
migrations.AddField(
model_name='questionnaire',
name='creation_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.CreateModel(
name='Customer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('name', models.CharField(max_length=100)),
('address', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.customeraddress')),
],
),
migrations.AddField(
model_name='activationkey',
name='customer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api.customer'),
),
]

View file

@ -202,6 +202,26 @@ class Address(models.Model):
managed = False managed = False
class CustomerAddress(models.Model):
city = models.CharField(max_length=100)
line1 = models.CharField(max_length=255)
line2 = models.CharField(max_length=255, blank=True, null=True)
postal_code = models.SmallIntegerField()
def __str__(self):
return f"{self.line1}, {self.line2}, {self.city} {self.postal_code}" \
if self.line2 else f"{self.line1}, {self.city} {self.postal_code}"
class Customer(models.Model):
email = models.EmailField()
name = models.CharField(max_length=100)
address = models.ForeignKey(CustomerAddress, on_delete=models.PROTECT)
def __str__(self):
return self.name
class ActivationKeySet(models.Model): class ActivationKeySet(models.Model):
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@ -223,8 +243,10 @@ class ActivationKey(models.Model):
key = models.CharField(max_length=20, unique=True, default=key_default) key = models.CharField(max_length=20, unique=True, default=key_default)
key_set = models.ForeignKey(ActivationKeySet, on_delete=models.PROTECT, null=True) key_set = models.ForeignKey(ActivationKeySet, on_delete=models.PROTECT, null=True)
activations = models.SmallIntegerField(default=0)
remaining_activations = models.SmallIntegerField(default=1) remaining_activations = models.SmallIntegerField(default=1)
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)
customer = models.ForeignKey(Customer, on_delete=models.PROTECT, null=True)
def __str__(self): def __str__(self):
return self.key return self.key
@ -235,6 +257,7 @@ class Questionnaire(models.Model):
soil_variant = models.ForeignKey(SoilVariant, on_delete=models.CASCADE) soil_variant = models.ForeignKey(SoilVariant, on_delete=models.CASCADE)
zone = models.ForeignKey(Zone, on_delete=models.CASCADE) zone = models.ForeignKey(Zone, on_delete=models.CASCADE)
key = models.ForeignKey(ActivationKey, on_delete=models.PROTECT, null=True) key = models.ForeignKey(ActivationKey, on_delete=models.PROTECT, null=True)
creation_date = models.DateTimeField(auto_now_add=True)
@property @property
def habitat(self): def habitat(self):

View file

@ -2,8 +2,9 @@ import csv
from io import StringIO from io import StringIO
from os.path import splitext from os.path import splitext
from .filters import * from .filters import is_in_christchurch, is_in_auckland
from .wms_utils import get_address_from_coordinates, get_point_from_coordinates from .models import EcologicalDistrictLayer, SoilOrder, Questionnaire, ActivationKey
from .wms_utils import get_address_from_coordinates
import pdfkit import pdfkit
import pandas as pd import pandas as pd
@ -22,75 +23,58 @@ HEADER_FIELDS = ['Names', 'Growth Form / Max Height (m) / Spacing (m) / Forest P
'Tolerances (Water / Drought / Frost / Salinity)', 'Ecosystem Services', 'Carbon Sequestration Rate', 'Planting Stage'] 'Tolerances (Water / Drought / Frost / Salinity)', 'Ecosystem Services', 'Carbon Sequestration Rate', 'Planting Stage']
def get_location_filters(params): def get_location_filters(questionnaire):
""" Retrives the selected location data from the request. """ Retrives the selected location data from the request.
""" """
filter_rows = [['LOCATION FILTERS:', ' ']] filter_rows = [['LOCATION FILTERS:', ' ']]
coordinates = params.get('coordinates')
if coordinates is not None: eco_district_layer = EcologicalDistrictLayer.objects.filter(geom__intersects=questionnaire.location).first()
eco_district_layer = ecological_district_coordinate_filter( address = get_address_from_coordinates(questionnaire.location)
coordinates).first()
point = get_point_from_coordinates(coordinates)
address = get_address_from_coordinates(coordinates)
filter_rows.append(['Point coordinates:', point]) filter_rows.append(['Point coordinates:', questionnaire.location])
filter_rows.append(['Ecological region:', eco_district_layer.ecologic_1 or '' ]) filter_rows.append(['Ecological region:', eco_district_layer.ecologic_1 or '' ])
filter_rows.append(['Ecological district:', eco_district_layer.ecologic_2 or ' ']) filter_rows.append(['Ecological district:', eco_district_layer.ecologic_2 or ' '])
filter_rows.append(['Property address:', address['full_address'] if address is not None else ' ']) filter_rows.append(['Property address:', address['full_address'] if address is not None else ' '])
else:
filter_rows.append(["None specified", " "])
return filter_rows return filter_rows
def get_soil_filters(params): def get_soil_filters(questionnaire):
""" Retrives the selected soil type data from the request params. """ Retrives the selected soil type data from the request params.
""" """
filter_rows = [['SOIL FILTERS:', ' ']] filter_rows = [['SOIL FILTERS:', ' ']]
soil_variant = params.get('soilVariant')
coordinates = params.get('coordinates')
if soil_variant is not None and coordinates is not None: soil_order = SoilOrder.objects.filter(soillayer__geom__intersects=questionnaire.location).first()
soil_order_obj = soil_order_coordinate_filter(coordinates).first()
filter_rows.append(['Soil Order:', f"{soil_order_obj.name or ' '} ({soil_order_obj.code or ' '})"]) filter_rows.append(['Soil Order:', f"{soil_order.name or ' '} ({soil_order.code or ' '})"])
filter_rows.append(['Soil Variant:', soil_variant]) filter_rows.append(['Soil Variant:', questionnaire.soil_variant])
else:
filter_rows.append(["None specified", " "])
return filter_rows return filter_rows
def get_site_filters(params): def get_site_filters(questionnaire):
""" Retrives the selected site data from the request params """ Retrives the selected site data from the request params
""" """
filter_rows = [['SITE FILTERS:', ' ']] filter_rows = [['SITE FILTERS:', ' ']]
habitat = params.get('habitat') zone = questionnaire.zone
zone = params.get('zone') habitat = zone.habitat
if zone is not None and habitat is not None:
habitat_json = json.loads(habitat) filter_rows.append(['Habitat:', habitat.name])
zone_json = json.loads(zone) filter_rows.append(['Zone Name:', zone.name])
filter_rows.append(['Habitat:', habitat_json.get("name", " ")]) filter_rows.append(['Zone Variant:', zone.variant])
filter_rows.append(['Zone Name:', zone_json.get("name", " ")]) filter_rows.append(['Zone Refined Variant:', zone.refined_variant])
filter_rows.append(['Zone Variant:', zone_json.get("variant", " ")])
filter_rows.append(['Zone Refined Variant:', zone_json.get("refined_variant", " ")])
else:
filter_rows.append(["None specified", " "])
return filter_rows return filter_rows
def get_additional_region_info(params): def get_additional_region_info(questionnaire):
""" If the location coordinates fall within the CHCH or Auckland regions then return a description of where to find more information. """ If the location coordinates fall within the CHCH or Auckland regions then return a description of where to find more information.
""" """
coordinates = params.get('coordinates') if is_in_christchurch(questionnaire.location):
if coordinates is not None: return [["Your location falls within the ecosystem type covered by the Christchurch Council ecosystem maps - further information can be obtained from Ōtautahi/Christchurch ecosystems map link to: https://ccc.govt.nz/environment/land/ecosystem-map", " "], [' ', ' ']]
if is_in_christchurch(coordinates): elif is_in_auckland(questionnaire.location):
return [["Your location falls within the ecosystem type covered by the Christchurch Council ecosystem maps - further information can be obtained from Ōtautahi/Christchurch ecosystems map link to: https://ccc.govt.nz/environment/land/ecosystem-map", " "], [' ', ' ']] return [["Your location falls within the ecosystem type covered by the Auckland Council Tiaki Tāmaki Makaurau Conservation map - further information can be obtained from tiaki Tāmaki Makaurau conservation Auckland - link to https://www.tiakitamakimakaurau.nz/conservation-map/", " "], [' ', ' ']]
elif is_in_auckland(coordinates):
return [["Your location falls within the ecosystem type covered by the Auckland Council Tiaki Tāmaki Makaurau Conservation map - further information can be obtained from tiaki Tāmaki Makaurau conservation Auckland - link to https://www.tiakitamakimakaurau.nz/conservation-map/", " "], [' ', ' ']]
return [] return []
@ -99,19 +83,25 @@ def get_filter_values(params):
""" """
filter_rows = [] filter_rows = []
try:
ak = ActivationKey.objects.get(key=params.get("key", ""))
q = Questionnaire.objects.get(key=ak)
except (ActivationKey.DoesNotExist, Questionnaire.DoesNotExist):
return filter_rows
# Add all the location filters # Add all the location filters
filter_rows += get_location_filters(params) filter_rows += get_location_filters(q)
filter_rows.append([' ', ' ']) filter_rows.append([' ', ' '])
# Add the soil filters # Add the soil filters
filter_rows += get_soil_filters(params) filter_rows += get_soil_filters(q)
filter_rows.append([' ', ' ']) filter_rows.append([' ', ' '])
# Add the project site filters # Add the project site filters
filter_rows += get_site_filters(params) filter_rows += get_site_filters(q)
filter_rows.append([' ', ' ']) filter_rows.append([' ', ' '])
filter_rows += get_additional_region_info(params) filter_rows += get_additional_region_info(q)
return filter_rows return filter_rows

View file

@ -104,7 +104,7 @@ class QuestionnaireSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Questionnaire model = Questionnaire
fields = '__all__' exclude = ['id']
def validate_soil_variant(self, value): def validate_soil_variant(self, value):
try: try:

View file

@ -32,4 +32,5 @@ def activate_key(sender, instance, created, *args, **kwargs):
"""Consume one activation on the key associated with the created Questionnaire""" """Consume one activation on the key associated with the created Questionnaire"""
if created and (key := instance.key): if created and (key := instance.key):
key.remaining_activations -= 1 key.remaining_activations -= 1
key.activations += 1
key.save() key.save()

View file

@ -4,20 +4,20 @@ import stripe
from datetime import timedelta from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseBadRequest, FileResponse from django.contrib.gis.geos import Point
from django.http import JsonResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseBadRequest, FileResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from .models import Habitat, HabitatImage, Plant, EcologicalDistrictLayer, SoilOrder, Zone, Questionnaire, ActivationKey, ActivationKeySet from .models import Habitat, HabitatImage, Plant, EcologicalDistrictLayer, SoilOrder, Zone, Questionnaire, ActivationKey, ActivationKeySet, Customer, CustomerAddress
from .serializers import HabitatImageSerializer, HabitatSerializer, PlantSerializer, SoilOrderSerializer, EcologicalDistrictLayerSerializer, AddressSerializer, ZoneSerializer, QuestionnaireSerializer from .serializers import HabitatImageSerializer, HabitatSerializer, PlantSerializer, SoilOrderSerializer, EcologicalDistrictLayerSerializer, AddressSerializer, ZoneSerializer, QuestionnaireSerializer
from .filters import * from .filters import *
from .wms_utils import get_address_from_coordinates, search_address from .wms_utils import get_address_from_coordinates, search_address
from .resource_generation_utils import generate_csv, get_filter_values, serialize_plants_queryset, create_planting_guide_pdf, PLANTING_GUIDE_PDF_FILENAME, CSV_FILENAME, storage from .resource_generation_utils import generate_csv, get_filter_values, serialize_plants_queryset, create_planting_guide_pdf, PLANTING_GUIDE_PDF_FILENAME, storage
from .redis import redis_client from .redis import redis_client
@ -42,11 +42,14 @@ class SoilOrderViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
""" Filtering soil order query set by coordinate parameters in the URL. """ Filtering soil order query set by coordinate parameters in the URL.
""" """
coordinates = self.request.query_params.get('coordinates') try:
if coordinates is not None: lat = float(self.request.query_params["lat"])
return soil_order_coordinate_filter(coordinates) lng = float(self.request.query_params["lng"])
except (KeyError, ValueError):
return SoilOrder.objects.all()
return SoilOrder.objects.filter(soillayer__geom__intersects=Point(lng, lat, srid=4326))
return SoilOrder.objects.all()
class EcologicalDistrictViewSet(viewsets.ModelViewSet): class EcologicalDistrictViewSet(viewsets.ModelViewSet):
@ -57,11 +60,13 @@ class EcologicalDistrictViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
""" Filtering ecological district/region query set by coordinate parameters in the URL. """ Filtering ecological district/region query set by coordinate parameters in the URL.
""" """
coordinates = self.request.query_params.get('coordinates') try:
if coordinates is not None: lat = float(self.request.query_params["lat"])
return ecological_district_coordinate_filter(coordinates) lng = float(self.request.query_params["lng"])
except (KeyError, ValueError):
return EcologicalDistrictLayer.objects.all()
return EcologicalDistrictLayer.objects.all() return EcologicalDistrictLayer.objects.filter(geom__intersects=Point(lng, lat, srid=4326))
class LINZPropertyViewSet(viewsets.ViewSet): class LINZPropertyViewSet(viewsets.ViewSet):
@ -69,32 +74,48 @@ class LINZPropertyViewSet(viewsets.ViewSet):
""" """
def list(self, request): def list(self, request):
coordinates = self.request.query_params.get('coordinates') try:
lat = float(self.request.query_params["lat"])
lng = float(self.request.query_params["lng"])
except (KeyError, ValueError):
lat = lng = None
address = self.request.query_params.get('search') address = self.request.query_params.get('search')
if address is not None: if address is not None:
results = search_address(address) results = search_address(address)
return Response(results) return Response(results)
elif coordinates is not None: elif lat and lng:
address_data = get_address_from_coordinates(coordinates) address_data = get_address_from_coordinates(Point(lng, lat, srid=4326))
serializer = AddressSerializer(address_data) serializer = AddressSerializer(address_data)
return Response(serializer.data) return Response(serializer.data)
else:
return HttpResponseBadRequest("No parameters given.") return HttpResponseBadRequest("Invalid parameters.")
class AuckCHCHRegionInformation(viewsets.ViewSet): class AuckCHCHRegionInformation(viewsets.ViewSet):
""" Filtered viewset defining if coordinate falls inside auckland and chch regions. """ Filtered viewset defining if coordinate falls inside auckland and chch regions.
""" """
def list(self, request): def list(self, request):
coordinates = self.request.query_params.get('coordinates') try:
if coordinates is not None: lat = float(self.request.query_params["lat"])
in_chch = is_in_christchurch(coordinates) lng = float(self.request.query_params["lng"])
in_auckland = is_in_auckland(coordinates) except (KeyError, ValueError):
region_details = {"in_chch": in_chch, "in_auckland": in_auckland} return HttpResponseBadRequest("Missing or invalid coordinates.")
return Response(region_details)
else: p = Point(lng, lat, srid=4326)
return HttpResponseBadRequest("No coordinate given.") in_chch = False
in_auckland = False
# can avoid computing intersections for Auckland if we use a conditional here
if is_in_christchurch(p):
in_chch = True
elif is_in_auckland(p):
in_auckland = True
return Response({"in_chch": in_chch, "in_auckland": in_auckland})
class HabitatViewSet(viewsets.ModelViewSet): class HabitatViewSet(viewsets.ModelViewSet):
""" Viewset for all habitats. """ Viewset for all habitats.
@ -188,35 +209,62 @@ def validate_key(request):
return HttpResponseBadRequest("'key' not specified") return HttpResponseBadRequest("'key' not specified")
try: try:
if ActivationKey.objects.get(key=key).remaining_activations > 0: ak = ActivationKey.objects.get(key=key)
return HttpResponse()
except ActivationKey.DoesNotExist: except ActivationKey.DoesNotExist:
pass return HttpResponseNotFound()
return HttpResponseNotFound() if ak.remaining_activations > 0:
# valid key, permit entry
return JsonResponse({"type": ak.key_set.name})
elif ak.activations == 1:
# key has been activated, but can return the existing data from that activation
return JsonResponse(QuestionnaireSerializer(ak.questionnaire_set.first()).data)
# key has multiple activations, but all are expended
# could return most recent questionnaire but user who uses many-use keys probably doesn't care
return HttpResponseBadRequest()
def activate_key(request): def activate_key(request):
"""Adds a single activation to a given key if a Stripe payment has succeeded""" """Adds a single activation to a given key if a Stripe payment has succeeded"""
redirect_url = "/apply"
try: stripe.api_key = settings.STRIPE_API_KEY
key = request.GET['key']
stripe_session_id = redis_client.getdel(key).decode()
stripe_session = stripe.checkout.Session.retrieve(stripe_session_id)
status = stripe_session.payment_status
except (KeyError, AttributeError):
return redirect(redirect_url)
match status: redirect_url = "/"
case "paid":
ActivationKey.objects.create( key = request.GET['key']
key=key, stripe_session_id = redis_client.get(key).decode()
key_set=ActivationKeySet.objects.get_or_create(name="Stripe", size=0)[0], stripe_session = stripe.checkout.Session.retrieve(stripe_session_id)
) is_physical = stripe_session.metadata.get("physical") == "true"
redirect_url += "?key=" + key kwargs = {}
case "open":
stripe.checkout.Session.expire(stripe_session_id) if is_physical:
address, _ = CustomerAddress.objects.get_or_create(
city=stripe_session.customer_details.address['city'],
line1=stripe_session.customer_details.address['line1'],
line2=stripe_session.customer_details.address['line2'],
postal_code=int(stripe_session.customer_details.address['postal_code']),
)
customer, _ = Customer.objects.get_or_create(
email=stripe_session.customer_details.email,
name=stripe_session.customer_details.name,
address=address,
)
key_set, _ = ActivationKeySet.objects.get_or_create(
name=settings.STRIPE_PHYSICAL_KEY_SET,
size=0,
)
kwargs["customer"] = customer
else:
key_set, _ = ActivationKeySet.objects.get_or_create(
name=settings.STRIPE_DIGITAL_KEY_SET,
size=0,
)
if stripe_session.payment_status == "paid":
ActivationKey.objects.create(key=key, key_set=key_set, **kwargs)
redis_client.delete(key)
redirect_url += "?key=" + key
return redirect(redirect_url) return redirect(redirect_url)
@ -225,13 +273,28 @@ def purchase_key(request):
"""Generate a prospective key and redirect to the Stripe payment portal""" """Generate a prospective key and redirect to the Stripe payment portal"""
stripe.api_key = settings.STRIPE_API_KEY stripe.api_key = settings.STRIPE_API_KEY
price_id = settings.STRIPE_DIGITAL_PRICE_ID
extra_kwargs = {}
key = ActivationKey.key_default() key = ActivationKey.key_default()
redirect_url = request.build_absolute_uri(reverse(activate_key)) + f"?key={key}" redirect_url = request.build_absolute_uri(reverse(activate_key)) + f"?key={key}"
# requesting checkout for physical copy
if request.GET.get("physical", "").lower() in {"t", "true", "y", "yes", "1"}:
price_id = settings.STRIPE_PHYSICAL_PRICE_ID
extra_kwargs = {
'shipping_address_collection': {
'allowed_countries': ['NZ'],
},
'metadata': {
'physical': 'true',
},
}
stripe_session = stripe.checkout.Session.create( stripe_session = stripe.checkout.Session.create(
line_items=[ line_items=[
{ {
"price": settings.STRIPE_PRICE_ID, "price": price_id,
"quantity": 1, "quantity": 1,
}, },
], ],
@ -247,6 +310,7 @@ def purchase_key(request):
mode='payment', mode='payment',
success_url=redirect_url, success_url=redirect_url,
cancel_url=redirect_url, cancel_url=redirect_url,
**extra_kwargs,
) )
redis_client.setex(key, timedelta(hours=8), stripe_session.id) redis_client.setex(key, timedelta(hours=8), stripe_session.id)

View file

@ -164,4 +164,7 @@ CELERY_BROKER_URL = REDIS_CELERY_URL
# Stripe payment processing # Stripe payment processing
STRIPE_API_KEY = os.environ['STRIPE_API_KEY'] STRIPE_API_KEY = os.environ['STRIPE_API_KEY']
STRIPE_PRICE_ID = os.environ['STRIPE_PRICE_ID'] STRIPE_DIGITAL_PRICE_ID = os.environ['STRIPE_DIGITAL_PRICE_ID']
STRIPE_PHYSICAL_PRICE_ID = os.environ['STRIPE_PHYSICAL_PRICE_ID']
STRIPE_DIGITAL_KEY_SET = "Stripe - digital"
STRIPE_PHYSICAL_KEY_SET = "Stripe - physical"

View file

@ -26,6 +26,7 @@ services:
- manage.py - manage.py
- collectstatic - collectstatic
- --noinput - --noinput
restart: on-failure
backend: backend:
<<: *django <<: *django

View file

@ -13,14 +13,6 @@ x-django: &django
restart: unless-stopped restart: unless-stopped
services: services:
collectstatic:
<<: *django
container_name: collectstatic
command:
- python
- manage.py
- collectstatic
- --noinput
backend: backend:
<<: *django <<: *django
@ -32,8 +24,6 @@ services:
condition: service_healthy condition: service_healthy
celery: celery:
condition: service_healthy condition: service_healthy
collectstatic:
condition: service_completed_successfully
expose: expose:
- "8000" - "8000"
command: command: