发布于 2026-01-06 3 阅读
0

在 Django 管理后台添加图表

在 Django 管理后台添加图表

介绍

Django 提供了一个开箱即用的功能齐全的后台管理界面,包含用于数据库管理的 CRUD 操作。这足以满足大多数基本内容和用户管理系统的使用场景。然而,它缺少用于显示摘要或历史趋势的探索性视图,而这正是后台管理面板通常应具备的功能。

幸运的是,django 管理应用程序是可扩展的,只需稍作调整,我们就可以向管理界面添加交互式 Javascript 图表。

问题

我想获得findwork.dev网站邮件订阅用户随时间变化的图表概览。该网站的邮件订阅用户数量是在增长还是停滞不前?上个月我们有多少订阅用户?哪一周新增订阅用户最多?所有订阅用户都验证了他们的邮箱地址吗?

通过探索性图表,我们可以获得网站运行情况的历史概览。

我最初探索的是现成的 Django 管理后台应用和仪表盘。我的要求是:具备图表功能、文档齐全且界面美观。虽然我尝试过的所有应用在样式上都比默认的管理后台要好,但它们要么缺乏文档,要么已经无人维护。

这时,一个想法突然涌上心头:为什么不扩展默认的管理应用程序呢?

扩展 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()
Enter fullscreen mode Exit fullscreen mode

为了在管理应用程序中显示电子邮件订阅者,我们需要创建一个继承自 的类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
Enter fullscreen mode Exit fullscreen mode

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

$ ./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)>
...
Enter fullscreen mode Exit fullscreen mode

如果我们进入变更列表视图,我们会看到我们添加了 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 %}
Enter fullscreen mode Exit fullscreen mode

太好了,我们现在已经成功自定义了管理后台界面。接下来,我们用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 %}
Enter fullscreen mode Exit fullscreen mode

瞧,我们现在已经将 Chart.js 图表渲染到 Django 管理后台了。唯一的问题是数据是硬编码的,而不是从我们的后端获取的。

将图表数据注入到管理模板中

ModelAdmin 类有一个名为changelist_view 的方法。该方法负责渲染变更列表页面。通过重写此方法,我们可以将图表数据注入到模板上下文中。

以下代码大致实现了这个功能:

  1. 按日统计新增订阅用户总数
  2. 将 Django QuerySet 编码为 JSON
  3. 将数据添加到模板上下文中
  4. 调用 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)
Enter fullscreen mode Exit fullscreen mode

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

将chartData变量中硬编码的数据替换为我们后端的数据:

// django_admin_chart_js/web/templates/admin/web/emailsubscriber/change_list.html
const chartData = {{ chart_data | safe }};
Enter fullscreen mode Exit fullscreen mode

重新加载页面即可查看我们精美的图表。

使用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")
        )
Enter fullscreen mode Exit fullscreen mode

我们还需要添加 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();
  });
Enter fullscreen mode Exit fullscreen mode

在图表下方添加一个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 %}
Enter fullscreen mode Exit fullscreen mode

Chart.js 内置了多种可视化图表,开箱即用。它易于上手,提供基本图表,并可根据需要进行自定义。

Chart.js 文档在这里,Django 管理文档在这里

完整的示例代码可以在Github上找到

文章来源:https://dev.to/danihodovic/integrating-chart-js-with-django-admin-1kjb