Собственные шаблонные теги и фильтры

Django содержит большое количество встроенных тегов и фильтров. Тем не менее, вам может понадобиться добавить собственный функционал к шаблонам. Вы можете сделать это добавив собственную библиотеку тегов и фильтров используя Python, затем добавить ее в шаблон с помощью тега {% load %}.

Добавление собственной библиотеки

Собственные теги и фильтры шаблонов должны определяться в приложении Django. Если они логически связаны с каким-то приложением, есть смысл добавить их в это приложение, иначе создайте новое приложение.

Приложение должно содержать каталог templatetags на том же уровне что и models.py, views.py и др. Если он не существует, создайте его. Не забудьте создать файл __init__.py чтобы каталог мог использоваться как пакет Python. После добавления этого модуля, необходимо перезапустить сервер, перед тем как использовать теги или фильтры в шаблонах.

Выше теги и фильтры будут находиться в модуле пакета templatetags. Название модуля будет использоваться при загрузке библиотеки в шаблоне, так что убедитесь что оно не совпадает с названиями библиотек других приложений.

Например, если теги/фильтры находятся в файле poll_extras.py, ваше приложение может выглядеть следующим образом:

polls/
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

И в шаблоне вы будуте использовать:

{% load poll_extras %}

Приложение содержащее собственные теги и фильтры должно быть добавлено в INSTALLED_APPS, чтобы тег {% load %} мог загрузить его. Это сделано в целях безопасности.

Не имеет значение сколько модулей добавлено в пакет templatetags. Помните что тег {% load %} использует название модуля, а не название приложения.

Библиотека тегов должна содержать переменную register равную экземпляру django.template.Library, в которой регистрируются все определенные теги и фильтры. Так что в начале вашего модуля укажите следующие строки:

from django import template

register = template.Library()

Behind the scenes

Вы можете найти большое количество примеров в исходном коде встроенных тегов и фильтров Django. Они находятся в файлах django/template/defaultfilters.py и django/template/defaulttags.py.

Подробности о теге load читайте в этой документации.

Создание собственного шаблонного фильтра

Фильтры это просто функции Python, которые принимают один или несколько аргументов:

  • Входящее значение – не обязательно строка.

  • Значение аргументов – можно указать значение по умолчанию или вообще не использовать аргументы.

Например, при {{ var|foo:"bar" }} функция фильтра foo будет выполнена со значением переменной var и аргументом "bar".

Функция фильтра всегда должна возвращать значение. Она не должна вызывать исключение. Исключения должны перехватываться. В случае ошибки фильтр должен вернуть оригинальное значение или пустую строку.

Пример фильтра:

def cut(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(arg, '')

И пример как его использовать:

{{ somevariable|cut:"0" }}

Большинство фильтров не принимают аргументы, например:

def lower(value): # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

Регистрация фильтров

django.template.Library.filter()

Создав функцию фильтра, ее необходимо зарегистрировать в экземпляре Library, чтобы использовать в шаблоне:

register.filter('cut', cut)
register.filter('lower', lower)

Метод Library.filter() принимает два аргумента:

  1. Название фильтра – строка.

  2. Функция компиляции – функция Python (не название функции строкой).

Вы можете использовать register.filter() как декоратор:

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

Если вы не укажете аргумент name, как показано во втором примере, Django будет использовать название функции в качестве названия фильтра.

Также register.filter() принимает три именованных аргумента: is_safe, needs_autoescape и expects_localtime. Эти аргументы описан в разделе фильтры и автоматическое экранирование и в разделе фильтры и часовые пояса далее.

Шаблонные фильтры, которые обрабатывают строки

django.template.defaultfilters.stringfilter()

Если вы создали фильтр, который работает только со строками, используйте декоратор stringfilter. Он преобразует объект в строковое значение перед передачей в функцию:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()

@register.filter
@stringfilter
def lower(value):
    return value.lower()

В этом случае вы можете передать число в фильтр и это не вызовет исключение AttributeError (так как число не содержит метод lower()).

Фильтры и автоматическое экранирование

Создавая собственный фильтр, учитывайте как он будет работать с политикой автоматического экранирования в Django. Есть три типа строк, которые могу передаваться в коде шаблона.

  • “Сырые” строки – это обычные типы Python str или unicode. При выводе они экранируются при включенном авто-экранировании, иначе – выводятся как есть.

  • Безопасные строки – строки, которые были помечены как безопасные. Указывают на то, что последующее экранирование не требуется. Они обычно используются для строк, которые содержат готовый HTML, которые необходимо отобразить на странице.

    Внутренне эти строки представлены типами SafeBytes или SafeText. Эти типа наследуются от базового класса SafeData, таким образом вы можете проверять их следующим образом:

    if isinstance(value, SafeData):
        # Do something with the "safe" string.
        ...
    
  • Строки с пометкой “требуют экранирования”всегда экранируются при выводе, независимо от того находятся они в блоке autoescape или нет. Такие строки экранируются только один раз, независимо от того, включено автоматическое экранирование или нет.

    Внутренне эти строки представлены типами EscapeBytes или EscapeText. Вам не обязательно это знать, можно просто использовать фильтр escape.

При создании фильтра вы можете столкнуться со следующими ситуациями:

  1. Ваш фильтр не добавляет никаких не экранированных HTML-символов (<, >, ', " or &) в результат. В таком случае вы можете полностью положиться на политику автоматического экранирования Django. Для этого передайте параметр is_safe с значением True при регистрации функции фильтра:

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    Этот параметр указывает Django что фильтр никак не изменяет “безопасность” переданной строки. То есть, если передать в фильтр “безопасную” строку, результат также будет “безопасным” для Django, если же передать “небезопасную” строку, Django автоматически экранирует результат фильтра.

    Другими словами можно сказать “этот фильтр безопасный – он никаким образом не добавляет небезопасный HTML в результат.”

    Причина использования параметра is_safe состоит в том, что большинство операций со строками превращает объект SafeData обратно в обычный объект str или unicode и чтобы не обрабатывать все эти ситуации самостоятельно, что может быть не просто, Django самостоятельно следит за изменениями.

    Например, у вас есть фильтр, который добавляет xx к концу переданного значения. Так как он не добавляет небезопасных HTML-символов в результат (кроме тех, которые присутствуют в переданном значении), вы должные пометить его с параметром is_safe:

    @register.filter(is_safe=True)
    def add_xx(value):
        return '%sxx' % value
    

    Если фильтр используется в шаблоне с включенным автоматическим экранированием, Django выполнит экранирование результата если входящие данные не были отмечены как “безопасные”.

    По умолчанию is_safe равен False.

    Будьте внимательны определяя безопасен ваш фильтр или нет. Если вы удаляете символы, вы можете случайно оставить открытые HTML теги или сущности(entities) в результате. Например, при удалении > из входящих данных <a> может превратиться в <a, который должен быть экранирован. Аналогично, удаление точки с запятой (;) может превратить &amp; в &amp, что не является правильной HTML-сущностью и должно быть экранировано. Большинство случаев будут не такими сложными, но вы должны быть внимательными.

    Параметр is_safe принуждает фильтр вернуть строку. Если ваш фильтр возвращает булево значение или не строку, использование is_safe может привести к непредвиденным последствиям (например, конвертирование False в строку ‘False’).

  2. Фильтр самостоятельно заботиться об экранировании результата. Это необходимо если вы добавляете новый HTML в результат. В таком случае результат должен быть помечен как безопасный чтобы избежать последующего экранирования.

    Для этого используйте функцию django.utils.safestring.mark_safe().

    Но будьте осторожны. Необходимо не просто пометить результат как безопасный. Вы должны убедиться что результат действительно безопасен независимо от того, включено автоматическое экранирование или нет. Идея в том, чтобы фильтр правильно работал как при включенном автоматическом экранировании так и выключенном.

    Для того, чтобы фильтр знал включено ли автоматическое экранирование, передайте параметр needs_autoescape со значением True при регистрации функций фильтра. (По умолчанию значение равно False). Этот параметр указывает Django что необходимо передать именованный аргумент autoescape при вызове функции фильтра, который равен True, если включено автоматическое экранирование.

    Например, давайте создадим фильтр, который выделяет первый символ строки:

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=None):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = '<strong>%s</strong>%s' % (esc(first), esc(other))
        return mark_safe(result)
    

    Параметр needs_autoescape и аргумент autoescape информируют фильтр о том, было ли включено автоматическое экранирование при вызове фильтра. Аргумент autoescape указывает необходимо ли использовать django.utils.html.conditional_escape для входящих данных. (В нашем примере мы использовали его для определения функции “escape”.) Функция conditional_escape() как и escape(), но использует экранирование только для не безопасных(SafeData) строк. Если передать объект SafeData функция conditional_escape() вернет его без изменений.

    Также мы пометили результат как безопасный и он будет вставлен непосредственно в шаблон без повторного экранирования.

    В этом случае нет необходимости беспокоиться о параметре is_safe. Так как вы самостоятельно учитываете автоматическое экранирование, параметр is_safe ничего не изменит.

Фильтры и временные зоны

Если вы создаете фильтр, который обрабатывает объекты datetime, скорее всего вы будете использовать параметр expects_localtime со значением True:

@register.filter(expects_localtime=True)
def businesshours(value):
    try:
        return 9 <= value.hour < 17
    except AttributeError:
        return ''

Если этот флаг установлен и первый аргумент вашего фильтра является значением времени с часовым поясом, Django преобразует это значение в текущий часовой пояс перед тем как передать в фильтр, в соответствии с правилами преобразования часовых поясов в шаблонах.

Создание собственного шаблонного тега

Теги сложнее фильтров, так как они могут делать, что угодно.

Краткий обзор

Уже описывалось что система шаблонов работает в два этапа: компиляция и выполнение. Создавая собственный тег вы определяете как выполняется компиляция и выполнение тега.

Когда Django компилирует шаблон, содержимое шаблона разбивается на “узлы” (nodes). Каждый узел это экземпляр django.template.Node с методом render(). Откомпилированный шаблон это просто список объектов Node. Когда вы вызываете метод render() откомпилированного объекта шаблона, шаблон просто вызывает render() для каждого объекта Node в списке узлов с переданным контекстом. Результаты объединяются для получения окончательного результата.

Таким образом, создавая собственный тег, вы указываете как “сырой” тег шаблона конвертируется в объект Node (функцию компиляции) и что делает метод render().

Создание функции компиляции

Для каждого тега, с которым сталкивается парсер шаблона, вызывается его функция Python с содержимым тега и объектом парсера. Эта функция должна вернуть экземпляр Node.

Например, давайте создадим тег, {% current_time %}, который отображает текущую дату и время отформатированные в соответствии с переданным параметром с синтаксисом аналогичным strftime(). Первым делом следует определиться с синтаксисом тега. В нашем случае тег будет использоваться следующим образом:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>

Парсер функции должен получить параметр и вернуть объект Node:

from django import template
def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError("%r tag requires a single argument" % token.contents.split()[0])
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name)
    return CurrentTimeNode(format_string[1:-1])

Заметки:

  • parser – парсер шаблона. Он нам не нужен в данном примере.

  • token.contents – содержимое тега. В нашем примере это 'current_time "%Y-%m-%d %I:%M %p"'.

  • Метод token.split_contents() разбивает аргументы разделенные пробелами при это не разбивая строки выделенные кавычками. Более простой метод token.contents.split() может быть не таким полезным и надежным так как разбивает по всем пробелам, включая пробелы в кавычках. Лучше всегда использовать token.split_contents().

  • Эта функция может вызвать исключение django.template.TemplateSyntaxError в случае синтаксической ошибки при использовании вашего тега.

  • Исключение TemplateSyntaxError использует переменную tag_name. Не вписывайте название тега в сообщение ошибки, потому что это привязывает название тега к функции. token.contents.split()[0] ‘’всегда’’ содержит название тега – даже если тег не содержит аргументы.

  • Функция возвращает экземпляр CurrentTimeNode передавая в конструктор необходимую информацию с тега. В нашем примере передается "%Y-%m-%d %I:%M %p". Кавычки удаляются с помощью format_string[1:-1].

  • Парсер – очень низкоуровневый. Разработчики Django экспериментировали с созданием различных микро-фреймверков поверх системы парсинга, используя техники, такие как грамматика EBNF, но эти эксперименты делали систему шаблонов медленной. Парсер низкоуровневый, так как это делает его быстрым.

Реализация выполнения тега

Следующим этапом мы создаем подкласс Node с методом render().

В продолжение нашего примера создадим класс CurrentTimeNode:

import datetime
from django import template

class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string
    def render(self, context):
        return datetime.datetime.now().strftime(self.format_string)

Заметки:

  • __init__() принимает аргумент format_string из do_current_time(). Всегда передавайте параметры в Node через __init__().

  • Метод render() выполняет основную работу.

  • render() не должен вызывать исключений, особенно на боевом сервере при DEBUG и TEMPLATE_DEBUG равных False. Однако в некоторых случаях, особенно при TEMPLATE_DEBUG равном True, метод может вызывать исключения для упрощения отладки. Например, некоторые встроенные теги вызывают django.template.TemplateSyntaxError, если передать неверное количество или тип аргументов.

Разделение компиляции и выполнения эффективно так как позволяет выполнить шаблон с несколькими контекстами без надобности выполнять парсинг каждый раз.

Работа с автоматическим экранированием

Вывод тега не экранируется. Однако, есть несколько вещей которые следует помнить.

Если метод render() добавляет переменную в контекст (вместо того, чтобы вернуть строку), он должен пометить ее как безопасную используя функцию mark_safe(), если это необходимо. В конечном итоге к переменной будет применяться автоматическое экранирование при выводе в шаблоне, так что необходимо пометить ее как безопасную чтобы избежать повторного экранирования значения.

Если тег создает новый контекст, необходимо установить параметр автоматического экранирования со значением текущего контекста. Метод __init__ класса Context принимает аргумент autoescape, который вы можете использовать. Например:

from django.template import Context

def render(self, context):
    # ...
    new_context = Context({'var': obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

Это не совсем обычная ситуация, но может быть полезно если вы самостоятельно выполняете шаблон. Например:

def render(self, context):
    t = template.loader.get_template('small_fragment.html')
    return t.render(Context({'var': obj}, autoescape=context.autoescape))

Если бы мы не передали значение context.autoescape в новый Context, результат всегда экранировался бы, что может быть неуместным при использовании тега в блоке {% autoescape off %}.

Учитываем потоко-безопасность

После парсинга тега и создания узла его метод render может быть вызван любое количество раз. Так как Django может выполняться в мульти-поточном окружении, один узел может выполняться с несколькими контекстами для различных запросов. По этом важно удостовериться что ваши шаблонные теги являются потоко-безопасными.

Чтобы ваш шаблонный тег был потоко-безопасный, вы не должны сохранять информацию о состоянии в узле. Например, Django предоставляет тег cycle, который переключается между аргументами при каждом вызове:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}>
        ...
    </tr>
{% endfor %}

Реализация CycleNode могла бы выглядеть следующим образом:

import itertools
from django import template

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)
    def render(self, context):
        return next(self.cycle_iter)

Однако, предположим что одновременно выполняется два экземпляра шаблона представленного выше:

  1. Поток 1 выполняет первую итерацию по циклу, CycleNode.render() возвращает ‘row1’

  2. Поток 2 выполняет первую итерацию по циклу, CycleNode.render() возвращает ‘row2’

  3. Поток 1 выполняет вторую итерацию по циклу, CycleNode.render() возвращает ‘row1’

  4. Поток 2 выполняет вторую итерацию по циклу, CycleNode.render() возвращает ‘row2’

CycleNode работает, но итерация происходит глобально. Так как Поток 1 и Поток 2 связаны, они используют одни значения. Это точно не то, что вам нужно!

Для решения этой проблемы Django предоставляет render_context в контексте текущего шаблона. render_context работает как и словарь в Python и должен использоваться для хранения состояния узлов между вызовами метода render.

Давайте перепишем CycleNode чтобы использовать render_context:

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cyclevars = cyclevars
    def render(self, context):
        if self not in context.render_context:
            context.render_context[self] = itertools.cycle(self.cyclevars)
        cycle_iter = context.render_context[self]
        return next(cycle_iter)

Заметим, что вполне безопасно сохранять в атрибутах объекта Node информацию, которая не изменяется. В случае CycleNode, параметр cyclevars не изменяется после создания экземпляра Node, и нет необходимости хранить его в render_context. Но информация, которая относится к конкретному шаблону, например текущая итерация узла CycleNode, должна сохраняться в render_context.

Примечание

Обратите внимание как мы используем self для привязки состояния к текущему узлу в render_context. В шаблоне может быть несколько CycleNode, и важно не нарушить состояние других узлов. Самый просто способ это использовать self в качестве ключа в render_context. Если вам необходимо хранить несколько переменных, используйте в render_context[self] словарь.

Регистрация тега

Теперь зарегистрируем тег в экземпляре Library вашего модуля, как описано выше. Например:

register.tag('current_time', do_current_time)

Метод tag() принимает два аргумента:

  1. Название шаблонного тега – строкой. Если параметр не указан, используется название функции.

  2. Функция компиляции – функция Python (не название функции строкой).

Как и для регистрации фильтра, можно использовать как декоратор:

@register.tag(name="current_time")
def do_current_time(parser, token):
    ...

@register.tag
def shout(parser, token):
    ...

Если не указать параметр name, как во втором примере, Django будет использовать название функции в качестве названия тега.

Передача переменных шаблона в тег

Хоть вы и можете передать любое количество аргументов в шаблонный тег используя token.split_contents(), все аргументы передаются как строка. Чтобы передать значение переменной шаблона, необходимо немного усложнить код.

Тег из примера выше форматирует текущее время и возвращает строку. Предположим вы хотите передать объект DateTimeField и отформатировать это значение:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

token.split_contents() вернет три значения:

  1. Название тега format_time.

  2. Строку "blog_entry.date_updated" (без кавычек).

  3. Строку форматирования "%Y-%m-%d %I:%M %p". Значение из split_contents() будет содержать кавычки для таких переменных.

Теперь ваш тег будет выглядеть следующим образом:

from django import template

def do_format_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, date_to_be_formatted, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError("%r tag requires exactly two arguments" % token.contents.split()[0])
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name)
    return FormatTimeNode(date_to_be_formatted, format_string[1:-1])

Теперь вам следует изменить метод render узла чтобы получить значение атрибута date_updated объекта blog_entry. Это может быть выполнено с использованием класса Variable() из django.template.

Чтобы использовать класс Variable, создайте экземпляр указав название переменной, потом вызовите variable.resolve(context). Например:

class FormatTimeNode(template.Node):
    def __init__(self, date_to_be_formatted, format_string):
        self.date_to_be_formatted = template.Variable(date_to_be_formatted)
        self.format_string = format_string

    def render(self, context):
        try:
            actual_date = self.date_to_be_formatted.resolve(context)
            return actual_date.strftime(self.format_string)
        except template.VariableDoesNotExist:
            return ''

Будет вызвано исключение VariableDoesNotExist если невозможно найти значение переменной в текущем контексте.

Простые теги

django.template.Library.simple_tag()

Большинство тегов принимают определенное количество аргументов – строки или переменные шаблона – и возвращают строку после обработки аргументов. Например, тег current_time, который мы написали, один из них.

Для создания подобных тегов, Django предоставляет функцию simple_tag. Эта функция, которая является методом django.template.Library, принимает функцию принимающую любое количество аргументов, оборачивает функцией render и регистрирует в системе шаблонов.

Функцию current_time можно переписать следующим образом:

import datetime
from django import template

register = template.Library()

def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

register.simple_tag(current_time)

Можно использовать как декоратор:

@register.simple_tag
def current_time(format_string):
    ...

Несколько вещей которые следует помнить о функции simple_tag:

  • Проверка количества обязательных аргументов и др. выполняется до вызова функции, вам не нужно этого делать.

  • Кавычки вокруг строк уже удалены, аргумент будет содержать готовую строку.

  • Если аргумент является переменной шаблона, наша функция получит ее значение.

Если тегу необходим текущий контекст, используйте параметр takes_context при регистрации тега:

# The first argument *must* be called "context" here.
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

register.simple_tag(takes_context=True)(current_time)

Или:

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

Подробности о параметре takes_context смотрите в разделе о включающих тегах.

Если вам нужно изменить название тега, передайте его параметром:

register.simple_tag(lambda x: x - 1, name='minusone')

@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2

Теги, зарегистрированные через simple_tag могут принимать любое количество позиционных или именованных аргументов. Например:

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Теперь в тег можно передать любое количество позиционных аргументов, разделенных пробелами. Как и в Python, значения для именованных аргументов можно указать, используя знак “=” после именованных аргументов. Например:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

Включающие теги

Еще один тип тегов – это теги, которые выполняют другой шаблон и показывают результат. Например, интерфейс администратора Django использует включающий тег для отображения кнопок под формой на страницах добавления/редактирования объектов. Эти кнопки выглядят всегда одинаково, но ссылки зависят от текущего объекта – небольшой шаблон, который выполняется с данными из текущего объекта, удобно использовать в данном случае. (В приложении администратора это тег submit_row.)

Такие теги называются “включающие теги”.

Лучше пояснить на примере. Давайте создадим тег, который выводит список вариантов ответов для объекта модели Poll, которая использовалась в учебнике. Мы будем использовать его следующим образом:

{% show_results poll %}

...результат будет выглядеть приблизительно следующим образом:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

Первым делом, создадим функцию, которая принимает аргумент и возвращает словарь с данными. Заметим, что все что нам нужно, это вернуть словарь и ничего более сложного. Он будет использоваться как контекст для включаемого фрагмента шаблона. Например:

def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

Теперь создадим шаблон, который будет использоваться для генерации результата. Этот шаблон полностью относится к тегу: создатель тега определяет его, не создатель шаблонов(template designer). Для нашего примера шаблон будет очень простым:

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

Теперь создадим и зарегистрируем тег, используя метод inclusion_tag() объекта Library. Для нашего примера, если шаблон тега называется results.html, мы зарегистрируем тег следующим образом:

# Here, register is a django.template.Library instance, as before
register.inclusion_tag('results.html')(show_results)

Также можно зарегистрировать включающий тег используя экземпляр django.template.Template:

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

Можно использовать и как декоратор:

@register.inclusion_tag('results.html')
def show_results(poll):
    ...

...при создании функции.

В некоторых случаях тег может требовать большого количества параметров. Может быть проблематично запомнить все параметры и их порядок. Чтобы решить эту проблему Django предоставляет параметр takes_context для включающего тега. Если указать takes_context при создании тега, тег не будет содержать обязательные аргументы, а функция Python будет принимать один аргумент – контекст текущего шаблона.

Например, предположим вы создаете тег, который будет использоваться в шаблонах с контекстом всегда содержащим переменные home_link и home_title. Вот как может выглядеть такой тег:

# The first argument *must* be called "context" here.
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }
# Register the custom tag as an inclusion tag with takes_context=True.
register.inclusion_tag('link.html', takes_context=True)(jump_link)

(Заметим, что первый параметр должен называться context.)

При вызове register.inclusion_tag() мы указали takes_context=True и название включаемого шаблона. Вот как может выглядеть шаблон link.html:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

Для использования тега необходимо загрузить библиотеку тегов и вызвать тег без аргументов:

{% jump_link %}

Заметим, что при использовании takes_context=True необязательно передавать аргументы. Тег будет иметь доступ ко всему контексту шаблона.

Параметр takes_context по умолчанию равен False. Если он равен True, в тег будет передан объект контекста.

inclusion_tag может принимать любое количество позиционных и именованных аргументов. Например:

@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Теперь в тег можно передать любое количество позиционных аргументов, разделенных пробелами. Как и в Python, значения для именованных аргументов можно указать, используя знак “=” после именованных аргументов. Например:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

Добавление переменной в контекст

Предыдущие примеры тегов просто выводят значение. Более гибкий способ - это добавить значение в переменную контекста вместо вывода результата. Таким образом автор шаблона может использовать результат выполнения тега несколько раз.

Чтобы добавить переменную в контекст, просто добавьте значение в контекст как в словарь в методе render() объекта Node. Вот обновленная версия CurrentTimeNode, которая устанавливает переменную current_time вместо вывода результата:

import datetime
from django import template

class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string
    def render(self, context):
        context['current_time'] = datetime.datetime.now().strftime(self.format_string)
        return ''

Заметим, что render() возвращает пустую строку. render() всегда должен возвращать строку. Если все, что делает тег, это добавление переменной в контекст, метод render() должен вернуть пустую строку.

Вот как вы можете использовать новую версию тега:

{% current_time "%Y-%M-%d %I:%M %p" %}<p>The time is {{ current_time }}.</p>

Variable scope in context

Любая переменная, добавленная в контекст будет доступна только в блоке(block) шаблона, в котором она была добавлена. Так сделано намерено, чтобы переменные не конфликтовали с контекстом другого блока.

Но есть одна проблема в CurrentTimeNode2: название переменной current_time “вшито” в тег. Это означает, что вы должны убедиться, что переменная {{ current_time }} не используется в шаблоне, потому что {% current_time %} перезапишет ее. Правильное решение – позволить указывать название переменной при вызове тега:

{% current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

Чтобы это сделать вам нужно изменить код функции компиляции и подкласса Node:

class CurrentTimeNode3(template.Node):
    def __init__(self, format_string, var_name):
        self.format_string = format_string
        self.var_name = var_name
    def render(self, context):
        context[self.var_name] = datetime.datetime.now().strftime(self.format_string)
        return ''

import re
def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        raise template.TemplateSyntaxError("%r tag requires arguments" % token.contents.split()[0])
    m = re.search(r'(.*?) as (\w+)', arg)
    if not m:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
    format_string, var_name = m.groups()
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name)
    return CurrentTimeNode3(format_string[1:-1], var_name)

Разница в том, что do_current_time() получает формат строки и название переменной и передает в конструктор CurrentTimeNode3.

Если вам нужно только добавить переменную в контекст, воспользуйтесь присваивающий тег (assignment tag).

Присваивающий тег

Для простого создания тегов, которые добавляют переменную в контекст, Django предоставляет функцию assignment_tag. Эта функция работает так же, как и simple_tag, за исключением, что она добавляет результат в контекст, а не возвращает его.

Функцию current_time можно переписать следующим образом:

def get_current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

register.assignment_tag(get_current_time)

Можно использовать как декоратор:

@register.assignment_tag
def get_current_time(format_string):
    ...

Вы можете добавить результат в переменную шаблона, используя аргумент as и название переменной, и использовать его при необходимости:

{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>

Если тегу необходим текущий контекст, используйте параметр takes_context при регистрации тега:

# The first argument *must* be called "context" here.
def get_current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

register.assignment_tag(takes_context=True)(get_current_time)

Или:

@register.assignment_tag(takes_context=True)
def get_current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

Подробности о параметре takes_context смотрите в разделе о включающих тегах.

assignment_tag может принимать любое количество позиционных и именованных аргументов. Например:

@register.assignment_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Теперь в тег можно передать любое количество позиционных аргументов, разделенных пробелами. Как и в Python, значения для именованных аргументов можно указать, используя знак “=” после именованных аргументов. Например:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile as the_result %}

Создание блочного тега

Шаблонные теги могут работать вместе. Например, встроенный тег {% comment %} скрывает содержимое до тега {% endcomment %}. Чтобы создать подобный тег, используйте parser.parse() в функции компиляции тега.

Вот простая реализация тега {% comment %}:

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

Примечание

Реализация {% comment %} немного отличается от нашего примера, позволяя использовать неправильные теги между {% comment %} и {% endcomment %}. Для этого используется parser.skip_past('endcomment') вместо parser.parse(('endcomment',)) перед parser.delete_first_token(), такой вариант не генерирует список узлов.

parser.parse() принимает кортеж названий тегов для “парсинга, пока они не встретятся”. Функция вернет объект django.template.NodeList, который является списком объектов Node встреченных ‘’до’’ любого из тегов указанных в кортеже.

В "nodelist = parser.parse(('endcomment',))" из нашего примера, nodelist – это список всех узлов встреченных между {% comment %} и {% endcomment %}, не включая {% comment %} и {% endcomment %}.

После вызова parser.parse() парсер не “обрабатывает” тег {% endcomment %}, поэтому необходимо вызвать parser.delete_first_token().

CommentNode.render() просто возвращает пустую строку. Все между {% comment %} и {% endcomment %} игнорируется.

Обработка блочного тега с сохранением содержимого

В примере выше, do_comment() игнорирует содержимое между {% comment %} и {% endcomment %}. Вместо этого можно выполнить какие-либо операции над содержимым блочного тега.

Например, у нас есть тег {% upper %}, который преобразует содержимое до тега {% endupper %} в верхний регистр.

Пример использования:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}

Как и в предыдущем примере мы будем использовать parser.parse(). Но в этот раз полученный nodelist передадим в Node:

def do_upper(parser, token):
    nodelist = parser.parse(('endupper',))
    parser.delete_first_token()
    return UpperNode(nodelist)

class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist
    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()

Новым здесь является вызов self.nodelist.render(context) в UpperNode.render().

Более сложные примеры ищите в исходном коде реализации {% if %}, {% for %}, {% ifequal %} или {% ifchanged %}. Они находятся django/template/defaulttags.py.