2 · Data and boundaries · 30 MIN
Django forms, CSRF and server validation
A form validates untrusted fields before domain work begins.
Django forms turn raw input into cleaned_data only after validation succeeds. Bound the length and range in the form rather than trusting HTML attributes. A rendered POST form should include csrf_token and keep CsrfViewMiddleware enabled. CSRF protection does not replace authentication or record ownership checks. This example previews cleaned values and deliberately performs no database write, making validation and method behavior easy to test before introducing persistence.
Keep valid fields visible after another field fails.
Read the example
# courses/forms.py
from django import forms
class PlanForm(forms.Form):
title = forms.CharField(min_length=3, max_length=80)
weekly_goal = forms.IntegerField(min_value=1, max_value=20)
# courses/views.py
from django.shortcuts import render
from .forms import PlanForm
def plan(request):
form = PlanForm(request.POST if request.method == "POST" else None)
preview = form.cleaned_data if request.method == "POST" and form.is_valid() else None
return render(request, "courses/plan.html", {"form": form, "preview": preview})
# courses/templates/courses/plan.html
# <form method="post">{% csrf_token %}{{ form.as_p }}<button>Preview</button></form>
# {% if preview %}<p>{{ preview.title }}: {{ preview.weekly_goal }}</p>{% endif %}Check the expected output
The form shows field errors for invalid input and a preview for a valid title and goal; a CSRF-less POST is rejected when middleware checks apply.
Your challenge
Register the form route, retain CSRF middleware and test valid, invalid and missing-token submissions before adding authenticated persistence.
Solution cost: O(f + m) field and string validation. time · O(f + m) bound form data. space
Common trap
Django’s default test client relaxes CSRF checks unless explicitly enabled.
Study the project implementation
# courses/forms.py
from django import forms
class PlanForm(forms.Form):
title = forms.CharField(min_length=3, max_length=80)
weekly_goal = forms.IntegerField(min_value=1, max_value=20)
# courses/views.py
from django.shortcuts import render
from .forms import PlanForm
def plan(request):
form = PlanForm(request.POST if request.method == "POST" else None)
preview = form.cleaned_data if request.method == "POST" and form.is_valid() else None
return render(request, "courses/plan.html", {"form": form, "preview": preview})
# courses/templates/courses/plan.html
# <form method="post">{% csrf_token %}{{ form.as_p }}<button>Preview</button></form>
# {% if preview %}<p>{{ preview.title }}: {{ preview.weekly_goal }}</p>{% endif %}Further reading: Official documentation
Next lesson: FastAPI request and response models →