[2.1.x] Fixed #29930 -- Allowed editing in admin with view-only inlines.
Co-authored-by: Tim Graham <timograham@gmail.com>
Backport of 8245c99ee6
from master
This commit is contained in:
parent
b623c49c39
commit
27f5b0aff3
|
@ -1948,7 +1948,24 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
'files': request.FILES,
|
'files': request.FILES,
|
||||||
'save_as_new': '_saveasnew' in request.POST
|
'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)
|
inline_instances.append(inline)
|
||||||
return formsets, inline_instances
|
return formsets, inline_instances
|
||||||
|
|
||||||
|
|
|
@ -22,3 +22,7 @@ Bugfixes
|
||||||
|
|
||||||
* Fixed admin view-only change form crash when using
|
* Fixed admin view-only change form crash when using
|
||||||
``ModelAdmin.prepopulated_fields`` (:ticket:`29929`).
|
``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`).
|
||||||
|
|
|
@ -1918,6 +1918,96 @@ class AdminViewPermissionsTest(TestCase):
|
||||||
new_article = Article.objects.latest('id')
|
new_article = Article.objects.latest('id')
|
||||||
self.assertRedirects(post, reverse('admin:admin_views_article_change', args=(new_article.pk,)))
|
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):
|
def test_delete_view(self):
|
||||||
"""Delete view should restrict access and actually delete items."""
|
"""Delete view should restrict access and actually delete items."""
|
||||||
delete_dict = {'post': 'yes'}
|
delete_dict = {'post': 'yes'}
|
||||||
|
|
|
@ -86,6 +86,10 @@ class ModelAdminTests(TestCase):
|
||||||
self.song = Song.objects.create(name='test', band=self.band)
|
self.song = Song.objects.create(name='test', band=self.band)
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.request = MockRequest()
|
self.request = MockRequest()
|
||||||
|
self.request.POST = {
|
||||||
|
'song_set-TOTAL_FORMS': 4,
|
||||||
|
'song_set-INITIAL_FORMS': 1,
|
||||||
|
}
|
||||||
self.request.user = self.MockAddUser()
|
self.request.user = self.MockAddUser()
|
||||||
self.ma = BandAdmin(Band, self.site)
|
self.ma = BandAdmin(Band, self.site)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue