Add admin interface and fix some model issues
[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 class Boulder(models.Model):
13     name = models.CharField(max_length=64)
14     difficulty = models.ForeignKey(
15         'BoulderDifficulty',
16         null=True,
17         on_delete=models.PROTECT,
18         related_name='boulders',
19     )
20     mountainproject = models.URLField(blank=True)
21
22     def __str__(self):
23         return '{} ({})'.format(self.name, self.difficulty)
24
25 class BoulderDifficulty(models.Model):
26     order = models.PositiveSmallIntegerField()
27     name = models.CharField(max_length=8)
28
29     class Meta:
30         ordering = ('order',)
31
32     def __str__(self):
33         return self.name
34
35 class Pitch(models.Model):
36     order = models.PositiveSmallIntegerField()
37     route = models.ForeignKey(
38         'Route',
39         on_delete=models.CASCADE,
40         related_name='pitches',
41     )
42     difficulty = models.ForeignKey(
43         'RouteDifficulty',
44         on_delete=models.PROTECT,
45         related_name='pitches',
46     )
47
48     class Meta:
49         ordering = ('order',)
50
51     def __str__(self):
52         return 'P{} ({})'.format(self.order, self.difficulty)
53
54 PROTECTION_STYLE_CHOICES = (
55     ('sport', 'Sport'),
56     ('toprope', 'Top Rope'),
57     ('trad', 'Trad'),
58 )
59
60 class Route(models.Model):
61     name = models.CharField(max_length=64)
62     protection_style = models.CharField(max_length=8, choices=PROTECTION_STYLE_CHOICES)
63     mountainproject = models.URLField(blank=True)
64
65     # TODO Write test for this
66     @property
67     def difficulty(self):
68         return self.pitches.order_by('-difficulty__order').first().difficulty
69
70     def __str__(self):
71         return '{} ({})'.format(self.name, self.difficulty)
72
73 class RouteDifficulty(models.Model):
74     order = models.PositiveSmallIntegerField()
75     name = models.CharField(max_length=8)
76
77     def __str__(self):
78         return self.name
79
80 ATTEMPT_RESULT_CHOICES = (
81     ('send', 'Sent'),
82     ('fall', 'Fall'),
83 )
84
85 PROTECTION_CHOICES = (
86     ('none', 'None'),
87     ('bolts', 'Bolts'),
88     ('gear', 'Gear'),
89     ('pad', 'Pad'),
90     ('tr', 'Top Rope'),
91 )
92
93 class Attempt(models.Model):
94     user = models.ForeignKey(User, on_delete=models.CASCADE)
95     date = models.DateField()
96     notes = models.TextField()
97     boulder = models.ForeignKey('Boulder', null=True, on_delete=models.PROTECT, related_name='attempts')
98     route = models.ForeignKey('Route', null=True, on_delete=models.PROTECT, related_name='attempts')
99     result = models.CharField(max_length=8, choices=ATTEMPT_RESULT_CHOICES)
100     prior_knowledge = models.BooleanField(default=True)
101     protection_used = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
102
103     class Meta:
104         constraints = (
105             models.CheckConstraint(
106                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
107                 name='attempt_boulder_xor_route',
108             ),
109         )
110
111         ordering = ('date',)
112
113 STYLE_CHOICES = (
114     ('onsight', 'On Sight'),
115     ('flash', 'Flash'),
116     ('project', 'Project'),
117     ('other', 'Other'),
118 )
119
120 class Todo(models.Model):
121     user = models.ForeignKey(User, on_delete=models.CASCADE)
122     notes = models.TextField()
123     protection = models.CharField(max_length=8, choices=PROTECTION_CHOICES)
124     boulder = models.ForeignKey('Boulder', null=True, on_delete=models.PROTECT, related_name='todos')
125     route = models.ForeignKey('Route', null=True, on_delete=models.PROTECT, related_name='todos')
126     style = models.CharField(max_length=8, choices=STYLE_CHOICES)
127
128     class Meta:
129         constraints = (
130             models.CheckConstraint(
131                 check=(Q(boulder__isnull=True) ^ Q(route__isnull=True)),
132                 name='todo_boulder_xor_route',
133             ),
134         )
135
136         ordering = ('route__name',)
137
138     def __str__(self):
139         if self.boulder:
140             climb = self.boulder
141         elif self.route:
142             climb = self.route
143         else:
144             raise Exception()
145
146         return '{} {}'.format(self.style, climb)