diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index ec2ad6ab9370..ea7707eb89bc 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 5d2c1d2018f4..c4743681849f 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 93abf79953c3..c8af03ba90e4 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 b336bbfbe9be..cd3286768871 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 ba4d0bf549dd..aa7c4e8636d6 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 c0de045c6802..c2a3836b90fd 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 6251614863df..b30a668a2e5f 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 e8d3da82bda2..70b68f6de588 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 @@
- {{ form.usable_password.errors }} -
{{ form.usable_password.legend_tag }} {{ form.usable_password }}
- {% if form.usable_password.help_text %} -
-

{{ form.usable_password.help_text|safe }}

-
- {% endif %} +
{{ form.usable_password.legend_tag }} + {% if form.usable_password.help_text %} +
+

{{ form.usable_password.help_text|safe }}

+
+ {% endif %} + {{ form.usable_password.errors }} + {{ form.usable_password }} +
- {{ form.password1.errors }} -
{{ form.password1.label_tag }} {{ form.password1 }}
+
+ {{ form.password1.label_tag }} {% if form.password1.help_text %}
{{ form.password1.help_text|safe }}
{% endif %} + {{ form.password1.errors }} + {{ form.password1 }} +
- {{ form.password2.errors }} -
{{ form.password2.label_tag }} {{ form.password2 }}
+
+ {{ form.password2.label_tag }} {% if form.password2.help_text %}
{{ form.password2.help_text|safe }}
{% endif %} + {{ form.password2.errors }} + {{ form.password2 }} +
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index 1fd303ea823e..70c68655c5b3 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -8,33 +8,35 @@
{{ fieldset.description|safe }}
{% endif %} {% for line in fieldset %} -
- {% if line.fields|length == 1 %}{{ line.errors }}{% else %}
{% endif %} +
{% for field in line %} {% if field.is_fieldset %}{{ field.label_tag }}{% endif %} -
- {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% 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 %} -
{{ field.contents }}
- {% else %} - {{ field.field }} - {% endif %} - {% 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 %} + {% endif %} {% if field.field.help_text %}
{{ field.field.help_text|safe }}
{% 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 %} +
{{ field.contents }}
+ {% else %} + {{ field.field }} + {% endif %} + {% endif %}
{% if field.is_fieldset %}{% endif %} {% endfor %} - {% if not line.fields|length == 1 %}
{% endif %}
{% endfor %} {% if fieldset.name and fieldset.is_collapsible %}{% endif %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index d377c201ce1e..91c99c7fd135 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 @@

{% 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.' %}

-
+
- {{ form.old_password.errors }} -
{{ form.old_password.label_tag }} {{ form.old_password }}
+
+ {{ form.old_password.label_tag }} + {{ form.old_password.errors }} + {{ form.old_password }} +
- {{ form.new_password1.errors }} -
{{ form.new_password1.label_tag }} {{ form.new_password1 }}
- {% if form.new_password1.help_text %} -
{{ form.new_password1.help_text|safe }}
- {% endif %} +
+ {{ form.new_password1.label_tag }} + {% if form.new_password1.help_text %} +
{{ form.new_password1.help_text|safe }}
+ {% endif %} + {{ form.new_password1.errors }} + {{ form.new_password1 }} +
- {{ form.new_password2.errors }} -
{{ form.new_password2.label_tag }} {{ form.new_password2 }}
- {% if form.new_password2.help_text %} -
{{ form.new_password2.help_text|safe }}
- {% endif %} +
+ {{ form.new_password2.label_tag }} + {% if form.new_password2.help_text %} +
{{ form.new_password2.help_text|safe }}
+ {% endif %} + {{ form.new_password2.errors }} + {{ form.new_password2 }} +
diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html index 2ad675da24f3..ffe51b59f83c 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 @@
- {{ form.new_password1.errors }} -
+
+ {{ form.new_password1.errors }} {{ form.new_password1 }}
- {{ form.new_password2.errors }} -
+
+ {{ form.new_password2.errors }} {{ form.new_password2 }}
diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index 3737414d81b3..31c84fdcc70e 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 @@
{% csrf_token %}
- {{ form.email.errors }}
+ {{ form.email.errors }} {{ form.email }}
diff --git a/django/contrib/auth/apps.py b/django/contrib/auth/apps.py index ad6f81680909..d25d67fe2217 100644 --- a/django/contrib/auth/apps.py +++ b/django/contrib/auth/apps.py @@ -6,7 +6,7 @@ from . import get_user_model from .checks import check_middleware, check_models_permissions, check_user_model -from .management import create_permissions +from .management import create_permissions, rename_permissions_after_model_rename from .signals import user_logged_in @@ -16,6 +16,10 @@ class AuthConfig(AppConfig): verbose_name = _("Authentication and Authorization") def ready(self): + post_migrate.connect( + rename_permissions_after_model_rename, + dispatch_uid="django.contrib.auth.management.rename_permissions", + ) post_migrate.connect( create_permissions, dispatch_uid="django.contrib.auth.management.create_permissions", diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index aff0cca342aa..254a135ec19c 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -158,7 +158,7 @@ def create_usable_password_field(help_text=usable_password_help_text): 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/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index e816357b2b06..a91fbab39632 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -1,15 +1,18 @@ """ -Creates permissions for all installed apps that need permissions. +Creates permissions for all installed apps that need permissions, and renames +them on model renames. """ import getpass +import sys import unicodedata from django.apps import apps as global_apps from django.contrib.auth import get_permission_codename from django.contrib.contenttypes.management import create_contenttypes from django.core import exceptions -from django.db import DEFAULT_DB_ALIAS, router +from django.core.management.color import color_style +from django.db import DEFAULT_DB_ALIAS, migrations, router, transaction def _get_all_permissions(opts): @@ -108,6 +111,121 @@ def create_permissions( print("Adding permission '%s'" % perm) +def _get_permission_metadata(apps, app_label, model_name): + try: + model = apps.get_model(app_label, model_name) + except LookupError: + # Model does not exist in this migration state, e.g. zero. + Permission = apps.get_model("auth", "Permission") + return Permission._meta.default_permissions, model_name + return ( + model._meta.default_permissions, + model._meta.verbose_name_raw, + ) + + +def rename_permissions_after_model_rename( + app_config, + verbosity=2, + plan=None, + using=DEFAULT_DB_ALIAS, + apps=global_apps, + stdout=sys.stdout, + **kwargs, +): + if not app_config.models_module: + return + + # This handler is connected to the global post_migrate signal, which is + # emitted for *all* apps — including test configurations where + # django.contrib.auth is NOT installed. + try: + Permission = apps.get_model("auth", "Permission") + except LookupError: + return + if not router.allow_migrate_model(using, Permission): + return + + db = using or router.db_for_write(Permission) + + app_label = app_config.label + + # Collect (from_model, to_model) pairs + renames = [ + (op.new_name, op.old_name) if backward else (op.old_name, op.new_name) + for migration, backward in (plan or []) + for op in migration.operations + if isinstance(op, migrations.RenameModel) + and migration.app_label == app_config.label + ] + + if not renames: + return + + planned = [] + conflicts = [] + + for old_name, new_name in renames: + old_suffix = f"_{old_name.lower()}" + new_suffix = f"_{new_name.lower()}" + + actions, verbose_name_raw = _get_permission_metadata(apps, app_label, new_name) + perms = Permission.objects.using(db).filter( + content_type__app_label=app_label, + codename__in=[f"{action}{old_suffix}" for action in actions], + ) + + for perm in perms: + for action in actions: + if not perm.codename.startswith(action + "_"): + continue + + old_codename = perm.codename + new_codename = f"{action}{new_suffix}" + new_name_str = f"Can {action} {verbose_name_raw}" + + planned.append((perm, old_codename, new_codename, new_name_str)) + + existing = { + p.codename + for p in Permission.objects.using(db).filter( + content_type__app_label=app_label, + codename__in=[new for _, _, new, _ in planned], + ) + } + + # Look for conflicts + for perm, old, new, _ in planned: + if new in existing and perm.codename != new: + conflicts.append((perm.pk, old, new)) + + # Raise error if conflicts found + if conflicts: + if verbosity: + style = color_style() + for pk, old, new in conflicts: + msg = ( + f"Failed to rename permission {pk} from '{old}' to '{new}'. " + f"Please resolve the conflict manually.\n" + ) + stdout.write(style.WARNING(msg)) + error_message = f"{len(conflicts)} permission rename conflict(s) detected." + raise RuntimeError(error_message) + + with transaction.atomic(using=db): + for perm, _, new_codename, new_name_str in planned: + perm.codename = new_codename + perm.name = new_name_str + perm.save(update_fields={"codename", "name"}, using=db) + + for _, from_codename, to_codename, _ in planned: + if verbosity >= 2: + stdout.write( + f"Renamed permission(s): " + f"{app_label}.{from_codename} → {to_codename}\n" + ) + + def get_system_username(): """ Return the current system user's username, or an empty string if the diff --git a/django/core/paginator.py b/django/core/paginator.py index d1842d6adb67..17a30a00f6ff 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -103,29 +103,22 @@ def _get_elided_page_range( 1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50. """ if num_pages <= (on_each_side + on_ends) * 2: - for page in page_range: - yield page + yield from page_range return if number > (1 + on_each_side + on_ends) + 1: - for page in range(1, on_ends + 1): - yield page + yield from range(1, on_ends + 1) yield self.ELLIPSIS - for page in range(number - on_each_side, number + 1): - yield page + yield from range(number - on_each_side, number + 1) else: - for page in range(1, number + 1): - yield page + yield from range(1, number + 1) if number < (num_pages - on_each_side - on_ends) - 1: - for page in range(number + 1, number + on_each_side + 1): - yield page + yield from range(number + 1, number + on_each_side + 1) yield self.ELLIPSIS - for page in range(num_pages - on_ends + 1, num_pages + 1): - yield page + yield from range(num_pages - on_ends + 1, num_pages + 1) else: - for page in range(number + 1, num_pages + 1): - yield page + yield from range(number + 1, num_pages + 1) def _get_page(self, *args, **kwargs): """ diff --git a/docs/intro/_images/admin05t.png b/docs/intro/_images/admin05t.png index de66ee7746a4..f4e3214721c9 100644 Binary files a/docs/intro/_images/admin05t.png and b/docs/intro/_images/admin05t.png differ diff --git a/docs/intro/_images/admin07.png b/docs/intro/_images/admin07.png index b591e822178f..b5d9a791fd94 100644 Binary files a/docs/intro/_images/admin07.png and b/docs/intro/_images/admin07.png differ diff --git a/docs/intro/_images/admin08t.png b/docs/intro/_images/admin08t.png index b77fdc03c6d2..23f48fdd41f1 100644 Binary files a/docs/intro/_images/admin08t.png and b/docs/intro/_images/admin08t.png differ diff --git a/docs/intro/_images/admin09.png b/docs/intro/_images/admin09.png index 16ccff4b416e..cf2e9a9e1ce8 100644 Binary files a/docs/intro/_images/admin09.png and b/docs/intro/_images/admin09.png differ diff --git a/docs/intro/_images/admin10t.png b/docs/intro/_images/admin10t.png index e0376ec700ae..5533531f167a 100644 Binary files a/docs/intro/_images/admin10t.png and b/docs/intro/_images/admin10t.png differ diff --git a/docs/intro/_images/admin14t.png b/docs/intro/_images/admin14t.png index 44ae24fe4001..2fea20f5c1ea 100644 Binary files a/docs/intro/_images/admin14t.png and b/docs/intro/_images/admin14t.png differ diff --git a/docs/ref/contrib/admin/_images/fieldsets.png b/docs/ref/contrib/admin/_images/fieldsets.png index e5bc614f25d8..5b3f472b8dcd 100644 Binary files a/docs/ref/contrib/admin/_images/fieldsets.png and b/docs/ref/contrib/admin/_images/fieldsets.png differ diff --git a/docs/ref/contrib/admin/_images/raw_id_fields.png b/docs/ref/contrib/admin/_images/raw_id_fields.png index 7f16b11032d3..42f1d48fc236 100644 Binary files a/docs/ref/contrib/admin/_images/raw_id_fields.png and b/docs/ref/contrib/admin/_images/raw_id_fields.png differ diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index d52857d6badb..b3c7c2c42699 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 6c6890b811ad..d10225dfdf80 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -99,6 +99,19 @@ Minor features preserve :ref:`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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -110,6 +123,9 @@ Minor features * The default iteration count for the PBKDF2 password hasher is increased from 1,200,000 to 1,500,000. +* :attr:`.Permission.name` and :attr:`.Permission.codename` values are now + renamed when renaming models via a migration. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -380,6 +396,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/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index f9d216e1dfaa..77888febb326 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -219,6 +219,18 @@ permissions for new models each time you run :djadmin:`manage.py migrate ` (the function that creates permissions is connected to the :data:`~django.db.models.signals.post_migrate` signal). +When a model is renamed in an installed application, Django automatically +updates the associated default permissions to match the new model name when +you run :djadmin:`manage.py migrate `. + +If a permission with the new codename already exists +(for example, due to a leftover permission from a previous migration), +Django raises an error with next steps. + +.. versionchanged:: 6.1 + + Updating permissions when models are renamed was added. + Assuming you have an application with an :attr:`~django.db.models.Options.app_label` ``foo`` and a model named ``Bar``, to test for basic permissions you should use: diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 6956c37740ed..50b3a7babaee 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -384,21 +384,23 @@ def test_stacked_inline_hidden_field_with_view_only_permissions(self): response = self.client.get(url) # The whole line containing name + position fields is not hidden. self.assertContains( - response, '
' + response, + "
', ) # The div containing the position field is hidden. self.assertInHTML( - '', response.rendered_content, ) self.assertInHTML( - '', response.rendered_content, ) @@ -419,17 +421,17 @@ def test_stacked_inline_single_hidden_field_in_line_with_view_only_permissions( # The whole line containing position field is hidden. self.assertInHTML( '