Marking Items Used

Food Tracker (Django)

Chapter 9 · Marking Items Used

A short chapter — and a good place to see how a decision this app's Firebase-based sibling course treated very carefully turns out to need almost no ceremony at all in a relational model.

The View

def mark_used(request, item_id): item = get_object_or_404(Item, id=item_id) item.status = "used" item.expiry_date = None item.used_at = timezone.now() item.save() return redirect("item_history")

get_object_or_404 is a small, genuinely useful Django shortcut — it combines the lookup and an automatic 404 response if nothing matches, instead of a manual try/except Item.DoesNotExist.

expiry_date = None: Simply the Right Answer Here

Food Tracker (React + Firebase)'s own Chapter 9 spent real effort explaining why deleteField(), not null, was the correct choice — because in Firestore's schema-on-read model, a document created used-from-the-start never had the field at all, and setting it to null on an existing document would have created a second, inconsistent representation of "no expiry." That entire problem simply doesn't exist here. Chapter 2's expiry_date column is always present, on every row, regardless of history — a used item created directly as used already has expiry_date = NULL from the very start, exactly matching what this view produces on a transition. There's only ever one shape for "no expiry" in a schema-on-write model, because every row already shares the identical set of columns.

Two different mechanics, the same correct underlying principle
Neither course's approach is more "correct" than the other — deleteField() and None are each the right tool for representing an identical idea ("this item has no expiry date") inside two genuinely different storage models. Schema-on-read has to actively choose between absence and a stored null, and picking wrong creates two different representations. Schema-on-write never has that choice to make at all — every row already has the same columns, so there's exactly one way to represent "no expiry," and it's simply NULL.

Should This Even Be a Plain Link?

mark_used takes no user input — no form fields, nothing to fill in. It's tempting to trigger it from a simple <a href="/mark-used/<id>/"> link. Resist that: a state-changing action behind a plain GET request is a real, substantive correctness problem, not a stylistic nitpick — browser prefetching, crawlers following links, or the browser's own back/forward cache can all trigger a GET request without the user ever intending to. GET is supposed to be safe and non-mutating per HTTP's own semantics; this action should be a POST, even with no actual form fields to collect:

<form method="post" action="{% url 'mark_used' item.id %}"> {% csrf_token %} <button type="submit">Mark Used</button> </form>
Reach for get_object_or_404 everywhere a lookup can fail
Any view fetching a specific row by ID benefits from the same pattern — a clean, automatic 404 instead of an unhandled exception if the ID doesn't exist.
A GET link for a mutating action is a real bug waiting to happen
A search engine crawler, a browser preloading a hovered link, or simply pressing back after marking something used could all silently re-trigger this exact action if it's reachable via GET — genuinely different from a cosmetic style preference, since it can cause real, unintended state changes with no user action actually behind them.
An "undo" would still need to restore the value from somewhere
Once expiry_date is set to None and saved, the original value is genuinely gone from that row — exactly the same limitation Food Tracker (React + Firebase)'s own Chapter 9 already named for deleteField(). A real undo feature would need to have captured the original date somewhere before clearing it (briefly, in the session, for an "undo" window) or simply ask the user to re-enter it — the same underlying lesson, now confirmed a second time in a completely different storage model.

Where This Course Is Headed

Recipe lookup, Django REST Framework, deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain why expiry_date = None is sufficient here, while the Firebase sibling course specifically needed deleteField() rather than null. What single underlying difference between the two courses' storage models explains this?

📄 View solution
Exercise 2

Explain the real problem with triggering mark_used from a plain GET link, giving at least one concrete scenario where this could cause an unintended state change.

📄 View solution
Exercise 3

Explain why a future "undo" feature can't simply set expiry_date back to a value after this view has already run, and what a genuine undo implementation would need to do instead.

📄 View solution

Chapter 9 Quick Reference

  • expiry_date = None — sufficient here; schema-on-write guarantees one consistent shape for "no expiry" regardless of history
  • get_object_or_404 — lookup + automatic 404, instead of a manual exception handler
  • A mutating action must be POST — never a plain GET link, even with no form fields to collect
  • Undo still isn't free — the original value is genuinely gone once saved, the same limitation as the Firebase course's own deleteField()
  • Next chapter: Recipe Lookup with TheMealDB