Refactor calculation of route grade
[climbing.kerkeslager.com] / src / climbing / models.py
1 from django.contrib.auth.models import User
2 from django.db import models
3
4 from core import utils
5
6 class Area(models.Model):
7     name = models.CharField(max_length=64)
8     notes = models.TextField(blank=True, null=True)
9
10     def __str__(self):
11         return self.name
12
13     @property
14     def sub_areas(self):
15         return utils.merge(
16             self.crags.order_by('name'),
17             self.clusters.order_by('name'),
18             'name',
19         )
20
21 class Crag(models.Model):
22     area = models.ForeignKey(
23         Area,
24         on_delete=models.CASCADE,
25         related_name='crags',
26     )
27     name = models.CharField(max_length=64)
28     notes = models.TextField(blank=True, null=True)
29
30     def __str__(self):
31         return self.name
32
33 class Route(models.Model):
34     area = models.ForeignKey(
35         Crag,
36         on_delete=models.CASCADE,
37         related_name='routes',
38     )
39     name = models.CharField(max_length=64)
40     mountain_project = models.URLField(blank=True, null=True)
41     notes = models.TextField(blank=True, null=True)
42
43     def __str__(self):
44         pitch_count = self.pitches.count()
45
46         if pitch_count == 0:
47             return self.name
48
49         if pitch_count == 1:
50             return '{} {}'.format(self.name, self.difficulty)
51
52         return '{} {} ({} pitches)'.format(
53             self.name,
54             self.difficulty,
55             pitch_count,
56         )
57
58     @property
59     def difficulty(self):
60         diff = None
61         diff_index = -1
62
63         for pitch in self.pitches.all():
64             p_diff_index = _route_difficulty_index(pitch.difficulty)
65
66             if p_diff_index > diff_index:
67                 diff_index = p_diff_index
68                 diff = pitch.difficulty
69
70         return diff
71
72 ROUTE_DIFFICULTY_CHOICES = (
73     ('5.0', '5.0'),
74     ('5.1', '5.1'),
75     ('5.2', '5.2'),
76     ('5.3', '5.3'),
77     ('5.4', '5.4'),
78     ('5.5', '5.5'),
79     ('5.6', '5.6'),
80     ('5.6+', '5.6+'),
81     ('5.7', '5.7'),
82     ('5.7+', '5.7+'),
83     ('5.8', '5.8'),
84     ('5.8+', '5.8+'),
85     ('5.9-', '5.9-'),
86     ('5.9', '5.9'),
87     ('5.9+', '5.9+'),
88     ('5.10a', '5.10a'),
89     ('5.10b', '5.10b'),
90     ('5.10c', '5.10c'),
91     ('5.10d', '5.10d'),
92     ('5.11a', '5.11a'),
93     ('5.11b', '5.11b'),
94     ('5.11c', '5.11c'),
95     ('5.11d', '5.11d'),
96     ('5.12a', '5.12a'),
97     ('5.12b', '5.12b'),
98     ('5.12c', '5.12c'),
99     ('5.12d', '5.12d'),
100     ('5.13a', '5.13a'),
101     ('5.13b', '5.13b'),
102     ('5.13c', '5.13c'),
103     ('5.13d', '5.13d'),
104     ('5.14a', '5.14a'),
105     ('5.14b', '5.14b'),
106     ('5.14c', '5.14c'),
107     ('5.14d', '5.14d'),
108     ('5.15a', '5.15a'),
109     ('5.15b', '5.15b'),
110     ('5.15c', '5.15c'),
111     ('5.15d', '5.15d'),
112 )
113
114 def _route_difficulty_index(difficulty):
115     for i, d in enumerate(ROUTE_DIFFICULTY_CHOICES):
116         if difficulty == d[0]:
117             return i
118     return -1
119
120 SAFETY_CHOICES = (
121     ('G', 'G'),
122     ('PG', 'PG'),
123     ('PG13', 'PG13'),
124     ('R', 'R'),
125     ('X', 'X'),
126 )
127
128 class Pitch(models.Model):
129     route = models.ForeignKey(
130         Route,
131         on_delete=models.CASCADE,
132         related_name='pitches',
133     )
134     name = models.CharField(max_length=64, blank=True, null=True)
135     difficulty = models.CharField(max_length=5, choices=ROUTE_DIFFICULTY_CHOICES)
136     safety = models.CharField(max_length=4, choices=SAFETY_CHOICES)
137     notes = models.TextField(blank=True, null=True)
138
139     class Meta:
140         verbose_name_plural = 'pitches'
141
142     def __str__(self):
143         if self.name:
144             return '{} ({})'.format(self.name, self.difficulty)
145         return 'Pitch ({})'.format(self.difficulty)
146
147 class Cluster(models.Model):
148     area = models.ForeignKey(
149         Area,
150         on_delete=models.CASCADE,
151         related_name='clusters',
152     )
153     name = models.CharField(max_length=64)
154     notes = models.TextField(blank=True, null=True)
155
156     def __str__(self):
157         return self.name
158
159 class Boulder(models.Model):
160     cluster = models.ForeignKey(
161         Cluster,
162         on_delete=models.CASCADE,
163         related_name='boulders',
164     )
165     name = models.CharField(max_length=64)
166     notes = models.TextField(blank=True, null=True)
167
168     def __str__(self):
169         return self.name
170
171 BOULDER_DIFFICULTY_CHOICES = (
172     ('V0', 'V0'),
173     ('V1', 'V1'),
174     ('V2', 'V2'),
175     ('V3', 'V3'),
176     ('V4', 'V4'),
177     ('V5', 'V5'),
178     ('V6', 'V6'),
179     ('V7', 'V7'),
180     ('V8', 'V8'),
181     ('V9', 'V9'),
182     ('V10', 'V10'),
183     ('V11', 'V11'),
184     ('V12', 'V12'),
185     ('V13', 'V13'),
186     ('V14', 'V14'),
187     ('V15', 'V15'),
188     ('V16', 'V16'),
189     ('V17', 'V17'),
190 )
191
192 class Problem(models.Model):
193     boulder = models.ForeignKey(
194         Boulder,
195         on_delete=models.CASCADE,
196         related_name='problems',
197     )
198     name = models.CharField(max_length=64)
199     difficulty = models.CharField(max_length=3, choices=BOULDER_DIFFICULTY_CHOICES)
200     safety = models.CharField(max_length=4, choices=SAFETY_CHOICES)
201     mountain_project = models.URLField(blank=True, null=True)
202     notes = models.TextField(blank=True, null=True)
203
204     def __str__(self):
205         return '{} ({})'.format(self.name, self.difficulty)
206
207 class RouteTodo(models.Model):
208     user = models.ForeignKey(User, on_delete=models.CASCADE)
209     route = models.ForeignKey(Route, on_delete=models.CASCADE)
210
211 class ProblemTodo(models.Model):
212     user = models.ForeignKey(User, on_delete=models.CASCADE)
213     problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
214
215 class RouteTick(models.Model):
216     user = models.ForeignKey(User, on_delete=models.CASCADE)
217     route = models.ForeignKey(Route, on_delete=models.CASCADE)
218     timestamp = models.DateTimeField()
219     notes = models.TextField(blank=True, null=True)
220
221 class ProblemTick(models.Model):
222     user = models.ForeignKey(User, on_delete=models.CASCADE)
223     problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
224     timestamp = models.DateTimeField()
225     notes = models.TextField(blank=True, null=True)