Building the Add-Item Flow with Django Forms

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

# pantry/forms.py from django import forms from .models import Item class ItemForm(forms.ModelForm): class Meta: model = Item fields = ["name", "barcode", "category", "expiry_date"] widgets = { "expiry_date": forms.DateInput(attrs={"type": "date"}), }

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

def add_item(request, name="", category=""): if request.method == "POST": form = ItemForm(request.POST) if form.is_valid(): item = form.save(commit=False) item.status = "active" item.save() return redirect("item_list") else: form = ItemForm(initial={"name": name, "category": category}) return render(request, "pantry/add_item.html", {"form": form})

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

{% extends "pantry/base.html" %} {% block content %} <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Save</button> </form> {% endblock %}

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.

The same theme, now very concrete
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.
Validation errors render themselves
{{ 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.
A missing csrf_token isn't a cosmetic omission
Forgetting {% 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

Exercise 1

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 solution
Exercise 2

Explain 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 solution
Exercise 3

Explain 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 solution

Chapter 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 (like status) 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