import string import os import datetime import logging import re import resource from subprocess import call from urllib.parse import urljoin from decimal import Decimal, getcontext getcontext().prec=2 #use 2 significant figures for decimal calculations import settings from django.db import models from django.contrib import admin from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.conf import settings from django.urls import reverse from django.template import Context, loader import troggle.core.models_survex """This file declares TroggleModel which inherits from django.db.models.Model All TroggleModel subclasses inherit persistence in the django relational database. This is known as the django Object Relational Mapping (ORM). There are more subclasses define in models_caves.py models_survex.py etc. """ # This variable is a dictionary holding gloablly visible indexes and cache functions. # It is a Global Object, see https://python-patterns.guide/python/module-globals/ # troggle.models.TROG TROG = { 'pagecache' : { 'expedition' : {} } } def get_process_memory(): usage=resource.getrusage(resource.RUSAGE_SELF) return usage[2]/1024.0 # def get_related_by_wikilinks(wiki_text): # found=re.findall(settings.QM_PATTERN,wiki_text) # res=[] # for wikilink in found: # qmdict={'urlroot':settings.URL_ROOT,'cave':wikilink[2],'year':wikilink[1],'number':wikilink[3]} # try: # cave_slugs = models_caves.CaveSlug.objects.filter(cave__kataster_number = qmdict['cave']) # qm=QM.objects.get(found_by__cave_slug__in = cave_slugs, # found_by__date__year = qmdict['year'], # number = qmdict['number']) # res.append(qm) # except QM.DoesNotExist: # print(('fail on '+str(wikilink))) # return res try: logging.basicConfig(level=logging.DEBUG, filename=settings.LOGFILE, filemode='w') except: # Opening of file for writing is going to fail currently, so decide it doesn't matter for now pass #This class is for adding fields and methods which all of our models will have. class TroggleModel(models.Model): 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 prodiuces a page listing these at /admin/core/dataissue/ This is a use of the NOTIFICATION pattern: https://martinfowler.com/eaaDev/Notification.html And we need to use it to replace all assertions in the code too: https://martinfowler.com/articles/replaceThrowWithNotification.html """ date = models.DateTimeField(auto_now_add=True, blank=True) parser = models.CharField(max_length=50, blank=True, null=True) message = models.CharField(max_length=400, blank=True, null=True) class Meta: ordering = ['date'] def __str__(self): return "%s - %s" % (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) 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])) # construction function. should be moved out def get_expedition_day(self, date): expeditiondays = self.expeditionday_set.filter(date=date) if expeditiondays: if len(expeditiondays) == 1: return expeditiondays[0] else: message ='! - more than one datum in an expeditionday: {}'.format(date) DataIssue.objects.create(parser='expedition', message=message) return expeditiondays[0] res = ExpeditionDay(expedition=self, date=date) res.save() return res def day_min(self): res = self.expeditionday_set.all() return res and res[0] or None def day_max(self): res = self.expeditionday_set.all() return res and res[len(res) - 1] or None class ExpeditionDay(TroggleModel): expedition = models.ForeignKey("Expedition",on_delete=models.CASCADE) date = models.DateField() class Meta: ordering = ('date',) def GetPersonTrip(self, personexpedition): personexpeditions = self.persontrip_set.filter(expeditionday=self) return personexpeditions and personexpeditions[0] or None 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) 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) #href = models.CharField(max_length=200) orderref = models.CharField(max_length=200) # for alphabetic user = models.OneToOneField(User, null=True, blank=True,on_delete=models.CASCADE) 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 "%s %s" % (self.first_name, self.last_name) return self.first_name def notability(self): 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): 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] class PersonExpedition(TroggleModel): """Person's attendance to one Expo """ 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) is_guest = models.BooleanField(default=False) COMMITTEE_CHOICES = ( ('leader','Expo leader'), ('medical','Expo medical officer'), ('treasurer','Expo treasurer'), ('sponsorship','Expo sponsorship coordinator'), ('research','Expo research coordinator'), ) expo_committee_position = models.CharField(blank=True,null=True,choices=COMMITTEE_CHOICES,max_length=200) nickname = models.CharField(max_length=100,blank=True, null=True) def GetPersonroles(self): res = [ ] for personrole in self.personrole_set.order_by('survexblock'): if res and res[-1]['survexpath'] == personrole.survexblock.survexpath: res[-1]['roles'] += ", " + str(personrole.role) else: res.append({'date':personrole.survexblock.date, 'survexpath':personrole.survexblock.survexpath, 'roles':str(personrole.role)}) return res class Meta: ordering = ('-expedition',) #order_with_respect_to = 'expedition' def __str__(self): return "%s: (%s)" % (self.person, self.expedition) #why is the below a function in personexpedition, rather than in person? - AC 14 Feb 09 def name(self): if self.nickname: return "%s (%s) %s" % (self.person.first_name, self.nickname, self.person.last_name) if self.person.last_name: return "%s %s" % (self.person.first_name, self.person.last_name) return self.person.first_name 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): survexblocks = [personrole.survexblock for personrole in self.survexpersonrole_set.all() ] return sum([survexblock.legslength for survexblock in set(survexblocks)]) # would prefer to return actual person trips so we could link to first and last ones def day_min(self): res = self.persontrip_set.aggregate(day_min=Min("expeditionday__date")) return res["day_min"] def day_max(self): res = self.persontrip_set.all().aggregate(day_max=models.Max("expeditionday__date")) return res["day_max"]