summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArtyom Kotovskiy <artyomkotovskiy@gmail.com>2026-04-25 00:00:31 -0400
committerJacob Walls <jacobtylerwalls@gmail.com>2026-04-28 13:44:04 -0400
commit5b3cfce51770f46c6dc100e9be7f199a37176762 (patch)
tree100cdc72934c5992d34f75ab43a085f6ba3dadc5
parentf3ff680c768a313d34eb2e15eb7322edec60920c (diff)
Refs #15759 -- Fixed ModelAdmin.list_editable form submission for non-editable instances.
Added formset that excludes objects for which user has no permission for POST formset as well. Fixed regression test: the test was not simulating real behaviour properly. By providing full form data for the post request we skipped the part where the user was actually limited in permissions and only modified some of the rows. Improved tests by getting rid of obj.id % 2 approach for granting permissions per object for users, since it is not the safest. Instead granting permissions simply by 'alive' parameter, which is simpler and more stable. Bug in 84db026228413dda4cd195464554d51c0b208e32.
-rw-r--r--django/contrib/admin/options.py13
-rw-r--r--tests/admin_views/admin.py2
-rw-r--r--tests/admin_views/tests.py36
3 files changed, 30 insertions, 21 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index e5502c42d5..e05881b16a 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -2026,13 +2026,16 @@ class ModelAdmin(BaseModelAdmin):
return queryset
return queryset.filter(pk__in=object_pks)
- def _get_formset_with_permissions(self, request, queryset):
+ def _get_formset_with_permissions(self, request, queryset, for_save=False):
"""
Construct a changelist formset, and remove list_editable fields
for objects the user cannot change.
"""
FormSet = self.get_changelist_formset(request)
- formset = FormSet(queryset=queryset)
+ if for_save:
+ formset = FormSet(data=request.POST, files=request.FILES, queryset=queryset)
+ else:
+ formset = FormSet(queryset=queryset)
for form in formset.forms:
if not self.has_change_permission(request, form.instance):
@@ -2158,7 +2161,11 @@ class ModelAdmin(BaseModelAdmin):
modified_objects = self._get_list_editable_queryset(
request, FormSet.get_default_prefix()
)
- cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects)
+ cl.formset = self._get_formset_with_permissions(
+ request,
+ queryset=modified_objects,
+ for_save=True,
+ )
if cl.formset.is_valid():
self._save_formset(request, cl.formset)
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 5e7a055ec3..d0448a1b64 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -385,7 +385,7 @@ class PersonNoChangePermissionsAdmin9(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if obj is None:
return True
- return obj.id % 2 == 0
+ return obj.alive
class FooAccountAdmin(admin.StackedInline):
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 1fdb90822c..3dba13d185 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -5076,14 +5076,14 @@ class AdminViewListEditable(TestCase):
self.client.force_login(self.superuser)
response = self.client.get(reverse("admin9:admin_views_person_changelist"))
- # Editable fields present
- self.assertContains(response, 'name="form-1-gender"')
- self.assertContains(response, 'name="form-1-alive"')
- # Non-editable fields should NOT have inputs
- self.assertNotContains(response, 'name="form-0-gender"')
- self.assertNotContains(response, 'name="form-0-alive"')
- self.assertNotContains(response, 'name="form-2-gender"')
- self.assertNotContains(response, 'name="form-2-alive"')
+ # Non-editable fields should NOT have inputs.
+ self.assertNotContains(response, 'name="form-1-gender"')
+ self.assertNotContains(response, 'name="form-1-alive"')
+ # Editable fields are present.
+ self.assertContains(response, 'name="form-0-gender"')
+ self.assertContains(response, 'name="form-0-alive"')
+ self.assertContains(response, 'name="form-2-gender"')
+ self.assertContains(response, 'name="form-2-alive"')
def test_list_editable_per_object_permissions_submission(self):
"""
@@ -5092,26 +5092,28 @@ class AdminViewListEditable(TestCase):
"""
self.client.logout()
self.client.force_login(self.superuser)
-
+ # Skip the instance lacking edit permission (include only its id).
data = {
"form-TOTAL_FORMS": "3",
"form-INITIAL_FORMS": "3",
"form-MAX_NUM_FORMS": "0",
- "form-0-gender": "2", # Change per1 (not allowed)
+ "form-0-gender": "2",
+ "form-0-alive": "checked",
"form-0-id": str(self.per1.pk),
- "form-1-gender": "2", # Change per2 (allowed)
- "form-1-id": str(self.per2.pk),
- "form-2-gender": "2", # Change per3 (not allowed)
+ "form-1-id": str(self.per2.pk), # not editable
+ "form-2-gender": "2",
+ "form-2-alive": "checked",
"form-2-id": str(self.per3.pk),
"_save": "Save",
}
response = self.client.post(
reverse("admin9:admin_views_person_changelist"), data, follow=True
)
- # per2 and per3 were updated, but per1 was not
- self.assertEqual(Person.objects.get(pk=self.per1.pk).gender, 1) # Unchanged
- self.assertEqual(Person.objects.get(pk=self.per2.pk).gender, 2)
- self.assertEqual(Person.objects.get(pk=self.per3.pk).gender, 1) # Unchanged
+ # per1 and per3 were updated, but per2 was not.
+ self.assertEqual(Person.objects.get(pk=self.per1.pk).gender, 2)
+ self.assertEqual(Person.objects.get(pk=self.per2.pk).gender, 1) # Unchanged
+ self.assertEqual(Person.objects.get(pk=self.per3.pk).gender, 2)
+
# Check for success message
self.assertEqual(len(response.context["messages"]), 1)