From 64e1a271f50d921a54388539b6ff7102a31c3d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Mangin?= Date: Thu, 9 Aug 2018 11:43:55 -0400 Subject: [PATCH] =?UTF-8?q?Fixed=20#29637=20--=20Fixed=20admin=20change=20?= =?UTF-8?q?form=20crash=20if=20the=20user=20doesn=E2=80=99t=20have=20the?= =?UTF-8?q?=20add=20permission=20to=20a=20TabularInline.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression in 825f0beda804e48e9197fcf3b0d909f9f548aa47. --- django/contrib/admin/options.py | 14 ++++++++------ docs/releases/2.1.1.txt | 4 ++++ tests/admin_inlines/admin.py | 6 +++++- tests/admin_inlines/tests.py | 33 ++++++++++++++++++++++++--------- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 0f4dd93cb3..62dc32e788 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2055,12 +2055,6 @@ class InlineModelAdmin(BaseModelAdmin): can_add = self.has_add_permission(request, obj) if request else True class DeleteProtectedModelForm(base_model_form): - def __init__(self, *args, **kwargs): - super(DeleteProtectedModelForm, self).__init__(*args, **kwargs) - if not can_change and not self.instance._state.adding: - self.fields = {} - if not can_add and self.instance._state.adding: - self.fields = {} def hand_clean_DELETE(self): """ @@ -2097,6 +2091,14 @@ class InlineModelAdmin(BaseModelAdmin): self.hand_clean_DELETE() return result + def has_changed(self): + # Protect against unauthorized edits. + if not can_change and not self.instance._state.adding: + return False + if not can_add and self.instance._state.adding: + return False + return super().has_changed() + defaults['form'] = DeleteProtectedModelForm if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): diff --git a/docs/releases/2.1.1.txt b/docs/releases/2.1.1.txt index a24cbc47e2..a6ed58a7bb 100644 --- a/docs/releases/2.1.1.txt +++ b/docs/releases/2.1.1.txt @@ -24,3 +24,7 @@ Bugfixes * Fixed translation failure of ``DurationField``'s "overflow" error message (:ticket:`29623`). + +* Fixed a regression where the admin change form crashed if the user doesn't + have the 'add' permission to a model that uses ``TabularInline`` + (:ticket:`29637`). diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 7ac13ed3a4..b5f343a55b 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -74,6 +74,10 @@ class InnerInline2(admin.StackedInline): js = ('my_awesome_inline_scripts.js',) +class InnerInline2Tabular(admin.TabularInline): + model = Inner2 + + class CustomNumberWidget(forms.NumberInput): class Media: js = ('custom_number.js',) @@ -236,7 +240,7 @@ site.register(TitleCollection, inlines=[TitleInline]) # only ModelAdmin media site.register(Holder, HolderAdmin, inlines=[InnerInline]) # ModelAdmin and Inline media -site.register(Holder2, HolderAdmin, inlines=[InnerInline2]) +site.register(Holder2, HolderAdmin, inlines=[InnerInline2, InnerInline2Tabular]) # only Inline media site.register(Holder3, inlines=[InnerInline3]) diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 4ce744f4ef..749b3dd75f 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -588,9 +588,8 @@ class TestInlinePermissions(TestCase): self.author_book_auto_m2m_intermediate_id = author_book_auto_m2m_intermediate.pk holder = Holder2.objects.create(dummy=13) - inner2 = Inner2.objects.create(dummy=42, holder=holder) + self.inner2 = Inner2.objects.create(dummy=42, holder=holder) self.holder_change_url = reverse('admin:admin_inlines_holder2_change', args=(holder.id,)) - self.inner2_id = inner2.id self.client.force_login(self.user) @@ -684,7 +683,7 @@ class TestInlinePermissions(TestCase): ) self.assertNotContains( response, - '' % self.inner2_id, + '' % self.inner2.id, html=True ) @@ -693,7 +692,7 @@ class TestInlinePermissions(TestCase): self.user.user_permissions.add(permission) response = self.client.get(self.holder_change_url) # Change permission on inner2s, so we can change existing but not add new - self.assertContains(response, '

Inner2s

') + self.assertContains(response, '

Inner2s

', count=2) # Just the one form for existing instances self.assertContains( response, '', @@ -701,7 +700,7 @@ class TestInlinePermissions(TestCase): ) self.assertContains( response, - '' % self.inner2_id, + '' % self.inner2.id, html=True ) # max-num 0 means we can't add new ones @@ -710,6 +709,14 @@ class TestInlinePermissions(TestCase): '', html=True ) + # TabularInline + self.assertContains(response, 'Dummy', html=True) + self.assertContains( + response, + '' % self.inner2.dummy, + html=True, + ) def test_inline_change_fk_add_change_perm(self): permission = Permission.objects.get(codename='add_inner2', content_type=self.inner_ct) @@ -726,7 +733,7 @@ class TestInlinePermissions(TestCase): ) self.assertContains( response, - '' % self.inner2_id, + '' % self.inner2.id, html=True ) @@ -746,7 +753,7 @@ class TestInlinePermissions(TestCase): ) self.assertContains( response, - '' % self.inner2_id, + '' % self.inner2.id, html=True ) self.assertContains(response, 'id="id_inner2_set-0-DELETE"') @@ -760,7 +767,7 @@ class TestInlinePermissions(TestCase): self.user.user_permissions.add(permission) response = self.client.get(self.holder_change_url) # All perms on inner2s, so we can add/change/delete - self.assertContains(response, '

Inner2s

') + self.assertContains(response, '

Inner2s

', count=2) # One form for existing instance only, three for new self.assertContains( response, @@ -769,10 +776,18 @@ class TestInlinePermissions(TestCase): ) self.assertContains( response, - '' % self.inner2_id, + '' % self.inner2.id, html=True ) self.assertContains(response, 'id="id_inner2_set-0-DELETE"') + # TabularInline + self.assertContains(response, 'Dummy', html=True) + self.assertContains( + response, + '' % self.inner2.dummy, + html=True, + ) @override_settings(ROOT_URLCONF='admin_inlines.urls')