Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2060,13 +2060,8 @@ def _changeform_view(self, request, object_id, form_url, extra_context):

else:
obj = self.get_object(request, unquote(object_id), to_field)

if request.method == "POST":
if not self.has_change_permission(request, obj):
raise PermissionDenied
else:
if not self.has_view_or_change_permission(request, obj):
raise PermissionDenied
if not self.has_view_or_change_permission(request, obj):
raise PermissionDenied

if obj is None:
return self._get_obj_does_not_exist_redirect(
Expand Down Expand Up @@ -2105,13 +2100,19 @@ def _changeform_view(self, request, object_id, form_url, extra_context):
and "_continue" not in request.POST
and "_addanother" not in request.POST
):
selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
if len(selected) != 1 or selected[0] != str(obj.pk):
raise BadRequest
queryset = self.model._default_manager.get_queryset()
if response := self.response_action(
request, queryset, action_location=ActionLocation.CHANGE_FORM
):
return response
return HttpResponseRedirect(request.get_full_path())

if not add and not self.has_change_permission(request, obj):
raise PermissionDenied

form = ModelForm(request.POST, request.FILES, instance=obj)
formsets, inline_instances = self._create_formsets(
request,
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<div>
{% if action_form and not is_popup %}
{% change_form_admin_actions %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ object_id|unlocalize }}">
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ original.pk|unlocalize }}">
{% endif %}
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
Expand Down
55 changes: 54 additions & 1 deletion tests/admin_views/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,10 @@ def test_popup_template_escaping(self):
self.assertIn(r"&quot;obj\\&quot;", output)


@override_settings(ROOT_URLCONF="admin_views.urls")
@override_settings(
ROOT_URLCONF="admin_views.urls",
MAILERS={"default": {"BACKEND": "django.core.mail.backends.locmem.EmailBackend"}},
)
class AdminActionsPermissionTests(TestCase):
@classmethod
def setUpTestData(cls):
Expand Down Expand Up @@ -568,6 +571,32 @@ def test_model_admin_no_delete_permission_externalsubscriber(self):
)
self.assertEqual(response.status_code, 403)

def test_minimum_permission(self):
permission = Permission.objects.get(codename="view_externalsubscriber")
self.user.user_permissions.add(permission)
es = ExternalSubscriber.objects.create(
name="John Doe", email="john@example.org"
)

cl_url = reverse("admin:admin_views_externalsubscriber_changelist")
cl_post = {ACTION_CHECKBOX_NAME: [es.pk], "action": "external_mail"}
cf_url = reverse("admin:admin_views_externalsubscriber_change", args=[es.pk])
cf_post = {ACTION_CHECKBOX_NAME: [es.pk], "CHANGE_FORM-action": "external_mail"}

for url, post_data in [(cl_url, cl_post), (cf_url, cf_post)]:
mail.outbox = []
with self.subTest(url=url):
response = self.client.get(url)
self.assertContains(
response,
'<option value="external_mail">'
"External mail (Another awesome action)</option>",
html=True,
)
response = self.client.post(url, post_data)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(response.status_code, 302)


@override_settings(
ROOT_URLCONF="admin_views.urls",
Expand Down Expand Up @@ -673,6 +702,30 @@ def test_run_action_permissions(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"OK")

def test_action_changeform_cannot_target_different_objects(self):
changeform_url = reverse(
"admin:admin_views_externalsubscriber_change", args=[self.s1.pk]
)
external_subscriber = ExternalSubscriber.objects.create(
name="Jane Austen", email="jane@example.org"
)
for invalid_checkbox_value in [
[external_subscriber.pk],
[self.s1.pk, external_subscriber.pk],
[],
]:
with self.subTest(invalid_checkbox_value=invalid_checkbox_value):
response = self.client.post(
changeform_url,
{
"CHANGE_FORM-action": "external_mail",
ACTION_CHECKBOX_NAME: invalid_checkbox_value,
"index": 0,
},
)
self.assertEqual(len(mail.outbox), 0)
self.assertEqual(response.status_code, 400)

def test_select_across_ignored(self):
ExternalSubscriber.objects.create(name="Bob Bobson", email="bob@example.org")
change_url = reverse(
Expand Down
Loading