Consolidate unit handling and use proxy classes.
[cookbook.git] / cookbook / models.py
1 from django.db import models
2 from django.forms import ValidationError
3 import markdown
4 from taggit.managers import TaggableManager
5
6
7 SI = 'SI (International System of Units)'
8 US = 'US (United States of America customary units)'
9
10 class Unit (models.Model):
11     "Kilograms, pounds, liters, gallons, etc."
12     TYPES = (
13         ('c', 'count'),
14         ('m', 'mass'),
15         ('v', 'volume'),
16         ('t', 'time'),
17         ('T', 'temperature'),
18         )
19     SYSTEMS = (
20         ('SI', SI),
21         ('US', US),
22         )
23     name = models.CharField(max_length=40)
24     abbrev = models.CharField('abbreviation', max_length=6)
25     type = models.CharField(max_length=1, choices=TYPES)
26     system = models.CharField(max_length=2, choices=SYSTEMS)
27     scale = models.DecimalField(
28         'SI/X-equivalent ratio (e.g. 453.6 for lb (g), 3.78 for gal (L))',
29         max_digits=30, decimal_places=15)
30     offset = models.DecimalField(
31         'X when SI-equivalent is zero (e.g. 32 for degrees Farenheit (C))',
32         max_digits=30, decimal_places=15)
33
34     def __unicode__(self):
35         return u'{0.abbrev}'.format(self)
36
37     def convert_from_si(self, value):
38         return value / self.scale + self.offset
39
40     def convert_to_si(self, value):
41         return (value - self.offset) * self.scale
42
43
44 class Amount (models.Model):
45     "1 kg, 2-3 lb., 0.5 (0.3-0.6) gal., etc."
46     value = models.DecimalField(
47         max_digits=10, decimal_places=4, null=True, blank=True)
48     min_value = models.DecimalField(
49         'minimum value', max_digits=10, decimal_places=4,
50         null=True, blank=True)
51     max_value = models.DecimalField(
52         'maximum value', max_digits=10, decimal_places=4,
53         null=True, blank=True)
54     unit = models.ForeignKey(Unit, null=True, blank=True)
55
56     class Meta:
57         abstract = True
58
59     def format_amount(self):
60         if self.unit is None:
61             for v in [self.value, self.min_value, self.max_value]:
62                 if v is not None:
63                     raise ValidationError(v)
64             return u'-'
65         if self.value is None:
66             if self.min_value is None:
67                 if self.max_value is not None:
68                     raise ValidationError('cannot only set max_value')
69                 fmt = u'- {0.unit}'
70             else:
71                 if self.max_value is None:
72                     raise ValidationError('cannot only set min_value')
73                 fmt = u'{0.min_value}-{0.max_value} {0.unit}'
74         else:
75             if self.min_value is None:
76                 if self.max_value is None:
77                     fmt = u'{0.value} {0.unit}'
78                 else:
79                     fmt = u'{0.value}-{0.max_value} {0.unit}'
80             else:
81                 if self.max_value is None:
82                     fmt = u'{0.min_value}-{0.value} {0.unit}'
83                 else:
84                     fmt = u'{0.value} ({0.min_value}-{0.max_value}) {0.unit}'
85         return fmt.format(self)
86
87     def validate_amount(self):
88         if self.value is None:
89             if self.min_value is None and self.max_value is not None:
90                 raise ValidationError('cannot only set max_value')
91             elif self.min_value is not None and self.max_value is None:
92                 raise ValidationError('cannot only set min_value')
93         if self.value is not None and self.unit is None:
94             raise ValidationError('values must have units')
95
96
97 class Directions (models.Model):
98     directions_markdown = models.TextField(
99         'directions', help_text='Markdown syntax', blank=True, null=True)
100     directions = models.TextField('directions as HTML', blank=True, null=True)
101
102     class Meta:
103         abstract = True
104
105     def save(self):
106         # https://code.djangoproject.com/wiki/UsingMarkup
107         if self.directions_markdown is None:
108             self.directions = None
109         else:
110             self.directions = markdown.markdown(self.directions_markdown)
111         super(Directions, self).save()
112
113
114 class Recipe (Amount, Directions):
115     name = models.CharField(max_length=200)
116     author = models.CharField(max_length=200, null=True, blank=True)
117     source = models.CharField(max_length=200, null=True, blank=True)
118     url = models.URLField('URL', null=True, blank=True)
119     tags = TaggableManager(blank=True)
120
121     class Meta:
122         ordering = ['name']
123
124     def __unicode__(self):
125         return u'{0.name}'.format(self)
126
127
128 class IngredientBlock (Directions):
129     name = models.CharField(max_length=200)
130     recipe = models.ForeignKey(Recipe)
131
132     class Meta:
133         ordering = ['recipe', 'name']
134
135     def __unicode__(self):
136         return u'{0.name}'.format(self)
137
138
139 class Ingredient (Amount):
140     "1 kg, 2 lb., 3.4 L, 0.5 gal., etc."
141     name = models.CharField(max_length=200)
142     note = models.CharField(max_length=200, null=True, blank=True)
143     block = models.ForeignKey(IngredientBlock)
144
145     def __unicode__(self):
146         fmt = '{i.name}'
147         args = {'i': self}
148         if self.unit:
149             fmt = '{amount} ' + fmt
150             args['amount'] = self.format_amount()
151         if self.note:
152             fmt += u', {i.note}'
153         return fmt.format(**args)