Fixed #12090 -- Show admin actions on the edit pages too

This commit is contained in:
Marcelo Galigniana 2022-08-29 23:19:37 -03:00
parent 0010721e89
commit 07a2b76d16
7 changed files with 114 additions and 11 deletions

View File

@ -1185,7 +1185,7 @@ class ModelAdminChecks(BaseModelAdminChecks):
Actions with an allowed_permission attribute require the ModelAdmin to
implement a has_<perm>_permission() method for each permission.
"""
actions = obj._get_base_actions()
actions = obj._get_base_actions(obj.actions)
errors = []
for func, name, _ in actions:
if not hasattr(func, "allowed_permissions"):
@ -1210,7 +1210,9 @@ class ModelAdminChecks(BaseModelAdminChecks):
def _check_actions_uniqueness(self, obj):
"""Check that every action has a unique __name__."""
errors = []
names = collections.Counter(name for _, name, _ in obj._get_base_actions())
names = collections.Counter(
name for _, name, _ in obj._get_base_actions(obj.actions)
)
for name, count in names.items():
if count > 1:
errors.append(

View File

@ -47,7 +47,7 @@ from django.forms.models import (
modelformset_factory,
)
from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
from django.http import HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect
from django.http.response import HttpResponseBase
from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.urls import reverse
@ -648,6 +648,8 @@ class ModelAdmin(BaseModelAdmin):
actions_selection_counter = True
checks_class = ModelAdminChecks
change_actions = ()
def __init__(self, model, admin_site):
self.model = model
self.opts = model._meta
@ -973,10 +975,10 @@ class ModelAdmin(BaseModelAdmin):
def _get_action_description(func, name):
return getattr(func, "short_description", capfirst(name.replace("_", " ")))
def _get_base_actions(self):
def _get_base_actions(self, class_actions):
"""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) for action in class_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}
@ -1016,12 +1018,22 @@ 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(self.actions)
)
return {name: (func, name, desc) for func, name, desc in actions}
def get_change_actions(self, request):
if not self.change_actions:
return {}
actions = self._filter_actions_by_permissions(
request, self._get_base_actions(self.change_actions)
)
return {name: (func, name, desc) for func, name, desc in actions}
def get_action_choices(self, request, 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).
"""
choices = [] + default_choices
@ -1781,7 +1793,18 @@ class ModelAdmin(BaseModelAdmin):
ModelForm = self.get_form(
request, obj, change=not add, fields=flatten_fieldsets(fieldsets)
)
actions = self.get_change_actions(request)
if request.method == "POST":
for name in actions:
if name in request.POST:
func, name, desc = actions[name]
response = func(self, request, obj)
msg = _('Executed action "%s"' % desc)
self.message_user(request, msg)
if isinstance(response, HttpResponse):
return response
else:
return HttpResponseRedirect(request.get_full_path())
form = ModelForm(request.POST, request.FILES, instance=obj)
formsets, inline_instances = self._create_formsets(
request,
@ -1861,6 +1884,7 @@ class ModelAdmin(BaseModelAdmin):
"inline_admin_formsets": inline_formsets,
"errors": helpers.AdminErrorList(form, formsets),
"preserved_filters": self.get_preserved_filters(request),
"actions": actions,
}
# Hide the "Save" and "Save and continue" buttons if "Save as New" was

View File

@ -36,6 +36,7 @@
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% include "admin/includes/edit_actions.html" %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
{% if errors %}

View File

@ -0,0 +1,7 @@
{% if actions %}
<div class="actions">
{% for action in actions.values %}
<input type="submit" name="{{action.1}}" value="{{action.2}}">
{% endfor %}
</div>
{% endif %}

View File

@ -418,8 +418,26 @@ def no_perm(modeladmin, request, selected):
return HttpResponse(content="No permission to perform this action", status=403)
@admin.action(description="External mail (Another awesome action)")
def change_external_mail(modeladmin, request, selected):
EmailMessage(
"Greetings from a function action",
"This is the test email from a function action",
selected.email,
["to@example.com"],
).send()
@admin.action(description="Redirect to (Awesome action)")
def change_redirect_to(modeladmin, request, selected):
from django.http import HttpResponseRedirect
return HttpResponseRedirect(f"/some-where-else/{selected.id}")
class ExternalSubscriberAdmin(admin.ModelAdmin):
actions = [redirect_to, external_mail, download, no_perm]
change_actions = [change_redirect_to, change_external_mail]
class PodcastAdmin(admin.ModelAdmin):

View File

@ -476,6 +476,57 @@ action)</option>
self.assertIn(r"&quot;obj\\&quot;", output)
@override_settings(ROOT_URLCONF="admin_views.urls")
class AdminChangeActionsTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(
username="super", password="secret", email="super@example.com"
)
cls.s1 = ExternalSubscriber.objects.create(
name="John Doe", email="john@example.org"
)
def setUp(self):
self.client.force_login(self.superuser)
def test_available_actions(self):
response = self.client.get(
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk])
)
self.assertContains(
response,
"""<div class="actions">
<input type="submit" name="delete_selected"
value="Delete selected %(verbose_name_plural)s">
<input type="submit" name="change_redirect_to"
value="Redirect to (Awesome action)">
<input type="submit" name="change_external_mail"
value="External mail (Another awesome action)">
</div>""",
html=True,
)
def test_custom_function_mail_change_action(self):
self.client.post(
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
{
"change_external_mail": "External mail (Another awesome action)",
},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Greetings from a function action")
self.assertEqual(mail.outbox[0].from_email, self.s1.email)
def test_custom_function_change_action_with_redirect(self):
response = self.client.post(
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
{"change_redirect_to": "Redirect to (Awesome action)"},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f"/some-where-else/{self.s1.pk}")
@override_settings(ROOT_URLCONF="admin_views.urls")
class AdminActionsPermissionTests(TestCase):
@classmethod

View File

@ -79,11 +79,11 @@ class AdminActionsTests(TestCase):
actions = None
ma1 = AdminA(Band, admin.AdminSite())
action_names = [name for _, name, _ in ma1._get_base_actions()]
action_names = [name for _, name, _ in ma1._get_base_actions(ma1.actions)]
self.assertEqual(action_names, ["delete_selected", "custom_action"])
# `actions = None` removes actions from superclasses.
ma2 = AdminB(Band, admin.AdminSite())
action_names = [name for _, name, _ in ma2._get_base_actions()]
action_names = [name for _, name, _ in ma2._get_base_actions(ma2.actions)]
self.assertEqual(action_names, ["delete_selected"])
def test_global_actions_description(self):
@ -104,7 +104,7 @@ class AdminActionsTests(TestCase):
ma = BandAdmin(Band, admin_site)
self.assertEqual(
[description for _, _, description in ma._get_base_actions()],
[description for _, _, description in ma._get_base_actions(ma.actions)],
[
"Delete selected %(verbose_name_plural)s",
"Site-wide admin action 1.",
@ -140,7 +140,7 @@ class AdminActionsTests(TestCase):
self.assertEqual(
[
desc
for _, name, desc in ma._get_base_actions()
for _, name, desc in ma._get_base_actions(ma.actions)
if name.startswith("custom_action")
],
[