Fixed #12090 -- Show admin actions on the edit pages too
This commit is contained in:
parent
0010721e89
commit
07a2b76d16
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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):
|
||||
|
|
|
@ -476,6 +476,57 @@ action)</option>
|
|||
self.assertIn(r""obj\\"", 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
|
||||
|
|
|
@ -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")
|
||||
],
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue