mirror of
https://github.com/django/django.git
synced 2025-08-20 16:59:12 +00:00
Fixed #35892 -- Supported Widget.use_fieldset in admin forms.
This commit is contained in:
parent
ad4a9e0f3b
commit
4187da258f
@ -173,6 +173,7 @@ class AdminField:
|
|||||||
self.is_first = is_first # Whether this field is first on the line
|
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_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput)
|
||||||
self.is_readonly = False
|
self.is_readonly = False
|
||||||
|
self.is_fieldset = self.field.field.widget.use_fieldset
|
||||||
|
|
||||||
def label_tag(self):
|
def label_tag(self):
|
||||||
classes = []
|
classes = []
|
||||||
@ -185,12 +186,14 @@ class AdminField:
|
|||||||
if not self.is_first:
|
if not self.is_first:
|
||||||
classes.append("inline")
|
classes.append("inline")
|
||||||
attrs = {"class": " ".join(classes)} if classes else {}
|
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
|
# checkboxes should not have a label suffix as the checkbox appears
|
||||||
# to the left of the label.
|
# to the left of the label.
|
||||||
return self.field.label_tag(
|
return self.field.label_tag(
|
||||||
contents=mark_safe(contents),
|
contents=mark_safe(contents),
|
||||||
attrs=attrs,
|
attrs=attrs,
|
||||||
label_suffix="" if self.is_checkbox else None,
|
label_suffix="" if self.is_checkbox else None,
|
||||||
|
tag=tag,
|
||||||
)
|
)
|
||||||
|
|
||||||
def errors(self):
|
def errors(self):
|
||||||
|
@ -36,12 +36,13 @@ form .form-row p {
|
|||||||
|
|
||||||
/* FORM LABELS */
|
/* FORM LABELS */
|
||||||
|
|
||||||
label {
|
legend, label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--body-quiet-color);
|
color: var(--body-quiet-color);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.required legend, legend.required,
|
||||||
.required label, label.required {
|
.required label, label.required {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@ -91,6 +92,20 @@ fieldset .inline-heading,
|
|||||||
|
|
||||||
/* ALIGNED FIELDSETS */
|
/* ALIGNED FIELDSETS */
|
||||||
|
|
||||||
|
.aligned fieldset {
|
||||||
|
width: 100%;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned fieldset > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned legend {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligned legend,
|
||||||
.aligned label {
|
.aligned label {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 4px 10px 0 0;
|
padding: 4px 10px 0 0;
|
||||||
@ -138,6 +153,10 @@ form .aligned div.radiolist {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form .aligned fieldset div.help {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
form .aligned p.help,
|
form .aligned p.help,
|
||||||
form .aligned div.help {
|
form .aligned div.help {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -170,6 +170,7 @@ input[type="submit"], button {
|
|||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
|
|
||||||
|
legend,
|
||||||
label {
|
label {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
@ -484,6 +485,7 @@ input[type="submit"], button {
|
|||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aligned legend,
|
||||||
.aligned label {
|
.aligned label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
|
@ -301,6 +301,10 @@ p.datetime {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.datetime label {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
.datetime span {
|
.datetime span {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -15,7 +15,8 @@ Requires core.js and SelectBox.js.
|
|||||||
const from_box = document.getElementById(field_id);
|
const from_box = document.getElementById(field_id);
|
||||||
from_box.id += '_from'; // change its ID
|
from_box.id += '_from'; // change its ID
|
||||||
from_box.className = 'filtered';
|
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')) {
|
for (const p of from_box.parentNode.getElementsByTagName('p')) {
|
||||||
if (p.classList.contains("info")) {
|
if (p.classList.contains("info")) {
|
||||||
@ -42,12 +43,20 @@ Requires core.js and SelectBox.js.
|
|||||||
const selector_available_title = quickElement('div', selector_available);
|
const selector_available_title = quickElement('div', selector_available);
|
||||||
selector_available_title.id = field_id + '_from_title';
|
selector_available_title.id = field_id + '_from_title';
|
||||||
selector_available_title.className = 'selector-available-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(
|
quickElement(
|
||||||
'p',
|
'p',
|
||||||
selector_available_title,
|
selector_available_title,
|
||||||
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
|
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');
|
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);
|
const selector_chosen_title = quickElement('div', selector_chosen);
|
||||||
selector_chosen_title.className = 'selector-chosen-title';
|
selector_chosen_title.className = 'selector-chosen-title';
|
||||||
selector_chosen_title.id = field_id + '_to_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(
|
quickElement(
|
||||||
'p',
|
'p',
|
||||||
selector_chosen_title,
|
selector_chosen_title,
|
||||||
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
|
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');
|
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
|
||||||
@ -134,7 +151,8 @@ Requires core.js and SelectBox.js.
|
|||||||
'multiple', '',
|
'multiple', '',
|
||||||
'size', from_box.size,
|
'size', from_box.size,
|
||||||
'name', from_box.name,
|
'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'
|
'class', 'filtered'
|
||||||
);
|
);
|
||||||
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
|
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
|
||||||
|
@ -91,7 +91,10 @@
|
|||||||
message = interpolate(message, [timezoneOffset]);
|
message = interpolate(message, [timezoneOffset]);
|
||||||
|
|
||||||
const warning = document.createElement('div');
|
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.classList.add('help', warningClass);
|
||||||
|
warning.id = `${field_id}_timezone_warning_helptext`;
|
||||||
warning.textContent = message;
|
warning.textContent = message;
|
||||||
inp.parentNode.appendChild(warning);
|
inp.parentNode.appendChild(warning);
|
||||||
},
|
},
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
{{ form.usable_password.errors }}
|
{{ 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 %}
|
{% 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 %}>
|
<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>
|
<p>{{ form.usable_password.help_text|safe }}</p>
|
||||||
|
@ -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 %}">
|
<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 %}
|
{% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %}
|
||||||
{% for field in line %}
|
{% 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>
|
<div>
|
||||||
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
{% 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 %}">
|
<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 %}
|
{% if field.is_checkbox %}
|
||||||
{{ field.field }}{{ field.label_tag }}
|
{{ field.field }}{% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field.label_tag }}
|
{% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
|
||||||
{% if field.is_readonly %}
|
{% if field.is_readonly %}
|
||||||
<div class="readonly">{{ field.contents }}</div>
|
<div class="readonly">{{ field.contents }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -31,6 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if field.is_fieldset %}</fieldset>{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not line.fields|length == 1 %}</div>{% endif %}
|
{% if not line.fields|length == 1 %}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<p class="datetime">
|
<p class="datetime">
|
||||||
{{ date_label }} {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br>
|
<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>
|
||||||
{{ time_label }} {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %}
|
<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>
|
</p>
|
||||||
|
@ -49,7 +49,16 @@ class FilteredSelectMultiple(forms.SelectMultiple):
|
|||||||
return context
|
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:
|
class Media:
|
||||||
js = [
|
js = [
|
||||||
"admin/js/calendar.js",
|
"admin/js/calendar.js",
|
||||||
@ -65,7 +74,7 @@ class AdminDateWidget(BaseAdminDateWidget):
|
|||||||
template_name = "admin/widgets/date.html"
|
template_name = "admin/widgets/date.html"
|
||||||
|
|
||||||
|
|
||||||
class BaseAdminTimeWidget(forms.TimeInput):
|
class BaseAdminTimeWidget(DateTimeWidgetContextMixin, forms.TimeInput):
|
||||||
class Media:
|
class Media:
|
||||||
js = [
|
js = [
|
||||||
"admin/js/calendar.js",
|
"admin/js/calendar.js",
|
||||||
@ -98,8 +107,13 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
|
|||||||
context = super().get_context(name, value, attrs)
|
context = super().get_context(name, value, attrs)
|
||||||
context["date_label"] = _("Date:")
|
context["date_label"] = _("Date:")
|
||||||
context["time_label"] = _("Time:")
|
context["time_label"] = _("Time:")
|
||||||
|
for widget in context["widget"]["subwidgets"]:
|
||||||
|
widget["attrs"]["aria-describedby"] = f"id_{name}_timezone_warning_helptext"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def id_for_label(self, id_):
|
||||||
|
return id_
|
||||||
|
|
||||||
|
|
||||||
class AdminRadioSelect(forms.RadioSelect):
|
class AdminRadioSelect(forms.RadioSelect):
|
||||||
template_name = "admin/widgets/radio.html"
|
template_name = "admin/widgets/radio.html"
|
||||||
@ -282,6 +296,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
|||||||
self.can_view_related = not multiple and can_view_related
|
self.can_view_related = not multiple and can_view_related
|
||||||
# To check if the related object is registered with this AdminSite.
|
# To check if the related object is registered with this AdminSite.
|
||||||
self.admin_site = admin_site
|
self.admin_site = admin_site
|
||||||
|
self.use_fieldset = True
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self, memo):
|
||||||
obj = copy.copy(self)
|
obj = copy.copy(self)
|
||||||
|
@ -530,6 +530,7 @@ class ClearableFileInput(FileInput):
|
|||||||
input_text = _("Change")
|
input_text = _("Change")
|
||||||
template_name = "django/forms/widgets/clearable_file_input.html"
|
template_name = "django/forms/widgets/clearable_file_input.html"
|
||||||
checked = False
|
checked = False
|
||||||
|
use_fieldset = True
|
||||||
|
|
||||||
def clear_checkbox_name(self, name):
|
def clear_checkbox_name(self, name):
|
||||||
"""
|
"""
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 20 KiB |
@ -30,13 +30,30 @@ QUnit.test('custom time shortcuts', function(assert) {
|
|||||||
assert.equal($('.clockbox').find('a').first().text(), '3 a.m.');
|
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 $ = django.jQuery;
|
||||||
const savedOffset = $('body').attr('data-admin-utc-offset');
|
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);
|
$('#qunit-fixture').append(timeField);
|
||||||
$('body').attr('data-admin-utc-offset', new Date().getTimezoneOffset() * -60 + 3600);
|
$('body').attr('data-admin-utc-offset', new Date().getTimezoneOffset() * -60 + 3600);
|
||||||
DateTimeShortcuts.init();
|
DateTimeShortcuts.init();
|
||||||
$('body').attr('data-admin-utc-offset', savedOffset);
|
$('body').attr('data-admin-utc-offset', savedOffset);
|
||||||
assert.equal($('.timezonewarning').text(), 'Note: You are 1 hour behind server time.');
|
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");
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,9 @@ QUnit.test('init', function(assert) {
|
|||||||
assert.equal($('#test').children().first().prop("tagName"), "DIV");
|
assert.equal($('#test').children().first().prop("tagName"), "DIV");
|
||||||
assert.equal($('#test').children().first().attr("class"), "selector");
|
assert.equal($('#test').children().first().attr("class"), "selector");
|
||||||
assert.equal($('.selector-available label').text().trim(), "Available things");
|
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').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-chosen select')[0].getAttribute('multiple'), '');
|
||||||
assert.equal($('.selector-chooseall').text(), "Choose all things");
|
assert.equal($('.selector-chooseall').text(), "Choose all things");
|
||||||
assert.equal($('.selector-chooseall').prop("tagName"), "BUTTON");
|
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-remove').prop("tagName"), "BUTTON");
|
||||||
assert.equal($('.selector-clearall').text(), "Remove all things");
|
assert.equal($('.selector-clearall').text(), "Remove all things");
|
||||||
assert.equal($('.selector-clearall').prop("tagName"), "BUTTON");
|
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 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-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 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-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.");
|
assert.equal($('.selector-filter label .help-tooltip')[0].getAttribute("aria-label"), "Type into this box to filter down the list of available things.");
|
||||||
|
@ -47,6 +47,7 @@ from .models import (
|
|||||||
Color2,
|
Color2,
|
||||||
ComplexSortedPerson,
|
ComplexSortedPerson,
|
||||||
Country,
|
Country,
|
||||||
|
Course,
|
||||||
CoverLetter,
|
CoverLetter,
|
||||||
CustomArticle,
|
CustomArticle,
|
||||||
CyclicOne,
|
CyclicOne,
|
||||||
@ -1190,6 +1191,10 @@ class CamelCaseAdmin(admin.ModelAdmin):
|
|||||||
filter_horizontal = ["m2m"]
|
filter_horizontal = ["m2m"]
|
||||||
|
|
||||||
|
|
||||||
|
class CourseAdmin(admin.ModelAdmin):
|
||||||
|
radio_fields = {"difficulty": admin.VERTICAL}
|
||||||
|
|
||||||
|
|
||||||
site = admin.AdminSite(name="admin")
|
site = admin.AdminSite(name="admin")
|
||||||
site.site_url = "/my-site-url/"
|
site.site_url = "/my-site-url/"
|
||||||
site.register(Article, ArticleAdmin)
|
site.register(Article, ArticleAdmin)
|
||||||
@ -1280,6 +1285,7 @@ site.register(ChapterXtra1, ChapterXtra1Admin)
|
|||||||
site.register(Pizza, PizzaAdmin)
|
site.register(Pizza, PizzaAdmin)
|
||||||
site.register(ReadOnlyPizza, ReadOnlyPizzaAdmin)
|
site.register(ReadOnlyPizza, ReadOnlyPizzaAdmin)
|
||||||
site.register(ReadablePizza)
|
site.register(ReadablePizza)
|
||||||
|
site.register(Course, CourseAdmin)
|
||||||
site.register(Topping, ToppingAdmin)
|
site.register(Topping, ToppingAdmin)
|
||||||
site.register(Album, AlbumAdmin)
|
site.register(Album, AlbumAdmin)
|
||||||
site.register(Song)
|
site.register(Song)
|
||||||
|
@ -623,6 +623,22 @@ class CyclicTwo(models.Model):
|
|||||||
return self.name
|
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):
|
class Topping(models.Model):
|
||||||
name = models.CharField(max_length=20)
|
name = models.CharField(max_length=20)
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ from .models import (
|
|||||||
Color,
|
Color,
|
||||||
ComplexSortedPerson,
|
ComplexSortedPerson,
|
||||||
Country,
|
Country,
|
||||||
|
Course,
|
||||||
CoverLetter,
|
CoverLetter,
|
||||||
CustomArticle,
|
CustomArticle,
|
||||||
CyclicOne,
|
CyclicOne,
|
||||||
@ -6908,6 +6909,30 @@ class SeleniumTests(AdminSeleniumTestCase):
|
|||||||
name_input_value = name_input.get_attribute("value")
|
name_input_value = name_input.get_attribute("value")
|
||||||
self.assertEqual(name_input_value, "Test section 1")
|
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"])
|
@screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
|
||||||
@override_settings(MESSAGE_LEVEL=10)
|
@override_settings(MESSAGE_LEVEL=10)
|
||||||
def test_messages(self):
|
def test_messages(self):
|
||||||
|
@ -399,7 +399,8 @@ class AdminDateWidgetTest(SimpleTestCase):
|
|||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
||||||
'<p class="date">'
|
'<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>',
|
'size="10"></p>',
|
||||||
)
|
)
|
||||||
# pass attrs to widget
|
# pass attrs to widget
|
||||||
@ -407,7 +408,8 @@ class AdminDateWidgetTest(SimpleTestCase):
|
|||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
||||||
'<p class="date">'
|
'<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>',
|
'size="20"></p>',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -418,7 +420,8 @@ class AdminTimeWidgetTest(SimpleTestCase):
|
|||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
||||||
'<p class="time">'
|
'<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>',
|
'size="8"></p>',
|
||||||
)
|
)
|
||||||
# pass attrs to widget
|
# pass attrs to widget
|
||||||
@ -426,7 +429,8 @@ class AdminTimeWidgetTest(SimpleTestCase):
|
|||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
w.render("test", datetime(2007, 12, 1, 9, 30)),
|
||||||
'<p class="time">'
|
'<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>',
|
'size="20"></p>',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -435,12 +439,16 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
|
|||||||
def test_render(self):
|
def test_render(self):
|
||||||
w = widgets.AdminSplitDateTime()
|
w = widgets.AdminSplitDateTime()
|
||||||
self.assertHTMLEqual(
|
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">'
|
'<p class="datetime">'
|
||||||
'Date: <input value="2007-12-01" type="text" class="vDateField" '
|
'<label for="id_test_0">Date:</label> '
|
||||||
'name="test_0" size="10"><br>'
|
'<input aria-describedby="id_test_timezone_warning_helptext" '
|
||||||
'Time: <input value="09:30:00" type="text" class="vTimeField" '
|
'value="2007-12-01" type="text" class="vDateField" '
|
||||||
'name="test_1" size="8"></p>',
|
'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):
|
def test_localization(self):
|
||||||
@ -449,12 +457,16 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
|
|||||||
with translation.override("de-at"):
|
with translation.override("de-at"):
|
||||||
w.is_localized = True
|
w.is_localized = True
|
||||||
self.assertHTMLEqual(
|
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">'
|
'<p class="datetime">'
|
||||||
'Datum: <input value="01.12.2007" type="text" '
|
'<label for="id_test_0">Datum:</label> '
|
||||||
'class="vDateField" name="test_0"size="10"><br>'
|
'<input aria-describedby="id_test_timezone_warning_helptext" '
|
||||||
'Zeit: <input value="09:30:00" type="text" class="vTimeField" '
|
'value="01.12.2007" type="text" '
|
||||||
'name="test_1" size="8"></p>',
|
'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>',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1550,6 +1550,12 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
|
|||||||
self.assertContains(
|
self.assertContains(
|
||||||
response, f"{self.admin.username}</a>\n› Change password"
|
response, f"{self.admin.username}</a>\n› Change password"
|
||||||
)
|
)
|
||||||
|
# Usable password field.
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'<fieldset class="flex-container">'
|
||||||
|
"<legend>Password-based authentication:</legend>",
|
||||||
|
)
|
||||||
# Submit buttons
|
# Submit buttons
|
||||||
self.assertContains(response, '<input type="submit" name="set-password"')
|
self.assertContains(response, '<input type="submit" name="set-password"')
|
||||||
self.assertContains(response, '<input type="submit" name="unset-password"')
|
self.assertContains(response, '<input type="submit" name="unset-password"')
|
||||||
|
@ -246,18 +246,19 @@ class ClearableFileInputTest(WidgetTest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
form = TestForm()
|
form = TestForm()
|
||||||
self.assertIs(self.widget.use_fieldset, False)
|
self.assertIs(self.widget.use_fieldset, True)
|
||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
'<div><label for="id_field">Field:</label>'
|
'<div><fieldset><legend for="id_field">Field:</legend>'
|
||||||
'<input id="id_field" name="field" type="file" required></div>'
|
'<input id="id_field" name="field" type="file" required></fieldset></div>'
|
||||||
'<div><label for="id_with_file">With file:</label>Currently: '
|
'<div><fieldset><legend for="id_with_file">With file:</legend>Currently: '
|
||||||
'<a href="something">something</a><br>Change:<input type="file" '
|
'<a href="something">something</a><br>Change:<input type="file" '
|
||||||
'name="with_file" id="id_with_file"></div>'
|
'name="with_file" id="id_with_file"></fieldset></div>'
|
||||||
'<div><label for="id_clearable_file">Clearable file:</label>'
|
'<div><fieldset><legend for="id_clearable_file">Clearable file:</legend>'
|
||||||
'Currently: <a href="something">something</a><input '
|
'Currently: <a href="something">something</a><input '
|
||||||
'type="checkbox" name="clearable_file-clear" id="clearable_file-clear_id">'
|
'type="checkbox" name="clearable_file-clear" id="clearable_file-clear_id">'
|
||||||
'<label for="clearable_file-clear_id">Clear</label><br>Change:'
|
'<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(),
|
form.render(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user