Note
A newer version of this tutorial using Django 1.9 is available from Leanpub: https://leanpub.com/tangowithdjango19
So far we have only presented data through the views and templates that we have created. In this chapter, we will run through how to capture data through web forms. Django comes with some neat form handling functionality, making it a pretty straightforward process to gather information from users and send it back to your web application. According to Django’s documentation on forms, the form handling functionality allows you to:
One of the major advantages of using Django’s forms functionality is that it can save you a lot of time and HTML hassle. This part of the tutorial will look at how to implement the necessary infrastructure that will allow users of Rango to add categories and pages to the database via forms.
The basic steps involved in creating a form and allowing users to enter data via the form is as follows.
This workflow is a bit more complicated than previous workflows, and the views that we have to construct have a lot more complexity as well. However, once you undertake the process a few times it will be pretty clear how everything pieces together.
First, create a file called forms.py within the rango application directory. While this step is not absolutely necessary, as you could put the forms in the models.py, this makes the codebase a lot cleaner and clearer to understand.
Within Rango’s forms.py module, we will be creating a number of classes that inherit from Django’s ModelForm. In essence, a ModelForm is a helper class that allows you to create a Django Form from a pre-existing model. As we’ve already got two models defined for Rango (Category and Page), we’ll create ModelForms for both.
In rango/forms.py add the following code.
from django import forms
from rango.models import Page, Category
class CategoryForm(forms.ModelForm):
name = forms.CharField(max_length=128, help_text="Please enter the category name.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
likes = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
slug = forms.CharField(widget=forms.HiddenInput(), required=False)
# An inline class to provide additional information on the form.
class Meta:
# Provide an association between the ModelForm and a model
model = Category
fields = ('name',)
class PageForm(forms.ModelForm):
title = forms.CharField(max_length=128, help_text="Please enter the title of the page.")
url = forms.URLField(max_length=200, help_text="Please enter the URL of the page.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
class Meta:
# Provide an association between the ModelForm and a model
model = Page
# What fields do we want to include in our form?
# This way we don't need every field in the model present.
# Some fields may allow NULL values, so we may not want to include them...
# Here, we are hiding the foreign key.
# we can either exclude the category field from the form,
exclude = ('category',)
#or specify the fields to include (i.e. not include the category field)
#fields = ('title', 'url', 'views')
#TODO(leifos): Note that in Django 1.7+ it is now required to specify the fields that are included, via fields, or specify the fields that are to be excluded, via exclude.
Django provides us with a number of ways to customise the forms that are created on our behalf. In the code sample above, we’ve specified the widgets that we wish to use for each field to be displayed. For example, in our PageForm class, we’ve defined forms.CharField for the title field, and forms.URLField for url field. Both fields provide text entry for users. Note the max_length parameters we supply to our fields - the lengths that we specify are identical to the maximum length of each field we specified in the underlying data models. Go back to Chapter model-label to check for yourself, or have a look at Rango’s models.py file.
You will also notice that we have included several IntegerField entries for the views and likes fields in each form. Note that we have set the widget to be hidden with the parameter setting widget=forms.HiddenInput(), and then set the value to zero with initial=0. This is one way to set the field to zero without giving the control to the user as the field will be hidden, yet the form will provide the value to the model. However, as you can see in the PageForm, despite the fact that we have a hidden field, we still need to include the field in the form. If in fields we excluded views, then the form would not contain that field (despite it being specified) and so the form would not return the value zero for that field. This may raise an error depending on how the model has been set up. If in the models we specified that the default=0 for these fields then we can rely on the model to automatically populate field with the default value - and thus avoid a not null error. In this case, it would not be necessary to have these hidden fields. We have also included the field slug in the form, and set it to use the``widget=forms.HiddenInput()``, but rather than specifying an initial or default value, we have said the field is not required by the form. This is because our model will be responsible on save() to populating this field. Essentially, you need to be careful when you define your models and forms to make sure that form is going to contain and pass on all the data that is required to populate your model correctly.
Besides the CharField and IntegerField widget, many more are available for use. As an example, Django provides EmailField (for e-mail address entry), ChoiceField (for radio input buttons), and DateField (for date/time entry). There are many other field types you can use, which perform error checking for you (e.g. is the value provided a valid integer?). We highly recommend you have a look at the official Django documentation on widgets to see what components exist and the arguments you can provide to customise them.
Perhaps the most important aspect of a class inheriting from ModelForm is the need to define which model we’re wanting to provide a form for. We take care of this through our nested Meta class. Set the model attribute of the nested Meta class to the model you wish to use. For example, our CategoryForm class has a reference to the Category model. This is a crucial step enabling Django to take care of creating a form in the image of the specified model. It will also help in handling flagging up any errors along with saving and displaying the data in the form.
We also use the Meta class to specify which fields that we wish to include in our form through the fields tuple. Use a tuple of field names to specify the fields you wish to include.
Note
We highly recommend you check out the official Django documentation on forms for further information about how to customise them.
With our CategoryForm class now defined, we’re now ready to create a new view to display the form and handle the posting of form data. To do this, add the following code to rango/views.py.
from rango.forms import CategoryForm
def add_category(request):
# A HTTP POST?
if request.method == 'POST':
form = CategoryForm(request.POST)
# Have we been provided with a valid form?
if form.is_valid():
# Save the new category to the database.
form.save(commit=True)
# Now call the index() view.
# The user will be shown the homepage.
return index(request)
else:
# The supplied form contained errors - just print them to the terminal.
print form.errors
else:
# If the request was not a POST, display the form to enter details.
form = CategoryForm()
# Bad form (or form details), no form supplied...
# Render the form with error messages (if any).
return render(request, 'rango/add_category.html', {'form': form})
The new add_category() view adds several key pieces of functionality for handling forms. First, we check the HTTP request method, to determine if it was a HTTP GET or POST. We can then handle different requests methods appropriately - i.e. whether we want to show a form (if it is a GET), or process form data (if it is a POST) - all from the same URL. The add_category() view function can handle three different scenarios:
Note
What do we mean by GET and POST? They are two different types of HTTP requests.
Django’s form handling machinery processes the data returned from a user’s browser via a HTTP POST request. It not only handles the saving of form data into the chosen model, but will also automatically generate any error messages for each form field (if any are required). This means that Django will not store any submitted forms with missing information which could potentially cause problems for your database’s referential integrity. For example, supplying no value in the category name field will return an error, as the field cannot be blank.
You’ll notice from the line in which we call render() that we refer to a new template called add_category.html which will contain the relevant Django template code and HTML for the form and page.
Create the file templates/rango/add_category.html. Within the file, add the following HTML markup and Django template code.
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>Add a Category</h1>
<form id="category_form" method="post" action="/rango/add_category/">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.help_text }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Create Category" />
</form>
</body>
</html>
Now, what does this code do? You can see that within the <body> of the HTML page that we place a <form> element. Looking at the attributes for the <form> element, you can see that all data captured within this form is sent to the URL /rango/add_category/ as a HTTP POST request (the method attribute is case insensitive, so you can do POST or post - both provide the same functionality). Within the form, we have two for loops - one controlling hidden form fields, the other visible form fields - with visible fields controlled by the fields attribute of your ModelForm Meta class. These loops produce HTML markup for each form element. For visible form fields, we also add in any errors that may be present with a particular field and help text which can be used to explain to the user what he or she needs to enter.
Note
The need for hidden as well as visible form fields is necessitated by the fact that HTTP is a stateless protocol. You can’t persist state between different HTTP requests which can make certain parts of web applications difficult to implement. To overcome this limitation, hidden HTML form fields were created which allow web applications to pass important information to a client (which cannot be seen on the rendered page) in a HTML form, only to be sent back to the originating server when the user submits the form.
You should also take note of the code snippet {% csrf_token %}. This is a Cross-Site Request Forgery (CSRF) token, which helps to protect and secure the HTTP POST action that is initiated on the subsequent submission of a form. The CSRF token is required by the Django framework. If you forget to include a CSRF token in your forms, a user may encounter errors when he or she submits the form. Check out the official Django documentation on CSRF tokens for more information about this.
Now we need to map the add_category() view to a URL. In the template we have used the URL /rango/add_category/ in the form’s submit attribute. So we will need to follow suit in rango/urls.py and modify the urlpatterns as follows.
urlpatterns = patterns('',
url(r'^$', views.index, name='index'),
url(r'^about/$', views.about, name='about'),
url(r'^add_category/$', views.add_category, name='add_category'), # NEW MAPPING!
url(r'^category/(?P<category_name_slug>[\w\-]+)/$', views.category, name='category'),)
Ordering doesn’t necessarily matter in this instance. However, take a look at the official Django documentation on how Django process a request for more information. Our new URL for adding a category is /rango/add_category/.
As a final step let’s put a link on the index page so that we can easily add categories. Edit the template rango/index.html and add the following HTML hyperlink just before the </body> closing tag.
<a href="/rango/add_category/">Add a New Category</a><br />
Now let’s try it out! Run your Django development server, and navigate to http://127.0.0.1:8000/rango/. Use your new link to jump to the add category page, and try adding a category. Figure 1 shows screenshots of the of the Add Category and Index Pages.
Figure 1: Adding a new category to Rango with our new form. The diagram illustrates the steps involved.
Note
If you add a number of categories, they will not always appear on the index page, that is because we are only showing the top 5 categories on the index page. If you log into the Admin interface you should be able to view all the categories that you have entered. To see what is happening as you entered them in rango/views.py in add_category(), you can get the reference to the category model object created from form.save(), with cat = form.save(commit=True) and then print to console the category and slug, with print cat, cat.slug to see what is being created.
Recall that our Page model has a url attribute set to an instance of the URLField type. In a corresponding HTML form, Django would reasonably expect any text entered into a url field to be a well-formed, complete URL. However, users can find entering something like http://www.url.com to be cumbersome - indeed, users may not even know what forms a correct URL!
In scenarios where user input may not be entirely correct, we can override the clean() method implemented in ModelForm. This method is called upon before saving form data to a new model instance, and thus provides us with a logical place to insert code which can verify - and even fix - any form data the user inputs. In our example above, we can check if the value of url field entered by the user starts with http:// - and if it doesn’t, we can prepend http:// to the user’s input.
class PageForm(forms.ModelForm):
...
def clean(self):
cleaned_data = self.cleaned_data
url = cleaned_data.get('url')
# If url is not empty and doesn't start with 'http://', prepend 'http://'.
if url and not url.startswith('http://'):
url = 'http://' + url
cleaned_data['url'] = url
return cleaned_data
Within the clean() method, a simple pattern is observed which you can replicate in your own Django form handling code.
This trivial example shows how we can clean the data being passed through the form before being stored. This is pretty handy, especially when particular fields need to have default values - or data within the form is missing, and we need to handle such data entry problems.
Note
Overriding methods implemented as part of the Django framework can provide you with an elegant way to add that extra bit of functionality for your application. There are many methods which you can safely override for your benefit, just like the clean() method in ModelForm as shown above. Check out the Official Django Documentation on Models for more examples on how you can override default functionality to slot your own in.
Now that you’ve worked through the chapter, try these exercises to solidify your knowledge on Django’s form functionality.
A next logical step would be to allow users to add pages to a given category. To do this, repeat the same workflow above for Pages - create a new view (add_page()), a new template (rango/add_page.html), URL mapping and then add a link from the category page. To get you started, here’s the view logic for you.
from rango.forms import PageForm
def add_page(request, category_name_slug):
try:
cat = Category.objects.get(slug=category_name_slug)
except Category.DoesNotExist:
cat = None
if request.method == 'POST':
form = PageForm(request.POST)
if form.is_valid():
if cat:
page = form.save(commit=False)
page.category = cat
page.views = 0
page.save()
# probably better to use a redirect here.
return category(request, category_name_slug)
else:
print form.errors
else:
form = PageForm()
context_dict = {'form':form, 'category': cat}
return render(request, 'rango/add_page.html', context_dict)
To help you with the exercises above, the following hints may be of some use to you.