diff options
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 Binary files differindex de66ee7746..f4e3214721 100644 --- a/docs/intro/_images/admin05t.png +++ b/docs/intro/_images/admin05t.png diff --git a/docs/intro/_images/admin07.png b/docs/intro/_images/admin07.png Binary files differindex b591e82217..b5d9a791fd 100644 --- a/docs/intro/_images/admin07.png +++ b/docs/intro/_images/admin07.png diff --git a/docs/intro/_images/admin08t.png b/docs/intro/_images/admin08t.png Binary files differindex b77fdc03c6..23f48fdd41 100644 --- a/docs/intro/_images/admin08t.png +++ b/docs/intro/_images/admin08t.png diff --git a/docs/intro/_images/admin09.png b/docs/intro/_images/admin09.png Binary files differindex 16ccff4b41..cf2e9a9e1c 100644 --- a/docs/intro/_images/admin09.png +++ b/docs/intro/_images/admin09.png diff --git a/docs/intro/_images/admin10t.png b/docs/intro/_images/admin10t.png Binary files differindex e0376ec700..5533531f16 100644 --- a/docs/intro/_images/admin10t.png +++ b/docs/intro/_images/admin10t.png diff --git a/docs/intro/_images/admin14t.png b/docs/intro/_images/admin14t.png Binary files differindex 44ae24fe40..2fea20f5c1 100644 --- a/docs/intro/_images/admin14t.png +++ b/docs/intro/_images/admin14t.png diff --git a/docs/ref/contrib/admin/_images/fieldsets.png b/docs/ref/contrib/admin/_images/fieldsets.png Binary files differindex e5bc614f25..5b3f472b8d 100644 --- a/docs/ref/contrib/admin/_images/fieldsets.png +++ b/docs/ref/contrib/admin/_images/fieldsets.png diff --git a/docs/ref/contrib/admin/_images/raw_id_fields.png b/docs/ref/contrib/admin/_images/raw_id_fields.png Binary files differindex 7f16b11032..42f1d48fc2 100644 --- a/docs/ref/contrib/admin/_images/raw_id_fields.png +++ b/docs/ref/contrib/admin/_images/raw_id_fields.png 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"') |
