Food Tracker (Django)
A Complete 13-Chapter Batteries-Included Course
Table of Contents
- Project Overview & Why Django
- Modeling the Item
- The Django Admin, Free
- Barcode Lookup & BarcodeCache
- Camera-Based Barcode Scanning
- The Add-Item Form
- Expiry Alerts
- Item History & Live Search
- Marking Items Used
- Recipe Lookup with TheMealDB
- Django REST Framework
- Deployment
- Capstone: A Complete, Working Food Tracker
Project Overview & Why Django
Food Tracker (Django)
Chapter 1 · Project Overview & Django Project/App Setup
This is one of four courses building the exact same app in four genuinely different architectures — Food Tracker (FastAPI), Food Tracker (React + Express), and Food Tracker (React + Firebase) are its siblings. Every one of them scans a barcode, tracks a use-by date, and alerts before something goes to waste. This course's own answer leans all the way into the opposite philosophy from its FastAPI sibling: one single, integrated framework, rather than a thin layer you assemble yourself from smaller pieces.
What the App Actually Does
The shared spec every course in the quartet builds toward:
- Scan a barcode with a phone or webcam camera, look it up against Open Food Facts (free, open, no API key) to fetch the product's name and details automatically.
- Record a use-by date for the item, and see it flagged once it's expiring soon.
- Keep a full history of every item ever added — some still active with a real expiry date, some already marked used with no expiry date at all — searchable in real time as you type.
- Look up recipes via TheMealDB (also free, no key) that use ingredients close to expiring.
A weekly meal planner is explicitly out of scope for all four courses — named future work, not something any of them will build.
Batteries Included, vs. a Thin API Layer
FastAPI, this quartet's other Python course, gives you essentially one thing: a fast, well-typed way to define HTTP routes. Everything else — the database layer, the templating, an admin interface — is a separate library you choose and wire in yourself. Django takes the opposite position entirely: an ORM, a templating engine, a full admin interface, a forms system, and authentication all ship together, pre-integrated, from the very first startproject command. Neither philosophy is simply "better" — they represent two real, different answers to how much a framework should decide for you up front.
Project vs. App: Two Different Things
A Django project (foodtracker) is the overall configuration — settings, URL routing, WSGI/ASGI entry points. An app (pantry) is a self-contained component holding its own models, views, and templates for one piece of functionality. A single project can hold several apps; this course's entire feature set lives inside one, pantry, since the app is genuinely small enough not to need splitting further.
MVT: Model-View-Template
Django's own name for its architecture is MVT, not the more familiar MVC — and the naming difference matters, because it's a real, common point of confusion. Django's Model is the ORM layer, same idea as MVC's Model. Django's Template is the presentation layer, same idea as MVC's View. Django's own View, confusingly, is actually the controller in classic MVC terms — the Python function or class that receives a request, talks to the Model, and picks a Template to render. Anyone arriving from traditional MVC terminology (or from FastAPI, which has no equivalent three-part naming at all) has to consciously remember that Django's "View" isn't what "view" means almost everywhere else.
Setting Up and Running It
SQLite is the genuine, appropriate choice here, not a placeholder to "upgrade later" — this is a personal, single-user app, exactly the case SQLite was designed for.
pantry's models exist (Chapter 2), Django's admin site gives a genuine, working CRUD interface for them for free, with zero custom code — something FastAPI has no equivalent of at all without building it by hand. This is the "batteries included" philosophy made concrete: an entire category of work Django considers part of the framework itself, that a thin-API-layer framework considers entirely the developer's own problem.
pantry reads clearly everywhere Django references app names — INSTALLED_APPS, migration folders, template lookup paths. A generic name like app1 would work identically but read as meaningless the moment the project has more than one app.
INSTALLED_APPS — and the resulting errors are often confusing rather than direct: a "template does not exist" error, for instance, rather than a clear "app not registered" message. If something Django-related seems to silently not exist, checking INSTALLED_APPS first is worth making a reflex.
Where This Course Is Headed
Modeling the Pantry Item with Django's ORM, the Django admin as an instant CRUD tool, barcode lookup via a Django view, camera-based scanning, the add-item flow via Django Forms, expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework for the interactive features that need real JSON, deployment, and a capstone.
Hands-On Exercises
Explain what Django's "View" actually corresponds to in classic MVC terms, and why this chapter calls it a common point of confusion for newcomers.
📄 View solutionExplain the difference between a Django project and a Django app, using this chapter's own startproject/startapp commands as your example.
📄 View solutionExplain what breaks if a newly created app is never added to INSTALLED_APPS, and why the resulting error can be confusing to diagnose.
📄 View solutionChapter 1 Quick Reference
- The shared app — barcode scan (Open Food Facts) → expiry tracking → alerts → searchable history → recipe lookup (TheMealDB); no meal planner
- Batteries included — ORM, admin, templating, forms, auth all ship together, unlike FastAPI's thin-layer approach
- Project vs. app —
foodtracker(config) vs.pantry(the feature itself) - MVT — Model (ORM), View (actually the controller), Template (presentation) — not classic MVC naming
- SQLite — a genuine, appropriate choice for this personal, single-user app
- This course's own throughline: one integrated framework deciding more up front, vs. FastAPI's compose-it-yourself approach
- Next chapter: Data Modeling with Django's ORM
Modeling the Item
Food Tracker (Django)
Chapter 2 · Data Modeling with Django's ORM
Chapter 1 set up the project and the app. This chapter defines the one Model that everything else in this course builds on.
The Model
null=True and blank=True Are Two Different Settings
null=True is a database-level setting — it lets the actual SQL column store NULL. blank=True is a form/admin-level setting — it lets Django's forms and the admin site accept an empty value without raising a validation error. They're independent, and both are needed on expiry_date for two separate reasons: null=True so a used item's row can genuinely have no expiry date stored, and blank=True so the admin form doesn't reject an empty expiry date as a validation failure, even though the database would happily accept it.
This app's own Firestore-based sibling course models the identical "no expiry" case by omitting the field from the document entirely — a genuine structural difference between schema-on-write (this course: an always-present column, sometimes holding NULL) and schema-on-read (that course: the field simply isn't there at all).
DateField vs. DateTimeField
expiry_date uses DateField — a use-by date has no meaningful time component. added_at and used_at use DateTimeField, since knowing roughly when within a day something happened is genuinely useful for those two fields specifically.
auto_now_add: Django's Own Trustworthy Timestamp
auto_now_add=True sets a field's value once, at creation, using the server's own clock — and makes the field non-editable through ordinary forms. This solves the identical problem the Firebase sibling course's own serverTimestamp() solves: never trust a client-supplied creation time. auto_now_add is easy to confuse with the similarly-named auto_now, which instead updates the field on every save — the wrong choice for added_at, which should only ever be set the one time the row is created.
choices= Is a Voluntary Constraint, Not a Guarantee
STATUS_CHOICES restricts what the admin site and Django Forms will offer as valid options for status — but whether that also becomes a real database-level constraint depends on the Django version in use, and shouldn't be assumed either way without checking. A raw SQL statement, or code that bypasses Django's own forms and admin validation, may still be able to write a value outside STATUS_CHOICES depending on that. choices= is genuinely useful for guiding the admin and forms layer; treating it as an unconditional database-level guarantee is the kind of assumption worth verifying rather than trusting blindly.
Migrations: Two Separate Steps, On Purpose
makemigrations compares the current models against the last recorded schema state and generates a migration file — a versioned, reviewable Python description of the change. migrate is the separate step that actually applies pending migrations to a real database. Keeping "describe the change" and "apply the change" as two distinct commands means a migration file can be reviewed and committed to version control before it ever touches a database, and the exact same migration can be applied identically across a developer's machine, staging, and production.
python manage.py shell opens an interactive Python shell with Django's app registry already loaded — Item.objects.create(name="Test", status="active") works immediately, letting the model itself be exercised before any URL, view, or template exists yet.
null=True without also setting blank=True (or the reverse) is the most common source of confusing bugs on a field like this one. A field that genuinely should be optional will still throw a "this field is required" validation error in a form or the admin if blank=True is missing — even though the database column itself would happily accept NULL. If a field behaves like it's required when it clearly shouldn't be, check blank before anything else.
Where This Course Is Headed
The Django admin as an instant CRUD tool for this exact model, barcode lookup via a Django view, camera-based scanning, the add-item flow via Django Forms, expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework, deployment, and a capstone.
Hands-On Exercises
Explain the difference between null=True and blank=True, and why expiry_date needs both rather than just one.
📄 View solutionExplain the difference between auto_now_add and auto_now, and why added_at specifically needs the former, not the latter.
📄 View solutionExplain what makemigrations does versus what migrate does, and why Django keeps them as two separate commands rather than one combined step.
📄 View solutionChapter 2 Quick Reference
null=True— database-level; allows a real SQLNULLblank=True— form/admin-level; allows an empty value in forms and the adminauto_now_add— set once at creation, server clock, non-editable;auto_nowupdates on every save insteadchoices=— constrains forms/admin; don't assume it's an unconditional database guarantee without checkingmakemigrationsvs.migrate— describe the change, then separately apply it; keeps changes reviewable and reproducible- Next chapter: The Django Admin: Instant CRUD for Free
The Django Admin, Free
Food Tracker (Django)
Chapter 3 · The Django Admin: Instant CRUD for Free
Chapter 1 promised a working CRUD interface before writing any business logic at all. Here's that promise, delivered.
Registering the Model
createsuperuser creates the admin's own login account — a separate concept entirely from any future end-user account this app might have. Visiting /admin/ and signing in with it now shows a complete, working list/add/edit/delete interface for Item, generated from just those two lines of code.
Customizing It With ModelAdmin
Plain registration gives a genuinely functional but generic list view. A ModelAdmin subclass shapes it into something actually useful day to day:
list_display— which columns actually show in the list view, instead of just each row's__str__.list_filter— a sidebar of quick filters, genuinely useful for jumping straight to, say, everything withstatus = "active".search_fields— the admin's own search box, which performs a real substring (SQLLIKE) match by default. Worth flagging honestly: this is a happy coincidence, not the same thing as the live search-as-you-type feature this course builds for the app itself later — the admin's search exists purely for the admin's own list view.readonly_fields—added_atis already non-editable (Chapter 2'sauto_now_addimplicitly setseditable=False), and Django's admin would normally just omit a non-editable field from the form entirely. Listing it inreadonly_fieldsinstead keeps its actual value visible — read-only — right there in the change form, rather than making it invisible.
A Real Tool, Not Just a Demo
Before Chapter 4's barcode lookup or Chapter 5's real add-item flow exist, this admin already lets real test Item rows be created by hand — genuinely useful for exercising Chapter 7's expiry query, or Chapter 8's search, against real data long before the app's own user-facing screens are built.
A Real Limit Worth Naming
The admin is a tool for whoever manages the site's data directly — it was never meant to be, and won't become, the pantry app's own end-user interface. There's no camera scanning here, no barcode-triggered lookup, no live search-as-you-type UX. Having a working admin is not the same as having a finished app; the two serve genuinely different audiences.
admin.py. This is exactly the "batteries included" tradeoff Chapter 1 named, made concrete rather than abstract.
list_editablelist_editable = ("status",) lets specific fields be edited directly from the list view itself, without opening the full change form — useful for a quick bulk "mark several as used" pass, once there's real data to try it on.
/admin/ is a powerful, largely unrestricted interface for anyone who successfully logs in — it isn't sandboxed the way the app's own end-user views might be. Real deployment deserves real consideration here: a genuinely strong password, restricting network access to it where feasible, and third-party two-factor packages are all common, real mitigations — treating the admin as purely a development convenience with no real-world exposure risk, once it's live, would be a mistake.
Where This Course Is Headed
Barcode lookup via a Django view, camera-based scanning, the add-item flow via Django Forms, expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework, deployment, and a capstone.
Hands-On Exercises
Explain what admin.site.register(Item) alone provides, and what a customized ModelAdmin subclass adds on top of it. Why do list_display, list_filter, and search_fields matter in practice rather than just being nice-to-haves?
📄 View solutionExplain why readonly_fields is still useful for added_at, given that auto_now_add already makes the field non-editable via editable=False.
📄 View solutionExplain the real security consideration this chapter raises about exposing /admin/ on a public deployment, and name at least two real mitigations it mentions.
📄 View solutionChapter 3 Quick Reference
admin.site.register(Item)— a full generic CRUD interface, two lines of codeModelAdmin—list_display,list_filter,search_fields,readonly_fieldsshape it into something actually usable- The admin's search ≠ the app's own live search — a happy coincidence of substring matching, not the same feature built later
- A dev tool, not the end-user UI — no camera scanning, no live search UX, a genuinely different audience
- Real production consideration — a public
/admin/needs a real password policy, restricted access, or 2FA - Next chapter: Barcode Lookup: Integrating Open Food Facts
Barcode Lookup & BarcodeCache
Food Tracker (Django)
Chapter 4 · Barcode Lookup: Integrating Open Food Facts
The admin gave a working CRUD interface for free. This chapter writes the first genuinely custom code in this course: a view that turns a scanned barcode into a real product name.
URLs and Views: Two Separate Files, on Purpose
Django deliberately keeps "which URL triggers this code" (urls.py) separate from "what that code actually does" (views.py) — two files, wired together explicitly. FastAPI's own decorator-based routing (@app.get("/path")) puts both in the same place, directly above the function it decorates. Neither is objectively better; it's a real, structural difference in where that one piece of wiring lives, worth knowing rather than assuming every framework does it the same way.
A Second Model: BarcodeCache
Open Food Facts needs no API key here either, so the same reasoning that justified a Cloud Function in this app's own Firebase sibling course applies again: caching, consistent error handling, future-proofing — nothing about secrecy.
Note primary_key=True on barcode here — the exact opposite of Chapter 2's own Item model, where barcode is deliberately an ordinary field, never the primary key, because the same product can be purchased and tracked more than once. BarcodeCache genuinely is one record per product, so keying it directly by barcode is correct here.
The View
Wiring the URL
<str:barcode> is Django's own path converter — it types and captures the URL segment directly inside the route pattern string, rather than as a Python function type annotation the way FastAPI declares a path parameter's type.
Migrating the New Model
requests Isn't a Django Battery
Worth being precise about what "batteries included" actually covers: Django ships an ORM, an admin, forms, and templating — everything for building the app's own request/response cycle. Making an outbound call to someone else's API is a different job entirely, and needs a separate library (pip install requests) regardless of how much Django itself provides natively.
Item tracks purchased instances (many can share a barcode); BarcodeCache tracks canonical products (exactly one per barcode). This app's own Firebase sibling course faced the identical question for its items vs. barcodeCache collections, and landed on the same answer for the same underlying reason — proof this is a genuine data-modeling principle, not a coincidence of one particular database technology.
/lookup/<a-real-barcode>/ directly in a browser returns the raw JSON response — a genuinely useful way to confirm this view works correctly before any frontend exists at all, the same "test one layer in isolation" habit Chapter 3's manage.py shell tip already established.
try/except around the requests.get() call at all. If Open Food Facts is slow, times out, or is simply down, this raises an unhandled exception, and whoever called this endpoint gets a raw Django 500 error page instead of a clean, meaningful response. This is a genuine, known gap in this simple version — a production-ready version would wrap the request in error handling and return a proper JSON error response instead of letting the exception propagate.
Where This Course Is Headed
Camera-based barcode scanning, the add-item flow via Django Forms, expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework, deployment, and a capstone.
Hands-On Exercises
Explain the structural difference between Django's separated urls.py/views.py routing and FastAPI's decorator-based routing. Where does "this URL maps to this code" actually live in each?
📄 View solutionExplain why BarcodeCache uses barcode as its primary key while Item does not, and connect this to how the Firebase sibling course resolved the identical design question.
📄 View solutionExplain what actually happens right now if Open Food Facts is down or times out while this view is running, and why this is described as a genuine, known gap rather than an acceptable final state.
📄 View solutionChapter 4 Quick Reference
- urls.py / views.py — routing and logic deliberately kept separate, unlike FastAPI's combined decorator approach
BarcodeCache—barcodeas primary key, correctly, since this collection is one-per-product<str:barcode>— Django's own URL path converter/typing systemrequests— not a Django battery; outbound API calls still need a separate library- Known gap — no error handling yet for a failing Open Food Facts request; a real 500 error results today
- Next chapter: Camera-Based Barcode Scanning (Frontend)
Camera-Based Barcode Scanning
Food Tracker (Django)
Chapter 5 · Camera-Based Barcode Scanning (Frontend)
Chapter 4 built the endpoint; this chapter builds the page a phone camera actually talks to. The underlying browser APIs are identical to what this app's React-based sibling courses use — but how that code gets packaged looks genuinely different here.
A Template, Not a Component
Django has no equivalent of a portable React component or hook — no lightweight, reusable frontend unit that carries its own logic and can simply be dropped into two different apps unmodified. What it has instead is template inheritance and includes ({% extends %}, {% include %}) — a coarser-grained way to reuse markup, not JavaScript behavior. The scanning logic in this course lives as page-specific script inside one template, not as something this course could hand to a sibling the way the two React-based courses share one component outright.
The View
The Template
A Real Full-Page-Navigation Consequence
Detecting a barcode here navigates to an entirely new URL — a genuine server round-trip and full page reload, not an in-memory state update. This is a real, honest consequence of the server-rendered-template default, not a mistake: the two React-based sibling courses handle a successful scan by updating component state directly, with no navigation at all. Chapter 11's Django REST Framework chapter is exactly where this could change — a fetch() call to a JSON endpoint instead of a URL redirect would avoid the page reload entirely, at the cost of writing more client-side JavaScript to manage that state by hand.
Camera Cleanup: The Same Gotcha, a Genuinely Harder Problem Here
stream.getTracks().forEach(t => t.stop()) still has to run before the camera is released — same underlying requirement as any camera-scanning code, in any framework. But without a component lifecycle, this template can only reliably guarantee cleanup on the one exit path it explicitly coded: a successful scan. A user who instead clicks a "Cancel" link, uses the browser's back button, or simply closes the tab leaves the camera running, with no equivalent of a React useEffect cleanup function guaranteed to fire regardless of how the component goes away. This is a genuine structural disadvantage of the plain-template approach for this specific concern, not something this chapter glosses over.
BarcodeDetector Support: The Same Real Limitation
Chrome/Edge on Android and desktop support it natively; Safari on iOS does not, and needs the same ZXing fallback this app's React-based courses rely on. This is a browser API limitation, entirely independent of which backend framework is serving the page.
getUserMedia, BarcodeDetector, the ZXing fallback — don't care which backend framework is involved, and are genuinely reusable in spirit across all four Food Tracker courses. What differs is packaging: two React-based sibling courses share one literal, portable component; this course's identical logic lives as a page-specific script, because Django has no equivalent lightweight frontend-component primitive to package it as.
pantry/static/pantry/scan.js, rather than inline inside the template, means Django's own static-file handling (collected and served properly once Chapter 12 covers deployment) applies to it, and it's ready to reuse directly if a second page in this app ever needs scanning too.
Where This Course Is Headed
The add-item flow via Django Forms, expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework — which directly revisits this chapter's own full-page-reload limitation — deployment, and a capstone.
Hands-On Exercises
Explain why this course's camera-scanning JavaScript can't be shared as a single reusable unit the way the two React-based sibling courses share one component. What does Django offer instead, and why is it a coarser-grained kind of reuse?
📄 View solutionExplain what happens, technically, when this chapter's scan.js detects a barcode, and how that differs from what happens in the React-based sibling courses on a successful scan. What would Chapter 11's DRF work change about this?
📄 View solutionExplain why camera cleanup is honestly a harder problem in this chapter's template-based approach than in a React component, referencing specifically what a React lifecycle hook guarantees that a plain template script cannot.
📄 View solutionChapter 5 Quick Reference
- No portable component — Django's reuse tools (
{% extends %}/{% include %}) work at the markup level, not the JS-behavior level - Same browser APIs as the React courses —
getUserMedia,BarcodeDetector, ZXing fallback — just packaged differently - A successful scan triggers a full page reload — a genuine server round-trip, not an in-memory update; Chapter 11's DRF work is where this could change
- Cleanup is genuinely harder here — only the coded exit path stops the camera; no lifecycle guarantee covers every way a user might leave
- Next chapter: Building the Add-Item Flow with Django Forms
The Add-Item Form
Food Tracker (Django)
Chapter 6 · Building the Add-Item Flow with Django Forms
Chapter 5's scan flow redirects here once a barcode's been looked up. This chapter builds the actual confirm-and-save step — and shows one of Django's most concrete "batteries included" payoffs yet.
ModelForm: Deriving a Form From the Model, Not Duplicating It
ModelForm generates both the form fields and their validation rules directly from Chapter 2's own Item model — max_length, blank, choices, all of it carries over automatically. FastAPI's own equivalent needs a separate Pydantic schema class, hand-written to mirror the same fields the SQLAlchemy model already defines — a real duplication FastAPI's own philosophy accepts as a reasonable tradeoff for its thinner-layer design. Django's ModelForm reads the single source of truth directly instead of restating it.
One View, Both GET and POST
One function handles the entire request cycle for this URL: a GET shows a blank or pre-filled form, a POST validates and saves it. A typical FastAPI/REST-style app usually splits this into two distinct endpoints instead — a genuine structural difference beyond just where routing lives (Chapter 4), extending into how the request/response cycle itself is organized.
commit=False returns the unsaved model instance the form built, without writing it to the database yet — giving a chance to set status = "active" directly in code, a field deliberately left out of the form entirely so the user never controls it, before the actual .save() happens.
The Template
Where ModelForm Stops Being the Right Tool
Chapter 5 already flagged that a full-page redirect isn't the only way this could work — a future JSON-driven version of this flow would want a fetch() call instead of a traditional form POST. ItemForm genuinely isn't suited to that: it's built to render HTML and consume form-encoded POST bodies, not to produce or consume JSON. That's specifically what Django REST Framework's own Serializers are for — JSON in, JSON out, no HTML rendering assumed. This chapter's ModelForm approach is a real, complete, working feature on its own terms, not a placeholder DRF simply replaces; Chapter 11 picks the serializer-based path for a genuinely different reason (the interactive features that need real JSON), not because this chapter's approach was wrong.
ModelForm reading directly from Chapter 2's model instead of restating its fields, and {% csrf_token %} requiring zero extra library to get real CSRF protection, are both direct continuations of Chapter 1's own "batteries included" framing — not abstract philosophy anymore, but two specific, working pieces of code that exist because the framework decided to provide them.
{{ form.as_p }} automatically displays any field-level validation errors back to the user, right alongside the offending field — no extra template code needed to wire that up.
{% csrf_token %} in a POST form causes Django's own CSRF middleware to reject the submission outright with a 403 Forbidden — a confusing error for anyone who doesn't yet know why, but genuinely the correct, secure default behavior working as designed, not a bug. This protection exists automatically, for every POST form, without installing anything extra.
Where This Course Is Headed
Expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework — which revisits this chapter's own ModelForm-vs-serializer fork directly — deployment, and a capstone.
Hands-On Exercises
Explain why Django's ModelForm avoids a duplication that FastAPI's own Pydantic-schema approach accepts, tying your answer back to Chapter 2's own model definition.
📄 View solutionExplain what commit=False actually does, and why status is set directly in the view rather than being included as a field in ItemForm itself.
📄 View solutionExplain exactly what happens if {% csrf_token %} is omitted from this form's template, and why this chapter treats Django's default CSRF behavior as a real security feature rather than boilerplate to work around.
📄 View solutionChapter 6 Quick Reference
ModelForm— derives fields and validation from Chapter 2's model directly; no separate schema to duplicate- One view, GET and POST — a single function handles both, unlike a typical split REST endpoint pair
commit=False— set fields the user shouldn't control (likestatus) before the actual save{% csrf_token %}— required for every POST form; omitting it means a 403, by design, not a bug- ModelForm vs. DRF serializers — a real, honest fork; this chapter's approach is complete on its own terms, not a placeholder
- Next chapter: Expiry Alerts
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
expiry_date__lte is Django's own field-lookup syntax — a double-underscore suffix mapping directly to a SQL comparison (__lte → <=, __gte → >=, __contains → LIKE, 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.
The Template
{% 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.
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).
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
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 solutionExplain 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 solutionExplain 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 solutionChapter 7 Quick Reference
__lte— Django's own double-underscore field-lookup syntax, vs. SQLAlchemy's operator overloadingtimezone.now()— always, notdatetime.now(), onceUSE_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
Item History & Live Search
Food Tracker (Django)
Chapter 8 · Item History & Live Search-as-You-Type
Every item ever added lives in one list — active items with a real expiry date, used items without one, exactly as Chapter 2 modeled them. This chapter makes that combined history searchable in real time, and lands on a genuinely different architecture than this app's Firebase-based sibling course did.
The History View
A Thin JSON Endpoint — Deliberately Not DRF Yet
Live search genuinely needs the frontend to ask the server "what matches this text?" repeatedly, without a full page reload per keystroke — exactly the kind of interactive feature Chapter 6 flagged as DRF's own eventual territory. But standing up Django REST Framework's full machinery for one simple, read-only endpoint would be premature. This chapter takes the honest middle path: a single minimal view returning JsonResponse directly, no serializer class involved. DRF's real payoff (Chapter 11) arrives once several endpoints and genuinely richer validation actually justify it — not before.
name__icontains is a real, native, case-insensitive substring match, built directly into Django's ORM — one line, no workaround, no shadow field. safe=False is required because JsonResponse defaults to expecting a dict (a historical safeguard against a JSON-hijacking risk that applied to older browsers when a bare array was the top-level response); returning a plain list means opting out of that default explicitly.
A Genuinely Fair, Direct Contrast
Food Tracker (React + Firebase)'s own Chapter 8 needed real, honest work to get case-insensitive, substring-anywhere matching at all — Firestore has no equivalent of SQL's LIKE, so that course built a nameLower shadow field and ultimately chose client-side filtering specifically to get genuine mid-word matching. Here, on a real relational database, icontains already does exactly that, natively, in the query itself. This is a genuine SQL-side advantage worth crediting plainly, the same way Chapter 7 already credited SQL honestly for not needing a composite index.
The Frontend Genuinely Needs Debouncing This Time
This is the one place this course's own approach is more expensive per keystroke than the Firebase course's own choice, and worth being precise about why: this view hits the real database on every single request, rather than filtering an already-loaded, small array in memory. Debouncing — waiting for typing to pause before actually firing the request — genuinely matters here, unlike the Firebase course's own chosen approach, which loaded the full history once and needed no debounce at all because filtering afterward never touched the network again.
icontains rather than needing a workaround field at all. Neither is the universally "right" search architecture — each is the correct answer to a different actual constraint.
/search/?q=chedd in a browser returns the raw JSON — confirming the query itself works before writing a single line of the debounced frontend JS.
[:20] limits the response size, not the query costLIKE '%...%' query (which is what icontains compiles to) generally can't use a standard B-tree index efficiently regardless of how few rows are ultimately returned — the database may still have to scan a large portion of the table to find those 20 matches. At real scale, a genuine full-text or trigram search feature (PostgreSQL's own trigram extension, for instance) would be the actual fix, not a smaller result slice.
Where This Course Is Headed
Marking items used, recipe lookup, Django REST Framework — now genuinely justified by more than one interactive endpoint — deployment, and a capstone.
Hands-On Exercises
Explain what name__icontains gives this course for free, and what workaround the Firebase sibling course's own Chapter 8 needed to build to get roughly the same capability.
📄 View solutionExplain why this course's search needs debouncing while the Firebase sibling course's own chosen search approach didn't. What's the actual underlying difference in where the filtering happens?
📄 View solutionExplain why slicing the queryset to [:20] doesn't actually solve the performance concern with a LIKE '%...%' query, and what this chapter names as the real fix at genuine scale.
📄 View solutionChapter 8 Quick Reference
name__icontains— free, native, case-insensitive substring search; no shadow field needed, unlike the Firebase sibling course- A thin
JsonResponseview — deliberately not DRF yet; one endpoint doesn't justify the full framework safe=False— required to return a plain JSON list rather than a dict- Debouncing genuinely needed here — every search hits the real database, unlike the Firebase course's own load-once-filter-in-memory approach
[:20]limits the response, not the query cost — a leading-wildcardLIKEcan't use a standard index regardless- Next chapter: Marking Items Used
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
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.
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:
get_object_or_404 everywhere a lookup can failGET — genuinely different from a cosmetic style preference, since it can cause real, unintended state changes with no user action actually behind them.
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
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 solutionExplain 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 solutionExplain 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 solutionChapter 9 Quick Reference
expiry_date = None— sufficient here; schema-on-write guarantees one consistent shape for "no expiry" regardless of historyget_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
Recipe Lookup with TheMealDB
Food Tracker (Django)
Chapter 10 · Recipe Lookup with TheMealDB
This chapter delivers the last named feature from Chapter 1's original spec, reusing the same integration pattern Chapter 4 already established — and running into a real, honest limitation specific to how this course's views are built.
The Same Limitation, Regardless of Framework
TheMealDB's filter-by-ingredient endpoint searches exactly one ingredient per request — this isn't a Django, FastAPI, or Firebase-specific constraint at all, it's simply a property of the external API itself, identical across all four Food Tracker courses. Every one of them has to fan out one request per expiring ingredient and merge the results afterward.
A View That Fans Out
A Real Cost of This Sequential Loop
Each requests.get() call here runs one after another, and this ordinary Django view blocks entirely until every single one finishes — five expiring ingredients means roughly five times the wait of firing them all at once. This is a genuine, honest cost specific to how this view is written: Food Tracker (FastAPI)'s own async patterns and Food Tracker (React + Firebase)'s Promise.all() both fire every ingredient lookup concurrently instead. Django does support async views (async def, since Django 3.1), but doing this properly would also require swapping the synchronous requests library for an async-compatible HTTP client — real additional complexity genuinely beyond this chapter's own scope, named honestly rather than glossed over.
Relevance Sorting — The One Place All Four Courses Converge
Sorting by len(matched_ingredients) descending surfaces recipes using the most expiring items first — directly serving Chapter 1's original point: using up as much soon-to-expire food as possible in one meal. This particular piece of logic is functionally identical across all four Food Tracker courses, each expressed in its own language's idioms — a genuine convergence point after several chapters of real, honest divergence.
Caching, Same Reasoning, a New Kind of Column
JSONField (native since Django 3.1, backed by real JSON column support in modern PostgreSQL, MySQL, and SQLite) is a genuinely interesting nuance worth naming directly: even a relational, schema-on-write framework has its own escape hatch for storing semi-structured, document-like data in a single column, when that's honestly the better fit — a list of matched-meal dictionaries here doesn't cleanly decompose into further normalized tables for this app's own modest needs. Relational and document approaches aren't a strict either/or; a real relational database can hold document-shaped data exactly where that's the more sensible choice.
JSONField is worth knowing about generallyWhere This Course Is Headed
Django REST Framework, deployment, and a capstone.
Hands-On Exercises
Explain why this view takes roughly N times as long as necessary for N expiring ingredients, and what the FastAPI and Firebase sibling courses do differently to avoid this specific cost.
📄 View solutionExplain how the matched_ingredients sort serves Chapter 1's original recipe-lookup intent, and why this chapter calls this one piece of logic essentially identical across all four Food Tracker courses.
📄 View solutionExplain what JSONField is, and why using it for RecipeCache doesn't actually contradict Django's own relational, schema-on-write identity.
📄 View solutionChapter 10 Quick Reference
- One ingredient per request — TheMealDB's own limitation, identical across all four courses
- Sequential, blocking loop — a real, honest cost; FastAPI's async and Firebase's
Promise.all()both avoid it by running concurrently matched_ingredientssort — the one piece of logic functionally identical across the whole quartetJSONField— Django's own escape hatch for document-shaped data inside a relational schema- Next chapter: Django REST Framework: Exposing an API Layer
Django REST Framework
Food Tracker (Django)
Chapter 11 · Django REST Framework: Exposing an API Layer
Chapter 8 deliberately avoided DRF for one simple search endpoint. By now there are four genuinely JSON-shaped features — search, add-item, mark-used, recipe lookup — plus Chapter 5's own full-page-reload cost still unresolved. That's the actual threshold this chapter waits for.
Installing DRF
A Serializer Isn't a Second ModelForm
Like Chapter 6's ModelForm, ModelSerializer derives its fields directly from the same Item model — no separate schema class to duplicate. But it's built for JSON in, JSON out, with no HTML rendering assumption at all. And read_only_fields works differently from Chapter 6's own commit=False pattern: commit=False lets the view set server-controlled fields after the form validates but before saving; read_only_fields instead tells the serializer itself to include a field in output (a GET response) while silently ignoring any attempt to set it via input — a genuinely different mechanism arriving at a similarly-shaped safety outcome, not the identical technique wearing a new name.
ViewSets and Routers: The Pattern Repeats, One Level Up
One ViewSet class, registered with a router, auto-generates a full set of RESTful endpoints — list, create, retrieve, update, delete — with no individual view functions written by hand for any of them. It's Django's own "batteries included" philosophy repeating itself one level deeper, this time inside DRF specifically.
The Honest Hybrid Shape
This chapter doesn't throw away Chapters 3 through 10 and replace everything with DRF. The admin (Chapter 3) stays exactly what it was. The dashboard and history pages could keep their server-rendered templates for the parts that don't need deep interactivity, while search, add-item, mark-used, and recipe lookup can genuinely migrate to these new /api/ endpoints, called via fetch() instead of a full-page form POST or redirect. Some pages stay templates; some features become a real JSON API. This hybrid shape is the honest, common reality most substantial Django applications actually settle into — not a failure to fully commit to one philosophy.
/api/items/ in an ordinary browser renders a genuinely pleasant, interactive HTML exploration UI for the JSON API itself — automatically, with zero extra code required to get it.
fetch() a DRF endpoint directly from the browser doesn't exempt that call from Django's own CSRF protection — a same-origin, session-authenticated browser request still needs the CSRF token included in the request's own headers for any unsafe method (POST/PUT/DELETE). Forgetting it produces the exact same 403 rejection Chapter 6 already covered for a template form, just now from JavaScript instead of an HTML <form>.
Where This Course Is Headed
Deployment, and a capstone tying every chapter together into one complete, working app.
Hands-On Exercises
Explain why this chapter is genuinely the right point to introduce DRF, tying your answer back to Chapter 8's own reasoning for avoiding it. What specifically changed between Chapter 8 and now?
📄 View solutionExplain the difference between Chapter 6's commit=False pattern and this chapter's read_only_fields. Both prevent user control of a field — how do they actually achieve that differently?
📄 View solutionExplain why switching Chapter 5's scan flow from a template form POST to a fetch() call against a DRF endpoint doesn't bypass Django's CSRF protection, and what would happen if the CSRF token were left out of that fetch() call.
📄 View solutionChapter 11 Quick Reference
ModelSerializer— derives from the same model asModelForm, but for JSON, not HTMLread_only_fieldsvs.commit=False— genuinely different mechanisms, similar safety outcomeViewSet+ router — one class auto-generates a full RESTful endpoint set- The hybrid shape — templates and a JSON API coexisting is the honest norm, not a compromise
- CSRF still applies to same-origin
fetch()calls — the token must be sent explicitly in headers - Next chapter: Deployment
Deployment
Food Tracker (Django)
Chapter 12 · Deployment
Every earlier chapter's "batteries included" moments were real. This one is where that story honestly stops covering everything.
DEBUG=False: The Single Most Important Flag
Django's development default, DEBUG=True, shows full, detailed error pages — stack traces, local variable values, even source snippets. Genuinely useful while developing; genuinely dangerous left on in production, since it hands anyone who triggers an error a real window into secrets, internal paths, and database structure. DEBUG=False swaps that for a generic error page instead — but it also activates a real requirement that trips up nearly every first-time Django deployment:
With DEBUG=False, Django refuses to serve any request at all if its Host header doesn't match something in ALLOWED_HOSTS — everything that worked perfectly with runserver locally can appear completely broken the moment DEBUG flips to False, purely because ALLOWED_HOSTS was never configured for the real domain.
SECRET_KEY: This Course's First Genuine Secret
Every external API this course has touched — Open Food Facts, TheMealDB — never needed a real secret at all. Django's own SECRET_KEY is different: it signs sessions, CSRF tokens (Chapter 6's own protection depends on it), and password-reset tokens. If it leaks, an attacker can forge valid session cookies directly. It must be loaded from an environment variable, never hardcoded or committed to source control — the one place in this entire course where an actual secret genuinely exists, and it belongs to Django itself, not to anything this app integrates with.
Static Files vs. Media Files
Static files are the app's own assets — CSS, and Chapter 5's own scan.js. Media files would be user-uploaded content (this app has none currently, but the distinction is worth knowing since it's a common point of confusion). python manage.py collectstatic gathers every app's static files into one directory for production serving — a step Django's own dev server never required, since it serves static files automatically during development without it.
Django itself explicitly does not serve static files efficiently in production. A real deployment needs either a dedicated web server (nginx) in front, or a library like WhiteNoise letting the Django app serve them itself reasonably well — a genuine decision point this chapter names rather than glosses over.
SQLite in Production: An Honest, Calibrated Note
Chapter 2 called SQLite the genuine, appropriate choice for this app, not a placeholder — and for this app's own realistically small, personal-use scope, that can remain true in production too. The honest caveat: SQLite's own file-level locking becomes a real concurrency bottleneck under genuine multi-user, high-write-volume load. If this were ever a shared, heavily-used deployment, PostgreSQL is the standard upgrade path — not because SQLite is "never production-ready" (a common overstatement), but because its real limits are concurrency-shaped, and worth knowing about honestly rather than either dismissing or overselling.
Running It For Real
Chapter 1's manage.py runserver was always explicitly a development-only server — Django's own documentation says directly not to use it in production, since it offers neither real concurrency nor production security hardening. Gunicorn (or another genuine WSGI server) is what actually serves production traffic.
python manage.py check --deploy scans the current settings for common production misconfigurations — DEBUG still True, missing SECURE_* settings, and more — worth running before trusting a deployment is actually ready.
DEBUG to False without also setting ALLOWED_HOSTS correctly makes the entire site appear to stop working — every request gets rejected outright, with no obvious explanation unless the connection between these two settings is already understood.
Where This Course Is Headed
One chapter left: a capstone tying together this course's own thread — from Chapter 1's batteries-included framing through this chapter's own real production considerations — into one complete, working app.
Hands-On Exercises
Explain why DEBUG=True is appropriate in development but a real security risk in production, and explain exactly what happens (and why) once DEBUG=False if ALLOWED_HOSTS hasn't been configured for the real domain.
📄 View solutionExplain why SECRET_KEY is a genuine secret in a way Open Food Facts and TheMealDB never required, and name at least two things it's actually used to protect.
📄 View solutionExplain why manage.py runserver and SQLite were both genuinely appropriate choices for earlier chapters, and why deployment is specifically where each one's own real limits actually become visible.
📄 View solutionChapter 12 Quick Reference
DEBUG=False+ALLOWED_HOSTS— the classic first-deployment mistake if not set togetherSECRET_KEY— this course's first genuine secret; signs sessions, CSRF tokens, password resetscollectstatic— a real production-only step; the dev server never needed it- SQLite in production — genuinely fine for this app's own scope; PostgreSQL is the real upgrade path for concurrency, not a blanket "never production-ready" rule
- Gunicorn, not
runserver—runserverwas always explicitly dev-only - Next chapter: Capstone — A Complete, Working Food Tracker
Capstone: A Complete, Working Food Tracker
Food Tracker (Django)
Chapter 13 · Capstone: A Complete, Working Food Tracker
Aisha manages this deployment day to day. Every step below is a real, working feature, each one built in a specific earlier chapter.
Before a single real user touches the app, Aisha logs into /admin/ and adds a couple of test items by hand — Chapter 1's own promise, delivered concretely by Chapter 3's two-line admin.site.register(Item), exercised here exactly as intended: real CRUD, before any custom view existed.
A user scans a yogurt carton's barcode. Chapter 5's scan.js decodes it and navigates to Chapter 4's lookup view, which checks BarcodeCache, finds nothing, queries Open Food Facts, and caches the result. The confirm-and-save form (Chapter 6) pre-fills name and category; commit=False sets status="active" before the actual save.
The real database row reflects every decision Chapter 2 made: name, barcode, category="dairy", status="active", a real expiry_date, and added_at set automatically by auto_now_add — never trusting a client-supplied value for that field.
A few days later, Chapter 7's dashboard query — status="active", expiry_date__lte a timezone.now()-based threshold — surfaces the item as expiring soon, with no composite-index drama at all, exactly the fair SQL advantage that chapter named.
Wanting to buy more of something bought before, a user types "yog". Chapter 8's debounced JS hits /search/, which uses name__icontains — genuine, native, case-insensitive substring matching, no shadow field ever needed — finding "Greek Yogurt" instantly.
The yogurt gets finished. A POST-only "Mark Used" button — never a plain GET link, per Chapter 9's own correctness point — sets status="used", stamps used_at, and sets expiry_date = None. The item stays in history forever, exactly as Chapter 2 designed, just without an expiry date attached anymore.
With chicken and eggs both nearing expiry, Chapter 10's suggest_recipes view fans out one request per ingredient to TheMealDB — sequentially, with the honest performance cost that chapter named — merges the results, and sorts by matched_ingredients count, surfacing a recipe using both chicken and eggs above one using only either alone.
By now, the team has followed Chapter 11's own hybrid path: the admin and dashboard stayed exactly as they were, but search, add-item, mark-used, and recipe lookup moved behind real DRF ViewSets, called via fetch() with the CSRF token included correctly. Chapter 5's own full-page-reload limitation — named honestly back when it was first built — is genuinely resolved here, not by rewriting that chapter's logic, but by Chapter 11 giving the app somewhere better to send its requests.
The whole session happens on a real deployment: DEBUG=False with ALLOWED_HOSTS set correctly, SECRET_KEY loaded from the environment, static files collected and served, gunicorn handling real traffic — SQLite still genuinely appropriate at this app's own realistic scale, exactly as Chapter 12 concluded.
Chapter Attribution
| Step | Chapter(s) applied |
|---|---|
| 1 — Admin seeding | Chapter 1 (batteries included), Chapter 3 (admin registration) |
| 2 — Scanning an item | Chapter 5 (scanning), Chapter 4 (lookup + cache), Chapter 6 (add-item form, commit=False) |
| 3 — The stored row | Chapter 2 (model design, auto_now_add) |
| 4 — Expiry alert | Chapter 7 (queryset, timezone.now()) |
| 5 — Search | Chapter 8 (icontains, debounced JS) |
| 6 — Marking used | Chapter 9 (expiry_date=None, POST-only action) |
| 7 — Recipe suggestion | Chapter 10 (fan-out, relevance sort, JSONField cache) |
| 8 — Faster interactions | Chapter 11 (DRF, resolving Chapter 5's own limitation) |
| 9 — Real production | Chapter 12 (deployment) |
django.contrib.auth is genuinely available and easy to add, but this course never actually wired it in, so every Item row remains ownerless, unlike the Firebase sibling course's own Chapter 11; Chapter 10's own sequential recipe-lookup performance cost was named honestly but never actually fixed; and no automated test suite or CI pipeline was covered anywhere in this course. Each is a genuine, reasonable next step — none were quietly assumed to already be done.
Hands-On Exercises
Explain how Step 8 resolves Chapter 5's own full-page-reload limitation without rewriting that chapter's own scanning logic. What specifically changed to make this possible?
📄 View solutionPick any two steps from Aisha's session and explain how each one depends on at least two earlier chapters working together, not just one chapter in isolation.
📄 View solutionExplain why the honest scope note specifically calls out the lack of a multi-user ownership model as a genuine gap, rather than assuming Django's built-in django.contrib.auth makes this a non-issue.
📄 View solutionChapter 13 Quick Reference — Course Complete
- 9 steps, 12 prior chapters — one continuous, realistic session with the finished, deployed app
- This course's own throughline, closed out: batteries included, one integrated framework deciding more up front than FastAPI's thin-layer approach
- Honest scope note: no meal planner, no offline/PWA, no multi-user ownership model despite
django.contrib.authbeing available, no fix for Chapter 10's own sequential recipe lookup, no automated tests/CI - Food Tracker (Django) is now complete — 13/13 chapters