• 12 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 7/20/22

Capture User Input With Django Forms

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:

The page contains a navbar with Bands and Listings, the title Contact Us, 3 fields, and a submit button.
Our "Contact Us" form.

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:

The 3 fields are no longer stacked vertically.
Our Contact Us page in progress.

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:

The web page with the HTML exposed.
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: 

The 3 fields are now stacked vertically.
Fields stacked vertically.

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 our  contact  view.

  • method  is set to  post  - 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 3 fields are filled in.
Requesting the same view as a POST request.
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:

The message
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:

The 3 fields are filled in again.
Try again!
The Terminal containing data about the message submitted.
Our "contact us" form.

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:

At the top of the page appears a message titled
Resubmission warning.

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.

Example of certificate of achievement
Example of certificate of achievement