自定义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 CustomerAdmin(BaseAdmin): list_display = ('id','qq','name','source','consultant','consult_course','date') list_filters = ['source','consultant','consult_course','date'] search_fields = ['qq','name','consultant__name'] list_per_page = 5 ordering = 'id' filter_horizontal = ('tags',)
配置好,那我们django怎么实现的?第一点:双击选项,会从这框移动那框,第二点:点击保存会把右框的内容提交到后端,第三点,这两个框依据什么生成自己的option选项?
针对第一点,给两个框的option标签都要绑定双击事件,把点击的option标签移动对方框,也就是select标签下,可以给个id以方便找到它,所以事件的方法的参数,点击对象,目标框ID,要有点过去的标签,到时候还有可能点回来,所以这里要注意target_id变化的关系
{% if field.name in admin_class.filter_horizontal %} <!--判断是admin里配置的 多对多字段--> <div class="col-md-5"> <select name="" id="id_{{ field.name }}_from" multiple class="filter-select-box"> {% get_m2m_obj_list admin_class field form_obj as m2m_obj_list %} {% for obj in m2m_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_to')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} </select> </div> <div class="col-md-1"> 箭头 </div> <div class="col-md-5"> {% get_m2m_selected_obj_list form_obj field as selected_obj_list %} <select tag="choose_list" name="{{ field.name }}" id="id_{{ field.name }}_to" multiple class="filter-select-box"> {% for obj in selected_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_from')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} </select> </div> {% else %} {{ field }} {% endif %}
js
function MoveElementTo(ele,target_id){ var move_ele = $(ele); var parent_id = move_ele.parent().attr('id'); console.log(parent_id); var move_ele_event = "MoveElementTo(this,'" + parent_id +"')"; move_ele.attr('ondblclick',move_ele_event); move_ele.appendTo("#" + target_id); };
针对第二点,由于是这个form表单,可以直接给第二框给定字段的name(field.name),我们会发现,即使这么做了,还是没效果,主要因为框是复选框,双击过去的值,并没有选中,所以我们还要save前(也是提交数据前submit),对这个框的数据进行全选,可以在form标签里定一个onsubmit事件
<form class="form-horizontal" method="post" onsubmit="return SelectAllChooseData()">{% csrf_token %}
function SelectAllChooseData(){ $('select[tag="choose_list"] option').each(function(){ $(this).prop('selected',true); }); return true; };
针对第三点,第一个框了,是要取到tag的所有选项的,而第二框则是只是取到当前这条数据的所有tag选项,用什么方法取呢?
从上图操作中我们可以看到,对于第二个框,我们可以用当前数据+字段+all就可以获取到,而第一框,则可以用当前表+字段+rel+to+objects+all获取,针对每个框都定义一个simple_tag函数
@register.simple_tag def get_m2m_obj_list(admin_class,field,form_obj): '''返回m2m所有待选数据''' #表结构对象的某个字段 field_obj = getattr(admin_class.model,field.name) all_obj_list = field_obj.rel.to.objects.all() #单条数据的对象中的某个字段 try: obj_instance_field = getattr(form_obj.instance,field.name) print(obj_instance_field) selected_obj_list = obj_instance_field.all() standby_obj_list = [] for obj in all_obj_list: if obj not in selected_obj_list: standby_obj_list.append(obj) except: standby_obj_list = all_obj_list return standby_obj_list @register.simple_tag def get_m2m_selected_obj_list(form_obj,field): '''返回已选择的m2m数据''' try: field_obj = getattr(form_obj.instance,field.name) return field_obj.all() except: #当form_obj中没有数据时 return []
数据删除
命令行有待确定(敲一遍)
action功能
对django自带admin里的action功能,可以说,这功能可以让你为所欲为,看到这个词,你是不是淫荡的笑了呢?
首先,我们看一下到底是干啥用的吧,这个也要在admin里配置的
class CustomerAdmin(admin.ModelAdmin): list_display = ('id','qq','source','consultant','content','date') list_filter = ('source','consultant','date') search_fields = ('qq','name') raw_id_fields = ('consult_course',) filter_horizontal = ('tags',) # list_editable = ('status',) actions = ['test_action',] #action配置,以及映射的函数 def test_action(self,arg1,arg2): print('self',self) print('arg1',arg1) print('arg2',arg2) return HttpResponse('test action')
在admin里执行该功能,必须要选中某条或多条数据,点击go就跳转了
而且那个映射函数,必须传入三个参数(包括self),否则就会报错,那我们看一下这三个参数分别打印的是什么吧?
第一个参数,是当前操作的这个表的admin类,第二个参数是request请求对象,第三个参数则是queryset数据对象,也就是你勾选的数据,并且上面HTTPResponse能实现跳转,那render和redirect也是能实现,是不是知道给了啥,而且也知道可以做啥,是不是感觉能为所欲为(阴阴的一笑)
好的,首先在我们的admin下,增加一个配置action操作的地方
class BaseAdmin(object): '''防止子类继承如果没写,执行过程依然能找到,只不过为空''' list_display = [] #显示的列 list_filters = [] #筛选条件 search_fields = [] #对哪些字段进行搜索 list_per_page = 20 #每页显示多少条 ordering = None #默认排序的列 filter_horizontal = [] #哪些多对多字段的复选框显示两框 actions = ['delete_selected_objs', ] class CustomerAdmin(BaseAdmin): list_display = ('id','qq','name','source','consultant','consult_course','date') list_filters = ['source','consultant','consult_course','date'] search_fields = ['qq','name','consultant__name'] list_per_page = 5 ordering = 'id' filter_horizontal = ('tags',) actions = ['delete_selected_objs','test',]
接下来怎么搞呢?我们看到django自带admin有个默认action动作--批量删除操作,那我们实现这个吧,首先从前端开始,需要在列表索引页里加上action下拉选项,点击go时执行操作,请求类型的话,post的吧,因为要涉及到删除操作数据(算做自己的准则吧),另外删除数据最好要有再次确认,所以这里要跳转,发给后端需要哪些数据呢?第一个,选中的数据(就id吧),第二个,选中的action操作,综上,使用form表单的post请求吧
<div class="row" style="margin-top:10px;"> <form onsubmit="return ActionSubmit(this)" method="post">{% csrf_token %} <div class="col-lg-2"> <select class="form-control" name="action" id="action_list" style="margin-left:30px;" > <option value="">-----------</option> {% for action in admin_class.actions %} <option value="{{ action }}">{% get_action_verbose_name admin_class action %}</option> {% endfor %} </select> </div> <div class="col-lg-1"> <button type="submit" class="btn">GO</button> </div> </form> </div>
这里需要注意的一点是,我们选择删除的数据的时候,并不是input标签,form不会帮我们自动获取,那这里是不是就要用js了,那数据怎么发过去呢?ajax?需要分两个请求发?这倒不必,我可以通过js获取数据后,构造一个input标签,把数据放在input标签里,把这个input标签添加到form表单里,然后form表单提交给后端,但是这一切需要在form提交前做,所以需要给form表单绑定欧尼submit事件,并且在这个事件,可以进行判断是否选中数据和action操作
function ActionSubmit(form_ele){ console.log('enter action submit'); var selected_ids = []; $('input[tag="obj_checkbox"]:checked').each(function(){ selected_ids.push($(this).val()); }); var selected_action = $("#action_list").val(); console.log(selected_ids); console.log(selected_action); if(selected_ids.length == 0){ alert('no object got selected!'); return }; if(!selected_action){ alert('No action got selected!'); return }; // start submit var selected_ids_ele = "<input name='selected_ids' type='hidden' value='" + selected_ids.toString() + "'>"; $(form_ele).append(selected_ids_ele); return true; };
提交后端后,由于post提交的是当前页,当前页就是列表索引页,所以要在列表索引页视图的增加一个分支,用于映射到action对应在admin下的函数
#action post请求 print(req.method) if req.method == "POST": print(req.POST) selected_ids = req.POST.get('selected_ids') action = req.POST.get('action') if selected_ids: selected_objs = admin_class.model.objects.filter(id__in=selected_ids.split(',')) else: raise KeyError('No object selected.') if hasattr(admin_class,action): action_func = getattr(admin_class,action) req._admin_action = action return action_func(admin_class(),req,selected_objs)
映射admin的对应函数里,在这里需要做两件事,第一件,展现再次确认页面,第二件,才是删除数据,删除数据必须要有再次确认页面的信号,刚跳转到确认页,只是先展示数据,render确认删除页就可以了,但是这里也要注意注意把接收到数据id和action操作名称,传给前端,因为当点击确认删除时,后端需要依据这些映射到哪里,进行删除哪些数据(这就是第二件事),有人说,刚才不是传了吗,第一件和第二件明显是两个请求,第二个请求是不会记住第一个请求传了什么的,你就想象这个就是在接力
当点击确认删除时,携带那些数据和action操作,以及确认信息(confirm=yes),传到后端,后端再次映射,在函数,进行信号确认,在删除数据
def delete_selected_objs(self,request,querysets): print('--->',self,request,querysets) app_name = self.model._meta.app_label table_name = self.model._meta.model_name if "yes" == request.POST.get('delete_confirm'): #做第二件事 querysets.delete() return redirect("/king_admin/%s/%s/"%(app_name,table_name)) selected_ids = ','.join([str(i.id) for i in querysets]) #做第一件事 return render(request,"king_admin/table_obj_delete.html",{"objs":querysets, 'admin_class':self, 'app_name':app_name, 'table_name':table_name, 'selected_ids':selected_ids, 'action':request._admin_action})
可读字段设置
需求来了,在平时,在修改数据时,有字段值是不允许改的,尤其是那些唯一值,能不能也在我们自定义的admin下配置哪些字段只读呢?
首先,我们要肯定目标--当然没问题,增加一个字段,用于配置可读
class BaseAdmin(object): '''防止子类继承如果没写,执行过程依然能找到,只不过为空''' list_display = [] #显示的列 list_filters = [] #筛选条件 search_fields = [] #对哪些字段进行搜索 list_per_page = 20 #每页显示多少条 ordering = None #默认排序的列 filter_horizontal = [] #哪些多对多字段的复选框显示两框 readonly_fields = [] #哪些字段只读 actions = ['delete_selected_objs', ] #订制操作
要给某些字段在修改时变成不可编辑,无法给input标签加上一个disabled的属性,但是问题来了,前端修改里input标签当时用form对象渲染了,早就已经生成了,那怎么加进去了?那只能在form对象返回给前端前就加进去了,还记得我们上面已经写了的动态生成form对象的函数create_model_form吗,我们是不是就可以在这里面加进行
生成对象是由类里__new__方法返回,要加就这里加
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' #循环只读字段 给其加上 不可编辑 if field_name in admin_class.readonly_fields: field_obj.widget.attrs['disabled'] = 'disabled' return ModelForm.__new__(cls,*args,**kwargs)
上面呢,也是实现了表面功夫,你通过浏览器的编辑改个值照样能发送,那这里就设计后端对值的验证了
去到django的官网,可以两个非常有意思的方法,一个是clean方法,对全部字段进行验证,还有一个是clean_字段 的方法,对单个字段进行验证的
那这里我首先想实现自定义全字段验证方法,说简单也简单,就是重写clean方法
def default_clean(self): '''给所有的form默认添加一个clean验证''' print('---running default clean',admin_class.readonly_fields) #self是指实例的form对象,修改数据时,给instance传了一个 数据对象 print('---obj instance',self.instance) error_list = [] for field in admin_class.readonly_fields: #只读字段 不可变,用数据库的值和前端的值进行对比 field_val = getattr(self.instance,field) # val in db #前端获取到的值 封装在clean_data里 print('cleaned data',self.cleaned_data) field_val_from_frontend = self.cleaned_data.get(field) print('---field compare',field,field_val,field_val_from_frontend) if field_val_from_frontend != field_val: #django也有对这个抛出异常有解释的,其中抛出单个和多个错误 error_list.append(ValidationError( _('Field %(field)s is readonly,data should be %(val)s'), code='invalid', params={'field':field,'val':field_val}, )) if error_list: raise ValidationError(error_list)
这里只是定义了这么一个函数,但是它和当前的form对象还没有半毛钱关系,也就是说当前端执行修改时,不会走到这个函数里进行验证,再次强调,form验证要自动执行,要和form下clean方法有关系 所以这里要这clean 和 上面这方法,绑定到form类里
class Meta: model = admin_class.model fields = "__all__" attrs = {'Meta':Meta, '__new__':__new__, 'clean':default_clean} _model_form_class = type('DynamicModelForm',(ModelForm,),attrs)
到此,我们在前端修改数据时,就执行我们定义的验证了,不过这样还不够好,因为我们期望是 form验证这个易扩展的,有人说,再次重写就可以扩展啊,可以是可以,但是又要把readonly功能在写一遍,然后再加上你的功能,有点费时间哦,我希望的是readonly功能成为固定功能,扩展不影响其使用
先在我们自动以admin下增加一个可以扩展的地方吧
class BaseAdmin(object): '''防止子类继承如果没写,执行过程依然能找到,只不过为空''' list_display = [] #显示的列 list_filters = [] #筛选条件 search_fields = [] #对哪些字段进行搜索 list_per_page = 20 #每页显示多少条 ordering = None #默认排序的列 filter_horizontal = [] #哪些多对多字段的复选框显示两框 readonly_fields = [] #哪些字段只读 actions = ['delete_selected_objs', ] #订制操作 def default_form_validation(self): '''用户可以在此进行自定义的表单验证 相当于django form的clean方法''' pass
这样,在admin子类的重写上面方法,然后再form类的进行验证的函数里,抛出异常前,调用上面函数,是不是就不影响之前的功能啦?对的
注意上面这个函数定义的self到底是admin类 还是 form类,最好打印下,确定是什么,以方便调用时,该传入啥值,如果是admin类,那就需要再传入form里需要验证的数据了(这里把form对象传入即可,在form类就是self)
def default_clean(self): '''给所有的form默认添加一个clean验证''' print('---running default clean',admin_class.readonly_fields) #self是指实例的form对象,修改数据时,给instance传了一个 数据对象 print('---obj instance',self.instance) error_list = [] for field in admin_class.readonly_fields: #只读字段 不可变,用数据库的值和前端的值进行对比 field_val = getattr(self.instance,field) # val in db #前端获取到的值 封装在clean_data里 print('cleaned data',self.cleaned_data) field_val_from_frontend = self.cleaned_data.get(field) print('---field compare',field,field_val,field_val_from_frontend) if field_val_from_frontend != field_val: #django也有对这个抛出异常有解释的,其中抛出单个和多个错误 error_list.append(ValidationError( _('Field %(field)s is readonly,data should be %(val)s'), code='invalid', params={'field':field,'val':field_val}, )) #invoke user's cutomized form validation #扩展调用 self.ValidationError = ValidationError respone = admin_class().default_form_validation(self) if respone: error_list.append(respone) if error_list: raise ValidationError(error_list)
记得取验证数据,是在form对象里cleaned_data里
def default_form_validation(self,form_obj): print('------customer validation',self) print('----instance',form_obj.instance) consult_content = form_obj.cleaned_data.get('content','') if len(consult_content) < 15: return form_obj.ValidationError( ('Field %(field)s 咨询内容不能少于15个字符'), code='invalid', params={'field':'content'}, )
自定义单字段form验证方法
和上面的一样,给哪个表写字段验证方法,我们就在对应的admin下定义这个方法
@staticmethod def clean_name(self): #对name字段验证 print("name clean validation:",self.cleaned_data["name"]) if not self.cleaned_data["name"]: self.add_error("name","cannot be null")
为什么要使用的是静态方法,这就和后面提到的动态绑定单字段方法有关系了,如果定义为普通方法,就必须要由对象调用,我这里,一直提示要传入了一个admin类实例,也是就是说,你那self必须是admin实例,但在动态绑定过程中,在给form类绑定后,我们希望self是form实例,所以这里静态方法完美的解决了这个问题,上面的self是额外的参数
上面全字段验证,要让它自动执行,是要和clean方法绑定好,那这个单字段的方法,就要和clean_字段名,但这是字段名是你写的单字段方法决定的,比如name字段,你定义了这么一个clean_name方法在admin下对不对,form那边可是不清楚你定义了几个方法以及哪几个字段的方法,要想动态绑到form对象下,只能循环所有的字段,尝试获取clean_字段 方法,看有没有,有就绑定
既然是动态绑定,就要返回类对象之前绑定,也就是__new__方法下
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' #循环只读字段 给其加上 不可编辑 if field_name in admin_class.readonly_fields: field_obj.widget.attrs['disabled'] = 'disabled' #循环字段时,去admin里找下有没有单字段验证方法,有就给form对象加上 if hasattr(admin_class,'clean_%s'%field_name): field_clean_func = getattr(admin_class,"clean_%s"%field_name) setattr(cls,"clean_%s"%field_name,field_clean_func) return ModelForm.__new__(cls,*args,**kwargs)
readonly_fields补充
上面的只读配置中,如果多对多字段(下拉框)在filter_horizontal里配置了(此时为两个下拉框),此时的下拉框,是我们在前端自己写的,并不是form对象渲染的,所以上面的readonly配置并不能在这里起作用,既然是这样,那我们也手动加上不可编辑属性吧
前端再在多对多分支下,在加个判断,如果在readonly_fields里,就把标签加上disabled,并把相关的事件,比如双击事件,去掉
<div class="col-sm-6"> <!--<input type="email" class="form-control" id="inputEmail3" placeholder="Email">--> {% if field.name in admin_class.filter_horizontal %} <!--判断是admin里配置的 多对多字段--> <div class="col-md-5"> <select name="" id="id_{{ field.name }}_from" multiple class="filter-select-box"> {% get_m2m_obj_list admin_class field form_obj as m2m_obj_list %} {% if field.name not in admin_class.readonly_fields %} {% for obj in m2m_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_to')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} {% else %} {% for obj in m2m_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% endif %} </select> {% if field.name not in admin_class.readonly_fields %} <a onclick="MoveAllElementTo('id_{{ field.name }}_from','id_{{ field.name }}_to')" class="btn btn-info pull-right">chooseAll <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span> </a> {% endif %} </div> <div class="col-md-1"> <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span> <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> </div> <div class="col-md-5"> {% get_m2m_selected_obj_list form_obj field as selected_obj_list %} <select tag="choose_list" name="{{ field.name }}" id="id_{{ field.name }}_to" multiple class="filter-select-box"> {% if field.name not in admin_class.readonly_fields %} {% for obj in selected_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_from')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} {% else %} {% for obj in selected_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% endif %} </select> {% if field.name not in admin_class.readonly_fields %} <a onclick="MoveAllElementTo('id_{{ field.name }}_to','id_{{ field.name }}_from')" class="btn btn-info"> <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> removeAll </a> {% endif %} </div> <span style="color:red">{{ field.errors.as_text }}</span> {% else %} {{ field }}<span style="color:red">{{ field.errors.as_text }}</span> {% endif %} </div>
这里你要记住,在提交前就要标签的disabled移除(onsubmit事件),否则不会获取禁用标签的值,提交后端就会报错,当然这里只需要关注一下,因为上面对这个做得还是很彻底了
上面对多对多的前端做好了,后端还是要做的,因为通过数据对象获取字段名,多对多获取可不是字段串了,而且多对多 对应的对象了,那和前端的值比较,就又要区别对待了
所以我们还在循环字段里,对获取到字段对象进行判断--是否有select_related方法或all方法,多对多获取的对象就有这方法,而其他的一般的字段是字符串,就没有这方法了,在default_clean下加
def default_clean(self): '''给所有的form默认添加一个clean验证''' print('---running default clean',admin_class.readonly_fields) #self是指实例的form对象,修改数据时,给instance传了一个 数据对象 print('---obj instance',self.instance) error_list = [] for field in admin_class.readonly_fields: #只读字段 不可变,用数据库的值和前端的值进行对比 field_val = getattr(self.instance,field) # val in db # print('hasattr_field',field_val,type(field_val)) # print(hasattr(field_val,'select_related')) #多对多分支form验证 if hasattr(field_val,"select_related"): #m2m m2m_objs = getattr(field_val,"select_related")() #前端数据格式为 [1,2,3] m2m_vals = [i[0] for i in m2m_objs.values_list('id')] set_m2m_vals = set(m2m_vals) set_m2m_vals_from_frontend = set([i.id for i in self.cleaned_data.get(field)]) print('m2m',set_m2m_vals,set_m2m_vals_from_frontend) if set_m2m_vals != set_m2m_vals_from_frontend: # error_list.append(ValidationError( # # _("Field %(field)s is readonly"), # code="invalid", # params={"field":field}, # )) self.add_error(field,'field is readonly') continue #前端获取到的值 封装在clean_data里 # print('cleaned data',self.cleaned_data) field_val_from_frontend = self.cleaned_data.get(field) print('---field compare',field,field_val,field_val_from_frontend) if field_val_from_frontend != field_val: #django也有对这个抛出异常有解释的,其中抛出单个和多个错误 error_list.append(ValidationError( _('Field %(field)s is readonly,data should be %(val)s'), code='invalid', params={'field':field,'val':field_val}, )) #invoke user's cutomized form validation #扩展调用 self.ValidationError = ValidationError respone = admin_class.default_form_validation(self) if respone: error_list.append(respone) if error_list: raise ValidationError(error_list)
readonly_fields功能基本实现了,但是你点add时,又不行了,因为add的页面也进行了只读限制,那这样肯定有问题了,怎么解决了?
看你写前端,是不是去掉disabled属性就可以了,前端有两个分支,一个是自己写的两个下拉框,一个form对象渲染的
先看form渲染的,还记得是在哪加的disabled的吗?是不是在创建类对象的__new__方法里加的,是不是在这加个判断,是add操作,就不加,那怎么判定为add操作呢,我们是不是可以人为的在视图函数里,调用创建form类对象,加个add操作的标志位呢?
def table_obj_add(req,app_name,table_name): admin_class = kingadmin.enabled_admins[app_name][table_name] admin_class.is_add_form = True 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() print('end views') return render(req,'king_admin/table_obj_add.html',{'form_obj':form_obj, 'admin_class':admin_class})
然后通过这个标志位,决定要不要加disabled
#form表单前端自带样式觉得丑,就可以这么自己加上样式 for field_name,field_obj in cls.base_fields.items(): field_obj.widget.attrs['class'] = 'form-control' if not hasattr(admin_class,"is_add_form"): #代表这里添加form,不需要disabled #循环只读字段 给其加上 不可编辑 if field_name in admin_class.readonly_fields: field_obj.widget.attrs['disabled'] = 'disabled'
再看下,我们自定义的,在前端根据admin_class的这个条件
{% if field.name in admin_class.filter_horizontal %} <!--判断是admin里配置的 多对多字段--> <div class="col-md-5"> <select name="" id="id_{{ field.name }}_from" multiple class="filter-select-box"> {% get_m2m_obj_list admin_class field form_obj as m2m_obj_list %} {% if field.name in admin_class.readonly_fields and not admin_class.is_add_form %} {% for obj in m2m_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% else %} {% for obj in m2m_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_to')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} {% endif %} </select> {% if field.name not in admin_class.readonly_fields or admin_class.is_add_form %} <a onclick="MoveAllElementTo('id_{{ field.name }}_from','id_{{ field.name }}_to')" class="btn btn-info pull-right">chooseAll <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span> </a> {% endif %} </div> <div class="col-md-1"> <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span> <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> </div> <div class="col-md-5"> {% get_m2m_selected_obj_list form_obj field as selected_obj_list %} <select tag="choose_list" name="{{ field.name }}" id="id_{{ field.name }}_to" multiple class="filter-select-box"> {% if field.name in admin_class.readonly_fields and not admin_class.is_add_form %} {% for obj in selected_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% else %} {% for obj in selected_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_from')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} {% endif %} </select> {% if field.name not in admin_class.readonly_fields or admin_class.is_add_form %} <a onclick="MoveAllElementTo('id_{{ field.name }}_to','id_{{ field.name }}_from')" class="btn btn-info"> <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> removeAll </a> {% endif %} </div> <span style="color:red">{{ field.errors.as_text }}</span> {% else %} {{ field }}<span style="color:red">{{ field.errors.as_text }}</span> {% endif %}
前端搞定,那看下后端,后端验证是在一个default_clean的函数下
表只读
如果我想实现表都只读,什么都不让操作,按惯例,我们还在自定义下增加一个配置字段
readonly_fields = [] #哪些字段只读 readonly_table = False #整张表是否只读
要想表只读,我不让操作--不可增加,不可删除,不可修改,前端就把相应那几个按钮不显示
{% if not admin_class.readonly_table %} <a href="{{ request.path }}add/" class="pull-right">Add</a> {% endif %}
{% if not admin_class.readonly_table %} <div class="form-group"> {% block delete_button %} <div class="col-sm-2"> <a class="btn btn-danger" href="{% url 'obj_delete' app_name table_name form_obj.instance.id %}">Delete</a> </div> {% endblock %} <div class="col-sm-10"> <button type="submit" class="btn btn-success pull-right">Save</button> </div> </div> {% endif %}
前端是可以了,但是后端还是可以删除的,那要这操作前加上判断,并返回错误信息
对应修改和增加,我们是不是可以在进行form验证的时候,进行这个判断呢?
def default_clean(self): '''给所有的form默认添加一个clean验证''' print('---running default clean',admin_class.readonly_fields) #self是指实例的form对象,修改数据时,给instance传了一个 数据对象 print('---obj instance',self.instance) #创建和修改时,如果表只读 抛出异常 if admin_class.readonly_table: raise ValidationError( _('Table is readonly,cannot be modified or added'), code='invalid' )
那对于删除的话,就在删除前,进行判断了
视图里
action_func里