Skip to content

Chapter 7: Template System

7.3 Custom Templates

Custom Filters

Django allows you to create custom filters to extend template functionality. A custom filter is a Python function that takes at most two parameters:

  1. The value of the variable (input)
  2. The value of the parameter (optional)

First, create a templatetags directory in your app directory and add an __init__.py file:

myapp/
    templatetags/
        __init__.py
        custom_filters.py

Then define filters in custom_filters.py:

python
from django import template

register = template.Library()

# Simple filter example
@register.filter
def lower(value):
    """Convert string to lowercase"""
    return value.lower()

# Filter with parameters example
@register.filter
def truncate_chars(value, max_length):
    """Truncate string to specified character count"""
    if len(value) > max_length:
        return value[:max_length] + '...'
    return value

# More complex filter example
@register.filter
def word_count(value):
    """Count words in text"""
    return len(value.split())

@register.filter
def multiply(value, arg):
    """Multiply value by argument"""
    try:
        return float(value) * float(arg)
    except (ValueError, TypeError):
        return 0

@register.filter
def highlight_search(text, search_term):
    """Highlight search keywords"""
    if search_term:
        return text.replace(
            search_term, 
            f'<mark>{search_term}</mark>'
        )
    return text

Using custom filters in templates:

html
{% load custom_filters %}

<p>Title: {{ title|lower }}</p>
<p>Summary: {{ content|truncate_chars:100 }}</p>
<p>Word count: {{ content|word_count }}</p>
<p>Price: {{ price|multiply:1.2|floatformat:2 }}</p>
<p>Search results: {{ content|highlight_search:query }}</p>

Custom Tags

Custom tags are more complex than filters and can execute more complex logic. They come in several types:

  1. Simple tags
  2. Inclusion tags
  3. Assignment tags

Define tags in templatetags/custom_tags.py:

python
from django import template
from django.utils.safestring import mark_safe
from ..models import Article, Category

register = template.Library()

# Simple tag example
@register.simple_tag
def total_articles():
    """Return total number of articles"""
    return Article.objects.count()

@register.simple_tag
def article_count_by_category(category_slug):
    """Return number of articles in specified category"""
    try:
        category = Category.objects.get(slug=category_slug)
        return category.articles.count()
    except Category.DoesNotExist:
        return 0

# Simple tag with parameters
@register.simple_tag(takes_context=True)
def user_article_count(context):
    """Return number of articles by current user"""
    user = context['user']
    if user.is_authenticated:
        return Article.objects.filter(author=user).count()
    return 0

# Inclusion tag example
@register.inclusion_tag('tags/recent_articles.html')
def recent_articles(count=5):
    """Display recent articles"""
    articles = Article.objects.filter(
        status='published'
    ).order_by('-publish_date')[:count]
    return {'articles': articles}

# Assignment tag example
@register.simple_tag
def get_categories():
    """Get all categories"""
    return Category.objects.all()

# Complex tag example
@register.tag
def highlight_text(parser, token):
    """
    Highlight text tag
    Usage: {% highlight_text "search term" %}text to search{% endhighlight_text %}
    """
    try:
        # Parse tag parameters
        tag_name, search_term = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires a single argument" % token.contents.split()[0]
        )
    
    # Parse tag content until endhighlight_text
    nodelist = parser.parse(('endhighlight_text',))
    parser.delete_first_token()
    
    return HighlightTextNode(nodelist, search_term)

class HighlightTextNode(template.Node):
    def __init__(self, nodelist, search_term):
        self.nodelist = nodelist
        self.search_term = template.Variable(search_term)
    
    def render(self, context):
        # Render tag content
        text = self.nodelist.render(context)
        try:
            search_term = self.search_term.resolve(context)
            # Highlight search term
            highlighted = text.replace(
                search_term,
                f'<mark>{search_term}</mark>'
            )
            return mark_safe(highlighted)
        except template.VariableDoesNotExist:
            return text

Using custom tags in templates:

html
{% load custom_tags %}

<!-- Simple tags -->
<p>Total articles: {% total_articles %}</p>
<p>Python category articles: {% article_count_by_category "python" %}</p>
<p>Your articles: {% user_article_count %}</p>

<!-- Inclusion tags -->
{% recent_articles 10 %}

<!-- Assignment tags -->
{% get_categories as categories %}
<ul>
{% for category in categories %}
    <li>{{ category.name }}</li>
{% endfor %}
</ul>

<!-- Complex tags -->
{% highlight_text "Django" %}
Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.
{% endhighlight_text %}

Template file for inclusion tag templates/tags/recent_articles.html:

html
<div class="recent-articles">
    <h3>Recent Articles</h3>
    <ul>
    {% for article in articles %}
        <li>
            <a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
            <span class="date">{{ article.publish_date|date:"m-d" }}</span>
        </li>
    {% empty %}
        <li>No articles available</li>
    {% endfor %}
    </ul>
</div>

Template Context Processors

Template context processors are functions that automatically add variables to all template contexts. They are defined in the TEMPLATES configuration in settings.py.

Create context_processors.py:

python
from django.conf import settings

def site_info(request):
    """Site information context processor"""
    return {
        'site_name': getattr(settings, 'SITE_NAME', 'My Website'),
        'site_description': getattr(settings, 'SITE_DESCRIPTION', 'A Django website'),
    }

def user_permissions(request):
    """User permissions context processor"""
    if request.user.is_authenticated:
        return {
            'user_permissions': request.user.get_all_permissions(),
            'is_admin': request.user.is_staff,
        }
    return {}

def common_data(request):
    """Common data context processor"""
    from ..models import Category
    
    try:
        categories = Category.objects.all()[:10]
    except:
        categories = []
    
    return {
        'common_categories': categories,
        'current_year': datetime.now().year,
    }

Register context processors in settings.py:

python
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        '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',
                # Custom context processors
                'myapp.context_processors.site_info',
                'myapp.context_processors.user_permissions',
                'myapp.context_processors.common_data',
            ],
        },
    },
]

Using variables provided by context processors in any template:

html
<!-- All templates can access these variables -->
<header>
    <h1>{{ site_name }}</h1>
    <p>{{ site_description }}</p>
</header>

<nav>
    <ul>
    {% for category in common_categories %}
        <li><a href="{{ category.get_absolute_url }}">{{ category.name }}</a></li>
    {% endfor %}
    </ul>
</nav>

<footer>
    <p>&copy; {{ current_year }} {{ site_name }}</p>
    {% if is_admin %}
    <p><a href="/admin/">Admin</a></p>
    {% endif %}
</footer>

Static File Handling

Correctly referencing static files (CSS, JavaScript, images, etc.) in templates:

html
{% load static %}

<!DOCTYPE html>
<html>
<head>
    <title>My Website</title>
    <!-- CSS files -->
    <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'css/responsive.css' %}">
</head>
<body>
    <!-- Images -->
    <img src="{% static 'images/logo.png' %}" alt="Logo">
    
    <div class="content">
        <!-- Page content -->
    </div>
    
    <!-- JavaScript files -->
    <script src="{% static 'js/jquery.min.js' %}"></script>
    <script src="{% static 'js/main.js' %}"></script>
    
    <!-- Static files with version control -->
    <script src="{% static 'js/app.js' %}?v=1.0"></script>
</body>
</html>

In development environment, configure static files in settings.py:

python
# settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "staticfiles"

In production environments, it's usually necessary to configure a web server (such as Nginx) to serve static files directly to improve performance.

Summary

Custom template functionality extends the capabilities of the Django template system:

  1. ✅ Custom filters handle variable formatting
  2. ✅ Custom tags implement complex logic
  3. ✅ Template context processors globally inject variables
  4. ✅ Proper referencing and management of static files
  5. ✅ Flexible use of inclusion tags and assignment tags

Mastering custom template functionality can create more powerful and flexible frontend interfaces.

Next

We will learn about the Django form system.

8.1 Form Basics →

Contents

Back to Course Outline

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).