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 %}
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})
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>
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)
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 %}
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"),
]
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>")
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 %}
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 theregister.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)
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>
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)
<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:
{% 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:
<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
:
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:
<!-- 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 arender
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)