[#41] Allow users to download the user/planng guide for a payment #101
31 changed files with 670 additions and 511 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)
|
||||||
|
|
|
@ -5,4 +5,5 @@ class ApiConfig(AppConfig):
|
||||||
name = 'right_tree.api'
|
name = 'right_tree.api'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
# flake8: noqa
|
||||||
import right_tree.api.signals
|
import right_tree.api.signals
|
||||||
|
|
|
@ -1,89 +1,52 @@
|
||||||
import json
|
from .models import (
|
||||||
|
Plant,
|
||||||
from django.http import Http404
|
EcologicalRegion,
|
||||||
from django.db.models import Q
|
EcologicalDistrictLayer,
|
||||||
|
ChristchurchRegion,
|
||||||
from .models import Plant, EcologicalRegion, EcologicalDistrictLayer, ChristchurchRegion, SoilOrder, SoilVariant
|
SoilOrder,
|
||||||
|
SoilVariant,
|
||||||
|
ActivationKey,
|
||||||
|
Questionnaire
|
||||||
|
)
|
||||||
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()
|
||||||
print(eco_district.ecologic_2)
|
print(eco_district.ecologic_2)
|
||||||
return eco_district is not None and eco_district.ecologic_2.name == 'Auckland'
|
return eco_district is not None and eco_district.ecologic_2.name == 'Auckland'
|
||||||
|
|
||||||
|
|
||||||
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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -11,7 +11,6 @@ from django.contrib.postgres.indexes import OpClass
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SoilOrder(models.Model):
|
class SoilOrder(models.Model):
|
||||||
code = models.CharField(unique=True, max_length=1)
|
code = models.CharField(unique=True, max_length=1)
|
||||||
name = models.CharField(unique=True, max_length=50)
|
name = models.CharField(unique=True, max_length=50)
|
||||||
|
@ -202,6 +201,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 +242,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 +256,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,96 +23,86 @@ 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:
|
|
||||||
if is_in_christchurch(coordinates):
|
|
||||||
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 Christchurch Council ecosystem maps - further information can be obtained from Ōtautahi/Christchurch ecosystems map link to: https://ccc.govt.nz/environment/land/ecosystem-map", " "], [' ', ' ']]
|
||||||
elif is_in_auckland(coordinates):
|
elif is_in_auckland(questionnaire.location):
|
||||||
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 [["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 []
|
||||||
|
|
||||||
|
|
||||||
def get_filter_values(params):
|
def get_filter_values(params):
|
||||||
""" Retrives all selected values/filters from the request parameters
|
""" Retrives all selected values/filters from the request parameters
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
from rest_framework import serializers, exceptions
|
from rest_framework import serializers, exceptions
|
||||||
from right_tree.api.models import *
|
|
||||||
|
from .models import (
|
||||||
|
ToleranceLevel,
|
||||||
|
EcologicalRegion,
|
||||||
|
EcologicalDistrictLayer,
|
||||||
|
SoilOrder,
|
||||||
|
SoilVariant,
|
||||||
|
HabitatImage,
|
||||||
|
Habitat,
|
||||||
|
Zone,
|
||||||
|
Plant,
|
||||||
|
Questionnaire,
|
||||||
|
ActivationKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ToleranceLevelSerializer(serializers.HyperlinkedModelSerializer):
|
class ToleranceLevelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
@ -39,8 +52,10 @@ class SoilVariantSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
model = SoilVariant
|
model = SoilVariant
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class HabitatImageSerializer(serializers.HyperlinkedModelSerializer):
|
class HabitatImageSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
id = serializers.ReadOnlyField()
|
id = serializers.ReadOnlyField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = HabitatImage
|
model = HabitatImage
|
||||||
fields = ['id', 'name', 'image_filename']
|
fields = ['id', 'name', 'image_filename']
|
||||||
|
@ -54,6 +69,7 @@ class HabitatSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
model = Habitat
|
model = Habitat
|
||||||
fields = ['id', 'name', 'images']
|
fields = ['id', 'name', 'images']
|
||||||
|
|
||||||
|
|
||||||
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
|
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
id = serializers.ReadOnlyField()
|
id = serializers.ReadOnlyField()
|
||||||
habitat = HabitatSerializer()
|
habitat = HabitatSerializer()
|
||||||
|
@ -63,6 +79,8 @@ class ZoneSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
model = Zone
|
model = Zone
|
||||||
fields = ['id', 'name', 'variant',
|
fields = ['id', 'name', 'variant',
|
||||||
'refined_variant', 'habitat', 'related_svg_segment', 'redirect_habitat', 'ignore_soil_order_filter', 'ignore_location_filter', 'tooltip_display_text']
|
'refined_variant', 'habitat', 'related_svg_segment', 'redirect_habitat', 'ignore_soil_order_filter', 'ignore_location_filter', 'tooltip_display_text']
|
||||||
|
|
||||||
|
|
||||||
class LocationDetailsSerializer(serializers.Serializer):
|
class LocationDetailsSerializer(serializers.Serializer):
|
||||||
ecologic_1 = serializers.CharField(max_length=50)
|
ecologic_1 = serializers.CharField(max_length=50)
|
||||||
ecologic_2 = serializers.CharField(max_length=50)
|
ecologic_2 = serializers.CharField(max_length=50)
|
||||||
|
@ -104,7 +122,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 get_filtered_plants, is_in_auckland, is_in_christchurch
|
||||||
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,12 +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.all()
|
||||||
|
|
||||||
|
return SoilOrder.objects.filter(soillayer__geom__intersects=Point(lng, lat, srid=4326))
|
||||||
|
|
||||||
|
|
||||||
class EcologicalDistrictViewSet(viewsets.ModelViewSet):
|
class EcologicalDistrictViewSet(viewsets.ModelViewSet):
|
||||||
""" Filtered viewset for ecological district/region details.
|
""" Filtered viewset for ecological district/region details.
|
||||||
|
@ -57,44 +59,62 @@ 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):
|
||||||
""" Filtered viewset for ecological district/region details.
|
""" Filtered viewset for ecological district/region details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 +208,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
|
||||||
|
|
||||||
|
redirect_url = "/"
|
||||||
|
|
||||||
key = request.GET['key']
|
key = request.GET['key']
|
||||||
stripe_session_id = redis_client.getdel(key).decode()
|
stripe_session_id = redis_client.get(key).decode()
|
||||||
stripe_session = stripe.checkout.Session.retrieve(stripe_session_id)
|
stripe_session = stripe.checkout.Session.retrieve(stripe_session_id)
|
||||||
status = stripe_session.payment_status
|
is_physical = stripe_session.metadata.get("physical") == "true"
|
||||||
except (KeyError, AttributeError):
|
kwargs = {}
|
||||||
return redirect(redirect_url)
|
|
||||||
|
|
||||||
match status:
|
if is_physical:
|
||||||
case "paid":
|
address, _ = CustomerAddress.objects.get_or_create(
|
||||||
ActivationKey.objects.create(
|
city=stripe_session.customer_details.address['city'],
|
||||||
key=key,
|
line1=stripe_session.customer_details.address['line1'],
|
||||||
key_set=ActivationKeySet.objects.get_or_create(name="Stripe", size=0)[0],
|
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
|
redirect_url += "?key=" + key
|
||||||
case "open":
|
|
||||||
stripe.checkout.Session.expire(stripe_session_id)
|
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
@ -225,13 +272,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 +309,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)
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ def wfs_getfeature(endpoint, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return response.json()
|
return response.json()
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError:
|
||||||
raise WFSError(
|
raise WFSError(
|
||||||
f"Failed to make WFS request to {url}: {response.content}")
|
f"Failed to make WFS request to {url}: {response.content}")
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -6,11 +6,7 @@
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"title": "Right Plant Right Place Right Time\nPlant Selector Tool for New Zealand.",
|
"title": "Right Plant Right Place Right Time\nPlant Selector Tool for New Zealand.",
|
||||||
"description": "Your native plant selection starts here! Use the map to select a planting site location within New Zealand. On the following pages you will provide more details on your project until the system has enough information to create your plant species list and planting plan. To start, click on the map and pan and zoom to the site location. Once the location is selected, click on the “next step” button to complete the process. Repeat this process for sites at different locations."
|
"description": "Your native plant selection starts here! Use the map or enter an address to select a planting site location within New Zealand. On the following pages you will provide more details on your project until the system has enough information to create your plant species list and planting plan. To start, click on the map and pan and zoom to the site location. Once the location is selected, click on the “next step” button to complete the process. Repeat this process for sites at different locations."
|
||||||
},
|
|
||||||
"address": {
|
|
||||||
"title": "Address",
|
|
||||||
"description": "Thank you for purchasing an activation key. Please start entering your address and select an option from the suggestions provided. On the following pages, you will provide more details on your project until the system has enough information to create your plant species list and planting plan. Once the location is selected, click on the “next step” button to complete the process."
|
|
||||||
},
|
},
|
||||||
"soil": {
|
"soil": {
|
||||||
"title": "Soil Variant Selection",
|
"title": "Soil Variant Selection",
|
||||||
|
@ -39,10 +35,6 @@
|
||||||
"results": {
|
"results": {
|
||||||
"title": "Plant List Results",
|
"title": "Plant List Results",
|
||||||
"forestDiagramDescription": "Forest Position Information Diagram"
|
"forestDiagramDescription": "Forest Position Information Diagram"
|
||||||
},
|
|
||||||
"complete": {
|
|
||||||
"title": "Application Complete",
|
|
||||||
"description": "You have completed your application and submitted your results. You may now return to the homepage or fill out another application for a different habitat or zone."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
68
frontend/src/components/providers/ActivationProvider.jsx
Normal file
68
frontend/src/components/providers/ActivationProvider.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { createContext, useState, useContext } from 'react';
|
||||||
|
import Repository from '../../repository/Repository';
|
||||||
|
import { useError } from './ErrorProvider';
|
||||||
|
|
||||||
|
const ActivationContext = createContext(null);
|
||||||
|
|
||||||
|
const ActivationProvider = ({children}) => {
|
||||||
|
const [key, setKey] = useState("");
|
||||||
|
const [isPhysicalKey, setIsPhysicalKey] = useState(false);
|
||||||
|
const { setError, resetError } = useError();
|
||||||
|
|
||||||
|
const validateKey = (value) => new Promise(resolve => {
|
||||||
|
|
||||||
|
const data = {key: value};
|
||||||
|
|
||||||
|
setKey(value);
|
||||||
|
|
||||||
|
Repository.post("/key/validate/", data).then(resp => {
|
||||||
|
if (resp.data) {
|
||||||
|
setIsPhysicalKey(resp.data?.type === "Stripe - physical");
|
||||||
|
resetError();
|
||||||
|
try {
|
||||||
|
const coordinates = resp.data.location.match(/(-?\d+\.\d+)\s(-?\d+\.\d+)/);
|
||||||
|
resolve({
|
||||||
|
coordinates: {
|
||||||
|
lng: parseFloat(coordinates[1]),
|
||||||
|
lat: parseFloat(coordinates[2]),
|
||||||
|
},
|
||||||
|
soilVariant: resp.data.soil_variant[0],
|
||||||
|
zone: {id: resp.data.zone},
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
resolve({...data, ...resp.data});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
switch (e.response.status) {
|
||||||
|
case 400:
|
||||||
|
case 404:
|
||||||
|
setError("Invalid or expired activation key. Please try again.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setError("Something went wrong. Please try again.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
key,
|
||||||
|
isDigitalKey: !isPhysicalKey,
|
||||||
|
isPhysicalKey,
|
||||||
|
validateKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ActivationContext.Provider value={value}>{children}</ActivationContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useActivator = () => useContext(ActivationContext);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ActivationProvider,
|
||||||
|
useActivator,
|
||||||
|
};
|
24
frontend/src/components/providers/ErrorProvider.jsx
Normal file
24
frontend/src/components/providers/ErrorProvider.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { createContext, useState, useContext } from 'react';
|
||||||
|
|
||||||
|
const ErrorContext = createContext(null);
|
||||||
|
|
||||||
|
const ErrorProvider = ({children}) => {
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const resetError = () => setError("");
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
resetError,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ErrorContext.Provider value={value}>{children}</ErrorContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useError = () => useContext(ErrorContext);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ErrorProvider,
|
||||||
|
useError,
|
||||||
|
};
|
|
@ -15,12 +15,14 @@ const StepperWizard = ({children}) => {
|
||||||
const isStep = n => (0 <= n && n < children.length);
|
const isStep = n => (0 <= n && n < children.length);
|
||||||
const setStepNext = () => setStep(n => isStep(n + 1) ? n + 1 : n);
|
const setStepNext = () => setStep(n => isStep(n + 1) ? n + 1 : n);
|
||||||
const setStepBack = () => setStep(n => isStep(n - 1) ? n - 1 : n);
|
const setStepBack = () => setStep(n => isStep(n - 1) ? n - 1 : n);
|
||||||
|
const setStepLast = () => setStep(children.length - 1);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
step,
|
step,
|
||||||
setStep,
|
setStep,
|
||||||
setStepNext,
|
setStepNext,
|
||||||
setStepBack,
|
setStepBack,
|
||||||
|
setStepLast,
|
||||||
isStep,
|
isStep,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,39 +4,36 @@ import StepInformation from '../StepInformation';
|
||||||
import staticText from '../../../assets/data/staticText.json'
|
import staticText from '../../../assets/data/staticText.json'
|
||||||
import keyBackgroundImage from '../../../assets/img/stepBackgrounds/step6.jpg';
|
import keyBackgroundImage from '../../../assets/img/stepBackgrounds/step6.jpg';
|
||||||
import { StepperFooter, useStepper } from '../../providers/StepperProvider';
|
import { StepperFooter, useStepper } from '../../providers/StepperProvider';
|
||||||
|
import { useActivator } from '../../providers/ActivationProvider';
|
||||||
import { useFilter } from '../../providers/FilterProvider';
|
import { useFilter } from '../../providers/FilterProvider';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Repository from '../../../repository/Repository';
|
import PurchaseButton from './PurchaseButton';
|
||||||
|
|
||||||
const ActivationStep = () => {
|
const ActivationStep = () => {
|
||||||
const MAX_LENGTH = 20;
|
const MAX_LENGTH = 20;
|
||||||
const [value, setValue] = useState(new URLSearchParams(window.location.search).get("key") || "");
|
const [value, setValue] = useState(new URLSearchParams(window.location.search).get("key") || "");
|
||||||
const [nextDisabled, setNextDisabled] = useState(value.length < MAX_LENGTH);
|
const [nextDisabled, setNextDisabled] = useState(value.length < MAX_LENGTH);
|
||||||
const [error, setError] = useState("");
|
const { setFilters, updateFilters } = useFilter();
|
||||||
const { updateFilters } = useFilter();
|
const { setStepNext, setStepLast } = useStepper();
|
||||||
const { setStepNext } = useStepper();
|
const { validateKey } = useActivator();
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
const newValue = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, MAX_LENGTH);
|
const newValue = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, MAX_LENGTH);
|
||||||
setNextDisabled(newValue.length !== MAX_LENGTH);
|
setNextDisabled(newValue.length !== MAX_LENGTH);
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
setError("");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNext = () => {
|
const onNext = async () => {
|
||||||
const data = { key: value };
|
const data = await validateKey(value);
|
||||||
Repository.post("/key/validate/", data).then(resp => {
|
|
||||||
|
if (data?.coordinates) {
|
||||||
|
setFilters(data);
|
||||||
|
setStepLast();
|
||||||
|
} else if (data) {
|
||||||
updateFilters(data);
|
updateFilters(data);
|
||||||
setStepNext();
|
setStepNext();
|
||||||
}).catch(e => {
|
}
|
||||||
setError(
|
|
||||||
e.response.status === 404
|
|
||||||
? "Invalid or expired activation key. Please try again."
|
|
||||||
: "Something went wrong. Please try again."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyPanel = (
|
const keyPanel = (
|
||||||
|
@ -51,14 +48,12 @@ const ActivationStep = () => {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="Enter activation key..."
|
placeholder="Enter activation key..."
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
error={error.length > 0}
|
|
||||||
helperText={error}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'row', py: 2, paddingRight: '10pt', paddingLeft: '10pt', minHeight: '68px' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'row', py: 2, paddingRight: '10pt', paddingLeft: '10pt', minHeight: '68px' }}>
|
||||||
<Box sx={{ flex: '1 1 auto' }} />
|
<Box sx={{ flex: '1 1 auto' }} />
|
||||||
<Button href="/api/key/purchase">Purchase Key</Button>
|
<PurchaseButton />
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
44
frontend/src/components/steps/activation/PurchaseButton.jsx
Normal file
44
frontend/src/components/steps/activation/PurchaseButton.jsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
|
const PurchaseButton = () => {
|
||||||
|
const [anchor, setAnchor] = useState(null);
|
||||||
|
const isOpen = Boolean(anchor);
|
||||||
|
|
||||||
|
const toggleMenu = (e) => {
|
||||||
|
setAnchor(isOpen ? null : e.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (href) => {
|
||||||
|
setAnchor(null);
|
||||||
|
window.location = href;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
id="purchase-button"
|
||||||
|
aria-controls={isOpen ? "basic-menu" : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={isOpen ? "true" : undefined}
|
||||||
|
onClick={toggleMenu}
|
||||||
|
>
|
||||||
|
Purchase Key
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
id="purchase-menu"
|
||||||
|
anchorEl={anchor}
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => toggleMenu(null)}
|
||||||
|
MenuListProps={{"aria-labelledby": "purchase-button"}}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => onSelect("/api/key/purchase")}>Purchase digital copy</MenuItem>
|
||||||
|
<MenuItem onClick={() => onSelect("/api/key/purchase?physical=true")}>Purchase physical copy</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchaseButton;
|
|
@ -1,49 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import Step from '../Step';
|
|
||||||
import StepInformation from '../StepInformation';
|
|
||||||
import staticText from '../../../assets/data/staticText.json'
|
|
||||||
import addressBackgroundImage from '../../../assets/img/stepBackgrounds/step1.jpg';
|
|
||||||
import AddressSearch from './AddressSearch';
|
|
||||||
import { StepperFooter } from '../../providers/StepperProvider';
|
|
||||||
import { useFilter } from '../../providers/FilterProvider';
|
|
||||||
|
|
||||||
|
|
||||||
const AddressStep = () => {
|
|
||||||
const [nextDisabled, setNextDisabled] = useState(true);
|
|
||||||
const { updateFilters } = useFilter();
|
|
||||||
|
|
||||||
const addressPanel = (
|
|
||||||
<div className="p-5">
|
|
||||||
<StepInformation
|
|
||||||
title={staticText.steps.address.title}
|
|
||||||
description={<p>{staticText.steps.address.description}</p>}
|
|
||||||
/>
|
|
||||||
<div className="p-4">
|
|
||||||
<AddressSearch onSelect={address => {
|
|
||||||
if (address) {
|
|
||||||
setNextDisabled(false);
|
|
||||||
updateFilters({
|
|
||||||
coordinates: {
|
|
||||||
lat: address.coordinates[1],
|
|
||||||
lng: address.coordinates[0],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setNextDisabled(true);
|
|
||||||
}
|
|
||||||
}}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Step
|
|
||||||
contentComponent={addressPanel}
|
|
||||||
backgroundImage={addressBackgroundImage}
|
|
||||||
/>
|
|
||||||
<StepperFooter nextDisabled={nextDisabled} />
|
|
||||||
</>);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddressStep;
|
|
|
@ -1,24 +0,0 @@
|
||||||
import Step from "../Step";
|
|
||||||
import StepInformation from "../StepInformation";
|
|
||||||
import completeBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
|
|
||||||
import staticText from "../../../assets/data/staticText.json";
|
|
||||||
import { StepperFooter } from "../../providers/StepperProvider";
|
|
||||||
|
|
||||||
export default function CompleteStep() {
|
|
||||||
const completeInfoPanel = (
|
|
||||||
<StepInformation
|
|
||||||
title={staticText.steps.complete.title}
|
|
||||||
description={<p>{staticText.steps.complete.description}</p>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Step
|
|
||||||
informationComponent={completeInfoPanel}
|
|
||||||
backgroundImage={completeBackgroundImage}
|
|
||||||
/>
|
|
||||||
<StepperFooter />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -25,7 +25,7 @@ const AddressSearchSuggestions = ({results, onClick}) => (
|
||||||
</Box>
|
</Box>
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
const AddressSearch = ({onSelect, classNames}) => {
|
const AddressSearch = ({onSelect}) => {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const [enable, setEnable] = useState(true);
|
const [enable, setEnable] = useState(true);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
|
@ -53,7 +53,7 @@ const AddressSearch = ({onSelect, classNames}) => {
|
||||||
}, [selected, onSelect]);
|
}, [selected, onSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div classNames={classNames}>
|
<Box sx={{width: '100%', height: '20%'}}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
@ -74,6 +74,7 @@ const AddressSearch = ({onSelect, classNames}) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
|
sx={{top: 0}}
|
||||||
/>
|
/>
|
||||||
<AddressSearchSuggestions
|
<AddressSearchSuggestions
|
||||||
results={results}
|
results={results}
|
||||||
|
@ -83,7 +84,7 @@ const AddressSearch = ({onSelect, classNames}) => {
|
||||||
setSelected(r);
|
setSelected(r);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>);
|
</Box>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddressSearch;
|
export default AddressSearch;
|
|
@ -1,31 +1,57 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
import Step from "../Step";
|
import Step from "../Step";
|
||||||
import LocationSelectorMap from "./Map";
|
import LocationSelectorMap from "./Map";
|
||||||
import StepInformation from "../StepInformation";
|
import StepInformation from "../StepInformation";
|
||||||
import staticText from "../../../assets/data/staticText.json";
|
import staticText from "../../../assets/data/staticText.json";
|
||||||
import locationBackgroundImage from "../../../assets/img/stepBackgrounds/step1.jpg";
|
import locationBackgroundImage from "../../../assets/img/stepBackgrounds/step1.jpg";
|
||||||
import { StepperFooter } from '../../providers/StepperProvider';
|
import { StepperFooter } from '../../providers/StepperProvider';
|
||||||
|
import AddressSearch from "./AddressSearch";
|
||||||
|
import { useFilter } from '../../providers/FilterProvider';
|
||||||
|
|
||||||
export default function LocationStep(props) {
|
export default function LocationStep({defaultIsSearch = false}) {
|
||||||
const [nextDisabled, setNextDisabled] = useState(true);
|
const [nextDisabled, setNextDisabled] = useState(true);
|
||||||
|
const [showSearch, setShowSearch] = useState(defaultIsSearch);
|
||||||
|
const { updateFilters } = useFilter();
|
||||||
|
|
||||||
const locationInfoPanel = (
|
const locationInfoPanel = (
|
||||||
|
<div>
|
||||||
<StepInformation
|
<StepInformation
|
||||||
title={staticText.steps.location.title}
|
title={staticText.steps.location.title}
|
||||||
description={<p>{staticText.steps.location.description}</p>}
|
description={<p>{staticText.steps.location.description}</p>}
|
||||||
/>
|
/>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'row', py: 2, paddingRight: '10pt', paddingLeft: '10pt', minHeight: '68px' }}>
|
||||||
|
<Box sx={{ flex: '1 1 auto' }} />
|
||||||
|
<Button onClick={() => setShowSearch(s => !s)}>Switch to {showSearch ? "map" : "search"}</Button>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const locationSelectionPanel = <LocationSelectorMap setNextDisabled={setNextDisabled} />;
|
const selectionComponent = showSearch
|
||||||
|
? <AddressSearch onSelect={address => {
|
||||||
|
if (address) {
|
||||||
|
setNextDisabled(false);
|
||||||
|
updateFilters({
|
||||||
|
coordinates: {
|
||||||
|
lat: address.coordinates[1],
|
||||||
|
lng: address.coordinates[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setNextDisabled(true);
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
|
: <LocationSelectorMap setNextDisabled={setNextDisabled} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Step
|
<Step
|
||||||
informationComponent={locationInfoPanel}
|
informationComponent={locationInfoPanel}
|
||||||
selectionComponent={locationSelectionPanel}
|
selectionComponent={selectionComponent}
|
||||||
backgroundImage={locationBackgroundImage}
|
backgroundImage={locationBackgroundImage}
|
||||||
/>
|
/>
|
||||||
<StepperFooter nextDisabled={nextDisabled} />
|
<StepperFooter nextDisabled={nextDisabled} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -23,9 +23,8 @@ import staticText from "../../../assets/data/staticText.json";
|
||||||
import forestGraphic from "../../../assets/img/habitats/1a_Forest_Section.png";
|
import forestGraphic from "../../../assets/img/habitats/1a_Forest_Section.png";
|
||||||
import { CircularProgress } from "@mui/material";
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
function TablePaginationActions(props) {
|
function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { count, page, rowsPerPage, onPageChange } = props;
|
|
||||||
|
|
||||||
const handleFirstPageButtonClick = (event) => {
|
const handleFirstPageButtonClick = (event) => {
|
||||||
onPageChange(event, 0);
|
onPageChange(event, 0);
|
||||||
|
@ -95,18 +94,21 @@ TablePaginationActions.propTypes = {
|
||||||
export default function PlantResultsTable(props) {
|
export default function PlantResultsTable(props) {
|
||||||
const [page, setPage] = React.useState(0);
|
const [page, setPage] = React.useState(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState(25);
|
const [rowsPerPage, setRowsPerPage] = React.useState(25);
|
||||||
|
const isLoading = props?.rows === null;
|
||||||
|
|
||||||
let rows = [];
|
let rows = [];
|
||||||
if (props.rows) {
|
if (!isLoading) {
|
||||||
rows =
|
rows =
|
||||||
rowsPerPage > 0
|
rowsPerPage > 0
|
||||||
? props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
? props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||||
: props.rows;
|
: props.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalRows = props?.rows?.length ?? 0;
|
||||||
|
|
||||||
// Avoid a layout jump when reaching the last page with empty rows.
|
// Avoid a layout jump when reaching the last page with empty rows.
|
||||||
const emptyRows =
|
const emptyRows =
|
||||||
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - props.rows.length) : 0;
|
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - totalRows) : 0;
|
||||||
|
|
||||||
const handleChangePage = (event, newPage) => {
|
const handleChangePage = (event, newPage) => {
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
|
@ -157,17 +159,15 @@ export default function PlantResultsTable(props) {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.length === 0 && (
|
{isLoading
|
||||||
<TableRow style={{ height: 150 }}>
|
? <TableRow style={{ height: 150 }}>
|
||||||
<TableCell colSpan={7}>
|
<TableCell colSpan={7}>
|
||||||
<Stack alignItems="center">
|
<Stack alignItems="center">
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
: rows.map((row) => (
|
||||||
{rows.length > 0 &&
|
|
||||||
rows.map((row) => (
|
|
||||||
<TableRow key={row.name}>
|
<TableRow key={row.name}>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{row.name}
|
{row.name}
|
||||||
|
@ -179,8 +179,8 @@ export default function PlantResultsTable(props) {
|
||||||
<TableCell align="right">{row.carbonSequestration}</TableCell>
|
<TableCell align="right">{row.carbonSequestration}</TableCell>
|
||||||
<TableCell align="right">{row.plantingStage}</TableCell>
|
<TableCell align="right">{row.plantingStage}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
{emptyRows > 0 && (
|
{emptyRows > 0 && (
|
||||||
<TableRow style={{ height: 53 * emptyRows }}>
|
<TableRow style={{ height: 53 * emptyRows }}>
|
||||||
<TableCell colSpan={7} />
|
<TableCell colSpan={7} />
|
||||||
|
@ -193,7 +193,7 @@ export default function PlantResultsTable(props) {
|
||||||
className="plant-list-pagination"
|
className="plant-list-pagination"
|
||||||
rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
|
rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
|
||||||
colSpan={7}
|
colSpan={7}
|
||||||
count={props.rows.length}
|
count={totalRows}
|
||||||
rowsPerPage={rowsPerPage}
|
rowsPerPage={rowsPerPage}
|
||||||
page={page}
|
page={page}
|
||||||
SelectProps={{
|
SelectProps={{
|
||||||
|
|
|
@ -5,10 +5,10 @@ import PlantList from "./PlantList";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import PlantRepository from "../../../repository/PlantRepository";
|
import PlantRepository from "../../../repository/PlantRepository";
|
||||||
import { Typography, Box } from "@mui/material";
|
import { Typography, Box, Modal } from "@mui/material";
|
||||||
import resultsBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
|
import resultsBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
|
||||||
import staticText from "../../../assets/data/staticText.json";
|
import staticText from "../../../assets/data/staticText.json";
|
||||||
import { useFilter } from "../../providers/FilterProvider";
|
import { useActivator } from "../../providers/ActivationProvider";
|
||||||
import { StepperFooter } from "../../providers/StepperProvider";
|
import { StepperFooter } from "../../providers/StepperProvider";
|
||||||
|
|
||||||
const RESULTS_DESCRIPTION = (
|
const RESULTS_DESCRIPTION = (
|
||||||
|
@ -30,56 +30,32 @@ const RESULTS_DESCRIPTION = (
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function ResultsStep(props) {
|
export default function ResultsStep(props) {
|
||||||
const [plants, setPlants] = useState([]);
|
const [plants, setPlants] = useState(null);
|
||||||
const { filters } = useFilter();
|
const [showModal, setShowModal] = useState(true);
|
||||||
|
const { key } = useActivator();
|
||||||
|
|
||||||
|
const closeModal = () => setShowModal(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatePlants = () => {
|
PlantRepository.getFilteredPlants(key)
|
||||||
PlantRepository.getFilteredPlants(filters)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
setPlants(response.status === 200 ? response.data : []);
|
||||||
setPlants(response.data);
|
}).catch(e => {
|
||||||
}
|
setPlants([]);
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
this.setState({ plants: ["No plants found."] });
|
|
||||||
});
|
});
|
||||||
};
|
}, [key]);
|
||||||
updatePlants();
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
function createData(
|
|
||||||
name,
|
|
||||||
growthForm,
|
|
||||||
moisturePreferences,
|
|
||||||
plantTolerances,
|
|
||||||
ecosystemServices,
|
|
||||||
carbonSequestration,
|
|
||||||
plantingStage
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
growthForm,
|
|
||||||
moisturePreferences,
|
|
||||||
plantTolerances,
|
|
||||||
ecosystemServices,
|
|
||||||
carbonSequestration,
|
|
||||||
plantingStage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTableRows = () => {
|
const getTableRows = () => {
|
||||||
return plants.map((plant) => {
|
// null if unloaded, empty if loaded but no results
|
||||||
return createData(
|
return plants?.map((plant) => ({
|
||||||
plant.display_name,
|
name: plant.display_name,
|
||||||
plant.display_growth_form,
|
growthForm: plant.display_growth_form,
|
||||||
plant.moisture_preferences,
|
moisturePreferences: plant.moisture_preferences,
|
||||||
plant.plant_tolerances,
|
plantTolerances: plant.plant_tolerances,
|
||||||
plant.ecosystem_services,
|
ecosystemServices: plant.ecosystem_services,
|
||||||
plant.carbon_sequestration,
|
carbonSequestration: plant.carbon_sequestration,
|
||||||
plant.stage
|
stage: plant.stage,
|
||||||
);
|
})) ?? null;
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = (response, fileType, fileName) => {
|
const download = (response, fileType, fileName) => {
|
||||||
|
@ -94,13 +70,13 @@ export default function ResultsStep(props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadCSV = () => {
|
const downloadCSV = () => {
|
||||||
PlantRepository.getPlantsCSV(filters).then((response) => {
|
PlantRepository.getPlantsCSV(key).then((response) => {
|
||||||
download(response, "text/csv", "plants.csv");
|
download(response, "text/csv", "plants.csv");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadPDF = () => {
|
const downloadPDF = () => {
|
||||||
PlantRepository.getPlantsPDF(filters).then((response) => {
|
PlantRepository.getPlantsPDF(key).then((response) => {
|
||||||
download(response, "application/pdf", "planting_guide.pdf");
|
download(response, "application/pdf", "planting_guide.pdf");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -142,6 +118,28 @@ export default function ResultsStep(props) {
|
||||||
backgroundImage={resultsBackgroundImage}
|
backgroundImage={resultsBackgroundImage}
|
||||||
/>
|
/>
|
||||||
<StepperFooter />
|
<StepperFooter />
|
||||||
|
<Modal open={showModal} onClose={closeModal}>
|
||||||
|
<Box sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
color: 'white',
|
||||||
|
p: 4,
|
||||||
|
}}>
|
||||||
|
<Typography variant="h5" component="h2">Questionnaire Complete</Typography>
|
||||||
|
<Typography sx={{ mt: 2 }}>You have completed the questionnaire for your chosen habitat. If you have purchased a physical copy of the planting guide, it should arrive at your chosen delivery address in a number of business days.</Typography>
|
||||||
|
<Typography sx={{ mt: 2 }}>You may now dismiss this popup to view a table of results. You are able to access this page at any time in future by supplying the activation key used at the start of this questionnaire.</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<Box sx={{ flex: '1 1 auto' }} />
|
||||||
|
<Button onClick={closeModal}>Close</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,45 +29,53 @@ export default function SummaryContent() {
|
||||||
!Object.keys(locationDetails).length && getLocationDetails();
|
!Object.keys(locationDetails).length && getLocationDetails();
|
||||||
});
|
});
|
||||||
|
|
||||||
function createData(name, value) {
|
|
||||||
return { name, value };
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationData = [
|
const locationData = [
|
||||||
createData(
|
{
|
||||||
"Geographical Coordinates (latitude, longitude)",
|
name: "Geographical Coordinates (latitude, longitude)",
|
||||||
`(${filters.coordinates.lat}, ${filters.coordinates.lng})`
|
value: `(${filters.coordinates.lat}, ${filters.coordinates.lng})`,
|
||||||
),
|
},
|
||||||
createData("Ecological Region", locationDetails.ecological_region || ""),
|
{
|
||||||
createData(
|
name: "Ecological Region",
|
||||||
"Ecological District",
|
value: locationDetails.ecological_region || "",
|
||||||
locationDetails.ecological_district || ""
|
},
|
||||||
),
|
{
|
||||||
createData("Property Name", locationDetails.full_address || ""),
|
name: "Ecological District",
|
||||||
|
value: locationDetails.ecological_district || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Property Name",
|
||||||
|
value: locationDetails.full_address || "",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const soilData = [
|
const soilData = [
|
||||||
createData(
|
{
|
||||||
"Soil Order",
|
name: "Soil Order",
|
||||||
`${locationDetails.soil_name} (${locationDetails.soil_code})` || ""
|
value: `${locationDetails.soil_name} (${locationDetails.soil_code})` || "",
|
||||||
),
|
},
|
||||||
createData("Soil Variant", filters.soilVariant),
|
{
|
||||||
|
name: "Soil Variant",
|
||||||
|
value: filters.soilVariant,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const siteData = [
|
const siteData = [
|
||||||
createData("Habitat", filters.habitat.name ?? ""),
|
{
|
||||||
createData(
|
name: "Habitat",
|
||||||
"Zone Name",
|
value: filters.habitat.name ?? "",
|
||||||
(filters.zone && filters.zone.name) ?? ""
|
},
|
||||||
),
|
{
|
||||||
createData(
|
name: "Zone Name",
|
||||||
"Zone Variant",
|
value: (filters.zone && filters.zone.name) ?? "",
|
||||||
(filters.zone && filters.zone.variant) ?? ""
|
},
|
||||||
),
|
{
|
||||||
createData(
|
name: "Zone Variant",
|
||||||
"Zone Refined Variant",
|
value: (filters.zone && filters.zone.variant) ?? "",
|
||||||
(filters.zone && filters.zone.refined_variant) ?? ""
|
},
|
||||||
),
|
{
|
||||||
|
name: "Zone Refined Variant",
|
||||||
|
value: (filters.zone && filters.zone.refined_variant) ?? "",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const regionInformation = () => {
|
const regionInformation = () => {
|
||||||
|
|
|
@ -9,17 +9,14 @@ import { FilterProvider } from "./components/providers/FilterProvider";
|
||||||
// Styles
|
// Styles
|
||||||
import "./assets/styles/main.scss";
|
import "./assets/styles/main.scss";
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import ApplyPage from "./pages/ApplyPage";
|
import { ActivationProvider } from "./components/providers/ActivationProvider";
|
||||||
|
import { ErrorProvider } from "./components/providers/ErrorProvider";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <MainPage />,
|
element: <MainPage />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/apply",
|
|
||||||
element: <ApplyPage />,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
|
@ -34,11 +31,15 @@ const darkTheme = createTheme({
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
<ErrorProvider>
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
|
<ActivationProvider>
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</ActivationProvider>
|
||||||
</FilterProvider>
|
</FilterProvider>
|
||||||
|
</ErrorProvider>
|
||||||
</div>
|
</div>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { Container } from "reactstrap";
|
|
||||||
import Header from "../components/Header";
|
|
||||||
import ActivationStep from "../components/steps/activation/ActivationStep";
|
|
||||||
import AddressStep from "../components/steps/address/AddressStep";
|
|
||||||
import SoilStep from "../components/steps/soilvariant/SoilStep";
|
|
||||||
import HabitatStep from "../components/steps/habitat/HabitatStep";
|
|
||||||
import ZoneStep from "../components/steps/zone/ZoneStep";
|
|
||||||
import SummaryStep from "../components/steps/summary/SummaryStep";
|
|
||||||
import CompleteStep from "../components/steps/complete/CompleteStep";
|
|
||||||
import { StepperWizard } from "../components/providers/StepperProvider";
|
|
||||||
import { useFilter } from "../components/providers/FilterProvider";
|
|
||||||
import { Box, Typography, Modal, Button } from '@mui/material';
|
|
||||||
|
|
||||||
|
|
||||||
const ApplyPage = () => {
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const { submit } = useFilter();
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
try {
|
|
||||||
await submit();
|
|
||||||
} catch (e) {
|
|
||||||
setError("There was a problem sending data to the API. Please try again.");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Container fluid className="main-container p-0">
|
|
||||||
<Header />
|
|
||||||
<StepperWizard>
|
|
||||||
<ActivationStep label="Activate" tooltip="Enter your activation key" />
|
|
||||||
<AddressStep label="Enter address" tooltip="Enter your address" />
|
|
||||||
<SoilStep label="Choose soil" tooltip="Describe the moisture content of your soil" />
|
|
||||||
<HabitatStep label="Choose habitat" tooltip="Specify type of landscape to be planted" />
|
|
||||||
<ZoneStep label="Select zone" tooltip="Specify geographical detail" />
|
|
||||||
<SummaryStep label="Summary" tooltip="Check your inputs" onSubmit={onSubmit} />
|
|
||||||
<CompleteStep label="Complete" tooltip="Complete your application" />
|
|
||||||
</StepperWizard>
|
|
||||||
</Container>
|
|
||||||
<Modal open={error.length > 0} onClose={() => setError("")}>
|
|
||||||
<Box sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
width: 400,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
border: '2px solid #000',
|
|
||||||
boxShadow: 24,
|
|
||||||
color: 'white',
|
|
||||||
p: 4,
|
|
||||||
}}>
|
|
||||||
<Typography variant="h4" component="h2">Error</Typography>
|
|
||||||
<Typography sx={{ mt: 2 }}>{error}</Typography>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
|
||||||
<Box sx={{ flex: '1 1 auto' }} />
|
|
||||||
<Button onClick={() => setError("")}>Close</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
</>);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApplyPage;
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { Container } from "reactstrap";
|
import { Container } from "reactstrap";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
|
import ActivationStep from "../components/steps/activation/ActivationStep";
|
||||||
import LocationStep from "../components/steps/location/LocationStep";
|
import LocationStep from "../components/steps/location/LocationStep";
|
||||||
import SoilStep from "../components/steps/soilvariant/SoilStep";
|
import SoilStep from "../components/steps/soilvariant/SoilStep";
|
||||||
import HabitatStep from "../components/steps/habitat/HabitatStep";
|
import HabitatStep from "../components/steps/habitat/HabitatStep";
|
||||||
|
@ -7,20 +9,52 @@ import ZoneStep from "../components/steps/zone/ZoneStep";
|
||||||
import SummaryStep from "../components/steps/summary/SummaryStep";
|
import SummaryStep from "../components/steps/summary/SummaryStep";
|
||||||
import ResultsStep from "../components/steps/results/ResultsStep";
|
import ResultsStep from "../components/steps/results/ResultsStep";
|
||||||
import { StepperWizard } from "../components/providers/StepperProvider";
|
import { StepperWizard } from "../components/providers/StepperProvider";
|
||||||
|
import { useFilter } from "../components/providers/FilterProvider";
|
||||||
|
import { Box, Typography, Modal, Button } from '@mui/material';
|
||||||
|
import { useActivator } from '../components/providers/ActivationProvider';
|
||||||
|
import { useError } from '../components/providers/ErrorProvider';
|
||||||
|
|
||||||
const MainPage = () => {
|
const MainPage = () => {
|
||||||
|
const { isPhysicalKey } = useActivator();
|
||||||
|
const { submit } = useFilter();
|
||||||
|
const { error, resetError } = useError();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Container fluid className="main-container p-0">
|
<Container fluid className="main-container p-0">
|
||||||
<Header />
|
<Header />
|
||||||
<StepperWizard>
|
<StepperWizard>
|
||||||
<LocationStep label="Select location" tooltip="Click on a location on the map" />
|
<ActivationStep label="Activate" tooltip="Enter your activation key" />
|
||||||
|
<LocationStep label="Select location" tooltip="Click on a location on the map" defaultIsSearch={isPhysicalKey} />
|
||||||
<SoilStep label="Choose soil" tooltip="Describe the moisture content of your soil" />
|
<SoilStep label="Choose soil" tooltip="Describe the moisture content of your soil" />
|
||||||
<HabitatStep label="Choose habitat" tooltip="Specify type of landscape to be planted" />
|
<HabitatStep label="Choose habitat" tooltip="Specify type of landscape to be planted" />
|
||||||
<ZoneStep label="Select zone" tooltip="Specify geographical detail" />
|
<ZoneStep label="Select zone" tooltip="Specify geographical detail" />
|
||||||
<SummaryStep label="Summary" tooltip="Check your inputs" />
|
<SummaryStep label="Summary" tooltip="Check your inputs" onSubmit={submit} />
|
||||||
<ResultsStep label="Results" tooltip="List of plant species and user guide" />
|
<ResultsStep label="Results" tooltip="List of plant species and user guide" />
|
||||||
</StepperWizard>
|
</StepperWizard>
|
||||||
</Container>);
|
</Container>
|
||||||
|
<Modal open={error.length > 0} onClose={resetError}>
|
||||||
|
<Box sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
color: 'white',
|
||||||
|
p: 4,
|
||||||
|
}}>
|
||||||
|
<Typography variant="h4" component="h2">Error</Typography>
|
||||||
|
<Typography sx={{ mt: 2 }}>{error}</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<Box sx={{ flex: '1 1 auto' }} />
|
||||||
|
<Button onClick={resetError}>Close</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MainPage;
|
export default MainPage;
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
import API from "./Repository";
|
import API from "./Repository";
|
||||||
|
|
||||||
const LocationRepsostory = {
|
const LocationRepsostory = {
|
||||||
getSoilDetails(filters) {
|
getSoilDetails(params) {
|
||||||
return API.get(`/soil/`, { params: filters });
|
return API.get(`/soil/`, {params});
|
||||||
},
|
},
|
||||||
|
|
||||||
getEcologicalDistrictDetails(filters) {
|
getEcologicalDistrictDetails(params) {
|
||||||
return API.get(`/ecologicaldistrict/`, { params: filters });
|
return API.get(`/ecologicaldistrict/`, {params});
|
||||||
},
|
},
|
||||||
|
|
||||||
getRegionDetails(filters) {
|
getRegionDetails(params) {
|
||||||
return API.get(`/region/`, { params: filters });
|
return API.get(`/region/`, {params});
|
||||||
},
|
},
|
||||||
|
|
||||||
getPropertyDetails(filters) {
|
getPropertyDetails(params) {
|
||||||
return API.get(`/address/`, { params: filters });
|
return API.get(`/address/`, {params});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getLocationData(filters) {
|
async getLocationData(filters) {
|
||||||
|
const params = { lat: filters.coordinates.lat, lng: filters.coordinates.lng };
|
||||||
const [
|
const [
|
||||||
soilDetails,
|
soilDetails,
|
||||||
ecologicalDistrictDetails,
|
ecologicalDistrictDetails,
|
||||||
propertyDetails,
|
propertyDetails,
|
||||||
regionDetails,
|
regionDetails,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getSoilDetails(filters),
|
this.getSoilDetails(params),
|
||||||
this.getEcologicalDistrictDetails(filters),
|
this.getEcologicalDistrictDetails(params),
|
||||||
this.getPropertyDetails(filters),
|
this.getPropertyDetails(params),
|
||||||
this.getRegionDetails(filters),
|
this.getRegionDetails(params),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let locationData = {};
|
let locationData = {};
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import Repository from "./Repository";
|
import Repository from "./Repository";
|
||||||
|
|
||||||
const PlantRepository = {
|
const PlantRepository = {
|
||||||
getFilteredPlants(filters) {
|
getFilteredPlants(key) {
|
||||||
return Repository.get(`/plants/`, { params: filters });
|
return Repository.get(`/plants/`, { params: {key} });
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlantsCSV(filters) {
|
getPlantsCSV(key) {
|
||||||
return Repository.get("/download/csv/", { params: filters });
|
return Repository.get("/download/csv/", { params: {key} });
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlantsPDF(filters) {
|
getPlantsPDF(key) {
|
||||||
return Repository.get("/download/pdf/", {
|
return Repository.get("/download/pdf/", {
|
||||||
params: filters,
|
params: {key},
|
||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue