OverIQ.com

Displaying Forms in Django

Last updated on July 27, 2020


So far we have been using Django Shell to demonstrate how the forms work. In this lesson, we will learn how to display forms in our templates.

Django provides the following three methods to display form elements:

1
2
3
4
>>>
>>> from djangobin.forms import LanguageForm
>>> f = LanguageForm()
>>>
  1. as_p()
  2. as_table()
  3. as_ul()

as_p() #

This method displays form fields a series of <p> tags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>>
>>> f.as_p()
<p><label for="id_name">Name:</label> <input type="text" name="name" maxlength="100" required id="id_name" /></p>
<p><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" required id="id_lang_code" /></p>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" required id="id_slug" /></p>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></p>
<p><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></p>
<p><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></p>
>>>
>>>

as_table() #

This method displays form fields a series of <tr> tags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>>
>>> f.as_table()
<tr><th><label for="id_name">Name:</label></th><td><input type="text" name="name" maxlength="100" required id="id_name" /></td></tr>
<tr><th><label for="id_lang_code">Lang code:</label></th><td><input type="text" name="lang_code" required id="id_lang_code" /></td></tr>
<tr><th><label for="id_slug">Slug:</label></th><td><input type="text" name="slug" required id="id_slug" /></td></tr>
<tr><th><label for="id_mime">Mime:</label></th><td><input type="text" name="mime" required id="id_mime" /></td></tr>
<tr><th><label for="id_created_on">Created on:</label></th><td><input type="text" name="created_on" required id="id_created_on" /></td></tr>
<tr><th><label for="id_updated_on">Updated on:</label></th><td><input type="text" name="updated_on" required id="id_updated_on" /></td></tr> 
>>>
>>>

as_ul() #

This method displays form fields a series of <li> tags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>>
>>> f.as_ul()
<li><label for="id_name">Name:</label> <input type="text" name="name" maxlength="100" required id="id_name" /></li>
<li><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" required id="id_lang_code" /></li>
<li><label for="id_slug">Slug:</label> <input type="text" name="slug" required id="id_slug" /></li>
<li><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></li>
<li><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></li>
<li><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></li>
>>>
>>>

Notice that the rendered HTML doesn't have the <form> tag and the submit button. The form methods only output the form fields. To make forms fully functional, we have to manually add <form> tag and submit button like this:

1
2
3
4
<form action="/url-to-submit/" method="post">    
    {{ form.as_p }}
    <input type="submit" value="Submit" />
</form>

We can also output form fields in the template by just typing {{ f }}, which is equivalent to {{ f.as_table }}.

1
2
3
4
<form action="/url-to-submit/" method="post">    
    {{ f }}
    <input type="submit" value="Submit" />
</form>

In bound state of the form, these methods also output validation errors along with the data filled in the previous request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>>
>>> f2 = LanguageForm()
>>>
>>> f2.is_bound
False
>>>
>>> print(f2.as_p())
<p><label for="id_name">Name:</label> <input type="text" name="name" maxlength="100" required id="id_name" /></p>
<p><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" required id="id_lang_code" /></p>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" required id="id_slug" /></p>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></p>
<p><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></p>
<p><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></p>
>>>

Let's bind the form f2 with some data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> 
>>> data = {
...     'name': 'sql',
...     'lang_code': 'sql',
...     'slug': 'sql###',               
... }
>>>
>>>
>>> f2 = LanguageForm(data)
>>>
>>> 
>>> f2.is_valid()
False
>>>
>>>
>>> print(f2.as_p())
<ul class="errorlist nonfield"><li>Slug and MIME shouldn&#39;t be same.</li></ul>
<p><label for="id_name">Name:</label> <input type="text" name="name" value="sql" maxlength="100" required id="id_name" /></p>
<p><label for="id_lang_code">Lang code:</label> <input type="text" name="lang_code" value="sql" required id="id_lang_code" /></p>
<ul class="errorlist"><li>Enter a valid &#39;slug&#39; consisting of letters, numbers, underscores or hyphens.</li></ul>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" value="sql###" required id="id_slug" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" required id="id_mime" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_created_on">Created on:</label> <input type="text" name="created_on" required id="id_created_on" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_updated_on">Updated on:</label> <input type="text" name="updated_on" required id="id_updated_on" /></p>
>>>

We have explored the forms enough in the shell, let's now create a real form.

Creating a Real Form #

Open views.py file from djangobin app and add add_lang() view towards the end of the file:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from django.shortcuts import HttpResponse, render, redirect
from .forms import LanguageForm

#...

def profile(request, username):
    return HttpResponse("<p>Profile page of #{}</p>".format(username))


def add_lang(request):
    if request.POST:
        f = LanguageForm(request.POST)
        if f.is_valid():
            lang = f.save()
            return redirect('djangobin:add_lang')

    else:
        f = LanguageForm()

    return render(request, 'djangobin/add_lang.html', {'form': f} )

Here is how it works:

  1. When a GET request comes, we create an unbound LanguageForm (line 18) and render an empty form (line 20).

  2. If the request is POST (line 11), we create a bound form (line 12) and validate it using is_valid() method. If the form is valid, we save the language and redirect the user to the add language page. On the other hand, if the validation fails, control comes out of the if-else statement and we return a new response containing form data as well as the validation errors (line 20).

Next, open urls.py and add add_lang URL patterns at the end of the file:

djangobin/django_project/djangobin/urls.py

1
2
3
4
5
6
#...
urlpatterns = [
    #...
    url('^tag/(?P<tag>[\w-]+)/$', views.tag_list, name='tag_list'),
    url('^add-lang/$', views.add_lang, name='add_lang'),
]

Finally, here is the code for add_lang.html template.

djangobin/django_project/djangobin/templates/djangobin/add_lang.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends "base.html" %}

{% block title %}
    Add Lanugage
{% endblock %}

{% block content %}

    <h2>Add a new Language</h2>

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

Nothing new here, except the {% csrf_token %} tag. The csrf_token is a special tag which Django uses to prevent CSRF (Cross-Site Request Forgery) attacks. You don't need to know how it works internally, just put {% csrf_token %} in your form templates and Django will take care of everything else. If you are interested in learning more about CSRF attacks click here.

By default, Django expects you to add csrf_token tag on every form. If you don't do so, then on submitting the form you would get an HTTP 403 FORBIDDEN error like this:

Start the server using ./manage.py runserver and visit http://localhost:8000/add-lang/. You will see an Add language like this:

Enter some duplicate data in the fields with a unique constraint. You will get validation errors like this:

The validation error for a field is displayed above the field itself. Also, notice the error at the top of the form which says "Slug and MIME shouldn't be same.". This is a non-field error, as a result, it is displayed at the top of the form above all the fields.

Keep in mind that LanguageForm form is performing two validations: form validation and model validation. The non-field error ("Slug and MIME shouldn't be same") is coming from form validation whereas errors about duplicate data are coming from model validation.

Now, enter a new language which doesn't already exists in the database. This time, the form will save the data into the database and then you will be redirected to the Add language page.

Our data is saved but we didn't get any confirmation about it. We can easily fix this issue using flash messages.

Flash Message #

A web application is all about user experience. After every action, you must notify the user about the result of the operation. These notifications are also commonly known as Flash Messages. Django provides a built-in framework named django.contrib.messages to display flash messages. The django.contrib.messages framework already comes preinstalled so you don't have to configure anything to use it.

To display flash messages we have to first import messages package from the django.contrib package.

from django.contrib import messages

The messages package provides a function named add_message() to set flash messages. The add_message() function accepts two arguments, the request object and the message you want to display. Here is an example:

1
2
from django.contrib import messages
messages.add_message(request, 'Email Sent!')

We can also pass message levels to the add_message() function. The message levels allow us to format the flash message in the template. The following table lists built-in messages levels, which can be imported from django.contrib.messages package.

Constant Description
DEBUG It is used to display development related messages.
INFO It is used to display informational messages.
SUCCESS It is used to display success related messages.
WARNING It is used to display warning related messages.
ERROR It is used to display error related messages.

Here are some examples of how to set message levels:

1
2
3
4
5
6
7
from django.contrib import messages

messages.add_message(request, messages.DEBUG, '10 queries executed.')
messages.add_message(request, messages.INFO, 'Your are loggedin as staff member.')
messages.add_message(request, messages.SUCCESS, 'Email verification sent.')
messages.add_message(request, messages.WARNING, 'Change you password.')
messages.add_message(request, messages.ERROR, 'Failed to update the profile.')

To access flash messages in the templates, we use the messages variable as follows:

1
2
3
4
5
6
7
{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

Just like request variable, the messages is another one of those special variables which you always have access to inside the template if you use the render() function.

Now you know how to use flash messages let's put it to use in Add language form.

Open djangobin's views.py and modify to use django.contrib.messages framework as follows:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django.shortcuts import HttpResponse, render, reverse, redirect
from django.contrib import messages
from .forms import LanguageForm

#...

def add_lang(request):
    if request.POST:
        f = LanguageForm(request.POST)
        if f.is_valid():
            lang = f.save()
            messages.add_message(request, messages.INFO, 'Language saved.')
            return redirect('djangobin:add_lang')

    else:
        f = LanguageForm()

    return render(request, 'djangobin/add_lang.html', {'form': f} )

Next, to display the flash message modify add_lang.html template as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{% extends "base.html" %}

{% block title %}
    Add Lanugage
{% endblock %}

{% block content %}

    <h2>Add a new Language</h2>

    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

Revisit add post form at http://127.0.0.1:8000/cadmin/post/add/ and add a language. This time you will get a success message like this:

Updating Data via Forms #

The forms we have built until now, can only create new records? What about updating those records?

In updating a record the first step is to show a form pre-populated with data from the database. Django provides an instance parameter just for this task. The following shell session demonstrates how to use it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
>>> 
>>> from djangobin.models import *
>>> from djangobin.forms import *
>>> 
>>> 
>>> l = Language.objects.get(name='SQL')
>>> 
>>> l
<Language: SQL>
>>> 
>>>
>>> f = LanguageForm(instance=l)
>>> 
>>> print(f.as_p())
<p><label for="id_name">Name:</label> <input type="text" name="name" value="SQL" maxlength="100" id="id_name" required /></p>
<p><label for="id_lang_code">Language Code:</label> <input type="text" name="lang_code" value="sql" maxlength="100" id="id_lang_code" required /></p>
<p><label for="id_slug">Slug:</label> <input type="text" name="slug" value="sql" maxlength="100" id="id_slug" required /></p>
<p><label for="id_mime">Mime:</label> <input type="text" name="mime" value="text/x-sql" maxlength="100" id="id_mime" required /> <span class="helptext">MIME to use when sending snippet as file</span></p>
<p><label for="id_file_extension">File extension:</label> <input type="text" name="file_extension" value=".sql" maxlength="10" id="id_file_extension" required /></p>
>>> 
>>>

Notice that the output of print(f.as_p()) contains all the form fields pre-populated with data from the database.

At this point, you might think, "Our form has data so it should be in bound state" right? The answer is: No, the form is still in the unbound state. The use of instance attribute is only restricted to displaying data. That's it. We can verify this fact by using is_bound attribute on the form object.

1
2
3
4
>>> 
>>> f.is_bound
False
>>>

So how we bind data to the form while updating objects?

To bind the data to the form pass a dictionary containing data, along with the instance attribute like this:

1
2
3
4
5
6
>>> 
>>> f = LanguageForm({}, instance=l)
>>> 
>>> f.is_bound
True
>>>

Although the dictionary is empty, our form is still in the bound state.

In the real world, we would pass request.POST instead of an empty dictionary ({}).

f = CategoryForm(request.POST, instance=c)

Another important thing to keep in mind is that while saving the data, the save() method will use data from request.POST not from instance=c.

Let's use this knowledge to create Change language form.

Open djangobin's views.py and add the update_lang() view function below the add_lang() view as follows:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.shortcuts import HttpResponse, render, redirect, get_object_or_404
from django.contrib import messages
from .forms import LanguageForm
from .models import Language

#...

def add_lang(request):
    #...


def update_lang(request, lang_slug):
    l = get_object_or_404(Language, slug__iexact=lang_slug)
    if request.POST:
        f = LanguageForm(request.POST, instance=l)
        if f.is_valid():
            lang = f.save()
            messages.add_message(request, messages.INFO, 'Language Updated.')
            return redirect('djangobin:update_lang', lang.slug)

    else:
        f = LanguageForm(instance=l)

    return render(request, 'djangobin/update_lang.html', {'form': f} )

In urls.py file, add update_lang URL pattern as follows:

djangobin/django_project/djangobin/urls.py

1
2
3
4
5
6
7
#...

urlpatterns = [
    #...
    url('^add-lang/$', views.add_lang, name='add_lang'),
    url('^update-lang/(?P<lang_slug>[\w-]+)/$', views.update_lang, name='update_lang'),
]

And here is the code for update_lang.html template:

djangobin/django_project/djangobin/templates/djangobin/update_lang.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{% extends "base.html" %}

{% block title %}
    Change Lanugage
{% endblock %}

{% block content %}

    <h2>Change Language</h2>

    {% if messages %}
        <ul class="messages">
            {% for message in messages %}
                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </table>
    </form>

{% endblock %}

Visit http://localhost:8000/update-lang/sql/ and you will see Change language page like this:

Just like Add language page, Change language page shows validation errors as well as pre-populates the data from the previous request. When you are done updating the language, hit "Submit" button to save your changes to the database.