Refs #35189 -- Improved admin fieldset's accessibility by setting aria-labelledby.

Before this change, HTML <fieldset> elements in the admin site did not
have an associated label to describe them. This commit defines a unique
HTML id for the heading labeling a fieldset, and sets its
aria-labelledby property to link the heading with the fieldset.
This commit is contained in:
Marijke Luttekes 2024-05-21 22:09:26 -03:00 committed by nessita
parent 9c5fe93349
commit 01ed59f753
7 changed files with 478 additions and 59 deletions

View File

@ -47,7 +47,7 @@
{% block field_sets %}
{% for fieldset in adminform %}
{% include "admin/includes/fieldset.html" %}
{% include "admin/includes/fieldset.html" with heading_level=2 id_suffix=forloop.counter0 %}
{% endfor %}
{% endblock %}

View File

@ -3,12 +3,14 @@
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}">
<fieldset class="module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
<h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
{% if inline_admin_formset.formset.max_num == 1 %}
<h2>{{ inline_admin_formset.opts.verbose_name|capfirst }}</h2>
{{ inline_admin_formset.opts.verbose_name|capfirst }}
{% else %}
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}
{% endif %}
</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
@ -19,9 +21,13 @@
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% with parent_counter=forloop.counter0 %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" with heading_level=4 id_prefix=parent_counter id_suffix=forloop.counter0 %}
{% endfor %}
{% endwith %}
{% 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 %}
</div>{% endfor %}

View File

@ -4,12 +4,14 @@
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }}
<fieldset class="module {{ inline_admin_formset.classes }}">
{% if inline_admin_formset.formset.max_num == 1 %}
<h2>{{ inline_admin_formset.opts.verbose_name|capfirst }}</h2>
{% else %}
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{% endif %}
<fieldset class="module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
<h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
{% if inline_admin_formset.formset.max_num == 1 %}
{{ inline_admin_formset.opts.verbose_name|capfirst }}
{% else %}
{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}
{% endif %}
</h2>
{{ inline_admin_formset.formset.non_form_errors }}
<table>
<thead><tr>

View File

@ -1,5 +1,8 @@
<fieldset class="module aligned {{ fieldset.classes }}">
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
{% with prefix=fieldset.formset.prefix|default:"fieldset" id_prefix=id_prefix|default:"0" id_suffix=id_suffix|default:"0" name=fieldset.name|default:""|slugify %}
<fieldset class="module aligned {{ fieldset.classes }}"{% if name %} aria-labelledby="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading"{% endif %}>
{% if name %}
<h{{ heading_level|default:2 }} id="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading" class="fieldset-heading">{{ fieldset.name }}</h{{ heading_level|default:2 }}>
{% endif %}
{% if fieldset.description %}
<div class="description">{{ fieldset.description|safe }}</div>
{% endif %}
@ -32,3 +35,4 @@
</div>
{% endfor %}
</fieldset>
{% endwith %}

View File

@ -40,6 +40,8 @@ from .models import (
OutfitItem,
ParentModelWithCustomPk,
Person,
Photo,
Photographer,
Poll,
Profile,
ProfileCollection,
@ -98,6 +100,57 @@ class AuthorAdmin(admin.ModelAdmin):
]
class PhotoInlineMixin:
model = Photo
extra = 2
fieldsets = [
(None, {"fields": ["image", "title"]}),
(
"Details",
{"fields": ["description", "creation_date"], "classes": ["collapse"]},
),
(
"Details", # Fieldset name intentionally duplicated
{"fields": ["update_date", "updated_by"]},
),
]
class PhotoTabularInline(PhotoInlineMixin, admin.TabularInline):
pass
class PhotoStackedExtra2Inline(PhotoInlineMixin, admin.StackedInline):
pass
class PhotoStackedExtra3Inline(PhotoInlineMixin, admin.StackedInline):
extra = 3
class PhotoStackedCollapsibleInline(PhotoInlineMixin, admin.StackedInline):
fieldsets = []
classes = ["collapse"]
class PhotographerAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["firstname", "fullname"]}),
("Advanced options", {"fields": ["nationality", "residency"]}),
(
"Advanced options", # Fieldset name intentionally duplicated
{"fields": ["siblings", "children"], "classes": ["collapse"]},
),
]
inlines = [
PhotoTabularInline,
PhotoTabularInline,
PhotoStackedExtra2Inline,
PhotoStackedExtra3Inline,
PhotoStackedCollapsibleInline,
]
class InnerInline(admin.StackedInline):
model = Inner
can_delete = False
@ -454,6 +507,7 @@ site.register(Teacher, TeacherAdmin)
site.register(Chapter, inlines=[FootNoteNonEditableInlineCustomForm])
site.register(OutfitItem, inlines=[WeaknessInlineCustomForm])
site.register(Person, inlines=[AuthorTabularInline, FashonistaStackedInline])
site.register(Photographer, PhotographerAdmin)
site.register(Course, ClassAdminStackedHorizontal)
site.register(CourseProxy, ClassAdminStackedVertical)
site.register(CourseProxy1, ClassAdminTabularVertical)

View File

@ -180,6 +180,27 @@ class ShoppingWeakness(models.Model):
item = models.ForeignKey(OutfitItem, models.CASCADE)
# Models for #35189
class Photographer(Person):
fullname = models.CharField(max_length=100)
nationality = models.CharField(max_length=100)
residency = models.CharField(max_length=100)
siblings = models.IntegerField()
children = models.IntegerField()
class Photo(models.Model):
photographer = models.ForeignKey(Photographer, on_delete=models.CASCADE)
image = models.CharField(max_length=100)
title = models.CharField(max_length=100)
description = models.TextField()
creation_date = models.DateField()
update_date = models.DateField()
updated_by = models.CharField(max_length=100)
# Models for #13510

View File

@ -117,7 +117,14 @@ class TestInline(TestDataMixin, TestCase):
"Autogenerated many-to-many inlines are displayed correctly (#13407)"
response = self.client.get(reverse("admin:admin_inlines_author_add"))
# The heading for the m2m inline block uses the right text
self.assertContains(response, "<h2>Author-book relationships</h2>")
self.assertContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
# The "add another" label is correct
self.assertContains(response, "Add another Author-book relationship")
# The '+' is dropped from the autogenerated form prefix (Author_books+)
@ -737,13 +744,35 @@ class TestInline(TestDataMixin, TestCase):
def test_inlines_plural_heading_foreign_key(self):
response = self.client.get(reverse("admin:admin_inlines_holder4_add"))
self.assertContains(response, "<h2>Inner4 stackeds</h2>", html=True)
self.assertContains(response, "<h2>Inner4 tabulars</h2>", html=True)
self.assertContains(
response,
(
'<h2 id="inner4stacked_set-heading" class="inline-heading">'
"Inner4 stackeds</h2>"
),
html=True,
)
self.assertContains(
response,
(
'<h2 id="inner4tabular_set-heading" class="inline-heading">'
"Inner4 tabulars</h2>"
),
html=True,
)
def test_inlines_singular_heading_one_to_one(self):
response = self.client.get(reverse("admin:admin_inlines_person_add"))
self.assertContains(response, "<h2>Author</h2>", html=True) # Tabular.
self.assertContains(response, "<h2>Fashionista</h2>", html=True) # Stacked.
self.assertContains(
response,
'<h2 id="author-heading" class="inline-heading">Author</h2>',
html=True,
) # Tabular.
self.assertContains(
response,
'<h2 id="fashionista-heading" class="inline-heading">Fashionista</h2>',
html=True,
) # Stacked.
def test_inlines_based_on_model_state(self):
parent = ShowInlineParent.objects.create(show_inlines=False)
@ -914,28 +943,50 @@ class TestInlinePermissions(TestCase):
def test_inline_add_m2m_noperm(self):
response = self.client.get(reverse("admin:admin_inlines_author_add"))
# No change permission on books, so no inline
self.assertNotContains(response, "<h2>Author-book relationships</h2>")
self.assertNotContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_noperm(self):
response = self.client.get(reverse("admin:admin_inlines_holder2_add"))
# No permissions on Inner2s, so no inline
self.assertNotContains(response, "<h2>Inner2s</h2>")
self.assertNotContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
self.assertNotContains(response, "Add another Inner2")
self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
def test_inline_change_m2m_noperm(self):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
self.assertNotContains(response, "<h2>Author-book relationships</h2>")
self.assertNotContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_change_fk_noperm(self):
response = self.client.get(self.holder_change_url)
# No permissions on Inner2s, so no inline
self.assertNotContains(response, "<h2>Inner2s</h2>")
self.assertNotContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
self.assertNotContains(response, "Add another Inner2")
self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
@ -959,7 +1010,14 @@ class TestInlinePermissions(TestCase):
self.assertIs(
response.context["inline_admin_formset"].has_delete_permission, False
)
self.assertContains(response, "<h2>Author-book relationships</h2>")
self.assertContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertContains(
response,
'<input type="hidden" name="Author_books-TOTAL_FORMS" value="0" '
@ -975,7 +1033,14 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(reverse("admin:admin_inlines_author_add"))
# No change permission on Books, so no inline
self.assertNotContains(response, "<h2>Author-book relationships</h2>")
self.assertNotContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
@ -986,7 +1051,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(reverse("admin:admin_inlines_holder2_add"))
# Add permission on inner2s, so we get the inline
self.assertContains(response, "<h2>Inner2s</h2>")
self.assertContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
self.assertContains(response, "Add another Inner2")
self.assertContains(
response,
@ -1002,7 +1071,14 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
self.assertNotContains(response, "<h2>Author-book relationships</h2>")
self.assertNotContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertNotContains(response, "Add another Author-Book Relationship")
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
@ -1026,7 +1102,14 @@ class TestInlinePermissions(TestCase):
self.assertIs(
response.context["inline_admin_formset"].has_delete_permission, False
)
self.assertContains(response, "<h2>Author-book relationships</h2>")
self.assertContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertContains(
response,
'<input type="hidden" name="Author_books-TOTAL_FORMS" value="1" '
@ -1059,7 +1142,14 @@ class TestInlinePermissions(TestCase):
self.assertIs(
response.context["inline_admin_formset"].has_delete_permission, True
)
self.assertContains(response, "<h2>Author-book relationships</h2>")
self.assertContains(
response,
(
'<h2 id="Author_books-heading" class="inline-heading">'
"Author-book relationships</h2>"
),
html=True,
)
self.assertContains(response, "Add another Author-book relationship")
self.assertContains(
response,
@ -1082,7 +1172,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Add permission on inner2s, so we can add but not modify existing
self.assertContains(response, "<h2>Inner2s</h2>")
self.assertContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
self.assertContains(response, "Add another Inner2")
# 3 extra forms only, not the existing instance form
self.assertContains(
@ -1105,7 +1199,16 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Change permission on inner2s, so we can change existing but not add new
self.assertContains(response, "<h2>Inner2s</h2>", count=2)
self.assertContains(
response,
'<h2 id="inner2_set-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
self.assertContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
# Just the one form for existing instances
self.assertContains(
response,
@ -1148,7 +1251,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Add/change perm, so we can add new and change existing
self.assertContains(response, "<h2>Inner2s</h2>")
self.assertContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
# One form for existing instance and three extra for new
self.assertContains(
response,
@ -1174,7 +1281,11 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Change/delete perm on inner2s, so we can change/delete existing
self.assertContains(response, "<h2>Inner2s</h2>")
self.assertContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
# One form for existing instance only, no new
self.assertContains(
response,
@ -1205,7 +1316,16 @@ class TestInlinePermissions(TestCase):
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# All perms on inner2s, so we can add/change/delete
self.assertContains(response, "<h2>Inner2s</h2>", count=2)
self.assertContains(
response,
'<h2 id="inner2_set-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
self.assertContains(
response,
'<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
html=True,
)
# One form for existing instance only, three for new
self.assertContains(
response,
@ -1367,22 +1487,69 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
response = modeladmin.changeform_view(request)
self.assertNotContains(response, "Add another Profile")
# Non-verbose model.
self.assertContains(response, "<h2>Non-verbose childss</h2>")
self.assertContains(
response,
(
'<h2 id="profile_set-heading" class="inline-heading">'
"Non-verbose childss</h2>"
),
html=True,
)
self.assertContains(response, "Add another Non-verbose child")
self.assertNotContains(response, "<h2>Profiles</h2>")
self.assertNotContains(
response,
'<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
html=True,
)
# Model with verbose name.
self.assertContains(response, "<h2>Childs with verbose names</h2>")
self.assertContains(
response,
(
'<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
"Childs with verbose names</h2>"
),
html=True,
)
self.assertContains(response, "Add another Childs with verbose name")
self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
self.assertNotContains(
response,
'<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
"Model with verbose name onlys</h2>",
html=True,
)
self.assertNotContains(response, "Add another Model with verbose name only")
# Model with verbose name plural.
self.assertContains(response, "<h2>Childs with verbose name plurals</h2>")
self.assertContains(
response,
(
'<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
"Childs with verbose name plurals</h2>"
),
html=True,
)
self.assertContains(response, "Add another Childs with verbose name plural")
self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
self.assertNotContains(
response,
'<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
"Model with verbose name plural only</h2>",
html=True,
)
# Model with both verbose names.
self.assertContains(response, "<h2>Childs with both verbose namess</h2>")
self.assertContains(
response,
(
'<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
"Childs with both verbose namess</h2>"
),
html=True,
)
self.assertContains(response, "Add another Childs with both verbose names")
self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
self.assertNotContains(
response,
'<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
"Model with both - plural name</h2>",
html=True,
)
self.assertNotContains(response, "Add another Model with both - name")
def test_verbose_name_plural_inline(self):
@ -1415,21 +1582,68 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
request.user = self.superuser
response = modeladmin.changeform_view(request)
# Non-verbose model.
self.assertContains(response, "<h2>Non-verbose childs</h2>")
self.assertContains(
response,
(
'<h2 id="profile_set-heading" class="inline-heading">'
"Non-verbose childs</h2>"
),
html=True,
)
self.assertContains(response, "Add another Profile")
self.assertNotContains(response, "<h2>Profiles</h2>")
self.assertNotContains(
response,
'<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
html=True,
)
# Model with verbose name.
self.assertContains(response, "<h2>Childs with verbose name</h2>")
self.assertContains(
response,
(
'<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
"Childs with verbose name</h2>"
),
html=True,
)
self.assertContains(response, "Add another Model with verbose name only")
self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
self.assertNotContains(
response,
'<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
"Model with verbose name onlys</h2>",
html=True,
)
# Model with verbose name plural.
self.assertContains(response, "<h2>Childs with verbose name plural</h2>")
self.assertContains(
response,
(
'<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
"Childs with verbose name plural</h2>"
),
html=True,
)
self.assertContains(response, "Add another Profile")
self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
self.assertNotContains(
response,
'<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
"Model with verbose name plural only</h2>",
html=True,
)
# Model with both verbose names.
self.assertContains(response, "<h2>Childs with both verbose names</h2>")
self.assertContains(
response,
(
'<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
"Childs with both verbose names</h2>"
),
html=True,
)
self.assertContains(response, "Add another Model with both - name")
self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
self.assertNotContains(
response,
'<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
"Model with both - plural name</h2>",
html=True,
)
def test_both_verbose_names_inline(self):
class NonVerboseProfileInline(TabularInline):
@ -1466,30 +1680,148 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
response = modeladmin.changeform_view(request)
self.assertNotContains(response, "Add another Profile")
# Non-verbose model.
self.assertContains(response, "<h2>Non-verbose childs - plural name</h2>")
self.assertContains(
response,
(
'<h2 id="profile_set-heading" class="inline-heading">'
"Non-verbose childs - plural name</h2>"
),
html=True,
)
self.assertContains(response, "Add another Non-verbose childs - name")
self.assertNotContains(response, "<h2>Profiles</h2>")
self.assertNotContains(
response,
'<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
html=True,
)
# Model with verbose name.
self.assertContains(response, "<h2>Childs with verbose name - plural name</h2>")
self.assertContains(
response,
(
'<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
"Childs with verbose name - plural name</h2>"
),
html=True,
)
self.assertContains(response, "Add another Childs with verbose name - name")
self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
self.assertNotContains(
response,
'<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
"Model with verbose name onlys</h2>",
html=True,
)
# Model with verbose name plural.
self.assertContains(
response,
"<h2>Childs with verbose name plural - plural name</h2>",
(
'<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
"Childs with verbose name plural - plural name</h2>"
),
html=True,
)
self.assertContains(
response,
"Add another Childs with verbose name plural - name",
)
self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
self.assertNotContains(
response,
'<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
"Model with verbose name plural only</h2>",
html=True,
)
# Model with both verbose names.
self.assertContains(response, "<h2>Childs with both - plural name</h2>")
self.assertContains(
response,
'<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
"Childs with both - plural name</h2>",
html=True,
)
self.assertContains(response, "Add another Childs with both - name")
self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
self.assertNotContains(
response,
'<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
"Model with both - plural name</h2>",
html=True,
)
self.assertNotContains(response, "Add another Model with both - name")
@override_settings(ROOT_URLCONF="admin_inlines.urls")
class TestInlineWithFieldsets(TestDataMixin, TestCase):
def setUp(self):
self.client.force_login(self.superuser)
def test_inline_headings(self):
response = self.client.get(reverse("admin:admin_inlines_photographer_add"))
# Page main title.
self.assertContains(response, "<h1>Add photographer</h1>", html=True)
# Headings for the toplevel fieldsets. The first one has no name.
self.assertContains(response, '<fieldset class="module aligned ">')
# The second and third have the same "Advanced options" name, but the
# second one has the "collapse" class.
for x, classes in ((1, ""), (2, "collapse")):
heading_id = f"fieldset-0-advanced-options-{x}-heading"
with self.subTest(heading_id=heading_id):
self.assertContains(
response,
f'<fieldset class="module aligned {classes}" '
f'aria-labelledby="{heading_id}">',
)
self.assertContains(
response,
f'<h2 id="{heading_id}" class="fieldset-heading">'
"Advanced options</h2>",
)
self.assertContains(response, f'id="{heading_id}"', count=1)
# Headings and subheadings for all the inlines.
for inline_admin_formset in response.context["inline_admin_formsets"]:
prefix = inline_admin_formset.formset.prefix
heading_id = f"{prefix}-heading"
formset_heading = (
f'<h2 id="{heading_id}" class="inline-heading">Photos</h2>'
)
self.assertContains(response, formset_heading, html=True)
self.assertContains(response, f'id="{heading_id}"', count=1)
# If this is a TabularInline, do not make further asserts since
# fieldsets are not shown as such in this table layout.
if "tabular" in inline_admin_formset.opts.template:
continue
# Headings for every formset (the amount depends on `extra`).
for y, inline_admin_form in enumerate(inline_admin_formset):
y_plus_one = y + 1
form_heading = (
f'<h3><b>Photo:</b> <span class="inline_label">#{y_plus_one}</span>'
"</h3>"
)
self.assertContains(response, form_heading, html=True)
# Every fieldset defined for an inline's form.
for z, fieldset in enumerate(inline_admin_form):
if fieldset.name:
heading_id = f"{prefix}-{y}-details-{z}-heading"
self.assertContains(
response,
f'<fieldset class="module aligned {fieldset.classes}" '
f'aria-labelledby="{heading_id}">',
)
fieldset_heading = (
f'<h4 id="{heading_id}" class="fieldset-heading">'
f"Details</h4>"
)
self.assertContains(response, fieldset_heading)
self.assertContains(response, f'id="{heading_id}"', count=1)
else:
fieldset_html = (
f'<fieldset class="module aligned {fieldset.classes}">'
)
self.assertContains(response, fieldset_html)
@override_settings(ROOT_URLCONF="admin_inlines.urls")
class SeleniumTests(AdminSeleniumTestCase):
available_apps = ["admin_inlines"] + AdminSeleniumTestCase.available_apps