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. Its syntax is:

{% 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 in the djangobin app's templates directory ( i.e. templates/djangobin ) with the following code:

djangobin/django_project/djangobin/templates/djangobin/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 that, add {% include 'djangobin/nav.html' %} tag to the datetime.html as follows:

djangobin/django_project/djangobin/templates/djangobin/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 'djangobin/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/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 via context.

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

djangobin/django_project/djangobin/views.py

1
2
3
4
5
#...

def today_is(request):
    now = datetime.datetime.now()
    return render(request, 'djangobin/datetime.html', {'now': now })

Modify it as follows:

djangobin/django_project/djangobin/views.py

1
2
3
4
5
6
#...

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

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

djangobin/django_project/djangobin/templates/djangobin/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/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 of 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 the 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 site 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 with the following code:

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 name="message" 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 this page 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 ( djangobin/django_project/). Create a new directory named templates inside the djangobin/django_project/ directory.

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

djangobin/django_project/django_project/settings.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#...

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 /home/overiq/djangobin/django_project/templates. Change the DIRS key so that it looks like this.

djangobin/django_project/django_project/settings.py

1
2
3
4
5
#...
'DIRS': [
    '/home/overiq/djangobin/django_project/',
],
#...

You should replace overiq 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 and directory structure. The takeaway from here is that never hardcode paths in your Django project. 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.

djangobin/django_project/django_project/settings.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#...

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 /home/overiq/djangobin/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.

1
2
3
4
5
6
7
8
django_project/
├── db.sqlite3
├── djangobin
├── django_project
├── manage.py
└── templates   <--- sitewide templates directory

3 directories, 2 files

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

djangobin/django_project/django_project/settings.py

1
2
3
4
5
6
7
#...

'DIRS': [ 
    os.path.join(BASE_DIR, 'templates'), 
],

#...

From now on Django will search the djangobin/django_project/templates directory for templates. Keep in mind that Django first searches for templates in the directories specified by the DIRS setting and then in the templates directory of the respective apps (assuming APP_DIRS is set to True). That means if you have a template named datetime.html inside djangobin directory in the sitewide templates directory then the today_is() view function will use datetime.html from the sitewide templates directory instead of the templates directory of the djangobin app.

Accessing Settings inside Views #

There will be times when you want to have access to settings from settings.py file in your view functions. To do so, you have to import settings variable from the django.conf package.

Open views.py in the djangobin app and modify the file as follows.

djangobin/django_project/djangobin/views.py

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

#...


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

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

djangobin/django_project/djangobin/templates/djangobin/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/time/. You should see the path to your Django project root directory as follows:

Templates Inheritance in Action #

To use Template Inheritance we must first create a base template. A base template is just a simple skeleton that can be used as a base for all other pages of the site. A base template defines areas where content can be inserted into. 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.

djangobin/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 %}Default Title{% 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 %}
        <p>Creative Commons Attribution 4.0 International Licence</p>
    {% 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.

djangobin/django_project/djangobin/templates/djangobin/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 the 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/time/. You should get the following output:

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

djangobin/django_project/djangobin/templates/djangobin/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 %}
    Date & Time
{% 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 }}. The {{ block.super }} allows us to refer to the default content from the parent template inside the child template.

Modify the title block in datatime.html as follows:

djangobin/django_project/djangobin/templates/djangobin/datetime.html

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

{% block title %}
    Date & Time - {{ block.super }}
{% 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 %}

Refresh the page and you will get the following output: