import random import string from functools import cached_property from pathlib import Path from django.db.models.functions import Lower from django.contrib.gis.db import models from django.contrib.gis.db.models.functions import Distance from django.contrib.postgres.indexes import OpClass from django.utils.text import slugify 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})" class ChristchurchRegion(models.Model): objectid = models.IntegerField() name = models.CharField(max_length=25) geom = models.MultiPolygonField(srid=2193) class ToleranceLevel(models.Model): level = models.CharField(max_length=1) def __str__(self): return self.level class Habitat(models.Model): name = models.CharField(max_length=50) def __str__(self): return self.name class HabitatImage(models.Model): habitat = models.ForeignKey( Habitat, related_name='images', on_delete=models.CASCADE) 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) habitat = models.ForeignKey( Habitat, blank=True, null=True, on_delete=models.CASCADE, related_name='zones') redirect_habitat = models.ForeignKey( Habitat, blank=True, null=True, on_delete=models.CASCADE, related_name='zone_redirects') related_svg_segment = models.CharField(null=True, blank=True, max_length=20) ignore_soil_order_filter = models.BooleanField(default=False) ignore_location_filter = models.BooleanField(default=False) 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) 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}" class Meta: ordering = ['name', 'variant', 'refined_variant', 'id'] class Plant(models.Model): name = models.CharField(unique=True, max_length=50) commonname = models.CharField(null=True, blank=True, max_length=200) maxheight = models.CharField(max_length=20) 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) @property def forest_position(self): return self.zones.all().filter(name="BUSH PROFILE POSITION", variant__in={"Core", "Border", "Skin", "Epiphytes"}).values('variant') @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 '' forest_position_values_str = ', '.join([position['variant'][0] for position in self.forest_position]) forest_position_str = f' / {forest_position_values_str}' if len(self.forest_position) != 0 else '' return f"{growth_form_str}{self.maxheight} / {self.spacing}{forest_position_str}" @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 "" def __str__(self): return self.name class Meta: ordering = ['stage', 'name', 'commonname', 'synonym'] 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 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): 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) key_set = models.ForeignKey(ActivationKeySet, on_delete=models.PROTECT, null=True) activations = models.SmallIntegerField(default=0) remaining_activations = models.SmallIntegerField(default=1) creation_date = models.DateTimeField(auto_now_add=True) customer = models.ForeignKey(Customer, on_delete=models.PROTECT, null=True) def __str__(self): return self.key class Questionnaire(models.Model): location = models.PointField() soil_variant = models.ForeignKey(SoilVariant, on_delete=models.CASCADE) zone = models.ForeignKey(Zone, on_delete=models.CASCADE) key = models.ForeignKey(ActivationKey, on_delete=models.PROTECT, null=True) creation_date = models.DateTimeField(auto_now_add=True) @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}") # 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() 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)