Source code for xadmin.plugins.actions

# coding=utf-8
"""
Action
======

功能
----

Action 插件在数据列表页面提供了数据选择功能, 选择后的数据可以经过 Action 做特殊的处理. 默认提供的 Action 为批量删除功能.

截图
----

.. image:: /images/plugins/action.png

使用
----

开发者可以设置 Model OptionClass 的 actions 属性, 该属性是一个列表, 包含您想启用的 Action 的类. 系统已经默认内置了删除数据的 Action,
当然您可以自己制作 Action 来实现特定的功能, 制作 Action 的实例如下.

    * 首先要创建一个 Action 类, 该类需要继承 BaseActionView. BaseActionView 是 :class:`~xadmin.views.ModelAdminView` 的子类::
    
        from xadmin.plugins.actions import BaseActionView

        class MyAction(BaseActionView):

            # 这里需要填写三个属性
            action_name = "my_action"    #: 相当于这个 Action 的唯一标示, 尽量用比较针对性的名字
            description = _(u'Test selected %(verbose_name_plural)s') #: 描述, 出现在 Action 菜单中, 可以使用 ``%(verbose_name_plural)s`` 代替 Model 的名字.

            model_perm = 'change'    #: 该 Action 所需权限

            # 而后实现 do_action 方法
            def do_action(self, queryset):
                # queryset 是包含了已经选择的数据的 queryset
                for obj in queryset:
                    # obj 的操作
                    ...
                # 返回 HttpResponse
                return HttpResponse(...)

    * 然后在 Model 的 OptionClass 中使用这个 Action::

        class MyModelAdmin(object):

            actions = [MyAction, ]

    * 这样就完成了自己的 Action

API
---
.. autoclass:: ActionPlugin

"""
from django import forms
from django.core.exceptions import PermissionDenied
from django.db import router
from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader
from django.template.response import TemplateResponse
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _, ungettext
from django.utils.text import capfirst
from xadmin.sites import site
from xadmin.util import model_format_dict, get_deleted_objects, model_ngettext
from xadmin.views import BaseAdminPlugin, ListAdminView
from xadmin.views.base import filter_hook, ModelAdminView


ACTION_CHECKBOX_NAME = '_selected_action'
checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)


def action_checkbox(obj):
    return checkbox.render(ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
action_checkbox.short_description = mark_safe(
    '<input type="checkbox" id="action-toggle" />')
action_checkbox.allow_tags = True
action_checkbox.allow_export = False
action_checkbox.is_column = False


class BaseActionView(ModelAdminView):
    action_name = None
    description = None
    icon = 'fa fa-tasks'

    model_perm = 'change'

    @classmethod
    def has_perm(cls, list_view):
        return list_view.get_model_perms()[cls.model_perm]

    def init_action(self, list_view):
        self.list_view = list_view
        self.admin_site = list_view.admin_site

    @filter_hook
    def do_action(self, queryset):
        pass


class DeleteSelectedAction(BaseActionView):

    action_name = "delete_selected"
    description = _(u'Delete selected %(verbose_name_plural)s')

    delete_confirmation_template = None
    delete_selected_confirmation_template = None

    model_perm = 'delete'
    icon = 'fa fa-times'

    @filter_hook
    def delete_models(self, queryset):
        n = queryset.count()
        if n:
            queryset.delete()
            self.message_user(_("Successfully deleted %(count)d %(items)s.") % {
                "count": n, "items": model_ngettext(self.opts, n)
            }, 'success')

    @filter_hook
    def do_action(self, queryset):
        # Check that the user has delete permission for the actual model
        if not self.has_delete_permission():
            raise PermissionDenied

        using = router.db_for_write(self.model)

        # Populate deletable_objects, a data structure of all related objects that
        # will also be deleted.
        deletable_objects, perms_needed, protected = get_deleted_objects(
            queryset, self.opts, self.user, self.admin_site, using)

        # The user has already confirmed the deletion.
        # Do the deletion and return a None to display the change list view again.
        if self.request.POST.get('post'):
            if perms_needed:
                raise PermissionDenied
            self.delete_models(queryset)
            # Return None to display the change list page again.
            return None

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

        if perms_needed or protected:
            title = _("Cannot delete %(name)s") % {"name": objects_name}
        else:
            title = _("Are you sure?")

        context = self.get_context()
        context.update({
            "title": title,
            "objects_name": objects_name,
            "deletable_objects": [deletable_objects],
            'queryset': queryset,
            "perms_lacking": perms_needed,
            "protected": protected,
            "opts": self.opts,
            "app_label": self.app_label,
            'action_checkbox_name': ACTION_CHECKBOX_NAME,
        })

        # Display the confirmation page
        return TemplateResponse(self.request, self.delete_selected_confirmation_template or
                                self.get_template_list('views/model_delete_selected_confirm.html'), context, current_app=self.admin_site.name)


[docs]class ActionPlugin(BaseAdminPlugin): # Actions actions = [] actions_selection_counter = True global_actions = [DeleteSelectedAction] def init_request(self, *args, **kwargs): self.actions = self.get_actions() return bool(self.actions) def get_list_display(self, list_display): if self.actions: list_display.insert(0, 'action_checkbox') self.admin_view.action_checkbox = action_checkbox return list_display def get_list_display_links(self, list_display_links): if self.actions: if len(list_display_links) == 1 and list_display_links[0] == 'action_checkbox': return list(self.admin_view.list_display[1:2]) return list_display_links def get_context(self, context): if self.actions and self.admin_view.result_count: av = self.admin_view selection_note_all = ungettext('%(total_count)s selected', 'All %(total_count)s selected', av.result_count) new_context = { 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(av.result_list)}, 'selection_note_all': selection_note_all % {'total_count': av.result_count}, 'action_choices': self.get_action_choices(), 'actions_selection_counter': self.actions_selection_counter, } context.update(new_context) return context def post_response(self, response, *args, **kwargs): request = self.admin_view.request av = self.admin_view # Actions with no confirmation if self.actions and 'action' in request.POST: action = request.POST['action'] if action not in self.actions: msg = _("Items must be selected in order to perform " "actions on them. No items have been changed.") av.message_user(msg) else: ac, name, description, icon = self.actions[action] select_across = request.POST.get('select_across', False) == '1' selected = request.POST.getlist(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.") av.message_user(msg) else: queryset = av.list_queryset._clone() if not select_across: # Perform the action only on the selected objects queryset = av.list_queryset.filter(pk__in=selected) response = self.response_action(ac, 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()) return response def response_action(self, ac, queryset): if isinstance(ac, type) and issubclass(ac, BaseActionView): action_view = self.get_model_view(ac, self.admin_view.model) action_view.init_action(self.admin_view) return action_view.do_action(queryset) else: return ac(self.admin_view, self.request, queryset) def get_actions(self): if self.actions is None: return SortedDict() actions = [self.get_action(action) for action in self.global_actions] for klass in self.admin_view.__class__.mro()[::-1]: class_actions = getattr(klass, 'actions', []) if not class_actions: continue actions.extend( [self.get_action(action) for action in class_actions]) # get_action might have returned None, so filter any of those out. actions = filter(None, actions) # Convert the actions into a SortedDict keyed by name. actions = SortedDict([ (name, (ac, name, desc, icon)) for ac, name, desc, icon in actions ]) return actions def get_action_choices(self): """ Return a list of choices for use in a form object. Each choice is a tuple (name, description). """ choices = [] for ac, name, description, icon in self.actions.itervalues(): choice = (name, description % model_format_dict(self.opts), icon) choices.append(choice) return choices def get_action(self, action): if isinstance(action, type) and issubclass(action, BaseActionView): if not action.has_perm(self.admin_view): return None return action, getattr(action, 'action_name'), getattr(action, 'description'), getattr(action, 'icon') elif callable(action): func = action action = action.__name__ elif hasattr(self.admin_view.__class__, action): func = getattr(self.admin_view.__class__, action) else: return None if hasattr(func, 'short_description'): description = func.short_description else: description = capfirst(action.replace('_', ' ')) return func, action, description, getattr(func, 'icon', 'tasks') # View Methods def result_header(self, item, field_name, row): if item.attr and field_name == 'action_checkbox': item.classes.append("action-checkbox-column") return item def result_item(self, item, obj, field_name, row): if item.field is None and field_name == u'action_checkbox': item.classes.append("action-checkbox") return item # Media def get_media(self, media): if self.actions and self.admin_view.result_count: media = media + self.vendor('xadmin.plugin.actions.js', 'xadmin.plugins.css') return media # Block Views def block_results_bottom(self, context, nodes): if self.actions and self.admin_view.result_count: nodes.append(loader.render_to_string('xadmin/blocks/model_list.results_bottom.actions.html', context_instance=context))
site.register_plugin(ActionPlugin, ListAdminView)