Дашборд на главной странице Django Admin: виджеты и графики
Как превратить главную страницу Django Admin в дашборд: переопределение index.html, данные через AdminSite.index или тег шаблона, Chart.js и кеш.
Содержание · 10 разделов
- Как устроена главная страница админки
- Настройте поиск шаблонов
- Подготовьте данные для виджетов
- Передайте данные в шаблон
- Через тег шаблона (без замены AdminSite)
- Через свой AdminSite и метод index()
- Создайте шаблон дашборда
- Добавьте график на Chart.js
- Кеширование агрегатов
- Частые ошибки
- Шаблон не подхватывается
- Пропал список приложений
- TemplateSyntaxError: Invalid block tag … dashboard_stats
- График пустой
- Главная страница стала медленной
- Частые вопросы
- Как сделать дашборд отдельной страницей, а не на главной?
- Как показать разным сотрудникам разные виджеты?
- Как обновлять цифры без перезагрузки страницы?
- Есть ли готовые решения для дашборда?
- Можно ли построить график без JavaScript?
- Итог
Главная страница Django Admin показывает список приложений и ваши последние действия. Её легко превратить в дашборд с ключевыми цифрами и графиком: не нужен ни отдельный фронтенд, ни сторонняя тема. Ядро Django при этом не трогаем.
Коротко: создайте templates/admin/index.html, начните его с {% extends "admin/index.html" %} и переопределите блок content, сохранив {{ block.super }}. Данные для виджетов передайте через свой тег шаблона или через метод index() собственного AdminSite. Агрегаты кешируйте. Примеры проверены на Django 5.2 LTS и 6.x.
Как устроена главная страница админки
Главную страницу отдаёт метод AdminSite.index(request, extra_context=None). Он собирает контекст из трёх частей:
each_context(request)— переменные, общие для всех страниц админки:site_header,site_title,available_appsи другие;titleиapp_list— заголовок и список приложений с моделями;extra_context— всё, что вы передали сами. Оно добавляется последним и может перезаписать любой ключ.
Затем рендерится шаблон admin/index.html или тот, что указан в атрибуте index_template. Этот шаблон наследует admin/base_site.html и заполняет несколько блоков:
| Блок | Что в нём по умолчанию |
|---|---|
extrastyle | подключение admin/css/dashboard.css |
content | список приложений (admin/app_list.html) внутри #content-main |
sidebar | модуль «Последние действия» |
extrahead | наследуется от base.html, по умолчанию пуст |
Виджеты удобнее всего вставить в блок content перед списком приложений.
Настройте поиск шаблонов
Django ищет шаблоны в таком порядке: сначала каталоги из TEMPLATES["DIRS"], затем папки templates/ приложений в порядке INSTALLED_APPS. Чтобы ваш admin/index.html нашёлся раньше встроенного, есть два варианта.
Первый вариант — общая папка шаблонов проекта:
# settings.py
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",
],
},
},
]
Второй вариант — шаблон лежит в приложении (shop/templates/admin/index.html), а само приложение стоит в INSTALLED_APPS выше django.contrib.admin.
Контекст-процессор request нужен в любом случае: через него в шаблонах доступен request, а без него админка выдаёт предупреждение при manage.py check.
Подготовьте данные для виджетов
Вынесите расчёт в отдельную функцию. Её можно вызвать и из шаблона, и из AdminSite, и из тестов. В примерах используется модель заказа:
# shop/models.py
from django.db import models
from django.utils import timezone
class Order(models.Model):
class Status(models.TextChoices):
NEW = "new", "Новый"
PAID = "paid", "Оплачен"
CANCELED = "canceled", "Отменён"
customer = models.CharField("Покупатель", max_length=200)
status = models.CharField(
"Статус", max_length=20, choices=Status.choices, default=Status.NEW
)
amount = models.DecimalField("Сумма", max_digits=10, decimal_places=2)
created = models.DateTimeField("Создан", default=timezone.now, db_index=True)
Функция считает три числа и ряд для графика. Все агрегаты считает база данных, Python только раскладывает результат по дням:
# shop/dashboard.py
from datetime import timedelta
from django.core.cache import cache
from django.db.models import Count, Sum
from django.db.models.functions import TruncDate
from django.utils import timezone
from .models import Order
CACHE_KEY = "admin-dashboard:v1"
CACHE_TIMEOUT = 300 # секунд
def _collect():
today = timezone.localdate()
start = today - timedelta(days=13)
paid = Order.objects.filter(status=Order.Status.PAID)
per_day = dict(
paid.filter(created__date__gte=start)
.annotate(day=TruncDate("created"))
.values("day")
.annotate(n=Count("id"))
.values_list("day", "n")
)
days = [start + timedelta(days=i) for i in range(14)]
return {
"orders_today": Order.objects.filter(created__date=today).count(),
"new_orders": Order.objects.filter(status=Order.Status.NEW).count(),
"revenue_30d": paid.filter(
created__date__gte=today - timedelta(days=29)
).aggregate(total=Sum("amount", default=0))["total"],
"chart": {
"labels": [d.strftime("%d.%m") for d in days],
"values": [per_day.get(d, 0) for d in days],
},
}
def get_dashboard_data():
return cache.get_or_set(CACHE_KEY, _collect, timeout=CACHE_TIMEOUT)
Дни без заказов база не вернёт, поэтому ряд дополняется нулями через per_day.get(d, 0), иначе на графике появятся разрывы. Связка values() + annotate() подробно разобрана в шпаргалке по QuerySet.
Передайте данные в шаблон
Через тег шаблона (без замены AdminSite)
Самый простой путь: стандартный admin.site остаётся как есть, а шаблон сам запрашивает данные через simple_tag:
# shop/templatetags/admin_dashboard.py
from django import template
from shop.dashboard import get_dashboard_data
register = template.Library()
@register.simple_tag(takes_context=True)
def dashboard_stats(context):
request = context["request"]
if not request.user.has_perm("shop.view_order"):
return None
return get_dashboard_data()
Не забудьте пустой shop/templatetags/__init__.py. Проверка has_perm важна: на главную страницу попадает любой сотрудник с is_staff, а сводка по выручке нужна не всем.
Через свой AdminSite и метод index()
Если вы уже используете собственный класс сайта (или их несколько), передайте данные через extra_context:
# config/admin.py
from django.contrib import admin
from shop.dashboard import get_dashboard_data
class ShopAdminSite(admin.AdminSite):
site_header = "Магазин: управление"
index_title = "Сводка"
def index(self, request, extra_context=None):
extra_context = extra_context or {}
if request.user.has_perm("shop.view_order"):
extra_context["stats"] = get_dashboard_data()
return super().index(request, extra_context)
Чтобы этот класс заменил стандартный admin.site, создайте свой AdminConfig и укажите его в INSTALLED_APPS вместо django.contrib.admin:
# config/apps.py
from django.contrib.admin.apps import AdminConfig
class ShopAdminConfig(AdminConfig):
default_site = "config.admin.ShopAdminSite"
# settings.py
INSTALLED_APPS = [
"shop",
"config.apps.ShopAdminConfig", # вместо "django.contrib.admin"
"django.contrib.auth",
# ...
]
В этом варианте переменная stats уже лежит в контексте, и строка с тегом dashboard_stats в шаблоне не нужна. Если данные нужны на всех страницах админки (например, плашка «тестовый стенд» в шапке), переопределите each_context(request): вызовите super() и допишите ключ в возвращённый словарь.
| Критерий | Тег шаблона | Свой AdminSite |
|---|---|---|
| Изменения в settings | не нужны | замена django.contrib.admin на свой AdminConfig |
| Где живёт логика | в шаблоне и теге | в Python-классе сайта |
| Удобно тестировать | тег вызывается из шаблона | index() проверяется тестовым клиентом |
| Когда выбирать | один дашборд на стандартной админке | несколько сайтов админки или уже есть свой класс |
Создайте шаблон дашборда
Создайте templates/admin/index.html. Строка {% extends "admin/index.html" %} в файле с тем же именем не приводит к рекурсии: Django пропускает текущий шаблон и находит встроенный. А {{ block.super }} в конце блока content сохраняет список приложений.
{% extends "admin/index.html" %}
{% load static admin_dashboard %}
{% block extrastyle %}{{ block.super }}
<link rel="stylesheet" href="{% static 'shop/dashboard.css' %}">
{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'shop/vendor/chart.umd.min.js' %}" defer></script>
<script src="{% static 'shop/dashboard.js' %}" defer></script>
{% endblock %}
{% block content %}
{% dashboard_stats as stats %}
{% if stats %}
<div class="dashboard-widgets">
<section class="module widget">
<h2>Заказы сегодня</h2>
<p class="widget-value">{{ stats.orders_today }}</p>
</section>
<section class="module widget">
<h2>Ждут обработки</h2>
<p class="widget-value">
<a href="{% url 'admin:shop_order_changelist' %}?status__exact=new">{{ stats.new_orders }}</a>
</p>
</section>
<section class="module widget">
<h2>Выручка за 30 дней</h2>
<p class="widget-value">{{ stats.revenue_30d|floatformat:"0g" }} ₽</p>
</section>
<section class="module widget widget-wide">
<h2>Оплаченные заказы за 14 дней</h2>
<canvas id="orders-chart" height="110"></canvas>
</section>
</div>
{{ stats.chart|json_script:"orders-chart-data" }}
{% endif %}
{{ block.super }}
{% endblock %}
Виджет «Ждут обработки» ведёт сразу в отфильтрованный список заказов: с дашборда можно сразу перейти к работе. Если нужен только один дашборд и не хочется перекрывать admin/index.html целиком, можно задать стандартному сайту другой шаблон: admin.site.index_template = "admin/dashboard.html".
Стили используют CSS-переменные админки, поэтому виджеты сами подстраиваются под светлую и тёмную тему:
/* shop/static/shop/dashboard.css */
.dashboard-widgets {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.dashboard-widgets .widget {
margin: 0;
}
.dashboard-widgets .widget-wide {
grid-column: 1 / -1;
padding-bottom: 12px;
}
.widget-value {
margin: 12px;
font-size: 2rem;
font-weight: 600;
color: var(--body-loud-color);
}
Добавьте график на Chart.js
Скачайте chart.umd.min.js из папки dist/ пакета chart.js версии 4 (на октябрь 2026 года актуальна 4.5.1) и положите в shop/static/shop/vendor/. Локальная копия надёжнее CDN: админка часто работает во внутренней сети, а политика CSP может запрещать сторонние скрипты.
mkdir -p shop/static/shop/vendor
curl -L -o shop/static/shop/vendor/chart.umd.min.js https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js
В Windows PowerShell 5.1 создайте папку командой mkdir shop/static/shop/vendor и вызывайте curl.exe: просто curl там — псевдоним Invoke-WebRequest с другими параметрами.
Данные передаются фильтром json_script. Он выводит безопасный блок <script type="application/json"> с экранированием. Не вставляйте JSON в код через |safe: так в страницу легко пробросить чужую строку с HTML. Сам скрипт лежит в статическом файле, без инлайн-кода:
// shop/static/shop/dashboard.js
document.addEventListener('DOMContentLoaded', () => {
const source = document.getElementById('orders-chart-data');
const canvas = document.getElementById('orders-chart');
if (!source || !canvas || !window.Chart) return;
const { labels, values } = JSON.parse(source.textContent);
const color = getComputedStyle(document.documentElement)
.getPropertyValue('--secondary')
.trim();
new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [
{ label: 'Оплачено заказов', data: values, borderColor: color, tension: 0.3 },
],
},
options: {
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } },
},
});
});
Скрипты с defer выполняются по порядку после разбора HTML, поэтому к моменту DOMContentLoaded библиотека уже загружена. Цвет линии берётся из переменной --secondary: если вы перекрасите админку по инструкции из статьи о переопределении шаблонов и стилей, график сменит цвет вместе с ней.
После добавления статики в продакшне выполните python manage.py collectstatic. Без этого шага браузер получит 404 на скрипты.
Кеширование агрегатов
Главную страницу админки открывают десятки раз в день, и каждый раз виджеты пересчитываются заново. cache.get_or_set() из примера выше выполняет запросы только при пустом кеше, а потом 5 минут отдаёт готовый словарь. Что важно учесть:
- Бэкенд кеша. По умолчанию Django использует
LocMemCache: у каждого процесса Gunicorn свой кеш. Для нескольких воркеров подключите Redis (django.core.cache.backends.redis.RedisCache, Django 4.0+) или Memcached. - Ключ кеша. В примере данные одинаковы для всех, кто имеет право
view_order. Если цифры зависят от пользователя (менеджер видит только свои заказы), добавьтеrequest.user.pkв ключ. - Сброс. Чтобы новые заказы появлялись сразу, удаляйте ключ в обработчике сигнала
post_saveмоделиOrder:cache.delete(CACHE_KEY). Учтите, чтоQuerySet.update()иbulk_create()сигналыpost_saveне отправляют. - Тяжёлые отчёты. Если расчёт идёт секунды, считайте его по расписанию в фоновой задаче (например, через Celery) и кладите результат в кеш. Тогда страница только читает готовое.
Частые ошибки
Шаблон не подхватывается
Проверьте путь: файл должен лежать именно в templates/admin/index.html, а не в templates/index.html. Папка из DIRS должна существовать, а приложение с шаблоном должно стоять выше django.contrib.admin. Быстрая проверка в python manage.py shell: from django.template.loader import get_template; get_template("admin/index.html").origin.name. Команда покажет путь к файлу, который реально используется.
Пропал список приложений
В блоке content нет {{ block.super }}, и вы целиком заменили содержимое. Верните {{ block.super }} после виджетов.
TemplateSyntaxError: Invalid block tag … dashboard_stats
Тег не загружен: в шаблоне нет {% load admin_dashboard %}. Если же Django пишет 'admin_dashboard' is not a registered tag library, модуль тегов не найден: проверьте файл __init__.py в templatetags/, наличие приложения в INSTALLED_APPS и перезапустите сервер после создания модуля.
График пустой
Откройте консоль браузера. Частые причины: 404 на chart.umd.min.js (нет collectstatic или неверный путь), не совпадает id в json_script и getElementById, у пользователя нет права view_order и блок не выводится вовсе.
Главная страница стала медленной
Каждый виджет — это минимум один запрос. Проверьте их число через Django Debug Toolbar, включите кеш и следите, чтобы агрегаты считались в базе (aggregate, annotate), а не циклом по объектам в Python. Про лишние запросы подробнее — в статье о проблеме N+1.
Частые вопросы
Как сделать дашборд отдельной страницей, а не на главной?
Добавьте в свой AdminSite маршрут через get_urls() и оберните представление в admin_view() — оно проверит вход и is_staff:
# config/admin.py
from django.contrib import admin
from django.template.response import TemplateResponse
from django.urls import path
from shop.dashboard import get_dashboard_data
class ShopAdminSite(admin.AdminSite):
def get_urls(self):
return [
path("dashboard/", self.admin_view(self.dashboard_view), name="dashboard"),
*super().get_urls(),
]
def dashboard_view(self, request):
context = {**self.each_context(request), "title": "Сводка", "stats": get_dashboard_data()}
return TemplateResponse(request, "admin/dashboard.html", context)
Ссылка на страницу — {% url 'admin:dashboard' %}. Шаблон admin/dashboard.html наследуйте от admin/base_site.html. Подробнее о своих страницах в админке — в статье об устройстве AdminSite и ModelAdmin.
Как показать разным сотрудникам разные виджеты?
Проверяйте права отдельно для каждого виджета: request.user.has_perm("shop.view_order"), членство в группе или свой флаг. Если от пользователя зависят сами цифры, добавьте его pk в ключ кеша — иначе один сотрудник увидит данные другого.
Как обновлять цифры без перезагрузки страницы?
Добавьте в get_urls() представление, которое возвращает JsonResponse(get_dashboard_data()), и запрашивайте его из dashboard.js через fetch() по таймеру, например раз в минуту. Оборачивайте его в admin_view(), как страницу дашборда, иначе данные станут доступны без входа.
Есть ли готовые решения для дашборда?
Есть в составе тем: в Unfold главная страница настраивается функцией из параметра DASHBOARD_CALLBACK и набором компонентов, в Grappelli дашборд собирается из модулей на Python-классах. Сравнение тем — в обзоре Unfold, Jazzmin и Grappelli. Если тема не нужна, подхода из этой статьи достаточно.
Можно ли построить график без JavaScript?
Да: нарисуйте его на сервере через Matplotlib в SVG и выведите в шаблоне как разметку. Так удобно делать отчёты с учётом фильтров списка — пример в статье «Графики и отчёты в Django Admin».
Итог
Дашборд в Django Admin собирается из трёх частей: шаблона admin/index.html с {% extends "admin/index.html" %}, источника данных (тег шаблона или AdminSite.index) и кеша для агрегатов. Графики добавляются статическим скриптом Chart.js с данными из json_script.
Остальные возможности админки собраны в полном руководстве по Django Admin. Графики прямо в списке объектов и выгрузку в CSV разбираем в статье «Графики и отчёты в Django Admin». Готовые темы со встроенными дашбордами, например Unfold, сравниваем в обзоре тем для Django Admin.
- #Django Admin
- #Дашборд
- #Chart.js
- #Шаблоны Admin
- #Кастомизация Admin