diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index bd37f519cd..e56357036a 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -457,6 +457,14 @@ class InlineAdminFormSet: def total_form_count(self): return self.formset.total_form_count + @property + def can_delete(self): + return ( + self.formset.can_delete + and self.has_delete_permission + and any(inlineadminform.original is not None for inlineadminform in self) + ) + @property def media(self): media = self.opts.media + self.formset.media diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index 494d4677fa..25c2879f5d 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -24,7 +24,7 @@ {% if field.help_text %}({{ field.help_text|striptags }}){% endif %} {% endfor %} - {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}{% translate "Delete?" %}{% endif %} + {% if inline_admin_formset.can_delete %}{% translate "Delete?" %}{% endif %} @@ -59,7 +59,7 @@ {% endfor %} {% endfor %} {% endfor %} - {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %} + {% if inline_admin_formset.can_delete %}{{ inline_admin_form.deletion_field.field }}{% endif %} {% endfor %} diff --git a/docs/intro/_images/admin11t.png b/docs/intro/_images/admin11t.png index 2dda5c0d05..a5b444f128 100644 Binary files a/docs/intro/_images/admin11t.png and b/docs/intro/_images/admin11t.png differ diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 4dbaaf8e22..7d30c48b82 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -829,6 +829,66 @@ class TestInline(TestDataMixin, TestCase): self.assertIs(parent.show_inlines, True) +@override_settings(ROOT_URLCONF="admin_inlines.urls") +class TestInlineCanDelete(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + username="tester", + password="password", + is_staff=True, + ) + # can_delete True case + pc_permission = Permission.objects.filter( + content_type=ContentType.objects.get_for_model(ProfileCollection) + ) + p_permission = Permission.objects.filter( + codename__in=["view_profile", "delete_profile"], + content_type=ContentType.objects.get_for_model(Profile), + ) + pc = ProfileCollection.objects.create() + Profile.objects.create(collection=pc, first_name="SiHyun", last_name="Lee") + # can_delete False case + sp_permission = Permission.objects.filter( + content_type=ContentType.objects.get_for_model(SomeParentModel) + ) + sc_permission = Permission.objects.filter( + codename__in=["view_somechildmodel"], + content_type=ContentType.objects.get_for_model(SomeChildModel), + ) + sp = SomeParentModel.objects.create(name="p") + SomeChildModel.objects.create(name="c", position="0", parent=sp) + cls.user.user_permissions.add( + *pc_permission, *p_permission, *sp_permission, *sc_permission + ) + + cls.pc_url = reverse( + "admin:admin_inlines_profilecollection_change", args=(pc.pk,) + ) + cls.sp_url = reverse( + "admin:admin_inlines_someparentmodel_change", args=(sp.pk,) + ) + + def setUp(self): + self.client.force_login(self.user) + + def test_tabular_inline_delete_field(self): + response = self.client.get(self.pc_url) + self.assertContains(response, "Delete?") + response = self.client.get(self.sp_url) + self.assertNotContains(response, "Delete?") + + def test_tabular_inline_delete_checkbox_layout(self): + response = self.client.get(self.pc_url) + self.assertContains( + response, + '', + ) + response = self.client.get(self.sp_url) + self.assertContains(response, '') + + @override_settings(ROOT_URLCONF="admin_inlines.urls") class TestInlineMedia(TestDataMixin, TestCase): def setUp(self): @@ -2220,13 +2280,11 @@ class SeleniumTests(AdminSeleniumTestCase): # Click on a few delete buttons self.selenium.find_element( By.CSS_SELECTOR, - "form#profilecollection_form tr.dynamic-profile_set#profile_set-1 " - "td.delete a", + "form#profilecollection_form tr.dynamic-profile_set#profile_set-1 " "a", ).click() self.selenium.find_element( By.CSS_SELECTOR, - "form#profilecollection_form tr.dynamic-profile_set#profile_set-2 " - "td.delete a", + "form#profilecollection_form tr.dynamic-profile_set#profile_set-2 " "a", ).click() # The rows are gone and the IDs have been re-sequenced self.assertCountSeleniumElements( @@ -2487,7 +2545,7 @@ class SeleniumTests(AdminSeleniumTestCase): "CREATION DATE", "UPDATE DATE", "UPDATED BY", - "DELETE?", + "", ], ) # There are no fieldset section names rendered.