2
0
mirror of https://expo.survex.com/repositories/troggle/.git synced 2024-12-24 01:12:27 +00:00
troggle/core/models/troggle.py

196 lines
7.2 KiB
Python

from decimal import Decimal, getcontext
from urllib.parse import urljoin
getcontext().prec = 2 # use 2 significant figures for decimal calculations
from django.db import models
from django.urls import reverse
import settings
"""This file declares TroggleModel which inherits from django.db.models.Model
All TroggleModel and models.Model subclasses inherit persistence in the django relational database. This is known as
the django Object Relational Mapping (ORM).
There are more subclasses defined in models/caves.py models/survex.py etc.
"""
class TroggleModel(models.Model):
"""This class is for adding fields and methods which all of our models will have."""
new_since_parsing = models.BooleanField(default=False, editable=False)
non_public = models.BooleanField(default=False)
def object_name(self):
return self._meta.object_name
def get_admin_url(self):
# we do not use URL_ROOT any more.
return urljoin("/admin/core/" + self.object_name().lower() + "/" + str(self.pk))
class Meta:
abstract = True
class DataIssue(TroggleModel):
"""When importing cave data any validation problems produce a message which is
recorded as a DataIssue. The django admin system automatically produces a page listing
these at /admin/core/dataissue/
This is a use of the NOTIFICATION pattern:
https://martinfowler.com/eaaDev/Notification.html
We have replaced all assertions in the code with messages and local fix-ups or skips:
https://martinfowler.com/articles/replaceThrowWithNotification.html
See also the use of stash_data_issue() & store_data_issues() in parsers/survex.py which defer writing to the database until the end of the import.
"""
date = models.DateTimeField(auto_now_add=True, blank=True)
parser = models.CharField(max_length=50, blank=True, null=True)
# message = models.CharField(max_length=800, blank=True, null=True) # causes extremely obscure error message
message = models.TextField(blank=True, null=True)
url = models.CharField(max_length=300, blank=True, null=True) # link to offending object
class Meta:
ordering = ["date"]
def __str__(self):
return f"{self.parser} - {self.message}"
#
# single Expedition, usually seen by year
#
class Expedition(TroggleModel):
year = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=100)
logbookfile = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return self.year
class Meta:
ordering = ("-year",)
get_latest_by = "year"
def get_absolute_url(self):
# we do not use URL_ROOT any more.
return reverse("expedition", args=[self.year])
class Person(TroggleModel):
"""single Person, can go on many years"""
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
fullname = models.CharField(max_length=200) # display name, but should not be used for lookups
nickname = models.CharField(max_length=200)
slug = models.SlugField(max_length=50, blank=True, null=True) # unique, enforced in code not in db
is_vfho = models.BooleanField(
help_text="VFHO is the Vereines für Höhlenkunde in Obersteier, a nearby Austrian caving club.",
default=False,
)
is_guest = models.BooleanField(default=False) # This is per-Person, not per-PersonExpedition
mug_shot = models.CharField(max_length=100, blank=True, null=True)
blurb = models.TextField(blank=True, null=True)
orderref = models.CharField(max_length=200) # for alphabetic
def get_absolute_url(self):
# we do not use URL_ROOT any more.
return reverse("person", kwargs={"slug": self.slug})
return reverse("person", kwargs={"first_name": self.first_name, "last_name": self.last_name})
class Meta:
verbose_name_plural = "People"
ordering = ("orderref",) # "Wookey" makes too complex for: ('last_name', 'first_name')
def __str__(self):
return self.slug
if self.last_name:
return f"{self.first_name} {self.last_name}"
return self.first_name
def notability(self):
"""This is actually recency: all recent cavers, weighted by number of expos"""
notability = Decimal(0)
max_expo_val = 0
max_expo_year = Expedition.objects.all().aggregate(models.Max("year"))
max_expo_val = int(max_expo_year["year__max"]) + 1
for personexpedition in self.personexpedition_set.all():
notability += Decimal(1) / (max_expo_val - int(personexpedition.expedition.year))
return notability
def bisnotable(self):
"""Boolean: is this person notable?"""
return self.notability() > Decimal(1) / Decimal(3)
def get_mugshot_url(self):
# insert code to extract src= url from the blrb text ? Or do it in the parser..
photo_url = f"/person/{self.slug}"
return photo_url
def surveyedleglength(self):
return sum([personexpedition.surveyedleglength() for personexpedition in self.personexpedition_set.all()])
def first(self):
return self.personexpedition_set.order_by("-expedition")[0]
def last(self):
return self.personexpedition_set.order_by("expedition")[0]
# moved from personexpedition
def name(self):
if self.nickname:
return f"{self.first_name} ({self.nickname}) {self.last_name}"
if self.last_name:
return f"{self.first_name} {self.last_name}"
return self.first_name
class PersonExpedition(TroggleModel):
"""Person's attendance to one Expo
CASCADE means that if an expedition or a person is deleted, the PersonExpedition
is deleted too
"""
expedition = models.ForeignKey(Expedition, on_delete=models.CASCADE)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
slugfield = models.SlugField(max_length=50, blank=True, null=True) # 2022 to be used in future
# is_guest = models.BooleanField(default=False) # This is per-Person, not per-PersonExpedition
class Meta:
ordering = ("-expedition", "-person")
# order_with_respect_to = 'expedition'
def __str__(self):
return f"{self.person}: ({self.expedition})"
def get_absolute_url(self):
# we do not use URL_ROOT any more.
return(f"/personexpedition/{self.person.slug}/{self.expedition.year}")
# why does this hang the system ?
return reverse(
"personexpedition",
kwargs={
"slug": self.slug,
"year": self.expedition.year,
},
)
# old style, no longer used
return reverse(
"personexpedition",
kwargs={
"first_name": self.person.first_name,
"last_name": self.person.last_name,
"year": self.expedition.year,
},
)
def surveyedleglength(self):
"""Survey length for this person on all survex trips on this expedition"""
survexblocks = [personrole.survexblock for personrole in self.survexpersonrole_set.all()]
return sum([survexblock.legslength for survexblock in set(survexblocks)])