Fixed #29930 -- Allowed editing in admin with view-only inlines.

Co-authored-by: Tim Graham <timograham@gmail.com>
This commit is contained in:
Carlton Gibson 2018-12-03 07:44:18 -08:00 committed by GitHub
parent 950112548e
commit 8245c99ee6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 116 additions and 1 deletions

View File

@ -1945,7 +1945,24 @@ class ModelAdmin(BaseModelAdmin):
'files': request.FILES,
'save_as_new': '_saveasnew' in request.POST
})
formsets.append(FormSet(**formset_params))
formset = FormSet(**formset_params)
def user_deleted_form(request, obj, formset, index):
"""Return whether or not the user deleted the form."""
return (
inline.has_delete_permission(request, obj) and
'{}-{}-DELETE'.format(formset.prefix, index) in request.POST
)
# Bypass validation of each view-only inline form (since the form's
# data won't be in request.POST), unless the form was deleted.
if not inline.has_change_permission(request, obj):
for index, form in enumerate(formset.initial_forms):
if user_deleted_form(request, obj, formset, index):
continue
form._errors = {}
form.cleaned_data = form.initial
formsets.append(formset)
inline_instances.append(inline)
return formsets, inline_instances

View File

@ -22,3 +22,7 @@ Bugfixes
* Fixed admin view-only change form crash when using
``ModelAdmin.prepopulated_fields`` (:ticket:`29929`).
* Fixed "Please correct the errors below" error message when editing an object
in the admin if the user only has the "view" permission on inlines
(:ticket:`29930`).

View File

@ -1912,6 +1912,96 @@ class AdminViewPermissionsTest(TestCase):
new_article = Article.objects.latest('id')
self.assertRedirects(post, reverse('admin:admin_views_article_change', args=(new_article.pk,)))
def test_change_view_with_view_only_inlines(self):
"""
User with change permission to a section but view-only for inlines.
"""
self.viewuser.user_permissions.add(get_perm(Section, get_permission_codename('change', Section._meta)))
self.client.force_login(self.viewuser)
# GET shows inlines.
response = self.client.get(reverse('admin:admin_views_section_change', args=(self.s1.pk,)))
self.assertEqual(len(response.context['inline_admin_formsets']), 1)
formset = response.context['inline_admin_formsets'][0]
self.assertEqual(len(formset.forms), 3)
# Valid POST changes the name.
data = {
'name': 'Can edit name with view-only inlines',
'article_set-TOTAL_FORMS': 3,
'article_set-INITIAL_FORMS': 3
}
response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), data)
self.assertRedirects(response, reverse('admin:admin_views_section_changelist'))
self.assertEqual(Section.objects.get(pk=self.s1.pk).name, data['name'])
# Invalid POST reshows inlines.
del data['name']
response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), data)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context['inline_admin_formsets']), 1)
formset = response.context['inline_admin_formsets'][0]
self.assertEqual(len(formset.forms), 3)
def test_change_view_with_view_and_add_inlines(self):
"""User has view and add permissions on the inline model."""
self.viewuser.user_permissions.add(get_perm(Section, get_permission_codename('change', Section._meta)))
self.viewuser.user_permissions.add(get_perm(Article, get_permission_codename('add', Article._meta)))
self.client.force_login(self.viewuser)
# GET shows inlines.
response = self.client.get(reverse('admin:admin_views_section_change', args=(self.s1.pk,)))
self.assertEqual(len(response.context['inline_admin_formsets']), 1)
formset = response.context['inline_admin_formsets'][0]
self.assertEqual(len(formset.forms), 6)
# Valid POST creates a new article.
data = {
'name': 'Can edit name with view-only inlines',
'article_set-TOTAL_FORMS': 6,
'article_set-INITIAL_FORMS': 3,
'article_set-3-id': [''],
'article_set-3-title': ['A title'],
'article_set-3-content': ['Added content'],
'article_set-3-date_0': ['2008-3-18'],
'article_set-3-date_1': ['11:54:58'],
'article_set-3-section': [str(self.s1.pk)],
}
response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), data)
self.assertRedirects(response, reverse('admin:admin_views_section_changelist'))
self.assertEqual(Section.objects.get(pk=self.s1.pk).name, data['name'])
self.assertEqual(Article.objects.count(), 4)
# Invalid POST reshows inlines.
del data['name']
response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), data)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context['inline_admin_formsets']), 1)
formset = response.context['inline_admin_formsets'][0]
self.assertEqual(len(formset.forms), 6)
def test_change_view_with_view_and_delete_inlines(self):
"""User has view and delete permissions on the inline model."""
self.viewuser.user_permissions.add(get_perm(Section, get_permission_codename('change', Section._meta)))
self.client.force_login(self.viewuser)
data = {
'name': 'Name is required.',
'article_set-TOTAL_FORMS': 6,
'article_set-INITIAL_FORMS': 3,
'article_set-0-id': [str(self.a1.pk)],
'article_set-0-DELETE': ['on'],
}
# Inline POST details are ignored without delete permission.
response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), data)
self.assertRedirects(response, reverse('admin:admin_views_section_changelist'))
self.assertEqual(Article.objects.count(), 3)
# Deletion successful when delete permission is added.
self.viewuser.user_permissions.add(get_perm(Article, get_permission_codename('delete', Article._meta)))
data = {
'name': 'Name is required.',
'article_set-TOTAL_FORMS': 6,
'article_set-INITIAL_FORMS': 3,
'article_set-0-id': [str(self.a1.pk)],
'article_set-0-DELETE': ['on'],
}
response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), data)
self.assertRedirects(response, reverse('admin:admin_views_section_changelist'))
self.assertEqual(Article.objects.count(), 2)
def test_delete_view(self):
"""Delete view should restrict access and actually delete items."""
delete_dict = {'post': 'yes'}

View File

@ -89,6 +89,10 @@ class ModelAdminTests(TestCase):
def setUp(self):
self.site = AdminSite()
self.request = MockRequest()
self.request.POST = {
'song_set-TOTAL_FORMS': 4,
'song_set-INITIAL_FORMS': 1,
}
self.request.user = self.MockAddUser()
self.ma = BandAdmin(Band, self.site)