summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlton Gibson <carlton.gibson@noumenal.es>2018-12-03 07:44:18 -0800
committerCarlton Gibson <carlton.gibson@noumenal.es>2018-12-03 17:34:36 +0100
commit27f5b0aff3442e5c25e84972dff4f5fe1edd4e68 (patch)
tree8f9e88efc8ed3cf981837265201feb2024eca2e4
parentb623c49c399cc0855935c0e953d7e22e9a884207 (diff)
[2.1.x] Fixed #29930 -- Allowed editing in admin with view-only inlines.
Co-authored-by: Tim Graham <timograham@gmail.com> Backport of 8245c99ee6032c2748ba46583d8cab15b2f9438e from master
-rw-r--r--django/contrib/admin/options.py19
-rw-r--r--docs/releases/2.1.4.txt4
-rw-r--r--tests/admin_views/tests.py90
-rw-r--r--tests/modeladmin/test_has_add_permission_obj_deprecation.py4
4 files changed, 116 insertions, 1 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index d0948359ab..ddaf130584 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1948,7 +1948,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
diff --git a/docs/releases/2.1.4.txt b/docs/releases/2.1.4.txt
index 8ba457846e..6d0c3dce46 100644
--- a/docs/releases/2.1.4.txt
+++ b/docs/releases/2.1.4.txt
@@ -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`).
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 2d1e58aa26..a94bbbccfc 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -1918,6 +1918,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'}
diff --git a/tests/modeladmin/test_has_add_permission_obj_deprecation.py b/tests/modeladmin/test_has_add_permission_obj_deprecation.py
index f5489a41db..c85b80da5f 100644
--- a/tests/modeladmin/test_has_add_permission_obj_deprecation.py
+++ b/tests/modeladmin/test_has_add_permission_obj_deprecation.py
@@ -86,6 +86,10 @@ class ModelAdminTests(TestCase):
self.song = Song.objects.create(name='test', band=self.band)
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)