import string import os import datetime import logging import re 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.db.models import Min, Max from django.conf import settings from django.core.urlresolvers import reverse from django.template import Context, loader import troggle.core.models_survex 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 urllib.parse.urljoin(settings.URL_ROOT, "/admin/core/" + self.object_name().lower() + "/" + str(self.pk)) class Meta: abstract = True class TroggleImageModel(models.Model): new_since_parsing = models.BooleanField(default=False, editable=False) def object_name(self): return self._meta.object_name def get_admin_url(self): return urllib.parse.urljoin(settings.URL_ROOT, "/admin/core/" + self.object_name().lower() + "/" + str(self.pk)) class Meta: abstract = True class DataIssue(TroggleModel): 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 urllib.parse.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: assert len(expeditiondays) == 1 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") 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) def get_absolute_url(self): return urllib.parse.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(Max('year')) max_expo_val = int(max_expo_year['year__max']) + 1 for personexpedition in self.personexpedition_set.all(): if not personexpedition.is_guest: print((personexpedition.expedition.year)) 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) person = models.ForeignKey(Person) 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 urllib.parse.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.personrole_set.all() ] return sum([survexblock.totalleglength 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=Max("expeditionday__date")) return res["day_max"] class LogbookEntry(TroggleModel): """Single parsed entry from Logbook """ LOGBOOK_ENTRY_TYPES = ( ("wiki", "Wiki style logbook"), ("html", "Html style logbook") ) date = models.DateField()#MJG wants to turn this into a datetime such that multiple Logbook entries on the same day can be ordered.ld() expeditionday = models.ForeignKey("ExpeditionDay", null=True)#MJG wants to KILL THIS (redundant information) expedition = models.ForeignKey(Expedition,blank=True,null=True) # yes this is double- title = models.CharField(max_length=settings.MAX_LOGBOOK_ENTRY_TITLE_LENGTH) cave_slug = models.SlugField(max_length=50) place = models.CharField(max_length=100,blank=True,null=True,help_text="Only use this if you haven't chosen a cave") text = models.TextField() slug = models.SlugField(max_length=50) filename = models.CharField(max_length=200,null=True) entry_type = models.CharField(default="wiki",null=True,choices=LOGBOOK_ENTRY_TYPES,max_length=50) class Meta: verbose_name_plural = "Logbook Entries" # several PersonTrips point in to this object ordering = ('-date',) def __getattribute__(self, item): if item == "cave": #Allow a logbookentries cave to be directly accessed despite not having a proper foreignkey return models_caves.CaveSlug.objects.get(slug = self.cave_slug).cave # parse error in python3.8 # https://stackoverflow.com/questions/41343263/provide-classcell-example-for-python-3-6-metaclass #https://github.com/django/django/pull/7653 #return TroggleModel.__getattribute__(item) #return super(LogbookEntry, self).__getattribute__(item) # works in py3.5, fails in 3.8 return TroggleModel.__getattribute__(self,item) # works in py 3.5 AND in 3.8 def __init__(self, *args, **kwargs): if "cave" in list(kwargs.keys()): if kwargs["cave"] is not None: kwargs["cave_slug"] = models_caves.CaveSlug.objects.get(cave=kwargs["cave"], primary=True).slug kwargs.pop("cave") # parse error in python3.8 return TroggleModel.__init__(self, *args, **kwargs) # seems OK in 3.5 & 3.8! failure later elsewhere with 3.8 #return TroggleModel().__init__(self, *args, **kwargs) # parses OK, fails at runtime in 3.8 #return super().__init__(self, *args, **kwargs) # fails in 3.8 #return super().__init__(*args, **kwargs) # works in py3.5 fails in 3.8 #return super(LogbookEntry, self).__init__(*args, **kwargs) # works in py3.5 #return TroggleModel.__init__(*args, **kwargs) # fails in py3.5, runtime fail in 3.8 def isLogbookEntry(self): # Function used in templates return True def get_absolute_url(self): return urllib.parse.urljoin(settings.URL_ROOT, reverse('logbookentry',kwargs={'date':self.date,'slug':self.slug})) def __str__(self): return "%s: (%s)" % (self.date, self.title) def get_next_by_id(self): LogbookEntry.objects.get(id=self.id+1) def get_previous_by_id(self): LogbookEntry.objects.get(id=self.id-1) def new_QM_number(self): """Returns """ if self.cave: nextQMnumber=self.cave.new_QM_number(self.date.year) else: return None return nextQMnumber def new_QM_found_link(self): """Produces a link to a new QM with the next number filled in and this LogbookEntry set as 'found by' """ return settings.URL_ROOT + r'/admin/core/qm/add/?' + r'found_by=' + str(self.pk) +'&number=' + str(self.new_QM_number()) def DayIndex(self): return list(self.expeditionday.logbookentry_set.all()).index(self) # # Single Person going on a trip, which may or may not be written up (accounts for different T/U for people in same logbook entry) # class PersonTrip(TroggleModel): personexpedition = models.ForeignKey("PersonExpedition",null=True) #expeditionday = models.ForeignKey("ExpeditionDay")#MJG wants to KILL THIS (redundant information) #date = models.DateField() #MJG wants to KILL THIS (redundant information) time_underground = models.FloatField(help_text="In decimal hours") logbook_entry = models.ForeignKey(LogbookEntry) is_logbook_entry_author = models.BooleanField(default=False) # sequencing by person (difficult to solve locally) #persontrip_next = models.ForeignKey('PersonTrip', related_name='pnext', blank=True,null=True)#MJG wants to KILL THIS (and use funstion persontrip_next_auto) #persontrip_prev = models.ForeignKey('PersonTrip', related_name='pprev', blank=True,null=True)#MJG wants to KILL THIS(and use funstion persontrip_prev_auto) def persontrip_next(self): futurePTs = PersonTrip.objects.filter(personexpedition = self.personexpedition, logbook_entry__date__gt = self.logbook_entry.date).order_by('logbook_entry__date').all() if len(futurePTs) > 0: return futurePTs[0] else: return None def persontrip_prev(self): pastPTs = PersonTrip.objects.filter(personexpedition = self.personexpedition, logbook_entry__date__lt = self.logbook_entry.date).order_by('-logbook_entry__date').all() if len(pastPTs) > 0: return pastPTs[0] else: return None def place(self): return self.logbook_entry.cave and self.logbook_entry.cave or self.logbook_entry.place def __str__(self): return "%s (%s)" % (self.personexpedition, self.logbook_entry.date)