Python

Как превратить админку Django в легковесную панель инструментов

Spread the love

В статье описано как добавить сводную таблицу и диаграммы в Django Admin

Оригинальная статья: Haki Benita — How to Turn Django Admin Into a Lightweight Dashboard

Django Admin — это мощный инструмент для управления данными в вашем приложении. Тем не менее, он не был разработан для добавления сводных таблиц и диаграмм. К счастью, разработчики Django Admin упростили нам подобную настройку.

Вот что мы собираемся сделать к концу статьи:

Django admin dashboard

Зачем нам это делать?

Существует множество инструментов, приложений и пакетов, которые могут создавать очень красивые панели. Я лично обнаружил, что если продукт не является реальной панелью мониторинга, в большинстве случаев все, что вам нужно, это простая сводная таблица и несколько диаграмм.

Второе, и не менее важное причина — отсутствие дополнительных зависимостей.

Если все, что вам нужно, это немного улучшить интерфейс администратора, то этот подход определенно стоит рассмотреть.

Начальная настройка

Мы собираемся использовать готовую модель Sale.

Чтобы использовать всю мощь Django Admin, мы собираемся создать нашу панель управления на базе ModelAdmin.

Для этого нам нужна модель:

# models.py

class SaleSummary(Sale):
    class Meta:
        proxy = True
        verbose_name = 'Sale Summary'
        verbose_name_plural = 'Sales Summary'


Proxy-модель расширяет функциональность другой модели, не создавая фактическую таблицу в базе данных.

Теперь, когда у нас есть модель, мы можем создать ModelAdmin:

# admin.py

from django.contrib import admin
from .models import SaleSummary

@admin.register(SaleSummary)
class SaleSummaryAdmin(ModelAdmin):
    change_list_template = 'admin/sale_summary_change_list.html'
    date_hierarchy = 'created'


Поскольку мы используем стандартную ModelAdmin, мы можем использовать все его функции. В этом примере я добавил date_hierarchy для фильтрации продаж по дате создания. Мы собираемся использовать это позже для графика.

Чтобы страница выглядела как «обычная» страница администратора, мы расширим шаблон change_list Django и поместим наш контент в блок result_list:

<!-- sales/templates/admin/sale_summary_change_list.html -->

{% extends "admin/change_list.html" %}

{% block content_title %}
    <h1> Sales Summary </h1>
{% endblock %}

{% block result_list %}
    <!-- Our content goes here... -->
{% endblock %}

{% block pagination %}{% endblock %}


Вот как стала выглядит наша страница на данный момент:

A bare Django admin dashboard

Добавление сводной таблицы

Контекст, отправляемый в шаблон, заполняется ModelAdmin в функции changelist_view.

Чтобы отобразить таблицу в шаблоне, мы извлекаем данные в changelist_view и добавляем их в контекст:

# admin.py

class SaleSummaryAdmin(ModelAdmin):

    # ...

    def changelist_view(self, request, extra_context=None):
        response = super().changelist_view(
            request,
            extra_context=extra_context,
        )

        try:
            qs = response.context_data['cl'].queryset
        except (AttributeError, KeyError):
            return response

        metrics = {
            'total': Count('id'),
            'total_sales': Sum('price'),
        }

        response.context_data['summary'] = list(
            qs
            .values('sale__category__name')
            .annotate(**metrics)
            .order_by('-total_sales')
        )

        return response


Давайте рассмотрим что мы сделали:

  1. Вызываем super, чтобы позволить Django делать свое дело (заполняем заголовки, breadcrumbs, наборы запросов, фильтров и т. д.).
  2. Извлекаем созданный для нас набор запросов из context. На этом этапе запрос фильтруется с помощью любых встроенных фильтров или иерархии дат, выбранных пользователем.
  3. Если мы не можем извлечь набор запросов из контекста, это, скорее всего, связано с неверными параметрами запроса. В таких случаях мы сразу возвращаем response.
  4. Суммируем общий объем продаж по категориям и возвращаем список (указание «метрики» станет понятным в следующем разделе).

Теперь, когда у нас есть данные в контексте, мы можем отобразить их в шаблоне:

<!-- sale_summary_change_list.html -->

{% load humanize %}

<!-- ... -->

{% block result_list %}

<div class="results">
    <table>

    <thead>
      <tr>
        <th>
          <div class="text">
            <a href="#">Category</a>
          </div>
        </th>
        <th>
          <div class="text">
            <a href="#">Total</a>
          </div>
        </th>
        <th>
          <div class="text">
            <a href="#">Total Sales</a>
          </div>
        </th>
        <th>
          <div class="text">
            <a href="#">
              <strong>% Of Total Sales</strong>
            </a>
          </div>
        </th>
      </tr>
    </thead>

    <tbody>
      {% for row in summary %}
      <tr class="{% cycle 'row1' 'row2' %}">
        <td> {{ row.sale__category__name }} </td>
        <td> {{ row.total | intcomma }} </td>
        <td> {{ row.total_sales | default:0 | intcomma }}$ </td>
        <td>
          <strong>
          {{ row.total_sales |
              default:0 |
              percentof:summary_total.total_sales }}
          </strong>
        </td>
      </tr>
      {% endfor %}
    </tbody>

  </table>
</div>

<!-- ... -->

{% endblock %}


Разметка важна. Чтобы получить нативный внешний вид Django, нам нужно отображать таблицы таким же образом, как Django их отображает.

Это то, что мы имеем на данном этапе:

Django admin dashboard with just a table

Сейчас у нас есть сводная таблица без нижней строки. Мы можем использовать метрики и сделать немного вуду Django ORM, чтобы быстро вычислить итоговую сумму:

# admin.py

class SaleSummaryAdmin(ModelAdmin):
    # ...
    def changelist_view(self, request, extra_context=None):
        # ...
        response.context_data['summary_total'] = dict(
            qs.aggregate(**metrics)
        )
        return response


Это довольно крутой трюк …

Добавим нижнюю строку к таблице:

<!-- sale_summary_change_list.html -->

<div class="results">
    <table>

        <!-- ... -->

        <tr style="font-weight:bold; border-top:2px solid #DDDDDD;">
            <td> Total </td>
            <td> {{ summary_total.total | intcomma }} </td>
            <td> {{ summary_total.total_sales | default:0 }}$ </td>
            <td> 100% </td>
        </tr>

    </table>
</div>


Сводная таблица начинает обретать форму:

Django admin dashboard with a summary table

Добавление фильтров

Мы используем «обычную» модель администратора, поэтому фильтры уже встроены. Давайте добавим фильтр по устройству:

# admin.py

class SaleSummaryAdmin(ModelAdmin):

    # ...

    list_filter = (
        'device',
    )


И результат:

Django admin dashboard with a filter

Добавление диаграммы

Панель инструментов не является полной без графика, поэтому мы собираемся добавить гистограмму, чтобы показать продажи с течением времени.

Для построения нашей диаграммы мы будем использовать обычный HTML и несколько старых добрых CSS с flexbox. Данные для диаграммы будут представлять собой временной ряд процентов, которые будут использоваться в качестве высоты столбцов.

Возвращаясь к нашему changelist_view, мы добавляем следующее:

# admin.py

from django.db.models.functions import Trunc
from django.db.models import DateTimeField

class SalesSummaryAdmin(ModelAdmin):

    # ...

    def changelist_view(self, request, extra_context=None):

        # ...

        summary_over_time = qs.annotate(
            period=Trunc(
                'created',
                'day',
                output_field=DateTimeField(),
            ),
        ).values('period')
        .annotate(total=Sum('price'))
        .order_by('period')

        summary_range = summary_over_time.aggregate(
            low=Min('total'),
            high=Max('total'),
        )
        high = summary_range.get('high', 0)
        low = summary_range.get('low', 0)

        response.context_data['summary_over_time'] = [{
            'period': x['period'],
            'total': x['total'] or 0,
            'pct': \
               ((x['total'] or 0) - low) / (high - low) * 100
               if high > low else 0,
        } for x in summary_over_time]

    return response


Давайте добавим гистограмму в шаблон и немного ее стилизуем:

<!-- sale_summary_change_list.html -->

<div class="results">

    <!-- ... -->

    <h2> Sales over time </h2>

    <style>
    .bar-chart {
      display: flex;
      justify-content: space-around;
      height: 160px;
      padding-top: 60px;
      overflow: hidden;
    }
    .bar-chart .bar {
        flex: 100%;
        align-self: flex-end;
        margin-right: 2px;
        position: relative;
        background-color: #79aec8;
    }
    .bar-chart .bar:last-child {
        margin: 0;
    }
    .bar-chart .bar:hover {
        background-color: #417690;
    }

    .bar-chart .bar .bar-tooltip {
        position: relative;
        z-index: 999;
    }
    .bar-chart .bar .bar-tooltip {
        position: absolute;
        top: -60px;
        left: 50%;
        transform: translateX(-50%);
        text-align: center;
        font-weight: bold;
        opacity: 0;
    }
    .bar-chart .bar:hover .bar-tooltip {
        opacity: 1;
    }
    </style>

    <div class="results">
        <div class="bar-chart">
        {% for x in summary_over_time %}
            <div class="bar" style="height:{{x.pct}}%">
                <div class="bar-tooltip">
                    {{x.total | default:0 | intcomma }}<br>
                    {{x.period | date:"d/m/Y"}}
                </div>
            </div>
        {% endfor %}
        </div>
    </div>

</div>


Для тех из вас, кто не знаком с flexbox, этот фрагмент CSS означает «рисовать снизу вверх, сместить влево и регулировать ширину по размеру».

Вот как это выглядит сейчас:

Django admin dashboard with a basic chart

Это выглядит довольно хорошо, но … Каждый столбец на графике представляет день. Что произойдет, когда мы попытаемся показать данные за один день? Или несколько лет?

Daily chart for several years

Такая диаграмма нечитаема и опасна. Извлечение такого количества данных может привести к зависанию сервера и/или созданию огромного HTML-файла.

Django Admin имеет иерархию дат — давайте посмотрим, сможем ли мы использовать это для корректировки периода столбцов на основе выбранной иерархии дат:

def get_next_in_date_hierarchy(request, date_hierarchy):
    if date_hierarchy + '__day' in request.GET:
        return 'hour'

    if date_hierarchy + '__month' in request.GET:
        return 'day'

    if date_hierarchy + '__year' in request.GET:
        return 'week'

    return 'month'

  • Если пользователь отфильтровал один день, каждый столбец будет один час (максимум 24 столбца).
  • Если пользователь выбрал месяц, каждый столбец будет один день (максимум 31 столбец).
  • Если пользователь выбрал год, каждый столбец будет иметь одну неделю (максимум 52 столбца).
  • Более того, и каждый столбец будет один месяц.

Теперь нам нужна всего одна небольшая корректировка для представления списка изменений:

# admin.py

class SalesSummaryAdmin(ModelAdmin):

    # ...

    def changelist_view(self, request, extra_context=None):

        # ...

        period = get_next_in_date_hierarchy(request, self.date_hierarchy)
        response.context_data['period'] = period

        summary_over_time = qs.annotate(
            period=Trunc('created', period, output_field=DateTimeField()),
        ).values('period')
        .annotate(total=Sum('price'))
        .order_by('period')

        # ...


Аргумент period, переданный Trunc, теперь является параметром. В результате мы получаем вот это:

Django admin dashboard chart with adjusted period
Была ли вам полезна эта статья?
[25 / 4.3]

Spread the love
Editorial Team

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago