DEV Community

Cover image for Understanding and Implementing Custom Template Tags in Django
Abdelrahman hassan hamdy
Abdelrahman hassan hamdy

Posted on • Edited on

Understanding and Implementing Custom Template Tags in Django

When you're developing a web application with Django, the built-in template tags and filters can handle a variety of tasks. However, sometimes you might need more power and flexibility to process the data you're working with, in which case, Django provides the option to write your custom template tags and filters.

Custom Template Tags

Getting Started

Before we delve into creating custom template tags, let's enhance our example application, Myblog, by adding a new Post detail page. This will involve:

  • Creating a new template for the post detail
  • Refactoring the byline rendering to remove duplicated code
  • Adding a link to the Post detail page in the index.html template
  • Adding a view to fetch and render the new template
  • Adding a URL mapping to the new view

First, create a new file post-detail.html inside the myblog/templates/blog directory, which will extend the base.html template and override the content block. The content for post-detail.html would look like this:

{% extends "base.html" %}
{% block content %}
<h2>{{ post.title }}</h2>
<div class="row">
    <div class="col">
        {% include "blog/post-byline.html" %}
    </div>
</div>
<div class="row">
    <div class="col">
        {{ post.content|safe }}
    </div>
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

The post_detail view in views.py would be:

from django.shortcuts import render, get_object_or_404

def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    return render(request, "blog/post-detail.html", {"post": post})
Enter fullscreen mode Exit fullscreen mode

Next, create another new file post-byline.html in the same directory. The content for post-byline.html would look like this:

{% load blog_extras %}
<small>By {{ post.author|author_details:request.user }} on {{ post.published_at|date:"M, d Y" }}</small>
Enter fullscreen mode Exit fullscreen mode

inside the blog_extras.py

from django import template
from django.contrib.auth import get_user_model
# from django.utils.html import escape
# from django.utils.safestring import mark_safe
from django.utils.html import format_html
from blog.models import Post

user_model = get_user_model()
register = template.Library()


@register.filter
def author_details(author, current_user):
    if not isinstance(author, user_model):
        # return empty string as safe default
        return ""

    if author == current_user:
        return format_html("<strong>me</strong>")

    if author.first_name and author.last_name:
        name = f"{author.first_name} {author.last_name}"
    else:
        name = f"{author.username}"

    if author.email:
        prefix = format_html('<a href="mailto:{}">', author.email)
        suffix = format_html("</a>")
    else:
        prefix = ""
        suffix = ""

    return format_html('{}{}{}', prefix, name, suffix)
Enter fullscreen mode Exit fullscreen mode

The index.html should now look like this:

{% extends "base.html" %}
{% block content %}
    <h2>Blog Posts</h2>
    {% for post in posts %}
    <div class="row">
        <div class="col">
            <h3>{{ post.title }}</h3>
            {% include "blog/post-byline.html" %}
            <p>{{ post.summary }}</p>
            <p>
                ({{ post.content|wordcount }} words)
            <a href="{% url "blog-post-detail" post.slug%}">Read More</a>
            </p>
        </div>
    </div>
    {% endfor %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Finally, add a new URL mapping in urls.py:

from django.urls import path
import blog.views

urlpatterns = [
    # ... other URL patterns ...
    path("post/<slug>/", blog.views.post_detail, name="blog-post-detail"),
]
Enter fullscreen mode Exit fullscreen mode

That's all we learned in the previous article

Creating a Simple Custom Template Tag

A simple custom template tag is built with a Python function that can take any number of arguments, even 0. This function is defined in a Python file inside the templatetags directory of a Django app. Let's create a custom tag for a Bootstrap row:

from django.utils.html import format_html
from django.template import Library

register = Library()

@register.simple_tag
def row(extra_classes=""):
    return format_html('<div class="row {}">', extra_classes)

@register.simple_tag
def endrow():
    return format_html("</div>")
Enter fullscreen mode Exit fullscreen mode

We can now use these tags in our templates:

{% load blog_extras %}
{% block content %}
    <h2>{{ post.title }}</h2>
    {% row %}
        <div class="col">
            {% include "blog/post-byline.html" %}
        </div>
    {% endrow %}
    {% row %}
        <div class="col">
            {{ post.content|safe }}
        </div>
    {% endrow %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Accessing the Template Context in Template Tags

So far, we have only examined how to access variables or values that are explicitly passed to a template tag function. However, with minor modifications to the way the tag is registered, we can enable the tag to access all the context variables that are available in the template in which it's used. This can be handy, for instance, when we need to access the request variable frequently without having to explicitly pass it into the template tag each time.

To provide the template tag access to the context, you need to make two modifications to the template tag function:

  • When registering, pass takes_context=True to the register.simple_tag decorator.
  • Add context as the first argument to the template tag function.

Here is an example that demonstrates how we can re-implement the author_details filter as a template tag that doesn't require any arguments:

@register.simple_tag(takes_context=True)
def author_details_tag(context):
    request = context["request"]
    current_user = request.user
    post = context["post"]
    author = post.author

    if author == current_user:
        return format_html("<strong>me</strong>")

    if author.first_name and author.last_name:
        name = f"{author.first_name} {author.last_name}"
    else:
        name = f"{author.username}"

    if author.email:
        prefix = format_html('<a href="mailto:{}">', author.email)
        suffix = format_html("</a>")
    else:
        prefix = ""
        suffix = ""

    return format_html("{}{}{}", prefix, name, suffix)
Enter fullscreen mode Exit fullscreen mode

The usage in the post-byline.html would look like this:

<small>By {% author_details_tag %} on {{ post.published_at|date:"M, d Y" }}</small>
Enter fullscreen mode Exit fullscreen mode

As you can see, we don't need to pass in any variables, although we could pass in arbitrary variables if we wanted. We have access to the template context and can access any variables we need by using it.

While the author_details_tag won't be used in the myblog project, you can still implement it yourself and test it out.

It's important to remember to revert the post-byline.html and blog_extras.py files back to their original state when you're finished. Here's the original version of the author_details filter:

@register.filter
def author_details(author, current_user):
    if not isinstance(author, user_model):
        # return empty string as safe default
        return ""

    if author == current_user:
        return format_html("<strong>me</strong>")

    if author.first_name and author.last_name:
        name = f"{author.first_name} {author.last_name}"
    else:
        name = f"{author.username}"

    if author.email:
        prefix = format_html('<a href="mailto:{}">', author.email)
        suffix = format_html("</a>")
    else:
        prefix = ""
        suffix = ""

    return format_html('{}{}{}', prefix, name, suffix)
Enter fullscreen mode Exit fullscreen mode
<small>By {{ post.author|author_details:request.user }} on {{ post.published_at|date:"M, d Y" }}</small>
Enter fullscreen mode Exit fullscreen mode

The ability to access the template context within custom tags can be very handy. It allows you to use all the context variables available in the original template, simplifying the use of tags and keeping the code clean and organized.

In the next section, we'll explore inclusion tags, and how you can use them to render one template inside another.

Inclusion Tags

Another way to include a template within another is with the include template tag. For example:

{% include "blog/post-byline.html" %}
Enter fullscreen mode Exit fullscreen mode

While easy to implement, this approach has a key limitation: included templates can only access variables already in the including template’s context. Thus, any extra variables must be passed in from the calling view. If you use a template in many places, you may end up repeating the data-loading code in several views.

Inclusion tags address this problem. They let you query for extra data within your template tag function, which can then be used to render a template.

Inclusion tags are registered with the Library.inclusion_tag function. This function requires one argument: the name of the template to render. Unlike simple tags, inclusion tags don’t return a string to render. Instead, they return a context dictionary used to render the template during registration.

A useful feature for a blog site is displaying recent posts. You can create an inclusion tag that fetches the five most recent posts (excluding the current post being viewed) and renders a template.

How to Implement:

Start by creating the template to render - post-list.html in the blog/templates/blog directory:

<h4>{{ title }}</h4>
<ul>
{% for post in posts %}
    <li><a href="{% url "blog-post-detail" post.slug %}">{{ post.title }}</a></li>
{% endfor %}
</ul>
Enter fullscreen mode Exit fullscreen mode

Now, in blog_extras.py, write a recent_posts function that accepts a post argument. Fetch the five most recent Post objects, excluding the Post passed in. Return a dictionary with posts and title keys. Decorate this function with register.inclusion_tag, passing the path to post-list.html:

from blog.models import Post

@register.inclusion_tag("blog/post-list.html")
def recent_posts(post):
    posts = Post.objects.exclude(pk=post.pk)[:5]
    return {"title": "Recent Posts", "posts": posts}
Enter fullscreen mode Exit fullscreen mode

Finally, use this template tag in the post-detail.html template. For instance:

<!-- existing code here -->
{% row %}
    {% col %}
        {% recent_posts post %}
    {% endcol %}
{% endrow %}
Enter fullscreen mode Exit fullscreen mode

Reload a post detail page in your browser, and you should see the "Recent Posts" section, provided you have more than one Post object.

As with a simple tag, you can also pass the context to the inclusion tag function by adding a context argument and adding takes_context=True to the decorator call.

The main benefit of using an inclusion tag is that you can pass in specific information to the inclusion tag that is not found in the regular template tag. In this example, a Post object is passed to the recent_posts template tag, enabling the template tag to access any information in the Post object.

Advanced Template Tags

Simple template tags and inclusion tags cover the majority of use cases for custom template tags. However, Django offers advanced template tags for more customization. These tags consist of two parts:

  • A parser function: Called when its template tag is encountered in a template. It parses the template tag and extracts variables from the context. It returns a template Node subclass.

  • The template Node subclass: It has a render method, which is called to return the string to be rendered in the template.

The parser function is registered similarly to other types of tags—by being decorated with @register.tag.

Advanced template tags are useful to:

  • Capture the content between two tags, perform operations on each node, and choose how they’re output. For instance, you can implement a custom permissions template tag that only outputs the content between them if the current user has the correct permissions.

  • Set context variables. A template tag could be used to set a value in the template context that is then accessible further on in the template.

While these situations are quite specific, advanced template tags come in handy when the simpler ones don't suffice. If you think you need advanced template tags, check out Django's full documentation on advanced custom template tags.

Top comments (0)