3b2deb8920b7c98c9e4cb2188580c52f49eb50f3
[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         verbose_name_plural = 'Pitches'
120
121     def __str__(self):
122         return 'P{} ({})'.format(self.order, self.difficulty)
123
124 PROTECTION_STYLE_CHOICES = (
125     ('sport', 'Sport'),
126     ('toprope', 'Top Rope'),
127     ('trad', 'Trad'),
128 )
129
130 class Route(models.Model):
131     area = models.ForeignKey('Area', on_delete=models.PROTECT)
132     name = models.CharField(max_length=64)
133     protection_style = models.CharField(max_length=8, choices=PROTECTION_STYLE_CHOICES)
134     mountainproject = models.URLField(blank=True)
135     notes = models.TextField(blank=True)
136
137     # TODO Write test for this
138     @property
139     def difficulty(self):
140         return self.pitches.order_by('-difficulty').first().difficulty
141
142     def __str__(self):
143         return '{} ({})'.format(self.name, self.difficulty)
144
145 ATTEMPT_RESULT_CHOICES = (
146     ('send', 'Sent'),
147     ('fall', 'Fall'),
148 )
149
150 PROTECTION_CHOICES = (
151     ('none', 'None'),
152     ('bolts', 'Bolts'),
153     ('gear', 'Gear'),
154     ('pad', 'Pad'),
155     ('tr', 'Top Rope'),
156 )
157
158 class Attempt(models.Model):
159     user = models.ForeignKey(User, on_delete=models.CASCADE)
160     date = models.DateField()
161     notes = models.TextField(blank=True)
162     boulder = models.ForeignKey('Boulder', null=True, on_delete=models.PROTECT, related_name='attempts')
163     route = models.ForeignKey('Route', null=True, on_delete=models.PROTECT, related_name='attempts')
164     result = models.CharField(max_length=8, choices=ATTEMPT_RESULT_CHOICES)
165     prior_knowledge = models.BooleanField(default=True)
166     protection_used = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
167
168     class Meta:
169         constraints = (
170             models.CheckConstraint(
171                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
172                 name='attempt_boulder_xor_route',
173             ),
174         )
175
176         ordering = ('date',)
177
178 STYLE_CHOICES = (
179     ('onsight', 'On Sight'),
180     ('flash', 'Flash'),
181     ('project', 'Project'),
182     ('other', 'Other'),
183 )
184
185 class Todo(models.Model):
186     user = models.ForeignKey(User, on_delete=models.CASCADE)
187     notes = models.TextField(blank=True)
188     protection = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
189     boulder = models.ForeignKey('Boulder', null=True, on_delete=models.PROTECT, related_name='todos')
190     route = models.ForeignKey('Route', null=True, on_delete=models.PROTECT, related_name='todos')
191     style = models.CharField(max_length=8, choices=STYLE_CHOICES)
192
193     class Meta:
194         constraints = (
195             models.CheckConstraint(
196                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
197                 name='todo_boulder_xor_route',
198             ),
199         )
200
201         ordering = ('route__name',)
202
203     def __str__(self):
204         if self.boulder:
205             climb = self.boulder
206         elif self.route:
207             climb = self.route
208         else:
209             raise Exception()
210
211         return '{} {}'.format(self.style, climb)