Set up Django forms and understand their structure and workflow

Problem

You want to set up Django forms to process data from users. You also want to understand how the structure and workflow associated with Django forms works.

Solution

A Django form is defined as a Python class that inherits its behavior from Django's forms.Form package/class. By using a Django form class you get to define form functionality in a single location, support form data validation and get tight integration with Django models. You can select from over two-dozen field types to generate a Django form class to enforce the input of a form's fields (e.g. text, number, email).

Django forms are dispatched and processed by standard Django view methods via GET and POST requests. Thanks to Django's forms.Form package/class, a Django form instance has access to various shortcut methods and initializers to make the validation and population of forms easy. In addition, Django forms also have built-in protection to thwart CSRF (Cross-Site Request Forgery) attacks.

How it works

Forms are the standard way users input or edit data in web applications. At the their lowest-level, forms are just made up of HTML tags with special meaning. While you can directly add a form's HTML tags to a Django template, you really want to avoid this and use Django's built-in form support to make form processing easier.

Django has a special forms package that offers a comprehensive way to work with forms. Among this package's features are the ability to define form functionality in a single location, support for form data validation and tight integration with Django models, among other things. Let's take a first look at a standalone Django form class in listing 1 which is used to back a contact form.

Listing 1 - Django form class definition


from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')  
      comment = forms.CharField(widget=forms.Textarea)

The first important aspect to note in listing 1 is the Django form definition is a sub-class of the forms.Form class, so it automatically has all the base functionality of this parent class. Next, you can see the form class has three attributes, two of the type forms.CharField and one of the type forms.EmailField. These form field definitions provide a way to restrict the input to a given type. For example, forms.CharField indicates the expected input is a set of characters and forms.EmailField indicates the expected input is an email. In addition, you can see each form field includes properties (e.g.required) to further set the behavior of the expected input. For the moment this should be enough detail about Django form field types, the recipe Django form field types: Widgets, options and validations goes into greater detail on just this topic.

Next, let's integrate the Django form in listing 1 to a Django view method so it can then be passed and rendered in a Django template. Listing 2 illustrates the initial iteration of this view method.

Listing 2 - Django view method that uses Django form

from django.shortcuts import render

def contact(request):
    form = ContactForm()
    return render(request,'about/contact.html',{'form':form})

The view method in listing 2 first instantiates the ContactForm form class and assigns it to the form reference. This form reference is then passed as an argument to be made available inside the about/contact.html template.

Next, inside the Django template you can output a Django form as a regular variable. Listing 3 illustrates how the Django form is rendered if you use the standard template syntax {{form.as_table}}.

Listing 3 - Django form instance rendered in template as HTML

<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" name="name" type="text" /></td></tr>
<tr><th><label for="id_email">Your email:</label></th><td><input id="id_email" name="email" type="email" /></td></tr>
<tr><th><label for="id_comment">Comment:</label></th><td><textarea cols="40" id="id_comment" name="comment" rows="10">
</textarea></td></tr>

In listing 3 you can see how the Django form is translated into HTML tags! If you look closely at listing 3, the HTML output for the Django form instance are just inner HTML table tags (e.g.<tr>,<th>,<td>). So the output is missing an HTML <table> wrapper tag and supporting HTML form tags (i.e. <form> tag and action attribute to indicate where to send the form, as well as a submit button). This means you'll need to add the missing HTML form tags to the template to create a working web form -- a process that's described shortly in listing 4.

In addition, if you don't want to output the entire form fields surrounded by HTML table elements like listing 3, there are many other syntax variations to output form fields granularly and stripped of HTML tags. In this case, the {{form.as_table}} reference in the template is used to simplify things, but the recipe Set up the layout for Django forms in templates elaborates on the different syntax variations to output Django forms in templates.

Next, let's take a look at figure 1 which shows a Django form's workflow to better illustrate how Django forms work from start to finish.

Figure 1 - Django forms workflow
Django forms workflow

The first two steps of the workflow in Figure 1 is what I've described up to this point. It consists of a user hitting a URL that's processed by a view method which returns an empty form based on the Django form class definition. Listing 3 shows the raw HTML output of the Django form, but as I already mentioned, the form is missing elements in order to become a functional web form, next I'll describe the elements you need to add to get a functional web form.

Functional web form syntax for Django forms

So far you've learned how a Django form class definition can quickly be turned into an HTML form. But this automatic HTML generation is only part of the benefit of using Django form class definitions, you can also validate form values and present errors to end users much more quickly. To perform these last actions, it's first necessary to have a functional web form. Listing 4 illustrates the template syntax to create a functional web form from a Django form.

Listing 4 - Django form template declaration for functional web form

<form method="POST">
  {% csrf_token %}

<table>
{{form.as_table}}
</table>
<input type="submit" value="Submit form">
</form>

The first thing to notice about listing 4 is the form is wrapped around the HTML <form> tag which is standard for all web forms. The reason Django forces you to explicitly set the <form> tag is because its attributes dictate much of a web form's behavior and can vary depending on the purpose of the form.

In listing 4, the method attribute tells a web browser that when a form is submitted it POST the data to the server. The POST method value is standard practice in web forms that process user data -- an alternative method option value is GET, but it's not a typical choice for transferring user data. The use of the POST method should become clearer shortly, but for more background on method values see HTTP request methods on wikipedia.

Another important <form> attribute -- which is actually missing in listing 4 -- is action which tells a web browser where to submit the form (i.e. what URL is charged with processing the form). In this case, because listing 4 has no action attribute, a browser's behavior is to POST the data to the same URL where it's currently on, meaning if the browser got the form from the /contact/ URL, it will POST the data to the same /contact/ URL. If you wanted to POST the form to a separate URL, then you would add the action attribute (e.g. action="/urltoprocessform/") to the <form> tag.

Keep in mind that because the same URL delivers the initial form -- via a GET request -- and must also process the form data -- via a POST request -- the backing view method of the URL must be designed to handle both cases. I'll describe a modified version of listing 2 -- which just handles the GET request case -- that also handles the POST case in the next section.

The {% csrf_token %} statement in listing 4 is a Django tag. {% csrf_token %} is a special tag reserved for cases when a web form is submitted via POST and processed by Django. The csrf initials mean Cross-Site Request Forgery, which is a default security mechanism enforced by Django. While it's possible to disable CSRF and not include the {% csrf_token %} Django tag in forms, I would advise against it and recommend you keep adding the {% csrf_token %} Django tag to all forms with POST, as CSRF works as a safeguard and mostly behind the scenes. The last section in this recipe describes in greater detail the reasoning behind CSRF and how it works in Django.

Next in listing 4 is the {{form.as_table}} snippet wrapped in an <table> tag which represents the Django form instance and outputs the HTML output illustrated in listing 3. Finally, there's the <input type="submit"> tag which generates the form's submit button -- that when clicked on by a user submits the form -- and the closing </form> tag.

Django view method to process form (POST handling)

Once you have a functional web form in Django, it's necessary to create a view method to process it. In the previous section, I mentioned how the same URL and by extension view method, would both handle generating the blank HTML form, as well as process the HTML form with data. Listing 5 illustrates a modified version of the view method in listing 3 that does exactly this.

Listing 5 - Django view method that sends and processes Django form

from django.shortcuts import render
from django.http import HttpResponseRedirect

def contact(request):
    if request.method == 'POST':
        # POST, generate form with data from the request
        form = ContactForm(request.POST)
        # check if it's valid:
        if form.is_valid():
            # process data, insert into DB, generate email,etc
            # redirect to a new URL:
            return HttpResponseRedirect('/contact/thankyou')
    else:
        # GET, generate blank form
        form = ContactForm()
    return render(request,'about/contact.html',{'form':form})

The most important construct in the view method of listing 5 is the if/else condition that checks the request method type. If the request method type is POST (i.e. data submission or step 3 in figure 1) the form's data is processed, but if the request method type is anything else (i.e. initial request or step 1 in figure 1) an empty form is generated to produce the same behavior as listing 2. Notice the last line in listing 5 is a return statement that assigns the form instance -- whether empty/unbound or populated/bound -- and returns it to a template for rendering.

Now let's take a closer look at the POST logic in listing 5. If the request method type is POST it means there's incoming user data, so we access the incoming data with the request.POST reference and initialize the Django form with it, it's that simple! There's no need to access individual form fields or make piecemeal assignments -- although you could if you wanted to and this process is described in the next recipe -- using request.POST as the argument of a Django form class is sufficient to populate a Django form instance with user data. It's worth mentioning that a Django form instance created in this manner (i.e. with user provided data) is known as a bound form instance.

At this point, we still don't know if a user provided valid data with respect to a Django form's field definitions (e.g. if values are text or a valid email). To validate a form's data, we use the is_valid() helper method on the bound form instance. If form.is_valid() is True the data is processed and subsequent action is taken, in listing 5 this additional action consists of redirecting control to the /contact/thankyou URL. If form.is_valid() is False it means the form data has errors, after which point control falls to the last return statement which now passes a bound form instance to render the template. By using a bound form instance in this last case, the user gets a rendered form filled with his initial data submission and errors so he's able to correct the data without reintroducing values from scratch.

I purposely didn't mention any more details about the is_valid() helper method or error message displays, because Django form processing can get a little complex. The Django form processing: Initialization, field access, validation and error handling covers all you need to know about Django form processing so it doesn't interfere in this initial recipe covering Django form basics.

CSRF: What is it and how does it work with Django ?

CSRF or Cross-Site Request Forgery is a technique used by cyber-criminals to force users into executing unwanted actions on a web application. When users interact with web forms, they make all kinds of state-changing tasks that range from making orders (e.g. products, money transfers) to changing their data (e.g. name, email, address). Most users tend to feel a heightened sense of security when they interact with web forms because they see an HTTPS/SSL security symbol or they've used a username/password prior to interacting with a web form, all of which leads to a feeling there's no way a cyber-criminal could eavesdrop, guess or interfere with their actions.

A CSRF attack relies for the most part on social engineering and lax application security on a web application, so the attack vector is open irrespective of other security measures (e.g. HTTPS/SSL, strong password). Figure 2 illustrates a CSRF vulnerable scenario on a web application.

Figure 2 - Web application with no CSRF protection
Web application with no CSRF protection

After user "X" interacts with web application "A" (e.g. making an order, updating his email) he simply navigates away and goes to other sites. Like most web applications, web application "A" maintains valid user sessions for hours or days, in case users come back and decide to do other things without having to sign-in again. Meanwhile, a cyber-criminal has also used site "A" and knows exactly where and how all of its web forms work (e.g. URLs, input parameters such as email, credit card).

Next, a cyber-criminal creates links or pages that mimic the submission of web forms on web application "A". For example, this could be a form that changes a user's email in order to overtake an account or transfers money from a user's account to steal funds. The cyber-criminal then seeds the Internet with these links or pages through email, social media or other web sites with enticing or frightening headlines: "Get a $100 coupon from site 'A'", "Urgent: Change your password on site 'A' because of a security risk'. In reality, these links or pages don't do what they advertise, but instead in a single click mimic web form submissions from site "A" (e.g. change a user's email or transfer funds).

Now lets turn our attention to unsuspecting user "X" that visited site "A" hours or days earlier. He catches a glimpse of these last advertisements and thinks "Wow, I can't pass this up". Thinking what harm can a click do, he clicks on the bogus advertisement, the user is then sent to a legitimate site 'A' page as a façade or the click 'appears' to have done nothing. User "X" thinks nothing of it and goes back to some other task. If site 'A' did not have web forms with CSRF protection, then user "X" just inadvertently -- in a single click -- performed an action on site "A" he wasn't aware of.

As you can see, in order to perform a CSRF attack all that's needed is for a user to have an active session on a given site and a cyber-criminal crafty enough to trick a user into clicking on a link or page that performs actions on said site. Hence the term's name: "Cross-Site", because the request doesn't come from the original site, and "Request Forgery" because it's a forged request by a cyber-criminal.

To protect against web form CSRF attacks, it's isn't sufficient for web applications to trust authenticated users, because as I've just described authenticated users could have inadvertently triggered actions they weren't aware of. Web forms must be equipped with a unique identifier -- often called a CSRF token -- that's unique to a user and has an expiration time, similar to a session identifier. In this manner, if a request is made by an authenticated user to a site, only requests that match his CSRF token are considered valid and all other requests are discarded, as illustrated figure 3.

Figure 3 - Web application with CSRF protection
Web application with CSRF protection

As you can see in figure 3, the inclusion of a CSRF token in a web form makes it very difficult to forge a user's request.

In Django, a CSRF token is generated in web forms with the {% csrf token %} tag that generates an HTML tag in the form <input type="hidden" name="csrfmiddlewaretoken" value="32_character_string">, where the 32-character string value varies by user. In this manner, if a Django application makes a POST request -- like those made by web forms -- it will only accept the request if the CSRF token is present and valid for a given user, otherwise it will generate a '403 Forbidden' page error.

Be aware CSRF is enabled by default on all Django applications thanks to its out-of-the-box middleware settings that includes the django.middleware.csrf.CsrfViewMiddleware class charged with enforcing CSRF functionality. If you wish to disable CSRF support in a Django application completely, you can just remove the django.middleware.csrf.CsrfViewMiddleware class from the MIDDLEWARE_CLASSES variable in settings.py.

If you wish to enable or disable CSRF on certain web forms, you can selectively use the @csrf_exempt() and @csrf_protect decorators on the view methods that process a web form's POST request.

To enable CSRF on all web forms and disable CSRF behavior on certain web forms, keep the django.middleware.csrf.CsrfViewMiddleware class in MIDDLWARE_CLASSES and decorate the view methods that process web forms you don't want CSRF validation on with the @csrf_exempt() decorator, as illustrated in listing 6.

Listing 6 - Django view method decorated with @csrf_exempt() to bypass CSRF enforcement

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def contact(request):
    # Any POST processing inside view method
    # ignores if there is or isn't a CSRF token
    

To disable CSRF on all web forms and enable CSRF behavior on certain web forms, remove the django.middleware.csrf.CsrfViewMiddleware class from MIDDLWARE_CLASSES and decorate the view methods that process web forms you want CSRF validation on with the @csrf_protect() decorator, as illustrated in listing 7.

Listing 7 - Django view method decorated with @csrf_protect() to enforce CSRF when CSRF is disabled at the project level

from django.views.decorators.csrf import csrf_protect

@csrf_protect
def contact(request):
    # Any POST processing inside view method
    # checks for the presence of a CSRF token
    # even when CsrfViewMiddleware is removed