Expiry Alerts

Food Tracker (Django)

Chapter 7 · Expiry Alerts

With items actually being added, this chapter builds the first payoff feature: surfacing what's about to go to waste.

The QuerySet

from datetime import timedelta from django.utils import timezone def dashboard(request): threshold = timezone.now().date() + timedelta(days=3) expiring_items = Item.objects.filter( status="active", expiry_date__lte=threshold ).order_by("expiry_date") return render(request, "pantry/dashboard.html", {"items": expiring_items})

expiry_date__lte is Django's own field-lookup syntax — a double-underscore suffix mapping directly to a SQL comparison (__lte<=, __gte>=, __containsLIKE, and so on). SQLAlchemy, Python's other major ORM, takes a genuinely different syntactic approach — operator overloading, so the identical comparison reads as Item.expiry_date <= threshold directly. Both are real, working styles for the same underlying idea; recognizing Django's own double-underscore convention on sight is worth having, since it looks unusual coming from any other ORM.

timezone.now(), Not datetime.now()

Django's modern default, USE_TZ=True, makes the project timezone-aware. Using plain datetime.now() in a timezone-aware project produces a naive datetime with no timezone attached at all — comparisons against Django's own timezone-aware stored values can then silently be wrong, especially once the deployed server and its users don't share a timezone. django.utils.timezone.now() is Django's own answer to exactly the same category of problem every Food Tracker course has hit in its own way — never trust the wrong clock.

No Composite Index Drama Here — a Genuinely Fair Point for This Course

Food Tracker (React + Firebase)'s own Chapter 7 needed an entire section on Firestore's composite-index requirement — a query combining an equality filter and a range filter on two different fields simply refuses to run at all until an index is explicitly declared. Django's ORM, running on a real relational database, has no equivalent hard requirement: the identical two-field filter here just runs. It might run slowly without an index as the table grows — ordinary SQL's own honest tradeoff, matching that same comparison the Firebase course already drew — but it never refuses outright the way Firestore does. This is one of the few places in the whole quartet where the relational side genuinely has the easier time, worth saying plainly rather than only ever finding fault with it.

# Optional — a real performance index, not a functional requirement class Meta: indexes = [models.Index(fields=["status", "expiry_date"])]

The Template

{% extends "pantry/base.html" %} {% block content %} <h1>Expiring Soon</h1> <ul> {% for item in items %} <li>{{ item.name }} — expires {{ item.expiry_date }}</li> {% empty %} <li>Nothing expiring soon.</li> {% endfor %} </ul> {% endblock %}

{% empty %} is Django's own built-in "if the loop had nothing to iterate" branch, right inside the {% for %} tag — no separate {% if items %} wrapper needed around it.

A fair advantage, stated plainly
This is the one chapter in the whole quartet where the relational courses genuinely have it easier than the document-database one: a two-field filter query here simply runs, where the same shape of query on Firestore needs an index declared ahead of time or it fails outright. Every architecture in this comparison has real strengths and real costs — this is one of the SQL side's real strengths, worth naming without hedging.
Test the queryset before the view exists
python manage.py shell, then Item.objects.filter(status="active", expiry_date__lte=timezone.now().date() + timedelta(days=3)) — the same isolate-and-verify habit already established for the model (Chapter 3) and the barcode view (Chapter 4).
A naive datetime can silently produce a wrong comparison
With USE_TZ=True (Django's modern default), mixing a naive datetime.now() value into a comparison against timezone-aware stored values doesn't necessarily raise an error — it can just as easily produce a subtly wrong result, especially once the deployed server and its users span different timezones. timezone.now() avoids the ambiguity entirely by staying timezone-aware throughout.

Where This Course Is Headed

Item history and search, marking items used, recipe lookup, Django REST Framework, deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain Django's expiry_date__lte field-lookup syntax, and show how SQLAlchemy would express the identical comparison using its own operator-overloading style.

📄 View solution
Exercise 2

Explain why this chapter's own two-field filter query doesn't need anything like the Firebase sibling course's composite-index requirement, and what tradeoff still exists if no database index is ever added.

📄 View solution
Exercise 3

Explain concretely what could go wrong if datetime.now() were used instead of timezone.now() in this chapter's own dashboard view, given USE_TZ=True.

📄 View solution

Chapter 7 Quick Reference

  • __lte — Django's own double-underscore field-lookup syntax, vs. SQLAlchemy's operator overloading
  • timezone.now() — always, not datetime.now(), once USE_TZ=True
  • No composite index required — a genuine, fair SQL advantage over the Firebase sibling course's own Chapter 7
  • Meta.indexes — an optional performance optimization here, not a hard functional requirement
  • {% empty %} — a built-in empty-loop branch inside {% for %}
  • Next chapter: Item History & Live Search-as-You-Type