Displaying Forms in Django

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

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 submit button. These methods only output form fields only. Why ? Because just ouputting 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>
>>>

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

Note: Output is formatted to make it readable

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\K\TGDB\django_project>python manage.py startapp cadmin
C:\Users\C\TGDB\django_project

(env) C:\Users\C\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:

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:

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:

<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %}
    <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="{% url 'category_list' %}">Category</a></li>
                    <li><a href="{% url 'tag_list' %}">Tag</a></li>
                    <li><a href="{% url 'account_info' %}">Account Info</a></li>
                    <li><a href="{% url 'password_change' %}">Change Password</a></li>
                    <li><a target="_blank" href="{% url 'post_list' %}">View Site</a></li>
                    <li><a href="{% url 'logout' %}">Logout</a></li>
                    <li></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 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 every form must have csrf_token tag, otherwise on submitting it you would get a HTTP 403 FORBIDDEN error like this.

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

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 the request is GET, then create an unbound form object (line 55) and return the response to the user (line 56).

If request is POST, then create a bound form by passing request.POST to the PostForm() (line 41). Then, validate the form using is_valid() method (line 49). If form is valid, save the category to the database (line 50) and redirect the user back to URL named post_add (line 41). On the other hand if validation failed, control comes out of the if else statement and returns a new response. 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:

[]

As you can see this form is same as the one provided by Django Admin app. Use this form to create some new posts.

[]

Please note that just like Add post form in Django admin this form will provide validation too.

Updating Posts

In updating a record the first step is to show a form prepopluated with data from the database. Django provides instance attribute just for this task.

>>>
>>> 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 to make it readable.

As you can see in the above output all the form fields is pre-poplulated with data from the database, thanks to the instance attribute.

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 restricted to only 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)

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

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

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

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:

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:

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.

Specifying primary of the post in the URL is slightly awkward, 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:

{% block content %}

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

{% endblock %}

Now visit Post Detail page and you should see a link with anchor "Edit" after author name like this:

[post_detail_page]

Click the link and it will take you to the Post Update page.

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