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