Django 的 Admin 系统可用性很高,尤其在一些简单的定制以后。本文主题为针对 change page (view)的常用定制,就是单个 model 实例(数据库某条记录)的显示与编辑页面,url 通常类似于 host/admin/modelname/1234/
。
定制的代码通常写在 app/admin.py
中的一个名为 ModelAdmin
的类里,主要以属性和方法的形式。或者更进一步说,代码大都存在于该类的 change_view(self, request, object_id, extra_context=None)
方法 或 save_model(self, request, obj, form, change)
方法中,这也就是下面示例代码在修改属性时都会使用一个 self.xxx = 'xxx'
语句的原因。
类名中的 Model 指的是 app/models.py
中具体的模型名。可定制的内容如下:
这两个属性的作用类似,类型也都是元组(推荐)。其中 fields=()
表示 “仅显示这些字段”,而 excludes=()
则表示 “不要显示这些字段”。视具体情境,一般二者只用其一,通常 fields
出现的机会更高些,毕竟他的控制更加精准。
class UserAdmin(models.ModelAdmin):
def change_view():
self.fields = ('name','mobile','email')
注意元组元素的类型为字符串,后面类似元素的类型一般也都是字符串,不再强调。
这个属性的作用就和他的名字一样。默认 change page 显示的字段都是可编辑状态的,因为 admin 本来就是做可视化编辑数据库之用的嘛。
...
self.readonly_fields = ('name',)
额外需注意的 Python 语法:对于单元素的元组,必须在元素后面加一个 “,”
。这是因为 Python 支持对语句使用小括号封装,你不加逗号,会被解释器误认为一条语句,而不是元组。
默认情况下,change page 对字段的显示是一个字段显示一行,然后把所有字段一字排下来。换句话说就是页面只有一个 <fieldset>
。而使用 fieldsets
属性,就可以建立多个 <fieldset>
,通常伴随着的,还有将不同字段放在同一行的操作:
...
self.fieldsets = (
(u'基本信息',{
'fields':(
('name','mobile','email',),#一行
('address','city','country',),#另一行
#新一行
),
'classes':('person',),#html 标签的 class 属性
}),
(u'身份证信息',{ #另一个 fieldset
'fields':(
('display_idcrdfnt','display_idcrdbck',),),
}),
)
这段代码因为括号太多看起来会比较乱,实际自己写一写就好了。其中 fieldsets
的元素为一个二元组:其中第一元为 <fieldset>
的名称,第二元为字典,键包括定义字段的 fields
和 定义html元素属性的 classes
。
前面的手段都是控制如何隐藏字段,显示 model 的子集。对于 model 里没有的字段,想在 Form 里显示的话就要把他们定义为 方法,并将方法名加入 readonly_fields
元组,因为这些字段都是不存在于数据库中的,所以必须展示为不可编辑状态。
...
self.fields = ('display_img',...)
self.readonly_fields = ('display_img',...)
def display_img(self,obj):
url = obj.img_url
return '<img ref="%s" />'%url
display_img.short_description = '用户照片'
display_img.allow_tags = True
上例中假设 model 的img_url
字段存储了用户照片的链接,但我们想在 change page 展示出用户的照片,而不是一段字符串,那么就可以通过这种方式自定义一个显示字段,最后定义的两个属性:
short_description
:对字段的说明性文字,会显示在图片的前面,类似于 sql 的 comment,或 model Field 的第一参数allow_tags
:tags 指的是 html 标签,上例中我们直接返回了一个 <img>
标签,那么这里就必须设置为 True,否则会被转义除了像上面通过 change_view()
方法来修改显示内容外,还可以直接编辑模板。template/admin/change_form.html
(或其他路径)文件就是渲染 admin 页面所使用的模板。通常我们会选择在以下路径派生一个 app 专用模板 template/admin/appname/change_form.html
。
而额外 context 的传递,就要通过 change_view(...,extra_context=None)
参数来实现了:
...
def change_view(self, request, object_id, extra_context=None):
...
extra_context = {'custom':'some_value'}
return super(ModelAdmin,self).change_view(...,extra_context)
在 change page 页面,如果用户点击了保存按钮,那么一个 POST 请求就会被提交上来,然后保存到数据库中。从用户提交请求到更新数据库之间有两个机会修改请求的内容。分别为 修改 POST 请求 和 修改 Model 实例。
用户提交的 POST 请求会首先被 change_view(self, request, object_id, extra_context=None)
捕获,因此可以在这个方法里直接对其进行修改。
...
def change_view(self, request, object_id, extra_context=None)
...
if request.method == 'POST':
request.POST['field'] = xxx
return super(Model,self).change_view()
注意:django 出于安全考虑,不在 fields
中的字段,添加到 POST
里也没用;在 read_only
中的字段,你改了也没用。
对于这种状况,要么你在方法中改一次 fields
或 readonly_fields
属性,要么就把修改放到下一节的 save_model()
方法里。
...
def change_view(...):
...
if request.method == 'POST':
new_readonly = list(self.readonly_fields)
new_readonly.remove('some_field')
self.readonly_fields = set(new_readonly)
...
return super(...)
要想避免因此操作导致用户下一次 GET change page 显示出额外可编辑字段的话,就应尽量把对如 readonly_fields
,fieldsets
等字段的定义写在 channge_view()
的 if request.method == 'GET':
里,而不是直接写成类属性。
但其实并不太推荐上面这种过于 hack 的写法。
更常用的对保存请求的修改发生在 save_model(self, request, obj, form, change)
里,参数里不仅有 request
,还有表示 Model 实例的 obj
可以用。这里因为 POST 已经被处理过了,所以你可以不受限制地修改 obj
的属性(当然要在数据有效的前提下)。
obj
和 form
分别是修改后待保存的 model 实例和 POST 提交的 Form 对象,即 obj.attr1
和 form.save(commit=False).attr1
的值是相同的,都是最新的数据。但 form
里可能不会包含全部的模型字段,因为可能有一些字段被隐藏,还有一些是只读状态。change
参数是一个布尔值,表示当前的保存请求是来自于新建还是变更操作。
def save_model(self, request, obj, form, change):
if change:
obj.mod_time = datetime.datetime.now()
return super(ModelAdmin,self).save_model(request, obj, form, change)
虽然上面这个功能可以在 Model 中简单使用 auto_now=True
来实现。
一个更有代表性的例子可能是:我们希望用户不必手动修改某个字段,而是依据其点击的提交按钮的不同,来自动修改该字段。具体修改提交按钮(submit button)的方法在更下面给出,这里先假设我们自定义了一个 <input type="submit" value="审核通过" name="apply_approved"/>
按钮,并希望用户按此按钮时,自动将 Model 实例的 state
属性改为 approved
:
...
def save_model(self,request,obj,...):
if 'apply_approved' in request.POST:
obj.state = 'approved'
...
return super(...)
注意:save_model()
方法实际执行的就是将 POST 请求更新到数据库中的过程,因此 obj.save()
和 super().save_model()
方法你要保证至少调用一个。不要将此方法用于否决 POST 请求(比如发现数据不合法时就简单的 pass 掉),这项要求是官方文档提出的,具体我也不清楚为什么。
这两种修改方法具体使用哪一种要视具体情况而定。一种可能的状况是:下节要讲到的 Model 里的 clean()
方法对用户提示 POST 数据非法的响应发生在 change_view()
之中,即 save_model()
之前,所以如果你想修改其中将要校验的字段,那么最好在 POST 里改,不然用户即接收不到提示,保存也不会生效。
django 对提交数据的有效性验证提供了三种方式,分别对应三个层面。
Form 对象校验
通过 Form 对象来进行数据校验的方法很容易找到,包括 django book 中也有一章在讲,这里就不再赘述了
Model 实例校验
django 的 ORM 将每一条数据库记录映射为一个 Model 的实例,那么通过该实例的方法来对属性进行验证就非常顺理成章了。如
from django.core.exceptions import ValidationError
class Model(models.Model):
...
def clean(self):
if self.age < 18 and self.18x_authorization == True:
raise ValidationError('too young, too simple.')
这里 raise ValidationError
会被 save_model()
捕捉,并将错误信息返回给 change page view,还有针对该非法字段的 CSS 样式展现。总之就是督促用户修改表单,再重新提交。注意若在 admin 的 save_model()
方法中引发此异常会导致 HTTP 500,所以记得要把它写在 Model 的 clean(self)
里。
说到给用户返回消息,还有一个专用方法:
from django.contrib import messages
...
messages.info(request, 'Hello world.')
messages.warning(request, 'FBI WARNING.')
messages.error(request, 'You shall not pass!')
validators 字段校验
这是一种针对 ModelField
的校验规则,具体我也没用过,可以去官网 ref/forms/validation/
查看文档。
这三种不同的校验方法所作用的层面也不同,一般在 admin 中,或其他处理表单提交的地方,Form 和 Model 实例较常用。Form 多针对单个字段的数据类型或内容合法性验证,Model 实例则擅长做多字段间的逻辑性验证。
在 admin 的模板加载路径内,可以找到一个 submit_line.html
文件,这便是 admin change page 底部那三个 保存并继续编辑、保存并新增、保存 按钮定义的位置。
可惜单在 template/admin/appname/change_form.html
同级目录下新建一个 submit_line.html
的方式并不能使其自动加载。你还得显式地在 change_form.html
中 include
它。
{% block submit_buttons_bottom %}
{% include "admin/audit/mmauth/submit_line.html" %}
{% endblock %}
django 1.4 起 submit row 才包含在一个 block 中,1.3 及以前的版本你需要手动修改 template/admin
目录下的公用模板.
在 admin 页面中,保存并转到下一个 (save and view next) 应该是一个很常用的按钮,可惜 django 并没有标配。
实现此功能的第一步是修改 submit_line
模板,把按钮改成(或添加成)我们需要的样子,注意按钮的 name = "_save"
属性,这个随 form 一起提交的字段是我们判断用户到底按了什么的依据。
在 save_model()
返回后,还有一个方法会被默认调用:response_change(self,request,obj)
。重定向到 下一个 的代码就可以写在这里:
...
def response_change(self,request,obj):
next = Auth.objects.filter(...)[:1]
if next:
return HttpResponseRedirect('../%d/'%next.get().id)
return super(ModelAdmin,self).response_change(request,obj)
Model 的 Field 第一位置参数是 verbose_name
,这个更加易读的字段名会被自动展示在 Admin Form 中,如果我们想在别处访问它的话,比如 messages 中,那么获取的方式可以是
Model[instance]._meta.get_field('field_name').verbose_name