Кастомные теги шаблонов Django

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

Структура файла templatetags

Создай папку templatetags в своём приложении и добавь туда файл __init__.py и файл с тегами:

1myapp/
2      templatetags/
3          __init__.py
4          custom_tags.py

Простой тег (simple_tag)

Используй для простых операций, которые возвращают значение:

 1from django import template
 2  from django.utils import timezone
 3  from datetime import timedelta
 4
 5  register = template.Library()
 6
 7  @register.simple_tag
 8  def get_current_time(format_string):
 9      return timezone.now().strftime(format_string)
10
11  @register.simple_tag
12  def calculate_discount(price, discount_percent):
13      return price * (1 - discount_percent / 100)
14
15  @register.simple_tag(takes_context=True)
16  def user_greeting(context):
17      user = context['user']
18      if user.is_authenticated:
19          return f"Привет, {user.username}!"
20      return "Привет, гость!"

Тег включения (inclusion_tag)

Создаёт фрагмент шаблона с собственным контекстом:

 1@register.inclusion_tag('myapp/recent_posts.html')
 2  def show_recent_posts(count=5):
 3      from myapp.models import Post
 4      recent_posts = Post.objects.filter(
 5          published=True
 6      ).order_by('-created_at')[:count]
 7      return {'posts': recent_posts}
 8
 9  @register.inclusion_tag('myapp/user_menu.html', takes_context=True)
10  def user_menu(context):
11      user = context['user']
12      menu_items = []
13
14      if user.is_authenticated:
15          if user.is_staff:
16              menu_items.append({'url': '/admin/', 'title': 'Админка'})
17          menu_items.extend([
18              {'url': '/profile/', 'title': 'Профиль'},
19              {'url': '/logout/', 'title': 'Выход'}
20          ])
21      else:
22          menu_items.extend([
23              {'url': '/login/', 'title': 'Вход'},
24              {'url': '/register/', 'title': 'Регистрация'}
25          ])
26
27      return {'menu_items': menu_items}

Шаблон для inclusion_tag

Создай файл myapp/recent_posts.html:

 1<div class="recent-posts">
 2      <h4>Последние посты</h4>
 3      {% if posts %}
 4          <ul>
 5          {% for post in posts %}
 6              <li>
 7                  <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
 8                  <small>{{ post.created_at|date:"d.m.Y" }}</small>
 9              </li>
10          {% endfor %}
11          </ul>
12      {% else %}
13          <p>Постов пока нет</p>
14      {% endif %}
15  </div>

Кастомные фильтры

Фильтры преобразуют значения в шаблонах:

 1@register.filter
 2  def truncate_chars(value, arg):
 3      """Обрезает текст до указанного количества символов"""
 4      try:
 5          length = int(arg)
 6      except ValueError:
 7          return value
 8
 9      if len(value) <= length:
10          return value
11
12      return value[:length] + '...'
13
14  @register.filter(is_safe=True)
15  def highlight_search(text, search_term):
16      """Подсвечивает поисковые термины в тексте"""
17      if not search_term:
18          return text
19
20      from django.utils.html import mark_safe
21      highlighted = text.replace(
22          search_term,
23          f'<mark>{search_term}</mark>'
24      )
25      return mark_safe(highlighted)
26
27  @register.filter
28  def file_size_format(bytes_value):
29      """Форматирует размер файла в читаемом виде"""
30      for unit in ['B', 'KB', 'MB', 'GB']:
31          if bytes_value < 1024.0:
32              return f"{bytes_value:.1f} {unit}"
33          bytes_value /= 1024.0
34      return f"{bytes_value:.1f} TB"

Использование в шаблонах

Сначала загрузи кастомные теги в шаблоне:

 1{% load custom_tags %}
 2
 3  <!-- Простые теги -->
 4  <p>Текущее время: {% get_current_time "%H:%M:%S" %}</p>
 5  <p>Цена со скидкой: {% calculate_discount 1000 15 %}</p>
 6  <p>{% user_greeting %}</p>
 7
 8  <!-- Теги включения -->
 9  {% show_recent_posts 3 %}
10  {% user_menu %}
11
12  <!-- Фильтры -->
13  <p>{{ long_text|truncate_chars:100 }}</p>
14  <p>{{ search_text|highlight_search:query }}</p>
15  <p>Размер файла: {{ file_size|file_size_format }}</p>

Продвинутые возможности

Создание тегов с множественными аргументами:

 1@register.simple_tag
 2  def pagination_info(page_obj, page_range=5):
 3      """Создаёт информацию для пагинации"""
 4      current_page = page_obj.number
 5      total_pages = page_obj.paginator.num_pages
 6
 7      start_page = max(1, current_page - page_range // 2)
 8      end_page = min(total_pages, start_page + page_range - 1)
 9
10      return {
11          'current_page': current_page,
12          'total_pages': total_pages,
13          'start_page': start_page,
14          'end_page': end_page,
15          'has_previous': page_obj.has_previous(),
16          'has_next': page_obj.has_next(),
17          'previous_page': page_obj.previous_page_number() if page_obj.has_previous() else None,
18          'next_page': page_obj.next_page_number() if page_obj.has_next() else None,
19      }
20
21  @register.inclusion_tag('myapp/pagination.html')
22  def render_pagination(page_obj, page_range=5):
23      return pagination_info(page_obj, page_range)

Кэширование тегов

Используй кэширование для дорогих операций:

 1from django.core.cache import cache
 2
 3  @register.simple_tag
 4  def cached_user_stats(user_id):
 5      """Кэширует статистику пользователя"""
 6      cache_key = f"user_stats_{user_id}"
 7      stats = cache.get(cache_key)
 8
 9      if stats is None:
10          # Выполняем дорогую операцию
11          from myapp.models import Post, Comment
12          stats = {
13              'posts_count': Post.objects.filter(author_id=user_id).count(),
14              'comments_count': Comment.objects.filter(author_id=user_id).count(),
15              'last_activity': Post.objects.filter(
16                  author_id=user_id
17              ).order_by('-created_at').first()
18          }
19          # Кэшируем на 1 час
20          cache.set(cache_key, stats, 3600)
21
22      return stats

Тестирование кастомных тегов

Создай тесты для проверки работы тегов:

 1from django.test import TestCase, RequestFactory
 2  from django.contrib.auth.models import User
 3  from django.template import Context, Template
 4  from myapp.templatetags.custom_tags import register
 5
 6  class CustomTagsTest(TestCase):
 7      def setUp(self):
 8          self.factory = RequestFactory()
 9          self.user = User.objects.create_user(
10              username='testuser',
11              password='testpass'
12          )
13
14      def test_multiply_tag(self):
15          template = Template('{% load custom_tags %}{% multiply 5 10 %}')
16          context = Context({})
17          self.assertEqual(template.render(context), '50')
18
19      def test_user_greeting_authenticated(self):
20          request = self.factory.get('/')
21          request.user = self.user
22          template = Template('{% load custom_tags %}{% user_greeting %}')
23          context = Context({'request': request})
24          self.assertIn('testuser', template.render(context))
25
26      def test_truncate_chars_filter(self):
27          template = Template('{% load custom_tags %}{{ "Hello World"|truncate_chars:5 }}')
28          context = Context({})
29          self.assertEqual(template.render(context), 'Hello...')

Лучшие практики

  • Именование: Используй понятные имена для тегов и фильтров
  • Документация: Добавляй docstring для всех тегов и фильтров
  • Производительность: Кэшируй дорогие операции
  • Безопасность: Используй is_safe=True только для проверенного HTML
  • Тестирование: Покрывай тегами unit-тестами
  • Переиспользование: Создавай универсальные теги для разных приложений

Частые ошибки и их решения

Ошибка: "TemplateSyntaxError: Invalid filter"

Решение: Убедись, что файл __init__.py существует в папке templatetags

Ошибка: "Template tag not found"

Решение: Проверь правильность загрузки тегов: {% load custom_tags %}

Ошибка: "Context variable not found"

Решение: Используй takes_context=True для доступа к контексту шаблона

FAQ

Q: Когда использовать кастомные теги?
A: Для логики, которая не помещается в шаблонах: вычисления, форматирование, сложные операции, работа с базой данных.

Q: Можно ли использовать теги в других тегах?
A: Да, но лучше создавать отдельные функции и вызывать их из тегов для лучшей тестируемости.

Q: Как передать переменные из шаблона в тег?
A: Используй параметры тега или takes_context=True для доступа к контексту.

Q: Нужно ли перезапускать сервер после изменения тегов?
A: Да, Django не перезагружает теги автоматически в production режиме.

Q: Как создать тег, который работает с несколькими приложениями?
A: Создай отдельное Django приложение только для тегов или используй общие утилиты.