From fa2a3de6ede10b005fc2c1d23f4cffb53eaec425 Mon Sep 17 00:00:00 2001 From: Rodrigo Vieira Date: Wed, 22 Apr 2026 18:53:27 -0300 Subject: Fixed #10919 -- Added delete_confirmation_max_display to ModelAdmin. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new ModelAdmin.delete_confirmation_max_display attribute allows limiting the number of related objects shown on the delete confirmation page. When the limit is reached, a "…and N more objects." message is shown. The feature relies on a new truncated_unordered_list template filter added to django.contrib.admin.templatetags.admin_filters. Thanks Jacob Tyler Walls for the review and guidance, Tobias McNulty for the report, and terminator14 for the solution suggested. --- tests/admin_views/customadmin.py | 16 +++++++++++++- tests/admin_views/test_actions.py | 13 ++++++++++++ tests/admin_views/tests.py | 20 ++++++++++++++++++ tests/admin_views/urls.py | 1 + tests/modeladmin/test_checks.py | 44 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/admin_views/customadmin.py b/tests/admin_views/customadmin.py index a63d24a9ee..9621cb4d52 100644 --- a/tests/admin_views/customadmin.py +++ b/tests/admin_views/customadmin.py @@ -60,8 +60,19 @@ class CustomPwdTemplateUserAdmin(UserAdmin): class BookAdmin(admin.ModelAdmin): + delete_confirmation_max_display = 1 + def get_deleted_objects(self, objs, request): - return ["a deletable object"], {"books": 1}, set(), [] + return ( + ["a deletable object", "another object", "last object"], + {"books": 1}, + set(), + [], + ) + + +class BookAdminZeroDisplay(BookAdmin): + delete_confirmation_max_display = 0 site = Admin2(name="admin2") @@ -80,3 +91,6 @@ site.register(models.Simple, base_admin.AttributeErrorRaisingAdmin) simple_site = Admin2(name="admin4") simple_site.register(User, CustomPwdTemplateUserAdmin) + +zero_display_site = Admin2(name="admin_zero_display") +zero_display_site.register(models.Book, BookAdminZeroDisplay) diff --git a/tests/admin_views/test_actions.py b/tests/admin_views/test_actions.py index c2c18e0b74..de5f5462a3 100644 --- a/tests/admin_views/test_actions.py +++ b/tests/admin_views/test_actions.py @@ -220,6 +220,19 @@ class AdminActionsTest(TestCase): # BookAdmin.get_deleted_objects() returns custom text. self.assertContains(response, "a deletable object") + def test_delete_selected_uses_delete_confirmation_max_display(self): + book = Book.objects.create(name="Test Book") + data = { + ACTION_CHECKBOX_NAME: [book.pk], + "action": "delete_selected", + "index": 0, + } + response = self.client.post(reverse("admin2:admin_views_book_changelist"), data) + self.assertContains(response, "a deletable object") + self.assertContains(response, "…and 2 more objects.") + self.assertNotContains(response, "another object") + self.assertNotContains(response, "last object") + def test_custom_function_mail_action(self): """A custom action may be defined in a function.""" action_data = { diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 399c485fea..1fdb90822c 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -4198,6 +4198,26 @@ class AdminViewDeletedObjectsTest(TestCase): # BookAdmin.get_deleted_objects() returns custom text. self.assertContains(response, "a deletable object") + def test_delete_view_uses_delete_confirmation_max_display(self): + book = Book.objects.create(name="Test Book") + response = self.client.get( + reverse("admin2:admin_views_book_delete", args=(book.pk,)) + ) + self.assertContains(response, "a deletable object") + self.assertContains(response, "…and 2 more objects.") + self.assertNotContains(response, "another object") + self.assertNotContains(response, "last object") + + def test_delete_view_hides_objects_when_delete_confirmation_max_display_is_zero( + self, + ): + book = Book.objects.create(name="Test Book") + response = self.client.get( + reverse("admin_zero_display:admin_views_book_delete", args=(book.pk,)) + ) + self.assertNotContains(response, "

Objects

") + self.assertNotContains(response, 'id="deleted-objects"') + @override_settings(ROOT_URLCONF="admin_views.urls") class TestGenericRelations(TestCase): diff --git a/tests/admin_views/urls.py b/tests/admin_views/urls.py index 3c43b8721d..0ebe688cfc 100644 --- a/tests/admin_views/urls.py +++ b/tests/admin_views/urls.py @@ -35,6 +35,7 @@ urlpatterns = [ path("test_admin/admin11/", admin.site11.urls), path("test_admin/admin12/", admin.site12.urls), path("test_admin/admin13/", admin.site13.urls), + path("test_admin/admin_zero_display/", customadmin.zero_display_site.urls), path("test_admin/has_permission_admin/", custom_has_permission_admin.site.urls), path("test_admin/autocomplete_admin/", autocomplete_site.urls), # Shares the admin URL prefix. diff --git a/tests/modeladmin/test_checks.py b/tests/modeladmin/test_checks.py index c493148eb9..496e46e8b0 100644 --- a/tests/modeladmin/test_checks.py +++ b/tests/modeladmin/test_checks.py @@ -1020,6 +1020,50 @@ class ListMaxShowAllCheckTests(CheckTestCase): self.assertIsValid(TestModelAdmin, ValidationTestModel) +class DeleteConfirmationMaxObjectsCheckTests(CheckTestCase): + def test_not_integer(self): + class TestModelAdmin(ModelAdmin): + delete_confirmation_max_display = "hello" + + self.assertIsInvalid( + TestModelAdmin, + ValidationTestModel, + ( + "The value of " + "'delete_confirmation_max_display'" + " must be a non-negative integer or None." + ), + "admin.E131", + ) + + def test_negative_integer(self): + class TestModelAdmin(ModelAdmin): + delete_confirmation_max_display = -1 + + self.assertIsInvalid( + TestModelAdmin, + ValidationTestModel, + ( + "The value of " + "'delete_confirmation_max_display'" + " must be a non-negative integer or None." + ), + "admin.E131", + ) + + def test_valid_case(self): + class TestModelAdmin(ModelAdmin): + delete_confirmation_max_display = 100 + + self.assertIsValid(TestModelAdmin, ValidationTestModel) + + def test_valid_none(self): + class TestModelAdmin(ModelAdmin): + delete_confirmation_max_display = None + + self.assertIsValid(TestModelAdmin, ValidationTestModel) + + class SearchFieldsCheckTests(CheckTestCase): def test_not_iterable(self): class TestModelAdmin(ModelAdmin): -- cgit v1.3