mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Add description_plural to admin action decorator
This commit is contained in:
parent
caae4f34f6
commit
3cef98620d
@ -7,6 +7,7 @@ from django.contrib.admin import helpers
|
||||
from django.contrib.admin.decorators import action
|
||||
from django.contrib.admin.utils import model_ngettext
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models.query import QuerySet
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
@ -14,7 +15,8 @@ from django.utils.translation import gettext_lazy
|
||||
|
||||
@action(
|
||||
permissions=["delete"],
|
||||
description=gettext_lazy("Delete selected %(verbose_name_plural)s"),
|
||||
description="Delete",
|
||||
description_plural=gettext_lazy("Delete selected %(verbose_name_plural)s"),
|
||||
)
|
||||
def delete_selected(modeladmin, request, queryset):
|
||||
"""
|
||||
@ -29,6 +31,11 @@ def delete_selected(modeladmin, request, queryset):
|
||||
opts = modeladmin.model._meta
|
||||
app_label = opts.app_label
|
||||
|
||||
# Handle the case when delete action is called from change view and
|
||||
# it is a single object
|
||||
if not isinstance(queryset, QuerySet):
|
||||
queryset = modeladmin.model.objects.filter(id=queryset.id)
|
||||
|
||||
# Populate deletable_objects, a data structure of all related objects that
|
||||
# will also be deleted.
|
||||
(
|
||||
|
@ -1,10 +1,13 @@
|
||||
def action(function=None, *, permissions=None, description=None):
|
||||
def action(
|
||||
function=None, *, permissions=None, description=None, description_plural=None
|
||||
):
|
||||
"""
|
||||
Conveniently add attributes to an action function::
|
||||
|
||||
@admin.action(
|
||||
permissions=['publish'],
|
||||
description='Mark selected stories as published',
|
||||
description='Mark story as published',
|
||||
description_plural='Mark selected stories as published',
|
||||
)
|
||||
def make_published(self, request, queryset):
|
||||
queryset.update(status='p')
|
||||
@ -15,7 +18,8 @@ def action(function=None, *, permissions=None, description=None):
|
||||
def make_published(self, request, queryset):
|
||||
queryset.update(status='p')
|
||||
make_published.allowed_permissions = ['publish']
|
||||
make_published.short_description = 'Mark selected stories as published'
|
||||
make_published.short_description = 'Mark story as published'
|
||||
make_published.plural_description = 'Mark selected stories as published'
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@ -23,6 +27,11 @@ def action(function=None, *, permissions=None, description=None):
|
||||
func.allowed_permissions = permissions
|
||||
if description is not None:
|
||||
func.short_description = description
|
||||
if description_plural is not None:
|
||||
func.plural_description = description_plural
|
||||
elif description is not None:
|
||||
func.plural_description = description
|
||||
|
||||
return func
|
||||
|
||||
if function is None:
|
||||
|
@ -1032,16 +1032,21 @@ class ModelAdmin(BaseModelAdmin):
|
||||
return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
|
||||
|
||||
@staticmethod
|
||||
def _get_action_description(func, name):
|
||||
def _get_action_description(func, name, change=False):
|
||||
try:
|
||||
return func.short_description
|
||||
if change:
|
||||
return func.short_description
|
||||
else:
|
||||
return func.plural_description
|
||||
except AttributeError:
|
||||
return capfirst(name.replace("_", " "))
|
||||
|
||||
def _get_base_actions(self):
|
||||
def _get_base_actions(self, change=False):
|
||||
"""Return the list of actions, prior to any request-based filtering."""
|
||||
actions = []
|
||||
base_actions = (self.get_action(action) for action in self.actions or [])
|
||||
base_actions = (
|
||||
self.get_action(action, change) for action in self.actions or []
|
||||
)
|
||||
# get_action might have returned None, so filter any of those out.
|
||||
base_actions = [action for action in base_actions if action]
|
||||
base_action_names = {name for _, name, _ in base_actions}
|
||||
@ -1050,7 +1055,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||
for name, func in self.admin_site.actions:
|
||||
if name in base_action_names:
|
||||
continue
|
||||
description = self._get_action_description(func, name)
|
||||
description = self._get_action_description(func, name, change)
|
||||
actions.append((func, name, description))
|
||||
# Add actions from this ModelAdmin.
|
||||
actions.extend(base_actions)
|
||||
@ -1072,7 +1077,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||
filtered_actions.append(action)
|
||||
return filtered_actions
|
||||
|
||||
def get_actions(self, request):
|
||||
def get_actions(self, request, change=False):
|
||||
"""
|
||||
Return a dictionary mapping the names of all actions for this
|
||||
ModelAdmin to a tuple of (callable, name, description) for each action.
|
||||
@ -1081,21 +1086,25 @@ class ModelAdmin(BaseModelAdmin):
|
||||
# this page.
|
||||
if self.actions is None or IS_POPUP_VAR in request.GET:
|
||||
return {}
|
||||
actions = self._filter_actions_by_permissions(request, self._get_base_actions())
|
||||
actions = self._filter_actions_by_permissions(
|
||||
request, self._get_base_actions(change)
|
||||
)
|
||||
return {name: (func, name, desc) for func, name, desc in actions}
|
||||
|
||||
def get_action_choices(self, request, default_choices=models.BLANK_CHOICE_DASH):
|
||||
def get_action_choices(
|
||||
self, request, change=False, default_choices=models.BLANK_CHOICE_DASH
|
||||
):
|
||||
"""
|
||||
Return a list of choices for use in a form object. Each choice is a
|
||||
tuple (name, description).
|
||||
"""
|
||||
choices = [] + default_choices
|
||||
for func, name, description in self.get_actions(request).values():
|
||||
for func, name, description in self.get_actions(request, change).values():
|
||||
choice = (name, description % model_format_dict(self.opts))
|
||||
choices.append(choice)
|
||||
return choices
|
||||
|
||||
def get_action(self, action):
|
||||
def get_action(self, action, change):
|
||||
"""
|
||||
Return a given action from a parameter, which can either be a callable,
|
||||
or the name of a method on the ModelAdmin. Return is a tuple of
|
||||
@ -1119,7 +1128,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
description = self._get_action_description(func, action)
|
||||
description = self._get_action_description(func, action, change)
|
||||
return func, action, description
|
||||
|
||||
def get_list_display(self, request):
|
||||
@ -1948,11 +1957,9 @@ class ModelAdmin(BaseModelAdmin):
|
||||
# Build the action form and populate it with available actions.
|
||||
if actions and not add:
|
||||
action_form = self.action_form(auto_id=None)
|
||||
action_choices = self.get_action_choices(request)
|
||||
# Remove "delete" action; change view already has a button for
|
||||
# that action.
|
||||
action_choices.pop(1)
|
||||
action_form.fields["action"].choices = action_choices
|
||||
action_form.fields["action"].choices = self.get_action_choices(
|
||||
request, change=True
|
||||
)
|
||||
media += action_form.media
|
||||
else:
|
||||
action_form = None
|
||||
|
@ -411,7 +411,10 @@ def redirect_to(modeladmin, request, selected):
|
||||
return HttpResponseRedirect("/some-where-else/")
|
||||
|
||||
|
||||
@admin.action(description="Download subscription")
|
||||
@admin.action(
|
||||
description="Download subscription",
|
||||
description_plural="Download selected subscriptions",
|
||||
)
|
||||
def download(modeladmin, request, selected):
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
@ -299,7 +299,7 @@ subscribers</option>
|
||||
<option value="redirect_to">Redirect to (Awesome action)</option>
|
||||
<option value="external_mail">External mail (Another awesome
|
||||
action)</option>
|
||||
<option value="download">Download subscription</option>
|
||||
<option value="download">Download selected subscriptions</option>
|
||||
<option value="no_perm">No permission to run</option>
|
||||
</select>""",
|
||||
html=True,
|
||||
@ -562,12 +562,8 @@ class AdminDetailActionsTest(TestCase):
|
||||
|
||||
def test_available_detail_actions(self):
|
||||
"""
|
||||
- Action 1 has not description.
|
||||
- Action 2 has custom description.
|
||||
- Action 3 ("Detail download") is not displayed because user does not
|
||||
have permission.
|
||||
- "Delete" action is not displayed in change view because already
|
||||
exists a button for it.
|
||||
'Download' action with singular description and
|
||||
'Delete' action present in dropdown.
|
||||
"""
|
||||
|
||||
response = self.client.get(
|
||||
@ -578,15 +574,40 @@ class AdminDetailActionsTest(TestCase):
|
||||
response,
|
||||
"""<label>Action: <select name="action" required>
|
||||
<option value="" selected>---------</option>
|
||||
<option value="delete_selected">Delete</option>
|
||||
<option value="redirect_to">Redirect to (Awesome action)</option>
|
||||
<option value="external_mail">External mail (Another awesome
|
||||
action)</option>
|
||||
<option value="external_mail">External mail (Another awesome action)
|
||||
</option>
|
||||
<option value="download">Download subscription</option>
|
||||
<option value="no_perm">No permission to run</option>
|
||||
</select>""",
|
||||
html=True,
|
||||
)
|
||||
|
||||
def test_available_list_actions(self):
|
||||
"""
|
||||
'Download' action with plural description.
|
||||
"""
|
||||
|
||||
response = self.client.get(
|
||||
reverse("admin:admin_views_externalsubscriber_changelist")
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"""<label>Action: <select name="action" required>
|
||||
<option value="" selected>---------</option>
|
||||
<option value="delete_selected">Delete selected external subscribers
|
||||
</option>
|
||||
<option value="redirect_to">Redirect to (Awesome action)</option>
|
||||
<option value="external_mail">External mail (Another awesome action)
|
||||
</option>
|
||||
<option value="download">Download selected subscriptions</option>
|
||||
<option value="no_perm">No permission to run</option>
|
||||
</select>""",
|
||||
html=True,
|
||||
)
|
||||
|
||||
def test_detail_actions_are_not_present_in_add_view(self):
|
||||
"""
|
||||
`add_view` inherits the same function as `change_view` but actions are
|
||||
@ -622,6 +643,7 @@ class AdminDetailActionsTest(TestCase):
|
||||
"""
|
||||
Edit the Subscriber and then choose an action won't save the changes.
|
||||
"""
|
||||
|
||||
self.client.post(
|
||||
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
||||
{
|
||||
@ -653,3 +675,24 @@ class AdminDetailActionsTest(TestCase):
|
||||
f"This is the content of the file written by {self.s1.name}".encode(),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_delete_action_in_detail_view(self):
|
||||
response = self.client.post(
|
||||
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
||||
{"action": "delete_selected"},
|
||||
)
|
||||
self.assertTrue(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
"Are you sure you want to delete the selected external subscriber?",
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
||||
{
|
||||
"action": "delete_selected",
|
||||
"post": "yes",
|
||||
},
|
||||
)
|
||||
self.assertTrue(response.status_code, 200)
|
||||
self.assertEqual(ExternalSubscriber.objects.count(), 0)
|
||||
|
Loading…
Reference in New Issue
Block a user