Unit 4: Forms in Django
I. Orientation: Forms as the Boundary Between Browser and Server
A form is the HTML mechanism by which a browser collects user input and submits it to a server; Django wraps that mechanism in a Python class layer (the django.forms package, present since Django 0.96 as "newforms") so that rendering, validation and cleaning happen in one place. Everything in this unit rests on a single idea: untrusted data arrives from the network, and the form is the gate that converts it into trusted Python objects.
Defining properties and conventions assumed throughout:
- Three responsibilities of a Django form: render HTML widgets, validate submitted data, and convert strings into Python types (
"2024-01-05"→datetime.date(2024, 1, 5)). - All incoming data is strings: HTTP transmits text, so
request.POST['age']is'25', never25. Type coercion is the form's job. - Bound vs unbound:
MyForm()is unbound (no data, no errors);MyForm(request.POST)is bound and can be validated.form.is_boundreports which. is_valid()is the trigger: it runs the full cleaning pipeline and populatesform.cleaned_data(valid values) andform.errors(anErrorDict).- Never trust client-side validation: HTML5
requiredortype="email"is a convenience only; an attacker can bypass it withcurl. - Template convention: every form that submits to Django with method POST must contain
{% csrf_token %}.
II. Introduction to Forms — the HTML substrate and Django's abstraction
A. Anatomy of an HTML form
A form is defined by three attributes and a set of named controls.
action: target URL. Empty (action="") means submit to the current URL, common in Django views that handle both GET and POST.method:getorpost, controlling where data is placed in the request.nameattribute: the dictionary key on the server.<input name="email">becomesrequest.POST['email'].- Widget types:
text,password,checkbox,radio,select,hidden,file. A file upload additionally requiresenctype="multipart/form-data".
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>B. Why Django abstracts forms
- Elimination of duplication: without a
Formclass, field names appear in the template, the view's validation code and the error messages — three places to keep in sync. - Single source of truth: the field declaration
age = forms.IntegerField(min_value=18)generates the widget, the coercion and the error message together. - Two families:
forms.Formfor arbitrary data (a search box, a contact form) andforms.ModelFormfor data backed by a model, which infers fields from the model definition.
III. Using GET, POST and HTTP — request semantics and the HttpRequest object
A. The HTTP request/response cycle
Django's view receives an HttpRequest and must return an HttpResponse; the method verb determines how form data reaches it.
- Request line:
GET /search/?q=django HTTP/1.1— verb, path, query string, protocol version. - Key
HttpRequestattributes:request.method('GET','POST'),request.GETandrequest.POST(bothQueryDictobjects),request.FILES,request.body(raw bytes),request.META(headers). QueryDictis immutable: it is a dict-like object allowing repeated keys; userequest.GET.getlist('tag')for?tag=a&tag=b, andrequest.GET.get('q', '')to avoidMultiValueDictKeyError.
B. GET versus POST contrasted
- GET: parameters are appended to the URL as a query string after
?, e.g./search/?q=django&page=2.- Semantics: safe and idempotent — it should only read state, never change it.
- Practical limits: URL length capped by servers/browsers (commonly ~2000 characters); values are visible, logged by proxies, stored in browser history, and bookmarkable.
- Correct uses: search queries, filters, pagination — anything a user should be able to share as a link.
- POST: parameters are placed in the request body, encoded as
application/x-www-form-urlencoded(ormultipart/form-data).- Semantics: non-idempotent — submitting twice creates two records.
- Practical limits: no meaningful size limit; body is not visible in the URL, though it is not encrypted — only HTTPS provides confidentiality.
- Correct uses: creating, updating or deleting data, logins, uploads.
- Verb-branching view pattern:
if request.method == 'POST': ... else: ...is the canonical Django structure for a page that both displays and accepts a form.
C. Other verbs in passing
GET/POSTonly from HTML forms: browsers cannot issuePUT,PATCHorDELETEfrom a plain<form>; those verbs reach Django via JavaScriptfetchor REST clients, whererequest.POSTis empty andrequest.bodymust be parsed.
IV. Building Forms Using Django — declarative fields, widgets, rendering
A. Declaring a form class
A form is a class whose attributes are field instances; declaration order becomes rendering order.
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
email = forms.EmailField(required=False, label='Your e-mail')
message = forms.CharField(widget=forms.Textarea)- Common fields:
CharField,EmailField,IntegerField,DecimalField,BooleanField,DateField,ChoiceField(choices=...),FileField,ModelChoiceField(queryset=...). - Shared arguments:
required(defaultTrue),label,initial,help_text,widget,validators,error_messages. - Field vs widget: the field handles validation and Python type; the widget handles HTML.
forms.CharField(widget=forms.PasswordInput)keeps the string validation but renders<input type="password">.
B. Rendering in templates
- Django renders fields, not the
<form>tag: you always write<form method="post">,{% csrf_token %}and the submit button yourself. - Whole-form helpers:
{{ form.as_p }},{{ form.as_div }}(Django 4.1+, now the default{{ form }}),{{ form.as_table }},{{ form.as_ul }}. - Manual control: loop for per-field markup.
<form method="post">{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field.errors }}{{ field.label_tag }}{{ field }}
<small>{{ field.help_text }}</small>
{% endfor %}
<button type="submit">Send</button>
</form>C. ModelForm
- Purpose: avoid restating model fields in the form.
- Declaration: an inner
class Metanamingmodelandfields(use an explicit list, orexclude;fields = '__all__'is convenient but risks exposing fields). form.save(): writes to the database and returns the instance;save(commit=False)returns an unsaved instance so you can setobj.author = request.userbefore saving.
V. Introduction to Cross Site Request Forgery (CSRF)
A. The attack mechanism
CSRF (also "session riding", XSRF) tricks a logged-in user's browser into issuing an unwanted state-changing request to a site that trusts it.
- Root cause: browsers attach cookies automatically to any request to a domain, regardless of which page initiated it — so the session cookie authenticates the forged request.
- Attack chain: the victim is authenticated at
bank.com; they visitevil.com, which contains a hidden auto-submitting form pointing atbank.com/transfer/; the browser sends it with the victim's cookies; the transfer succeeds.
<!-- hosted on evil.com -->
<form action="https://bank.com/transfer/" method="post" id="f">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('f').submit();</script>- What the attacker does not need: the ability to read the response. CSRF is a write attack, unlike XSS which can read data.
- Why GET must be safe: if
/delete/?id=5mutates data, a mere<img src="...">on any site triggers it.
B. Defence principle
- Secret-token requirement: demand proof that the request came from a page the server itself served, via a token the attacker cannot read (blocked by the same-origin policy).
- Complementary defences:
SameSite=Laxcookies (Django's default) stop cookies on cross-site POSTs, andOrigin/Refererchecking on HTTPS.
VI. CSRF Support in Django
A. The middleware and the token
django.middleware.csrf.CsrfViewMiddleware: enabled by default inMIDDLEWARE; it rejects anyPOST,PUT,PATCHorDELETElacking a valid token with HTTP 403 Forbidden ("CSRF verification failed").{% csrf_token %}: renders a hidden input,<input type="hidden" name="csrfmiddlewaretoken" value="...">; the middleware compares it against the value in thecsrftokencookie (masked per-request to defeat BREACH).RequestContextrequirement: the tag only works when the response is rendered with a request-aware context, i.e.render(request, ...).- AJAX: send the token in the
X-CSRFTokenheader, read from the cookie or from{{ csrf_token }}.
B. Decorators and exemptions
@csrf_exempt: disables the check for one view — used only for endpoints authenticated by another means (e.g. a signed webhook).@csrf_protect: enforces the check on a single view when the middleware is disabled.@requires_csrf_token: ensures the token is available in an error view.@ensure_csrf_cookie: forces the cookie to be set on a GET, needed when a JavaScript app POSTs without ever rendering a Django form.- Related settings:
CSRF_COOKIE_SECURE,CSRF_COOKIE_HTTPONLY, andCSRF_TRUSTED_ORIGINS(required for HTTPS deployments behind a proxy or on a different origin).
VII. Implementing POST Redirect in Django — the POST/Redirect/GET pattern
A. The problem and the pattern
Rendering a template directly in response to a POST leaves the browser's last request as a POST, so refreshing (F5) or pressing Back re-submits it and duplicates the record.
- Rule: on successful POST, return a redirect (HTTP 302, or 303 See Other) so the browser's final request is an idempotent GET.
- On failure: re-render the template with the bound form so errors and the user's typed values are preserved — do not redirect.
from django.shortcuts import render, redirect
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
form.save()
return redirect('contact_thanks') # 302 → GET
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})- Redirect helpers:
redirect()accepts a URL name (reversed), a model instance (viaget_absolute_url()) or a literal path;HttpResponseRedirectis the underlying 302 class,HttpResponsePermanentRedirectthe 301. - Carrying feedback across the redirect: the
messagesframework,messages.success(request, 'Saved.'), since the new GET is a fresh request with no context.
VIII. Data Validation with Django Forms — the cleaning pipeline
A. Order of validation
is_valid() runs full_clean(), which processes each field and then the form as a whole; any raised ValidationError is collected rather than propagated.
- Per field:
to_python()coerces the string →validate()checks emptiness/type →run_validators()applies thevalidatorslist →clean_<fieldname>()if defined. - Whole form:
clean()runs last, for rules spanning two fields. - Outputs:
form.cleaned_data['email']for valid values;form.errors['email'](a list) andform.non_field_errors()forclean()errors.
B. Layers of validation
- Field arguments and built-in validators:
max_length,min_value,EmailValidator,RegexValidator,MinLengthValidator— declarative, reusable, and the first choice. - Custom methods: used when the rule needs a database lookup or another field's value.
def clean_username(self):
name = self.cleaned_data['username']
if User.objects.filter(username__iexact=name).exists():
raise forms.ValidationError('Username already taken.')
return name # must return the cleaned value
def clean(self):
cleaned = super().clean()
if cleaned.get('password') != cleaned.get('confirm'):
raise forms.ValidationError('Passwords do not match.')
return cleaned- Key contract:
clean_<field>()must return the value, otherwisecleaned_dataloses it. - In
clean()use.get(): a field that failed its own validation is absent fromcleaned_data, socleaned['password']would raiseKeyError. - Error codes and localisation:
ValidationError('Too small', code='min_value', params={'limit': 18})supports translation and programmatic handling.
C. Applications and limitations
- Form validation is not model validation:
Model.full_clean()is not called byModel.save(), so constraints bypassed by direct ORM writes still need database-level guards (unique=True,CheckConstraint). - Race conditions: a uniqueness check in
clean_username()can pass for two concurrent requests; the authoritative guarantee is the database's unique index, so catchIntegrityErroras well. - File uploads:
FileFieldvalidation must inspectrequest.FILES, and the form must be constructed asMyForm(request.POST, request.FILES).
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →