浏览代码

Fixed #31867 -- Made TabularInline handling of hidden fields with view-only permissions consistent with StackedInline.

antoinehumbert 4 年之前
父节点
当前提交
de95c82667

+ 11 - 1
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),
                 }

+ 4 - 17
django/contrib/admin/templates/admin/edit_inline/tabular.html

@@ -15,11 +15,9 @@
      <thead><tr>
        <th class="original"></th>
      {% for field in inline_admin_formset.fields %}
-       {% if not field.widget.is_hidden %}
-         <th class="column-{{ field.name }}{% if field.required %} required{% endif %}">{{ field.label|capfirst }}
-         {% if field.help_text %}<img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}">{% endif %}
-         </th>
-       {% endif %}
+       <th class="column-{{ field.name }}{% if field.required %} required{% endif %}{% if field.widget.is_hidden %} hidden{% endif %}">{{ field.label|capfirst }}
+       {% if field.help_text %}<img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}">{% endif %}
+       </th>
      {% endfor %}
      {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}<th>{% translate "Delete?" %}</th>{% endif %}
      </tr></thead>
@@ -41,21 +39,11 @@
             </p>{% 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 %}
         </td>
         {% for fieldset in inline_admin_form %}
           {% for line in fieldset %}
             {% for field in line %}
-              {% if field.is_readonly or not field.field.is_hidden %}
-              <td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
+              <td class="{% if field.field.name %}field-{{ field.field.name }}{% endif %}{% if field.field.is_hidden %} hidden{% endif %}">
               {% if field.is_readonly %}
                   <p>{{ field.contents }}</p>
               {% else %}
@@ -63,7 +51,6 @@
                   {{ field.field }}
               {% endif %}
               </td>
-              {% endif %}
             {% endfor %}
           {% endfor %}
         {% endfor %}

+ 3 - 2
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

+ 36 - 0
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])

+ 127 - 0
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('<th class="column-position hidden">Position</th>', response.rendered_content)
+        self.assertInHTML('<td class="field-position hidden"><p>0</p></td>', response.rendered_content)
+        self.assertInHTML('<td class="field-position hidden"><p>1</p></td>', 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, '<div class="form-row field-name field-position">')
+        # The div containing the position field is hidden.
+        self.assertInHTML(
+            '<div class="fieldBox field-position hidden">'
+            '<label class="inline">Position:</label>'
+            '<div class="readonly">0</div></div>',
+            response.rendered_content,
+        )
+        self.assertInHTML(
+            '<div class="fieldBox field-position hidden">'
+            '<label class="inline">Position:</label>'
+            '<div class="readonly">1</div></div>',
+            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(
+            '<div class="form-row hidden field-position">'
+            '<div><label>Position:</label>'
+            '<div class="readonly">0</div></div></div>',
+            response.rendered_content,
+        )
+        self.assertInHTML(
+            '<div class="form-row hidden field-position">'
+            '<div><label>Position:</label>'
+            '<div class="readonly">1</div></div></div>',
+            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(
+            '<thead><tr><th class="original"></th>'
+            '<th class="column-name required">Name</th>'
+            '<th class="column-position required hidden">Position</th>'
+            '<th>Delete?</th></tr></thead>',
+            response.rendered_content,
+        )
+        # The non-field error must be spanned on 3 (visible) columns.
+        self.assertInHTML(
+            '<tr class="row-form-errors"><td colspan="3">'
+            '<ul class="errorlist nonfield"><li>A non-field error</li></ul></td></tr>',
+            response.rendered_content,
+        )
+
     def test_non_related_name_inline(self):
         """
         Multiple inlines with related_name='+' have correct form prefixes.

+ 3 - 0
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),
 ]