Django admin应用开发(3) 批量操作

Stella981
• 阅读 453

第三节 admin Actions

3.1 实现批量操作

在Django admin实现批量操作是比较简单的。

第一步,定义一个回调函数,将在点击对象列表页面上的“执行”按钮时触发(从用户的角度来看的确如此,但在Django内部当然还需要一些检查操作,见下文详述)。它的形式如def action_handler(model_admin, request, queryset)三个参数分别表示当前的modelAdmin实例、当前请求对象和用户选定的对象集。

回调函数和View函数类似,你可以在这个函数做任何事情。比如渲染一个页面或者执行业务逻辑。

第二步(可选),添加一个描述文本,将显示在changelist页面的操作下拉列表中。一般的做法是为回调函数增加short_description属性。如果没有指定将使用回调函数名称。

第三步,注册到ModelAdmin中。在ModelAdmin中和批量操作有关的选项有变量actions和函数get_actions(self, request)两种方式定义,前者返回一个列表,后者返回一个SortedDict(有序字典)。

如actions = ['action_handler']

常用用法是将所有的操作写在actions,在get_actions中再根据request删除一些没有用的操作。下面的代码显示了只有超级管理员才能执行删除对象的操作。

actions = ['delete_selected', ....]

def get_actions(self, request):
    actions = super(XxxAdmin, self).get_actions(request)
    if 'delete_selected' in actions and not request.user.is_superuser:
        del actions['delete_selected']
    return actions

还有一种情况,如果操作是通用,可以使用AdminSite的add_actions方法注册到AdminSite对象中,这样所有的ModelAdmin都有这个操作。

3.2 批量操作的Django实现

admin内置了一个全站点可用的批量操作——删除所选对象(delete_selected)。通过阅读相关源代码可以了解在Django内部是怎么实现的。

当用户选定一些对象并选择一个操作,点击执行按钮,发送了一个POST请求,信息如下:

方法/地址

POST  /admin/(app_lable)/(module_name)

数据

changelist页面含有一个id为changelist_form的大表单,此时主要数据如下:

action=delete_selected   值为操作回调函数的名称

select_accoss=0

_selected_action=1,2,3 选定对象的PK列表(_selected_action被定义为常量helper.ACTION_CHECKBOX_NAME)

后台对应view

changelist_view

在changelist_view中与action处理有关的代码如下:

        # If the request was POSTed, this might be a bulk action or a bulk
        # edit. Try to look up an action or confirmation first, but if this
        # isn't an action the POST will fall through to the bulk edit check,
        # below.
        action_failed = False
        selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)

        # Actions with no confirmation
        if (actions and request.method == 'POST' and
                'index' in request.POST and '_save' not in request.POST):
            if selected:
                response = self.response_action(request, queryset=cl.get_query_set(request))
                if response:
                    return response
                else:
                    action_failed = True
            else:
                msg = _("Items must be selected in order to perform "
                        "actions on them. No items have been changed.")
                self.message_user(request, msg)
                action_failed = True

        # Actions with confirmation
        if (actions and request.method == 'POST' and
                helpers.ACTION_CHECKBOX_NAME in request.POST and
                'index' not in request.POST and '_save' not in request.POST):
            if selected:
                response = self.response_action(request, queryset=cl.get_query_set(request))
                if response:
                    return response
                else:
                    action_failed = True

开始的注释已经写的很明白了,如果请求是POST过来的,可能是action和批量编辑的两种操作。在action有内容,POST中没有index和_save参数时被认为是批量操作,后者在批量编辑中使用。

在对批量处理中首先从POST数据得到选定对象的PK值赋值给selected,这是一个list。然后分是否有确认流程分成两种不同的情况

action with no confirmation

在changelist页面提交数据

actions with confirmation

在其他用户自定义页面提交数据

actions=True

必须有操作

同左

helpers.ACTION_CHECKBOX_NAME

可有可无,当然若果没有的将提示没有选择对象,也不会有任何改变

必须存在,因为此时前台的模板页面是用户自己定义的,所以需要保证它必须存在

index 动作所在的表单序号

in POST

not in POST 

helper.ACTION_CHECKBOX_NAME在POST即为有确认页面。从上述代码来看二者之间只有在selected==None时,如果没有确认时会提示没有选定对象。

在确认是批量操作且有选定对象就开始调用response_action方法。这个方法的源代码如下;

    def response_action(self, request, queryset):
        """
        Handle an admin action. This is called if a request is POSTed to the
        changelist; it returns an HttpResponse if the action was handled, and
        None otherwise.
        """

        # There can be multiple action forms on the page (at the top
        # and bottom of the change list, for example). Get the action
        # whose button was pushed.
        try:
            action_index = int(request.POST.get('index', 0))
        except ValueError:
            action_index = 0

        # Construct the action form.
        data = request.POST.copy()
        data.pop(helpers.ACTION_CHECKBOX_NAME, None)
        data.pop("index", None)

        # Use the action whose button was pushed
        try:
            data.update({'action': data.getlist('action')[action_index]})
        except IndexError:
            # If we didn't get an action from the chosen form that's invalid
            # POST data, so by deleting action it'll fail the validation check
            # below. So no need to do anything here
            pass

        action_form = self.action_form(data, auto_id=None)
        action_form.fields['action'].choices = self.get_action_choices(request)

        # If the form's valid we can handle the action.
        if action_form.is_valid():
            action = action_form.cleaned_data['action']
            select_across = action_form.cleaned_data['select_across']
            func, name, description = self.get_actions(request)[action]

            # Get the list of selected PKs. If nothing's selected, we can't
            # perform an action on it, so bail. Except we want to perform
            # the action explicitly on all objects.
            selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
            if not selected and not select_across:
                # Reminder that something needs to be selected or nothing will happen
                msg = _("Items must be selected in order to perform "
                        "actions on them. No items have been changed.")
                self.message_user(request, msg)
                return None

            if not select_across:
                # Perform the action only on the selected objects
                queryset = queryset.filter(pk__in=selected)

            response = func(self, request, queryset)

            # Actions may return an HttpResponse, which will be used as the
            # response from the POST. If not, we'll be a good little HTTP
            # citizen and redirect back to the changelist page.
            if isinstance(response, HttpResponse):
                return response
            else:
                return HttpResponseRedirect(request.get_full_path())
        else:
            msg = _("No action selected.")
            self.message_user(request, msg)
            return None

该方法对提交的action表单进行验证是否有选定的操作。根据选择的action值获取它的回调函数对象func,之后获取queryset,response = func(self, request, queryset)就开始调用我们的函数了,并返回。

3.3 一个Demo

这是实际项目的一个需求,Django默认删除对象时使用的是级联删除,需要改写成如果有外键引用则不能删除,显示各确认页面。主要步骤:

定义一个新的删除对象回调函数delete_with_ref_check如下:由delete_selected函数改造,源代码可参见django.contrib.admin.actions模块

    def delete_with_ref_check(self, request, queryset):
        """
        Reform the default action which deletes the selected objects.
        if queryset cannot be deleted and display a error page if there are ref objs
        source code: django/contrib/admin/actions.py

        This action first check if there are objs refing on the queryset.
        if True ,then displays a error page which shows objs refing the queryset.
         else displays a confirmation page whichs shows queryset
         (Note using the same one template named 'delete_selected_ref_confirmation.html')

        Next, it delets all selected objects and redirects back to the change list.
        """
        opts = self.model._meta
        app_label = opts.app_label

        # Check that the user has delete permission for the actual model
        if not self.has_delete_permission(request):
            raise PermissionDenied

        # The user has already confirmed the deletion.
        # Do the deletion and return a None to display the change list view again.
        if request.POST.get('post'):
            n = queryset.count()
            if n:
                for obj in queryset:
                    obj_display = force_unicode(obj)
                    self.log_deletion(request, obj, obj_display)
                queryset.delete()
                self.message_user(request, _("Successfully deleted %(count)d %(items)s.") % {
                    "count": n, "items": model_ngettext(self.opts, n)
                })
            # Return None to display the change list page again.
            return None

        if len(queryset) == 1:
            objects_name = force_unicode(opts.verbose_name)
        else:
            objects_name = force_unicode(opts.verbose_name_plural)

        ref_obj_number_info = self.get_ref_obj_number_info(queryset)
        if ref_obj_number_info['total'] > 0:
            title = u'无法删除'
        else:
            title = u'删除确认'
        redirect_url = urlresolvers.reverse('admin:%s_%s_changelist' %(opts.app_label, opts.module_name), current_app=self.admin_site.name)

        context = {
            'breadcrumbs': self.breadcrumbs,
            'current_breadcrumb': u'删除%s' % self.verbose_name,
            'title': title,
            'ref_obj_number_info': ref_obj_number_info,
            "objects_name": objects_name,
            'queryset': queryset,
            "opts": opts,
            "app_label": app_label,
            'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
            'redirect_url':redirect_url
        }

        # Display the confirmation page
        return TemplateResponse(request, self.delete_selected_confirmation_template or [
            "admin/%s/%s/delete_selected_ref_confirmation.html" % (app_label, opts.object_name.lower()),
            "admin/%s/delete_selected_ref_confirmation.html" % app_label,
            "admin/delete_selected_ref_confirmation.html"
        ], context, current_app=self.admin_site.name)

    delete_with_ref_check.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s")
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Nginx + lua +[memcached,redis]
精品案例1、Nginxluamemcached,redis实现网站灰度发布2、分库分表/基于Leaf组件实现的全球唯一ID(非UUID)3、Redis独立数据监控,实现订单超时操作/MQ死信操作SelectPollEpollReactor模型4、分布式任务调试Quartz应用
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
4个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这