mirror of https://github.com/django/django.git
Fixed #31867 -- Made TabularInline handling of hidden fields with view-only permissions consistent with StackedInline.
This commit is contained in:
parent
06e59d97a3
commit
de95c82667
|
@ -177,11 +177,17 @@ class AdminReadonlyField:
|
||||||
else:
|
else:
|
||||||
help_text = help_text_for_field(class_name, form._meta.model)
|
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 = {
|
self.field = {
|
||||||
'name': class_name,
|
'name': class_name,
|
||||||
'label': label,
|
'label': label,
|
||||||
'help_text': help_text,
|
'help_text': help_text,
|
||||||
'field': field,
|
'field': field,
|
||||||
|
'is_hidden': is_hidden,
|
||||||
}
|
}
|
||||||
self.form = form
|
self.form = form
|
||||||
self.model_admin = model_admin
|
self.model_admin = model_admin
|
||||||
|
@ -302,6 +308,10 @@ class InlineAdminFormSet:
|
||||||
if fk and fk.name == field_name:
|
if fk and fk.name == field_name:
|
||||||
continue
|
continue
|
||||||
if not self.has_change_permission or field_name in self.readonly_fields:
|
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 {
|
yield {
|
||||||
'name': field_name,
|
'name': field_name,
|
||||||
'label': meta_labels.get(field_name) or label_for_field(
|
'label': meta_labels.get(field_name) or label_for_field(
|
||||||
|
@ -310,7 +320,7 @@ class InlineAdminFormSet:
|
||||||
self.opts,
|
self.opts,
|
||||||
form=empty_form,
|
form=empty_form,
|
||||||
),
|
),
|
||||||
'widget': {'is_hidden': False},
|
'widget': {'is_hidden': widget_is_hidden},
|
||||||
'required': False,
|
'required': False,
|
||||||
'help_text': meta_help_texts.get(field_name) or help_text_for_field(field_name, self.opts.model),
|
'help_text': meta_help_texts.get(field_name) or help_text_for_field(field_name, self.opts.model),
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,9 @@
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th class="original"></th>
|
<th class="original"></th>
|
||||||
{% for field in inline_admin_formset.fields %}
|
{% for field in inline_admin_formset.fields %}
|
||||||
{% if not field.widget.is_hidden %}
|
<th class="column-{{ field.name }}{% if field.required %} required{% endif %}{% if field.widget.is_hidden %} hidden{% endif %}">{{ field.label|capfirst }}
|
||||||
<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 %}
|
{% 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>
|
</th>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}<th>{% translate "Delete?" %}</th>{% endif %}
|
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}<th>{% translate "Delete?" %}</th>{% endif %}
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
|
@ -41,21 +39,11 @@
|
||||||
</p>{% endif %}
|
</p>{% endif %}
|
||||||
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% 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 %}
|
{% 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>
|
</td>
|
||||||
{% for fieldset in inline_admin_form %}
|
{% for fieldset in inline_admin_form %}
|
||||||
{% for line in fieldset %}
|
{% for line in fieldset %}
|
||||||
{% for field in line %}
|
{% for field in line %}
|
||||||
{% if field.is_readonly or not field.field.is_hidden %}
|
<td class="{% if field.field.name %}field-{{ field.field.name }}{% endif %}{% if field.field.is_hidden %} hidden{% endif %}">
|
||||||
<td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
|
|
||||||
{% if field.is_readonly %}
|
{% if field.is_readonly %}
|
||||||
<p>{{ field.contents }}</p>
|
<p>{{ field.contents }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -63,7 +51,6 @@
|
||||||
{{ field.field }}
|
{{ field.field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -106,9 +106,10 @@ def cell_count(inline_admin_form):
|
||||||
"""Return the number of cells used in a tabular inline."""
|
"""Return the number of cells used in a tabular inline."""
|
||||||
count = 1 # Hidden cell with hidden 'id' field
|
count = 1 # Hidden cell with hidden 'id' field
|
||||||
for fieldset in inline_admin_form:
|
for fieldset in inline_admin_form:
|
||||||
# Loop through all the fields (one per cell)
|
# Count all visible fields.
|
||||||
for line in fieldset:
|
for line in fieldset:
|
||||||
for field in line:
|
for field in line:
|
||||||
|
if not field.field.is_hidden:
|
||||||
count += 1
|
count += 1
|
||||||
if inline_admin_form.formset.can_delete:
|
if inline_admin_form.formset.can_delete:
|
||||||
# Delete checkbox
|
# Delete checkbox
|
||||||
|
|
|
@ -342,6 +342,35 @@ class ClassAdminStackedVertical(admin.ModelAdmin):
|
||||||
inlines = [ClassStackedVertical]
|
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])
|
site.register(TitleCollection, inlines=[TitleInline])
|
||||||
# Test bug #12561 and #12778
|
# Test bug #12561 and #12778
|
||||||
# only ModelAdmin media
|
# only ModelAdmin media
|
||||||
|
@ -373,3 +402,10 @@ site.register(Course, ClassAdminStackedHorizontal)
|
||||||
site.register(CourseProxy, ClassAdminStackedVertical)
|
site.register(CourseProxy, ClassAdminStackedVertical)
|
||||||
site.register(CourseProxy1, ClassAdminTabularVertical)
|
site.register(CourseProxy1, ClassAdminTabularVertical)
|
||||||
site.register(CourseProxy2, ClassAdminTabularHorizontal)
|
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])
|
||||||
|
|
|
@ -36,6 +36,26 @@ class TestInline(TestDataMixin, TestCase):
|
||||||
cls.holder = Holder.objects.create(dummy=13)
|
cls.holder = Holder.objects.create(dummy=13)
|
||||||
Inner.objects.create(dummy=42, holder=cls.holder)
|
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):
|
def setUp(self):
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
@ -227,6 +247,113 @@ class TestInline(TestDataMixin, TestCase):
|
||||||
response.rendered_content,
|
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):
|
def test_non_related_name_inline(self):
|
||||||
"""
|
"""
|
||||||
Multiple inlines with related_name='+' have correct form prefixes.
|
Multiple inlines with related_name='+' have correct form prefixes.
|
||||||
|
|
|
@ -4,4 +4,7 @@ from . import admin
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('admin2/', admin.site2.urls),
|
||||||
|
path('admin3/', admin.site3.urls),
|
||||||
|
path('admin4/', admin.site4.urls),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue