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 %} {% 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.