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 %}{% endif %}
- |
- {% endif %}
+ {{ field.label|capfirst }}
+ {% if field.help_text %}{% 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(
+ '
'
+ ' |
',
+ 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),
]