Django 自定义 Admin change page 的一般方法

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 & exclude 属性

这两个属性的作用类似,类型也都是元组(推荐)。其中 fields=() 表示 “仅显示这些字段”,而 excludes=() 则表示 “不要显示这些字段”。视具体情境,一般二者只用其一,通常 fields 出现的机会更高些,毕竟他的控制更加精准。


class UserAdmin(models.ModelAdmin):
    def change_view():
        self.fields = ('name','mobile','email')

注意元组元素的类型为字符串,后面类似元素的类型一般也都是字符串,不再强调。

readonly_fields 属性

这个属性的作用就和他的名字一样。默认 change page 显示的字段都是可编辑状态的,因为 admin 本来就是做可视化编辑数据库之用的嘛。


...
    self.readonly_fields = ('name',)

额外需注意的 Python 语法:对于单元素的元组,必须在元素后面加一个 “,” 。这是因为 Python 支持对语句使用小括号封装,你不加逗号,会被解释器误认为一条语句,而不是元组。

fieldsets 属性

默认情况下,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,否则会被转义


直接修改 admin 模板

除了像上面通过 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 请求

用户提交的 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 中的字段,你改了也没用。

对于这种状况,要么你在方法中改一次 fieldsreadonly_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_fieldsfieldsets 等字段的定义写在 channge_view()if request.method == 'GET': 里,而不是直接写成类属性。

但其实并不太推荐上面这种过于 hack 的写法。

在保存前修改 Model 实例

更常用的对保存请求的修改发生在 save_model(self, request, obj, form, change) 里,参数里不仅有 request ,还有表示 Model 实例的 obj 可以用。这里因为 POST 已经被处理过了,所以你可以不受限制地修改 obj 的属性(当然要在数据有效的前提下)。

objform 分别是修改后待保存的 model 实例和 POST 提交的 Form 对象,即 obj.attr1form.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 里改,不然用户即接收不到提示,保存也不会生效。

数据校验(data validation)

django 对提交数据的有效性验证提供了三种方式,分别对应三个层面。

  1. Form 对象校验

    通过 Form 对象来进行数据校验的方法很容易找到,包括 django book 中也有一章在讲,这里就不再赘述了

  2. 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!')
    
  3. validators 字段校验

    这是一种针对 ModelField 的校验规则,具体我也没用过,可以去官网 ref/forms/validation/ 查看文档。

这三种不同的校验方法所作用的层面也不同,一般在 admin 中,或其他处理表单提交的地方,Form 和 Model 实例较常用。Form 多针对单个字段的数据类型或内容合法性验证,Model 实例则擅长做多字段间的逻辑性验证。

修改底部提交栏

submit_line 模板

在 admin 的模板加载路径内,可以找到一个 submit_line.html 文件,这便是 admin change page 底部那三个 保存并继续编辑保存并新增保存 按钮定义的位置。

可惜单在 template/admin/appname/change_form.html 同级目录下新建一个 submit_line.html 的方式并不能使其自动加载。你还得显式地在 change_form.htmlinclude 它。


{% 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)


其他

访问 Field 的 verbose_name

Model 的 Field 第一位置参数是 verbose_name ,这个更加易读的字段名会被自动展示在 Admin Form 中,如果我们想在别处访问它的话,比如 messages 中,那么获取的方式可以是


Model[instance]._meta.get_field('field_name').verbose_name


你可能感兴趣的:(Django 自定义 Admin change page 的一般方法)