添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

DjangoAdmin

DjangoAdmin本身就是一套大而全的系统,官方文档中介绍了很多配置方法,但仍然有大量的骚操作是文档中没有的,所以遇到特殊需求的时候,求助文档不一定有用。

在我看来 DjangoAdmin 虽然能快速生成一套管理后台,但如果要做大量特殊需求的定制,其成本不亚于用 Vue/React 重新开发一套,简单的使用成本不高,但深入定制的话需要对 DjangoAdmin 的工作流程比较熟悉,把源码啃熟了(有些 Python 源码没有类型注解是很难读懂的),才能在原有基础上雕花,有时候还存在着后续维护的问题。

不过还是瑕不掩瑜了,谁能拒绝配置了几行代码就可以用的管理后台呢?

而且还不需要你做出一套 RESTFul API 来实现前后端分离,直接一把梭,起飞~

一些参考资料

  • Django Admin 后台自定制技巧:https://www.cnblogs.com/LyShark/p/12119539.html
  • DjangoAdmin-字段自动补全(django_admin_autocomplete_all 库)使用与坑:https://django-admin-autocomplete-all.readthedocs.io/en/latest/readme.html
  • DjangoAdmin添加自定义Widget:http://garmoncheg.blogspot.com/2014/07/django-adding-custom-widget-to-django.html
  • How to Turn Django Admin Into a Lightweight Dashboard:https://hakibenita.com/how-to-turn-django-admin-into-a-lightweight-dashboard
  • GitHub 上有很多 DjangoAdmin 的替换主题,所以不要抱怨 DjangoAdmin 的界面丑啦,好看的主题很多!

    我最先使用的是 adminx,但这个侵入性太强了,需要对 admin 的配置代码做大量修改,实在是不划算,可能官方也意识到这个问题,后续应该是停更了。

    到了 Django 2.x 时代后面,admin 的界面主题多了起来,有个国产的 SimpleUI 很不错,基于 Vue + ElementUI 实现的,star很多,算是比较成熟的一类,我愿称之为国产之光。

    其他的我也大多有测试,但用起来总有一些兼容的问题,所以目前还是比较推荐国产之光。

    SimpleUI

    已经在多个产品中使用,使用 vue+elementUI(非单页应用),支持多标签页

    一些相关的参考资料

  • Django Admin SimpleUI 自定义列:https://simpleui.72wo.com/topic/1266
  • django-jazzmin

    地址:https://github.com/farridav/django-jazzmin

    这个是偶然发现的,对于看腻了 ElementUI 的人来说,会有眼前一亮的感觉

    使用 Bootstrap+AdminLTE 重写,效果还不错

    (就是偶尔会莫名卡死

    而且细节方面做得不如 SimpleUI,比如搜索框没有 placeholder 的提示之类的。

    本文只记录特殊需求的实现,对于 DjangoAdmin 的常规配置就不复制粘贴了 ,网上随便一搜都有很多。

    我之前已经写过不少 DjangoAdmin 的定制案例文章了,最近也做了不少定制,不过我不想写新文章来单独记录某个需求的实现过程了,直接在本文里记录,同时保持本文更新~

  • 给Django Admin添加验证码和多次登录尝试限制
  • Django中间件之实现Admin后台IP白名单
  • 给Django的Admin添加自定义Action 并移除需要选择对象的限制
  • 告别单调,Django后台主页改造 - 使用AdminLTE组件
  • 添加自定义列

    本案例基于 SimpleUI

    根据不同的发票类型,显示不同颜色的 Tag 组件

    这里使用的是 ElementUI 的 Tag 组件,文档:https://element.eleme.cn/#/zh-CN/component/tag

    前面提到过 SimpleUI 不是单页应用,是直接在网页上使用 vue 和 elementUI,并没有webpack环境

    所以要加入 elementUI 的组件不能直接简单的

    <el-tag type="success">标签</el-tag>
    

    而是要用 webpack 生成出来的

    <div class="el-tag el-tag--success el-tag--light">标签</div>
    

    ok,开始上Python代码

    假设有个 model 叫 Invoice,中文名发票,定义如下

    class Invoice(models.Model):
      invoice_type = models.CharField('发票类型')
    

    在需要自定义的 ModelAdmin 中,增加一个方法

    # 发票类型颜色
    @admin.display(description='发票类别')
    def invoice_type_tag(self, obj: Invoice):
      def el_tag(color_type, content):
        生成 ElementUI 的 tag 组件
    		:param color_type: success, info, warning, danger
    		:param content:
        :return:
        from django.utils.safestring import mark_safe
        type_class = '' if len(color_type) == 0 else f'el-tag--{color_type}'
        return mark_safe(f'<div class="el-tag el-tag--small {type_class} el-tag--light">{content}</div>')
      if obj.invoice_type.startswith('普通'):
        return el_tag('', obj.invoice_type)
      if obj.invoice_type.startswith('专用'):
        return el_tag('danger', obj.invoice_type)
      if obj.invoice_type.startswith('电子专票'):
        return el_tag('info', obj.invoice_type)
      if obj.invoice_type.startswith('电子普票'):
        return el_tag('warning', obj.invoice_type)
    

    然后把这个 invoice_type_tag 加到 list_display 中即可

    PS:这里的 @admin.display() 装饰器是Django3.2版本之后新增的,很方便,相当于以前的

    invoice_type_tag.short_description = '发票类别'
    

    PS:注意HTML代码需要用 mark_safe 方法包装起来,才能正常渲染,不然会被转义!

    扩展:添加链接

    如果要做成跳转链接,也很容易。

    这里用到 ElementUI 的 Link 组件。

    @admin.display(description='打开链接')
    def open_thumb(self, obj: Photo):
      url = reverse('invoice:detail', kwargs={'pk': obj.pk})
      return mark_safe(f'''
      <a class="el-link el-link--primary is-underline" href="{url}" target="_blank">
      	<span class="el-link--inner">打开链接</span>
    

    显示进度条

    原理同上面的添加自定义列

    # 进度条
    @admin.display(description='进度条')
    def progress_bar(self, obj):
        html = f'''
            <div role="progressbar" aria-valuenow="{obj.progress}" aria-valuemin="0" aria-valuemax="100"
                class="el-progress el-progress--line is-light el-progress--text-inside">
                <div class="el-progress-bar">
                    <div class="el-progress-bar__outer" style="height: 22px;">
                        <div class="el-progress-bar__inner" style="width: {obj.progress}%;">
                            <div class="el-progress-bar__innerText">{obj.progress}%</div>
        from django.utils.safestring import mark_safe
        return mark_safe(html)
    

    页面上显示合计数额

    这个功能比较麻烦,因为需要魔改 template

    首先我们要知道,这个列表对应的是哪个 template,在 admin 包的 templates 目录下面的找了半天,最终发现这个页面是 change_list ,而且因为页面比较复杂,被分成了好几部分

    我们只需要修改 change_list.html 这个文件就行了。

    admin.py

    OK,模板代码先不管,我们来写Python代码计算总金额。

    要实现将数据放在 context 里传给 template,得重写个 ChangeList 对象

    from django.db.models import Sum
    from django.contrib.admin.views.main import ChangeList
    class InvoiceChangeList(ChangeList):
        def get_results(self, request):
            super(InvoiceChangeList, self).get_results(request)
            totals = self.result_list.aggregate(Sum('amount'))
            self.total_amount = totals['amount__sum']
    

    使用 Sum 这个聚合方法,计算总金额。

    通过Python语言的动态特性,加 total_amount 这个属性添加到 ChangeList 对象中

    这样在 template 里就能通过 {{ cl.total_amount }} 的方式拿到这个属性。

    然后改一下 ModelAdmin :

    class InvoiceAdmin(ImportExportModelAdmin):
      # 如果你改了 template 的名称,这里可以对应修改,否则默认即可
      change_list_template = 'change_list.html'
      # 添加这个代码
      def get_changelist(self, request, **kwargs):
        return InvoiceChangeList
    

    后端部分搞定了,接下来是前端的模板部分。

    template

    为了在页面上添加新元素,我们来修改 change_list.html 文件。

    **注意,不要直接复制这个文件来修改!**原因是你修改完的 template 会覆盖其他组件,这样以后换 admin 主题,或者使用 import-export 这类会修改 admin 页面的插件时无法生效,也就是所谓的兼容问题。

    Django也想到了这种情况,这些 template 都是组件化的,我们写一个扩展 template 就可以了。

    在项目的 templates/admin 目录下新建 change_list.html 文件,代码如下

    {% extends "admin/change_list.html" %}
    {% block result_list %}
        {{ block.super }}
        <div style="text-align: right; margin: 20px 5px; font-size: 20px;">
            总金额:{{ cl.total_amount }} 元
    {% endblock %}
    

    注意:如果用了 django-import-export 插件,则要根据使用到的功能来添加 object-tools-items block。

    比如你的 ModelAdmin 继承自 ImportExportModelAdmin,那我们转到源码,可以看到它重写了 template

    class ImportExportMixin(ImportMixin, ExportMixin):
        Import and export mixin.
        #: template for change_list view
        change_list_template = 'admin/import_export/change_list_import_export.html'
    class ImportExportModelAdmin(ImportExportMixin, admin.ModelAdmin):
        Subclass of ModelAdmin with import/export functionality.
    

    然后再看看 admin/import_export/change_list_import_export.html 这个文件

    {% extends "admin/import_export/change_list.html" %}
    {% block object-tools-items %}
      {% include "admin/import_export/change_list_import_item.html" %}
      {% include "admin/import_export/change_list_export_item.html" %}
      {{ block.super }}
    {% endblock %}
    

    可以看到它在 object-tools-items 中添加了俩组件,把这一块 block 的代码复制到我们的 change_list.html 中即可。

  • https://stackoverflow.com/questions/34924886/django-admin-changelist-view
  • 分权限的软删除

    这个需求是:普通用户删除执行软删除,管理员可以直接删除

    在模型里加一个是否删除的字段来表示软删除状态

    models.py
    class SoftDelete(models.Model):
        name = models.CharField('名称', max_length=20)
        is_deleted = models.BooleanField('是否软删除', default=False)
        def __str__(self):
            return self.name
        class Meta:
            verbose_name = '软删除'
            verbose_name_plural = verbose_name
    
    admin.py
    @admin.register(SoftDelete)
    class SoftDeleteAdmin(admin.ModelAdmin):
        list_display = ['name']
        fields = ['name']
        def get_queryset(self, request: HttpRequest):
            # 管理员可以看到全部记录
            if request.user.is_superuser:
                queryset = super().get_queryset(request)
            # 普通用户不能看到被软删除的记录
            else:
                queryset = super().get_queryset(request).filter(is_deleted=False)
            if not self.has_view_or_change_permission(request):
                queryset = queryset.none()
            return queryset
        def get_list_display(self, request: HttpRequest):
            # 管理员才能查看是否软删除的状态
            if request.user.is_superuser:
                insert_if_not_exists(self.list_display, len(self.list_display), 'is_deleted')
            else:
                # 删掉不给普通用户看的字段
                delete_if_exists(self.list_display, 'is_deleted')
            return self.list_display
        def get_fields(self, request, obj=None):
            # 管理员才能查看是否软删除的状态
            if request.user.is_superuser:
                insert_if_not_exists(self.fields, len(self.fields), 'is_deleted')
            else:
                # 删掉不给普通用户看的字段
                delete_if_exists(self.fields, 'is_deleted')
            return self.fields
        # 在列表删除
        def delete_queryset(self, request: HttpRequest, queryset):
            # 管理员才能真正的删除
            if request.user.is_superuser:
                return queryset.delete()
            # 普通用户使用软删除
            else:
                return queryset.update(is_deleted=True, deleted_user=request.user)
        # 在详情页面删除
        def delete_model(self, request, obj: SoftDelete):
            print('delete_model', obj)
            # 管理员才能真正的删除
            if request.user.is_superuser:
                obj.delete()
            # 普通用户使用软删除
            else:
                obj.is_deleted = True
                obj.save()
    

    这里记得处理删除操作要把 delete_querysetdelete_model 这俩方法都重写。

  • delete_queryset :列表页的删除操作
  • delete_model :详情页的删除操作
  • 这部分记录我在逛GitHub时发现的比较有意思的扩展库,记录一下

    Django AdminPlus

    地址:https://github.com/jsocol/django-adminplus

    可以方便的给admin增加新页面

    django-adminactions

    地址:https://github.com/saxix/django-adminactions

    可以给admin添加一系列的actions

  • Export as CSV
  • Export as Excel
  • Export as fixture
  • Export delete tree
  • Mass update records
  • Graph queryset
  • Merge records
  • Find Duplicates
  • If articles including programming codes (e.g. Java, Python, C#, Go) are exceptions, which are released under 所有內容皆以 知识共享署名-相同方式共享 4.0 国际许可协议 进行发布。
    含有程序代码內容的文章 (如 Java, Python, C#, Go) 除外,包含的程序代码皆以 GPL v3 发布。 本站使用 StarBlog 开源博客系统, 基于 .NetCore 技术构建