mirror of https://github.com/django/django.git
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:
parent
9c5fe93349
commit
01ed59f753
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue