mirror of
https://github.com/django/django.git
synced 2024-12-23 01:25:58 +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.decorators import action
|
||||||
from django.contrib.admin.utils import model_ngettext
|
from django.contrib.admin.utils import model_ngettext
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.utils.translation import gettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
@ -14,7 +15,8 @@ from django.utils.translation import gettext_lazy
|
|||||||
|
|
||||||
@action(
|
@action(
|
||||||
permissions=["delete"],
|
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):
|
def delete_selected(modeladmin, request, queryset):
|
||||||
"""
|
"""
|
||||||
@ -29,6 +31,11 @@ def delete_selected(modeladmin, request, queryset):
|
|||||||
opts = modeladmin.model._meta
|
opts = modeladmin.model._meta
|
||||||
app_label = opts.app_label
|
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
|
# Populate deletable_objects, a data structure of all related objects that
|
||||||
# will also be deleted.
|
# 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::
|
Conveniently add attributes to an action function::
|
||||||
|
|
||||||
@admin.action(
|
@admin.action(
|
||||||
permissions=['publish'],
|
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):
|
def make_published(self, request, queryset):
|
||||||
queryset.update(status='p')
|
queryset.update(status='p')
|
||||||
@ -15,7 +18,8 @@ def action(function=None, *, permissions=None, description=None):
|
|||||||
def make_published(self, request, queryset):
|
def make_published(self, request, queryset):
|
||||||
queryset.update(status='p')
|
queryset.update(status='p')
|
||||||
make_published.allowed_permissions = ['publish']
|
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):
|
def decorator(func):
|
||||||
@ -23,6 +27,11 @@ def action(function=None, *, permissions=None, description=None):
|
|||||||
func.allowed_permissions = permissions
|
func.allowed_permissions = permissions
|
||||||
if description is not None:
|
if description is not None:
|
||||||
func.short_description = description
|
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
|
return func
|
||||||
|
|
||||||
if function is None:
|
if function is None:
|
||||||
|
@ -1032,16 +1032,21 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
|
return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_action_description(func, name):
|
def _get_action_description(func, name, change=False):
|
||||||
try:
|
try:
|
||||||
return func.short_description
|
if change:
|
||||||
|
return func.short_description
|
||||||
|
else:
|
||||||
|
return func.plural_description
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return capfirst(name.replace("_", " "))
|
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."""
|
"""Return the list of actions, prior to any request-based filtering."""
|
||||||
actions = []
|
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.
|
# get_action might have returned None, so filter any of those out.
|
||||||
base_actions = [action for action in base_actions if action]
|
base_actions = [action for action in base_actions if action]
|
||||||
base_action_names = {name for _, name, _ in base_actions}
|
base_action_names = {name for _, name, _ in base_actions}
|
||||||
@ -1050,7 +1055,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
for name, func in self.admin_site.actions:
|
for name, func in self.admin_site.actions:
|
||||||
if name in base_action_names:
|
if name in base_action_names:
|
||||||
continue
|
continue
|
||||||
description = self._get_action_description(func, name)
|
description = self._get_action_description(func, name, change)
|
||||||
actions.append((func, name, description))
|
actions.append((func, name, description))
|
||||||
# Add actions from this ModelAdmin.
|
# Add actions from this ModelAdmin.
|
||||||
actions.extend(base_actions)
|
actions.extend(base_actions)
|
||||||
@ -1072,7 +1077,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
filtered_actions.append(action)
|
filtered_actions.append(action)
|
||||||
return filtered_actions
|
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
|
Return a dictionary mapping the names of all actions for this
|
||||||
ModelAdmin to a tuple of (callable, name, description) for each action.
|
ModelAdmin to a tuple of (callable, name, description) for each action.
|
||||||
@ -1081,21 +1086,25 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
# this page.
|
# this page.
|
||||||
if self.actions is None or IS_POPUP_VAR in request.GET:
|
if self.actions is None or IS_POPUP_VAR in request.GET:
|
||||||
return {}
|
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}
|
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
|
Return a list of choices for use in a form object. Each choice is a
|
||||||
tuple (name, description).
|
tuple (name, description).
|
||||||
"""
|
"""
|
||||||
choices = [] + default_choices
|
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))
|
choice = (name, description % model_format_dict(self.opts))
|
||||||
choices.append(choice)
|
choices.append(choice)
|
||||||
return choices
|
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,
|
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
|
or the name of a method on the ModelAdmin. Return is a tuple of
|
||||||
@ -1119,7 +1128,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
description = self._get_action_description(func, action)
|
description = self._get_action_description(func, action, change)
|
||||||
return func, action, description
|
return func, action, description
|
||||||
|
|
||||||
def get_list_display(self, request):
|
def get_list_display(self, request):
|
||||||
@ -1948,11 +1957,9 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
# Build the action form and populate it with available actions.
|
# Build the action form and populate it with available actions.
|
||||||
if actions and not add:
|
if actions and not add:
|
||||||
action_form = self.action_form(auto_id=None)
|
action_form = self.action_form(auto_id=None)
|
||||||
action_choices = self.get_action_choices(request)
|
action_form.fields["action"].choices = self.get_action_choices(
|
||||||
# Remove "delete" action; change view already has a button for
|
request, change=True
|
||||||
# that action.
|
)
|
||||||
action_choices.pop(1)
|
|
||||||
action_form.fields["action"].choices = action_choices
|
|
||||||
media += action_form.media
|
media += action_form.media
|
||||||
else:
|
else:
|
||||||
action_form = None
|
action_form = None
|
||||||
|
@ -411,7 +411,10 @@ def redirect_to(modeladmin, request, selected):
|
|||||||
return HttpResponseRedirect("/some-where-else/")
|
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):
|
def download(modeladmin, request, selected):
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@ subscribers</option>
|
|||||||
<option value="redirect_to">Redirect to (Awesome action)</option>
|
<option value="redirect_to">Redirect to (Awesome action)</option>
|
||||||
<option value="external_mail">External mail (Another awesome
|
<option value="external_mail">External mail (Another awesome
|
||||||
action)</option>
|
action)</option>
|
||||||
<option value="download">Download subscription</option>
|
<option value="download">Download selected subscriptions</option>
|
||||||
<option value="no_perm">No permission to run</option>
|
<option value="no_perm">No permission to run</option>
|
||||||
</select>""",
|
</select>""",
|
||||||
html=True,
|
html=True,
|
||||||
@ -562,12 +562,8 @@ class AdminDetailActionsTest(TestCase):
|
|||||||
|
|
||||||
def test_available_detail_actions(self):
|
def test_available_detail_actions(self):
|
||||||
"""
|
"""
|
||||||
- Action 1 has not description.
|
'Download' action with singular description and
|
||||||
- Action 2 has custom description.
|
'Delete' action present in dropdown.
|
||||||
- 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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@ -578,15 +574,40 @@ class AdminDetailActionsTest(TestCase):
|
|||||||
response,
|
response,
|
||||||
"""<label>Action: <select name="action" required>
|
"""<label>Action: <select name="action" required>
|
||||||
<option value="" selected>---------</option>
|
<option value="" selected>---------</option>
|
||||||
|
<option value="delete_selected">Delete</option>
|
||||||
<option value="redirect_to">Redirect to (Awesome action)</option>
|
<option value="redirect_to">Redirect to (Awesome action)</option>
|
||||||
<option value="external_mail">External mail (Another awesome
|
<option value="external_mail">External mail (Another awesome action)
|
||||||
action)</option>
|
</option>
|
||||||
<option value="download">Download subscription</option>
|
<option value="download">Download subscription</option>
|
||||||
<option value="no_perm">No permission to run</option>
|
<option value="no_perm">No permission to run</option>
|
||||||
</select>""",
|
</select>""",
|
||||||
html=True,
|
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):
|
def test_detail_actions_are_not_present_in_add_view(self):
|
||||||
"""
|
"""
|
||||||
`add_view` inherits the same function as `change_view` but actions are
|
`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.
|
Edit the Subscriber and then choose an action won't save the changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
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(),
|
f"This is the content of the file written by {self.s1.name}".encode(),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
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