Refactor difficulties, add pitch names
[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 class Boulder(models.Model):
33     name = models.CharField(max_length=64)
34     difficulty = models.CharField(
35         choices=BOULDER_DIFFICULTY_CHOICES,
36         max_length=8,
37     )
38     mountainproject = models.URLField(blank=True)
39
40     def __str__(self):
41         return '{} ({})'.format(self.name, self.difficulty)
42
43 PITCH_DIFFICULTY_CHOICES = (
44     ('3', '3'),
45     ('4', '4'),
46     ('5.0', '5.0'),
47     ('5.1', '5.1'),
48     ('5.2', '5.2'),
49     ('5.3', '5.3'),
50     ('5.4', '5.4'),
51     ('5.5', '5.5'),
52     ('5.6', '5.6'),
53     ('5.7', '5.7'),
54     ('5.7+', '5.7+'),
55     ('5.8', '5.8'),
56     ('5.8+', '5.8+'),
57     ('5.9', '5.9'),
58     ('5.9+', '5.9+'),
59     ('5.10a', '5.10a'),
60     ('5.10b', '5.10b'),
61     ('5.10c', '5.10c'),
62     ('5.10d', '5.10d'),
63     ('5.11a', '5.11a'),
64     ('5.11b', '5.11b'),
65     ('5.11c', '5.11c'),
66     ('5.11d', '5.11d'),
67     ('5.12a', '5.12a'),
68     ('5.12b', '5.12b'),
69     ('5.12c', '5.12c'),
70     ('5.12d', '5.12d'),
71     ('5.13a', '5.13a'),
72     ('5.13b', '5.13b'),
73     ('5.13c', '5.13c'),
74     ('5.13d', '5.13d'),
75     ('5.14a', '5.14a'),
76     ('5.14b', '5.14b'),
77     ('5.14c', '5.14c'),
78     ('5.14d', '5.14d'),
79     ('5.15a', '5.15a'),
80     ('5.15b', '5.15b'),
81     ('5.15c', '5.15c'),
82     ('5.15d', '5.15d'),
83 )
84
85 class Pitch(models.Model):
86     order = models.PositiveSmallIntegerField()
87     route = models.ForeignKey(
88         'Route',
89         on_delete=models.CASCADE,
90         related_name='pitches',
91     )
92     difficulty = models.CharField(
93         choices=PITCH_DIFFICULTY_CHOICES,
94         max_length=8,
95     )
96     name = models.CharField(blank=True, max_length=32)
97
98     class Meta:
99         ordering = ('order',)
100
101     def __str__(self):
102         return 'P{} ({})'.format(self.order, self.difficulty)
103
104 PROTECTION_STYLE_CHOICES = (
105     ('sport', 'Sport'),
106     ('toprope', 'Top Rope'),
107     ('trad', 'Trad'),
108 )
109
110 class Route(models.Model):
111     name = models.CharField(max_length=64)
112     protection_style = models.CharField(max_length=8, choices=PROTECTION_STYLE_CHOICES)
113     mountainproject = models.URLField(blank=True)
114
115     # TODO Write test for this
116     @property
117     def difficulty(self):
118         return self.pitches.order_by('-difficulty').first().difficulty
119
120     def __str__(self):
121         return '{} ({})'.format(self.name, self.difficulty)
122
123 ATTEMPT_RESULT_CHOICES = (
124     ('send', 'Sent'),
125     ('fall', 'Fall'),
126 )
127
128 PROTECTION_CHOICES = (
129     ('none', 'None'),
130     ('bolts', 'Bolts'),
131     ('gear', 'Gear'),
132     ('pad', 'Pad'),
133     ('tr', 'Top Rope'),
134 )
135
136 class Attempt(models.Model):
137     user = models.ForeignKey(User, on_delete=models.CASCADE)
138     date = models.DateField()
139     notes = models.TextField()
140     boulder = models.ForeignKey('Boulder', null=True, on_delete=models.PROTECT, related_name='attempts')
141     route = models.ForeignKey('Route', null=True, on_delete=models.PROTECT, related_name='attempts')
142     result = models.CharField(max_length=8, choices=ATTEMPT_RESULT_CHOICES)
143     prior_knowledge = models.BooleanField(default=True)
144     protection_used = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
145
146     class Meta:
147         constraints = (
148             models.CheckConstraint(
149                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
150                 name='attempt_boulder_xor_route',
151             ),
152         )
153
154         ordering = ('date',)
155
156 STYLE_CHOICES = (
157     ('onsight', 'On Sight'),
158     ('flash', 'Flash'),
159     ('project', 'Project'),
160     ('other', 'Other'),
161 )
162
163 class Todo(models.Model):
164     user = models.ForeignKey(User, on_delete=models.CASCADE)
165     notes = models.TextField()
166     protection = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
167     boulder = models.ForeignKey('Boulder', null=True, on_delete=models.PROTECT, related_name='todos')
168     route = models.ForeignKey('Route', null=True, on_delete=models.PROTECT, related_name='todos')
169     style = models.CharField(max_length=8, choices=STYLE_CHOICES)
170
171     class Meta:
172         constraints = (
173             models.CheckConstraint(
174                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
175                 name='todo_boulder_xor_route',
176             ),
177         )
178
179         ordering = ('route__name',)
180
181     def __str__(self):
182         if self.boulder:
183             climb = self.boulder
184         elif self.route:
185             climb = self.route
186         else:
187             raise Exception()
188
189         return '{} {}'.format(self.style, climb)