DTL, наследование, custom tags и template partials Django 6.0
Шаблон превращает подготовленный контекст в HTML, письмо, XML или другой текст. DTL намеренно не исполняет произвольный Python: сложная выборка, права и бизнес-правила должны быть решены до рендеринга.
Основной синтаксис актуален для Django 5.2 LTS и 6.0. Template partials отдельно помечены как возможность Django 6.0.
Типовая конфигурация ищет проектные шаблоны из DIRS и шаблоны установленных приложений при APP_DIRS=True:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]Имена внутри приложения помещают в собственную папку: blog/templates/blog/post_list.html. Иначе два файла templates/index.html разных приложений конкурируют по порядку загрузчиков.
Загрузчик возвращает скомпилированный объект шаблона. Начиная с Django 4.1, cached template loader включается автоматически, если OPTIONS["loaders"] не задан. Ручная настройка нужна для нестандартных loaders, а отдельной встроенной команды collecttemplates в Django нет.
View передаёт данные словарём:
return render(request, "blog/post_detail.html", {
"post": post,
"comments": comments,
})DTL использует {{ post.title }}. Для каждой части после точки движок последовательно пробует:
Callable без обязательных аргументов может быть вызван автоматически. Поэтому {{ post.comments.count }} способен незаметно выполнить SQL. Шаблонный цикл по непредзагруженным связям создаёт N+1 точно так же, как Python-цикл во view. Подготовьте select_related(), prefetch_related() и annotate() заранее.
Отсутствующая переменная по умолчанию отображается пустой строкой. Это удобно для пользователя, но маскирует опечатки. Тестируйте содержимое важных страниц, а не только статус 200.
{% if post.is_published %}
<span>Опубликовано</span>
{% endif %}
{% for comment in comments %}
<article>{{ comment.text|linebreaksbr }}</article>
{% empty %}
<p>Комментариев пока нет.</p>
{% endfor %}У forloop есть counter, first, last и parentloop. Длинные условия и вычисления лучше заменить понятным значением контекста: can_edit, comments_count, status_label.
URL строится по имени:
<a href="{% url 'blog:post_detail' slug=post.slug %}">
{{ post.title }}
</a>Так изменение пути не требует поиска жёстко заданных строк по всем шаблонам.
{{ post.published_at|date:"d.m.Y H:i" }}
{{ post.body|truncatewords:40 }}
{{ amount|floatformat:2 }}
{{ value|default_if_none:"—" }}default срабатывает для любого falsy-значения, включая 0, пустую строку и False. default_if_none заменяет только None. Это различие важно для цены, счётчика и boolean-поля.
Встроенный pluralize рассчитан на простое разделение «один/не один» и не выражает три русские числовые формы строкой из трёх окончаний. Для локализованного текста используйте gettext:
{% load i18n %}
{% blocktranslate count counter=comments_count %}
{{ counter }} комментарий
{% plural %}
{{ counter }} комментариев
{% endblocktranslate %}В .po-каталоге переводчик задаёт формы согласно plural rules русского языка.
В HTML-шаблоне переменная экранируется по умолчанию:
{{ user_input }}Символы <, >, &, кавычки не станут HTML-разметкой. Но автоэкранирование не является контекстным анализатором JavaScript, CSS и URL. Не вставляйте пользовательское значение внутрь сырого script:
{# небезопасная архитектура #}
<script>const name = "{{ user_input }}";</script>Для передачи JSON используйте json_script и читайте содержимое элемента из JavaScript:
{% load static %}
{{ chart_data|json_script:"chart-data" }}
<script src="{% static 'charts.js' %}" defer></script>safe, mark_safe и {% autoescape off %} отключают важную защиту. Они допустимы только для HTML, чья безопасность установлена на доверенной границе. Markdown от пользователя сначала ограничивают и санитизируют разрешающим списком тегов и атрибутов; простое слово «trusted» в имени переменной ничего не гарантирует.
Экранирование также не проверяет схему ссылки. URL от пользователя следует валидировать, чтобы не пропустить опасную схему вроде javascript:.
{# templates/base.html #}
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>{% block title %}Блог{% endblock %}</title>
{% block extra_head %}{% endblock %}
</head>
<body>
{% include "partials/navigation.html" %}
<main>{% block content %}{% endblock %}</main>
{% block scripts %}{% endblock %}
</body>
</html>{% extends "base.html" %}
{% load static %}
{% block title %}Публикации — {{ block.super }}{% endblock %}
{% block content %}
{% for post in posts %}
{% include "blog/_post_card.html" with post=post only %}
{% empty %}
<p>Публикаций пока нет.</p>
{% endfor %}
{% endblock %}extends должен быть первым template tag, но не обязательно первой физической строкой файла: перед ним допустим текст, хотя лишний вывод обычно не нужен. block.super добавляет содержимое родителя. include ... only передаёт фрагменту явный минимальный контекст и делает зависимость видимой.
load действует только в текущем шаблоне. Библиотека, загруженная в дочернем файле, не становится доступной в родительском, и наоборот. Каждый файл должен загружать используемые им нестандартные теги.
Partial — именованный фрагмент внутри того же файла:
{% partialdef post-card %}
<article id="post-{{ post.pk }}">
<h2>
<a href="{% url 'blog:post_detail' slug=post.slug %}">
{{ post.title }}
</a>
</h2>
</article>
{% endpartialdef %}
{% for post in posts %}
{% partial post-card %}
{% endfor %}Partial видит текущий контекст и может использоваться несколько раз. Его можно загрузить отдельно по имени template.html#partial-name:
return render(
request,
"blog/post_list.html#post-card",
{"post": post},
)Это полезно для HTML-фрагмента, который обновляется отдельным запросом. В Django 5.2 встроенных partialdef/partial нет; если код должен работать на 5.2 и 6.0, используйте include или осознанно подключите совместимый сторонний пакет.
Partial не заменяет компонентную модель frontend-фреймворка: у него нет изолированных props, состояния или автоматической клиентской гидратации.
Context processor — функция request -> dict, зарегистрированная в TEMPLATES[...]["OPTIONS"]["context_processors"]. Она применяется при рендеринге с RequestContext, в частности через shortcut render().
def product_context(request):
return {
"support_email": "support@example.com",
"release_channel": "stable",
}Не выполняйте там тяжёлый запрос на каждой странице. Данные, нужные одному экрану, передавайте из его view; стабильную глобальную конфигурацию можно кэшировать. Явный контекст view имеет приоритет с учётом порядка применения processors в RequestContext, поэтому не переиспользуйте неоднозначные имена.
Библиотека находится в templatetags установленного приложения:
blog/
└── templatetags/
├── __init__.py
└── blog_ui.pyfrom django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def initials(value):
return "".join(part[0].upper() for part in value.split() if part)
@register.simple_tag(takes_context=True)
def active_class(context, view_name):
match = context["request"].resolver_match
return "is-active" if match and match.view_name == view_name else ""{% load blog_ui %}
<a class="{% active_class 'blog:list' %}" href="{% url 'blog:list' %}">
Публикации
</a>Фильтр должен быть предсказуемым и не обращаться к базе. Tag, который возвращает HTML, строят через format_html(), а не через mark_safe() над строкой с внешними значениями. Inclusion tag с запросом к базе скрывает стоимость от view и затрудняет оптимизацию — использовать его стоит только с ясным кэшированием и тестом числа запросов.
После добавления нового модуля templatetags может понадобиться перезапуск development server, потому что библиотека обнаруживается при загрузке приложений.
{% load cache i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% cache 600 public_sidebar LANGUAGE_CODE %}
{% include "partials/public_sidebar.html" only %}
{% endcache %}Ключ должен включать каждую переменную, влияющую на HTML: язык, tenant, роль или пользователя. Нельзя кэшировать персональные данные под общим ключом. Источник данных всё равно готовит view: cache tag не исправляет N+1 внутри шаблона.
DTL, наследование, include, custom tags и автоэкранирование актуальны. С Django 4.1 cached loader обычно уже включён автоматически; старые инструкции с обязательной ручной production-настройкой избыточны, если нет custom loaders.
Template partials появились в Django 6.0. В legacy можно встретить сторонний django-template-partials с похожей идеей; при миграции сверяйтесь с его руководством и не удаляйте пакет, пока все шаблоны не переведены на встроенный синтаксис.
Далее: Формы и Виджеты