Understanding and Implementing Custom Template Tags in Django
Tags: webdev, django, python, backend
Published: 2023-07-12 19:12:51

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:

```html
{% 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 %}
```

The `post_detail` view in `views.py` would be:

from django.shortcuts import render, get_object_or_404

```python
def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    return render(request, "blog/post-detail.html", {"post": post})
```

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

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

inside the blog_extras.py

```python
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)
```

The `index.html` should now look like this:

```html
{% 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 %}
```

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

```python
from django.urls import path
import blog.views

urlpatterns = [
    # ... other URL patterns ...
    path("post/<slug>/", blog.views.post_detail, name="blog-post-detail"),
]
```
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:

```python
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>")
```

We can now use these tags in our templates:

```html
{% 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 %}
```

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




```python
@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)
```

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

```html
<small>By {% author_details_tag %} on {{ post.published_at|date:"M, d Y" }}</small>
```

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:

```python
@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)
```

```html
<small>By {{ post.author|author_details:request.user }} on {{ post.published_at|date:"M, d Y" }}</small>
```

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:

```html
{% include "blog/post-byline.html" %}
```

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:


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

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

```python
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}
```

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

```html
<!-- existing code here -->
{% row %}
    {% col %}
        {% recent_posts post %}
    {% endcol %}
{% endrow %}
```

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.