В статье описано как добавить сводную таблицу и диаграммы в Django Admin
Оригинальная статья: Haki Benita — How to Turn Django Admin Into a Lightweight Dashboard
Django Admin — это мощный инструмент для управления данными в вашем приложении. Тем не менее, он не был разработан для добавления сводных таблиц и диаграмм. К счастью, разработчики Django Admin упростили нам подобную настройку.
Вот что мы собираемся сделать к концу статьи:
Существует множество инструментов, приложений и пакетов, которые могут создавать очень красивые панели. Я лично обнаружил, что если продукт не является реальной панелью мониторинга, в большинстве случаев все, что вам нужно, это простая сводная таблица и несколько диаграмм.
Второе, и не менее важное причина — отсутствие дополнительных зависимостей.
Если все, что вам нужно, это немного улучшить интерфейс администратора, то этот подход определенно стоит рассмотреть.
Мы собираемся использовать готовую модель 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 %}
Вот как стала выглядит наша страница на данный момент:
Контекст, отправляемый в шаблон, заполняется 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
Давайте рассмотрим что мы сделали:
Теперь, когда у нас есть данные в контексте, мы можем отобразить их в шаблоне:
<!-- 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 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>
Сводная таблица начинает обретать форму:
Мы используем «обычную» модель администратора, поэтому фильтры уже встроены. Давайте добавим фильтр по устройству:
# admin.py class SaleSummaryAdmin(ModelAdmin): # ... list_filter = ( 'device', )
И результат:
Панель инструментов не является полной без графика, поэтому мы собираемся добавить гистограмму, чтобы показать продажи с течением времени.
Для построения нашей диаграммы мы будем использовать обычный 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 означает «рисовать снизу вверх, сместить влево и регулировать ширину по размеру».
Вот как это выглядит сейчас:
Это выглядит довольно хорошо, но … Каждый столбец на графике представляет день. Что произойдет, когда мы попытаемся показать данные за один день? Или несколько лет?
Такая диаграмма нечитаема и опасна. Извлечение такого количества данных может привести к зависанию сервера и/или созданию огромного 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'
Теперь нам нужна всего одна небольшая корректировка для представления списка изменений:
# 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, теперь является параметром. В результате мы получаем вот это:
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…