Skip to content

Models

Dependencies

SingletonModel

Model representing a singleton model.

Attributes:

Source code in backend/api/models.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SingletonModel(models.Model):
    """
    Model representing a singleton model.

    Attributes:
    """

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        """
        Override save method to validate only one instance exists.
        """
        if not self.pk and self.__class__.objects.exists():
            raise ValidationError("There is already one instance of this model")
        return super(SingletonModel, self).save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        """
        Override delete method to block deletes.
        """
        raise ValidationError("You cannot delete this object")

save(*args, **kwargs)

Override save method to validate only one instance exists.

Source code in backend/api/models.py
23
24
25
26
27
28
29
def save(self, *args, **kwargs):
    """
    Override save method to validate only one instance exists.
    """
    if not self.pk and self.__class__.objects.exists():
        raise ValidationError("There is already one instance of this model")
    return super(SingletonModel, self).save(*args, **kwargs)

delete(*args, **kwargs)

Override delete method to block deletes.

Source code in backend/api/models.py
31
32
33
34
35
def delete(self, *args, **kwargs):
    """
    Override delete method to block deletes.
    """
    raise ValidationError("You cannot delete this object")

Store

Store

Model representing a Store.

Attributes:

Name Type Description
name CharField

The name of a store. Required. Unique.

Source code in backend/api/models.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Store(models.Model):
    """
    Model representing a Store.

    Attributes:
        name (CharField): The name of a store. Required. Unique.
    """

    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        """
        Returns:
            (String): The Store Object name.
        """
        return self.name

__str__()

Returns:

Type Description
String

The Store Object name.

Source code in backend/api/models.py
48
49
50
51
52
53
def __str__(self):
    """
    Returns:
        (String): The Store Object name.
    """
    return self.name

Aisle

Aisle

Model representing an aisle in a store.

Attributes:

Name Type Description
name CharField

The name of the Aisle.

order IntegerField

The order of appearance for Aisle.

store Store

An object respresenting a store.

Source code in backend/api/models.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class Aisle(models.Model):
    """
    Model representing an aisle in a store.

    Attributes:
        name (CharField): The name of the Aisle.
        order (IntegerField): The order of appearance for Aisle.
        store (Store): An object respresenting a store.
    """

    name = models.CharField(max_length=50)
    order = models.IntegerField(default=1)
    store = models.ForeignKey(Store, on_delete=models.CASCADE)

    def __str__(self):
        """
        Returns:
            (String): The Aisle Object name.
        """
        return f"{self.store.name} | {self.name}"

__str__()

Returns:

Type Description
String

The Aisle Object name.

Source code in backend/api/models.py
70
71
72
73
74
75
def __str__(self):
    """
    Returns:
        (String): The Aisle Object name.
    """
    return f"{self.store.name} | {self.name}"

Item

Item

Model representing an item.

Attributes:

Name Type Description
name CharField

The name of the Item. Unique.

matches CharField

Alternate spelling that matches.

plural CharField

Plural spelling of item name.

aisle Aisle

An object representing an aisle.

image ImageField

A photo of the item, for telling it apart from similar products on a shelf. Optional.

thumbnail ImageField

The downscaled rendition shown on list rows. Written by the upload endpoint alongside image; never set on its own.

Source code in backend/api/models.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class Item(models.Model):
    """
    Model representing an item.

    Attributes:
        name (CharField): The name of the Item. Unique.
        matches (CharField): Alternate spelling that matches.
        plural (CharField): Plural spelling of item name.
        aisle (Aisle): An object representing an aisle.
        image (ImageField): A photo of the item, for telling it apart from
            similar products on a shelf. Optional.
        thumbnail (ImageField): The downscaled rendition shown on list rows.
            Written by the upload endpoint alongside `image`; never set on its
            own.
    """

    name = models.CharField(max_length=50, unique=True)
    matches = models.CharField(max_length=254, null=True, blank=True)
    plural = models.CharField(max_length=50, null=True, blank=True)
    aisle = models.ForeignKey(
        Aisle, on_delete=models.SET_NULL, null=True, blank=True, default=None
    )
    image = models.ImageField(upload_to="items/", null=True, blank=True)
    thumbnail = models.ImageField(upload_to="items/thumbs/", null=True, blank=True)

    def __str__(self):
        """
        Returns:
            (String): The Item Object name.
        """
        return self.name

__str__()

Returns:

Type Description
String

The Item Object name.

Source code in backend/api/models.py
103
104
105
106
107
108
def __str__(self):
    """
    Returns:
        (String): The Item Object name.
    """
    return self.name

ShoppingList

ShoppingList

Model representing a ShopingList object.

Attributes:

Name Type Description
name CharField

The name of the shopping list.

store Store

An object representing a store.

Source code in backend/api/models.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class ShoppingList(models.Model):
    """
    Model representing a ShopingList object.

    Attributes:
        name (CharField): The name of the shopping list.
        store (Store): An object representing a store.
    """

    name = models.CharField(max_length=50)
    store = models.ForeignKey(Store, on_delete=models.CASCADE)

    def __str__(self):
        """
        Returns:
            (String): The ShoppingList Object name.
        """
        return f"{self.store.name} | {self.name}"

__str__()

Returns:

Type Description
String

The ShoppingList Object name.

Source code in backend/api/models.py
123
124
125
126
127
128
def __str__(self):
    """
    Returns:
        (String): The ShoppingList Object name.
    """
    return f"{self.store.name} | {self.name}"

ListItem

ListItem

Model representing a ListItem object.

Attributes:

Name Type Description
qty IntegerField

The numder of items for this list item.

purchased BooleanField

Wether this list item has been purchased.

notes TextField

Notes associated with this list item.

purch_date(DateFild) TextField

The date this list item was purchased.

item Item

An object representing an Item.

aisle Aisle

An object representing an Aisle.

shopping_list ShoppingList

An object representing a ShoppingList.

Source code in backend/api/models.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class ListItem(models.Model):
    """
    Model representing a ListItem object.

    Attributes:
        qty (IntegerField): The numder of items for this list item.
        purchased (BooleanField): Wether this list item has been purchased.
        notes (TextField): Notes associated with this list item.
        purch_date(DateFild): The date this list item was purchased.
        item (Item): An object representing an Item.
        aisle (Aisle): An object representing an Aisle.
        shopping_list (ShoppingList): An object representing a ShoppingList.

    """

    qty = models.IntegerField(default=1)
    purchased = models.BooleanField(default=False)
    notes = models.TextField(null=True, blank=True)
    purch_date = models.DateField(
        auto_now=False, auto_now_add=False, null=True, blank=True
    )
    item = models.ForeignKey(Item, on_delete=models.CASCADE)
    aisle = models.ForeignKey(Aisle, on_delete=models.CASCADE, null=True)
    shopping_list = models.ForeignKey(ShoppingList, on_delete=models.CASCADE)

    def __str__(self):
        """
        Returns:
            (String): The ListItem Object name.
        """
        return self.item.name

__str__()

Returns:

Type Description
String

The ListItem Object name.

Source code in backend/api/models.py
156
157
158
159
160
161
def __str__(self):
    """
    Returns:
        (String): The ListItem Object name.
    """
    return self.item.name

Freezer

Freezer

Model representing a Freezer.

Attributes:

Name Type Description
name CharField

The name of the freezer. Required. Unique.

location CharField

Where the freezer is, eg. "Garage". Optional.

Source code in backend/api/models.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class Freezer(models.Model):
    """
    Model representing a Freezer.

    Attributes:
        name (CharField): The name of the freezer. Required. Unique.
        location (CharField): Where the freezer is, eg. "Garage". Optional.
    """

    name = models.CharField(max_length=50, unique=True)
    location = models.CharField(max_length=50, null=True, blank=True)

    def __str__(self):
        """
        Returns:
            (String): The Freezer Object name.
        """
        return self.name

__str__()

Returns:

Type Description
String

The Freezer Object name.

Source code in backend/api/models.py
176
177
178
179
180
181
def __str__(self):
    """
    Returns:
        (String): The Freezer Object name.
    """
    return self.name

FreezerItem

FreezerItem

Model representing a frozen food stored in a Freezer.

Unlike ListItem, this does not reference the Item catalog. Freezer contents are often one-off leftovers ("chili, Nov 3") that would only pollute the shopping list catalog if they were forced into it.

Attributes:

Name Type Description
name CharField

The name of the frozen food.

qty IntegerField

How much is stored. Default = 1.

unit CharField

The unit for qty, eg. "lbs", "bags". Optional.

date_added DateField

The date this was put in the freezer. Optional, since food already in the freezer often has no date on it.

discard_date DateField

The date this should be thrown out. Optional.

notes TextField

Notes associated with this frozen food.

freezer Freezer

An object representing a Freezer.

image ImageField

A photo of the frozen food. Optional, and carried here rather than on Item because FreezerItem has no Item FK.

thumbnail ImageField

The downscaled rendition shown on freezer rows. Written by the upload endpoint alongside image; never set on its own.

Source code in backend/api/models.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
class FreezerItem(models.Model):
    """
    Model representing a frozen food stored in a Freezer.

    Unlike ListItem, this does not reference the Item catalog. Freezer
    contents are often one-off leftovers ("chili, Nov 3") that would only
    pollute the shopping list catalog if they were forced into it.

    Attributes:
        name (CharField): The name of the frozen food.
        qty (IntegerField): How much is stored. Default = 1.
        unit (CharField): The unit for qty, eg. "lbs", "bags". Optional.
        date_added (DateField): The date this was put in the freezer. Optional,
            since food already in the freezer often has no date on it.
        discard_date (DateField): The date this should be thrown out. Optional.
        notes (TextField): Notes associated with this frozen food.
        freezer (Freezer): An object representing a Freezer.
        image (ImageField): A photo of the frozen food. Optional, and carried
            here rather than on Item because FreezerItem has no Item FK.
        thumbnail (ImageField): The downscaled rendition shown on freezer rows.
            Written by the upload endpoint alongside `image`; never set on its
            own.
    """

    name = models.CharField(max_length=50)
    qty = models.IntegerField(default=1)
    unit = models.CharField(max_length=20, null=True, blank=True)
    date_added = models.DateField(
        auto_now=False, auto_now_add=False, null=True, blank=True
    )
    discard_date = models.DateField(
        auto_now=False, auto_now_add=False, null=True, blank=True
    )
    notes = models.TextField(null=True, blank=True)
    freezer = models.ForeignKey(Freezer, on_delete=models.CASCADE)
    image = models.ImageField(upload_to="freezeritems/", null=True, blank=True)
    thumbnail = models.ImageField(
        upload_to="freezeritems/thumbs/", null=True, blank=True
    )

    class Meta:
        ordering = ["freezer", "name"]

    @property
    def days_until_discard(self):
        """
        Returns:
            (int): Days until discard_date, negative once past it. None if no
                discard_date is set.
        """
        if not self.discard_date:
            return None
        return (self.discard_date - date.today()).days

    @property
    def is_expired(self):
        """
        Returns:
            (bool): True if the discard date has passed.
        """
        days = self.days_until_discard
        return days is not None and days < 0

    def __str__(self):
        """
        Returns:
            (String): The FreezerItem Object name.
        """
        return f"{self.freezer.name} | {self.name}"

__str__()

Returns:

Type Description
String

The FreezerItem Object name.

Source code in backend/api/models.py
247
248
249
250
251
252
def __str__(self):
    """
    Returns:
        (String): The FreezerItem Object name.
    """
    return f"{self.freezer.name} | {self.name}"

FreezerLog

FreezerLog

Model representing something that happened to a frozen food.

Answers "what happened to that meatloaf?" months later, so it is written to be readable on its own: the food's name, its unit and the freezer names are copied in as text rather than followed through a FK. A FreezerItem is deleted the moment the last of it is used, which is precisely when its log entries become interesting, so a cascade would erase the answer along with the question.

The FKs are kept as a convenience for as long as the rows survive, and go null rather than taking the log with them.

Attributes:

Name Type Description
action CharField

What happened — one of ACTION_CHOICES.

name CharField

The food's name when the event happened.

qty IntegerField

How many the event concerned. For a use or a move this is the number used or moved, not the number remaining.

unit CharField

The unit for qty, copied from the food. Optional.

freezer_name CharField

The freezer this happened in, as text.

to_freezer_name CharField

Where a move sent it, as text. None for every other action.

freezer Freezer

The freezer, while it still exists. Optional.

freezeritem FreezerItem

The food, while it still exists. Optional, and null for anything used up, thrown out or since deleted.

occurred DateTimeField

When it happened. Set once, on write.

Source code in backend/api/models.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
class FreezerLog(models.Model):
    """
    Model representing something that happened to a frozen food.

    Answers "what happened to that meatloaf?" months later, so it is written to
    be readable on its own: the food's name, its unit and the freezer names are
    **copied in as text** rather than followed through a FK. A FreezerItem is
    deleted the moment the last of it is used, which is precisely when its log
    entries become interesting, so a cascade would erase the answer along with
    the question.

    The FKs are kept as a convenience for as long as the rows survive, and go
    null rather than taking the log with them.

    Attributes:
        action (CharField): What happened — one of ACTION_CHOICES.
        name (CharField): The food's name when the event happened.
        qty (IntegerField): How many the event concerned. For a use or a move
            this is the number used or moved, not the number remaining.
        unit (CharField): The unit for qty, copied from the food. Optional.
        freezer_name (CharField): The freezer this happened in, as text.
        to_freezer_name (CharField): Where a move sent it, as text. None for
            every other action.
        freezer (Freezer): The freezer, while it still exists. Optional.
        freezeritem (FreezerItem): The food, while it still exists. Optional,
            and null for anything used up, thrown out or since deleted.
        occurred (DateTimeField): When it happened. Set once, on write.
    """

    ACTION_ADDED = "added"
    ACTION_USED = "used"
    ACTION_MOVED = "moved"
    ACTION_DISCARDED = "discarded"

    ACTION_CHOICES = [
        (ACTION_ADDED, "Added"),
        (ACTION_USED, "Used"),
        (ACTION_MOVED, "Moved"),
        (ACTION_DISCARDED, "Thrown out"),
    ]

    action = models.CharField(max_length=10, choices=ACTION_CHOICES)
    name = models.CharField(max_length=50)
    qty = models.IntegerField(default=1)
    unit = models.CharField(max_length=20, null=True, blank=True)
    freezer_name = models.CharField(max_length=50)
    to_freezer_name = models.CharField(max_length=50, null=True, blank=True)
    freezer = models.ForeignKey(
        Freezer, on_delete=models.SET_NULL, null=True, blank=True
    )
    freezeritem = models.ForeignKey(
        FreezerItem, on_delete=models.SET_NULL, null=True, blank=True
    )
    occurred = models.DateTimeField(auto_now_add=True)

    class Meta:
        # Newest first, with id breaking ties: several entries can share a
        # timestamp, and without the tiebreak their order is undefined and the
        # history page shuffles between reads.
        ordering = ["-occurred", "-id"]
        indexes = [
            # The history page's two access patterns: the unfiltered feed, and
            # a name search.
            models.Index(fields=["-occurred"]),
            models.Index(fields=["name"]),
        ]

    @classmethod
    def record(cls, action, freezeritem, qty=None, freezer=None, to_freezer=None):
        """
        Writes one entry for something that just happened to a food.

        Called from the API handlers rather than a post_delete signal, so that a
        cascade — deleting a whole freezer — does not read as having thrown out
        everything inside it. The tradeoff is that admin-side deletes go
        unlogged, which is the right way round: the log is a record of what was
        done to the food, not of every row that left the table.

        Args:
            action (str): One of the ACTION_* values.
            freezeritem (FreezerItem): The food the event concerns.
            qty (int): How many the event concerned. Defaults to the food's
                whole quantity, which is what "added" and "thrown out" mean.
            freezer (Freezer): The freezer it happened in. Defaults to the
                food's own, which is wrong once a move has reassigned it — pass
                the source explicitly there.
            to_freezer (Freezer): Where a move sent it.

        Returns:
            (FreezerLog): The entry written.
        """
        source = freezer or freezeritem.freezer
        return cls.objects.create(
            action=action,
            name=freezeritem.name,
            qty=freezeritem.qty if qty is None else qty,
            unit=freezeritem.unit,
            freezer_name=source.name if source else "",
            to_freezer_name=to_freezer.name if to_freezer else None,
            freezer=source,
            # A row being used up is deleted right after this is written, which
            # nulls the FK — by design.
            freezeritem=freezeritem if freezeritem.pk else None,
        )

    def __str__(self):
        """
        Returns:
            (String): The FreezerLog Object description.
        """
        return f"{self.occurred:%Y-%m-%d} | {self.action} {self.name}"

__str__()

Returns:

Type Description
String

The FreezerLog Object description.

Source code in backend/api/models.py
360
361
362
363
364
365
def __str__(self):
    """
    Returns:
        (String): The FreezerLog Object description.
    """
    return f"{self.occurred:%Y-%m-%d} | {self.action} {self.name}"

Version

Version

Model representing app version.

Fields: - version_number (CharField): The current version of the app.

Source code in backend/api/models.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
class Version(SingletonModel):
    """
    Model representing app version.

    Fields:
    - version_number (CharField): The current version of the app.
    """

    version_number = models.CharField(max_length=10)

    def __str__(self):
        """
        Returns:
            (String): The version number.
        """
        return self.version_number

__str__()

Returns:

Type Description
String

The version number.

Source code in backend/api/models.py
378
379
380
381
382
383
def __str__(self):
    """
    Returns:
        (String): The version number.
    """
    return self.version_number