Fixed #29637 -- Fixed admin change form crash if the user doesn’t have the add permission to a TabularInline.

Regression in 825f0beda8.
This commit is contained in:
Clément Mangin 2018-08-09 11:43:55 -04:00 committed by Tim Graham
parent d0928d6454
commit 64e1a271f5
4 changed files with 41 additions and 16 deletions

View File

@ -2055,12 +2055,6 @@ class InlineModelAdmin(BaseModelAdmin):
can_add = self.has_add_permission(request, obj) if request else True can_add = self.has_add_permission(request, obj) if request else True
class DeleteProtectedModelForm(base_model_form): 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): def hand_clean_DELETE(self):
""" """
@ -2097,6 +2091,14 @@ class InlineModelAdmin(BaseModelAdmin):
self.hand_clean_DELETE() self.hand_clean_DELETE()
return result 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 defaults['form'] = DeleteProtectedModelForm
if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): if defaults['fields'] is None and not modelform_defines_fields(defaults['form']):

View File

@ -24,3 +24,7 @@ Bugfixes
* Fixed translation failure of ``DurationField``'s "overflow" error message * Fixed translation failure of ``DurationField``'s "overflow" error message
(:ticket:`29623`). (: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`).

View File

@ -74,6 +74,10 @@ class InnerInline2(admin.StackedInline):
js = ('my_awesome_inline_scripts.js',) js = ('my_awesome_inline_scripts.js',)
class InnerInline2Tabular(admin.TabularInline):
model = Inner2
class CustomNumberWidget(forms.NumberInput): class CustomNumberWidget(forms.NumberInput):
class Media: class Media:
js = ('custom_number.js',) js = ('custom_number.js',)
@ -236,7 +240,7 @@ site.register(TitleCollection, inlines=[TitleInline])
# only ModelAdmin media # only ModelAdmin media
site.register(Holder, HolderAdmin, inlines=[InnerInline]) site.register(Holder, HolderAdmin, inlines=[InnerInline])
# ModelAdmin and Inline media # ModelAdmin and Inline media
site.register(Holder2, HolderAdmin, inlines=[InnerInline2]) site.register(Holder2, HolderAdmin, inlines=[InnerInline2, InnerInline2Tabular])
# only Inline media # only Inline media
site.register(Holder3, inlines=[InnerInline3]) site.register(Holder3, inlines=[InnerInline3])

View File

@ -588,9 +588,8 @@ class TestInlinePermissions(TestCase):
self.author_book_auto_m2m_intermediate_id = author_book_auto_m2m_intermediate.pk self.author_book_auto_m2m_intermediate_id = author_book_auto_m2m_intermediate.pk
holder = Holder2.objects.create(dummy=13) 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.holder_change_url = reverse('admin:admin_inlines_holder2_change', args=(holder.id,))
self.inner2_id = inner2.id
self.client.force_login(self.user) self.client.force_login(self.user)
@ -684,7 +683,7 @@ class TestInlinePermissions(TestCase):
) )
self.assertNotContains( self.assertNotContains(
response, response,
'<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2_id, '<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2.id,
html=True html=True
) )
@ -693,7 +692,7 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission) self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url) response = self.client.get(self.holder_change_url)
# Change permission on inner2s, so we can change existing but not add new # Change permission on inner2s, so we can change existing but not add new
self.assertContains(response, '<h2>Inner2s</h2>') self.assertContains(response, '<h2>Inner2s</h2>', count=2)
# Just the one form for existing instances # Just the one form for existing instances
self.assertContains( self.assertContains(
response, '<input type="hidden" id="id_inner2_set-TOTAL_FORMS" value="1" name="inner2_set-TOTAL_FORMS">', response, '<input type="hidden" id="id_inner2_set-TOTAL_FORMS" value="1" name="inner2_set-TOTAL_FORMS">',
@ -701,7 +700,7 @@ class TestInlinePermissions(TestCase):
) )
self.assertContains( self.assertContains(
response, response,
'<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2_id, '<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2.id,
html=True html=True
) )
# max-num 0 means we can't add new ones # max-num 0 means we can't add new ones
@ -710,6 +709,14 @@ class TestInlinePermissions(TestCase):
'<input type="hidden" id="id_inner2_set-MAX_NUM_FORMS" value="0" name="inner2_set-MAX_NUM_FORMS">', '<input type="hidden" id="id_inner2_set-MAX_NUM_FORMS" value="0" name="inner2_set-MAX_NUM_FORMS">',
html=True html=True
) )
# TabularInline
self.assertContains(response, '<th class="required">Dummy</th>', html=True)
self.assertContains(
response,
'<input type="number" name="inner2_set-2-0-dummy" value="%s" '
'class="vIntegerField" id="id_inner2_set-2-0-dummy">' % self.inner2.dummy,
html=True,
)
def test_inline_change_fk_add_change_perm(self): def test_inline_change_fk_add_change_perm(self):
permission = Permission.objects.get(codename='add_inner2', content_type=self.inner_ct) permission = Permission.objects.get(codename='add_inner2', content_type=self.inner_ct)
@ -726,7 +733,7 @@ class TestInlinePermissions(TestCase):
) )
self.assertContains( self.assertContains(
response, response,
'<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2_id, '<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2.id,
html=True html=True
) )
@ -746,7 +753,7 @@ class TestInlinePermissions(TestCase):
) )
self.assertContains( self.assertContains(
response, response,
'<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2_id, '<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2.id,
html=True html=True
) )
self.assertContains(response, 'id="id_inner2_set-0-DELETE"') self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
@ -760,7 +767,7 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission) self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url) response = self.client.get(self.holder_change_url)
# All perms on inner2s, so we can add/change/delete # All perms on inner2s, so we can add/change/delete
self.assertContains(response, '<h2>Inner2s</h2>') self.assertContains(response, '<h2>Inner2s</h2>', count=2)
# One form for existing instance only, three for new # One form for existing instance only, three for new
self.assertContains( self.assertContains(
response, response,
@ -769,10 +776,18 @@ class TestInlinePermissions(TestCase):
) )
self.assertContains( self.assertContains(
response, response,
'<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2_id, '<input type="hidden" id="id_inner2_set-0-id" value="%i" name="inner2_set-0-id">' % self.inner2.id,
html=True html=True
) )
self.assertContains(response, 'id="id_inner2_set-0-DELETE"') self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
# TabularInline
self.assertContains(response, '<th class="required">Dummy</th>', html=True)
self.assertContains(
response,
'<input type="number" name="inner2_set-2-0-dummy" value="%s" '
'class="vIntegerField" id="id_inner2_set-2-0-dummy">' % self.inner2.dummy,
html=True,
)
@override_settings(ROOT_URLCONF='admin_inlines.urls') @override_settings(ROOT_URLCONF='admin_inlines.urls')