Send Data From the Browser to the Server With a Form
In the last chapter, we covered the ‘R’ of our CRUD interface - Read. That was a good operation to start with because reading data is the simplest operation of all. We’re only working with data that already exists in the database. That’s why we sometimes call this a read-only operation.
The other operations - create, update, and delete - are write operations because they somehow change the existing data.
If we want our users to create a new Band
object, they need an interface in the browser that can send a name, genre, and other Band
fields to the server.
That’s where forms come in. Forms are all about sending data from the front end to the backend. We’ll be using them for all write operations.
Forms will present some new concepts to learn, so in this chapter, we’ll take a detour from our CRUD interface to focus on forms and how to use them in Django so that you’re well prepared for the following chapters.
So let’s learn how to create a “Contact Us” form that lets users send a message to the application administrators.
Define a Form in Django
Here’s what our “Contact Us” form is going to look like:
Begin by defining the form as a class. Create the file ‘listings/forms.py’ and add this code:
# listings/forms.py
from django import forms
class ContactUsForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField()
message = forms.CharField(max_length=1000)
We’ve defined three form fields in our ContactUsForm
. Form fields are similar to model fields - there are different types of fields for different types of data. We can also specify when fields should be optional with required=False
. We allow the user to remain anonymous by making the name
field optional. And we can set max_length
just like in a model.
Next, let’s use our ContactUsForm
in the contact
view that we built in Part 2:
# listings/views.py
…
from listings.forms import ContactUsForm
…
def contact(request):
form = ContactUsForm() # instantiate a new form here
return render(request,
'listings/contact.html',
{'form': form}) # pass that form to the template
And now we edit our ‘contact.html’ template:
# listings/templates/listings/contact.html
…
<p>We're here to help.</p>
<form action="" method="post" novalidate>
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
…
Let’s add this page to our urlpatterns
too by updating merchex/urls.py.
urlpatterns = [
…
path('contact-us/', views.contact, name='contact'),
]
Let’s take a look at that in the browser before we break down what we just did in the template:
In our template, we added a <form>
tag with an <input>
of type=”submit”
inside. This is standard for any HTML form you might build. But now things get interesting.
Instead of manually typing out <input>
tags for each of our form fields, we simply type {{ form }}
. We also add {% csrf_token %}
which we’ll discuss in the Part 4 summary.
In order to see what’s going on, let’s take a look at the rendered HTML in our browser’s devtools:
You can see that Django automatically rendered a <label>
and an <input>
for each of our form fields, name
, email
, and message
. This is great - it means any time we want to add a new field to this form, we just add it to the ContactUsForm
class, and Django will take care of the HTML.
Let’s make one small tweak, though - it would look better if the fields were displayed in a stack instead of left-to-right:
# listings/templates/listings/contact.html
...
{{ form.as_p }}
...
This wraps each label-field pair in a <p>
tag, thus stacking them:
Take another look at the <form>
tag we used:
# listings/templates/listings/contact.html
...
<form action="" method="post" novalidate>
...
The
action
attribute means “the URL where we’ll send the form data.” If you set this to an empty string - or indeed omit the attribute altogether - it will be sent back to the URL of the page we are already on - i.e.. http://127.0.0.1:8000/contact-us/. That means we will handle the form data in ourcontact
view.method
is set topost
- that means the data will be sent as an HTTP POST request. That’s a little different to the GET requests we’ve been using because in addition to a “method” and a “path,” it also includes a request “body” that contains the form data.novalidate
disables your browser’s form validation. This is a helpful feature of most browsers, and we’ll switch it back on later. First, we should verify our form works correctly without it.
Check out all of the steps to include forms in your Django pages by following the screencast.
Now that we know our form data will be arriving at the server as a POST request, let’s see how we can handle POST requests in our view.
Handle POST requests in a Django View
To get a clearer look at how POST data works in a view, we’re going to use a little logging to the terminal, with print
statements.
…
def contact(request):
# add these print statements so we can take a look at `request.method` and `request.POST`
print('The request method is:', request.method)
print('The POST data is:', request.POST)
form = ContactUsForm()
…
First, let’s request this view as a GET request. The surest way to do this is to click in the browser address bar and hit Enter. (Clicking ‘reload’ might cause a POST if you do it immediately after submitting the form). Now take a look in the terminal:
The request method is: GET
The POST data is: <QueryDict: {}>
You can see that request.POST
is an empty QueryDict
during a GET request (which is a special type of Python dict
).
Now let’s request the same view as a POST request. Do this by entering some data into the form fields, and clicking ‘Submit’:
The request method is POST
The POST data is: <QueryDict: {'csrfmiddlewaretoken': ['OUsGBpHaOq8L6t9KiICU6a84HCTOfRdlEhzHDKg1OonioK5BbvItiiHVMzScJyv9'], 'name': ['Patrick'], 'email': ['patrick@example.com'], 'message': ['Loving the site!']}>
This time you can see our QueryDict
contains our form data! (Including the mysterious CSRF token, which we will explain soon.)
Next, we need to somehow handle both request scenarios in our view:
If this is a GET request, we should display an empty form to the user.
If this is a POST request, we should take a look at the data and see if it is valid. (We’ll get to the actual sending of the email soon enough.)
So we need an if
statement:
def contact(request):
# ...we can delete the logging statements that were here...
if request.method == 'POST':
# create an instance of our form, and fill it with the POST data
form = ContactUsForm(request.POST)
else:
# this must be a GET request, so create an empty form
form = ContactUsForm()
return render(request,
'listings/contact.html',
{'form': form})
In both branches of the if
statement, we create a form that gets passed to the template - but in the case of a POST request, we also fill the form with the POST data.
When we submit a filled-out form, the data is still visible in the browser when the page reloads. But that’s not all - if we submit invalid data, the form will display error messages:
What you see here is server-side validation - our Django form has validated the fields against our rules, generated error messages where there were problems, and then rendered them in the template as part of the form.
The user can then edit the values and resubmit the form, and Django will check it again. This cycle can happen as many times as necessary until all form fields are valid.
And at that point, we are finally ready to perform the action we wanted to do in the first place - to send an email!
Perform an Action When all Form Fields Are Valid
We now have some more logic to implement in our view:
If this is a POST request:
If the form data is valid, send an email.
If the form data is not valid, display the form again with error messages (as we already do).
We need a nested if
statement:
from django.core.mail import send_mail
...
def contact(request):
if request.method == 'POST':
# create an instance of our form, and fill it with the POST data
form = ContactUsForm(request.POST)
if form.is_valid():
send_mail(
subject=f'Message from {form.cleaned_data["name"] or "anonymous"} via MerchEx Contact Us form',
message=form.cleaned_data['message'],
from_email=form.cleaned_data['email'],
recipient_list=['admin@merchex.xyz'],
)
# if the form is not valid, we let execution continue to the return
# statement below, and display the form again (with errors).
else:
# this must be a GET request, so create an empty form
form = ContactUsForm()
return render(request,
'listings/contact.html',
{'form': form})
We import Django’s send_mail
function at the top. Then we insert the nested if
statement, beginning with: if form.is_valid():
.
If all our form fields contain valid data, then form.is_valid()
returns True
, and then we call send_mail
to send our email.
So should I check my inbox for email now? 🙂
Sending a real email would involve setting up an SMTP server, which we don’t have time to cover here! But we can use Django’s mock email server to test our form. This will print any emails Django sends to the terminal. Add this line to the very bottom of settings.py:
# merchex/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
We can submit our form and watch the results appear in the terminal:
And there’s our “contact us” form!
We’re almost done, but there’s one more thing we should do to improve the usability of our form.
Redirect the Browser After a POST to Avoid Duplicate POSTs
Try this: immediately after submitting the form and watching your email appear in the terminal (do this again if you closed the browser in the meantime), try clicking the browser’s refresh button. What do you see?
Most modern browsers will show you a warning like this:
Have you ever wondered why this warning appears? It only ever happens when you try to refresh immediately after a POST. Because we usually don’t want to repeat POST actions:
We don’t want to send a duplicate email.
Post a duplicate blog.
Make a duplicate payment.
So this warning box is useful, but we can go one step further. We can redirect the browser away from the original page to somewhere new. A redirect is a type of HTTP response that instructs the browser to load a new page. The benefits of this are two-fold:
We will reduce the likelihood of a duplicate POST.
We can improve the user experience by showing a confirmation page instead of just reloading the same page.
To implement the redirect, we’re going to use the function redirect
.
redirect
is a handy shortcut. We can provide it with a URL pattern with arguments or a URL path itself.
Add this import statement and another line of code within our if
statement:
from django.shortcuts import redirect # add this import
…
if form.is_valid():
send_mail(
subject=f'Message from {form.cleaned_data["name"] or "anonymous"} via MerchEx Contact Us form',
message=form.cleaned_data['message'],
from_email=form.cleaned_data['email'],
recipient_list=['admin@merchex.xyz'],
)
return redirect('email-sent') # add this return statement
When the form is valid and the email has been sent, this redirect will guide the browser away from the form page to a confirmation page. This is a page that has a URL pattern with the name 'email-sent'
. Of course, this hasn’t been created yet. You should now be comfortable with creating a URL pattern, view, and template on your own for this confirmation page, so I’ll leave that to you!
Now you’ve finished the chapter, check out the screencast to make sure you understood everything correctly.
Now You Try! Add More Pages to the Nav Bar
Now that the “contact us” page is complete, add a link to it to the navbar, along with a link to the “about us” page.
Let’s Recap!
To create a form in Django, begin by defining the form as a class in forms.py. This is where you define the different types of fields on the form - CharField, EmailField, etc. - much like you do on a model.
Create an instance of the form in your view and pass it to the template. When you render the form with
{{ form }}
, Django generates a lot of the form HTML markup.You handle two scenarios in the view - a GET request where you display a new empty form, and a POST request where you validate the user’s input and carry out the action of the form.
Now that you have a grounding in the concepts of forms in Django, you’re ready to apply this to the different write operations, beginning with the ‘C’ in CRUD - Create.