自定义admin

Posted 财经知识狂魔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义admin相关的知识,希望对你有一定的参考价值。

  平时我们用的django自带admin,怎么评价呢?一个字简陋,而且也人性化,如下图,首先只显示数据对象,如果要查看详细还有点进去,其次不能对自己想要的数据进行刷选

  我们的期望是:数据如excel显示,可以搜索查询,也可以条件查询

  ok!是没问题的,这个我们可以实现自订制

 

场景分析

  登录到django的admin里,首先就是一个列表索引页,并且是分app显示的,点击表就进入到表里,可以查询看数据行,想看行数据,还有要点击一下

  如果上述描述让你领悟不到,可自行登录到django的admin看一下

  首先从下面两张图中,我们可以大胆提出,从列表索引页点击进入到各表的url组件成是:APP名+表名(小写)

  auth也是django里一个app

  另外在django的admin中,是自己注册哪些表就显示哪些表,并且还可继承admin一个类来定义显示条件,这些都是写在一个配置文件里

class CustomerAdmin(admin.ModelAdmin):
    list_display = (\'id\',\'qq\',\'source\',\'consultant\',\'content\',\'status\',\'date\')
    list_filter = (\'source\',\'consultant\',\'date\')
    search_fields = (\'qq\',\'name\')
    raw_id_fields = (\'consult_course\',)
    filter_horizontal = (\'tags\',)
    list_editable = (\'status\',)

admin.site.register(models.Customer,CustomerAdmin)

   上述代码中,还有一个注册动作,主要干了一件什么事呢?为了能前端区别显示,前端肯定依据一个什么东西,而这个注册动作则是把 显示条件(admin类) 和 显示表 (models表类) 这个对应关系 进行了一个存储,前端就依据这个存储做到区别对待的

  存哪并不重要,内存即可,但是要想清楚,存储时的数据结构,首先注册时,注册表和admin表是一对一进行存储的,所以在最底层肯定是以键值对的字段存储的,另外在列表索引页时,是区分了APP的,所以存储里还有APP名称,app和表是一对多的关系,所以以APP为键,对应关系字典为值,如{‘crm’:{models.Customer:CustomerAdmin}},当表对应url访问时,前端就按照这样存储结构找到对应models对象按照显示条件进行显示了

  大概分析后,我们就要开始进行代码实战了

 

代码实战

  我们也定义自己的注册配置文件吧,首先我们也定义一个父类,简单点就一个显示哪些内容和筛选哪些内容,admin自定义类要继承这个父类

class BaseAdmin(object):
    \'\'\'防止子类继承如果没写,执行过程依然能找到,只不过为空\'\'\'
    list_display = []
    list_filter = []

  另外注册的时候register(表类,admin自定义类)  我们定义好的 表类和admin自定义类  存储结构,是有app 名的,怎么获取了?肯定是从表类下手了,看看有什么属性可以获取到APP名称

  是不是很漂亮,在django中给表类还提供了_meta的属性获取APP名称,那到这里,app名作为数据存储的key值搞定,那接下就是 表类,admin自定义类  对应关系字典了,由于django自带admin中 表的url  其中表名是小写的,所以需要用到下列方法

   注册方法:

#注册函数
def register(model_class,admin_class=None):
    \'\'\'
    把  表类   和   自定义admin类   对应关系写入到enabled_admins字典中
    :param model_class: 表类  比如UserProfile
    :param admin_class: 自定义admin类
    :return: 
    \'\'\'
    app_name = model_class._meta.app_label
    table_name = model_class._meta.model_name
    if app_name not in enabled_admins:
        enabled_admins[app_name] = {}
    #我们会发现字典中存储 也只是 app名 和  表名 和  admin类对应关系,其实在这存储结构里 models类和admin类没有直接的关系
    #而表名是表类名的小写字符串,如果你觉得可以通过APP名和表名  反射 去操作 表类的一些属性,还是挺麻烦的
    #既然在这个函数里 传入了表类,何不在admin类上绑定一个属性 就是  表类,以便后面方面操作了?
    admin_class.model = model_class
    enabled_admins[app_name][table_name] = admin_class

 ·  注册完后,每次访问表格索引页时,把注册字典传到前端进行循环渲染就可以了

  另外,每个表格显示的名字,由表类下verbose_name或verbose_name_plural决定的

<div class="panel-body">
              {% for app_name,app_tables in table_list.items %}
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>
                                {{ app_name }}
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for table_name,admin in app_tables.items %}
                        <tr>
                            <!--在列表索引页 的 表名是在定义表类时的verbose_name 或 verbose_name_plural-->
                            <td>{{ admin.model._meta.verbose_name_plural }}</td>
                            <td>add</td>
                            <td>change</td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
              {% endfor %}
              </div>

   但是运行时,报了如下错,这是怎么一回事呢?

  上面报错的意思是在django模板语言中,不支持_,那这里怎么处理呢?只能使用使自定义simple_tag了,怎么定义,可见我另外一个博客http://www.cnblogs.com/xinsiwei18/p/5905646.html#autoid-11-2-0

  这里需要注意的是 修改成django的自定义模板语言后,需要重启django,另外在templatetags文件夹下一定要有__init__.py文件

#! /usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = "laoliu"

from django import template
from django.utils.safestring import mark_safe

register = template.Library()


@register.simple_tag
def render_table_name(admin_class):
    return admin_class.model._meta.verbose_name_plural

 前端:

              <div class="panel-body">
              {% for app_name,app_tables in table_list.items %}
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>
                                {{ app_name }}
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for table_name,admin in app_tables.items %}
                        <tr>
                            <!--在列表索引页 的 表名是在定义表类时的verbose_name 或 verbose_name_plural-->
                            <td>{% render_table_name admin %}</td>
                            <td>add</td>
                            <td>change</td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
              {% endfor %}
              </div>

   现在有如下效果:

  做到这里,我们列表索引页大概框架就搭起来了,那接下来就是表格显示页了

  上面已经分析了,表格页的url为app名 + 表类名的小写,那在列表索引页的表名加上a标签

<a href="{% url \'table_objs\' app_name table_name %}">{% render_table_name admin %}</a>

  路由系统:

urlpatterns = [
    url(r\'^$\', views.index,name=\'table_index\'),
    url(r\'^/(\\w+)/(\\w+)/$\', views.display_table_objs,name=\'table_objs\'),
]

  注意这里的table_objs是别名,如果想用别名显示对应的url,就要用url渲染函数了,由于这条路由是动态匹配的,必须传入两个参数,所以在前端就传入app名和表名两个参数

  实现跳转效果后,进入表格页,那么怎么呈现数据了?

  当然是先找到对应的表,再根据我们定义admin类的显示条件来显示了.

  那怎么找到对应的表和定义admin类呢?还记得那个上面定义好的  存储对应关系的字典吗?根据url可知app名和表名,那就用两个名去那个字典里取到admin类,而且admin类里绑定了表格对象,获取到后,把admin类返回前端就可以了

def display_table_objs(req,app_name,table_name):
    print(\'-->\',app_name,table_name)
    admin_class = kingadmin.enabled_admins[app_name][table_name]
    return render(req,\'king_admin/table_objs.html\',{\'admin_class\':admin_class})

  在这里,用admin类获取所有的表格数据,也要用到simple_tag

@register.simple_tag
def get_query_sets(admin_class):
    return admin_class.model.objects.all()

    前端:

    <div class="panel panel-info">
        <div class="panel-heading">
            <h3 class="panel-title">panel title</h3>
        </div>
        <div class="panel-body">
            <table class="table table-hover">
                <thead>
                    <tr>
                        {% for column in admin_class.list_display %}
                            <th>{{ column }}</th>
                        {% endfor %}
                    </tr>
                </thead>
                <tbody>
                    <!--在前端模板语言可以通过as获取到数据进行别名 -->
                    {% get_query_sets admin_class as query_sets%}
                    {% for obj in query_sets %}
                    <tr>
                        <td>{{ obj }}</td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>

   效果如下

  大概的效果已经出来了,接下来就把数据根据上面的字段一一对应显示了,那问题来了,你要想对应显示,必须循环admin的显示条件列表list_display,但这里的字段是字符串,在前端直接用这个取值是不行的,而且前端也不支持映射,既然前端解决不了的事,也只能交给后端来做了--simple_tag,思路是这样,我让前端把循环的每行数据 和 admin类 传入到渲染函数,依据admin类中的显示列表,构造显示标签,返回给前端

@register.simple_tag
def build_table_row(obj,admin_class):
    row_ele = \'\'
    for column in admin_class.list_display:

        column_data = getattr(obj,column)
        row_ele += "<td>%s</td>"%column_data

    return mark_safe(row_ele)

 前端

    <div class="panel panel-info">
        <div class="panel-heading">
            <h3 class="panel-title">panel title</h3>
        </div>
        <div class="panel-body">
            <table class="table table-hover">
                <thead>
                    <tr>
                        {% for column in admin_class.list_display %}
                            <th>{{ column }}</th>
                        {% endfor %}
                    </tr>
                </thead>
                <tbody>
                <!--在前端模板语言可以通过as获取到数据进行别名 -->
                {% get_query_sets admin_class as query_sets %}
                {% for obj in query_sets %}
                <tr>
                    {% build_table_row obj admin_class %}
                </tr>
                {% endfor %}
                </tbody>
            </table>
        </div>
    </div>

 

  效果如下

 

  好像更完美了,但是还是有两个显示问题一个source字段(显示数字,没按choices里内容显示),一个date字段(没有按照我们的阅读习惯显示)

  我们先看一下source这个问题,先看表是怎么定义的

class Customer(models.Model):
    \'\'\'客户信息表\'\'\'
    name = models.CharField(max_length=32, blank=True, null=True)  # blank对admin起作用,而null对数据库起作用,一般两个成对写上
    qq = models.CharField(max_length=64, unique=True)
    qq_name = models.CharField(max_length=64, blank=True, null=True)
    phone = models.CharField(max_length=64, blank=True, null=True)
    source_choices = ((0, \'转介绍\'),
                      (1, \'QQ群\'),
                      (2, \'官网\'),
                      (3, \'百度推广\'),
                      (4, \'51CTO\'),
                      (5, \'知乎\'),
                      (6, \'市场推广\')
                      )
    source = models.SmallIntegerField(choices=source_choices)  # 小数字字段省空间
    referral_from = models.CharField(verbose_name=\'转介绍人qq\', max_length=64, blank=True, null=True)

    consult_course = models.ForeignKey("Course", verbose_name=\'咨询课程\')
    content = models.TextField(verbose_name=\'咨询详情\')
    tags = models.ManyToManyField(\'Tag\', blank=True, null=True)
    consultant = models.ForeignKey(\'UserProfile\', verbose_name=\'销售顾问\')
    memo = models.TextField(blank=True, null=True, verbose_name=\'备注\')
    date = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return self.qq

    class Meta:
        verbose_name = "客户信息表"  #设置了这个,在admin中就可以显示中文,不过这个还会加上一个1个s
        verbose_name_plural = "客户信息表"  #这个就不会加s

   从上面类中我们可以看出,如果上面那条数据的source要显示的话,按道理应该显示QQ群(1--QQ群),但这里还是显示数字,这个怎么解决了?

  循环显示条件过程中,我们必须判断是哪些是choices字段,那依据什么判断呢?记住,这里还是只是根据字符串去找

  在上图我会发现,在字段类型里,有个属性.choices,如果返回不为空值,那么这个字段就是choices类型,而且在行数据对象里有个方法,可以直接获取  这行数据 choices字对应的值,那就好办了

  simple_tag修改如下

@register.simple_tag
def build_table_row(obj,admin_class):
    row_ele = \'\'
    for column in admin_class.list_display:
        field_obj = obj._meta.get_field(column)
        if field_obj.choices:   #choices type
            column_data = getattr(obj,\'get_%s_display\'%column)()
        else:
            column_data = getattr(obj,column)
        row_ele += "<td>%s</td>"%column_data

    return mark_safe(row_ele)

   到这里就完美的解决了,choices字段显示的值了

  那date怎么判断,是不是也判断日期这种类型?看下图

  我们会发现,通过反射获取到date数据是UTC时间,而且数据直接可以调用strftime方法,就可以按照我们想要的方式进行转化,另外数据类型的__name__属性可以帮助我们判定是不是date数据

@register.simple_tag
def build_table_row(obj,admin_class):
    row_ele = \'\'
    for column in admin_class.list_display:
        field_obj = obj._meta.get_field(column)
        if field_obj.choices:   #choices type
            column_data = getattr(obj,\'get_%s_display\'%column)()
        else:
            column_data = getattr(obj,column)

        if type(column_data).__name__ == \'datetime\':
            column_data = column_data.strftime(\'%Y-%m-%d %H-%M-%S\')

        row_ele += "<td>%s</td>" % column_data
    return mark_safe(row_ele)

   写到这里,效果有如下:

   上面搞定后,接下来就是过滤了,写过滤前,要先把分页给搞定了

  django是自带了分页的,我们可以去到django的官网去查看使用方法,也可以copy代码使用

>>> from django.core.paginator import Paginator
>>> objects = [\'john\', \'paul\', \'george\', \'ringo\']
>>> p = Paginator(objects, 2)

>>> p.count
4
>>> p.num_pages
2
>>> type(p.page_range)
<class \'range_iterator\'>
>>> p.page_range
range(1, 3)

>>> page1 = p.page(1)
>>> page1
<Page 1 of 2>
>>> page1.object_list
[\'john\', \'paul\']

>>> page2 = p.page(2)
>>> page2.object_list
[\'george\', \'ringo\']
>>> page2.has_next()
False
>>> page2.has_previous()
True
>>> page2.has_other_pages()
True
>>> page2.next_page_number()
Traceback (most recent call last):
...
EmptyPage: That page contains no results
>>> page2.previous_page_number()
1
>>> page2.start_index() # The 1-based index of the first item on this page
3
>>> page2.end_index() # The 1-based index of the last item on this page
4

>>> p.page(0)
Traceback (most recent call last):
...
EmptyPage: That page number is less than 1
>>> p.page(3)
Traceback (most recent call last):
...
EmptyPage: That page contains no results

   我们必须清楚,上面的前端query_sets是查询到的所有的数据,也就是说,它是把所有的都展示了,那现在我们要按照页码显示对应数据,怎么弄?这里涉及到两个地方的变动,一个是数据区域的数据,一个就是下面的页码,数据区域的数据在视图函数下,根据前端传过来页码,过滤数据,然后传给前端进行渲染了(在tags的函数返回所有数据,在这里就用不上了)

  视图函数如下:

def display_table_objs(req,app_name,table_name):
    print(\'-->\',app_name,table_name)
    #admin_class  根据这个类,获取显示条件  还操作.model下的数据
    admin_class = kingadmin.enabled_admins[app_name][table_name]

    object_list = admin_class.model.objects.all()
    paginator = Paginator(object_list, 1) # Show 25 contacts per page

    page = req.GET.get(\'page\')
    try:
        query_sets = paginator.page(page)
    except PageNotAnInteger:
        query_sets = paginator.page(1)
    except EmptyPage:
        query_sets = paginator.page(paginator.num_pages)

    return render(req,\'king_admin/table_objs.html\',{\'admin_class\':admin_class,
                                                    \'query_sets\': query_sets})

   而页码的话,通过django的page1.number可以知道当前页,那我们可不可以这样做,比如前后显示两页,循环所有的页码,用当前页减循环页,绝对值小于等于2,就simple_tag 返回页码标签对象,反之,返回一个空,上页和下页是固定死的,每个a标签挑转到对应的页码(?page=数字)

@register.simple_tag
def render_page_ele(loop_counter,query_sets):

    # query_sets.number  当前页
    if abs(query_sets.number - loop_counter) <= 1:
        ele_class = \'\'
        if query_sets.number == loop_counter:
            ele_class = \'active\'
        ele = \'\'\'<li class="%s"><a href="?page=%s">%s</a></li>\'\'\'%(ele_class,loop_counter,loop_counter)

        return  mark_safe(ele)
    return \'\'

 前端:

    <div class="panel panel-info">
        <div class="panel-heading">
            <h3 class="panel-title">panel title</h3>
        </div>
        <div class="panel-body">
            <table class="table table-hover">
                <thead>
                    <tr>
                        {% for column in admin_class.list_display %}
                            <th>{{ column }}</th>
                        {% endfor %}
                    </tr>
                </thead>
                <tbody>
                <!--在前端模板语言可以通过as获取到数据进行别名 -->
                <!--{#{% get_query_sets admin_class as query_sets %}#}-->
                {% for obj in query_sets %}
                <tr>
                    {% build_table_row obj admin_class %}
                </tr>
                {% endfor %}
                </tbody>
            </table>

            <!--分页-->

        <nav aria-label="...">
             <ul class="pagination">
                {% if query_sets.has_previous %}
                    <li><a href="?page={{ query_sets.previous_page_number }}">上页</a></li>
                {% endif %}
                 <!--循环每个页码loop_counter,然后交个simple_tag函数render_page_ele来决定显不显示-->
                 {% for loop_counter in query_sets.paginator.page_range %}
                    {% render_page_ele loop_counter query_sets %}
                 {% endfor %}

                {% if query_sets.has_next %}
                    <li><a href="?page={{ query_sets.next_page_number }}">下页</a></li>
                {% endif %}

             </ul>
        </nav>
        </div>
    </div>

 

  过滤:条件筛选就做成由几个下拉框组成 外加一个搜索筛选

  由于在admin那个类里,我们是定义显示哪些筛选条件的,一个list_filters的类变量,所以我们需要循环这个类变量,生成select标签,我们还是统一的把条件,admin类传给simple_tag函数,让后端帮我们生成标签

            <div class="row">
                {% for condtion in admin_class.list_filters %}
                    <div class="col-lg-2">
                        <span>{{ condtion }}</span>
                        {% render_filter_ele condtion admin_class %}
                    </div>
                {% endfor %}
            </div>

  在simple_tag里,每个condtion就是select标签的name值了,那option的值是哪些了?

  这个得要分开了,如果是condtion对应字段是外键,那我们要求option的value为外键ID,名就是外键的值,如果是choices类型的话,那就choices定义的元组显示,那这两个类型依据什么判断了?

  在很很很上面,我们用的是行数据对象能通过gei_field获取到字段,在这里,我们发现,其实表对象通过get_field也照样能获取到字段,那判断是不是choices字段就可以直接用choices属性了,而外键判断的话,用外键类型的__name__进行判断就是了

@register.simple_tag
def render_filter_ele(condtion,admin_class):

    #这里增加一个默认不选的option
    select_ele = \'\'\'<select class="form-control" name=\'%s\'><option>----</option>\'\'\'%condtion
    #这里涉及select下拉框选项常见的两类:choices类和外键类,所以需要区分
    field_obj = admin_class.model._meta.get_field(condtion)
    if field_obj.choices:
        for choice_item in field_obj.choices:
            select_ele += \'\'\'<option value=\'%s\'>%s</option>\'\'\'%choice_item

    if type(field_obj).__name__ == \'ForeignKey\':
        #这里的1主要过滤掉  其前面不需要的横线
        for choice_item in field_obj.get_choices()[1:]:
            select_ele += \'\'\'<option value=\'%s\'>%s</option>\'\'\' % choice_item
    
    select_ele += \'</select>\'
    return mark_safe(select_ele)

   那到这里上面解决了在前端刷选条件的显示问题了,接下来就要提交这些条件,到后端取数据了,提交的话,直接用form表单,select表前在form表单,请求类型就get请求,这样的话,url类似于?source=5&consultant=2&consult_course=

  那在后端,提交过来的数据应该在哪里进行筛选了?明显要在分页前,为了小模块化,我们就单独定义个过滤函数,只要在分页前,调用函数即可

def table_filter(request,admin_class):
    \'\'\'
    进行条件过滤并返回过滤后的数据
    :param request: 
    :param admin_class: 
    :return: 
    \'\'\'
    filter_conditions = {}
    for k,v in request.GET.items():
        if v:
            filter_conditions[k] = v

    return admin_class.model.objects.filter(**filter_conditions)

  过滤效果是达到了,但是还是有个小问题,那就是你提交检索后,你之前的筛选条件是不保存的,如果要保存,那需要依据获取前端的filter_condtions来判断select标签哪个选项是选中的,所以在视图里需要把筛选条件字段传到前端,前端再把它传到订制select标签的simple_tag里,已判断哪个被选中

@register.simple_tag
def render_filter_ele(condtion,admin_class,filter_condtions):
    print(\'filter-->\',filter_condtions)
    #这里增加一个默认不选的option
    select_ele = \'\'\'<select class="form-control" name=\'%s\'><option value=\'\'>----</option>\'\'\'%condtion
    #这里涉及select下拉框选项常见的两类:choices类和外键类,所以需要区分
    field_obj = admin_class.model._meta.get_field(condtion)
    if field_obj.choices:
        choice_data = field_obj.choices
    if type(field_obj).__name__ == \'ForeignKey\':
        # 这里的1主要过滤掉  其前面不需要的横线
        choice_data = field_obj.get_choices()[1:]
    for choice_item in choice_data:
        selected = \'\'
        #这里需要注意的是  前端提交过来的筛选条件是字符串  对比注意数据类型
        if filter_condtions.get(condtion) == str(choice_item[0]):  #filter_condtions获取值最好用get,因为前端提交过来的空值是会过滤掉的,而condtion则是你在admin中定义的,字典没有的值,用get不会报错
            selected = \'selected\'
        select_ele += \'\'\'<option value=\'%s\' %s>%s</option>\'\'\'%(choice_item[0],selected,choice_item[1])


    select_ele += \'</select>\'
    return mark_safe(select_ele)

 前端

            <div class="row">
                <!--action不写,默认当前页-->
                <form method="get">
                    {% for condtion in admin_class.list_filters %}
                    <div class="col-lg-2">
                        <span>{{ condtion }}</span>
                        {% render_filter_ele condtion admin_class filter_condtions %}
                    </div>
                    {% endfor %}
                    <button type="submit" class="btn btn-class">检索</button>
                </form>
            </div>

   好的,讲到这里,基本的过滤已经解决了

   

  分页bug修复:在我们过滤好数据后,点击页码,会报一个filed的字段错误,另外这个过程也不保存我们筛选条件

  第一个报错的好解决,出现问题是因为点击页码会有一个page的参数传给后端,后端把这个参数做为数据某个字段的过滤条件,所以会报字段错误,这里就需要把page参数排除在筛选字典之外

  对于第二个不完美的地方,我们只要在页码a标签的href值加入已经传到前端的筛选条件就可以了

@register.simple_tag
def render_page_ele(loop_counter,query_sets,filter_condtions):
    filters = \'\'
    for k,v in filter_condtions.items():
        filters += \'&%s=%s\'%(k,v)

    # query_sets.number  当前页
    if abs(query_sets.number - loop_counter) <= 2:
        ele_class = \'\'
        if query_sets.number == loop_counter:
            ele_class = \'active\'
        ele = \'\'\'<li class="%s"><a href="?page=%s%s">%s</a></li>\'\'\'%(ele_class,loop_counter,filters,loop_counter)

        return  mark_safe(ele)
    return \'\'

@register.simple_tag
def render_req_filter(filter_conditions):
    filter_str = \'\'
    for k,v in filter_conditions.items():
        filter_str += \'&%s=%s\'%(k,v)

    return filter_str

   前端:

            <!--分页-->

        <nav aria-label="...">
             <ul class="pagination">
                {% if query_sets.has_previous %}
                    <li><a href="?page={{ query_sets.previous_page_number }}{% render_req_filter filter_condtions %}">上页</a></li>
                {% endif %}
                 <!--循环每个页码loop_counter,然后交个simple_tag函数render_page_ele来决定显不显示-->
                 {% for loop_counter in query_sets.paginator.page_range %}
                    {% render_page_ele loop_counter query_sets filter_condtions %}
                 {% endfor %}

                {% if query_sets.has_next %}
                    <li><a href="?page={{ query_sets.next_page_number }}{% render_req_filter filter_condtions %}">下页</a></li>
                {% endif %}

             </ul>
        </nav>

   最后在给分页的数据加个统计数据,就可以了

  在前端,query_sets是分页对象,要显示总条数,这么用就可以了,

               <tfoot>
                <tr>
                    <td>总计{{ query_sets.paginator.count }}条</td>
                </tr>
                </tfoot>

  分页进一步优化

                 <!--页码显示优化-->
                 {% render_pages query_sets filter_condtions %}
@register.simple_tag
def render_pages(query_sets,filter_condtions):
    page_btns = \'\'

    filters = \'\'
    for k,v in filter_condtions.items():
        filters += \'&%s=%s\'%(k,v)

    added_dot_ele = False
    for page_num in query_sets.paginator.page_range:

        # query_sets.number  当前页
     #最后两个 or 条件为 最前两页 和最后两页 if abs(query_sets.number - page_num) <= 1 or page_num < 3 or page_num > query_sets.paginator.num_pages - 2: ele_class = \'\' if query_sets.number == page_num: ele_class = \'active\' page_btns += \'\'\'<li class="%s"><a href="?page=%s%s">%s</a></li>\'\'\'%(ele_class,page_num,filters,page_num) added_dot_ele = False else: if added_dot_ele is False: page_btns += \'<li><a>...</a></li>\' added_dot_ele = True return mark_safe(page_btns)

   最终效果:

 

排序

  在django自带admin里,排序是多列排序,怎么实现的呢?从url我们又可以猜测大致规律,在定义了每个字段的数字,比如ID定义数字为1的话,?o=1表示正向排序,等于-1时为反向排序,当然url效果是点击get请求,所以在a标签href值,点击时会变,如果当前点了正序,下次点时,就要反序了,此时href值就为?o=-1了,那我们这里呢,就不用数字了,就直接用字段名,也是可以的,所以在前端,生成列时,还要生成对应href值

                <thead>
                    <tr>
                        {% for column in admin_class.list_display %}
                            <th><a href="?o={{ column }}">{{ column }}</a></th>
                        {% endfor %}
                    </tr>
                </thead>

   这里需要注意了,上面我们过滤时,是不是会获取所有前端发来的信息作为过滤条件啊?在这里,除了排除page参数,还要排除这个排除参数o,剩下的才是过滤条件

def table_filter(request,admin_class):
    \'\'\'
    进行条件过滤并返回过滤后的数据
    :param request: 
    :param admin_class: 
    :return: 
    \'\'\'
    filter_conditions = {}
    for k,v in request.GET.items():
        if k == \'page\' or k == \'o\':  #分页和排序参数不做为过滤条件,排除掉
            continue
        if v:
            filter_conditions[k] = v

    return admin_class.model.objects.filter(**filter_conditions),filter_conditions

   到这里你要就有个疑问了,是过滤前排序了,还是过滤后排序了?明显过滤后嘛,我只要这么做,把过滤好的数据传给一个排序的函数,排好后返回给前端-->table_sort

def display_table_objs(req,app_name,table_name):
    print(\'-->\',app_name,table_name)
    #admin_class  根据这个类,获取显示条件  还操作.model下的数据
    admin_class = kingadmin.enabled_admins[app_name][table_name]

    # object_list = admin_class.model.objects.all()
    object_list,filter_condtions = table_filter(req,admin_class)  #过滤数据
    object_list = table_sort(req, object_list)  #排序数据
    paginator = Paginator(object_list, admin_class.list_per_page) # Show 25 contacts per page

    page = req.GET.get(\'page\')
    try:
        query_sets = paginator.page(page)
    except PageNotAnInteger:
        query_sets = paginator.page(1)
    except EmptyPage:
        query_sets = paginator.page(paginator.num_pages)

    return render(req,\'king_admin/table_objs.html\',{\'admin_class\':admin_class,
                                                    \'query_sets\': query_sets,
                                                    \'filter_condtions\':filter_condtions})

 排序函数

def table_sort(request,objs):
    orderby_key = request.GET.get(\'o\')
    if orderby_key:
        return objs.order_by(orderby_key)
    return objs

   注意到上面没,ORM的order_by其实是可以通过在字段前加不加-,实现正反向排序的,看下图

  做到这里,好像页面是实现了排序,但是再次点击时,没有反向排序的效果,那是因为a标签的href值不是动态的,按道理我点击正向后,里面要变成-1的,遇到这类问题了,你就要永远记住这里真理了,前端不会记住状态,只有靠后端,所以这里就需要视图函数里把  哪个字段进行了排序 正向排还是反向排 返回给前端,这里我们就这样,返回给前端最需要数据,比如刚才进行id正向排序,那此时前端需要的值就是-id,请求值和返回值是一个取反的过程

def table_sort(request,objs):
    orderby_key = request.GET.get(\'o\')
    if orderby_key:
        res = objs.order_by(orderby_key)
        if orderby_key.startswith(\'-\'):
            orderby_key = orderby_key.strip(\'-\')
        else:
            orderby_key = \'-%s\'%orderby_key
    else:
        res = objs
    return res,orderby_key

   在前端获取到值后,循环过程中,我们需要依据后端给字段值进行对比,如果循环字段和后端给的字段名相等,那就把这个值赋给这行的a标签值,但是对比时候,这个值,有可能带-,所以对比前,需要去掉这个-,前端无法做到,只能再一次用到simple_tag

                        {% for column in admin_class.list_display %}
                            {% build_table_header_column column order_key %}
                            <!--<th><a href="?o={{ column }}">{{ column }}</a></th>-->
                        {% endfor %}

 simple_tag

@register.simple_tag
def build_table_header_column(column,order_key):
    ele = \'\'\'<th><a href="?o={order_key}">{column}</a></th>\'\'\'
    if column == order_key.strip(\'-\'):  #排序当前字段
        pass
    else:
        order_key = column
    ele = ele.format(order_key=order_key, column=column)
    return mark_safe(ele)

   不过这里还有两个问题,就是如果先筛选好数据,点排序的话,是不保存筛选条件的,另外就是 点击页码,也不存在排序条件的

  第一个问题的话,问题出现点就是点击排序的a标签并没有加入已有的筛选条件,所有在构造列的a标签,需要把后端返回的筛选条件,加入

@register.simple_tag
def build_table_header_column(column,order_key,filter_condtions):
    filter_str = \'\'
    for k,v in filter_condtions.items():
        filter_str += \'&%s=%s\'%(k,v)

    ele = \'\'\'<th>
    <a href="?o={order_key}{filter_str}">{column}</a>
    {sort_icon}
    </th>\'\'\'
    if order_key:

        if column == order_key.strip(\'-\'):  #排序当前字段
            # 正序
            if \'-\' in order_key:
                sort_icon = \'\'\'<span class="glyphicon glyphicon-triangle-bottom" aria-hidden="true"></span>\'\'\'

            else:
                sort_icon = \'\'\'<span class="glyphicon glyphicon-triangle-top" aria-hidden="true"></span>\'\'\'
        else:
            order_key = column
            sort_icon = \'\'
    else:  #没有排序
        order_key = column
        sort_icon = \'\'
    ele = ele.format(order_key=order_key, column=column,sort_icon=sort_icon,filter_str=filter_str)
    return mark_safe(ele)

  第二问题的话,也就是页码a标签,加了page,加了筛选条件,但是就是没有加排序,所以这里要加入排序条件,也可以让后端获取到前端的o值返回就解决了,在生成页码标签的渲染函数里给标签加上这个值就可以了

 

搜索框

  数据筛选最后剩下一个搜索框了,要想了解运行机理,我们要先看django自带admin是怎么做的,在输入框,输入搜索关键字后,点击搜索,url多了一个参数就是q=‘关键词’,并且如果是条件筛选后的数据,点击搜索,是在筛选数据基础上进行搜索的,所以这里我们可以这么处理,即把搜索框放在和筛选条件同一form表单下

            <div class="row">
                <!--action不写,默认当前页-->
                <form method="get">
                    <div class="row" style="margin:15px;">
                        {% for condtion in admin_class.list_filters %}
                        <div class="col-lg-2">
                            <span>{{ condtion }}</span>
                            {% render_filter_ele condtion admin_class filter_condtions %}
                        </div>
                        {% endfor %}
                        <button type="submit" class="btn btn-class">检索</button>
                    </div>
                    <div class="row">
                        <div class="col-lg-2">
                            <input type="search" name="_q" class="form-control" style="margin-left:30px;">
                        </div>
                        <div class="col-lg-2">
                            <button type="submit" class="btn btn-success">search</button>
                        </div>
                    </div>
                </form>
            </div>

   由于搜索是筛选基础上进行搜索的,所以后端搜索代码就在加载过滤后,分页前

def display_table_objs(req,app_name,table_name):
    print(\'-->\',app_name,table_name)
    #admin_class  根据这个类,获取显示条件  还操作.model下的数据
    admin_class = kingadmin.enabled_admins[app_name][table_name]

    # object_list = admin_class.model.objects.all()
    object_list,filter_condtions = table_filter(req,admin_class)  #过滤数据

    object_list = table_search(req,admin_class,object_list)  #搜索查询

    object_list,order_key = table_sort(req, object_list)  #排序数据
    paginator = Paginator(object_list, admin_class.list_per_page) # Show 25 contacts per page

    page = req.GET.get(\'page\')
    try:
        query_sets = paginator.page(page)
    except PageNotAnInteger:
        query_sets = paginator.page(1)
    except EmptyPage:
        query_sets = paginator.page(paginator.num_pages)

    return render(req,\'king_admin/table_objs.html\',{\'admin_class\':admin_class,
                                                    \'query_sets\': query_sets,
                                                    \'filter_condtions\':filter_condtions,
                                                    \'order_key\':order_key,
                                                    \'previous_order_key\':req.GET.get(\'o\',\'\'),
                                                    \'search_text\':req.GET.get(\'_q\',\'\')})

   在这里我们在admin class再加一个静态字段,方便配置是要对哪几列数据进行搜索

class CustomerAdmin(BaseAdmin):
    list_display = (\'id\',\'qq\',\'name\',\'source\',\'consultant\',\'consult_course\',\'date\')
    list_filters = [\'source\',\'consultant\',\'consult_course\']
    search_fields = [\'qq\',\'name\',\'consultant__name\']
    list_per_page = 5

   搜索查询里,对于每列之间的关系是或的关系,比如在上面配置的字段是qq,name,consultant__name(这个是外键),我输入的关键词要在这三列中随便哪列能找到 ,要构造或的关系,我们可以使用django提供Q啊,如下

  所以我们可以对前端传过来参数,构造成Q的形态进行查询

def table_search(request,admin_class,objs):
    search_key = request.GET.get(\'_q\',\'\')
    q_obj = Q()
    q_obj.connector = \'OR\'
    for filed in admin_class.search_fields:
        q_obj.children.append((\'%s__contains\'%filed,search_key))

    objs = objs.filter(q_obj)
    return objs

   这里还要注意,页码里和排序点击,a标签里要加入对应的search条件,方法和过滤,分页那里差不多,这里就不赘述了

 

日期过滤

  平时我们用到的时间过滤,一般两种形式的,一种两个筛选条件,起始时间和结束时间,另外一种,就是最近多少,比如最近一礼拜,最近一个月,所以date这个筛选下拉框就不是把数据里的值列出来,而根据自己定义的时间段去定义option项

  因此,我们还要在生成筛选字段框那里,在加入一个分支,判断时间字段的

  

  上面导入datetime,为什么要在django里导入,而不是从直接导入了?直接导入datetime是系统时间,django里的这个时间才是项目时间

  我们做成第二种,时间筛选就定义为选中一个选项,就是给后端发送一个起始时间,把起始时间到当前时间的所有数据都过滤出来

  另外要注意的是,url发送时间数据时是这样的,&date=2018-3-1,但是实际上,我们要是比这个时间大,而且django筛选大于的数据是这样的,date__gte,所以我们可以在提交数据时,就按照django的要求进行提交了?也就是&date__gte=2018-3-1 ,我们可以在生成过滤标签的simple_tag函数里大做文章了

@register.simple_tag
def render_filter_ele(condtion,admin_class,filter_condtions):
    print(\'filter-->\',filter_condtions)
    #这里增加一个默认不选的option
    select_ele = \'\'\'<select class="form-control" name=\'{filter_field}\'><option value=\'\'>----</option>\'\'\'
    #这里涉及select下拉框选项常见的两类:choices类和外键类,所以需要区分
    field_obj = admin_class.model._meta.get_field(condtion)
    if type(field_obj).__name__ not in [\'DateTimeField\',\'DateField\']:
        if field_obj.choices:
            choice_data = field_obj.choices
        if type(field_obj).__name__ == \'ForeignKey\':
            # 这里的1主要过滤掉  其前面不需要的横线
            choice_data = field_obj.get_choices()[1:]
        for choice_item in choice_data:
            selected = \'\'
            #这里需要注意的是  前端提交过来的筛选条件是字符串  对比注意数据类型
            if filter_condtions.get(condtion) == str(choice_item[0]):  #filter_condtions获取值最好用get,因为前端提交过来的空值是会过滤掉的,而condtion则是你在admin中定义的,字典没有的值,用get不会报错
                selected = \'selected\'
            select_ele += \'\'\'<option value=\'%s\' %s>%s</option>\'\'\'%(choice_item[0],selected,choice_item[1])

        filter_field_name = condtion
    #日期字段
    else:
        date_els = []
        today_ele = datetime.now().date()
        date_els.append((\'今天\',datetime.now().date()))
        date_els.append((\'昨天\',today_ele - timedelta(days=1)))
        date_els.append((\'近7天\',today_ele - timedelta(days=7)))
        date_els.append((\'本月\',today_ele.replace(day=1)))
        date_els.append((\'近30天\',today_ele - timedelta(days=30)))
        date_els.append((\'近90天\',today_ele - timedelta(days=90)))
        date_els.append((\'近180天\',today_ele - timedelta(days=180)))
        date_els.append((\'本年\',today_ele.replace(month=1,day=1)))
        date_els.append((\'近一年\',today_ele - timedelta(days=180)))
        selected = \'\'
        for item in date_els:
            select_ele += \'\'\'<option value=\'%s\' %s>%s</option>\'\'\'%(item[1],selected,item[0])

        filter_field_name = \'%s__gte\'%condtion

    select_ele += \'</select>\'
    select_ele = select_ele.format(filter_field=filter_field_name)
    return mark_safe(select_ele)

 

 详细数据修改页

  数据表格的筛选功能完成了,那接下来就要看数据的修改怎么实现了?

  欲谋己,须模它,先看django的详细数据怎么进行修改的吧?每行数据的第一列可点,进行详细数据修改页,url上  再加  数据行id + 操作关键词,如:/11/change

  第一行可点,在生成行时判定第一列,加个a标签就是了

@register.simple_tag
def build_table_row(obj,admin_class,request):
    row_ele = \'\'
    for index,column in enumerate(admin_class.list_display):
        field_obj = obj._meta.get_field(column)
        if field_obj.choices:   #choices type
            column_data = getattr(obj,\'get_%s_display\'%column)()
        else:
            column_data = getattr(obj,column)

        if type(column_data).__name__ == \'datetime\':
            column_data = column_data.strftime(\'%Y-%m-%d %H:%M:%S\')

        if index == 0:  #让第一列的数据加a标签可点,以进入到数据修改页
            column_data = \'\'\'<a href="{request_path}{obj_id}/change/">{column_data}</a>\'\'\'.format(request_path=request.path,
                                                                                                    obj_id=obj.id,
                                                                                                    column_data=column_data)

        row_ele += "<td>%s</td>" % column_data
    return mark_safe(row_ele)

   上面有个知识点,request.path(django模板渲染自嵌request请求对象的),就是当前请求的url路径,进行修改页,只要在这基础上,加上数据id和修改关键词

 

form表单验证

  当点击某条数据时,进入到数据修改页是表的各项数据修改框,这里明显就一个form表单验证啊,而且admin上配置的表都可以这么操作,表与表之间,它们的字段都是不同的,所以每个表都要生成一个form表单,平时我们如果要配置一个表是如下操作

from django.forms import forms,ModelForm

from app01 import models

class CustomerModelForm(ModelForm):
    class Meta:
        model = models.Customer
        fields = "__all__"

   而我们admin上配置时,根本就没有写入上述代码,那这么说,django admin是动态生成上述form表单的,那怎么生成的?无非就是针对表动态生成form表单类吗?还记得吗?在python中,一切事物皆对象,类这种对象是type创建的啊,平时我们都是class关键词来创建的,其实我们也可以用type来创建

def func(self):
    print \'hello wupeiqi\'
  
Foo = type(\'Foo\',(object,), {\'func\': func})
#type第一个参数:类名
#type第二个参数:当前类的基类
#type第三个参数:类的成员

  依据上面这个,我们就可以动态生成form表单类了,type第一个参数,你就给所有form类取个统一的名字,第二个参数要继承的父类就是ModelForm,第三参数传入了Meta,其中这里就定义了和哪张表绑定,至于是哪张表,在admin里定义了,admin_class.model

def create_model_form(request,admin_class):
    \'\'\'动态生成model_form\'\'\'

    def __new__(cls,*args,**kwargs):
        #super(CustomerForm,self).__new__(*args,**kwargs)
        print("base fields",cls.base_fields)  
        #form表单前端自带样式觉得丑,就可以这么自己加上样式
        for field_name,field_obj in cls.base_fields.items():
            field_obj.widget.attrs[\'class\'] = \'form-control\'
        return ModelForm.__new__(cls,*args,**kwargs)

    class Meta:
        model = admin_class.model
        fields = "__all__"

    attrs = {\'Meta\':Meta,
             \'__new__\':__new__}
    _model_form_class = type(\'DynamicModelForm\',(ModelForm,),attrs)
    # setattr(_model_form_class,\'__new__\',__new__)

    return _model_form_class

   好了,依据上面函数就可以动态生成form表单类,只要在视图函数调用这个函数就可以了

def table_obj_change(req,app_name,table_name,obj_id):
    admin_class = kingadmin.enabled_admins[app_name][table_name]
    model_form_class = create_model_form(req,admin_class)

    obj = admin_class.model.objects.get(id=obj_id)
    if req.method == "POST":
        #此时提交过来的post请求是修改数据,为了让前端通过form显示修改后的数据,可以直接把post数据传给form
        #如果不给instance赋值,是创建,给了,才是修改
        form_obj = model_form_class(req.POST,instance=obj)  #更新
        if form_obj.is_vaild():
            form_obj.save()
    else:
        form_obj = model_form_class(instance=obj)
    return render(req,\'king_admin/table_obj_change.html\',{\'form_obj\':form_obj})

   平时我们直接实例化form类返回给前端的话,表单数据是啥都没有,但是实例的时候,通过instance传入某条数据,那么前端就会显示传入的数据,还有把前端提交过来的修改数据传入到form实例,而instance什么都不做,那这个默认为创建,如果instance传了某条数据,那这个就修改这条数据,is_vaild验证无误后,save一下

  前端

{% extends \'king_admin/index.html\' %}
{% load tags %}
{% block container %}
change table

<form class="form-horizontal" method="post">{% csrf_token %}
    <span style="color:red;">{{ form_obj.errors }}</span>
    {% for field in form_obj %}
      <div class="form-group">
        <label for="inputEmail3" class="col-sm-2 control-label" style="font-weight:normal">
            {% if field.field.required %}
                <b>{{ field.label }}</b>
            {% else %}
                {{ field.label }}
            {% endif %}
        </label>
        <div class="col-sm-6">
          <!--<input type="email" class="form-control" id="inputEmail3" placeholder="Email">-->
            {{ field }}
        </div>
      </div>
    {% endfor %}

    <div class="form-group">
        <button type="submit" class="btn btn-success pull-right">Save</button>
    </div>
</form>


{% endblock %}

   前端中,错误信息errors,字段名label,以及字段可不可空required(必填 加粗)

 

数据添加

  数据添加的话,页面显示和修改页差不多,模板直接继承修改页就可以了

  而视图里,不用传入数据,也就是不用给instance赋值,添加保存好后跳转到上一页即可

def table_obj_add(req,app_name,table_name):
    admin_class = kingadmin.enabled_admins[app_name][table_name]
    model_form_class = create_model_form(req,admin_class)

    if req.method == "POST":
        #添加
        form_obj = model_form_class(req.POST)
        if form_obj.is_valid():
            form_obj.save()
            return redirect(req.path.replace(\'/add/\',\'/\'))
    else:
        form_obj = model_form_class()
    return render(req,\'king_admin/table_obj_add.html\',{\'form_obj\':form_obj})

 

复选框优化

   在我们做的修改页和添加页,manyTomany就是简单的复选框,django自带admin里还提供一个静态字段可以用于设置哪些多对多字段可以显示两个框来进行操作,比如设置tag字段

    filter_horizontal = (\'tags\',)

   然后就有了下面的显示效果

 

   会不会感觉上面的效果会更直观,显示哪些已选,对我们也是要实现这样的效果

  既然django自带的里有对这样的配置,那我们也增加一个这样配置

 

class BaseAdmin(object):
    \'\'\'防止子类继承如果没写,执行过程依然能找到,只不过为空\'\'\'
    list_display = []  #显示的列
    list_filters = []  #筛选条件
    search_fields = []  #对哪些字段进行搜索
    list_per_page = 20  #每页显示多少条
    ordering = None  #默认排序的列
    filter_horizontal = []  #哪些多对多字段的复选框显示两框

class CustomerA

以上是关于自定义admin的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段(vue主模板)

VSCode自定义代码片段——声明函数

VSCode自定义代码片段——.vue文件的模板

VSCode自定义代码片段——git命令操作一个完整流程

VSCode自定义代码片段8——声明函数

VSCode自定义代码片段1——vue主模板