Django-Admin.ru

Дашборд на главной странице Django Admin: виджеты и графики

Как превратить главную страницу Django Admin в дашборд: переопределение index.html, данные через AdminSite.index или тег шаблона, Chart.js и кеш.

Редакция Django-Admin.ru
· обновлено · 11 мин чтения

Содержание · 10 разделов
  1. Как устроена главная страница админки
  2. Настройте поиск шаблонов
  3. Подготовьте данные для виджетов
  4. Передайте данные в шаблон
  5. Через тег шаблона (без замены AdminSite)
  6. Через свой AdminSite и метод index()
  7. Создайте шаблон дашборда
  8. Добавьте график на Chart.js
  9. Кеширование агрегатов
  10. Частые ошибки
  11. Шаблон не подхватывается
  12. Пропал список приложений
  13. TemplateSyntaxError: Invalid block tag … dashboard_stats
  14. График пустой
  15. Главная страница стала медленной
  16. Частые вопросы
  17. Как сделать дашборд отдельной страницей, а не на главной?
  18. Как показать разным сотрудникам разные виджеты?
  19. Как обновлять цифры без перезагрузки страницы?
  20. Есть ли готовые решения для дашборда?
  21. Можно ли построить график без JavaScript?
  22. Итог

Главная страница 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
↑ ↓ выборEnter открытьEsc закрыть