• 3.1
  • 3.2
  • 5.0
  • Версия документации: 6.1

Как создать пользовательские теги и фильтры шаблонов

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

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

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

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

Сервер для разработки не перезапускается автоматически

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

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

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

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

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

{% load poll_extras %}

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

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

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

from django import template

register = template.Library()

Модуль с шаблонными тегами можно также зарегистрировать через аргумент 'libraries' класса DjangoTemplates. Это полезно, если вы хотите изменить название библиотеки тегов. Также вы можете зарегистрировать библиотеку без установки приложения.

За кулисами

Вы можете найти большое количество примеров в исходном коде встроенных тегов и фильтров 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, чтобы использовать в шаблонах Django:

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. При выводе они экранируются при включенном авто-экранировании, иначе – выводятся как есть.

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

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

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # Do something with the "safe" string.
        ...
    

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

  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 и, чтобы не обрабатывать все эти ситуации самостоятельно, что может быть не просто, 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-сущностью и должно быть экранировано. Большинство случаев будут не такими сложными, но вы должны быть внимательными.

    Marking a filter is_safe will coerce the filter’s return value to a string. If your filter should return a boolean or other non-string value, marking it is_safe will probably have unintended consequences (such as converting a boolean False to the string „False“).

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

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

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

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

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

    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=True):
        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 ничего не изменит.

Предупреждение

Защита от XSS уязвимостей при использовании встроенных фильтров.

Фильтры Django используют autoescape=True, чтобы избежать XSS уязвимостей.

В предыдущих версиях Django autoescape равен None по умолчанию, будьте осторожны при использовании фильтров в коде. Вам необходимо передать autoescape=True, чтобы активировать экранирование.

Например, если вы хотите написать фильтр urlize_and_linebreaks, который использует фильтры urlize и linebreaksbr, он будет выглядеть следующим образом:

from django.template.defaultfilters import linebreaksbr, urlize


@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
    return linebreaksbr(urlize(text, autoescape=autoescape), autoescape=autoescape)

Тогда:

{{ comment|urlize_and_linebreaks }}

можно использовать вместо:

{{ comment|urlize|linebreaksbr }}

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

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

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

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

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

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

Простые теги

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()


@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

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

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

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

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

В отличии от других утилит тегов, simple_tag обрабатывает результат функцией conditional_escape(), если контекст шаблона в режиме автоматического экранирования, чтобы убедиться в правильности HTML и защитить вас от XSS атак.

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

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

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

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

Подробности о параметре 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 %}

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

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

Simple block tags

New in Django 5.2.
django.template.Library.simple_block_tag()

When a section of rendered template needs to be passed into a custom tag, Django provides the simple_block_tag helper function to accomplish this. Similar to simple_tag(), this function accepts a custom tag function, but with the additional content argument, which contains the rendered content as defined inside the tag. This allows dynamic template sections to be easily incorporated into custom tags.

For example, a custom block tag which creates a chart could look like this:

from django import template
from myapp.charts import render_chart

register = template.Library()


@register.simple_block_tag
def chart(content):
    return render_chart(source=content)

The content argument contains everything in between the {% chart %} and {% endchart %} tags:

{% chart %}
  digraph G {
      label = "Chart for {{ request.user }}"
      A -> {B C}
  }
{% endchart %}

If there are other template tags or variables inside the content block, they will be rendered before being passed to the tag function. In the example above, request.user will be resolved by the time render_chart is called.

Block tags are closed with end{name} (for example, endchart). This can be customized with the end_name parameter:

@register.simple_block_tag(end_name="endofchart")
def chart(content):
    return render_chart(source=content)

Which would require a template definition like this:

{% chart %}
  digraph G {
      label = "Chart for {{ request.user }}"
      A -> {B C}
  }
{% endofchart %}

A few things to note about simple_block_tag:

  • The first argument must be called content, and it will contain the contents of the template tag as a rendered string.

  • Variables passed to the tag are not included in the rendering context of the content, as would be when using the {% with %} tag.

Just like simple_tag, simple_block_tag:

  • Validates the quantity and quality of the arguments.

  • Strips quotes from arguments if necessary.

  • Escapes the output accordingly.

  • Supports passing takes_context=True at registration time to access context. Note that in this case, the first argument to the custom function must be called context, and content must follow.

  • Supports renaming the tag by passing the name argument when registering.

  • Supports accepting any number of positional or keyword arguments.

  • Supports storing the result in a template variable using the as variant.

Content Escaping

simple_block_tag behaves similarly to simple_tag regarding auto-escaping. For details on escaping and safety, refer to simple_tag. Because the content argument has already been rendered by Django, it is already escaped.

A complete example

Consider a custom template tag that generates a message box that supports multiple message levels and content beyond a simple phrase. This could be implemented using a simple_block_tag as follows:

testapp/templatetags/testapptags.py
from django import template
from django.utils.html import format_html


register = template.Library()


@register.simple_block_tag(takes_context=True)
def msgbox(context, content, level):
    format_kwargs = {
        "level": level.lower(),
        "level_title": level.capitalize(),
        "content": content,
        "open": " open" if level.lower() == "error" else "",
        "site": context.get("site", "My Site"),
    }
    result = """
    <div class="msgbox {level}">
      <details{open}>
        <summary>
          <strong>{level_title}</strong>: Please read for <i>{site}</i>
        </summary>
        <p>
          {content}
        </p>
      </details>
    </div>
    """
    return format_html(result, **format_kwargs)

When combined with a minimal view and corresponding template, as shown here:

testapp/views.py
from django.shortcuts import render


def simpleblocktag_view(request):
    return render(request, "test.html", context={"site": "Important Site"})
testapp/templates/test.html
{% extends "base.html" %}

{% load testapptags %}

{% block content %}

  {% msgbox level="error" %}
    Please fix all errors. Further documentation can be found at
    <a href="http://example.com">Docs</a>.
  {% endmsgbox %}

  {% msgbox level="info" %}
    More information at: <a href="http://othersite.com">Other Site</a>/
  {% endmsgbox %}

{% endblock %}

The following HTML is produced as the rendered output:

<div class="msgbox error">
  <details open>
    <summary>
      <strong>Error</strong>: Please read for <i>Important Site</i>
    </summary>
    <p>
      Please fix all errors. Further documentation can be found at
      <a href="http://example.com">Docs</a>.
    </p>
  </details>
</div>

<div class="msgbox info">
  <details>
    <summary>
      <strong>Info</strong>: Please read for <i>Important Site</i>
    </summary>
    <p>
      More information at: <a href="http://othersite.com">Other Site</a>
    </p>
  </details>
</div>

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

django.template.Library.inclusion_tag()

Еще один тип тегов – это теги, которые выполняют другой шаблон и показывают результат. Например, интерфейс администратора 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")
def show_results(poll): ...

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

from django.template.loader import get_template

t = get_template("results.html")
register.inclusion_tag(t)(show_results)

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

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

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

@register.inclusion_tag("link.html", takes_context=True)
def jump_link(context):
    return {
        "link": context["home_link"],
        "title": context["home_title"],
    }

Заметим, что первый параметр должен называться 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 %}

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

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

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

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

When Django compiles a template, it splits the raw template text into nodes. Each node is an instance of django.template.Node and has a render() method. A compiled template is a list of Node objects. When you call render() on a compiled template object, the template calls render() on each Node in its node list, with the given context. The results are all concatenated together to form the output of the template.

Таким образом, создавая собственный тег, вы указываете как «сырой» тег шаблона конвертируется в объект 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 в случае синтаксической ошибки при использовании вашего тега.

  • The TemplateSyntaxError exceptions use the tag_name variable. Don’t hardcode the tag’s name in your error messages, because that couples the tag’s name to your function. token.contents.split()[0] will always be the name of your tag – even when the tag has no arguments.

  • Функция возвращает экземпляр 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() не должен вызывать исключений, особенно на боевом сервере. Однако, в некоторых случаях, особенно при TEMPLATE_DEBUG равном True, метод может вызывать исключения для упрощения отладки. Например, некоторые встроенные теги вызывают django.template.TemplateSyntaxError, если передать неверное количество или тип аргументов.

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

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

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

Если метод 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 = context.template.engine.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])

You also have to change the renderer to retrieve the actual contents of the date_updated property of the blog_entry object. This can be accomplished by using the Variable() class in 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 если невозможно найти значение переменной в текущем контексте.

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

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

Чтобы добавить переменную в контекст, просто добавьте значение в контекст как в словарь в методе render(). Вот обновленная версия 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>

Область видимости переменной в контексте

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

But, there’s a problem with CurrentTimeNode2: The variable name current_time is hardcoded. This means you’ll need to make sure your template doesn’t use {{ current_time }} anywhere else, because the {% current_time %} will blindly overwrite that variable’s value. A cleaner solution is to make the template tag specify the name of the output variable, like so:

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

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

import re


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 ""


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.

В конце концов, если вам необходимо просто добавить переменную в контекст, обратите внимание на simple_tag(), который позволяет добавить результат тега в переменные шаблона.

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

Шаблонные теги могут работать вместе. Например, встроенный тег {% 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() takes a tuple of names of block tags to parse until. It returns an instance of django.template.NodeList, which is a list of all Node objects that the parser encountered before it encountered any of the tags named in the tuple.

В "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().

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

Back to Top