OverIQ.com

Template Inheritance in Django

Last updated on July 27, 2020


Including templates using include tag #

The include tag allows us to include the contents of a template inside another template. Here is the syntax of the include tag:

{% include template_name %}

The template_name could be a string or a variable.

Let's take an example:

Create a new file named nav.html for the blog app in the directory blog/templates/blog and add the following code to it:

TGDB/django_project/blog/templates/blog/nav.html

1
2
3
4
5
6
<nav>
    <a href="#">Home</a>
    <a href="#">blog</a>
    <a href="#">Contact</a>
    <a href="#">Career</a>
</nav>

Say you want to include the contents of nav.html in the datetime.html file. To do this, add {% include 'blog/nav.html' %} tag to the datetime.html as follows:

TGDB/django_project/blog/templates/blog/datetime.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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% include 'blog/nav.html' %}

    {# This is a comment #}

    {# check the existence of now variable in the template using if tag #}

    {% if now %}   
        <p>Current date and time is {{ now }}</p>
    {% else %}               
        <p>now variable is not available</p>
    {% endif %}

    <p>Current scheme: {{ request.scheme }}</p>

</body>
</html>

Open your browser and visit http://127.0.0.1:8000/blog/time/, you should be able to see a simple navigation bar at the top of the page like this:

Notice that in the include tag we are using the same convention to specify template name that we have used in the get_template() and render() method. Instead of hardcoding the template name directly in the template we can also pass it using a context variable.

Open views.py file inside the blog app. At this point, today_is() view function should look like this:

TGDB/django_project/blog/views.py

1
2
3
4
#...
def today_is(request):
    now = datetime.datetime.now()
    return render(request, 'blog/datetime.html', {'now': now })

Modify it as follows:

TGDB/django_project/blog/views.py

1
2
3
4
#...
def today_is(request):
    now = datetime.datetime.now()
    return render(request, 'blog/datetime.html', {'now': now, 'template_name': 'blog/nav.html' })

Open datetime.html template and replace {% include "blog/nav.html" %} tag with
{% include template_name %}.

TGDB/django_project/blog/templates/blog/datetime.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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% include template_name %}

    {# This is a comment #}

    {# check the existence of now variable in the template using if tag #}

    {% if now %}   
        <p>Current date and time is {{ now }}</p>
    {% else %}               
        <p>now variable is not available</p>
    {% endif %}

    <p>Current scheme: {{ request.scheme }}</p>

</body>
</html>

Refresh the page at http://127.0.0.1:8000/blog/time/ and you should see current date and time just as before.

What if include tag fails to find the template?

In that case, Django will do one the following two things:

  1. If DEBUG mode is set to True then Django template system will throw TemplateDoesNotExist exception.
  2. If DEBUG is set to False then it will fail silently displaying nothing in the place of include tag.

Template Inheritance #

So far we have been creating very simple templates but in the real world, this is rarely the case. To give a common look and feel across all the pages of a website some HTML code is repeated. Typically header, navigation, footer, and sidebar remains the same throughout the site. The problem arises when we want to modify some part of the page. To better understand the issue, let's take an example.

Suppose we have two pages namely home.html and contact.html.

home.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
31
32
33
34
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
    
    <nav>
        <a href="#">Home</a>
        <a href="#">blog</a>
        <a href="#">Contact</a>
        <a href="#">Career</a>
    </nav>

    <aside>
        <h3>Ut enim ad minim veniam</h3>
        <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod            
            quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo.
        </p>
    </aside>

    <article>
        <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
            tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam
        </p>
    </article>

    <footer> &copy; 2017. All rights reserved </footer>

</body>
</html>

contact.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
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Contact</title>
</head>
<body>
    
    <nav>
        <a href="#">Home</a>
        <a href="#">blog</a>
        <a href="#">Contact</a>
        <a href="#">Career</a>
    </nav>

    <aside>
        <h3>Ut enim ad minim veniam</h3>
        <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod            
            quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo.
        </p>
    </aside>

    <article>
        <p>
            <form>
                 Name: <input type="text" name="name" required> <br>
                 Email: <input type="email" name="email" required> <br>
                 Query: <textarea required></textarea> <br>
                 <input type="submit" name="submit" value="Send">
            </form>
        </p>
    </article>

    <footer> &copy; 2017. All rights reserved </footer>

</body>
</html>

Let's say at the time of deployment the company decided to change the anchor text of a link in the navigation from Career to Jobs. To make this change we have to manually visit each and every page. What if we had 50 or 100 pages? These days it is very common for sites to have hundreds or even thousands of pages. As you can see this method doesn't scale very well.

If you are reading all these pages carefully, you might say "Hey why don't we use include tag ?". Yes, sure we can use include tag to solve this particular problem very easily but as we will see Template Inheritance provides a much more powerful and elegant method to tackle issues like this.

Sitewide Templates #

Django Templates which we are going to create in the upcoming section are not specific to any app. In fact, any installed app in the project can use these templates. As already discussed, by default Django looks for templates in the templates directory of every installed app. But Django doesn't define any default location to look for templates that are not specific to any app. Therefore, we can store these sitewide templates almost anywhere.

To keep things simple and organized we will store our sitewide templates in the templates directory inside the project root directory ( TGDB/django_project). Create a new directory named templates inside the TGDB/django_project directory.

Now we have to inform Django about this directory. Open settings.py file located at TGDB/django_project/django_project and find TEMPLATES variable. This variable contains every setting related to the templates. If you haven't changed anything it should look like this:

TGDB/django_project/django_project/settings.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
#...

The setting we are concerned here is the DIRS key. Currently, it is set to empty list. The DIRS key specifies a list of paths where template engine should look for templates. To make Django search for sitewide templates in our newly created templates directory we have to provide DIRS the absolute path to it.

On my PC the absolute path to templates is C:/Users/Q/TGDB/django_project/templates. Change the DIRS key so that it looks like this.

TGDB/django_project/django_project/settings.py

1
2
3
4
5
#...
'DIRS': [
    'C:/Users/Q/TGDB/django_project/templates',
],
#...

You should replace Q with your username.

There is one problem though, if you send your Django project to someone else, it will not work unless you and that person share the exact same username, drive letter and directory structure. The problem is that we are hardcoding paths. Hardcoding paths makes our project less portable - meaning it might not run on other computers.

The solution is to use dynamic paths.

Notice the BASE_DIR variable at the top of the settings.py file.

TGDB/django_project/django_project/settings.py

1
2
3
4
5
6
7
8
9
#...
import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
#...

This variable stores the path to Django project root directory. In my case, it points to C:/Users/Q/TGDB/django_project. On your machine, it might be different. Now we have access to Django project root directory in the form of BASE_DIR. As a result, it becomes very easy to refer to other directories. Our sitewide templates directory is inside the directory pointed to by BASE_DIR.

We can now use Python's os.path.join() function to join multiple paths. Update DIRS value so that it looks like this:

TGDB/django_project/django_project/settings.py

1
2
3
4
5
#...
'DIRS': [ 
    os.path.join(BASE_DIR, 'templates'), 
],
#...

From now on Django will search templates directory for templates.

I know some of you might be thinking "How do I print the value of BASE_DIR variable inside a view function ?". Open views.py in the blog app and modify the file as follows.

TGDB/django_project/blog/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.http import HttpResponse
from django.shortcuts import render_to_response, render
import datetime
from django.conf import settings

#...

def today_is(request):
    now = datetime.datetime.now()
    return render(request, 'blog/datetime.html', {
                                    'now': now,
                                    'template_name': 'blog/nav.html' ,
                                    'base_dir': settings.BASE_DIR }
                                )

Open datetime.html and append following <p> tag at the end of the file.

TGDB/django_project/blog/templates/blog/datetime.html

1
2
3
4
5
6
7
#...
    <p>Current scheme: {{ request.scheme }}</p>

    <p>BASE_DIR : {{ base_dir }}</p>

</body>
</html>

Open your browser and visit http://127.0.0.1:8000/blog/time/, you should see a page like this:

However, If you just want to quickly print BASE_DIR value for testing purpose, add the a print() statement after the BASE_DIR variable as follows:

TGDB/django_project/django_project/settings.py

1
2
3
4
5
6
#...
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

print("BASE_DIR  = ",  BASE_DIR)
#...

Save the file and start the server if not already running. In the shell running the server you should see BASE_DIR value as follows:

Creating Sitewide Templates #

To use Template Inheritance we must first create a base template. A base template is just a simple skeleton that we will be used as a base for all other pages on our site. A base template defines areas where content can be inserted into. In Django, we define this area using the block tag. Create a new file named base.html inside the sitewide templates directory and add the following code to it.

TGDB/django_project/templates/base.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
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}The Great Django Blog{% endblock %}</title>
</head>
<body>
    
    <nav>
        <a href="#">Home</a>
        <a href="#">blog</a>
        <a href="#">Contact</a>
        <a href="#">Career</a>
    </nav>

    {% block content %}

    {% endblock %}

    <hr>

    {% block footer %}
        This is footer
    {% endblock %}

</body>
</html>

This is our base template. Here we are using a new template tag that you haven't yet encountered. The block tag accepts a single argument, block_name to specify the name of the block. Here we have created three blocks namely title, content and footer. The block tag defines a block that can be overridden by the child templates. What is a child template? A template which uses base.html as its base or more formally speaking a child template is a template which extends the base template. A child template can override, add or leave alone the contents of the parent block.

To provide some default content to the child templates add the content between {% block %} and {% endblock %}.

To use our base template we first have to create a child template. Open datetime.html template from the blog app, delete everything in it and then add the following code.

TGDB/django_project/blog/templates/blog/datetime.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{% extends "base.html" %}

{% block content %}

    {% if now %}
        <p>Current date and time is {{ now }}</p>
    {% else %}
        <p>now variable is not available</p>
    {% endif %}

    <p>Current scheme: {{ request.scheme }}</p>
    <p>BASE_DIR : {{ base_dir }}</p>

{% endblock %}

Here is how it works:

In line 1, {% extends "base.html" %} tells Django template engine that the current template is a child template and inherits from base.html. This must be the first line in the child template otherwise it won't work. When template engine encounters this line it immediately loads the parent template (i.e base.html) and then replaces the blocks of content in parent template with the blocks of content of the same name defined in the child template. If a block of matching name is not found in the child template then the content from the parent template is used. Notice that in child template we are not overriding title and footer blocks, so the default content from parent template will be used while rendering the child template.

Start the server if not already running and visit http://127.0.0.1:8000/blog/time/, you should get the following output:

To change the title in the child template add the title block in datatime.html as follows:

TGDB/django_project/blog/templates/blog/datetime.html

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

{% block title %}
    Title from child template
{% endblock %}

{% block content %}

    {% if now %}
        <p>Current date and time is {{ now }}</p>
    {% else %}
        <p>now variable is not available</p>
    {% endif %}

    <p>Current scheme: {{ request.scheme }}</p>
    <p>BASE_DIR : {{ base_dir }}</p>

{% endblock %}

</body>
</html>

Refresh the page and you will get following output:

Say you have overridden a block in the child template but you still want default content from the parent template. To do that use {{ block.super }}. Modify the footer block in datatime.html as follows:

TGDB/django_project/blog/templates/blog/datetime.html

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

{% block title %}
    Title from child template
{% endblock %}

{% block content %}

    {% if now %}
        <p>Current date and time is {{ now }}</p>
    {% else %}
        <p>now variable is not available</p>
    {% endif %}

    <p>Current scheme: {{ request.scheme }}</p>
    <p>BASE_DIR : {{ base_dir }}</p>

{% endblock %}

{% block footer %}
    {{ block.super }}
    <p>All rights reserved</p>
{% endblock %}

Although, we are overriding footer in the child template, {{ block.super }} allows us to refer to the default content from the parent template inside the child template.

Refresh the page and you will get the following output:

Note: To checkout this version of the repository type git checkout 10a.