59.8 Forms and ModelForms
Right, forms. The part of web development that makes you long for the sweet, sweet release of just writing raw HTML. But you can’t, because you need validation, and security, and to not have users injecting script tags into your database. That’s where Django’s form system comes in, and it’s one of the framework’s secret weapons. It handles the tedious, security-critical crap so you can focus on the actual logic of your app. Let’s get into it.
The Humble Django Form: More Than Just <input>
Think of a Django Form class not as an HTML form, but as a validation machine. Its primary job is to define what data we expect, how to validate it, and then how to display it. The HTML rendering is almost a side effect.
Here’s the simplest possible example. Let’s say we need a form for users to suggest new, utterly ridiculous feature ideas for our app.
# forms.py
from django import forms
class FeatureSuggestionForm(forms.Form):
title = forms.CharField(max_length=100)
description = forms.CharField(widget=forms.Textarea)
urgency = forms.ChoiceField(
choices=[
('low', 'Maybe next decade?'),
('medium', 'I guess?'),
('high', 'THE WORLD WILL END WITHOUT THIS!'),
]
)
is_public = forms.BooleanField(required=False)
We’ve just defined our data schema and validation rules. max_length=100? Handled. required=False? Handled. Now, let’s use this beast in a view.
The Form Dance in a View
Using a form in a view follows a predictable pattern. It’s a two-step: show the form (GET request), process the form (POST request). Get this pattern tattooed on your brain.
# views.py
from django.shortcuts import render, redirect
from .forms import FeatureSuggestionForm
def suggest_feature(request):
# Did the user submit data? (POST request)
if request.method == 'POST':
# Populate the form with the user's data
form = FeatureSuggestionForm(request.POST)
# Check if it's all valid (this is where the magic happens)
if form.is_valid():
# Form data is clean! Do something.
title = form.cleaned_data['title']
description = form.cleaned_data['description']
# ... save to a model, send an email, etc.
return redirect('success_url') # Always redirect after a successful POST!
else:
# It's a GET request, just show a blank form
form = FeatureSuggestionForm()
# If the form is invalid (or it's a GET), render the form with any errors
return render(request, 'suggestion_form.html', {'form': form})
Notice form.cleaned_data? That’s your reward for passing validation. It’s a dictionary of data that has been cleaned and converted to Python types. It’s safe. You can trust it. Unlike request.POST which is a dirty, untrustworthy mess straight from the user.
Rendering the Form in a Template (Without Looking Ugly)
You could manually write every <input>, but why? Django can do it for you, though its default output is… functional. Let’s look at your options in suggestion_form.html:
<form method="post">
{% csrf_token %} <!-- NON-NEGOTIABLE. Forget this and Django will slap you. -->
<!-- Option 1: Just dump the whole form -->
{{ form.as_p }}
<!-- Option 2: More control, field by field -->
<div class="field-wrapper">
{{ form.title.errors }}
<label for="{{ form.title.id_for_label }}">Title:</label>
{{ form.title }}
</div>
<div class="field-wrapper">
{{ form.description.errors }}
<label for="{{ form.description.id_for_label }}">Description:</label>
{{ form.description }}
</div>
<!-- Option 3: Even more control? Loop through it. -->
{% for field in form %}
<div class="field-wrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<button type="submit">Suggest Brilliance</button>
</form>
The as_p() method wraps each field in a <p> tag. There’s also as_table() and as_ul(). They’re great for prototypes but you’ll almost always need the granular control of Option 2 or 3 for a real site.
ModelForms: Because You’re Lazy and Smart
Now, the main event. If you have a Django model, writing a form that maps to it is an act of profound repetition. Django hates repetition. Enter ModelForm.
Let’s say we have this model:
# models.py
from django.db import models
class FeatureSuggestion(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
urgency = models.CharField(max_length=10, choices=[...]) # same choices as before
is_public = models.BooleanField(default=False)
submitted_date = models.DateTimeField(auto_now_add=True)
Our ModelForm becomes trivial:
# forms.py
from django.forms import ModelForm
from .models import FeatureSuggestion
class FeatureSuggestionModelForm(ModelForm):
class Meta:
model = FeatureSuggestion
fields = ['title', 'description', 'urgency', 'is_public']
# or exclude = ['submitted_date'] to include everything *except* a field
That’s it. This form now has all the validation rules defined by the model (max_length, required, etc.). The view logic is identical. The killer feature? Calling form.save().
# views.py
def suggest_feature(request):
if request.method == 'POST':
form = FeatureSuggestionModelForm(request.POST)
if form.is_valid():
# This one line creates and saves the model instance. Magic.
new_suggestion = form.save()
# Now you have a saved object, maybe do something with it?
return redirect('detail_view', pk=new_suggestion.pk)
# ... rest of the view is the same
When the Magic Isn’t Quite Enough: Customizing ModelForms
Sometimes the automatically generated field isn’t what you want. Maybe you want to change the widget or add help text. No problem. You just declare the field on the form yourself, overriding the default.
class FeatureSuggestionModelForm(ModelForm):
# Let's make the description use a smaller textarea and have help text
description = forms.CharField(
widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}),
help_text="Try to keep it under 1000 words. We have short attention spans."
)
class Meta:
model = FeatureSuggestion
fields = '__all__' # A bold choice. Includes every field on the model.
The Gotchas (Because Of Course There Are Gotchas)
The
fieldsMeta attribute is NOT optional. After a certain version, Django will scream at you if you don’t explicitly setfieldsorexcludein yourModelForm.Meta. This is a security feature to prevent you from accidentally exposing every field in your model, includinguser.is_admin. Always be explicit.That
requiredattribute. Django sets the HTMLrequiredattribute based on your model/form field. If you haveblank=Trueon your model field, it won’t berequiredin the form. This is usually what you want, but be aware.Save with
commit=False. This is your superpower.form.save(commit=False)creates the model instance but doesn’t send it to the database. Why? So you can add data that isn’t in the form, like setting theuserfield to the current request.user.if form.is_valid(): new_suggestion = form.save(commit=False) new_suggestion.user = request.user # Set the user who submitted it new_suggestion.save() # NOW it gets saved to the DB.
The form system is deep. You can write custom validators, clean specific fields, clean the form as a whole, and more. But this is the 90%. Master this, and you’ve already lapped everyone trying to hand-roll their form validation.