Displaying Forms in Django

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

Note: Before following along, if you have running instance of Django Shell kill it and restart it again using python manage.py shell command.

Django provides the following three methods to display form elements:

>>> from blog.forms import AuthorForm
>>> f = AuthorForm()
Form Method Code in template Rendered HTML
as_p() {{ f.as_p }} <p><label for="id_name">Author Name:</label><input id="id_name" maxlength="50" name="name" type="text" required /></p><p><label for="id_username">Username:</label><input id="id_username" maxlength="50" name="username" type="text" required /></p><p><label for="id_email">Email:</label><input id="id_email" maxlength="254" name="email" type="email" required /></p><p><label for="id_active">Active:</label><input id="id_active" name="active" type="checkbox" /></p>
as_table() {{f.as_table}} <tr><th><label for="id_name">Author Name:</label></th><td><input id="id_name" maxlength="50" name="name" type="text" required /></td></tr><tr><th><label for="id_email">Email:</label></th><td><input id="id_email" maxlength="254" name="email" type="email" required /></td></tr><tr><th><label for="id_active">Active:</label></th><td><input id="id_active" name="active" type="checkbox" /></td></tr>
as_ul() {{ f.as_ul }} <li><label for="id_name">Author Name:</label><input id="id_name" maxlength="50" name="name" type="text" required /></li><li><label for="id_email">Email:</label><input id="id_email" maxlength="254" name="email" type="email" required /></li><li><label for="id_active">Active:</label><input id="id_active" name="active" type="checkbox" /></li>

Notice that rendered HTML don't have <form> tag and the submit button. These methods only output form fields only. Why ? Because just outputting form fields makes it easier to have multiple forms in the template. To make forms fully functional, we have to manually add <form> tag and submit button like this:

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

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

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

In addition to outputting form fields, in bound state of the form these methods also outputs validation errors.

>>>
>>> f2 = AuthorForm()
>>>
>>> f2.is_bound
False
>>>
>>> print(f2.as_p())
<p>
    <label for="id_name">Author Name:</label> 
    <input id="id_name" maxlength="50" name="name" type="text" required />
</p>
<p>
    <label for="id_email">Email:</label> 
    <input id="id_email" maxlength="254" name="email" type="email" required />
</p>
<p>
    <label for="id_active">Active:</label> 
    <input id="id_active" name="active" type="checkbox" />
</p>
>>>

Note: The output of print(f2.as_p()) is formatted for readability.

Lets bound f2 with some data.

>>>
>>> data = {
...  'name': 'author',
...  'active': True,
... }
>>>
>>>
>>> f2 = AuthorForm(data)
>>>
>>>
>>> f2.is_bound
True
>>>
>>> f2.is_valid()
False
>>>
>>> print(f2.as_p())
<ul class="errorlist">
    <li>Author name can&#39;t be &#39;admin/author&#39;</li>
</ul>
<p>
    <label for="id_name">Author Name:</label>
    <input id="id_name" maxlength="50" name="name" type="text" value="author" required />
</p>
<ul class="errorlist">
    <li>This field is required.</li>
</ul>
<p>
    <label for="id_email">Email:</label>
    <input id="id_email" maxlength="254" name="email" type="email" required />
</p>
<p>
    <label for="id_active">Active:</label>
    <input checked="checked" id="id_active" name="active" type="checkbox" />
</p>
>>>
>>>

Notice how Django shows error using unordered list just above the field to which it belongs.

Creating Custom Admin Panel - cadmin #

Templates we will be creating in the upcoming section belongs to the new app called cadmin. The cadmin is a custom admin panel just like Django Admin. The users of our site will use this admin panel to manage their content. Just as Django admin, cadmin will be a separate app. However, it will use models from our blog app. Before we do anything else, let's create cadmin app.

Open the terminal or command prompt and enter the following command to create cadmin app.

(env) C:\Users\Q\TGDB\django_project>python manage.py startapp cadmin
C:\Users\Q\TGDB\django_project

(env) C:\Users\Q\TGDB\django_project>

Inform our Django project about the existence of cadmin app by adding it into the INSTALLED_APP list in settings.py file. Open settings.py file in TGDB/django_project/django_project add "cadmin" to the INSTALLED_APP as follows:

TGDB/django_project/django_project/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'cadmin'
]

Create a urls.py file for cadmin app and add the following code to it:

TGDB/django_project/cadmin/urls.py

from django.conf.urls import url, include
from cadmin import views

urlpatterns = [
    url(r'^post/add/$', views.post_add, name='post_add'),
]

Now we need to add this URLConf to the sitewide urls.py.

Open sitewide urls.py file in Django project configuration directory (TGDB/django_project/django_project) and url(r'^cadmin/', admin.site.urls), to the end of the urlpatterns list as follows:

urlpatterns = [
    url(r'', include('blog.urls')),
    url(r'^admin/', admin.site.urls),
    url(r'^cadmin/', include('cadmin.urls')),
]

Creating base template for cadmin app #

Create a new directory called templates in the cadmin app. Inside the templates directory create another directory called cadmin.

Next, create a new template called base.html inside the directory cadmin/templates/cadmin and add the following code to it:

TGDB/django_project/cadmin/templates/cadmin/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}The Great Django Blog :: Admin Panel{% endblock %}</title>
</head>
<body>

    {% block content %}

    {% endblock %}

    <div class="footer">
        <div class="section-inner clearfix">
            <p>&copy; The Great Django Blog</p>
        </div>
    </div>

</body>
</html>

Create another file called base_admin.html and add the following code to it:

{% extends "cadmin/base.html" %}

{% block content %}

    <div class="header">
        <div class="section-inner clearfix">
            <h2 class="title"><a href="{% url 'post_list' %}">The Great Django Blog - Admin Panel</a></h2>
            <div class="user-welcome">
                <p>Welcome {{ request.user.username }}</p>
            </div>
        </div>
    </div>

    <div class="content">
        <div class="section-inner">
            <div class="sidebar">
                <ul>
                    <li><a href="{% url 'post_list' %}">Post</a></li>
                    <li><a href="">Category</a></li>
                    <li><a href="">Tag</a></li>
                    <li><a href="">Account Info</a></li>
                    <li><a href="">Change Password</a></li>
                    <li><a target="_blank" href="">View Site</a></li>
                    <li><a href="">Logout</a></li>
                </ul>
            </div>

            {% block main %}

            {% endblock %}

        </div>

    </div>

{% endblock %}

Here we are creating two base templates: base.html and base_admin.html. The base_admin.html inherits from base.html. So Why two base templates ? Because we have two layouts one for the login and logout page and second one for inner pages. In other words, login.html and logout.html will inherit from base.html, while all the inner pages of the cadmin app will inherit from base_admin.html.

Create a new file called post_add.html with the following code:

{% extends "cadmin/base_admin.html" %}

{% block title %}
    Add New Post - {{ block.super }}
{% endblock %}

{% block main %}

    <p>&#187; <a href="">All Posts</a> &#187; Add Post</p>

    <h3>Add Post</h3>

    <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 {% csrf_token %}. The csrf_token is a special tag in Django which helps us 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 a HTTP 403 FORBIDDEN error like this.

403-forbidden.png

Let's now add a view function to allows users to create new post. Open views.py from cadmin app add post_add() view function as follows:

TGDB/django_project/cadmin/views.py

from django.shortcuts import render, redirect
from blog.forms import PostForm

# Create your views here.


def post_add(request):

    # If request is POST, create a bound form (form with data)
    if request.method == "POST":
        f = PostForm(request.POST)

        # check whether form is valid or not
        # if the form is valid, save the data to the database
        # and redirect the user back to the add post form

        # If form is invalid show form with errors again
        if f.is_valid():
            #  save data
            f.save()
            return redirect('post_add')

    # if request is GET the show unbound form to the user
    else:
        f = PostForm()
    return render(request, 'cadmin/post_add.html', {'form': f})

Here is how it works:

If request is coming using GET method, then create an unbound form object (line 25) and return the response to the user (line 26).

If request is coming using POST method, bind the form with the data by passing request.POST to the PostForm() (line 11). Then validate the form using is_valid() method (line 18). If form data is valid, save the category to the database (line 20) and redirect the user back to URL named post_add (line 21). On the other hand, if validation failed, control comes out of the if-else statement and returns a new response containing form data as well as validation errors. This time form is bound with data, so that the form can be pre-populated with field data that was filled in the previous request.

To view the fruits of our labour visit http://127.0.0.1:8000/cadmin/post/add/ and you will get a page like this:

add-post-form.png

Let's try creating a post. In the Add Post form, enter data in all the fields except title and content, then submit the form. You should get validation errors like this:

bound-add-post-form.png

Notice the validation errors above the title and content field. Django also pre-populates author, category and tags fields with data from the previous request.

Enter the some data in the title and content field and submit the form again. This time you should get an empty form without any validation errors or pre-populated data. It means our post is successfully saved into the database.

Updating Posts #

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

>>>
>>> from blog.models import Post
>>> from blog.forms import PostForm
>>>
>>> p = Post.objects.get(id=3)
>>> p
<Post: The Great Django Blog>
>>> f = PostForm(instance=p)
>>>
>>> print(f.as_p())
<p>
    <label for="id_title">Title:</label>
    <input id="id_title" maxlength="200" name="title" type="text" value="The Great Django Blog" required />
</p>
<p>
    <label for="id_content">Content:</label>
    <textarea cols="40" id="id_content" name="content" rows="10" required>
A Django project can be configured with one or several template engines (or even
 zero if you don&#39;t use templates). Django ships built-in backends for its ow
n template system, creatively called the Django template language (DTL), and for
 the popular alternative Jinja2.
     </textarea>
</p>
<p>
    <label for="id_author">Author:</label>
    <select id="id_author" name="author" required>
        <option value="">---------</option>
        <option value="6">tom</option>
        <option value="7">jerry</option>
        <option value="8">spike</option>
        <option value="9" selected="selected">tyke</option>
        <option value="10">jetson</option>
    </select>
</p>
<p>
    <label for="id_category">Category:</label>
    <select id="id_category" name="category" required>
        <option value="">---------</option>
        <option value="1" selected="selected">python</option>
        <option value="3">java</option>
        <option value="4">cat</option>
    </select>
</p>
<p>
    <label for="id_tags">Tags:</label>
    <select multiple="multiple" id="id_tags" name="tags" required>
        <option value="1">django</option>
        <option value="2" selected="selected">flask</option>
    </select>
</p>
>>>

Note: The output of print(f.as_p()) is formatted for better readability.

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 say, "Our form has data so it should be in bound state" right ? The answer is: No, the form is still in 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.

>>>
>>> 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, along with instance attribute like this:

>>>
>>> f = PostForm({}, instance=c)
>>> f.is_bound
True
>>>

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

In real world we would be passing request.POST instead of 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 post update form.

Open cadmin app's views.py and add the post_update() view function after post_add() view as follows:

TGDB/django_project/cadmin/views.py

from django.shortcuts import render, redirect, get_object_or_404, reverse
from blog.forms import PostForm
from blog.models import Post, Author, Category, Tag

...


def post_update(request, pk):
    post = get_object_or_404(Post, pk=pk)

    # If request is POST, create a bound form(form with data)
    if request.method == "POST":
        f = PostForm(request.POST, instance=post)

        # check whether form is valid or not
        # if the form is valid, save the data to the database
        # and redirect the user back to the update post form

        # If form is invalid show form with errors again
        if f.is_valid():
            f.save()
            return redirect(reverse('post_update', args=[post.id]))

    # if request is GET the show unbound form to the user, along with data
    else:
        f = PostForm(instance=post)

    return render(request, 'cadmin/post_update.html', {'form': f, 'post': post})

Create a new template template called post_update.html in cadmin/templates/cadmin, with the following code:

TGDB/django_project/cadmin/templates/cadmin/post_update.html

{% extends "cadmin/base_admin.html" %}

{% block title %}
    Update Post - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="main">
        <p>&#187; <a href="">All Posts</a> &#187; Post Update</p>

        <h3>Post Update</h3>

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

    </div>

{% endblock %}

Finally, add a URL pattern called post_update to cadmin's urls.py as follows:

TGDB/django_project/cadmin/urls.py

urlpatterns = [
    url(r'^post/add/$', views.post_add, name='post_add'),
    url(r'^post/update/(?P<pk>[\d]+)/$', views.post_update, name='post_update'),
]

Our post update page is ready, visit http://127.0.0.1:8000/cadmin/post/update/1/ to edit the post whose primary key is 1. You should see post update page like this:

post-update-page.png

Note: In case you don't have a post whose primary key is 1, then simply specify any primary key which does exists in your blog_post table.

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

Specifying the primary key of the post manually in the URL is slightly awkward! That's why, in the next step we will add a link to post update page in post detail page.

Open post_detail.html in the blog app and update the template to include a link to post_update URL pattern as follows:

TGDB/django_project/blog/templates/blog/post_detail.html

...
        <div class="section-inner clearfix">
            <h1>{{ post.title|capfirst }}</h1>
            <p class="post-info">
                {% include 'blog/post_info.html' %}
                | <span><a href="{% url 'post_update' post.id  %}">Edit</a></span>
            </p>
            <p>
                {{ post.content }}
            </p>
        </div>
...

Now visit post detail page and you should see a link with anchor "Edit", just after the tag like this:

post-update-link-in-post-detail-page.png

Click on the link and it will take you to the post update page for that post.

post-update-page-2.png

That's enough for now, we will add pages to add/update categories and tags later.