Django Forms

HTML forms are the backbone of interactive Web sites, from the simplicity of Google’s single search box to ubiquitous blog comment submission forms to complex custom data-entry interfaces.

This chapter covers how you can use Django to access user-submitted form data, validate it and do something with it. Along the way, we’ll cover HttpRequest and Form objects.

Getting Data from the Request Object

I introduced HttpRequest objects in Chapter 2 when we first covered view functions, but I didn’t have much to say about them at the time. Recall that each view function takes an HttpRequest object as its first parameter,
as in our hello() view:

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello world")

HttpRequest objects, such as the variable request here, have a number of interesting attributes and methods that you should familiarize yourself with, so that you know what’s possible. You can use these attributes to get information about the current request (i.e., the user/Web browser that’s loading the current page on your Django-powered site), at the time the view function is executed.

Information About the URL

HttpRequest objects contain several pieces of information about the currently requested URL (Table 6-1).

Table 6-1: HttpRequest methods and attributes

Attribute/method Description Example
request.path The full path, not including the domain but including the leading slash. “/hello/”
request.get_host() The host (i.e., the “domain,” in common parlance). “127.0.0.1:8000” or “www.example.com”
request.get_full_path() The path, plus a query string (if available). “/hello/?print=true”
request.is_secure() True if the request was made via HTTPS. Otherwise, False. True or False

Always use these attributes/methods instead of hard-coding URLs in your views. This makes for more flexible code that can be reused in other places. A simplistic example:

# BAD!
def current_url_view_bad(request):
    return HttpResponse("Welcome to the page at /current/")

# GOOD
def current_url_view_good(request):
    return HttpResponse("Welcome to the page at %s"
% request.path)

Other Information About the Request

request.META is a Python dictionary containing all available HTTP headers for the given request – including the user’s IP address and user agent (generally the name and version of the Web browser). Note that the full list of available headers depends on which headers the user sent and which headers your Web server sets. Some commonly available keys in this dictionary are:

  • HTTP_REFERER – The referring URL, if any. (Note the misspelling of REFERER.)
  • HTTP_USER_AGENT – The user’s browser’s user-agent string, if any. This looks something like: "Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17".
  • REMOTE_ADDR – The IP address of the client, e.g., "12.345.67.89". (If the request has passed through any proxies, then this might be a comma-separated list of IP addresses, e.g., "12.345.67.89,23.456.78.90".)

Note that because request.META is just a basic Python dictionary, you’ll get a KeyError exception if you try to access a key that doesn’t exist. (Because HTTP headers are external data – that is, they’re submitted by your users’ browsers – they shouldn’t be trusted, and you should always design your application to fail gracefully if a particular header is empty or doesn’t exist.) You should either use a try/except clause or the get() method to handle the case of undefined keys:

# BAD!
def ua_display_bad(request):
    ua = request.META['HTTP_USER_AGENT']  # Might raise KeyError!
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 1)
def ua_display_good1(request):
    try:
        ua = request.META['HTTP_USER_AGENT']
    except KeyError:
        ua = 'unknown'
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 2)
def ua_display_good2(request):
    ua = request.META.get('HTTP_USER_AGENT', 'unknown')
    return HttpResponse("Your browser is %s" % ua)

I encourage you to write a small view that displays all of the request.META data so you can get to know what’s in there. Here’s what that view might look like:

def display_meta(request):
    values = request.META.items()
    values.sort()
    html = []
    for k, v in values:
        html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, v))
    return HttpResponse('<table>%s</table>' % '\n'.join(html))

Another good way to see what sort of information that the request object contains is to look closely at the Django error pages when you crash the system – there is a wealth of useful information in there, including all the HTTP headers and other request objects (request.path for example).

Information About Submitted Data

Beyond basic metadata about the request, HttpRequest objects have two attributes that contain information submitted by the user: request.GET and request.POST. Both of these are dictionary-like objects that give you access to GET and POST data. POST data generally is submitted from an HTML <form>, while GET data can come from a <form> or the query string in the page’s URL.

A Simple Django Form-Handling Example

Continuing the ongoing example of books, authors and publishers, let’s create a simple view that lets users search our book database by title. Generally, there are two parts to developing a form: the HTML user interface and the backend view code that processes the submitted data. The first part is easy; let’s just set up a view that displays a search form:

from django.shortcuts import render

def search_form(request):
    return render(request, 'search_form.html')

As you learned in Chapter 3, this view can live anywhere on your Python path. For sake of argument, put it in books/views.py. The accompanying template, search_form.html, could look like this:

<html>
<head>
    <title>Search</title>
</head>
<body>
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

Save this file to your mysite/templates directory you created in Chapter 3, or you can create a new folder books/templates. Just make sure you have 'APP_DIRS' in your settings file set to True. The URLpattern in urls.py could look like this:

from books import views

urlpatterns = [
    # ...
    url(r'^search-form/$', views.search_form),
    # ...
]

(Note that we’re importing the views module directly, instead of something like from mysite.views import search_form, because the former is less verbose. We’ll cover this importing approach in more detail in Chapter 7.) Now, if you run the development server and visit http://127.0.0.1:8000/search-form/, you’ll see the search interface. Simple enough. Try submitting the form, though, and you’ll get a Django 404 error. The form points to the URL /search/, which hasn’t yet been implemented. Let’s fix that with a second view function:

# urls.py

urlpatterns = [
    # ...
    url(r'^search-form/$', views.search_form),
    url(r'^search/$', views.search),
    # ...
]

# books/views.py

from django.http import HttpResponse

# ...

def search(request):
    if 'q' in request.GET:
        message = 'You searched for: %r' % request.GET['q']
    else:
        message = 'You submitted an empty form.'
    return HttpResponse(message)

For the moment, this merely displays the user’s search term, so we can make sure the data is being submitted to Django properly, and so you can get a feel for how the search term flows through the system. In short:

  1. The HTML <form> defines a variable q. When it’s submitted, the value of q is sent via GET (method="get") to the URL /search/.
  2. The Django view that handles the URL /search/ (search()) has access to the q value in request.GET.

An important thing to point out here is that we explicitly check that 'q' exists in request.GET. As I pointed out in the request.META section above, you shouldn’t trust anything submitted by users or even assume that they’ve submitted anything in the first place. If we didn’t add this check, any submission of an empty form would raise KeyError in the view:

# BAD!
def bad_search(request):
    # The following line will raise KeyError if 'q' hasn't
    # been submitted!
    message = 'You searched for: %r' % request.GET['q']
    return HttpResponse(message)

Query String Parameters

Because GET data is passed in the query string (e.g., /search/?q=django), you can use request.GET to access query string variables. In Chapter 2’s introduction of Django’s URLconf system, I compared Django’s pretty URLs to more traditional PHP/Java URLs such as /time/plus?hours=3 and said I’d show you how to do the latter in Chapter 6. Now you know how to access query string parameters in your views (like hours=3 in this example) – use request.GET.

POST data works the same way as GET data – just use request.POST instead of request.GET. What’s the difference between GET and POST? Use GET when the act of submitting the form is just a request to “get” data. Use POST whenever the act of submitting the form will have some side effect –
changing data, or sending an e-mail, or something else that’s beyond simple display of data. In our book search example, we’re using GET because the query doesn’t change any data on our server. (See the w3.org site if you want to learn more about GET and POST.) Now that we’ve verified request.GET is being passed in properly, let’s hook the user’s search query into our book database (again, in views.py):

from django.http import HttpResponse
from django.shortcuts import render
from books.models import Book

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render(request, 'search_results.html',
                      {'books': books, 'query': q})
    else:
        return HttpResponse('Please submit a search term.')

A couple of notes on what we did here:

  • Aside from checking that 'q' exists in request.GET, we also make sure that request.GET['q'] is a non-empty value before passing it to the database query.
  • We’re using Book.objects.filter(title__icontains=q) to query our book table for all books whose title includes the given submission. The icontains is a lookup type (as explained in Chapter 4 and Appendix B), and the statement can be roughly translated as “Get the books whose title contains q, without being case-sensitive.”

This is a very simple way to do a book search. We wouldn’t recommend using a simple icontains query on a large production database, as it can be slow. (In the real world, you’d want to use a custom search system of some sort. Search the Web for open-source full-text search to get an idea of the possibilities.)

We pass books, a list of Book objects, to the template. The search_results.html file might include something like this:

<html>     
    <head>         
    <title>Book Search</title>     
    </head>     
    <body>       
        <p>You searched for: <strong>{{ query }}</strong></p>
   
        {% if books %}           
            <p>Found {{ books|length }} book{{ books|pluralize }}.</p>
            <ul>               
                {% for book in books %}               
                <li>{{ book.title }}</li>               
                {% endfor %}           
            </ul>       
        {% else %}           
            <p>No books matched your search criteria.</p>       
        {% endif %}     
    </body>
</html>

Note usage of the pluralize template filter, which outputs an “s” if appropriate, based on the number of books found.

Improving Our Simple Form-Handling Example

As in previous chapters, I’ve shown you the simplest thing that could possibly work. Now I’ll point out some problems and show you how to improve it. First, our search() view’s handling of an empty query is poor – we’re just displaying a “Please submit a search term.” message, requiring the user to hit the browser’s back button.

This is horrid and unprofessional, and if you ever actually implement something like this in the wild, your Django privileges will be revoked. It would be much better to redisplay the form, with an error above it, so that the user can try again immediately. The easiest way to do that would be to render the template again, like this:

from django.http import HttpResponse
from django.shortcuts import render
from books.models import Book

def search_form(request):
    return render(request, 'search_form.html')

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render(request, 'search_results.html', {'books': books, 'query': q})
    else:
        return render(request, 'search_form.html', {'error': True})

(Note that I’ve included search_form() here so you can see both views in one place.) Here, we’ve improved search() to render the search_form.html template again, if the query is empty. And because we need to display an error message in that template, we pass a template variable. Now we can edit search_form.html to check for the error variable:

<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term.</p>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

We can still use this template from our original view, search_form(), because search_form() doesn’t pass error to the template – so the error message won’t show up in that case. With this change in place, it’s a better application, but it now begs the question: is a dedicated search_form() view really necessary?

As it stands, a request to the URL /search/ (without any GET parameters) will display the empty form (but with an error). We can remove the search_form() view, along with its associated URLpattern, as long as we change search() to hide the error message when somebody visits /search/ with no GET parameters:

def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render(request, 'search_results.html',
                          {'books': books, 'query': q})
    return render(request, 'search_form.html', {'error': error})

In this updated view, if a user visits /search/ with no GET parameters, they’ll see the search form with no error message. If a user submits the form with an empty value for 'q', they’ll see the search form with an error message. And, finally, if a user submits the form with a non-empty value for 'q', they’ll see the search results.

We can make one final improvement to this application, to remove a bit of redundancy. Now that we’ve rolled the two views and URLs into one and /search/ handles both search-form display and result display, the HTML <form> in search_form.html doesn’t have to hard-code a URL. Instead of this:

<form action="/search/" method="get">

It can be changed to this:

<form action="" method="get">

The action="" means “Submit the form to the same URL as the current page.” With this change in place, you won’t have to remember to change the action if you ever hook the search() view to another URL.