From de95c826673be9ea519acc86fd898631d1a11356 Mon Sep 17 00:00:00 2001 From: antoinehumbert Date: Wed, 12 Aug 2020 21:34:20 +0200 Subject: [PATCH] Fixed #31867 -- Made TabularInline handling of hidden fields with view-only permissions consistent with StackedInline. --- django/contrib/admin/helpers.py | 12 +- .../templates/admin/edit_inline/tabular.html | 21 +-- .../admin/templatetags/admin_modify.py | 5 +- tests/admin_inlines/admin.py | 36 +++++ tests/admin_inlines/tests.py | 127 ++++++++++++++++++ tests/admin_inlines/urls.py | 3 + 6 files changed, 184 insertions(+), 20 deletions(-) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 0728409046..dd154f72ee 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -177,11 +177,17 @@ class AdminReadonlyField: else: help_text = help_text_for_field(class_name, form._meta.model) + if field in form.fields: + is_hidden = form.fields[field].widget.is_hidden + else: + is_hidden = False + self.field = { 'name': class_name, 'label': label, 'help_text': help_text, 'field': field, + 'is_hidden': is_hidden, } self.form = form self.model_admin = model_admin @@ -302,6 +308,10 @@ class InlineAdminFormSet: if fk and fk.name == field_name: continue if not self.has_change_permission or field_name in self.readonly_fields: + form_field = empty_form.fields.get(field_name) + widget_is_hidden = False + if form_field is not None: + widget_is_hidden = form_field.widget.is_hidden yield { 'name': field_name, 'label': meta_labels.get(field_name) or label_for_field( @@ -310,7 +320,7 @@ class InlineAdminFormSet: self.opts, form=empty_form, ), - 'widget': {'is_hidden': False}, + 'widget': {'is_hidden': widget_is_hidden}, 'required': False, 'help_text': meta_help_texts.get(field_name) or help_text_for_field(field_name, self.opts.model), } diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index 7a4e7cb226..4c0a08cbfc 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -15,11 +15,9 @@ {% for field in inline_admin_formset.fields %} - {% if not field.widget.is_hidden %} - {{ field.label|capfirst }} - {% if field.help_text %}({{ field.help_text|striptags }}){% endif %} - - {% endif %} + {{ field.label|capfirst }} + {% if field.help_text %}({{ field.help_text|striptags }}){% endif %} + {% endfor %} {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}{% translate "Delete?" %}{% endif %} @@ -41,21 +39,11 @@

{% endif %} {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} {% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %} - {% spaceless %} - {% for fieldset in inline_admin_form %} - {% for line in fieldset %} - {% for field in line %} - {% if not field.is_readonly and field.field.is_hidden %}{{ field.field }}{% endif %} - {% endfor %} - {% endfor %} - {% endfor %} - {% endspaceless %} {% for fieldset in inline_admin_form %} {% for line in fieldset %} {% for field in line %} - {% if field.is_readonly or not field.field.is_hidden %} - + {% if field.is_readonly %}

{{ field.contents }}

{% else %} @@ -63,7 +51,6 @@ {{ field.field }} {% endif %} - {% endif %} {% endfor %} {% endfor %} {% endfor %} diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index fcc1755199..583b7639e8 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -106,10 +106,11 @@ def cell_count(inline_admin_form): """Return the number of cells used in a tabular inline.""" count = 1 # Hidden cell with hidden 'id' field for fieldset in inline_admin_form: - # Loop through all the fields (one per cell) + # Count all visible fields. for line in fieldset: for field in line: - count += 1 + if not field.field.is_hidden: + count += 1 if inline_admin_form.formset.can_delete: # Delete checkbox count += 1 diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 9171c7102a..c444526241 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -342,6 +342,35 @@ class ClassAdminStackedVertical(admin.ModelAdmin): inlines = [ClassStackedVertical] +class ChildHiddenFieldForm(forms.ModelForm): + class Meta: + model = SomeChildModel + fields = ['name', 'position', 'parent'] + widgets = {'position': forms.HiddenInput} + + def _post_clean(self): + super()._post_clean() + if self.instance is not None and self.instance.position == 1: + self.add_error(None, ValidationError('A non-field error')) + + +class ChildHiddenFieldTabularInline(admin.TabularInline): + model = SomeChildModel + form = ChildHiddenFieldForm + + +class ChildHiddenFieldInFieldsGroupStackedInline(admin.StackedInline): + model = SomeChildModel + form = ChildHiddenFieldForm + fields = [('name', 'position')] + + +class ChildHiddenFieldOnSingleLineStackedInline(admin.StackedInline): + model = SomeChildModel + form = ChildHiddenFieldForm + fields = ('name', 'position') + + site.register(TitleCollection, inlines=[TitleInline]) # Test bug #12561 and #12778 # only ModelAdmin media @@ -373,3 +402,10 @@ site.register(Course, ClassAdminStackedHorizontal) site.register(CourseProxy, ClassAdminStackedVertical) site.register(CourseProxy1, ClassAdminTabularVertical) site.register(CourseProxy2, ClassAdminTabularHorizontal) +# Used to test hidden fields in tabular and stacked inlines. +site2 = admin.AdminSite(name='tabular_inline_hidden_field_admin') +site2.register(SomeParentModel, inlines=[ChildHiddenFieldTabularInline]) +site3 = admin.AdminSite(name='stacked_inline_hidden_field_in_group_admin') +site3.register(SomeParentModel, inlines=[ChildHiddenFieldInFieldsGroupStackedInline]) +site4 = admin.AdminSite(name='stacked_inline_hidden_field_on_single_line_admin') +site4.register(SomeParentModel, inlines=[ChildHiddenFieldOnSingleLineStackedInline]) diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index c96853a7ff..12f36f4483 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -36,6 +36,26 @@ class TestInline(TestDataMixin, TestCase): cls.holder = Holder.objects.create(dummy=13) Inner.objects.create(dummy=42, holder=cls.holder) + cls.parent = SomeParentModel.objects.create(name='a') + SomeChildModel.objects.create(name='b', position='0', parent=cls.parent) + SomeChildModel.objects.create(name='c', position='1', parent=cls.parent) + + cls.view_only_user = User.objects.create_user( + username='user', password='pwd', is_staff=True, + ) + parent_ct = ContentType.objects.get_for_model(SomeParentModel) + child_ct = ContentType.objects.get_for_model(SomeChildModel) + permission = Permission.objects.get( + codename='view_someparentmodel', + content_type=parent_ct, + ) + cls.view_only_user.user_permissions.add(permission) + permission = Permission.objects.get( + codename='view_somechildmodel', + content_type=child_ct, + ) + cls.view_only_user.user_permissions.add(permission) + def setUp(self): self.client.force_login(self.superuser) @@ -227,6 +247,113 @@ class TestInline(TestDataMixin, TestCase): response.rendered_content, ) + def test_tabular_inline_hidden_field_with_view_only_permissions(self): + """ + Content of hidden field is not visible in tabular inline when user has + view-only permission. + """ + self.client.force_login(self.view_only_user) + url = reverse( + 'tabular_inline_hidden_field_admin:admin_inlines_someparentmodel_change', + args=(self.parent.pk,), + ) + response = self.client.get(url) + self.assertInHTML('Position', response.rendered_content) + self.assertInHTML('

0

', response.rendered_content) + self.assertInHTML('

1

', response.rendered_content) + + def test_stacked_inline_hidden_field_with_view_only_permissions(self): + """ + Content of hidden field is not visible in stacked inline when user has + view-only permission. + """ + self.client.force_login(self.view_only_user) + url = reverse( + 'stacked_inline_hidden_field_in_group_admin:admin_inlines_someparentmodel_change', + args=(self.parent.pk,), + ) + response = self.client.get(url) + # The whole line containing name + position fields is not hidden. + self.assertContains(response, '
') + # The div containing the position field is hidden. + self.assertInHTML( + '', + response.rendered_content, + ) + self.assertInHTML( + '', + response.rendered_content, + ) + + def test_stacked_inline_single_hidden_field_in_line_with_view_only_permissions(self): + """ + Content of hidden field is not visible in stacked inline when user has + view-only permission and the field is grouped on a separate line. + """ + self.client.force_login(self.view_only_user) + url = reverse( + 'stacked_inline_hidden_field_on_single_line_admin:admin_inlines_someparentmodel_change', + args=(self.parent.pk,), + ) + response = self.client.get(url) + # The whole line containing position field is hidden. + self.assertInHTML( + '', + response.rendered_content, + ) + self.assertInHTML( + '', + response.rendered_content, + ) + + def test_tabular_inline_with_hidden_field_non_field_errors_has_correct_colspan(self): + """ + In tabular inlines, when a form has non-field errors, those errors + are rendered in a table line with a single cell spanning the whole + table width. Colspan must be equal to the number of visible columns. + """ + parent = SomeParentModel.objects.create(name='a') + child = SomeChildModel.objects.create(name='b', position='0', parent=parent) + url = reverse( + 'tabular_inline_hidden_field_admin:admin_inlines_someparentmodel_change', + args=(parent.id,), + ) + data = { + 'name': parent.name, + 'somechildmodel_set-TOTAL_FORMS': 1, + 'somechildmodel_set-INITIAL_FORMS': 1, + 'somechildmodel_set-MIN_NUM_FORMS': 0, + 'somechildmodel_set-MAX_NUM_FORMS': 1000, + '_save': 'Save', + 'somechildmodel_set-0-id': child.id, + 'somechildmodel_set-0-parent': parent.id, + 'somechildmodel_set-0-name': child.name, + 'somechildmodel_set-0-position': 1, + } + response = self.client.post(url, data) + # Form has 3 visible columns and 1 hidden column. + self.assertInHTML( + '' + 'Name' + 'Position' + 'Delete?', + response.rendered_content, + ) + # The non-field error must be spanned on 3 (visible) columns. + self.assertInHTML( + '' + '
  • A non-field error
', + response.rendered_content, + ) + def test_non_related_name_inline(self): """ Multiple inlines with related_name='+' have correct form prefixes. diff --git a/tests/admin_inlines/urls.py b/tests/admin_inlines/urls.py index be569cdca5..5345386917 100644 --- a/tests/admin_inlines/urls.py +++ b/tests/admin_inlines/urls.py @@ -4,4 +4,7 @@ from . import admin urlpatterns = [ path('admin/', admin.site.urls), + path('admin2/', admin.site2.urls), + path('admin3/', admin.site3.urls), + path('admin4/', admin.site4.urls), ]