diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 9565cbc24d..9961ddd729 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1681,7 +1681,16 @@ class ModelAdmin(BaseModelAdmin): if isinstance(response, HttpResponseBase): return response else: - return HttpResponseRedirect(request.get_full_path()) + # If action was executed from change view, redirect to + # list view once finished. + return HttpResponseRedirect( + request.get_full_path() + if not change + else reverse( + "admin:%s_%s_changelist" + % (self.opts.app_label, self.opts.model_name) + ) + ) else: msg = _("No action selected.") self.message_user(request, msg, messages.WARNING) @@ -1873,12 +1882,15 @@ class ModelAdmin(BaseModelAdmin): if request.method == "POST": if actions and request.POST.get("action", ""): action_failed = False - response = self.response_action(request, obj, change=not add) + response = self.response_action( + request, + self.get_queryset(request).filter(id=object_id), + change=not add, + ) if response: return response else: action_failed = True - if action_failed: # Redirect back to the changelist page to avoid resubmitting the # form if the user refreshes the browser or uses the "No, take diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py index c1f9e10752..8ee7a8ff5e 100644 --- a/django/contrib/admin/templatetags/admin_urls.py +++ b/django/contrib/admin/templatetags/admin_urls.py @@ -86,14 +86,3 @@ def admin_actions_tag(parser, token): return InclusionAdminNode( parser, token, func=admin_actions, template_name="actions.html" ) - - -@register.tag(name="change_list_object_tools") -def change_list_object_tools_tag(parser, token): - """Display the row of change list object tools.""" - return InclusionAdminNode( - parser, - token, - func=lambda context: context, - template_name="change_list_object_tools.html", - ) diff --git a/docs/ref/contrib/admin/_images/adding-actions-to-the-modeladmin-detail-view.png b/docs/ref/contrib/admin/_images/adding-actions-to-the-modeladmin-detail-view.png new file mode 100644 index 0000000000..d2452e6728 Binary files /dev/null and b/docs/ref/contrib/admin/_images/adding-actions-to-the-modeladmin-detail-view.png differ diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 263d43f8a3..23b3c2a6b3 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -149,6 +149,24 @@ That code will give us an admin change list that looks something like this: .. image:: _images/adding-actions-to-the-modeladmin.png +The actions will be available also in the detail view but the description should be +adjusted to make sense we are talking about only one object:: + + @admin.action( + description="Mark selected story as published", + description_plural="Mark selected stories as published", + ) + def make_published(modeladmin, request, queryset): + pass + +It will looks like this: + +.. image:: _images/adding-actions-to-the-modeladmin-detail-view.png + +.. note:: + + In this case ``queryset`` will be a :class:`~django.db.models.query.QuerySet` of one object. + That's really all there is to it! If you're itching to write your own actions, you now know enough to get started. The rest of this document covers more advanced techniques. diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 1fd76b5c52..9bb6d4b3a5 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -6,6 +6,7 @@ from django import forms from django.contrib import admin from django.contrib.admin import BooleanFieldListFilter from django.contrib.admin.views.main import ChangeList +from django.contrib.auth import get_permission_codename from django.contrib.auth.admin import GroupAdmin, UserAdmin from django.contrib.auth.models import Group, User from django.core.exceptions import ValidationError @@ -416,11 +417,10 @@ def redirect_to(modeladmin, request, selected): description_plural="Download selected subscriptions", ) def download(modeladmin, request, selected): - from django.db.models.query import QuerySet - - if isinstance(selected, QuerySet): + if selected.count() > 1: buf = StringIO("This is the content of the file") else: + selected = selected.get() buf = StringIO(f"This is the content of the file written by {selected.name}") return StreamingHttpResponse(FileWrapper(buf)) @@ -431,8 +431,19 @@ def no_perm(modeladmin, request, selected): return HttpResponse(content="No permission to perform this action", status=403) +@admin.action(permissions=["custom"]) +def custom_action(modeladmin, request, selected): + return HttpResponse(content="OK", status=200) + + class ExternalSubscriberAdmin(admin.ModelAdmin): - actions = [redirect_to, external_mail, download, no_perm] + actions = [redirect_to, external_mail, download, no_perm, custom_action] + + def has_custom_permission(self, request): + """Does the user have the custom permission?""" + opts = self.opts + codename = get_permission_codename("custom", opts) + return request.user.has_perm("%s.%s" % (opts.app_label, codename)) class PodcastAdmin(admin.ModelAdmin): diff --git a/tests/admin_views/test_actions.py b/tests/admin_views/test_actions.py index 809a0a8261..77a67b0e6e 100644 --- a/tests/admin_views/test_actions.py +++ b/tests/admin_views/test_actions.py @@ -3,6 +3,7 @@ import json from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.views.main import IS_POPUP_VAR from django.contrib.auth.models import Permission, User +from django.contrib.contenttypes.models import ContentType from django.core import mail from django.db import connection from django.template.loader import render_to_string @@ -269,7 +270,9 @@ class AdminActionsTest(TestCase): reverse("admin:admin_views_externalsubscriber_changelist"), action_data ) content = b"".join(list(response)) - self.assertEqual(content, b"This is the content of the file") + self.assertEqual( + content, b"This is the content of the file written by John Doe" + ) self.assertEqual(response.status_code, 200) def test_custom_function_action_no_perm_response(self): @@ -294,13 +297,12 @@ class AdminActionsTest(TestCase): response, """