Fluent in Django: 10+ Django template filters you should know

Prerequisites

You will need the basic knowledge of Django. If you don't feel comfortable with Django yet, try this beginner-friendly tutorial.

Django template filters

While tags lives between curly braces and a percent ({% tag %}), variables live between double curly braces ({{ variable }}). You can influence the variable by using filters.

Filters are functions registered to the template engine that takes a value as input and return transformed value.

IF YOU'RE CURIOUS

All the default filter functions are written in a file defaultfilters.py. This is how a wordcount filter looks like:

@register.filter(is_safe=False)
@stringfilter
def wordcount(value):
   """Return the number of words."""
   return len(value.split())

A filter is separated from a variable with a pipe {{ variable|filter }}. It is possible to use more than one filter on the variable - you just add another pipe and specify the filter ({{ variable|filter1|filter2 }}).

Initial setup

We will work on a similar project as for the Django template tags. If you did that tutorial, feel free to skip the initial setup and work on the project you created based on the Django template tags tutorial. We will work on a template with a different name, so feel free to join the initial setup from where we're changing the tutorial/urls.py. If you didn't you'll be easily able to follow, just go through the initial setup first.

Start with setting up a new Django project:

$ mkdir django_template_tutorial
$ cd django_template_tutorial
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install Django
(venv)$ django-admin startproject django_template_filters .

Create a new app:

(venv)$ django-admin startapp tutorial
```__

and register it:
```python
# django_templates/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tutorial',
]

Create a model called Expense:

# tutorial/models.py
from django.db import models


class Expense(models.Model):
    datetime = models.DateTimeField()
    name = models.CharField(max_length=100)
    amount = models.FloatField()

Create and run the migrations:

(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate

If you got lost during the initial setup, I encourage you to go back to basics.

Create a folder called templates inside the tutorial folder and inside it, another folder, again called tutorial. Inside said structure, create a template called filtered_expenses.html:

django_templates
    └── tutorial
        └── templates
            └── tutorial
                └── filtered_expenses.html

Create a basic html for your filters:

<!--tutorial/templates/tutorial/filtered_expenses.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Expenses</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="container mt-5">
    <!-- we'll add the code for filters here when we finnish the setup-->
</div>
</body>
</html>

Everything we're going to add will be added between the <div class="container mt-5"> and </div>.

Now we need to create an url where our template will be shown.

Open django_templates/urls.py and include tutorial's urls in it:

# django_template_filters/urls.py

from django.contrib import admin
from django.urls import path, include  # this is new


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tutorial.urls')), # this is new
]

We haven't created tutorial/urls.py yet.

Create the file and define just the root url:

# tutorial/urls.py

from django.urls import path
from tutorial import views

urlpatterns = [
    path('', views.filter_expenses_list),
]

And the last thing is to create a view called filter_expenses_list:

# tutorial/views.py

from django.shortcuts import render
from tutorial.models import Expense


def filter_expenses_list(request):
    filter_expenses_list = Expense.objects.order_by('-datetime')

    return render(request, 'tutorial/filtered_expenses.html', {'expenses': filter_expenses_list})

The last thing to do is to add some expenses via admin so we will be able to work with something.

Register the Expense model on the admin site in the admin.py file:

# tutorial/admin.py

from django.contrib import admin
from .models import Expense

admin.site.register(Expense)

Create the superuser:

(venv)$ python manage.py createsuperuser

Start the server:

(venv)$ python manage.py runserver

Open 127.0.0.1:8000/admin, log in with the superuser you created, and add 5 expenses.

This was a long setup, right? Since this is an advanced tutorial, I didn't explain the setting project up. If you didn't understand something, feel free to consult the beginners tutorial.

Let's get to work

Change case

Filters allow you to change the case on the word - word can be all caps, all lowercase, you can capitalize the first letter... To see the effect of each of those filters, open your admin panel and make the expense names varied - one all lowercase, one starting with a number, one with multiple words...

Edit your html file, so it will list the expenses in a table:

<!--templates/tutorial/filtered_expenses.html-->
<table class="table">
    <tbody>
    {% for expense in expenses %}
    <tr>
        <th>{{ expense.name }}</th>
        <th>{{ expense.datetime }}</th>
        <th>{{ expense.amount }}</th>
    </tr>
    {% endfor %}
</table>

We will be adding different filters to {{ expense.name }} part.

title

title converts a string into a title case by making words start with an uppercase character and the remaining characters lowercase.

<!--templates/tutorial/filtered_expenses.html-->
<table class="table">
    <tbody>
    {% for expense in expenses %}
    <tr>
        <th>{{ expense.name|title }}</th> <!-- filter added -->
        <th>{{ expense.datetime }}</th>
        <th>{{ expense.amount }}</th>
    </tr>
    {% endfor %}
</table>

title.png

capfirst

capfirst capitalizes the first character of the value. If the first character is not a letter, this filter has no effect. Switch title for capfirst filter to the {{ expense.name }}.

<!--templates/tutorial/filtered_expenses.html-->
<table class="table">
    <tbody>
    {% for expense in expenses %}
    <tr>
        <th>{{ expense.name|capfirst }}</th> <!-- filter added -->
        <th>{{ expense.datetime }}</th>
        <th>{{ expense.amount }}</th>
    </tr>
    {% endfor %}
</table>

capfirst.png

lower

lower converts a string into all lowercase. Switch the capfirst for a lower.

<!--templates/tutorial/filtered_expenses.html-->
<table class="table">
    <tbody>
    {% for expense in expenses %}
    <tr>
        <th>{{ expense.name|lower }}</th> <!-- filter added -->
        <th>{{ expense.datetime }}</th>
        <th>{{ expense.amount }}</th>
    </tr>
    {% endfor %}
</table>

lower.png

upper

upper converts a string into all uppercase. Switch the lower for an upper.

<!--templates/tutorial/filtered_expenses.html-->
<table class="table">
    <tbody>
    {% for expense in expenses %}
    <tr>
        <th>{{ expense.name|upper }}</th> <!-- filter added -->
        <th>{{ expense.datetime }}</th>
        <th>{{ expense.amount }}</th>
    </tr>
    {% endfor %}
</table>

upper.png

date

Let's say we're only interested to show the date of the expense, not the time. Edit the {{ expense.datetime }} part in the template:

<!--templates/tutorial/filtered_expenses.html-->

<!--templates/tutorial/filtered_expenses.html-->
<table class="table">
    <tbody>
    {% for expense in expenses %}
    <tr>
        <th>{{ expense.name }}</th>
        <th>{{ expense.datetime|date:"jS F Y" }}</th> <!-- filter added -->
        <th>{{ expense.amount }}</th>
    </tr>
    {% endfor %}
</table>

|date is usually followed by a colon and a desired format in the string. If not, DATE_FORMAT is used (you can set it in settings.py). In this case, we displayed the date as:

  • days: jS - day of the month without the leading zero (j) + English ordinal suffix for the day of the month (S)
  • month: F - the full-textual name of the month
  • year: Y - 4 digits year

date_format.png

You can check all the possible date formats in the documentation.

In the same way, you set the time format. You can also combine it to show both.

timesince

timesince formats a date as the time since the date in the variable. Let's provide a user with a notice as to how much time has passed since their last expense.

Add this code to your html file:

<!--templates/tutorial/filtered_expenses.html-->

<div class="alert alert-success">
    It's been {{ expenses.0.datetime|timesince }} since your last expense.
</div>

timesince shows: years, months, weeks, days, hours, minutes. However, the time that passed is not always exact (the difference between 2020-02-02 and 2021-04-21 is "1 year, 2 months").

timesince.png

Similar to timesince is a timeuntil except that it goes from now to the date in the future.

random

random returns a random item from the given list.

How about we show the user some random saying about money to encourage them to spend money thoughtfully?

Create a list of sayings in the view and pass it to the template:

# tutorial/views.py

def filter_expenses_list(request):
    filter_expenses_list = Expense.objects.order_by('-datetime')

    # this is new
    encouragements = [
        "Opportunity is missed by most people because it is dressed in overalls and looks like work.",
        "Money is a means to get through this world, but it cannot add a day to our lives.",
        "It’s good to have money and the things that money can buy, but it’s good too to check up once in a while and make sure that you haven’t lost the things that money can’t buy.",
        "And when you start putting some rainy-day money aside, you’ll understand that it isn’t “doing nothing.” It’s letting you sleep at night."
    ]

    return render(request, 'tutorial/filtered_expenses.html', {'expenses': filter_expenses_list, "encouragements": encouragements}) # encouragaments variable is new

Add this in your html file:

<!--templates/tutorial/filtered_expenses.html-->
<div class="card mb-3">
    <h5 class="card-header">Message of encouragement</h5>
    <div class="card-body">
        <p class="card-text">{{ encouragements|random }}</p>
    </div>
</div>

You provided a list of strings and now random each time the page refreshes outputs one.

random.png

unordered_list

unordere_list creates an html list out of a python list. The list can be nested.

the list is created without the <ul> tags.

We've shown a random saying about the money. But what if someone would like to see all of the sayings? Let's create an html list of all our sayings:

<!--templates/tutorial/filtered_expenses.html-->

<ul>
    {{ encouragements|unordered_list }}
</ul>

unordered_list.png

Add expense tags

Lets's say you want your users to be able to use tags, so they can group their expenses, etc.

Open models.py and add a tags JSONfield:

# tutorial/models.py

class Expense(models.Model):
    datetime = models.DateTimeField()
    name = models.CharField(max_length=100)
    amount = models.FloatField()
    tags = models.JSONField()

Apply the migrations:

(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate

Go to the admin panel and add few tags in a JSON format.

valid_json.png

slice

There is no limit as to how many tags your user enters. But you can't show 25 tags. You'll use slice to show just the first 2 tags.

slice returns a slice of the list, using the same syntax as Python’s list slicing.

<!--templates/tutorial/filtered_expenses.html-->

<div class="row m-4">
    {% for expense in expenses %}
    <div class="card m-2" style="max-width: 18rem;">
        <div class="card-body">
            <h5 class="card-title">{{ expense.name }}</h5>
        </div>
        <div class="card-footer bg-transparent">
            {{ expense.tags|slice:":2" }} <!-- slice -->
        </div>
    </div>
    {% endfor %}
</div>

slice.png

As you can see, 2 tags are showing at most.

cut

The Food expense has no tags and it looks super ugly. You can get rid of the empty list with cut. cut removes all values of the argument from the given string.

<!--templates/tutorial/filtered_expenses.html-->

<div class="row m-4">
    {% for expense in expenses %}
    <div class="card m-2" style="max-width: 18rem;">
        <div class="card-body">
            <h5 class="card-title">{{ expense.name }}</h5>
        </div>
        <div class="card-footer bg-transparent">
            {{ expense.tags|slice:":2"|cut:"['']" }} <!-- cut added -->
        </div>
    </div>
    {% endfor %}
</div>

Now part of the string that exactly matches the argument (['']) will be removed.

cut.png

This is not the only thing going on here. As you can see, you left the slice there and added the cut. You can chain filters one after the other.

length

You don't want to show a list of tags on the expense list, you just want to show how many of the tags an expense has. This can be done with length:

<!--templates/tutorial/filtered_expenses.html-->

<div class="row m-4">
    {% for expense in expenses %}
    <div class="card m-2" style="max-width: 18rem;">
        <div class="card-body">
            <h5 class="card-title">{{ expense.name }}</h5>
        </div>
        <div class="card-footer bg-transparent">
            There are {{ expense.tags|length }} tags for this expense. <!-- show just the length of the tags list -->
        </div>
    </div>
    {% endfor %}
</div>

length.png

pluralize

The first two that have 2/3 tags look good. But the last one with a single tag still has written "tags" (plural) instead of "tag"(singular). And you know what? Even that Django has covered - with pluralize.

<div class="row m-4">
    {% for expense in expenses %}
    <div class="card m-2" style="max-width: 18rem;">
        <div class="card-body">
            <h5 class="card-title">{{ expense.name }}</h5>
        </div>
        <div class="card-footer bg-transparent">
            There are {{ expense.tags|length }} tag{{ expense.tags|pluralize }} for this expense.
        </div>
    </div>
    {% endfor %}
</div>

pluralize.png

TRICK!

You managed to cover the tag(s) problem, but there is still problem with "are" word ("There ARE 1 tag..."). You can solve that using pluralize:

<div class="card-footer bg-transparent">
  There {{ expense.tags|pluralize:"is,are" }} {{ expense.tags|length }} tag{{ expense.tags|pluralize }} for this expense.
</div>

Now "is" is used for a singular tag and "are" is used for multiple tags.

pluralize_trick.png

Article model

For the next couple of filters, the expenses might not be the best showcase. So let's say you add a blog to lure more clients to use your expense tracker. Open models.py and add another model, Article:

# tutorial/models.py

class Article(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()

apply the migrations:

(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate

Add the model to admin:

# tutorial/admin.py
from django.contrib import admin
from .models import Expense, Article

admin.site.register(Expense)
admin.site.register(Article)

Open the admin panel and add few articles.

If you have them, you need to show them.

Open views.py, retrieve all the articles, and pass them into the template:

from django.shortcuts import render
from tutorial.models import Expense, Article

def filter_expenses_list(request):
    filter_expenses_list = Expense.objects.order_by('-datetime')
    articles = Article.objects.all()

    encouragements = [
        "...",
    ]

    return render(request, 'tutorial/filtered_expenses.html', {'expenses': filter_expenses_list, "encouragements": encouragements, "articles": articles})

wordcount

We want to list all the articles and show how long each of them is. This can be done with the wordcount filter.

<ul>
    {% for article in articles %}
    <li>{{ article.title }}: {{ article.content|wordcount }}</li>
    {% endfor %}
</ul>

wordcount.png

truncate

On a blog list, you usually show just a part of the text, the whole showing only in detail. Django has a few built-in possibilities to truncate the text:

  • truncatechars
  • truncatechars_html
  • truncatewords
  • truncatewords_html

Since they're so similar, we're going to use 2, but you check the other 2 in the documentation.

truncatechars

truncatechars truncates a string to the maximum of the specified number of characters. It ends with an ellipsis ("...").

<div class="row">
    {% for article in articles %}
    <div class="card" style="width: 18rem;">
        <div class="card-body">
            <h5 class="card-title">{{ article.title }}</h5>
            <p class="card-text">{{ article.content|truncatechars:150 }}</p> <!-- filter added -->
        </div>
    </div>
    {% endfor %}
</div>

Here we're listing the articles in cards. The content of the article is truncated at 150 characters.

truncate_chars.png

truncatewords_html

truncatewords_html truncates a string after a certain number of words with the respect to html tags. The unclosed html tags are closed immediately after the truncation. We don't have covered the html characters in the admin (if you want to do that, you can see how it's done in this post), so we'll have to fake it.

<div class="card" style="width: 18rem;">
    <div class="card-body">
        <h5 class="card-title">{{ article.title }}</h5>
        {{ "<p class='card-text'><b>This text exists to prove</b> that html tags don't get truncated.</p>"|truncatewords_html:10 }} <!-- filter added -->
    </div>
</div>

Here we truncate a string that includes html.

word_truncate_html.png

As you can see, the paragraph tag closes despite the truncation:

word_truncate_html_inspector.png

Conclusion

Many Django filters can be useful for you. If you want to check more than you got to know in this post, you can read about them in the documentation.

As for this tutorial, you got to know:

  • different ways to change cases
    • title
    • capfirst
    • lower
    • upper
  • a way to stile date
  • how much time has passed since some date/time with timesince
  • how to produce a random item from a list
  • how to create an html unordered_list from a Python list
  • how to get only a part of the list with slice
  • how to cut something out of a string
  • how to find out the length of the value
  • how to cover case, where there can be one or multiple things with pluralize
  • how to do a wordcount
  • multiple possibilities for truncating text:
    • truncatechars
    • truncatewords_html

Cover image by Daniel Mena from Pixabay

Comments (1)

Siddharth Chandra's photo

Bookmarked!