Django Admin 管理后台添加自定义信息及定制化页面


前言

Django自带的Admin管理后台主要提供了对数据库表增删改查的功能,能符合一般情况下的使用场景,不过既然作为管理后台,管理员总有一些统计的工作,希望能便于直观看到总览或者资源统计列表等信息,此时就需要在页面中添加一些自定义信息,甚至于利用重写模板,新增定制页面。


一、场景举例

假设目前需要在订单表管理页面OrderAdmin添加几条关于用户的信息,另外添加两个页面的入口,用于统计资源使用情况,实现后效果如图:
自定义信息及定制化页面入口

定制化资源列表统计页面

二、开发步骤

1.引入库

admin.py中引入代码如下(示例):

from django.contrib.admin.views.main import ChangeList

2.重写changelist_view方法

在OrderAdmin中重写changelist_view方法,代码如下(示例):

    def changelist_view(self, request, extra_context=None):
        cl = ChangeList(request,
                        self.model,
                        self.list_display,
                        self.list_display_links,
                        self.list_filter,
                        self.date_hierarchy,
                        self.search_fields,
                        self.list_select_related,
                        self.list_per_page,
                        self.list_max_show_all,
                        self.list_editable,
                        self,
                        self.sortable_by)
        # getting query set with same filters like current change list
        queryset = cl.get_queryset(request)

        uri = "%susers/statistics/" % (CAS_INTERNAL_SERVER_URL)

        appid = settings.CAS_APP_ID
        appkey = settings.CAS_APP_KEY
        randomn = random.randint(100000, 999999)
        timestamp = int(time.time())
        platform = 'http://cloud.nscc-tj.cn/api/v1/cas_login/'
        # platform = 'http%3A%2F%2Fpassport.nscc-tj.cn%2Flinyi_redirect%2F'

        str1 = "appkey=%s&random=%s&timestamp=%s" % (appkey, randomn, timestamp)
        sigHash = hashlib.sha256()
        sigHash.update(str1.encode())
        sig = sigHash.hexdigest()

        data = json.dumps({
            'appid': appid,
            'random': randomn,
            'timestamp': timestamp,
            'sig': sig,
            'platform': platform
            # 'sms_params': [sms_code]
        })
        # 调用CAS接口,获取用户数统计
        res = requests.post(uri, data)

        if res.status_code < 300:
            print('get online users\' number successful')
            half_hour_online_user_login_num = res.json().get('half_hour_online_user_login_num')
            history_online_user_login_num = res.json().get('history_online_user_login_num')
            three_month_online_user_login_num = res.json().get('three_month_online_user_login_num')
            three_month_online_user_num = res.json().get('three_month_online_user_num')
            three_month_online_total_user_num = res.json().get('three_month_online_total_user_num')
            print("three_month_online_total_user_num:", three_month_online_total_user_num)
        else:
            half_hour_online_user_login_num = random.randint(0, 99)
            history_online_user_login_num = random.randint(999, 999999)
            three_month_online_user_login_num = random.randint(999, 999999)
            three_month_online_user_num = random.randint(999, 999999)
        extra_context = extra_context or {}
        extra_context['half_hour_online_user_login_num'] = half_hour_online_user_login_num
        extra_context['history_online_user_login_num'] = history_online_user_login_num
        extra_context['three_month_online_user_login_num'] = three_month_online_user_login_num
        extra_context['three_month_online_user_num'] = three_month_online_user_num
        return super(OrderAdmin, self).changelist_view(request, extra_context)

该处使用的api网络请求的用户统计数据。

3.重写模板页面

在admin同级目录下,创建templates/admin/目录,在该目录下创建change_list.html,比如OrderAdmin所在的文件在与manage.py同级别目录frontend/admin/order.py中,那change_list.html就应该放在frontend/templates/admin/frontend/order/目录下。代码如下:

{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_list %}

{% block extrastyle %}
  {{ block.super }}
  <link rel="stylesheet" type="text/css" href="{%%20static%20"admin/css/changelists.css" %}">
  {% if cl.formset %}
    <link rel="stylesheet" type="text/css" href="{%%20static%20"admin/css/forms.css" %}">
  {% endif %}
  {% if cl.formset or action_form %}
    <script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
  {% endif %}
  {{ media.css }}
  {% if not actions_on_top and not actions_on_bottom %}
    <style>
      #changelist table thead th:first-child {width: inherit}
    </style>
  {% endif %}
{% endblock %}

{% block extrahead %}
{{ block.super }}
{{ media.js }}
{% endblock %}

{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}

{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
&rsaquo; {{ cl.opts.verbose_name_plural|capfirst }}
</div>
{% endblock %}
{% endif %}

{% block coltype %}flex{% endblock %}

{% block content %}
  <div id="content-main">
    {% block object-tools %}
        <ul class="object-tools">
          {% block object-tools-items %}
            {% change_list_object_tools %}
          {% endblock %}
        </ul>
    {% endblock %}
    {% if cl.formset and cl.formset.errors %}
        <p class="errornote">
        {% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
        </p>
        {{ cl.formset.non_form_errors }}
    {% endif %}
    <div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
      {% block search %}{% search_form cl %}{% endblock %}
      {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}

      {% block filters %}
        {% if cl.has_filters %}
          <div id="changelist-filter">
            <h2>{% trans 'Filter' %}</h2>
            {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
          </div>
        {% endif %}
      {% endblock %}

      <form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
      {% if cl.formset %}
        <div>{{ cl.formset.management_form }}</div>
      {% endif %}

      {% block result_list %}
              当前在线用户数:{{ half_hour_online_user_login_num }}人<br>
              历史访问量:{{ history_online_user_login_num }}人次<br>
              90天内访问量:{{ three_month_online_user_login_num }}人次<br>
              90天内登录用户数:{{ three_month_online_user_num }}人<br>
          <a href="/admin/resource/list_for_leader/">资源列表</a><br>
          <a href="/admin/resource/overview_for_leader/">资源总览</a><br>
          {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
          {% result_list cl %}
          {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
      {% endblock %}
      {% block pagination %}{% pagination cl %}{% endblock %}
      </form>
    </div>
  </div>
{% endblock %}

4.业务逻辑(views开发)

上面的模板页面已经渲染了自定义信息,也给出了定制化页面入口,以资源总览页为例,按照Django开发模型,需要指定models、在views中写业务逻辑、在urls中指定路由,其中views逻辑为:

# 管理后台用户资源总览页
def resource_overview(request):
    user_queryset = models.User.objects.all()
    users = []
    for user in user_queryset:
        resource_info = get_user_resource_info(user)
        buy_resource_num = resource_info['instance_info']['云主机总台数'] + resource_info['instance_info']['CPU总核数'] \
                           + resource_info['instance_info']['总内存(GiB)'] + resource_info['instance_info']['系统盘总容量(GiB)'] \
                           + resource_info['bare_metal_info']['物理机总台数'] + resource_info['bare_metal_info']['CPU总核数'] \
                           + resource_info['bare_metal_info']['总内存(GiB)'] + resource_info['bare_metal_info']['硬盘总容量(GiB)'] \
                           + resource_info['volume_info']['数据盘总数'] + resource_info['volume_info']['总容量(GiB)'] \
                           + resource_info['share_info']['总容量(GiB)'] + resource_info['object_storage_info']['总容量(GiB)'] \
                           + resource_info['public_ip_info']['IP总数'] + resource_info['public_ip_info']['联通共享总带宽'] \
                           + resource_info['public_ip_info']['联通独立总带宽'] + resource_info['public_ip_info']['电信共享总带宽'] \
                           + resource_info['public_ip_info']['电信独立总带宽'] + resource_info['public_ip_info']['共享总带宽'] \
                           + resource_info['load_balancer_info']['负载均衡器总数'] + resource_info['load_balancer_info']['侦听器数量'] \
                           + resource_info['load_balancer_info']['服务资源数量'] + resource_info['vpn_gateway_info']['VPN网关总数'] \
                           + resource_info['vpn_gateway_info']['总带宽(Mbps(共享))']
        if buy_resource_num == 0:
            continue
        users.append({
            'user_id': user.id,
            'username': user.username,
            'mobile': user.mobile,
            'is_contract_user': user.is_contract_user,
            'instance_num': resource_info['instance_info']['云主机总台数'],
            'instance_cpu': resource_info['instance_info']['CPU总核数'],
            'instance_ram': resource_info['instance_info']['总内存(GiB)'],
            'instance_rom': resource_info['instance_info']['系统盘总容量(GiB)'],
            'bare_metal_num': resource_info['bare_metal_info']['物理机总台数'],
            'bare_metal_cpu': resource_info['bare_metal_info']['CPU总核数'],
            'bare_metal_ram': resource_info['bare_metal_info']['总内存(GiB)'],
            'bare_metal_rom': resource_info['bare_metal_info']['硬盘总容量(GiB)'],
            'disk_num': resource_info['volume_info']['数据盘总数'],
            'disk_capacity': resource_info['volume_info']['总容量(GiB)'],
            'share_capacity': resource_info['share_info']['总容量(GiB)'],
            'object_storage_capacity': resource_info['object_storage_info']['总容量(GiB)'],
            'public_ip_num': resource_info['public_ip_info']['IP总数'],
            'unicom_total_bandwidth': resource_info['public_ip_info']['联通共享总带宽'] + resource_info['public_ip_info'][
                '联通独立总带宽'],
            'telecom_total_bandwidth': resource_info['public_ip_info']['电信共享总带宽'] + resource_info['public_ip_info'][
                '电信独立总带宽'],
            'share_total_bandwidth': resource_info['public_ip_info']['共享总带宽'],
            'load_balancer_num': resource_info['load_balancer_info']['负载均衡器总数'],
            'listener_num': resource_info['load_balancer_info']['侦听器数量'],
            'member_num': resource_info['load_balancer_info']['服务资源数量'],
            'vpn_num': resource_info['vpn_gateway_info']['VPN网关总数'],
            'vpn_total_bandwidth': resource_info['vpn_gateway_info']['总带宽(Mbps(共享))'],
            'manage_full_name': user.manage_full_name,
            'manage_company': user.manage_company,
            'manage_salesperson': user.manage_salesperson
        })
    records = [(x['user_id'], x['username'], x['mobile'], x['is_contract_user'], x['instance_num'], x['instance_cpu'], x['instance_ram'],
                x['instance_rom'], x['bare_metal_num'], x['bare_metal_cpu'], x['bare_metal_ram'], x['bare_metal_rom'],
                x['disk_num'], x['disk_capacity'], x['share_capacity'], x['object_storage_capacity'],
                x['public_ip_num'], x['unicom_total_bandwidth'], x['telecom_total_bandwidth'],
                x['share_total_bandwidth'], x['load_balancer_num'], x['listener_num'], x['member_num'], x['vpn_num'],
                x['vpn_total_bandwidth'],
                x['manage_full_name'], x['manage_company'], x['manage_salesperson']) for x in users]
    # records = [(dtype[x.username], dstatus[x.mobile], x.email, x.user_roles) for x in users]
    theads = ['用户ID', '用户名', '手机号', '是否合同用户', '云主机数量', '云主机CPU', '云主机总内存(GiB)', '云主机系统盘总容量(GiB)', '物理机数量', '物理机CPU',
              '物理机总内存(GiB)', '物理机硬盘总容量(GiB)', '数据盘数量', '数据盘总容量(GiB)', '文件存储总容量(GiB)', '对象存储总容量(GiB)', '公网IP数量', '联通总带宽',
              '电信总带宽', '无独立IP总带宽', '负载均衡器数量', '侦听器数量', '服务资源数量', 'VPN网关数量', 'VPN网关总带宽',
              '姓名', '企业', '销售']
    title = '用户资源总览'
    return render(request, 'frontend/templates/admin/user_resource_statistics/resource_overview/resource_overview.html',
                  {"theads": theads, "trows": records, "title": title})

5.指定models

由于没有实体数据库表,所以我们只需要指定一个空白的models

class ResourceOverviewModels(models.Model):
    # pass
    class Meta:
        app_label = 'frontend'
        verbose_name = '用户资源总览'
        verbose_name_plural = '用户资源总览'

6.指定urls

在urls中指定路由

from frontend.views.admin import user_resource_overview_views

urlpatterns = [
    path('overview/', user_resource_overview_views.resource_overview),
]

7.注册admin管理器

将空白models注册在admin管理器中

from frontend.models.admin.resource_overview import ResourceOverviewModels
from frontend.views.admin.user_resource_overview_views import resource_overview

class ResourceOverviewAdmin(admin.ModelAdmin):
    def changelist_view(self, request, extra_content=None):
        return resource_overview(request)
        
admin.site.register(ResourceOverviewModels, ResourceOverviewAdmin)

总结

以上就是对Django admin管理后台定制化的一个二次开发过程记录,代码不是完整的,看起来可能会有点乱,主要是记录一下需要注意的点,开发过程中作为参照,以便掌握框架内各组件的关系。


版权声明:本文为ZeroChia原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。