summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/contrib/admin/static/admin/css/base.css17
-rw-r--r--django/contrib/admin/static/admin/css/forms.css93
-rw-r--r--django/contrib/admin/static/admin/css/responsive.css64
-rw-r--r--django/contrib/admin/static/admin/css/responsive_rtl.css11
-rw-r--r--django/contrib/admin/static/admin/css/rtl.css61
-rw-r--r--django/contrib/admin/static/admin/css/widgets.css4
-rw-r--r--django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js9
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html30
-rw-r--r--django/contrib/admin/templates/admin/includes/fieldset.html34
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html35
-rw-r--r--django/contrib/admin/templates/registration/password_reset_confirm.html8
-rw-r--r--django/contrib/admin/templates/registration/password_reset_form.html2
-rw-r--r--django/contrib/auth/forms.py2
-rw-r--r--docs/intro/_images/admin05t.pngbin34301 -> 43712 bytes
-rw-r--r--docs/intro/_images/admin07.pngbin18339 -> 17554 bytes
-rw-r--r--docs/intro/_images/admin08t.pngbin20706 -> 19985 bytes
-rw-r--r--docs/intro/_images/admin09.pngbin6955 -> 9249 bytes
-rw-r--r--docs/intro/_images/admin10t.pngbin23087 -> 57196 bytes
-rw-r--r--docs/intro/_images/admin14t.pngbin17776 -> 40806 bytes
-rw-r--r--docs/ref/contrib/admin/_images/fieldsets.pngbin18267 -> 56736 bytes
-rw-r--r--docs/ref/contrib/admin/_images/raw_id_fields.pngbin997 -> 4003 bytes
-rw-r--r--docs/ref/contrib/admin/index.txt13
-rw-r--r--docs/releases/6.1.txt18
-rw-r--r--tests/admin_inlines/tests.py24
-rw-r--r--tests/admin_views/admin.py1
-rw-r--r--tests/admin_views/models.py4
-rw-r--r--tests/admin_views/tests.py38
-rw-r--r--tests/admin_widgets/tests.py35
-rw-r--r--tests/auth_tests/test_templates.py46
-rw-r--r--tests/auth_tests/test_views.py3
30 files changed, 252 insertions, 300 deletions
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index ec2ad6ab93..ea7707eb89 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -498,9 +498,6 @@ input, textarea, select, .form-row p, form .button {
font-weight: normal;
font-size: 0.8125rem;
}
-.form-row div.help {
- padding: 2px 3px;
-}
textarea {
vertical-align: top;
@@ -700,7 +697,7 @@ ul.messagelist li.error {
}
ul.errorlist {
- margin: 0 0 4px;
+ margin: 0;
padding: 0;
color: var(--error-fg);
background: var(--body-bg);
@@ -709,7 +706,6 @@ ul.errorlist {
ul.errorlist li {
font-size: 0.8125rem;
display: block;
- margin-bottom: 4px;
overflow-wrap: break-word;
}
@@ -731,17 +727,6 @@ td ul.errorlist li {
margin: 0;
}
-.form-row.errors {
- margin: 0;
- border: none;
- border-bottom: 1px solid var(--hairline-color);
- background: none;
-}
-
-.form-row.errors ul.errorlist li {
- padding-left: 0;
-}
-
.errors input, .errors select, .errors textarea,
td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea {
border: 1px solid var(--error-fg);
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css
index 5d2c1d2018..c474368184 100644
--- a/django/contrib/admin/static/admin/css/forms.css
+++ b/django/contrib/admin/static/admin/css/forms.css
@@ -24,15 +24,18 @@ form .form-row p {
.flex-container {
display: flex;
+ gap: 10px;
+ flex-direction: column;
+ align-items: flex-start;
}
-.form-multiline {
- flex-wrap: wrap;
+.flex-container div.checkbox {
+ display: flex;
}
-.form-multiline > div,
-.form-multiline > fieldset {
- padding-bottom: 10px;
+.form-multiline {
+ flex-wrap: wrap;
+ flex-direction: row;
}
/* FORM LABELS */
@@ -41,6 +44,7 @@ legend, label {
font-weight: normal;
color: var(--body-quiet-color);
font-size: 0.8125rem;
+ padding: 0;
}
.required legend, legend.required,
@@ -59,7 +63,8 @@ form div.radiolist.inline div {
}
form div.radiolist label {
- width: auto;
+ display: inline-block;
+ padding: 4px 10px 0 0;
}
form div.radiolist input[type="radio"] {
@@ -94,7 +99,6 @@ fieldset .inline-heading,
/* ALIGNED FIELDSETS */
.aligned fieldset {
- flex-grow: 1;
border-top: none;
}
@@ -104,21 +108,7 @@ fieldset .inline-heading,
.aligned legend {
float: inline-start;
-}
-
-.aligned legend,
-.aligned label {
- display: block;
- padding: 4px 10px 0 0;
- min-width: 160px;
- width: 160px;
- word-wrap: break-word;
-}
-
-.aligned label:not(.vCheckboxLabel):after {
- content: '';
- display: inline-block;
- vertical-align: middle;
+ padding: 0.5rem 0;
}
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
@@ -143,30 +133,14 @@ fieldset .inline-heading,
width: 350px;
}
-form .aligned ul {
- margin-left: 160px;
- padding-left: 10px;
-}
-
form .aligned div.radiolist {
display: block;
margin: 0;
padding: 0;
}
-form .aligned p.help,
-form .aligned div.help {
- margin-top: 0;
- margin-left: 160px;
- padding-left: 10px;
-}
-
-form .aligned p.date div.help.timezonewarning,
-form .aligned p.datetime div.help.timezonewarning,
-form .aligned p.time div.help.timezonewarning {
+form .aligned fieldset div.help {
margin-left: 0;
- padding-left: 0;
- font-weight: normal;
}
form .aligned p.help:last-child,
@@ -175,16 +149,6 @@ form .aligned div.help:last-child {
padding-bottom: 0;
}
-form .aligned input + p.help,
-form .aligned textarea + p.help,
-form .aligned select + p.help,
-form .aligned input + div.help,
-form .aligned textarea + div.help,
-form .aligned select + div.help {
- margin-left: 160px;
- padding-left: 10px;
-}
-
form .aligned select option:checked {
background-color: var(--selected-row);
color: var(--body-fg);
@@ -194,6 +158,11 @@ form .aligned ul li {
list-style: none;
}
+form .aligned div.help ul {
+ padding-left: 0;
+ margin-left: 0;
+}
+
form .aligned table p {
margin-left: 0;
padding-left: 0;
@@ -212,32 +181,6 @@ form .aligned table p {
width: 610px;
}
-fieldset .fieldBox {
- margin-right: 20px;
-}
-
-/* WIDE FIELDSETS */
-
-.wide label,
-.wide legend {
- width: 200px;
-}
-
-form .wide p.help,
-form .wide ul.errorlist,
-form .wide div.help {
- padding-left: 50px;
-}
-
-form div.help ul {
- padding-left: 0;
- margin-left: 0;
-}
-
-.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
- width: 450px;
-}
-
/* COLLAPSIBLE FIELDSETS */
.collapse summary .fieldset-heading,
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css
index 93abf79953..c8af03ba90 100644
--- a/django/contrib/admin/static/admin/css/responsive.css
+++ b/django/contrib/admin/static/admin/css/responsive.css
@@ -205,12 +205,6 @@ input[type="submit"], button {
min-height: 0;
}
- fieldset .fieldBox + .fieldBox {
- margin-top: 10px;
- padding-top: 10px;
- border-top: 1px solid var(--hairline-color);
- }
-
textarea {
max-width: 100%;
max-height: 120px;
@@ -224,7 +218,7 @@ input[type="submit"], button {
.aligned .datetimeshortcuts,
.aligned .related-lookup + strong {
align-self: center;
- margin-left: 15px;
+ margin-left: 0.5rem;
}
form .aligned div.radiolist {
@@ -465,11 +459,7 @@ input[type="submit"], button {
}
.flex-container {
- flex-flow: column;
- }
-
- .flex-container.checkbox-row {
- flex-flow: row;
+ align-items: stretch;
}
textarea {
@@ -480,22 +470,6 @@ input[type="submit"], button {
width: auto;
}
- fieldset .fieldBox + .fieldBox {
- margin-top: 15px;
- padding-top: 15px;
- }
-
- .aligned legend,
- .aligned label {
- width: 100%;
- min-width: auto;
- padding: 0 0 10px;
- }
-
- .aligned label:after {
- max-height: 0;
- }
-
.aligned .form-row input,
.aligned .form-row select,
.aligned .form-row textarea {
@@ -513,13 +487,6 @@ input[type="submit"], button {
padding: 1px 0 0 5px;
}
- .aligned label + p,
- .aligned label + div.help,
- .aligned label + div.readonly {
- padding: 0;
- margin-left: 0;
- }
-
.aligned p.file-upload {
font-size: 0.8125rem;
}
@@ -533,37 +500,10 @@ input[type="submit"], button {
padding-bottom: 0;
}
- .aligned .timezonewarning {
- flex: 1 0 100%;
- margin-top: 5px;
- }
-
- form .aligned .form-row div.help {
- width: 100%;
- margin: 5px 0 0;
- padding: 0;
- }
-
- form .aligned ul,
- form .aligned ul.errorlist {
- margin-left: 0;
- padding-left: 0;
- }
-
- form .aligned div.radiolist {
- margin-top: 5px;
- margin-right: 15px;
- margin-bottom: -3px;
- }
-
form .aligned div.radiolist:not(.inline) div + div {
margin-top: 5px;
}
- form .aligned fieldset div.flex-container {
- display: unset;
- }
-
/* Related widget */
.related-widget-wrapper {
diff --git a/django/contrib/admin/static/admin/css/responsive_rtl.css b/django/contrib/admin/static/admin/css/responsive_rtl.css
index b336bbfbe9..cd32867688 100644
--- a/django/contrib/admin/static/admin/css/responsive_rtl.css
+++ b/django/contrib/admin/static/admin/css/responsive_rtl.css
@@ -48,17 +48,6 @@
/* MOBILE */
@media (max-width: 767px) {
- [dir="rtl"] .aligned .related-lookup,
- [dir="rtl"] .aligned .datetimeshortcuts {
- margin-left: 0;
- margin-right: 15px;
- }
-
- [dir="rtl"] .aligned ul,
- [dir="rtl"] form .aligned ul.errorlist {
- margin-right: 0;
- }
-
[dir="rtl"] #changelist-filter {
margin-left: 0;
margin-right: 0;
diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css
index ba4d0bf549..aa7c4e8636 100644
--- a/django/contrib/admin/static/admin/css/rtl.css
+++ b/django/contrib/admin/static/admin/css/rtl.css
@@ -124,29 +124,8 @@ thead th.sorted .text {
/* FORMS */
-.aligned label,
-.aligned legend {
- padding: 0 0 3px 1em;
-}
-
-.submit-row a.deletelink {
- margin-left: 0;
- margin-right: auto;
-}
-
-.vDateField, .vTimeField {
- margin-left: 2px;
-}
-
-.aligned .form-row input {
- margin-left: 5px;
-}
-
form .aligned ul {
- margin-right: 163px;
- padding-right: 10px;
- margin-left: 0;
- padding-left: 0;
+ margin: 0;
}
form ul.inline li {
@@ -155,48 +134,10 @@ form ul.inline li {
padding-left: 7px;
}
-form .aligned p.help,
-form .aligned div.help {
- margin-left: 0;
- margin-right: 160px;
- padding-right: 10px;
-}
-
-form div.help ul,
-form .aligned .checkbox-row + .help,
-form .aligned p.date div.help.timezonewarning,
-form .aligned p.datetime div.help.timezonewarning,
-form .aligned p.time div.help.timezonewarning {
- margin-right: 0;
- padding-right: 0;
-}
-
-form .wide p.help,
-form .wide ul.errorlist,
-form .wide div.help {
- padding-left: 0;
- padding-right: 50px;
-}
-
.submit-row {
text-align: right;
}
-fieldset .fieldBox {
- margin-left: 20px;
- margin-right: 0;
-}
-
-.errorlist li {
- background-position: 100% 12px;
- padding: 0;
-}
-
-.errornote {
- background-position: 100% 12px;
- padding: 10px 12px;
-}
-
/* WIDGETS */
.calendarnav-previous {
diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css
index c0de045c68..c2a3836b90 100644
--- a/django/contrib/admin/static/admin/css/widgets.css
+++ b/django/contrib/admin/static/admin/css/widgets.css
@@ -302,7 +302,8 @@ p.datetime {
}
p.datetime label {
- display: inline;
+ display: block;
+ padding: 0.5rem 0;
}
.datetime span {
@@ -313,7 +314,6 @@ p.datetime label {
}
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
- margin-left: 5px;
margin-bottom: 4px;
}
diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
index 6251614863..b30a668a2e 100644
--- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
+++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
@@ -68,7 +68,7 @@
}
// Check if warning is already there.
- if (inp.parentNode.querySelectorAll('.' + warningClass).length) {
+ if (inp.parentNode.parentNode.querySelectorAll('.' + warningClass).length) {
return;
}
@@ -96,7 +96,12 @@
warning.classList.add('help', warningClass);
warning.id = `${field_id}_timezone_warning_helptext`;
warning.textContent = message;
- inp.parentNode.appendChild(warning);
+ const errorList = inp.parentNode.parentNode.querySelector('ul.errorlist');
+ if (errorList) {
+ errorList.before(warning);
+ } else {
+ inp.parentNode.before(warning);
+ }
},
// Add clock widget to a given field
addClock: function(inp) {
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 e8d3da82bd..70b68f6de5 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -39,29 +39,37 @@
<fieldset class="module aligned">
<div class="form-row">
- {{ form.usable_password.errors }}
- <fieldset>{{ 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>
- </div>
- {% endif %}
+ <fieldset class="flex-container{% if form.usable_password.errors %} errors{% endif %}">{{ form.usable_password.legend_tag }}
+ {% 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>
+ </div>
+ {% endif %}
+ {{ form.usable_password.errors }}
+ {{ form.usable_password }}
+ </fieldset>
</div>
<div class="form-row field-password1">
- {{ form.password1.errors }}
- <div class="flex-container">{{ form.password1.label_tag }} {{ form.password1 }}</div>
+ <div class="flex-container{% if form.password1.errors %} errors{% endif %}">
+ {{ form.password1.label_tag }}
{% if form.password1.help_text %}
<div class="help"{% if form.password1.id_for_label %} id="{{ form.password1.id_for_label }}_helptext"{% endif %}>{{ form.password1.help_text|safe }}</div>
{% endif %}
+ {{ form.password1.errors }}
+ {{ form.password1 }}
+ </div>
</div>
<div class="form-row field-password2">
- {{ form.password2.errors }}
- <div class="flex-container">{{ form.password2.label_tag }} {{ form.password2 }}</div>
+ <div class="flex-container{% if form.password2.errors %} errors{% endif %}">
+ {{ form.password2.label_tag }}
{% if form.password2.help_text %}
<div class="help"{% if form.password2.id_for_label %} id="{{ form.password2.id_for_label }}_helptext"{% endif %}>{{ form.password2.help_text|safe }}</div>
{% endif %}
+ {{ form.password2.errors }}
+ {{ form.password2 }}
+ </div>
</div>
</fieldset>
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
index 1fd303ea82..70c68655c5 100644
--- a/django/contrib/admin/templates/admin/includes/fieldset.html
+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
@@ -8,33 +8,35 @@
<div class="description">{{ fieldset.description|safe }}</div>
{% endif %}
{% for line in fieldset %}
- <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 %}
+ <div class="form-row{% if not line.fields|length == 1 %} flex-container form-multiline{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
{% for field in line %}
{% if field.is_fieldset %}<fieldset{% 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 }}{% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
- {% else %}
- {% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
- {% if field.is_readonly %}
- <div class="readonly">{{ field.contents }}</div>
- {% else %}
- {{ field.field }}
- {% endif %}
- {% endif %}
+ <div class="flex-container{% if not field.is_readonly and field.errors or line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% endif %}{% if field.is_checkbox %} checkbox-row{% endif %}">
+ {% if field.is_checkbox %}
+ <div class="checkbox">
+ {{ field.field }}
+ {% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
</div>
+ {% else %}
+ {% if not field.is_fieldset %}{{ field.label_tag }}{% endif %}
+ {% endif %}
{% if field.field.help_text %}
<div class="help{% if field.field.is_hidden %} hidden{% endif %}"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div>{{ field.field.help_text|safe }}</div>
</div>
{% endif %}
+ {% if line.fields|length == 1 %}{{ line.errors }}{% endif %}
+ {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
+ {% if not field.is_checkbox %}
+ {% if field.is_readonly %}
+ <div class="readonly">{{ field.contents }}</div>
+ {% else %}
+ {{ field.field }}
+ {% endif %}
+ {% endif %}
</div>
{% if field.is_fieldset %}</fieldset>{% endif %}
{% endfor %}
- {% if not line.fields|length == 1 %}</div>{% endif %}
</div>
{% endfor %}
{% if fieldset.name and fieldset.is_collapsible %}</details>{% endif %}
diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html
index d377c201ce..91c99c7fd1 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -31,27 +31,36 @@
<p>{% translate 'Please enter your old password, for security’s sake, and then enter your new password twice so we can verify you typed it in correctly.' %}</p>
-<fieldset class="module aligned wide">
+<fieldset class="module aligned">
<div class="form-row">
- {{ form.old_password.errors }}
- <div class="flex-container">{{ form.old_password.label_tag }} {{ form.old_password }}</div>
+ <div class="flex-container{% if form.old_password.errors %} errors{% endif %}">
+ {{ form.old_password.label_tag }}
+ {{ form.old_password.errors }}
+ {{ form.old_password }}
+ </div>
</div>
<div class="form-row">
- {{ form.new_password1.errors }}
- <div class="flex-container">{{ form.new_password1.label_tag }} {{ form.new_password1 }}</div>
- {% if form.new_password1.help_text %}
- <div class="help"{% if form.new_password1.id_for_label %} id="{{ form.new_password1.id_for_label }}_helptext"{% endif %}>{{ form.new_password1.help_text|safe }}</div>
- {% endif %}
+ <div class="flex-container{% if form.new_password1.errors %} errors{% endif %}">
+ {{ form.new_password1.label_tag }}
+ {% if form.new_password1.help_text %}
+ <div class="help"{% if form.new_password1.id_for_label %} id="{{ form.new_password1.id_for_label }}_helptext"{% endif %}>{{ form.new_password1.help_text|safe }}</div>
+ {% endif %}
+ {{ form.new_password1.errors }}
+ {{ form.new_password1 }}
+ </div>
</div>
<div class="form-row">
- {{ form.new_password2.errors }}
- <div class="flex-container">{{ form.new_password2.label_tag }} {{ form.new_password2 }}</div>
- {% if form.new_password2.help_text %}
- <div class="help"{% if form.new_password2.id_for_label %} id="{{ form.new_password2.id_for_label }}_helptext"{% endif %}>{{ form.new_password2.help_text|safe }}</div>
- {% endif %}
+ <div class="flex-container{% if form.new_password2.errors %} errors{% endif %}">
+ {{ form.new_password2.label_tag }}
+ {% if form.new_password2.help_text %}
+ <div class="help"{% if form.new_password2.id_for_label %} id="{{ form.new_password2.id_for_label }}_helptext"{% endif %}>{{ form.new_password2.help_text|safe }}</div>
+ {% endif %}
+ {{ form.new_password2.errors }}
+ {{ form.new_password2 }}
+ </div>
</div>
</fieldset>
diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html
index 2ad675da24..ffe51b59f8 100644
--- a/django/contrib/admin/templates/registration/password_reset_confirm.html
+++ b/django/contrib/admin/templates/registration/password_reset_confirm.html
@@ -20,16 +20,16 @@
<fieldset class="module aligned">
<input class="hidden" autocomplete="username" value="{{ form.user.get_username }}">
<div class="form-row field-password1">
- {{ form.new_password1.errors }}
- <div class="flex-container">
+ <div class="flex-container{% if form.new_password1.errors %} errors{% endif %}">
<label for="id_new_password1">{% translate 'New password:' %}</label>
+ {{ form.new_password1.errors }}
{{ form.new_password1 }}
</div>
</div>
<div class="form-row field-password2">
- {{ form.new_password2.errors }}
- <div class="flex-container">
+ <div class="flex-container{% if form.new_password2.errors %} errors{% endif %}">
<label for="id_new_password2">{% translate 'Confirm password:' %}</label>
+ {{ form.new_password2.errors }}
{{ form.new_password2 }}
</div>
</div>
diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html
index 3737414d81..31c84fdcc7 100644
--- a/django/contrib/admin/templates/registration/password_reset_form.html
+++ b/django/contrib/admin/templates/registration/password_reset_form.html
@@ -17,9 +17,9 @@
<form method="post">{% csrf_token %}
<fieldset class="module aligned">
<div class="form-row field-email">
- {{ form.email.errors }}
<div class="flex-container">
<label for="id_email">{% translate 'Email address:' %}</label>
+ {{ form.email.errors }}
{{ form.email }}
</div>
</div>
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index aff0cca342..254a135ec1 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -158,7 +158,7 @@ class SetUnusablePasswordMixin:
required=False,
initial="true",
choices={"true": _("Enabled"), "false": _("Disabled")},
- widget=forms.RadioSelect(attrs={"class": "radiolist inline"}),
+ widget=forms.RadioSelect(attrs={"class": "radiolist"}),
help_text=help_text,
)
diff --git a/docs/intro/_images/admin05t.png b/docs/intro/_images/admin05t.png
index de66ee7746..f4e3214721 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 b591e82217..b5d9a791fd 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 b77fdc03c6..23f48fdd41 100644
--- a/docs/intro/_images/admin08t.png
+++ b/docs/intro/_images/admin08t.png
Binary files differ
diff --git a/docs/intro/_images/admin09.png b/docs/intro/_images/admin09.png
index 16ccff4b41..cf2e9a9e1c 100644
--- a/docs/intro/_images/admin09.png
+++ b/docs/intro/_images/admin09.png
Binary files differ
diff --git a/docs/intro/_images/admin10t.png b/docs/intro/_images/admin10t.png
index e0376ec700..5533531f16 100644
--- a/docs/intro/_images/admin10t.png
+++ b/docs/intro/_images/admin10t.png
Binary files differ
diff --git a/docs/intro/_images/admin14t.png b/docs/intro/_images/admin14t.png
index 44ae24fe40..2fea20f5c1 100644
--- a/docs/intro/_images/admin14t.png
+++ b/docs/intro/_images/admin14t.png
Binary files differ
diff --git a/docs/ref/contrib/admin/_images/fieldsets.png b/docs/ref/contrib/admin/_images/fieldsets.png
index e5bc614f25..5b3f472b8d 100644
--- a/docs/ref/contrib/admin/_images/fieldsets.png
+++ b/docs/ref/contrib/admin/_images/fieldsets.png
Binary files differ
diff --git a/docs/ref/contrib/admin/_images/raw_id_fields.png b/docs/ref/contrib/admin/_images/raw_id_fields.png
index 7f16b11032..42f1d48fc2 100644
--- a/docs/ref/contrib/admin/_images/raw_id_fields.png
+++ b/docs/ref/contrib/admin/_images/raw_id_fields.png
Binary files differ
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index d52857d6ba..b3c7c2c426 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -418,22 +418,23 @@ subclass::
* ``classes``
A list or tuple containing extra CSS classes to apply to the fieldset.
This can include any custom CSS class defined in the project, as well
- as any of the CSS classes provided by Django. Within the default admin
- site CSS stylesheet, two particularly useful classes are defined:
- ``collapse`` and ``wide``.
+ as the CSS class provided by Django: ``collapse``.
Example::
{
- "classes": ["wide", "collapse"],
+ "classes": ["collapse"],
}
- Fieldsets with the ``wide`` style will be given extra horizontal
- space in the admin interface.
Fieldsets with a name and the ``collapse`` style will be initially
collapsed, using an expandable widget with a toggle for switching
their visibility.
+ .. versionchanged:: 6.1
+
+ The ``wide`` class has been removed, as it was made obsolete by the
+ new admin layout.
+
* ``description``
A string of optional extra text to be displayed at the top of each
fieldset, under the heading of the fieldset.
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 6c6890b811..ef6ae1d424 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -99,6 +99,19 @@ Minor features
preserve :ref:`named groups <field-choices-named-groups>` (e.g.
``choices=[("Group", [("1", "Item")]), ...]``).
+* In order to improve accessibility of the admin change forms:
+
+ * Form fields are now shown below their respective labels instead of next to
+ them.
+
+ * Help text is now shown after the field label and before the field input.
+
+ * Validation errors are now shown after the help text and before the field
+ input.
+
+ * Checkboxes are an exception to the above changes and continue to be
+ displayed in their original layout.
+
:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -380,6 +393,11 @@ backends.
* Set the new ``DatabaseFeatures.supports_inspectdb`` attribute to ``False``
if the management command isn't supported.
+:mod:`django.contrib.admin`
+---------------------------
+
+* The ``wide`` class is removed, as it was made obsolete by the new layout.
+
:mod:`django.contrib.gis`
-------------------------
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 6956c37740..50b3a7baba 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -384,21 +384,23 @@ class TestInline(TestDataMixin, TestCase):
response = self.client.get(url)
# The whole line containing name + position fields is not hidden.
self.assertContains(
- response, '<div class="form-row field-name field-position">'
+ response,
+ "<div class="
+ '"form-row flex-container form-multiline field-name field-position">',
)
# The div containing the position field is hidden.
self.assertInHTML(
- '<div class="flex-container fieldBox field-position hidden">'
+ '<div class="flex-container field-position fieldBox hidden">'
'<label class="inline">Position:</label>'
- '<div class="readonly">0</div></div>'
- '<div class="help hidden"><div>Position help_text.</div></div>',
+ '<div class="help hidden"><div>Position help_text.</div></div>'
+ '<div class="readonly">0</div></div>',
response.rendered_content,
)
self.assertInHTML(
- '<div class="flex-container fieldBox field-position hidden">'
+ '<div class="flex-container field-position fieldBox hidden">'
'<label class="inline">Position:</label>'
- '<div class="readonly">1</div></div>'
- '<div class="help hidden"><div>Position help_text.</div></div>',
+ '<div class="help hidden"><div>Position help_text.</div></div>'
+ '<div class="readonly">1</div></div>',
response.rendered_content,
)
@@ -419,17 +421,17 @@ class TestInline(TestDataMixin, TestCase):
# The whole line containing position field is hidden.
self.assertInHTML(
'<div class="form-row hidden field-position">'
- '<div><div class="flex-container"><label>Position:</label>'
- '<div class="readonly">0</div></div>'
+ '<div class="flex-container"><label>Position:</label>'
'<div class="help hidden"><div>Position help_text.</div></div>'
+ '<div class="readonly">0</div>'
"</div></div>",
response.rendered_content,
)
self.assertInHTML(
'<div class="form-row hidden field-position">'
- '<div><div class="flex-container"><label>Position:</label>'
- '<div class="readonly">1</div></div>'
+ '<div class="flex-container"><label>Position:</label>'
'<div class="help hidden"><div>Position help_text.</div></div>'
+ '<div class="readonly">1</div>'
"</div></div>",
response.rendered_content,
)
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 6f7cd79e50..0f05a66746 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -498,6 +498,7 @@ class PictureAdmin(admin.ModelAdmin):
class LanguageAdmin(admin.ModelAdmin):
list_display = ["iso", "shortlist", "english_name", "name"]
list_editable = ["shortlist"]
+ fields = [("iso", "english_name"), "name"]
class RecommendationAdmin(admin.ModelAdmin):
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index 5191d1605e..38e26cb95a 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -407,8 +407,8 @@ class Picture(models.Model):
class Language(models.Model):
- iso = models.CharField(max_length=5, primary_key=True)
- name = models.CharField(max_length=50)
+ iso = models.CharField(max_length=5, primary_key=True, help_text="iso helptext")
+ name = models.CharField(max_length=50, help_text="name helptext")
english_name = models.CharField(max_length=50)
shortlist = models.BooleanField(default=False)
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 4489dc9950..ac4265c4b4 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -7311,12 +7311,10 @@ class SeleniumTests(AdminSeleniumTestCase):
By.CSS_SELECTOR, "#content-main .field-difficulty, .form-multiline"
)
# Two field boxes.
- field_boxes = multiline.find_elements(By.CSS_SELECTOR, "div > div.fieldBox")
+ field_boxes = multiline.find_elements(By.XPATH, "./*")
self.assertEqual(len(field_boxes), 2)
# One of them is under a <fieldset>.
- under_fieldset = multiline.find_elements(
- By.CSS_SELECTOR, "fieldset > div > div.fieldBox"
- )
+ under_fieldset = multiline.find_elements(By.TAG_NAME, "fieldset")
self.assertEqual(len(under_fieldset), 1)
self.take_screenshot("horizontal_fieldset")
@@ -7461,6 +7459,26 @@ class SeleniumTests(AdminSeleniumTestCase):
self.assertTrue(changelist_filter.is_displayed())
self.take_screenshot("filter_sidebar")
+ @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
+ def test_form_errors_render_layout(self):
+ from selenium.webdriver.common.by import By
+
+ self.admin_login(
+ username="super", password="secret", login_url=reverse("admin:index")
+ )
+ self.selenium.get(
+ self.live_server_url + reverse("admin:admin_views_language_add")
+ )
+
+ with self.wait_page_loaded():
+ self.selenium.find_element(By.NAME, "_save").click()
+
+ form_rows = self.selenium.find_elements(By.CSS_SELECTOR, "div.form-row")
+ for row in form_rows:
+ error_list = row.find_element(By.CSS_SELECTOR, "ul.errorlist")
+ self.assertTrue(error_list.is_displayed())
+ self.take_screenshot("error_list")
+
@override_settings(ROOT_URLCONF="admin_views.urls")
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
@@ -9063,10 +9081,18 @@ class TestLabelVisibility(TestCase):
)
def assert_fieldline_visible(self, response):
- self.assertContains(response, '<div class="form-row field-first field-second">')
+ self.assertContains(
+ response,
+ "<div class="
+ '"form-row flex-container form-multiline field-first field-second">',
+ )
def assert_fieldline_hidden(self, response):
- self.assertContains(response, '<div class="form-row hidden')
+ self.assertContains(
+ response,
+ "<div class="
+ '"form-row flex-container form-multiline hidden field-first field-second">',
+ )
@override_settings(ROOT_URLCONF="admin_views.urls")
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index e0ae5b7747..5a8b29b83a 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -1203,6 +1203,41 @@ class DateTimePickerSeleniumTests(AdminWidgetSeleniumTestCase):
# The right month and year are displayed.
self.wait_for_text("#calendarin0 caption", expected_caption)
+ @override_settings(TIME_ZONE="Asia/Seoul")
+ def test_timezone_warning_message(self):
+ from selenium.webdriver.common.by import By
+
+ self.admin_login(username="super", password="secret", login_url="/")
+
+ self.selenium.get(
+ self.live_server_url + reverse("admin:admin_widgets_member_add")
+ )
+
+ datetime = self.selenium.find_element(By.CSS_SELECTOR, "p.datetime")
+ warnings = self.selenium.find_elements(
+ By.CSS_SELECTOR, "div.field-birthdate div.timezonewarning"
+ )
+ self.assertEqual(len(warnings), 1)
+
+ warning = warnings[0]
+ self.assertTrue(warning.is_displayed())
+ next_element = warning.find_element(By.XPATH, "./following-sibling::*[1]")
+ # Warning messages are generally located just above the field block.
+ self.assertEqual(next_element, datetime)
+
+ date = datetime.find_element(By.TAG_NAME, "input")
+ date.send_keys("invalid")
+ with self.wait_page_loaded():
+ self.selenium.find_element(By.NAME, "_save").click()
+
+ errors = self.selenium.find_element(By.ID, "id_birthdate_error")
+ warning = self.selenium.find_element(
+ By.CSS_SELECTOR, "div.help.timezonewarning"
+ )
+ next_element = warning.find_element(By.XPATH, "./following-sibling::*[1]")
+ # warning message appears above the error message.
+ self.assertEqual(next_element, errors)
+
@requires_tz_support
@override_settings(TIME_ZONE="Asia/Singapore")
diff --git a/tests/auth_tests/test_templates.py b/tests/auth_tests/test_templates.py
index edde6ca6b4..775409af59 100644
--- a/tests/auth_tests/test_templates.py
+++ b/tests/auth_tests/test_templates.py
@@ -37,6 +37,20 @@ class AuthTemplateTests(TestCase):
)
self.assertContains(response, "<h1>Password reset</h1>")
+ def test_password_reset_view_error_form(self):
+ response = self.client.post(reverse("password_reset"), {})
+ self.assertContains(
+ response,
+ '<div class="flex-container">'
+ '<label for="id_email">Email address:</label>'
+ '<ul class="errorlist" id="id_email_error">'
+ "<li>This field is required.</li></ul>"
+ '<input type="email" name="email" autocomplete="email" maxlength="254" '
+ 'required aria-invalid="true" aria-describedby="id_email_error" '
+ 'id="id_email"></div>',
+ html=True,
+ )
+
def test_password_reset_view_error_title(self):
response = self.client.post(reverse("password_reset"), {})
self.assertContains(
@@ -96,6 +110,38 @@ class AuthTemplateTests(TestCase):
response, "<title>Error: Enter new password | Django site admin</title>"
)
+ def test_password_reset_confirm_view_error_form(self):
+ client = PasswordResetConfirmClient()
+ default_token_generator = PasswordResetTokenGenerator()
+ token = default_token_generator.make_token(self.user)
+ uidb64 = urlsafe_base64_encode(str(self.user.pk).encode())
+ url = reverse(
+ "password_reset_confirm", kwargs={"uidb64": uidb64, "token": token}
+ )
+ response = client.post(url, {})
+ self.assertContains(
+ response,
+ '<div class="flex-container errors">'
+ '<label for="id_new_password1">New password:</label>'
+ '<ul class="errorlist" id="id_new_password1_error">'
+ "<li>This field is required.</li></ul>"
+ '<input type="password" name="new_password1" autocomplete="new-password" '
+ 'required aria-invalid="true" aria-describedby="id_new_password1_error" '
+ 'id="id_new_password1"></div>',
+ html=True,
+ )
+ self.assertContains(
+ response,
+ '<div class="flex-container errors">'
+ '<label for="id_new_password2">Confirm password:</label>'
+ '<ul class="errorlist" id="id_new_password2_error">'
+ "<li>This field is required.</li></ul>"
+ '<input type="password" name="new_password2" autocomplete="new-password" '
+ 'required aria-invalid="true" aria-describedby="id_new_password2_helptext '
+ 'id_new_password2_error" id="id_new_password2"></div>',
+ html=True,
+ )
+
@override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
def test_password_reset_confirm_view_custom_username_hint(self):
custom_user = CustomUser.custom_objects.create_user(
diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
index d7d59d9fc0..a3863b6233 100644
--- a/tests/auth_tests/test_views.py
+++ b/tests/auth_tests/test_views.py
@@ -1555,7 +1555,8 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
# Usable password field.
self.assertContains(
response,
- "<fieldset><legend>Password-based authentication:</legend>",
+ '<fieldset class="flex-container">'
+ "<legend>Password-based authentication:</legend>",
)
# Submit buttons
self.assertContains(response, '<input type="submit" name="set-password"')