Dynamic Content & Forms

Website Rebuild with Django

Chapter 8 · Dynamic Content & Forms

Website Rebuild with Next.js 8 gave the admin a way to actually change something — updatePageTitle, a Server Action, with no auth check yet, deliberately: the security gap gets closed properly in that course's own Chapter 9. This chapter builds the identical feature for this rebuild — the same page title, the same deliberate gap, closed the same way in this course's own Chapter 9 — but through a mechanism with no real equivalent in Next.js: three separate, explicit pieces, rather than one function.

Django Has No Single "Server Actions" Equivalent

A Next.js Server Action is a function that runs on the server, callable seemingly directly from a form — the client/server boundary is deliberately blurred, hidden behind what looks like an ordinary function call. Django keeps that boundary fully explicit instead: a real HTTP POST, to a real URL, handled by a real view function, validated by a real form class. Nothing about a Django mutation is hidden — every piece can be read, in isolation, in its own file.

The Form: ModelForm

# content/forms.py from django import forms from .models import Page class PageTitleForm(forms.ModelForm): class Meta: model = Page fields = ['title']

A ModelForm derives its fields and validation directly from the model itself — Chapter 2's own Page definition — the same "batteries-included, no separate schema to maintain" theme running since Chapter 1's INSTALLED_APPS.

The View

# content/views.py from django.shortcuts import redirect, get_object_or_404 from .forms import PageTitleForm from .models import Page def update_page_title(request, full_path): page = get_object_or_404(Page, full_path=full_path.rstrip('/')) if request.method == 'POST': form = PageTitleForm(request.POST, instance=page) if form.is_valid(): form.save() return redirect('page_detail', full_path=full_path)
No permission check yet — deliberately, not an oversight
Anyone who knows or guesses this URL can currently rename any page — there's no login requirement anywhere in this view. That's the exact same gap Next.js Rebuild 8 left open on purpose in its own updatePageTitle Server Action, and it gets closed the same way here: Chapter 9's admin authentication work is what makes leaving it open right now the correct order to build in, not a mistake to fix immediately.

The Template: {% csrf_token %}

<form method="post" action="{% url 'update_page_title' full_path=page.full_path %}"> {% csrf_token %} {{ form.as_p }} <button type="submit">Save</button> </form>

{{ form.as_p }} renders the whole ModelForm's fields automatically — one line, no manual <input> tags to keep in sync with the model.

Forgetting {% csrf_token %} isn't a security hole — it's a broken form
Django protects every POST request with CSRF verification by default, at the framework level, whether the template remembers to include a token or not. Leave {% csrf_token %} out of a form, and the submission is simply rejected — a 403 Forbidden, every time, no exceptions. This is one of the single most common "why won't my form submit" confusions for anyone new to Django: the form looks completely correct, submits fine in isolation, and still fails, because the one hidden field the framework actually requires was never rendered.

Next.js Server Actions vs. Django's Explicit Three Pieces

Next.jsDjango
The mutation itselfOne function, marked to run on the serverA view function, a ModelForm, and a URL — three separate, explicit pieces
Client/server boundaryDeliberately blurred — looks like calling a normal functionFully explicit — a real HTTP POST to a real, inspectable URL
CSRF-equivalent protectionBuilt into the framework's own Server Action handlingBuilt into Django, but requires an explicit {% csrf_token %} in the template
The central fact this chapter is built on
Django's version is genuinely more code than a single Next.js Server Action — and that's the tradeoff, not a downside to smooth over. Every mutation stays fully visible: a real URL you can find in urls.py, a real view you can read top to bottom, a real form whose validation rules live in one place. This is the same explicit-over-implicit theme that's run through this entire course, from Chapter 1's project/app split through Chapter 3's own routing — Django keeps choosing to show its work rather than hide it behind a more convenient abstraction.

Hands-On Exercises

Exercise 1

Explain why update_page_title has no permission or login check yet, and why this chapter treats that as a deliberate, temporary gap rather than an oversight.

📄 View solution
Exercise 2

Explain what happens if {% csrf_token %} is left out of the form template, and why this happens even though the form otherwise looks completely correct.

📄 View solution
Exercise 3

Explain the real difference between how a Next.js Server Action handles a mutation and how this chapter's own view + ModelForm + URL combination handles the identical kind of mutation.

📄 View solution

Chapter 8 Quick Reference

  • No "Server Actions" equivalent — Django keeps the client/server boundary fully explicit instead of blurring it
  • forms.ModelForm — auto-derives fields and validation directly from a model
  • A real view, a real URL, a real form — three separate, explicit, individually-readable pieces per mutation
  • No auth check yet, deliberately — the same gap Next.js Rebuild 8 left open, closed the same way in this course's own Chapter 9
  • {% csrf_token %} — required inside every POST form; omitting it means a guaranteed 403, not a subtle bug
  • Explicit over implicit — more code than a Server Action, but every piece stays visible and independently readable
  • Next chapter: Admin Authentication