2023-03-06 18:10:25 +13:00
|
|
|
import random
|
|
|
|
import string
|
|
|
|
|
2023-02-21 16:43:46 +13:00
|
|
|
from functools import cached_property
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from django.db.models.functions import Lower
|
2021-10-15 14:33:13 +13:00
|
|
|
from django.contrib.gis.db import models
|
2023-02-21 16:43:46 +13:00
|
|
|
from django.contrib.gis.db.models.functions import Distance
|
|
|
|
from django.contrib.postgres.indexes import OpClass
|
|
|
|
from django.utils.text import slugify
|
|
|
|
|
2021-10-15 14:33:13 +13:00
|
|
|
|
|
|
|
class SoilOrder(models.Model):
|
|
|
|
code = models.CharField(unique=True, max_length=1)
|
|
|
|
name = models.CharField(unique=True, max_length=50)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return f"{self.name} ({self.code})"
|
|
|
|
|
|
|
|
|
|
|
|
class SoilVariant(models.Model):
|
|
|
|
name = models.CharField(unique=True, max_length=10)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class SoilLayer(models.Model):
|
|
|
|
nzsc_class = models.CharField(max_length=4)
|
|
|
|
nzsc_group = models.CharField(max_length=2)
|
|
|
|
nzsc_order = models.ForeignKey(SoilOrder, on_delete=models.CASCADE)
|
|
|
|
shape_leng = models.FloatField()
|
|
|
|
geom = models.PolygonField(srid=2193)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.nzsc_class
|
|
|
|
|
|
|
|
|
|
|
|
class EcologicalRegion(models.Model):
|
|
|
|
name = models.CharField(unique=True, max_length=50)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class EcologicalDistrictLayer(models.Model):
|
|
|
|
ecological = models.CharField(max_length=5)
|
|
|
|
ecologic_1 = models.CharField(max_length=50)
|
|
|
|
ecologic_2 = models.ForeignKey(EcologicalRegion, on_delete=models.CASCADE)
|
|
|
|
shape_leng = models.FloatField()
|
|
|
|
shape_area = models.FloatField()
|
|
|
|
geom = models.PolygonField(srid=2193)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return f"{self.ecologic_1} ({self.ecologic_2})"
|
|
|
|
|
|
|
|
|
2021-12-07 10:39:30 +13:00
|
|
|
class ChristchurchRegion(models.Model):
|
2021-12-01 09:46:17 +13:00
|
|
|
objectid = models.IntegerField()
|
|
|
|
name = models.CharField(max_length=25)
|
|
|
|
geom = models.MultiPolygonField(srid=2193)
|
|
|
|
|
|
|
|
|
2021-10-15 14:33:13 +13:00
|
|
|
class ToleranceLevel(models.Model):
|
|
|
|
level = models.CharField(max_length=1)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.level
|
|
|
|
|
2021-11-05 14:22:07 +13:00
|
|
|
|
2021-11-04 11:18:04 +13:00
|
|
|
class Habitat(models.Model):
|
|
|
|
name = models.CharField(max_length=50)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class HabitatImage(models.Model):
|
|
|
|
habitat = models.ForeignKey(
|
2021-11-05 11:13:59 +13:00
|
|
|
Habitat, related_name='images', on_delete=models.CASCADE)
|
2021-11-04 11:18:04 +13:00
|
|
|
name = models.CharField(max_length=50)
|
|
|
|
image_filename = models.CharField(max_length=50, default='-')
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class Zone(models.Model):
|
|
|
|
name = models.CharField(max_length=50)
|
|
|
|
variant = models.CharField(null=True, blank=True, max_length=50)
|
|
|
|
refined_variant = models.CharField(null=True, blank=True, max_length=100)
|
2021-12-02 14:13:18 +13:00
|
|
|
habitat = models.ForeignKey(
|
|
|
|
Habitat, blank=True, null=True, on_delete=models.CASCADE, related_name='zones')
|
2021-11-04 11:18:04 +13:00
|
|
|
redirect_habitat = models.ForeignKey(
|
2021-11-11 13:26:04 +13:00
|
|
|
Habitat, blank=True, null=True, on_delete=models.CASCADE, related_name='zone_redirects')
|
2021-12-02 14:13:18 +13:00
|
|
|
related_svg_segment = models.CharField(null=True, blank=True, max_length=20)
|
2021-11-10 13:11:13 +13:00
|
|
|
ignore_soil_order_filter = models.BooleanField(default=False)
|
|
|
|
ignore_location_filter = models.BooleanField(default=False)
|
2021-12-13 11:56:14 +13:00
|
|
|
tooltipText = models.CharField(null=True, blank=True, max_length=500)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def tooltip_display_text(self):
|
|
|
|
return self.tooltipText if self.tooltipText is not None else str(self)
|
2021-11-04 11:18:04 +13:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
refined_variant_str = f", {self.refined_variant}" if self.refined_variant is not None else ""
|
|
|
|
variant_str = f"({self.variant}{refined_variant_str})" if self.variant is not None else ""
|
|
|
|
return f"{self.name} {variant_str}"
|
|
|
|
|
2021-11-05 16:18:47 +13:00
|
|
|
class Meta:
|
|
|
|
ordering = ['name', 'variant', 'refined_variant', 'id']
|
|
|
|
|
2021-11-04 11:18:04 +13:00
|
|
|
|
2021-11-04 14:08:31 +13:00
|
|
|
class Plant(models.Model):
|
|
|
|
name = models.CharField(unique=True, max_length=50)
|
|
|
|
commonname = models.CharField(null=True, blank=True, max_length=200)
|
2021-12-07 14:54:59 +13:00
|
|
|
maxheight = models.CharField(max_length=20)
|
2021-11-04 14:08:31 +13:00
|
|
|
spacing = models.FloatField()
|
|
|
|
synonym = models.CharField(null=True, blank=True, max_length=200)
|
|
|
|
water_tolerance = models.ForeignKey(
|
|
|
|
ToleranceLevel, related_name='water_tolerance', on_delete=models.CASCADE)
|
|
|
|
drought_tolerance = models.ForeignKey(
|
|
|
|
ToleranceLevel, related_name='drought_tolerance', on_delete=models.CASCADE)
|
|
|
|
frost_tolerance = models.ForeignKey(
|
|
|
|
ToleranceLevel, related_name='frost_tolerance', on_delete=models.CASCADE)
|
|
|
|
salinity_tolerance = models.ForeignKey(
|
|
|
|
ToleranceLevel, related_name='salinity_tolerance', on_delete=models.CASCADE)
|
|
|
|
purpose = models.TextField(null=True, blank=True)
|
|
|
|
stage = models.PositiveIntegerField()
|
|
|
|
growth_form = models.CharField(null=True, blank=True, max_length=50)
|
|
|
|
ecological_regions = models.ManyToManyField(EcologicalRegion)
|
|
|
|
soil_order = models.ManyToManyField(SoilOrder)
|
|
|
|
soil_variants = models.ManyToManyField(SoilVariant)
|
|
|
|
zones = models.ManyToManyField(Zone)
|
|
|
|
|
2021-11-10 10:33:34 +13:00
|
|
|
@property
|
|
|
|
def forest_position(self):
|
2021-12-07 14:54:59 +13:00
|
|
|
return self.zones.all().filter(name="BUSH PROFILE POSITION", variant__in={"Core", "Border", "Skin", "Epiphytes"}).values('variant')
|
2021-11-10 10:33:34 +13:00
|
|
|
|
2021-11-08 15:51:05 +13:00
|
|
|
@property
|
|
|
|
def display_name(self):
|
|
|
|
common_name_str = f' / {str(self.commonname)}' if self.commonname is not None else ''
|
|
|
|
synonym_str = f' / {str(self.synonym)}' if self.synonym is not None else ''
|
|
|
|
return f"{self.name}{common_name_str}{synonym_str}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def display_growth_form(self):
|
|
|
|
growth_form_str = f'{str(self.growth_form)} / ' if self.growth_form is not None else ''
|
2021-11-10 10:33:34 +13:00
|
|
|
forest_position_values_str = ', '.join([position['variant'][0] for position in self.forest_position])
|
2021-12-07 14:54:59 +13:00
|
|
|
forest_position_str = f' / {forest_position_values_str}' if len(self.forest_position) != 0 else ''
|
2021-11-10 10:33:34 +13:00
|
|
|
return f"{growth_form_str}{self.maxheight} / {self.spacing}{forest_position_str}"
|
2021-11-08 15:51:05 +13:00
|
|
|
|
|
|
|
@property
|
|
|
|
def moisture_preferences(self):
|
|
|
|
return ', '.join([variant.name for variant in self.soil_variants.all()])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def plant_tolerances(self):
|
|
|
|
return f"{self.water_tolerance.level} / {self.drought_tolerance.level} / {self.frost_tolerance.level} / {self.salinity_tolerance.level}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ecosystem_services(self):
|
|
|
|
return f"{str(self.purpose) or ''}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def carbon_sequestration(self):
|
|
|
|
return ""
|
|
|
|
|
2021-11-04 14:08:31 +13:00
|
|
|
def __str__(self):
|
2021-11-05 14:22:07 +13:00
|
|
|
return self.name
|
2021-11-17 10:32:54 +13:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
ordering = ['stage', 'name', 'commonname', 'synonym']
|
2023-02-21 16:43:46 +13:00
|
|
|
|
|
|
|
|
|
|
|
class Address(models.Model):
|
|
|
|
ogc_fid = models.AutoField(primary_key=True)
|
|
|
|
|
|
|
|
# fields for queries
|
|
|
|
address_number = models.IntegerField() # ignore the 'A' in '32A' for now
|
|
|
|
suburb_locality_ascii = models.CharField(max_length=255)
|
|
|
|
town_city_ascii = models.CharField(max_length=255)
|
|
|
|
full_road_name_ascii = models.CharField(max_length=255)
|
|
|
|
wkb_geometry = models.PointField()
|
|
|
|
|
|
|
|
# use only for displaying to user
|
|
|
|
full_address = models.CharField(max_length=500)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
db_table = 'linz\".\"nz_street_address'
|
|
|
|
indexes = [
|
|
|
|
models.Index(OpClass(Lower('full_road_name_ascii'), name='text_pattern_ops'), name='full_road_name_lower_idx'),
|
|
|
|
models.Index(OpClass(Lower('suburb_locality_ascii'), name='text_pattern_ops'), name='suburb_locality_lower_idx'),
|
|
|
|
models.Index(OpClass(Lower('town_city_ascii'), name='text_pattern_ops'), name='town_city_lower_idx'),
|
|
|
|
models.Index(fields=['address_number'], name='address_number_idx'),
|
|
|
|
]
|
|
|
|
|
|
|
|
# effectively read-only
|
|
|
|
# https://docs.djangoproject.com/en/3.2/ref/models/options/#managed
|
|
|
|
managed = False
|
|
|
|
|
|
|
|
|
2023-04-20 14:59:00 +12:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2023-03-06 18:10:25 +13:00
|
|
|
class ActivationKeySet(models.Model):
|
|
|
|
creation_date = models.DateTimeField(auto_now_add=True)
|
|
|
|
name = models.CharField(max_length=255, unique=True)
|
|
|
|
size = models.PositiveSmallIntegerField()
|
|
|
|
initial_activations = models.SmallIntegerField(default=1)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class ActivationKey(models.Model):
|
|
|
|
|
|
|
|
def key_default():
|
|
|
|
return "".join(
|
|
|
|
random.choice(
|
|
|
|
[string.ascii_uppercase, string.digits][random.randint(0, 1)]
|
|
|
|
) for _ in range(20)
|
|
|
|
)
|
|
|
|
|
|
|
|
key = models.CharField(max_length=20, unique=True, default=key_default)
|
2023-03-29 16:37:39 +13:00
|
|
|
key_set = models.ForeignKey(ActivationKeySet, on_delete=models.PROTECT, null=True)
|
2023-04-20 14:59:00 +12:00
|
|
|
activations = models.SmallIntegerField(default=0)
|
2023-03-06 18:10:25 +13:00
|
|
|
remaining_activations = models.SmallIntegerField(default=1)
|
|
|
|
creation_date = models.DateTimeField(auto_now_add=True)
|
2023-04-20 14:59:00 +12:00
|
|
|
customer = models.ForeignKey(Customer, on_delete=models.PROTECT, null=True)
|
2023-03-06 18:10:25 +13:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.key
|
|
|
|
|
|
|
|
|
2023-02-21 16:43:46 +13:00
|
|
|
class Questionnaire(models.Model):
|
|
|
|
location = models.PointField()
|
|
|
|
soil_variant = models.ForeignKey(SoilVariant, on_delete=models.CASCADE)
|
|
|
|
zone = models.ForeignKey(Zone, on_delete=models.CASCADE)
|
2023-03-06 18:10:25 +13:00
|
|
|
key = models.ForeignKey(ActivationKey, on_delete=models.PROTECT, null=True)
|
2023-04-20 14:59:00 +12:00
|
|
|
creation_date = models.DateTimeField(auto_now_add=True)
|
2023-02-21 16:43:46 +13:00
|
|
|
|
|
|
|
@property
|
|
|
|
def habitat(self):
|
|
|
|
return self.zone.habitat
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def address(self):
|
|
|
|
return \
|
|
|
|
Address.objects.filter(
|
|
|
|
wkb_geometry__dwithin=(self.location, 1e-3), # order of ~100 metres in degrees for NZ
|
|
|
|
).annotate(
|
|
|
|
distance=Distance('wkb_geometry', self.location),
|
|
|
|
).order_by(
|
|
|
|
'distance',
|
|
|
|
).first()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def plants(self):
|
|
|
|
return self.zone.plant_set.all() & self.soil_variant.plant_set.all()
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def slug(self):
|
|
|
|
return "_".join(
|
|
|
|
map(slugify, [
|
|
|
|
self.address.full_address if self.address else str(self.location),
|
|
|
|
self.zone.name,
|
|
|
|
self.zone.variant,
|
|
|
|
self.zone.refined_variant,
|
|
|
|
self.soil_variant.name,
|
|
|
|
str(self.zone.pk), # need this for uniqueness
|
|
|
|
str(self.soil_variant.pk), # # need this for uniqueness
|
|
|
|
])
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class Export(models.Model):
|
|
|
|
creation_date = models.DateTimeField()
|
|
|
|
completion_date = models.DateTimeField(null=True)
|
|
|
|
questionnaires = models.ManyToManyField(Questionnaire)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def complete(self):
|
|
|
|
return self.completion_date is not None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def completion(self):
|
|
|
|
if self.complete:
|
|
|
|
return 1.0
|
|
|
|
|
|
|
|
from .resource_generation_utils import storage
|
|
|
|
|
|
|
|
_, files = storage.listdir(f"export_{self.pk}")
|
|
|
|
|
2023-03-02 12:04:01 +13:00
|
|
|
# 0.25 multiplier as there are four files per questionnaire:
|
|
|
|
# csv, filters PDF, plants PDF, merged PDF
|
|
|
|
return 0.25 * len(files) / self.questionnaires.count()
|
2023-02-21 16:43:46 +13:00
|
|
|
|
|
|
|
def export(self):
|
|
|
|
from .tasks import generate_pdf
|
|
|
|
from .resource_generation_utils import storage
|
|
|
|
|
|
|
|
parent = Path(storage.path(f"export_{self.pk}"))
|
|
|
|
|
|
|
|
if not parent.exists():
|
|
|
|
parent.mkdir()
|
|
|
|
|
|
|
|
for q in self.questionnaires.all():
|
|
|
|
generate_pdf.delay(q.pk, self.pk)
|