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): return urljoin(settings.URL_ROOT, "/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) 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): return urljoin(settings.URL_ROOT, reverse("expedition", args=[self.year])) # class ExpeditionDay(TroggleModel): # """Exists only on Expedition now. Removed links from logbookentry, personlogentry, survex stuff etc. # """ # expedition = models.ForeignKey("Expedition",on_delete=models.CASCADE) # date = models.DateField() # class Meta: # ordering = ('date',) 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) nickname = models.CharField(max_length=200) is_vfho = models.BooleanField( help_text="VFHO is the Vereines für Höhlenkunde in Obersteier, a nearby Austrian caving club.", default=False, ) 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): return urljoin( settings.URL_ROOT, 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): 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(): if not personexpedition.is_guest: 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 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) class Meta: ordering = ("-expedition",) # order_with_respect_to = 'expedition' def __str__(self): return f"{self.person}: ({self.expedition})" def get_absolute_url(self): return urljoin( settings.URL_ROOT, 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)])