summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorantoliny0919 <antoliny0919@gmail.com>2025-07-28 07:59:26 +0900
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-08-19 16:35:56 +0200
commit4187da258fe212d494cb578a0bc2b52c4979ab95 (patch)
tree67f6205a16b1f6785f1ed4e06348d332c36a50f3
parentad4a9e0f3b1de261409bc083aa49dba705531824 (diff)
Fixed #35892 -- Supported Widget.use_fieldset in admin forms.
-rw-r--r--django/contrib/admin/helpers.py3
-rw-r--r--django/contrib/admin/static/admin/css/forms.css21
-rw-r--r--django/contrib/admin/static/admin/css/responsive.css2
-rw-r--r--django/contrib/admin/static/admin/css/widgets.css4
-rw-r--r--django/contrib/admin/static/admin/js/SelectFilter2.js30
-rw-r--r--django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js3
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html2
-rw-r--r--django/contrib/admin/templates/admin/includes/fieldset.html6
-rw-r--r--django/contrib/admin/templates/admin/widgets/split_datetime.html4
-rw-r--r--django/contrib/admin/widgets.py19
-rw-r--r--django/forms/widgets.py1
-rw-r--r--docs/intro/_images/admin05t.pngbin35611 -> 34301 bytes
-rw-r--r--docs/intro/_images/admin07.pngbin12924 -> 18339 bytes
-rw-r--r--docs/intro/_images/admin08t.pngbin14547 -> 20706 bytes
-rw-r--r--js_tests/admin/DateTimeShortcuts.test.js21
-rw-r--r--js_tests/admin/SelectFilter2.test.js8
-rw-r--r--tests/admin_views/admin.py6
-rw-r--r--tests/admin_views/models.py16
-rw-r--r--tests/admin_views/tests.py25
-rw-r--r--tests/admin_widgets/tests.py40
-rw-r--r--tests/auth_tests/test_views.py6
-rw-r--r--tests/forms_tests/widget_tests/test_clearablefileinput.py15
22 files changed, 193 insertions, 39 deletions
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 969167f0e2..bd37f519cd 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -173,6 +173,7 @@ class AdminField:
self.is_first = is_first # Whether this field is first on the line
self.is_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput)
self.is_readonly = False
+ self.is_fieldset = self.field.field.widget.use_fieldset
def label_tag(self):
classes = []
@@ -185,12 +186,14 @@ class AdminField:
if not self.is_first:
classes.append("inline")
attrs = {"class": " ".join(classes)} if classes else {}
+ tag = "legend" if self.is_fieldset else None
# checkboxes should not have a label suffix as the checkbox appears
# to the left of the label.
return self.field.label_tag(
contents=mark_safe(contents),
attrs=attrs,
label_suffix="" if self.is_checkbox else None,
+ tag=tag,
)
def errors(self):
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css
index cf96331dea..057c5258f2 100644
--- a/django/contrib/admin/static/admin/css/forms.css
+++ b/django/contrib/admin/static/admin/css/forms.css
@@ -36,12 +36,13 @@ form .form-row p {
/* FORM LABELS */
-label {
+legend, label {
font-weight: normal;
color: var(--body-quiet-color);
font-size: 0.8125rem;
}
+.required legend, legend.required,
.required label, label.required {
font-weight: bold;
}
@@ -91,6 +92,20 @@ fieldset .inline-heading,
/* ALIGNED FIELDSETS */
+.aligned fieldset {
+ width: 100%;
+ border-top: none;
+}
+
+.aligned fieldset > div {
+ width: 100%;
+}
+
+.aligned legend {
+ float: left;
+}
+
+.aligned legend,
.aligned label {
display: block;
padding: 4px 10px 0 0;
@@ -138,6 +153,10 @@ form .aligned div.radiolist {
padding: 0;
}
+form .aligned fieldset div.help {
+ margin-left: 0;
+}
+
form .aligned p.help,
form .aligned div.help {
margin-top: 0;
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css
index bf6cd6a56c..fbf0d88b40 100644
--- a/django/contrib/admin/static/admin/css/responsive.css
+++ b/django/contrib/admin/static/admin/css/responsive.css
@@ -170,6 +170,7 @@ input[type="submit"], button {
/* Forms */
+ legend,
label {
font-size: 1rem;
}
@@ -484,6 +485,7 @@ input[type="submit"], button {
padding-top: 15px;
}
+ .aligned legend,
.aligned label {
width: 100%;
min-width: auto;
diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css
index 6a61821254..ea967018fb 100644
--- a/django/contrib/admin/static/admin/css/widgets.css
+++ b/django/contrib/admin/static/admin/css/widgets.css
@@ -301,6 +301,10 @@ p.datetime {
font-weight: bold;
}
+p.datetime label {
+ display: inline;
+}
+
.datetime span {
white-space: nowrap;
font-weight: normal;
diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js
index 970b511b0c..2100280220 100644
--- a/django/contrib/admin/static/admin/js/SelectFilter2.js
+++ b/django/contrib/admin/static/admin/js/SelectFilter2.js
@@ -15,7 +15,8 @@ Requires core.js and SelectBox.js.
const from_box = document.getElementById(field_id);
from_box.id += '_from'; // change its ID
from_box.className = 'filtered';
- from_box.setAttribute('aria-labelledby', field_id + '_from_title');
+ from_box.setAttribute('aria-labelledby', field_id + '_from_label');
+ from_box.setAttribute('aria-describedby', `${field_id}_helptext ${field_id}_choose_helptext`);
for (const p of from_box.parentNode.getElementsByTagName('p')) {
if (p.classList.contains("info")) {
@@ -42,12 +43,20 @@ Requires core.js and SelectBox.js.
const selector_available_title = quickElement('div', selector_available);
selector_available_title.id = field_id + '_from_title';
selector_available_title.className = 'selector-available-title';
- quickElement('label', selector_available_title, interpolate(gettext('Available %s') + ' ', [field_name]), 'for', field_id + '_from');
+ quickElement(
+ 'label',
+ selector_available_title,
+ interpolate(gettext('Available %s') + ' ', [field_name]),
+ 'id',
+ field_id + '_from_label',
+ 'for',
+ field_id + '_from'
+ );
quickElement(
'p',
selector_available_title,
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
- 'class', 'helptext'
+ 'id', `${field_id}_choose_helptext`, 'class', 'helptext'
);
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
@@ -102,12 +111,20 @@ Requires core.js and SelectBox.js.
const selector_chosen_title = quickElement('div', selector_chosen);
selector_chosen_title.className = 'selector-chosen-title';
selector_chosen_title.id = field_id + '_to_title';
- quickElement('label', selector_chosen_title, interpolate(gettext('Chosen %s') + ' ', [field_name]), 'for', field_id + '_to');
+ quickElement(
+ 'label',
+ selector_chosen_title,
+ interpolate(gettext('Chosen %s') + ' ', [field_name]),
+ 'id',
+ field_id + '_to_label',
+ 'for',
+ field_id + '_to'
+ );
quickElement(
'p',
selector_chosen_title,
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
- 'class', 'helptext'
+ 'id', `${field_id}_remove_helptext`, 'class', 'helptext'
);
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
@@ -134,7 +151,8 @@ Requires core.js and SelectBox.js.
'multiple', '',
'size', from_box.size,
'name', from_box.name,
- 'aria-labelledby', field_id + '_to_title',
+ 'aria-labelledby', field_id + '_to_label',
+ 'aria-describedby', `${field_id}_helptext ${field_id}_remove_helptext`,
'class', 'filtered'
);
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
index 8168172a97..6251614863 100644
--- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
+++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
@@ -91,7 +91,10 @@
message = interpolate(message, [timezoneOffset]);
const warning = document.createElement('div');
+ const id = inp.id;
+ const field_id = inp.closest('p.datetime') ? id.slice(0, id.lastIndexOf("_")) : id;
warning.classList.add('help', warningClass);
+ warning.id = `${field_id}_timezone_warning_helptext`;
warning.textContent = message;
inp.parentNode.appendChild(warning);
},
diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html
index ce20c8ac44..01c357d8c7 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -40,7 +40,7 @@
<div class="form-row">
{{ form.usable_password.errors }}
- <div class="flex-container">{{ form.usable_password.label_tag }} {{ form.usable_password }}</div>
+ <fieldset class="flex-container">{{ form.usable_password.legend_tag }} {{ form.usable_password }}</fieldset>
{% if form.usable_password.help_text %}
<div class="help"{% if form.usable_password.id_for_label %} id="{{ form.usable_password.id_for_label }}_helptext"{% endif %}>
<p>{{ form.usable_password.help_text|safe }}</p>
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
index 9c9b31965a..aa8d94b41d 100644
--- a/django/contrib/admin/templates/admin/includes/fieldset.html
+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
@@ -11,13 +11,14 @@
<div class="form-row{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
{% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %}
{% for field in line %}
+ {% if field.is_fieldset %}<fieldset class="flex-container"{% if field.field.help_text %} aria-describedby="{{ field.field.id_for_label }}_helptext"{% endif %}>{{ field.label_tag }}{% endif %}
<div>
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% endif %}{% if field.is_checkbox %} checkbox-row{% endif %}">
{% if field.is_checkbox %}
- {{ field.field }}{{ field.label_tag }}
+ {{ field.field }}{% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
{% else %}
- {{ field.label_tag }}
+ {% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
@@ -31,6 +32,7 @@
</div>
{% endif %}
</div>
+ {% if field.is_fieldset %}</fieldset>{% endif %}
{% endfor %}
{% if not line.fields|length == 1 %}</div>{% endif %}
</div>
diff --git a/django/contrib/admin/templates/admin/widgets/split_datetime.html b/django/contrib/admin/templates/admin/widgets/split_datetime.html
index 7fc7bf6833..ac73fa361f 100644
--- a/django/contrib/admin/templates/admin/widgets/split_datetime.html
+++ b/django/contrib/admin/templates/admin/widgets/split_datetime.html
@@ -1,4 +1,4 @@
<p class="datetime">
- {{ date_label }} {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br>
- {{ time_label }} {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %}
+ <label {% if widget.attrs.id %}for="{{ widget.subwidgets.0.attrs.id }}"{% endif %}>{{ date_label }}</label> {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br>
+ <label {% if widget.attrs.id %}for="{{ widget.subwidgets.1.attrs.id }}"{% endif %}>{{ time_label }}</label> {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %}
</p>
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index a601fc2667..124f3307af 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -49,7 +49,16 @@ class FilteredSelectMultiple(forms.SelectMultiple):
return context
-class BaseAdminDateWidget(forms.DateInput):
+class DateTimeWidgetContextMixin:
+ def get_context(self, name, value, attrs):
+ context = super().get_context(name, value, attrs)
+ context["widget"]["attrs"][
+ "aria-describedby"
+ ] = f"id_{name}_timezone_warning_helptext"
+ return context
+
+
+class BaseAdminDateWidget(DateTimeWidgetContextMixin, forms.DateInput):
class Media:
js = [
"admin/js/calendar.js",
@@ -65,7 +74,7 @@ class AdminDateWidget(BaseAdminDateWidget):
template_name = "admin/widgets/date.html"
-class BaseAdminTimeWidget(forms.TimeInput):
+class BaseAdminTimeWidget(DateTimeWidgetContextMixin, forms.TimeInput):
class Media:
js = [
"admin/js/calendar.js",
@@ -98,8 +107,13 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
context = super().get_context(name, value, attrs)
context["date_label"] = _("Date:")
context["time_label"] = _("Time:")
+ for widget in context["widget"]["subwidgets"]:
+ widget["attrs"]["aria-describedby"] = f"id_{name}_timezone_warning_helptext"
return context
+ def id_for_label(self, id_):
+ return id_
+
class AdminRadioSelect(forms.RadioSelect):
template_name = "admin/widgets/radio.html"
@@ -282,6 +296,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
self.can_view_related = not multiple and can_view_related
# To check if the related object is registered with this AdminSite.
self.admin_site = admin_site
+ self.use_fieldset = True
def __deepcopy__(self, memo):
obj = copy.copy(self)
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 5a25b66e9a..b77e57abce 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -530,6 +530,7 @@ class ClearableFileInput(FileInput):
input_text = _("Change")
template_name = "django/forms/widgets/clearable_file_input.html"
checked = False
+ use_fieldset = True
def clear_checkbox_name(self, name):
"""
diff --git a/docs/intro/_images/admin05t.png b/docs/intro/_images/admin05t.png
index 0007fae910..de66ee7746 100644
--- a/docs/intro/_images/admin05t.png
+++ b/docs/intro/_images/admin05t.png
Binary files differ
diff --git a/docs/intro/_images/admin07.png b/docs/intro/_images/admin07.png
index d13b0e9ef8..b591e82217 100644
--- a/docs/intro/_images/admin07.png
+++ b/docs/intro/_images/admin07.png
Binary files differ
diff --git a/docs/intro/_images/admin08t.png b/docs/intro/_images/admin08t.png
index 824bf562e2..b77fdc03c6 100644
--- a/docs/intro/_images/admin08t.png
+++ b/docs/intro/_images/admin08t.png
Binary files differ
diff --git a/js_tests/admin/DateTimeShortcuts.test.js b/js_tests/admin/DateTimeShortcuts.test.js
index 9a8cb8d311..6cb534610c 100644
--- a/js_tests/admin/DateTimeShortcuts.test.js
+++ b/js_tests/admin/DateTimeShortcuts.test.js
@@ -30,13 +30,30 @@ QUnit.test('custom time shortcuts', function(assert) {
assert.equal($('.clockbox').find('a').first().text(), '3 a.m.');
});
-QUnit.test('time zone offset warning', function(assert) {
+QUnit.test('time zone offset warning - single field', function(assert) {
const $ = django.jQuery;
const savedOffset = $('body').attr('data-admin-utc-offset');
- const timeField = $('<input type="text" name="time_test" class="vTimeField">');
+ // Single date or time field.
+ const timeField = $('<input id="id_updated_at" type="text" name="updated_at" class="vTimeField">');
$('#qunit-fixture').append(timeField);
$('body').attr('data-admin-utc-offset', new Date().getTimezoneOffset() * -60 + 3600);
DateTimeShortcuts.init();
$('body').attr('data-admin-utc-offset', savedOffset);
assert.equal($('.timezonewarning').text(), 'Note: You are 1 hour behind server time.');
+ assert.equal($('.timezonewarning').attr("id"), "id_updated_at_timezone_warning_helptext");
+});
+
+QUnit.test('time zone offset warning - date and time field', function(assert) {
+ const $ = django.jQuery;
+ const savedOffset = $('body').attr('data-admin-utc-offset');
+ // DateTimeField with fieldset containing date and time inputs.
+ const dateTimeField = '<p class="datetime">' +
+ '<input id="id_updated_at_0" type="text" name="updated_at_0" class="vDateField">' +
+ '<input id="id_updated_at_1" type="text" name="updated_at_1" class="vTimeField">' +
+ '</p>';
+ $('#qunit-fixture').append($(dateTimeField));
+ $('body').attr('data-admin-utc-offset', new Date().getTimezoneOffset() * -60 + 3600);
+ DateTimeShortcuts.init();
+ $('body').attr('data-admin-utc-offset', savedOffset);
+ assert.equal($('.timezonewarning').attr("id"), "id_updated_at_timezone_warning_helptext");
});
diff --git a/js_tests/admin/SelectFilter2.test.js b/js_tests/admin/SelectFilter2.test.js
index 1fd46bd0ce..533c24811c 100644
--- a/js_tests/admin/SelectFilter2.test.js
+++ b/js_tests/admin/SelectFilter2.test.js
@@ -13,7 +13,9 @@ QUnit.test('init', function(assert) {
assert.equal($('#test').children().first().prop("tagName"), "DIV");
assert.equal($('#test').children().first().attr("class"), "selector");
assert.equal($('.selector-available label').text().trim(), "Available things");
+ assert.equal($('.selector-available label').attr("id"), "id_from_label");
assert.equal($('.selector-chosen label').text().trim(), "Chosen things");
+ assert.equal($('.selector-chosen label').attr("id"), "id_to_label");
assert.equal($('.selector-chosen select')[0].getAttribute('multiple'), '');
assert.equal($('.selector-chooseall').text(), "Choose all things");
assert.equal($('.selector-chooseall').prop("tagName"), "BUTTON");
@@ -23,10 +25,12 @@ QUnit.test('init', function(assert) {
assert.equal($('.selector-remove').prop("tagName"), "BUTTON");
assert.equal($('.selector-clearall').text(), "Remove all things");
assert.equal($('.selector-clearall').prop("tagName"), "BUTTON");
- assert.equal($('.selector-available .filtered').attr("aria-labelledby"), "id_from_title");
+ assert.equal($('.selector-available .filtered').attr("aria-labelledby"), "id_from_label");
+ assert.equal($('.selector-available .filtered').attr("aria-describedby"), "id_helptext id_choose_helptext");
assert.equal($('.selector-available .selector-available-title label').text(), "Available things ");
assert.equal($('.selector-available .selector-available-title .helptext').text(), 'Choose things by selecting them and then select the "Choose" arrow button.');
- assert.equal($('.selector-chosen .filtered').attr("aria-labelledby"), "id_to_title");
+ assert.equal($('.selector-chosen .filtered').attr("aria-labelledby"), "id_to_label");
+ assert.equal($('.selector-chosen .filtered').attr("aria-describedby"), "id_helptext id_remove_helptext");
assert.equal($('.selector-chosen .selector-chosen-title label').text(), "Chosen things ");
assert.equal($('.selector-chosen .selector-chosen-title .helptext').text(), 'Remove things by selecting them and then select the "Remove" arrow button.');
assert.equal($('.selector-filter label .help-tooltip')[0].getAttribute("aria-label"), "Type into this box to filter down the list of available things.");
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index b6d009b388..69570a8062 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -47,6 +47,7 @@ from .models import (
Color2,
ComplexSortedPerson,
Country,
+ Course,
CoverLetter,
CustomArticle,
CyclicOne,
@@ -1190,6 +1191,10 @@ class CamelCaseAdmin(admin.ModelAdmin):
filter_horizontal = ["m2m"]
+class CourseAdmin(admin.ModelAdmin):
+ radio_fields = {"difficulty": admin.VERTICAL}
+
+
site = admin.AdminSite(name="admin")
site.site_url = "/my-site-url/"
site.register(Article, ArticleAdmin)
@@ -1280,6 +1285,7 @@ site.register(ChapterXtra1, ChapterXtra1Admin)
site.register(Pizza, PizzaAdmin)
site.register(ReadOnlyPizza, ReadOnlyPizzaAdmin)
site.register(ReadablePizza)
+site.register(Course, CourseAdmin)
site.register(Topping, ToppingAdmin)
site.register(Album, AlbumAdmin)
site.register(Song)
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index 8f102dece3..4e2d13efa8 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -623,6 +623,22 @@ class CyclicTwo(models.Model):
return self.name
+class Course(models.Model):
+ DIFFICULTY_CHOICES = [
+ ("beginner", "Beginner Class"),
+ ("intermediate", "Intermediate Class"),
+ ("advanced", "Advanced Class"),
+ ]
+
+ title = models.CharField(max_length=100)
+ materials = models.FileField(upload_to="test_upload")
+ difficulty = models.CharField(
+ max_length=20, choices=DIFFICULTY_CHOICES, null=True, blank=True
+ )
+ categories = models.ManyToManyField(Category, blank=True)
+ start_datetime = models.DateTimeField(null=True, blank=True)
+
+
class Topping(models.Model):
name = models.CharField(max_length=20)
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 26fdacfa00..db638c1ea8 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -70,6 +70,7 @@ from .models import (
Color,
ComplexSortedPerson,
Country,
+ Course,
CoverLetter,
CustomArticle,
CyclicOne,
@@ -6908,6 +6909,30 @@ class SeleniumTests(AdminSeleniumTestCase):
name_input_value = name_input.get_attribute("value")
self.assertEqual(name_input_value, "Test section 1")
+ def test_use_fieldset_fields_render(self):
+ from selenium.webdriver.common.by import By
+
+ self.admin_login(
+ username="super", password="secret", login_url=reverse("admin:index")
+ )
+ course = Course.objects.create(
+ title="Django Class", materials="django_documents"
+ )
+ expected_legend_tags_text = [
+ "Materials:",
+ "Difficulty:",
+ "Categories:",
+ "Start datetime:",
+ ]
+ url = reverse("admin:admin_views_course_change", args=(course.pk,))
+ self.selenium.get(self.live_server_url + url)
+ fieldsets = self.selenium.find_elements(
+ By.CSS_SELECTOR, "fieldset.aligned fieldset"
+ )
+ for index, fieldset in enumerate(fieldsets):
+ legend = fieldset.find_element(By.TAG_NAME, "legend")
+ self.assertEqual(legend.text, expected_legend_tags_text[index])
+
@screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
@override_settings(MESSAGE_LEVEL=10)
def test_messages(self):
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index c47e0e3ec1..9a5c846bdd 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -399,7 +399,8 @@ class AdminDateWidgetTest(SimpleTestCase):
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'<p class="date">'
- '<input value="2007-12-01" type="text" class="vDateField" name="test" '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="2007-12-01" type="text" class="vDateField" name="test" '
'size="10"></p>',
)
# pass attrs to widget
@@ -407,7 +408,8 @@ class AdminDateWidgetTest(SimpleTestCase):
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'<p class="date">'
- '<input value="2007-12-01" type="text" class="myDateField" name="test" '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="2007-12-01" type="text" class="myDateField" name="test" '
'size="20"></p>',
)
@@ -418,7 +420,8 @@ class AdminTimeWidgetTest(SimpleTestCase):
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'<p class="time">'
- '<input value="09:30:00" type="text" class="vTimeField" name="test" '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="09:30:00" type="text" class="vTimeField" name="test" '
'size="8"></p>',
)
# pass attrs to widget
@@ -426,7 +429,8 @@ class AdminTimeWidgetTest(SimpleTestCase):
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'<p class="time">'
- '<input value="09:30:00" type="text" class="myTimeField" name="test" '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="09:30:00" type="text" class="myTimeField" name="test" '
'size="20"></p>',
)
@@ -435,12 +439,16 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
def test_render(self):
w = widgets.AdminSplitDateTime()
self.assertHTMLEqual(
- w.render("test", datetime(2007, 12, 1, 9, 30)),
+ w.render("test", datetime(2007, 12, 1, 9, 30), attrs={"id": "id_test"}),
'<p class="datetime">'
- 'Date: <input value="2007-12-01" type="text" class="vDateField" '
- 'name="test_0" size="10"><br>'
- 'Time: <input value="09:30:00" type="text" class="vTimeField" '
- 'name="test_1" size="8"></p>',
+ '<label for="id_test_0">Date:</label> '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="2007-12-01" type="text" class="vDateField" '
+ 'name="test_0" size="10" id="id_test_0"><br>'
+ '<label for="id_test_1">Time:</label> '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="09:30:00" type="text" class="vTimeField" '
+ 'name="test_1" size="8" id="id_test_1"></p>',
)
def test_localization(self):
@@ -449,12 +457,16 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
with translation.override("de-at"):
w.is_localized = True
self.assertHTMLEqual(
- w.render("test", datetime(2007, 12, 1, 9, 30)),
+ w.render("test", datetime(2007, 12, 1, 9, 30), attrs={"id": "id_test"}),
'<p class="datetime">'
- 'Datum: <input value="01.12.2007" type="text" '
- 'class="vDateField" name="test_0"size="10"><br>'
- 'Zeit: <input value="09:30:00" type="text" class="vTimeField" '
- 'name="test_1" size="8"></p>',
+ '<label for="id_test_0">Datum:</label> '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="01.12.2007" type="text" '
+ 'class="vDateField" name="test_0" size="10" id="id_test_0"><br>'
+ '<label for="id_test_1">Zeit:</label> '
+ '<input aria-describedby="id_test_timezone_warning_helptext" '
+ 'value="09:30:00" type="text" class="vTimeField" '
+ 'name="test_1" size="8" id="id_test_1"></p>',
)
diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
index 0a4f7d28c8..23dc858ddd 100644
--- a/tests/auth_tests/test_views.py
+++ b/tests/auth_tests/test_views.py
@@ -1550,6 +1550,12 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
self.assertContains(
response, f"{self.admin.username}</a>\n&rsaquo; Change password"
)
+ # Usable password field.
+ self.assertContains(
+ response,
+ '<fieldset class="flex-container">'
+ "<legend>Password-based authentication:</legend>",
+ )
# Submit buttons
self.assertContains(response, '<input type="submit" name="set-password"')
self.assertContains(response, '<input type="submit" name="unset-password"')
diff --git a/tests/forms_tests/widget_tests/test_clearablefileinput.py b/tests/forms_tests/widget_tests/test_clearablefileinput.py
index ae54cc4b5d..8d3bff4b45 100644
--- a/tests/forms_tests/widget_tests/test_clearablefileinput.py
+++ b/tests/forms_tests/widget_tests/test_clearablefileinput.py
@@ -246,18 +246,19 @@ class ClearableFileInputTest(WidgetTest):
)
form = TestForm()
- self.assertIs(self.widget.use_fieldset, False)
+ self.assertIs(self.widget.use_fieldset, True)
self.assertHTMLEqual(
- '<div><label for="id_field">Field:</label>'
- '<input id="id_field" name="field" type="file" required></div>'
- '<div><label for="id_with_file">With file:</label>Currently: '
+ '<div><fieldset><legend for="id_field">Field:</legend>'
+ '<input id="id_field" name="field" type="file" required></fieldset></div>'
+ '<div><fieldset><legend for="id_with_file">With file:</legend>Currently: '
'<a href="something">something</a><br>Change:<input type="file" '
- 'name="with_file" id="id_with_file"></div>'
- '<div><label for="id_clearable_file">Clearable file:</label>'
+ 'name="with_file" id="id_with_file"></fieldset></div>'
+ '<div><fieldset><legend for="id_clearable_file">Clearable file:</legend>'
'Currently: <a href="something">something</a><input '
'type="checkbox" name="clearable_file-clear" id="clearable_file-clear_id">'
'<label for="clearable_file-clear_id">Clear</label><br>Change:'
- '<input type="file" name="clearable_file" id="id_clearable_file"></div>',
+ '<input type="file" name="clearable_file" id="id_clearable_file">'
+ "</fieldset></div>",
form.render(),
)