Add related names to users on Attempt and Todo
[tickle] / tickle / models.py
1 from django.contrib.auth.models import User
2 from django.core.exceptions import ValidationError
3 from django.db import models
4 from django.db.models import Q
5
6 class QQ:
7     def __xor__(self, other):
8         return (self & (~other)) | ((~self) & other)
9
10 Q.__bases__ += (QQ, )
11
12 BOULDER_DIFFICULTY_CHOICES = (
13     ('v0', 'v0'),
14     ('v1', 'v1'),
15     ('v2', 'v2'),
16     ('v3', 'v3'),
17     ('v4', 'v4'),
18     ('v5', 'v5'),
19     ('v6', 'v6'),
20     ('v7', 'v7'),
21     ('v8', 'v8'),
22     ('v9', 'v9'),
23     ('v10', 'v10'),
24     ('v11', 'v11'),
25     ('v12', 'v12'),
26     ('v13', 'v13'),
27     ('v14', 'v14'),
28     ('v15', 'v15'),
29     ('v16', 'v16'),
30 )
31
32 # TODO Provide a way of getting only Area objects which contain boulders/routes
33 class Area(models.Model):
34     parent = models.ForeignKey(
35         'self',
36         blank=True,
37         null=True,
38         on_delete=models.CASCADE,
39         related_name='children',
40     )
41     name = models.CharField(max_length=64)
42     notes = models.TextField(blank=True)
43
44     def __str__(self):
45         if self.parent is None:
46             return self.name
47
48         return '{} > {}'.format(self.parent, self.name)
49
50 class Boulder(models.Model):
51     area = models.ForeignKey(
52         'Area',
53         on_delete=models.PROTECT,
54         related_name='boulders',
55     )
56     name = models.CharField(max_length=64)
57     difficulty = models.CharField(
58         choices=BOULDER_DIFFICULTY_CHOICES,
59         max_length=8,
60     )
61     mountainproject = models.URLField(blank=True)
62     notes = models.TextField(blank=True)
63
64     def __str__(self):
65         return '{} ({})'.format(self.name, self.difficulty)
66
67 PITCH_DIFFICULTY_CHOICES = (
68     ('3', '3'),
69     ('4', '4'),
70     ('5.0', '5.0'),
71     ('5.1', '5.1'),
72     ('5.2', '5.2'),
73     ('5.3', '5.3'),
74     ('5.4', '5.4'),
75     ('5.5', '5.5'),
76     ('5.6', '5.6'),
77     ('5.7', '5.7'),
78     ('5.7+', '5.7+'),
79     ('5.8', '5.8'),
80     ('5.8+', '5.8+'),
81     ('5.9', '5.9'),
82     ('5.9+', '5.9+'),
83     ('5.10a', '5.10a'),
84     ('5.10b', '5.10b'),
85     ('5.10c', '5.10c'),
86     ('5.10d', '5.10d'),
87     ('5.11a', '5.11a'),
88     ('5.11b', '5.11b'),
89     ('5.11c', '5.11c'),
90     ('5.11d', '5.11d'),
91     ('5.12a', '5.12a'),
92     ('5.12b', '5.12b'),
93     ('5.12c', '5.12c'),
94     ('5.12d', '5.12d'),
95     ('5.13a', '5.13a'),
96     ('5.13b', '5.13b'),
97     ('5.13c', '5.13c'),
98     ('5.13d', '5.13d'),
99     ('5.14a', '5.14a'),
100     ('5.14b', '5.14b'),
101     ('5.14c', '5.14c'),
102     ('5.14d', '5.14d'),
103     ('5.15a', '5.15a'),
104     ('5.15b', '5.15b'),
105     ('5.15c', '5.15c'),
106     ('5.15d', '5.15d'),
107 )
108
109 class Pitch(models.Model):
110     order = models.PositiveSmallIntegerField()
111     route = models.ForeignKey(
112         'Route',
113         on_delete=models.CASCADE,
114         related_name='pitches',
115     )
116     difficulty = models.CharField(
117         choices=PITCH_DIFFICULTY_CHOICES,
118         max_length=8,
119     )
120     name = models.CharField(blank=True, max_length=32)
121     notes = models.TextField(blank=True)
122
123     class Meta:
124         ordering = ('order',)
125         verbose_name_plural = 'Pitches'
126
127     def __str__(self):
128         return 'P{} ({})'.format(self.order, self.difficulty)
129
130 PROTECTION_STYLE_CHOICES = (
131     ('sport', 'Sport'),
132     ('toprope', 'Top Rope'),
133     ('trad', 'Trad'),
134 )
135
136 class Route(models.Model):
137     area = models.ForeignKey(
138         'Area',
139         on_delete=models.PROTECT,
140         related_name='routes'
141     )
142     name = models.CharField(max_length=64)
143     protection_style = models.CharField(max_length=8, choices=PROTECTION_STYLE_CHOICES)
144     mountainproject = models.URLField(blank=True)
145     notes = models.TextField(blank=True)
146
147     # TODO Write test for this
148     @property
149     def difficulty(self):
150         return self.pitches.order_by('-difficulty').first().difficulty
151
152     def __str__(self):
153         return '{} ({})'.format(self.name, self.difficulty)
154
155 ATTEMPT_RESULT_CHOICES = (
156     ('send', 'Sent'),
157     ('fall', 'Fall'),
158     ('unknown', 'Unknown'),
159 )
160
161 PROTECTION_CHOICES = (
162     ('none', 'None'),
163     ('bolts', 'Bolts'),
164     ('gear', 'Gear'),
165     ('pad', 'Pad'),
166     ('tr', 'Top Rope'),
167 )
168
169 class Attempt(models.Model):
170     user = models.ForeignKey(
171         User,
172         on_delete=models.CASCADE,
173         related_name='attempts',
174     )
175     date = models.DateField()
176     notes = models.TextField(blank=True)
177     boulder = models.ForeignKey(
178         'Boulder',
179         blank=True,
180         null=True,
181         on_delete=models.PROTECT,
182         related_name='attempts',
183     )
184     route = models.ForeignKey(
185         'Route',
186         blank=True,
187         null=True,
188         on_delete=models.PROTECT,
189         related_name='attempts',
190     )
191     result = models.CharField(max_length=8, choices=ATTEMPT_RESULT_CHOICES)
192     prior_knowledge = models.BooleanField(default=True)
193     protection_used = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
194
195     class Meta:
196         constraints = (
197             models.CheckConstraint(
198                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
199                 name='attempt_boulder_xor_route',
200             ),
201         )
202
203         ordering = ('date',)
204
205     def __str__(self):
206         return '{} on {}'.format(
207             self.route,
208             self.date,
209         )
210
211 STYLE_CHOICES = (
212     ('onsight', 'On Sight'),
213     ('flash', 'Flash'),
214     ('project', 'Project'),
215     ('other', 'Other'),
216 )
217
218 class Todo(models.Model):
219     user = models.ForeignKey(
220         User,
221         on_delete=models.CASCADE,
222         related_name='todos',
223     )
224     notes = models.TextField(blank=True)
225     protection = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
226     boulder = models.ForeignKey(
227         'Boulder',
228         blank=True,
229         null=True,
230         on_delete=models.PROTECT,
231         related_name='todos',
232     )
233     route = models.ForeignKey(
234         'Route',
235         blank=True,
236         null=True,
237         on_delete=models.PROTECT,
238         related_name='todos',
239     )
240     style = models.CharField(max_length=8, choices=STYLE_CHOICES)
241
242     class Meta:
243         constraints = (
244             models.CheckConstraint(
245                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
246                 name='todo_boulder_xor_route',
247             ),
248         )
249
250         ordering = ('route__name',)
251
252     def __str__(self):
253         if self.boulder:
254             climb = self.boulder
255         elif self.route:
256             climb = self.route
257         else:
258             raise Exception()
259
260         return '{} {}'.format(self.style, climb)