介绍
Django 提供功能管理 UI 开箱即用的 CRUD 界面用于 db 管理。这包括基本内容和用户管理系统的大多数用例。但是,它没有显示摘要或历史趋势的探索性视图,这是您期望从管理仪表板获得的内容。
幸运的是,django管理应用程序是可扩展的,通过一些调整,我们可以添加交互式Javascript图表到管理员。
问题
我想在findwork.dev上获取电子邮件订阅者随时间的图表概览。就电子邮件订阅者而言,网站是增长还是停滞?上个月我们有多少订户?我们获得大多数订阅者的星期是哪一周?是否所有订阅者都在验证其电子邮件?
使用探索性图表,我们可以获得网站性能的历史概述。
我最初探索了现成的Django管理应用程序和仪表板的土地。要求是,它包括图表能力,有详细的记录,看起来不错。虽然我尝试的所有应用程序看起来都比默认管理员在样式方面更好,但它们要么缺少文档,要么没有维护。
- xadmin - 没有英文文档
- django-jet - 未经维护,因为核心团队正在研究SaaS 替代方案
- django-grapinelli - 无制图能力
这时,一个想法突然浮现在脑海:为什么不扩展默认管理应用呢?
扩展 django 管理员
django管理应用程序是由模型管理类组成。这些表示在管理界面中的模型的可视视图。默认情况下,ModelAdmin 类附带 5 个默认视图:
- 更改列表 - 模型集合的列表视图
- 添加 - 允许您添加新模型实例的视图
- 更改 - 用于更新模型实例的视图
- 删除 - 用于确认删除模型实例的视图
- 历史记录 - 对模型实例执行的操作的历史记录
当您要查看特定模型时,"更改列表"视图是默认管理员视图。我想在这里添加一个图表,以便每当我打开电子邮件订阅者页面时,都会显示随着时间的推移添加的订阅者。
假设我们有一个电子邮件订阅者模型,如下所示:
1 2 3 4 5 6 7 |
# web/models.py from django.db import models class EmailSubscriber(models.Model): email = models.EmailField() created_at = models.DateTimeField() |
为了在管理应用中呈现电子邮件订阅者,我们需要创建一个从 扩展的类。django.contrib.admin.ModelAdmin
基本模型管理员如下所示:
1 2 3 4 5 6 7 8 9 |
# 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 |
让我们添加一些订阅者,以便我们有一个初始数据集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ ./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)> ... |
如果我们输入 ChangeList 视图,我们将看到我们添加了 100 个新订阅者,随机创建时间http://localhost:8000/admin/web/emailsubscriber/。

假设我们要添加一个图表,该图表汇总了一段时间内条形图中的订阅者数量。我们希望将其放在订阅者列表的上方,这样,您一进入网站即可见。
下面的红色区域勾勒出我想直观地放置图表的位置。

如果我们创建一个新文件,我们可以强制 django 管理员加载我们的模板,而不是默认模板。让我们在
web/templates/admin/web/emailsubscriber/change_list.html
.
重写管理模板时的命名方案是
{{app}}/templates/admin/{{app}}/{{model}}/change_list.html
.
默认的 ChangeList 视图是可扩展的,并且有多个块可以覆盖以满足您的需要。检查默认管理模板时,我们可以看到它包含可以重写的块。我们需要重写内容块,以更改模型表之前呈现的内容。
让我们扩展默认的"更改列表"视图并添加自定义文本:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 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 图表。我们需要重写外头块来添加脚本和样式元素来在标头中加载 Chart.js。
Chart.js代码基于此处找到的演示条形图。我稍微修改了它,以读取 X 轴上的时间序列数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# 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 %} |

Voil®,我们现在已经呈现了一个图表.js图表到django管理员。唯一的问题是数据是硬编码的,而不是从我们的后端派生的。
将图表数据注入管理模板
ModelAdmin 类具有一个称为更改列表_视图的方法。此方法负责呈现 ChangeList 页。通过重写此方法,我们可以将图表数据注入到模板上下文中。
下面的代码大致执行以下操作:
- 以每日间隔聚合新订户的总数
- 将 Django 查询集编码为 JSON
- 将数据添加到模板上下文
- 调用 super() 方法来呈现页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# 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) |
数据现在应在技术上添加到模板上下文中,但现在我们必须在图表中使用它,而不是硬编码数据。
将图表数据变量中的硬编码数据替换为后端的数据:
1 2 3 |
// django_admin_chart_js/web/templates/admin/web/emailsubscriber/change_list.html const chartData = {{ chart_data | safe }}; |
重新加载页面以查看我们美丽的图表。

使用 JS 动态加载数据
在上面的示例中,我们将初始图表数据直接注入 html 模板。在初始页面加载后,我们可以进行更多的交互式和提取数据。为此,我们需要:
- 向模型管理员添加新终结点,该终结点返回 JSON 数据
- 添加 JS 逻辑,在按钮单击时进行 AJAX 调用并重新呈现图表
添加新终结点需要我们将get_urls()方法覆盖到模型管理员之上,并注入我们自己的终结点 URL。
请务必注意,您的自定义 URL 应先于默认 URL。默认的允许性,将匹配任何内容,因此请求永远不会通过我们的自定义方法。
我们的 python 代码现在应该如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# 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 逻辑,以在按钮单击时重新加载图表数据并重新呈现图表。在图表变量的声明下方添加以下行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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 按钮:
1 2 3 4 5 6 7 8 9 10 11 |
{% 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 附带不同的开箱式可视化效果。它很容易得到的基本图表,并提供定制,以万一你需要它。他们的文件在这里。Django 管理文档在这里
完整的示例代码可以在Github上找到。
你喜欢这个帖子吗?
当我们编写新内容时,接收更新。

