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()    
    html = []
    for k in sorted(values):
        html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, values[k]))
    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. When you used startapp to create your books app, Django created a new views.py file for you in your \books folder. Go ahead and add a new view to this file:

# \books\views.py

from django.shortcuts import render

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

Next step is to create the template, however we first need to create a couple of new folders for the template. If you haven’t changed it, the 'APP_DIRS' in your settings file is set to True. This means that Django will search all of your apps for a folder named \templates.

Create a new \templates folder inside your books app folder. Then go ahead and create another folder inside the new \templates folder and call it books. Your final folder structure will be books\templates\books\.

This inner books folder is important for namespacing your templates. Because Django will search all apps for a matching template, creating a namespace for the app templates ensures that Django uses the correct template if two apps used the same template name.

Create the following search_form.html file and save it to your new folder:

# mysite_project\mysite\books\templates\books\search_form.html

<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>

Now we need to create a URLconf so Django can find our new view. Inside your books folder, create a new urls.py file (startapp doesn’t create this file). Add the following URL pattern to this new urls.py:

# mysite\books\urls.py

from django.conf.urls import url
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 books.views import search_form, because the former is less verbose. We’ll cover this importing approach in more detail in Chapter 7.)

One last thing – when Django searches for URL patterns, it will only search the base mysite\urls.py file, unless we explicitly include the URL patterns from other apps. So let’s go ahead and modify our site urlpatterns:

# mysite\urls.py

from django.conf.urls import include, url

urlpatterns = [
# ...
    url(r'^', include('books.urls')),
]

This new URL pattern must be added to the end of the urlpatterns list. This is because the r'^' regex sends everything to books.urls, so we want to make sure none of the other patterns match, before sending Django to check books\urls.py for a matching pattern.

Now, if you run the development server and visit http://127.0.0.1:8000/search-form/, you’ll see the search interface (Figure 6.1). Simple enough.

A simple Django search form
Figure 6-1. A Simple Search Form

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:

# books/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 (Figure 6-2).

A simple Django search form
Figure 6-2. Result returned from simple search form

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, 'books/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. I 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. To get our new search form working, let’s create the search_results.html file:

# \books\templates\books\search_results.html

<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.

Now, when you run the development server and visit http://127.0.0.1:8000/search-form/, your search term should return a more useful result (Figure 6-3).

A more useful Django search form
Figure 6-3. Result returned from our improved search form

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 (Figure 6-4).

A Django search form with error message
Figure 6-4. Our improved search form now displays and error message when no search term is entered.

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, 'books/search_results.html', {'books': books, 'query': q})
    return render(request, 'books/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.

<<< Users, Groups and Permissions | Table of Contents | Form Validation >>>