summaryrefslogtreecommitdiffstats
path: root/core/models/troggle.py
blob: 62fe9c1a6cd6bb241e77f73de0d1c3b22b08af61 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
from django.contrib.auth.models import User

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 expo many years
    
    Note that the class "User" and the class "Group
    are standrd Django classes 
    definied in django.contrib.auth.models
    """

    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, blank=True)
    slug = models.SlugField(max_length=50, blank=True, null=True) # unique, enforced in code not in db
    # no delete cascade. We have users without Persons, and Persons without users
    user = models.OneToOneField(User, models.SET_NULL, blank=True, null=True) 

    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, blank=True)  # for alphabetic

    def get_absolute_url(self):
        # we do not use URL_ROOT any more.
        return reverse("person", kwargs={"slug": self.slug})
 
    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, db_index=True)
    person = models.ForeignKey(Person, on_delete=models.CASCADE, db_index=True)
    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.
        # This is crackers, the whole point of get_absolute_url is to use the automatic reverse resolution, see below
        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,
            },
        )

    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)])