1
0
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:
Marcelo Galigniana 2023-10-01 03:50:49 +02:00
parent caae4f34f6
commit 3cef98620d
5 changed files with 99 additions and 30 deletions

View File

@ -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.
(

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)