在 Django 管理后台添加图表
介绍
Django 提供了一个开箱即用的功能齐全的后台管理界面,包含用于数据库管理的 CRUD 操作。这足以满足大多数基本内容和用户管理系统的使用场景。然而,它缺少用于显示摘要或历史趋势的探索性视图,而这正是后台管理面板通常应具备的功能。
幸运的是,django 管理应用程序是可扩展的,只需稍作调整,我们就可以向管理界面添加交互式 Javascript 图表。
问题
我想获得findwork.dev网站邮件订阅用户随时间变化的图表概览。该网站的邮件订阅用户数量是在增长还是停滞不前?上个月我们有多少订阅用户?哪一周新增订阅用户最多?所有订阅用户都验证了他们的邮箱地址吗?
通过探索性图表,我们可以获得网站运行情况的历史概览。
我最初探索的是现成的 Django 管理后台应用和仪表盘。我的要求是:具备图表功能、文档齐全且界面美观。虽然我尝试过的所有应用在样式上都比默认的管理后台要好,但它们要么缺乏文档,要么已经无人维护。
- xadmin - 无英文文档
- django-jet——由于核心团队正在开发SaaS 替代方案,因此已停止维护。
- django-grapinelli - 不支持图表绘制功能
这时,一个想法突然涌上心头:为什么不扩展默认的管理应用程序呢?
扩展 django-admin
Django 管理后台应用由ModelAdmin 类组成。这些类代表了模型在管理界面中的可视化视图。默认情况下,ModelAdmin 类包含 5 个默认视图:
- ChangeList - 模型集合的列表视图
- 添加 - 一个允许您添加新模型实例的视图
- 更改 - 用于更新模型实例的视图
- 删除 - 用于确认删除模型实例的视图
- 历史记录 - 对模型实例执行的操作历史记录
当您想要查看特定模型时,“变更列表”视图是默认的管理员视图。我希望在这里添加一个图表,以便每次打开“电子邮件订阅者”页面时,都能看到随时间推移新增的订阅者。
假设我们有一个如下所示的电子邮件订阅者模型:
# web/models.py
from django.db import models
class EmailSubscriber(models.Model):
email = models.EmailField()
created_at = models.DateTimeField()
为了在管理应用程序中显示电子邮件订阅者,我们需要创建一个继承自 的类django.contrib.admin.ModelAdmin。
一个基本的模型管理界面大致如下所示:
# web/admin.py
from django.contrib import admin
from .models import EmailSubscriber
@admin.register(EmailSubscriber)
class EmailSubscriberAdmin(admin.ModelAdmin):
list_display = ("id", "email", "created_at") # display these table columns in the list view
ordering = ("-created_at",) # sort by most recent subscriber
让我们添加一些订阅者,以便我们拥有一个初始数据集:
$ ./manage.py shell
Python 3.7.3 (default, Apr 9 2019, 04:56:51)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
from web.models import EmailSubscriber
from django.utils import timezone
from datetime import timedelta
import random
for i in range(0, 100):
EmailSubscriber.objects.create(email=f"user_{i}@email.com", created_at=timezone.now() - timedelta(days=random.randint(0, 100)))
...
<EmailSubscriber: EmailSubscriber object (1)>
<EmailSubscriber: EmailSubscriber object (2)>
<EmailSubscriber: EmailSubscriber object (3)>
...
如果我们进入变更列表视图,我们会看到我们添加了 100 个新的订阅者,创建时间是随机的http://localhost:8000/admin/web/emailsubscriber/。
假设我们想添加一个图表,用柱状图的形式汇总一段时间内的订阅者数量。我们希望将其放在订阅者列表上方,以便用户一进入网站就能看到。
下图红色区域标出了我希望在视觉上放置图表的位置。
如果我们创建一个新文件,就可以强制 django-admin 加载我们创建的模板,而不是默认模板。让我们创建一个空文件。
web/templates/admin/web/emailsubscriber/change_list.html。
覆盖管理模板时的命名方案是
{{app}}/templates/admin/{{app}}/{{model}}/change_list.html。
默认的变更列表视图是可扩展的,包含多个可重写的模块,以满足您的需求。检查默认的管理模板,我们可以看到它包含可重写的模块。我们需要重写内容模块,以便更改模型表之前呈现的内容。
让我们扩展默认的变更列表视图并添加自定义文本:
# web/templates/admin/web/emailsubscriber/change_list.html
{% extends "admin/change_list.html" %}
{% load static %}
{% block content %}
<h1>Custom message!</h1>
<!-- Render the rest of the ChangeList view by calling block.super -->
{{ block.super }}
{% endblock %}
太好了,我们现在已经成功自定义了管理后台界面。接下来,我们用Chart.js添加一个 JavaScript 图表。我们需要重写extrahead 代码块,添加脚本和样式元素,以便在头部加载 Chart.js。
Chart.js 代码基于他们提供的演示柱状图(链接在此)。我对其进行了一些修改,使其能够读取 X 轴上的时间序列数据。
# web/templates/admin/web/emailsubscriber/change_list.html
{% extends "admin/change_list.html" %}
{% load static %}
<!-- Override extrahead to add Chart.js -->
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const ctx = document.getElementById('myChart').getContext('2d');
// Sample data
const chartData = [
{"date": "2019-08-08T00:00:00Z", "y": 3},
{"date": "2019-08-07T00:00:00Z", "y": 10},
{"date": "2019-08-06T00:00:00Z", "y": 15},
{"date": "2019-08-05T00:00:00Z", "y": 4},
{"date": "2019-08-03T00:00:00Z", "y": 2},
{"date": "2019-08-04T00:00:00Z", "y": 11},
{"date": "2019-08-02T00:00:00Z", "y": 3},
{"date": "2019-08-01T00:00:00Z", "y": 2},
];
// Parse the dates to JS
chartData.forEach((d) => {
d.x = new Date(d.date);
});
// Render the chart
const chart = new Chart(ctx, {
type: 'bar',
data: {
datasets: [
{
label: 'new subscribers',
data: chartData,
backgroundColor: 'rgba(220,20,20,0.5)',
},
],
},
options: {
responsive: true,
scales: {
xAxes: [
{
type: 'time',
time: {
unit: 'day',
round: 'day',
displayFormats: {
day: 'MMM D',
},
},
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
});
});
</script>
{% endblock %}
{% block content %}
<!-- Render our chart -->
<div style="width: 80%;">
<canvas style="margin-bottom: 30px; width: 60%; height: 50%;" id="myChart"></canvas>
</div>
<!-- Render the rest of the ChangeList view -->
{{ block.super }}
{% endblock %}
瞧,我们现在已经将 Chart.js 图表渲染到 Django 管理后台了。唯一的问题是数据是硬编码的,而不是从我们的后端获取的。
将图表数据注入到管理模板中
ModelAdmin 类有一个名为changelist_view 的方法。该方法负责渲染变更列表页面。通过重写此方法,我们可以将图表数据注入到模板上下文中。
以下代码大致实现了这个功能:
- 按日统计新增订阅用户总数
- 将 Django QuerySet 编码为 JSON
- 将数据添加到模板上下文中
- 调用 super() 方法来渲染页面
# django_admin_chart_js/web/admin.py
import json
from django.contrib import admin
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Count
from django.db.models.functions import TruncDay
from .models import EmailSubscriber
@admin.register(EmailSubscriber)
class EmailSubscriberAdmin(admin.ModelAdmin):
list_display = ("id", "email", "created_at")
ordering = ("-created_at",)
def changelist_view(self, request, extra_context=None):
# Aggregate new subscribers per day
chart_data = (
EmailSubscriber.objects.annotate(date=TruncDay("created_at"))
.values("date")
.annotate(y=Count("id"))
.order_by("-date")
)
# Serialize and attach the chart data to the template context
as_json = json.dumps(list(chart_data), cls=DjangoJSONEncoder)
extra_context = extra_context or {"chart_data": as_json}
# Call the superclass changelist_view to render the page
return super().changelist_view(request, extra_context=extra_context)
从技术上讲,数据现在应该已经添加到模板上下文中,但我们现在必须在图表中使用它,而不是使用硬编码的数据。
将chartData变量中硬编码的数据替换为我们后端的数据:
// django_admin_chart_js/web/templates/admin/web/emailsubscriber/change_list.html
const chartData = {{ chart_data | safe }};
重新加载页面即可查看我们精美的图表。
使用JS动态加载数据
在上面的示例中,我们将初始图表数据直接注入到 HTML 模板中。我们可以实现更具交互性的效果,并在页面初始加载后获取数据。为此,我们需要:
- 在我们的模型管理界面中添加一个新的端点,该端点返回 JSON 数据
- 添加 JS 逻辑,以便在按钮点击时发起 AJAX 请求并重新渲染图表。
添加新端点需要我们重写modeladmin 的get_urls()方法,并注入我们自己的端点 URL。
需要注意的是,您的自定义 URL 应该放在默认 URL 之前。默认 URL 允许任何内容,因此请求永远不会到达我们的自定义方法。
我们的Python代码现在应该看起来像这样:
# web/admin.py
import json
from django.contrib import admin
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Count
from django.db.models.functions import TruncDay
from django.http import JsonResponse
from django.urls import path
from .models import EmailSubscriber
@admin.register(EmailSubscriber)
class EmailSubscriberAdmin(admin.ModelAdmin):
list_display = ("id", "email", "created_at")
ordering = ("-created_at",)
...
def get_urls(self):
urls = super().get_urls()
extra_urls = [
path("chart_data/", self.admin_site.admin_view(self.chart_data_endpoint))
]
# NOTE! Our custom urls have to go before the default urls, because they
# default ones match anything.
return extra_urls + urls
# JSON endpoint for generating chart data that is used for dynamic loading
# via JS.
def chart_data_endpoint(self, request):
chart_data = self.chart_data()
return JsonResponse(list(chart_data), safe=False)
def chart_data(self):
return (
EmailSubscriber.objects.annotate(date=TruncDay("created_at"))
.values("date")
.annotate(y=Count("id"))
.order_by("-date")
)
我们还需要添加 JavaScript 逻辑,以便在按钮点击时重新加载图表数据并重新渲染图表。请在图表变量声明下方添加以下代码:
// django_admin_chart_js/web/templates/admin/web/emailsubscriber/change_list.html
const chart = new Chart...
...
// Reload chart data from the backend on button click
const btn = document.querySelector('#reload');
btn.addEventListener('click', async() => {
const res = await fetch("/admin/web/emailsubscriber/chart_data/");
const json = await res.json();
json.forEach((d) => {
d.x = new Date(d.date);
});
chart.data.datasets[0].data = json;
chart.update();
});
在图表下方添加一个HTML按钮:
{% block content %}
<!-- Render our chart -->
<div style="width: 80%;">
<canvas style="margin-bottom: 30px; width: 60%; height: 50%;" id="myChart"></canvas>
</div>
<button id="reload" style="margin: 1rem 0">Reload chart data</button>
<!-- Render the rest of the ChangeList view -->
{{ block.super }}
{% endblock %}
Chart.js 内置了多种可视化图表,开箱即用。它易于上手,提供基本图表,并可根据需要进行自定义。
Chart.js 文档在这里,Django 管理文档在这里。
完整的示例代码可以在Github上找到。
文章来源:https://dev.to/danihodovic/integrating-chart-js-with-django-admin-1kjb





