向Django Admin添加图表

介绍

Django 提供功能管理 UI 开箱即用的 CRUD 界面用于 db 管理。这包括基本内容和用户管理系统的大多数用例。但是,它没有显示摘要或历史趋势的探索性视图,这是您期望从管理仪表板获得的内容。

幸运的是,django管理应用程序是可扩展的,通过一些调整,我们可以添加交互式Javascript图表到管理员。

问题

我想在findwork.dev上获取电子邮件订阅者随时间的图表概览。就电子邮件订阅者而言,网站是增长还是停滞?上个月我们有多少订户?我们获得大多数订阅者的星期是哪一周?是否所有订阅者都在验证其电子邮件?

使用探索性图表,我们可以获得网站性能的历史概述。

我最初探索了现成的Django管理应用程序和仪表板的土地。要求是,它包括图表能力,有详细的记录,看起来不错。虽然我尝试的所有应用程序看起来都比默认管理员在样式方面更好,但它们要么缺少文档,要么没有维护。

这时,一个想法突然浮现在脑海:为什么不扩展默认管理应用呢?

扩展 django 管理员

django管理应用程序是由模型管理类组成。这些表示在管理界面中的模型的可视视图。默认情况下,ModelAdmin 类附带 5 个默认视图:

  • 更改列表 - 模型集合的列表视图
  • 添加 - 允许您添加新模型实例的视图
  • 更改 - 用于更新模型实例的视图
  • 删除 - 用于确认删除模型实例的视图
  • 历史记录 - 对模型实例执行的操作的历史记录

当您要查看特定模型时,"更改列表"视图是默认管理员视图。我想在这里添加一个图表,以便每当我打开电子邮件订阅者页面时,都会显示随着时间的推移添加的订阅者。

假设我们有一个电子邮件订阅者模型,如下所示:

Python
# web/models.py from django.db import models class EmailSubscriber(models.Model): email = models.EmailField() created_at = models.DateTimeField()
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

基本模型管理员如下所示:

Python
# 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
# 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
 

让我们添加一些订阅者,以便我们有一个初始数据集:

Python
$ ./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)> ...
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 视图是可扩展的,并且有多个块可以覆盖以满足您的需要。检查默认管理模板时,我们可以看到它包含可以重写的块。我们需要重写内容块,以更改模型表之前呈现的内容。

让我们扩展默认的"更改列表"视图并添加自定义文本:

Python
# 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 %}
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 轴上的时间序列数据。

Python
# 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 %}
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 页。通过重写此方法,我们可以将图表数据注入到模板上下文中。

下面的代码大致执行以下操作:

  1. 以每日间隔聚合新订户的总数
  2. 将 Django 查询集编码为 JSON
  3. 将数据添加到模板上下文
  4. 调用 super() 方法来呈现页面
Python
# 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
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 )
 

数据现在应在技术上添加到模板上下文中,但现在我们必须在图表中使用它,而不是硬编码数据。

图表数据变量中的硬编码数据替换为后端的数据:

Python
// django_admin_chart_js/web/templates/admin/web/emailsubscriber/change_list.html const chartData = {{ chart_data | safe }};
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 代码现在应该如下所示:

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") )
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 逻辑,以在按钮单击时重新加载图表数据并重新呈现图表。在图表变量的声明下方添加以下行:

Python
// 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(); });
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 按钮:

Python
{% 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 %}
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上找到。

你喜欢这个帖子吗?

当我们编写新内容时,接收更新。




  • zeropython 微信公众号 5868037 QQ号 5868037@qq.com QQ邮箱
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值