第三节 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删除一些没有用的操作。下面的代码显示了只有超级管理员才能执行删除对象的操作。
1actions = ['delete_selected', ....] 2 3def get_actions(self, request): 4 actions = super(XxxAdmin, self).get_actions(request) 5 if 'delete_selected' in actions and not request.user.is_superuser: 6 del actions['delete_selected'] 7 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处理有关的代码如下:
1 # If the request was POSTed, this might be a bulk action or a bulk 2 # edit. Try to look up an action or confirmation first, but if this 3 # isn't an action the POST will fall through to the bulk edit check, 4 # below. 5 action_failed = False 6 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) 7 8 # Actions with no confirmation 9 if (actions and request.method == 'POST' and 10 'index' in request.POST and '_save' not in request.POST): 11 if selected: 12 response = self.response_action(request, queryset=cl.get_query_set(request)) 13 if response: 14 return response 15 else: 16 action_failed = True 17 else: 18 msg = _("Items must be selected in order to perform " 19 "actions on them. No items have been changed.") 20 self.message_user(request, msg) 21 action_failed = True 22 23 # Actions with confirmation 24 if (actions and request.method == 'POST' and 25 helpers.ACTION_CHECKBOX_NAME in request.POST and 26 'index' not in request.POST and '_save' not in request.POST): 27 if selected: 28 response = self.response_action(request, queryset=cl.get_query_set(request)) 29 if response: 30 return response 31 else: 32 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方法。这个方法的源代码如下;
1 def response_action(self, request, queryset): 2 """ 3 Handle an admin action. This is called if a request is POSTed to the 4 changelist; it returns an HttpResponse if the action was handled, and 5 None otherwise. 6 """ 7 8 # There can be multiple action forms on the page (at the top 9 # and bottom of the change list, for example). Get the action 10 # whose button was pushed. 11 try: 12 action_index = int(request.POST.get('index', 0)) 13 except ValueError: 14 action_index = 0 15 16 # Construct the action form. 17 data = request.POST.copy() 18 data.pop(helpers.ACTION_CHECKBOX_NAME, None) 19 data.pop("index", None) 20 21 # Use the action whose button was pushed 22 try: 23 data.update({'action': data.getlist('action')[action_index]}) 24 except IndexError: 25 # If we didn't get an action from the chosen form that's invalid 26 # POST data, so by deleting action it'll fail the validation check 27 # below. So no need to do anything here 28 pass 29 30 action_form = self.action_form(data, auto_id=None) 31 action_form.fields['action'].choices = self.get_action_choices(request) 32 33 # If the form's valid we can handle the action. 34 if action_form.is_valid(): 35 action = action_form.cleaned_data['action'] 36 select_across = action_form.cleaned_data['select_across'] 37 func, name, description = self.get_actions(request)[action] 38 39 # Get the list of selected PKs. If nothing's selected, we can't 40 # perform an action on it, so bail. Except we want to perform 41 # the action explicitly on all objects. 42 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) 43 if not selected and not select_across: 44 # Reminder that something needs to be selected or nothing will happen 45 msg = _("Items must be selected in order to perform " 46 "actions on them. No items have been changed.") 47 self.message_user(request, msg) 48 return None 49 50 if not select_across: 51 # Perform the action only on the selected objects 52 queryset = queryset.filter(pk__in=selected) 53 54 response = func(self, request, queryset) 55 56 # Actions may return an HttpResponse, which will be used as the 57 # response from the POST. If not, we'll be a good little HTTP 58 # citizen and redirect back to the changelist page. 59 if isinstance(response, HttpResponse): 60 return response 61 else: 62 return HttpResponseRedirect(request.get_full_path()) 63 else: 64 msg = _("No action selected.") 65 self.message_user(request, msg) 66 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模块
1 def delete_with_ref_check(self, request, queryset): 2 """ 3 Reform the default action which deletes the selected objects. 4 if queryset cannot be deleted and display a error page if there are ref objs 5 source code: django/contrib/admin/actions.py 6 7 This action first check if there are objs refing on the queryset. 8 if True ,then displays a error page which shows objs refing the queryset. 9 else displays a confirmation page whichs shows queryset 10 (Note using the same one template named 'delete_selected_ref_confirmation.html') 11 12 Next, it delets all selected objects and redirects back to the change list. 13 """ 14 opts = self.model._meta 15 app_label = opts.app_label 16 17 # Check that the user has delete permission for the actual model 18 if not self.has_delete_permission(request): 19 raise PermissionDenied 20 21 # The user has already confirmed the deletion. 22 # Do the deletion and return a None to display the change list view again. 23 if request.POST.get('post'): 24 n = queryset.count() 25 if n: 26 for obj in queryset: 27 obj_display = force_unicode(obj) 28 self.log_deletion(request, obj, obj_display) 29 queryset.delete() 30 self.message_user(request, _("Successfully deleted %(count)d %(items)s.") % { 31 "count": n, "items": model_ngettext(self.opts, n) 32 }) 33 # Return None to display the change list page again. 34 return None 35 36 if len(queryset) == 1: 37 objects_name = force_unicode(opts.verbose_name) 38 else: 39 objects_name = force_unicode(opts.verbose_name_plural) 40 41 ref_obj_number_info = self.get_ref_obj_number_info(queryset) 42 if ref_obj_number_info['total'] > 0: 43 title = u'无法删除' 44 else: 45 title = u'删除确认' 46 redirect_url = urlresolvers.reverse('admin:%s_%s_changelist' %(opts.app_label, opts.module_name), current_app=self.admin_site.name) 47 48 context = { 49 'breadcrumbs': self.breadcrumbs, 50 'current_breadcrumb': u'删除%s' % self.verbose_name, 51 'title': title, 52 'ref_obj_number_info': ref_obj_number_info, 53 "objects_name": objects_name, 54 'queryset': queryset, 55 "opts": opts, 56 "app_label": app_label, 57 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 58 'redirect_url':redirect_url 59 } 60 61 # Display the confirmation page 62 return TemplateResponse(request, self.delete_selected_confirmation_template or [ 63 "admin/%s/%s/delete_selected_ref_confirmation.html" % (app_label, opts.object_name.lower()), 64 "admin/%s/delete_selected_ref_confirmation.html" % app_label, 65 "admin/delete_selected_ref_confirmation.html" 66 ], context, current_app=self.admin_site.name) 67 68 delete_with_ref_check.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s")