actions.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. from collections import OrderedDict
  2. from django import forms, VERSION as django_version
  3. from django.core.exceptions import PermissionDenied
  4. from django.db import router
  5. from django.http import HttpResponse, HttpResponseRedirect
  6. from django.template import loader
  7. from django.template.response import TemplateResponse
  8. from django.utils import six
  9. from django.utils.encoding import force_text
  10. from django.utils.safestring import mark_safe
  11. from django.utils.translation import ugettext as _, ungettext
  12. from django.utils.text import capfirst
  13. from django.contrib.admin.utils import get_deleted_objects
  14. from xadmin.plugins.utils import get_context_dict
  15. from xadmin.sites import site
  16. from xadmin.util import model_format_dict, model_ngettext
  17. from xadmin.views import BaseAdminPlugin, ListAdminView
  18. from xadmin.views.base import filter_hook, ModelAdminView
  19. from xadmin import views
  20. ACTION_CHECKBOX_NAME = '_selected_action'
  21. checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
  22. def action_checkbox(obj):
  23. return checkbox.render(ACTION_CHECKBOX_NAME, force_text(obj.pk))
  24. action_checkbox.short_description = mark_safe(
  25. '<input type="checkbox" id="action-toggle" />')
  26. action_checkbox.allow_tags = True
  27. action_checkbox.allow_export = False
  28. action_checkbox.is_column = False
  29. class BaseActionView(ModelAdminView):
  30. action_name = None
  31. description = None
  32. icon = 'fa fa-tasks'
  33. model_perm = 'change'
  34. @classmethod
  35. def has_perm(cls, list_view):
  36. return list_view.get_model_perms()[cls.model_perm]
  37. def init_action(self, list_view):
  38. self.list_view = list_view
  39. self.admin_site = list_view.admin_site
  40. @filter_hook
  41. def do_action(self, queryset):
  42. pass
  43. def __init__(self, request, *args, **kwargs):
  44. super().__init__(request, *args, **kwargs)
  45. if django_version > (2, 0):
  46. for model in self.admin_site._registry:
  47. if not hasattr(self.admin_site._registry[model], 'has_delete_permission'):
  48. setattr(self.admin_site._registry[model], 'has_delete_permission', self.has_delete_permission)
  49. class DeleteSelectedAction(BaseActionView):
  50. action_name = "delete_selected"
  51. description = _(u'Delete selected %(verbose_name_plural)s')
  52. delete_confirmation_template = None
  53. delete_selected_confirmation_template = None
  54. delete_models_batch = True
  55. model_perm = 'delete'
  56. icon = 'fa fa-times'
  57. @filter_hook
  58. def delete_models(self, queryset):
  59. n = queryset.count()
  60. if n:
  61. if self.delete_models_batch:
  62. self.log('delete', _('Batch delete %(count)d %(items)s.') % {"count": n, "items": model_ngettext(self.opts, n)})
  63. queryset.delete()
  64. else:
  65. for obj in queryset:
  66. self.log('delete', '', obj)
  67. obj.delete()
  68. self.message_user(_("Successfully deleted %(count)d %(items)s.") % {
  69. "count": n, "items": model_ngettext(self.opts, n)
  70. }, 'success')
  71. @filter_hook
  72. def do_action(self, queryset):
  73. # Check that the user has delete permission for the actual model
  74. if not self.has_delete_permission():
  75. raise PermissionDenied
  76. # Populate deletable_objects, a data structure of all related objects that
  77. # will also be deleted.
  78. if django_version > (2, 1):
  79. deletable_objects, model_count, perms_needed, protected = get_deleted_objects(
  80. queryset, self.opts, self.admin_site)
  81. else:
  82. using = router.db_for_write(self.model)
  83. deletable_objects, model_count, perms_needed, protected = get_deleted_objects(
  84. queryset, self.opts, self.user, self.admin_site, using)
  85. # The user has already confirmed the deletion.
  86. # Do the deletion and return a None to display the change list view again.
  87. if self.request.POST.get('post'):
  88. if perms_needed:
  89. raise PermissionDenied
  90. self.delete_models(queryset)
  91. # Return None to display the change list page again.
  92. return None
  93. if len(queryset) == 1:
  94. objects_name = force_text(self.opts.verbose_name)
  95. else:
  96. objects_name = force_text(self.opts.verbose_name_plural)
  97. if perms_needed or protected:
  98. title = _("Cannot delete %(name)s") % {"name": objects_name}
  99. else:
  100. title = _("Are you sure?")
  101. context = self.get_context()
  102. context.update({
  103. "title": title,
  104. "objects_name": objects_name,
  105. "deletable_objects": [deletable_objects],
  106. 'queryset': queryset,
  107. "perms_lacking": perms_needed,
  108. "protected": protected,
  109. "opts": self.opts,
  110. "app_label": self.app_label,
  111. 'action_checkbox_name': ACTION_CHECKBOX_NAME,
  112. })
  113. # Display the confirmation page
  114. return TemplateResponse(self.request, self.delete_selected_confirmation_template or
  115. self.get_template_list('views/model_delete_selected_confirm.html'), context)
  116. class ActionPlugin(BaseAdminPlugin):
  117. # Actions
  118. actions = []
  119. actions_selection_counter = True
  120. global_actions = [DeleteSelectedAction]
  121. def init_request(self, *args, **kwargs):
  122. self.actions = self.get_actions()
  123. return bool(self.actions)
  124. def get_list_display(self, list_display):
  125. if self.actions:
  126. list_display.insert(0, 'action_checkbox')
  127. self.admin_view.action_checkbox = action_checkbox
  128. return list_display
  129. def get_list_display_links(self, list_display_links):
  130. if self.actions:
  131. if len(list_display_links) == 1 and list_display_links[0] == 'action_checkbox':
  132. return list(self.admin_view.list_display[1:2])
  133. return list_display_links
  134. def get_context(self, context):
  135. if self.actions and self.admin_view.result_count:
  136. av = self.admin_view
  137. selection_note_all = ungettext('%(total_count)s selected',
  138. 'All %(total_count)s selected', av.result_count)
  139. new_context = {
  140. 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(av.result_list)},
  141. 'selection_note_all': selection_note_all % {'total_count': av.result_count},
  142. 'action_choices': self.get_action_choices(),
  143. 'actions_selection_counter': self.actions_selection_counter,
  144. }
  145. context.update(new_context)
  146. return context
  147. def post_response(self, response, *args, **kwargs):
  148. request = self.admin_view.request
  149. av = self.admin_view
  150. # Actions with no confirmation
  151. if self.actions and 'action' in request.POST:
  152. action = request.POST['action']
  153. if action not in self.actions:
  154. msg = _("Items must be selected in order to perform "
  155. "actions on them. No items have been changed.")
  156. av.message_user(msg)
  157. else:
  158. ac, name, description, icon = self.actions[action]
  159. select_across = request.POST.get('select_across', False) == '1'
  160. selected = request.POST.getlist(ACTION_CHECKBOX_NAME)
  161. if not selected and not select_across:
  162. # Reminder that something needs to be selected or nothing will happen
  163. msg = _("Items must be selected in order to perform "
  164. "actions on them. No items have been changed.")
  165. av.message_user(msg)
  166. else:
  167. queryset = av.list_queryset._clone()
  168. if not select_across:
  169. # Perform the action only on the selected objects
  170. queryset = av.list_queryset.filter(pk__in=selected)
  171. response = self.response_action(ac, queryset)
  172. # Actions may return an HttpResponse, which will be used as the
  173. # response from the POST. If not, we'll be a good little HTTP
  174. # citizen and redirect back to the changelist page.
  175. if isinstance(response, HttpResponse):
  176. return response
  177. else:
  178. return HttpResponseRedirect(request.get_full_path())
  179. return response
  180. def response_action(self, ac, queryset):
  181. if isinstance(ac, type) and issubclass(ac, BaseActionView):
  182. action_view = self.get_model_view(ac, self.admin_view.model)
  183. action_view.init_action(self.admin_view)
  184. return action_view.do_action(queryset)
  185. else:
  186. return ac(self.admin_view, self.request, queryset)
  187. def get_actions(self):
  188. if self.actions is None:
  189. return OrderedDict()
  190. actions = [self.get_action(action) for action in self.global_actions]
  191. for klass in self.admin_view.__class__.mro()[::-1]:
  192. class_actions = getattr(klass, 'actions', [])
  193. if not class_actions:
  194. continue
  195. actions.extend(
  196. [self.get_action(action) for action in class_actions])
  197. # get_action might have returned None, so filter any of those out.
  198. actions = filter(None, actions)
  199. if six.PY3:
  200. actions = list(actions)
  201. # Convert the actions into a OrderedDict keyed by name.
  202. actions = OrderedDict([
  203. (name, (ac, name, desc, icon))
  204. for ac, name, desc, icon in actions
  205. ])
  206. return actions
  207. def get_action_choices(self):
  208. """
  209. Return a list of choices for use in a form object. Each choice is a
  210. tuple (name, description).
  211. """
  212. choices = []
  213. for ac, name, description, icon in self.actions.values():
  214. choice = (name, description % model_format_dict(self.opts), icon)
  215. choices.append(choice)
  216. return choices
  217. def get_action(self, action):
  218. if isinstance(action, type) and issubclass(action, BaseActionView):
  219. if not action.has_perm(self.admin_view):
  220. return None
  221. return action, getattr(action, 'action_name'), getattr(action, 'description'), getattr(action, 'icon')
  222. elif callable(action):
  223. func = action
  224. action = action.__name__
  225. elif hasattr(self.admin_view.__class__, action):
  226. func = getattr(self.admin_view.__class__, action)
  227. else:
  228. return None
  229. if hasattr(func, 'short_description'):
  230. description = func.short_description
  231. else:
  232. description = capfirst(action.replace('_', ' '))
  233. return func, action, description, getattr(func, 'icon', 'tasks')
  234. # View Methods
  235. def result_header(self, item, field_name, row):
  236. if item.attr and field_name == 'action_checkbox':
  237. item.classes.append("action-checkbox-column")
  238. return item
  239. def result_item(self, item, obj, field_name, row):
  240. if item.field is None and field_name == u'action_checkbox':
  241. item.classes.append("action-checkbox")
  242. return item
  243. # Media
  244. def get_media(self, media):
  245. if self.actions and self.admin_view.result_count:
  246. media = media + self.vendor('xadmin.plugin.actions.js', 'xadmin.plugins.css')
  247. return media
  248. # Block Views
  249. def block_results_bottom(self, context, nodes):
  250. if self.actions and self.admin_view.result_count:
  251. nodes.append(loader.render_to_string('xadmin/blocks/model_list.results_bottom.actions.html',
  252. context=get_context_dict(context)))
  253. site.register_plugin(ActionPlugin, ListAdminView)