right-tree/backend/right_tree/api/models.py
Matthew Northcott 1b800ff8ef Bug fixes
- report Export progress correctly
- move Questionnaire ecological district display to admin.py
2023-03-02 12:04:01 +13:00

273 lines
9.4 KiB
Python

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 Questionnaire(models.Model):
location = models.PointField()
soil_variant = models.ForeignKey(SoilVariant, on_delete=models.CASCADE)
zone = models.ForeignKey(Zone, on_delete=models.CASCADE)
@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)