До этого момента наш сайт возвращает только HTML без какого-либо оформления. В реальном приложении к страницам подключаются CSS, JavaScript и изображения — всё это называется статическими файлами: они не генерируются динамически, а отдаются браузеру как есть.
Django разделяет два типа файлов, которые легко перепутать:
- Статические файлы (
static) — CSS, JS, иконки, шрифты. Часть исходного кода проекта, не меняются в процессе работы сайта. - Медиафайлы (
media) — файлы, загруженные пользователями: постеры фильмов, аватары. Появляются в процессе работы сайта. Разберём их в модуле 5, когда дойдём до форм с загрузкой файлов.
Откроем settings.py и проверим настройки:
# filmsite/settings.py
# URL-префикс для статических файлов в браузере
STATIC_URL = '/static/'
# Папки, где Django ищет статические файлы помимо папок приложений
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Папка, куда collectstatic собирает все файлы для продакшна
# (используется при деплое — модуль 9)
STATIC_ROOT = BASE_DIR / 'staticfiles'django.contrib.staticfiles уже включён в INSTALLED_APPS по умолчанию — он отвечает за обслуживание статических файлов в режиме разработки.
Статические файлы можно хранить двумя способами, которые Django объединяет автоматически:
filmsite/
├── static/ ← общие файлы проекта
│ └── css/
│ └── base.css
└── films/
└── static/
└── films/ ← файлы приложения (та же логика, что с шаблонами)
├── css/
│ └── films.css
└── img/
└── logo.png
Поддиректория с именем приложения внутри films/static/ — по той же причине, что и в шаблонах: защита от конфликтов имён между приложениями.
Чтобы использовать статические файлы в шаблоне, нужно сначала загрузить библиотеку тегов командой {% load static %}, а затем получить URL файла через тег {% static %}:
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<link rel="stylesheet" href="{% static 'css/base.css' %}">
</head>{% static 'css/base.css' %} преобразуется в /static/css/base.css — абсолютный URL, по которому браузер запросит файл.
Добавим {% load static %} в базовый шаблон. Важно: {% load %} работает только в том шаблоне, где написан — дочерние шаблоны не наследуют загруженные библиотеки:
<!-- templates/base.html -->
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Сайт фильмов{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/base.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{% url 'films:index' %}">Главная</a> |
<a href="{% url 'films:film_list' %}">Каталог</a> |
<a href="{% url 'films:about' %}">О сайте</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2024 Сайт фильмов</p>
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>Добавили два новых блока: extra_css и extra_js — для страниц, которым нужны дополнительные стили или скрипты. Дочерний шаблон может их заполнить через {{ block.super }}, не затрагивая базовые файлы.
Создадим базовый CSS-файл static/css/base.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Arial, sans-serif;
max-width: 960px;
margin: 0 auto;
padding: 20px;
color: #333;
}
nav {
padding: 12px 0;
border-bottom: 1px solid #ddd;
margin-bottom: 24px;
}
nav a {
color: #333;
text-decoration: none;
margin-right: 12px;
}
nav a:hover {
text-decoration: underline;
}
footer {
margin-top: 40px;
padding-top: 16px;
border-top: 1px solid #ddd;
color: #999;
font-size: 14px;
}DTL намеренно ограничен — в шаблонах нельзя вызывать произвольные Python-функции. Но иногда нужна логика, которая не покрывается стандартными фильтрами и тегами. Для этого Django позволяет создавать собственные теги шаблонов.
Пользовательские теги хранятся в специальной директории templatetags внутри приложения:
films/
├── templatetags/
│ ├── __init__.py ← обязательно, делает папку пакетом
│ └── film_tags.py ← наши теги
├── models.py
└── views.py
Имя файла (film_tags.py) — это имя библиотеки, которую нужно загрузить в шаблоне: {% load film_tags %}.
simple_tag — самый простой тип пользовательского тега. Это Python-функция, которая принимает аргументы и возвращает значение, подставляемое в шаблон.
Хардкодить год в футере — плохая практика: раз в год придётся вручную обновлять шаблон. Сделаем тег, который возвращает текущий год автоматически:
# films/templatetags/film_tags.py
from django import template
import datetime
register = template.Library()
@register.simple_tag
def current_year():
return datetime.date.today().yearОбъект register — это точка входа для регистрации тегов и фильтров. Декоратор @register.simple_tag регистрирует функцию как тег.
Использование в шаблоне:
<!-- templates/base.html -->
{% load film_tags %}
...
<footer>
<p>© {% current_year %} Сайт фильмов</p>
</footer>simple_tag может принимать аргументы — позиционные и именованные:
@register.simple_tag
def film_url(film_id, anchor=''):
"""Возвращает URL страницы фильма, опционально с якорем."""
from django.urls import reverse
url = reverse('films:film_detail', kwargs={'film_id': film_id})
return f'{url}#{anchor}' if anchor else url<a href="{% film_url film.id 'reviews' %}">Рецензии на {{ film.title }}</a>Результат simple_tag можно сохранить в переменную контекста через as:
@register.simple_tag
def get_top_films(count=3):
"""Возвращает список фильмов с наивысшим рейтингом."""
# Сейчас — заглушка, в модуле 3 подключим QuerySet
films = [
{'id': 1, 'title': 'Крёстный отец', 'year': 1972},
{'id': 3, 'title': 'Побег из Шоушенка', 'year': 1994},
]
return films[:count]{% get_top_films 2 as top_films %}
{% for film in top_films %}
<p>{{ film.title }}</p>
{% endfor %}inclusion_tag — более мощный тип тега. Он не просто возвращает значение, а рендерит отдельный шаблон и вставляет результат в страницу.
Это идеальный инструмент для повторяющихся блоков с собственной логикой: виджет последних фильмов на боковой панели, блок навигации с активным пунктом, мини-форма поиска.
Создадим виджет, который будет показывать несколько последних фильмов — его можно вставить в любой шаблон сайта:
# films/templatetags/film_tags.py
from django import template
import datetime
register = template.Library()
@register.simple_tag
def current_year():
return datetime.date.today().year
@register.inclusion_tag('films/includes/latest_films.html')
def latest_films(count=3):
"""Рендерит виджет с последними добавленными фильмами."""
# Заглушка — в модуле 3 заменим на запрос к БД
films = [
{'id': 1, 'title': 'Крёстный отец', 'year': 1972},
{'id': 2, 'title': 'Список Шиндлера', 'year': 1993},
{'id': 3, 'title': 'Побег из Шоушенка', 'year': 1994},
]
return {'films': films[:count]}Обрати внимание: inclusion_tag возвращает словарь — это контекст для шаблона виджета.
Создаём шаблон виджета films/templates/films/includes/latest_films.html:
{% load film_tags %}
<div class="latest-films">
<h3>Последние добавленные</h3>
<ul>
{% for film in films %}
<li><a href="{% url 'films:film_detail' film_id=film.id %}">{{ film.title }}</a></li>
{% empty %}
<li>Фильмов пока нет.</li>
{% endfor %}
</ul>
</div>Используем виджет в базовом шаблоне:
<!-- templates/base.html -->
{% load static %}
{% load film_tags %}
...
<aside>
{% latest_films 3 %}
</aside>Иногда тегу нужен доступ к объекту запроса — например, чтобы знать текущего пользователя или отметить активный пункт навигации. Для этого добавляем takes_context=True:
@register.inclusion_tag('films/includes/nav.html', takes_context=True)
def nav_menu(context):
"""Рендерит навигацию с выделением активного раздела."""
request = context['request']
return {
'current_path': request.path,
}<!-- films/templates/films/includes/nav.html -->
<nav>
<a href="{% url 'films:index' %}"
{% if current_path == '/' %}class="active"{% endif %}>
Главная
</a>
<a href="{% url 'films:film_list' %}"
{% if '/films/' in current_path %}class="active"{% endif %}>
Каталог
</a>
</nav>Это самая частая ошибка при работе с тегами. Если {% load film_tags %} написан в base.html, дочерний шаблон всё равно должен загрузить библиотеку сам — если использует теги из неё:
<!-- Не сработает — дочерний шаблон не наследует {% load %} из base.html -->
{% extends 'base.html' %}
{% block content %}
{% current_year %} {# TemplateSyntaxError #}
{% endblock %}
<!-- Правильно — загружаем библиотеку явно -->
{% extends 'base.html' %}
{% load film_tags %}
{% block content %}
{% current_year %}
{% endblock %}Папка templatetags без файла __init__.py не будет распознана Django как Python-пакет — теги не загрузятся:
films/templatetags/
├── __init__.py ← обязателен, может быть пустым
└── film_tags.py
В режиме разработки (DEBUG = True) Django сам обслуживает статические файлы. В продакшне этого не происходит — нужно запустить команду collectstatic, которая собирает все статические файлы в папку STATIC_ROOT, откуда их раздаёт Nginx. Разберём это в модуле 9 при деплое.
filmsite/
├── static/
│ └── css/
│ └── base.css
├── templates/
│ └── base.html
├── filmsite/
│ └── settings.py
└── films/
├── static/
│ └── films/
│ └── css/
│ └── films.css
├── templates/
│ └── films/
│ ├── index.html
│ ├── about.html
│ ├── film_list.html
│ ├── film_detail.html
│ └── includes/
│ ├── film_card.html
│ └── latest_films.html
├── templatetags/
│ ├── __init__.py
│ └── film_tags.py
└── views.py
- Чем статические файлы отличаются от медиафайлов в Django?
- Почему
{% load static %}нужно писать в каждом шаблоне, который использует статические файлы, — даже если базовый шаблон его уже загружает? - В чём разница между
simple_tagиinclusion_tag? - Что вернёт
inclusion_tag— строку или словарь? Почему? - Зачем нужен файл
__init__.pyв папкеtemplatetags?
Тип: расширь проект
Часть 1. Подключи статические файлы к проекту:
- Убедись, что в
settings.pyнастроеныSTATIC_URLиSTATICFILES_DIRS - Создай файл
static/css/base.cssс минимальными стилями на свой вкус (шрифт, отступы, оформление навигации) - Подключи его в
base.htmlчерез{% load static %}и{% static %}
Часть 2. Создай пользовательский тег film_tags:
- Создай папку
films/templatetags/с файлами__init__.pyиfilm_tags.py - Напиши
simple_tagс именемcurrent_year, который возвращает текущий год - Используй его в футере
base.htmlвместо хардкода:© {% current_year %} Сайт фильмов - Напиши
inclusion_tagс именемlatest_films, который принимает аргументcountи рендерит шаблонfilms/includes/latest_films.htmlсо списком фильмов из заглушки - Вставь
{% latest_films 3 %}вbase.html— например, перед закрывающим тегом</body>