1
0
mirror of https://github.com/django/django.git synced 2025-10-26 15:16:09 +00:00

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

@@ -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