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

Язык шаблонов 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для того , чтобы в теге работы. Это функция безопасности: она позволяет размещать код Python для многих библиотек шаблонов на одном хост-компьютере, не предоставляя доступ ко всем из них для каждой установки Django.{% load %}

Нет ограничений на количество модулей, которые вы кладете в templatetagsпакет. Просто имейте в виду, что оператор будет загружать теги / фильтры для данного имени модуля Python, а не для имени приложения.{% 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" }}foovar"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-код, который предназначен для интерпретации как есть на стороне клиента.

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

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

Код шаблонного фильтра попадает в одну из двух ситуаций:

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

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

    Этот флаг сообщает 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-теги или объекты. Например, удаление a >из ввода может превратиться <a>в <a, который нужно будет экранировать на выходе, чтобы избежать проблем. Точно так же удаление точки с запятой ( ;) может превратиться &amp;в &amp, который больше не является допустимым объектом и, следовательно, требует дальнейшего экранирования. В большинстве случаев не так сложно, но следите за любыми подобными проблемами при просмотре кода.

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

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

    Чтобы пометить вывод как безопасную строку, используйте django.utils.safestring.mark_safe().

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

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

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

    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или нет. (В последнем случае мы используем функцию идентификации как функцию «выхода».) conditional_escape()Функция аналогична, escape()за исключением того, что она экранирует только ввод, который не является SafeDataэкземпляром. Если SafeData экземпляр передается conditional_escape(), данные возвращаются без изменений.

    Наконец, в приведенном выше примере мы не забываем пометить результат как безопасный, чтобы наш HTML-код вставлялся непосредственно в шаблон без дальнейшего экранирования.

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

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

Избежание XSS-уязвимостей при повторном использовании встроенных фильтров

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

В более старых версиях Django будьте осторожны при повторном использовании встроенных фильтров 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 ''

Когда этот флаг установлен, если первым аргументом вашего фильтра является datetime с учетом часового пояса, 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>

Теги включения

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}

Затем создайте шаблон, используемый для рендеринга вывода тега. Этот шаблон является фиксированной функцией тега: его определяет создатель тега, а не разработчик шаблона. По нашему примеру, шаблон очень короткий:

<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переменные , которые точка вернуться на главную страницу. Вот как будет выглядеть функция Python:

@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.

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

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

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

Когда Django компилирует шаблон, он разбивает исходный текст шаблона на «узлы». Каждый узел является экземпляром 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()обычно должен выходить из строя тихо, особенно в производственной среде. Однако в некоторых случаях, особенно если context.template.engine.debugесть True, этот метод может вызвать исключение, чтобы упростить отладку. Например, несколько основных тегов поднимаются, django.template.TemplateSyntaxErrorесли получают неправильное количество или тип аргументов.

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

Соображения об автоматическом экранировании

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

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

Кроме того, если тег шаблона создает новый контекст для выполнения некоторой суб-рендеринга, установите для атрибута auto-escape значение текущего контекста. __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, связанный с contextшаблоном, который в настоящее время обрабатывается. Он render_contextведет себя как словарь Python и должен использоваться для хранения Nodeсостояния между вызовами 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для определения области CycleNodeконкретной информации в render_context. В CycleNodesданном шаблоне их может быть несколько , поэтому нам нужно быть осторожными, чтобы не затереть информацию о состоянии другого узла. Самый простой способ сделать это - всегда использовать 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])

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

Установка переменной в контексте

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

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

Но есть проблема 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 класса, например:

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()принимает кортеж имен блочных тегов '' для синтаксического анализа до ''. Он возвращает экземпляр django.template.NodeList, который представляет собой список всех Nodeобъектов, с которыми синтаксический анализатор столкнулся «до», когда он обнаружил какой-либо из тегов, указанных в кортеже.

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

After 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)in UpperNode.render().

Дополнительные примеры сложного рендеринга см. В исходном коде in и in .{% for %}django/template/defaulttags.py{% if %}django/template/smartif.py

Copyright ©2021 All rights reserved