[#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:
parent
d09e6f3914
commit
13c8436d98
11 changed files with 255 additions and 179 deletions
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
50
backend/right_tree/api/migrations/0017_auto_20230420_1457.py
Normal file
50
backend/right_tree/api/migrations/0017_auto_20230420_1457.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -26,6 +26,7 @@ services:
|
||||||
- manage.py
|
- manage.py
|
||||||
- collectstatic
|
- collectstatic
|
||||||
- --noinput
|
- --noinput
|
||||||
|
restart: on-failure
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
<<: *django
|
<<: *django
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue