On this episode, we will learn about HTML forms and Django’s form system to use when collecting input from users.
Listen at Spotify.
Last Episode
On the previous episode, we looked at templates, the primary tool that Django provides to build user interfaces in your Django app.
Web Forms 101
HTML can describe the type
of data
that you may want your users
to send to your site.
Collecting this data is done
with a handful of tags.
The primary HTML tags to consider are form
, input
, and select
.
A form
tag is the container
for all the data
that you want a user to send
to your application.
The tag has two critical attributes
that tell the browser how to send data: action
and method
.
<form method="GET" action="/some/form/">
<input type="text" name="message">
<button type="submit">Send me!</button>
</form>
When the form’s method is GET
,
the form data will be sent as part
of the URL
in a querystring.
The GET request sent to the server will look
like /some/form/?message=Hello
.
This type of form submission is most useful
when we don’t need to save data
and are trying to do some kind of query.
The POST
method of sending form data is for data
that we want to be secure or saved
within an application.
We’ve seen that form
is the container
that guides how to send form data.
input
and select
are the tags
that let us display a meaningful form
to the user.
You can see a full list of types on the MDN input documentation page.
The other attribute, name
, is the identifier
that the form will pair with the user data.
The server uses the identifier
so it can distinguish
between the pieces of data
that a form submission may include.
Django Forms
Django’s form features act as a bridge between HTML forms and Python classes and data types. When presenting a form to a user in view, the form system is able to display the proper HTML form tags and structure. When receiving this form data from a user’s submission, the form system can translate the browser’s raw form data into native Python data that we can use.
# application/forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(max_length=1000)
- Django forms are sub-classes
of the
Form
class. - The data that we want to collect is listed as class level attributes.
If the template looks like:
{{ form.as_p }}
Then Django will render:
<p><label for="id_name">Name:</label>
<input type="text" name="name" maxlength="100" required id="id_name"></p>
<p><label for="id_email">Email:</label>
<input type="email" name="email" required id="id_email"></p>
<p><label for="id_message">Message:</label>
<input type="text" name="message" maxlength="1000" required id="id_message">
</p>
To make it possible submit the form,
we need to wrap this rendered output
with a form
tag
and include a submit button
and a CSRF token.
<form action="{% url "some-form-view" %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<p><input type="submit" value="Send the form!"></p>
</form>
# application/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from .forms import ContactForm
def contact_us(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
# Do something with the form data like send an email.
return HttpResponseRedirect(reverse('some-form-success-view'))
else:
form = ContactForm()
return render(request, 'contact_form.html', {'form': form})
- When the form is valid, the view does the extra work represented by the comment and redirects to a new view that can show some kind of success message.
- When the form is invalid,
the view goes out of the
if
clause and callsrender
. Since the data is bound to theform
, the contact form has enough information to show which form fields caused the errors that made the form invalid.
That’s the core of form handling!
The presented view is a common pattern
for handling form views
in Django.
In fact, this view pattern is so common
that Django provides a built-in view
to implement what is done in the example
named FormView
.
# application/views.py
from django.views.generic import FormView
from .forms import ContactForm
class ContactUs(FormView):
form = ContactForm
template = 'contact_form.html'
def get_success_url(self):
return reverse('some-form-success-view')
def form_valid(self, form):
# Do something with the form data like send an email.
return super().form_valid(form)
Form Fields
With the basics of form handling done, we can turn our attention to the kinds of fields that forms can use. The extensive list of fields is in the Django documentation, and we will look at a few of the most common ones in this article.
Fields are the critical intersection between the world of the browser and HTML and the Python world with all of its robust data types.
CharField
CharField
is a real workhorse
for Django forms.
The CharField
captures text input
and uses a standard input
tag
with a type of text
.
If you want to collect more text,
like in a feedback form,
you can switch
from the default TextInput
widget
to a Textarea
widget.
This will make your form render a textarea
tag
that will give far more space
for any input.
# application/forms.py
from django import forms
class FeedbackForm(forms.Form):
email = forms.EmailField()
comment = forms.CharField(widget=forms.Textarea)
EmailField
The EmailField
is like a specialized version
of the CharField
.
The field uses an input
tag
with a type of email
.
Many modern browsers can help
to check that valid email addresses are provided.
Also, when this field is validated within the framework,
Django will attempt to validate the email address too
in case the browser wasn’t able to do it.
DateField
A DateField
is another field
that is mostly like a CharField
.
The field even uses the input
type of text
when rendered.
The difference with this field comes
from the data type that the form will provide
after it is validated.
A DateField
will convert
a variety of string formats
into a Python datetime.date
object.
ChoiceField
A ChoiceField
is useful
when you want a user to make a choice
from a list of options.
For this field type,
we must provide a list of choices
that the user can pick from.
Imagine that we want to ask users
what their favorite meal of the day is.
Here’s a form that can do that.
# application/forms.py
from django import forms
class SurveyForm(forms.Form):
MEALS = [("b", "Breakfast"), ("l", "Lunch"), ("d", "Dinner")]
favorite_meal = forms.ChoiceField(choices=MEALS)
This will contain a form
with a select
tag
that looks like:
<p>
<label for="id_favorite_meal">Favorite meal:</label>
<select name="favorite_meal" id="id_favorite_meal">
<option value="b">Breakfast</option>
<option value="l">Lunch</option>
<option value="d">Dinner</option>
</select>
</p>
Validating Forms
What is is_valid
actually doing?
It does a lot!
When is_valid
is True
,
the form’s data will be
in a dictionary named cleaned_data
with keys
that match the field names
declared by the forms.
With the validated data,
you access cleaned_data
to do your work.
For instance,
if we had some kind of integration
with a support ticket system,
perhaps our FeedbackForm
above is handled
in the view like:
if form.is_valid():
email = form.cleaned_data['email']
comment = form.cleaned_data['comment']
create_support_ticket(email, comment)
return HttpReponseRedirect(reverse('feedback-received'))
When is_valid
is False
,
Django will store the errors it found
in an errors
attribute.
This attribute will be used
when the form is re-rendered
on the page
(because, if you recall from the view example,
the form view pattern sends a bound form
back through a render
call
in the failure case).
If you have a form field,
you can add customization
by writing a method on the form class.
The format of the method must match with the field name
and prepend clean_
.
Let’s imagine that we want a website
for Bobs.
In order to sign up for the website,
your email address must have “bob”
in it.
We can write a clean method
to check for that.
# application/forms.py
from django import forms
class SignUpForm(forms.Form):
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
def clean_email(self):
email = self.cleaned_data['email']
if 'bob' not in email:
raise forms.ValidationError('Sorry, you are not a Bob.')
return email
These clean_<field name>
methods are hooks
that let you include extra checking.
This hook system gives you the perfect place
to put validation logic for data
that is specific
to your application.
But what about validating multiple pieces
of data?
This might happen when data has some kind
of interrelationship.
For instance,
if you’re putting together a genealogy website,
you may have a form that records birth and death dates.
You might want to check those dates.
# application/forms.py
from django import forms
class HistoricalPersonForm(forms.Form):
name = forms.CharField()
date_of_birth = forms.DateField()
date_of_death = forms.DateField()
def clean(self):
cleaned_data = super().clean()
date_of_birth = cleaned_data.get('date_of_birth')
date_of_death = cleaned_data.get('date_of_death')
if date_of_birth and date_of_death and date_of_birth > date_of_death:
raise forms.ValidationError('Birth date must be before death date.')
return cleaned_data
Summary
That’s how forms make it possible to collect data from your users so your site can interact with them. We’ve seen:
- Web forms and the
form
HTML tag - The
Form
class that Django uses to handle form data in Python - How forms are rendered to users by Django
- Controlling what fields are in forms
- How to do form validation
Next Time
In the next episode, we’re going to talk about storing data in a database. Django gives us the ability to do this storage with models. Models are a way to represent your application’s data and make your website very dynamic.
You can follow the show on Spotify. Or follow me or the show on X at @mblayman or @djangoriffs.
Please rate or review on Apple Podcasts, Spotify, or from wherever you listen to podcasts. Your rating will help others discover the podcast, and I would be very grateful.
Django Riffs is supported by listeners like you. If you can contribute financially to cover hosting and production costs, please check out my Patreon page to see how you can help out.