diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 6e4edbd66d..0000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,7 +0,0 @@
-**/*.min.js
-**/vendor/**/*.js
-django/contrib/gis/templates/**/*.js
-django/views/templates/*.js
-docs/_build/**/*.js
-node_modules/**.js
-tests/**/*.js
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 332755a844..0000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-    "rules": {
-        "camelcase": ["off", {"properties": "always"}],
-        "comma-spacing": ["error", {"before": false, "after": true}],
-        "curly": ["error", "all"],
-        "dot-notation": ["error", {"allowKeywords": true}],
-        "eqeqeq": ["error"],
-        "indent": ["error", 4],
-        "key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
-        "linebreak-style": ["error", "unix"],
-        "new-cap": ["off", {"newIsCap": true, "capIsNew": true}],
-        "no-alert": ["off"],
-        "no-eval": ["error"],
-        "no-extend-native": ["error", {"exceptions": ["Date", "String"]}],
-        "no-multi-spaces": ["error"],
-        "no-octal-escape": ["error"],
-        "no-script-url": ["error"],
-        "no-shadow": ["error", {"hoist": "functions"}],
-        "no-underscore-dangle": ["error"],
-        "no-unused-vars": ["error", {"vars": "local", "args": "none"}],
-        "no-var": ["error"],
-        "prefer-const": ["error"],
-        "quotes": ["off", "single"],
-        "semi": ["error", "always"],
-        "space-before-blocks": ["error", "always"],
-        "space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}],
-        "space-infix-ops": ["error", {"int32Hint": false}],
-        "strict": ["error", "global"]
-    },
-    "env": {
-        "browser": true,
-        "es6": true
-    },
-    "globals": {
-        "django": false
-    }
-}
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..f2116902ef
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,15 @@
+# Trac ticket number
+<!-- Replace XXXXX with the corresponding Trac ticket number, or delete the line and write "N/A" if this is a trivial PR. -->
+
+ticket-XXXXX
+
+# Branch description
+Provide a concise overview of the issue or rationale behind the proposed changes.
+
+# Checklist
+- [ ] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
+- [ ] The commit message is written in past tense, mentions the ticket number, and ends with a period.
+- [ ] I have checked the "Has patch" ticket flag in the Trac system.
+- [ ] I have added or updated relevant tests.
+- [ ] I have added or updated relevant docs, including release notes if applicable.
+- [ ] I have attached screenshots in both light and dark modes for any UI changes.
diff --git a/.github/workflows/reminders_check.yml b/.github/workflows/reminders_check.yml
new file mode 100644
index 0000000000..6b5ef92367
--- /dev/null
+++ b/.github/workflows/reminders_check.yml
@@ -0,0 +1,17 @@
+name: Check reminders
+
+on:
+  schedule:
+    - cron: '0 * * * *'  # At the start of every hour
+  workflow_dispatch:
+
+permissions:
+  contents: read
+  pull-requests: write
+
+jobs:
+  reminders:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check reminders and notify users
+        uses: agrc/reminder-action@e59091b4e9705a6108120cb50823108df35b5392
diff --git a/.github/workflows/reminders_create.yml b/.github/workflows/reminders_create.yml
new file mode 100644
index 0000000000..97059e507b
--- /dev/null
+++ b/.github/workflows/reminders_create.yml
@@ -0,0 +1,17 @@
+name: Create reminders
+
+on:
+  issue_comment:
+    types: [created, edited]
+  workflow_dispatch:
+
+permissions:
+  contents: read
+  pull-requests: write
+
+jobs:
+  reminders:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check comments and create reminders
+        uses: agrc/create-reminder-action@922893a5705067719c4c4751843962f56aabf5eb
diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml
index b4ef57cd6a..8b1f01ad86 100644
--- a/.github/workflows/schedule_tests.yml
+++ b/.github/workflows/schedule_tests.yml
@@ -37,6 +37,32 @@ jobs:
       - name: Run tests
         run: python tests/runtests.py -v2
 
+  pyc-only:
+    runs-on: ubuntu-latest
+    name: Byte-compiled Django with no source files (only .pyc files)
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+          cache: 'pip'
+      - name: Install libmemcached-dev for pylibmc
+        run: sudo apt-get install libmemcached-dev
+      - name: Install and upgrade packaging tools
+        run: python -m pip install --upgrade pip setuptools wheel
+      - run: python -m pip install .
+      - name: Prepare site-packages
+        run: |
+          DJANGO_PACKAGE_ROOT=$(python -c 'import site; print(site.getsitepackages()[0])')/django
+          echo $DJANGO_PACKAGE_ROOT
+          python -m compileall -b $DJANGO_PACKAGE_ROOT
+          find $DJANGO_PACKAGE_ROOT -name '*.py' -print -delete
+      - run: python -m pip install -r tests/requirements/py3.txt
+      - name: Run tests
+        run: python tests/runtests.py --verbosity=2
+
   pypy-sqlite:
     runs-on: ubuntu-latest
     name: Ubuntu, SQLite, PyPy3.10
@@ -64,7 +90,7 @@ jobs:
     continue-on-error: true
     services:
       postgres:
-        image: postgres:13-alpine
+        image: postgres:14-alpine
         env:
           POSTGRES_DB: django
           POSTGRES_USER: user
@@ -137,7 +163,7 @@ jobs:
     name: Selenium tests, PostgreSQL
     services:
       postgres:
-        image: postgres:13-alpine
+        image: postgres:14-alpine
         env:
           POSTGRES_DB: django
           POSTGRES_USER: user
diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml
index c85a258949..7b9db7d064 100644
--- a/.github/workflows/screenshots.yml
+++ b/.github/workflows/screenshots.yml
@@ -30,17 +30,31 @@ jobs:
       - name: Install and upgrade packaging tools
         run: python -m pip install --upgrade pip setuptools wheel
       - run: python -m pip install -r tests/requirements/py3.txt -e .
+
       - name: Run Selenium tests with screenshots
-        id: generate-screenshots
         working-directory: ./tests/
+        run: python -Wall runtests.py --verbosity=2 --noinput --selenium=chrome --headless --screenshots --settings=test_sqlite --parallel=2
+
+      - name: Cache oxipng
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/
+          key: ${{ runner.os }}-cargo
+
+      - name: Install oxipng
+        run: which oxipng || cargo install oxipng
+
+      - name: Optimize screenshots
+        run: oxipng --interlace=0 --opt=4 --strip=safe tests/screenshots/*.png
+
+      - name: Organize screenshots
         run: |
-          python -Wall runtests.py --verbosity 2 --noinput --selenium=chrome --headless --screenshots --settings=test_sqlite --parallel 2
-          echo "date=$(date)" >> $GITHUB_OUTPUT
-          echo "🖼️ **Screenshots created**" >> $GITHUB_STEP_SUMMARY
-          echo "Generated screenshots for ${{ github.event.pull_request.head.sha }} at $(date)" >> $GITHUB_STEP_SUMMARY
+          mkdir --parents "/tmp/screenshots/${{ github.event.pull_request.head.sha }}"
+          mv tests/screenshots/* "/tmp/screenshots/${{ github.event.pull_request.head.sha }}/"
 
       - name: Upload screenshots
         uses: actions/upload-artifact@v4
         with:
           name: screenshots-${{ github.event.pull_request.head.sha }}
-          path: tests/screenshots/
+          path: /tmp/screenshots/
+          if-no-files-found: error
diff --git a/.github/workflows/selenium.yml b/.github/workflows/selenium.yml
index fa916a0ded..7e46e0cfb1 100644
--- a/.github/workflows/selenium.yml
+++ b/.github/workflows/selenium.yml
@@ -43,7 +43,7 @@ jobs:
     name: PostgreSQL
     services:
       postgres:
-        image: postgres:13-alpine
+        image: postgres:14-alpine
         env:
           POSTGRES_DB: django
           POSTGRES_USER: user
diff --git a/.gitignore b/.gitignore
index 7b065ff5fc..4da040dcfc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
 *.pot
 *.py[co]
 .tox/
+venv/
 __pycache__
 MANIFEST
 dist/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d6ea11e8e0..d1c74a66c8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/psf/black-pre-commit-mirror
-    rev: 24.1.0
+    rev: 24.2.0
     hooks:
     - id: black
       exclude: \.py-tpl$
@@ -9,8 +9,9 @@ repos:
     hooks:
       - id: blacken-docs
         additional_dependencies:
-        - black==24.1.0
+        - black==24.2.0
         files: 'docs/.*\.txt$'
+        args: ["--rst-literal-block"]
   - repo: https://github.com/PyCQA/isort
     rev: 5.13.2
     hooks:
@@ -20,6 +21,6 @@ repos:
     hooks:
       - id: flake8
   - repo: https://github.com/pre-commit/mirrors-eslint
-    rev: v8.56.0
+    rev: v9.2.0
     hooks:
       - id: eslint
diff --git a/AUTHORS b/AUTHORS
index 8eb4044dfc..55b704b94c 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -6,6 +6,7 @@ people who have submitted patches, reported bugs, added translations, helped
 answer newbie questions, and generally made Django that much better:
 
     Aaron Cannon <cannona@fireantproductions.com>
+    Aaron Linville <aaron@linville.org>
     Aaron Swartz <http://www.aaronsw.com/>
     Aaron T. Myers <atmyers@gmail.com>
     Abeer Upadhyay <ab.esquarer@gmail.com>
@@ -20,6 +21,7 @@ answer newbie questions, and generally made Django that much better:
     Adam Johnson <https://github.com/adamchainz>
     Adam Malinowski <https://adammalinowski.co.uk/>
     Adam Vandenberg
+    Adam Zapletal <https://adamzap.com/>
     Ade Lee <alee@redhat.com>
     Adiyat Mubarak <adiyatmubarak@gmail.com>
     Adnan Umer <u.adnan@outlook.com>
@@ -44,6 +46,8 @@ answer newbie questions, and generally made Django that much better:
     Albert Wang <https://github.com/albertyw/>
     Alcides Fonseca
     Aldian Fazrihady <mobile@aldian.net>
+    Alejandro García Ruiz de Oteiza <https://github.com/AlexOteiza>
+    Aleksander Milinkevich <milinsoft@gmail.com>
     Aleksandra Sendecka <asendecka@hauru.eu>
     Aleksi Häkli <aleksi.hakli@iki.fi>
     Alex Dutton <django@alexdutton.co.uk>
@@ -318,6 +322,7 @@ answer newbie questions, and generally made Django that much better:
     Erik Karulf <erik@karulf.com>
     Erik Romijn <django@solidlinks.nl>
     eriks@win.tue.nl
+    Erin Kelly <erin.ch.kelly@gmail.com>
     Erwin Junge <erwin@junge.nl>
     Esdras Beleza <linux@esdrasbeleza.com>
     Espen Grindhaug <http://grindhaug.org/>
@@ -325,6 +330,7 @@ answer newbie questions, and generally made Django that much better:
     Eugene Lazutkin <http://lazutkin.com/blog/>
     Evan Grim <https://github.com/egrim>
     Fabian Büchler <fabian.buechler@inoqo.com>
+    Fabian Braun <fsbraun@gmx.de>
     Fabrice Aneche <akh@nobugware.com>
     Faishal Manzar <https://github.com/faishal882>
     Farhaan Bukhsh <farhaan.bukhsh@gmail.com>
@@ -369,6 +375,7 @@ answer newbie questions, and generally made Django that much better:
     George Karpenkov <george@metaworld.ru>
     George Song <george@damacy.net>
     George Vilches <gav@thataddress.com>
+    George Y. Kussumoto <georgeyk.dev@gmail.com>
     Georg "Hugo" Bauer <gb@hugo.westfalen.de>
     Georgi Stanojevski <glisha@gmail.com>
     Gerardo Orozco <gerardo.orozco.mosqueda@gmail.com>
@@ -415,7 +422,6 @@ answer newbie questions, and generally made Django that much better:
     Iacopo Spalletti <i.spalletti@nephila.it>
     Ian A Wilson <http://ianawilson.com>
     Ian Clelland <clelland@gmail.com>
-    Ian G. Kelly <ian.g.kelly@gmail.com>
     Ian Holsman <http://feh.holsman.net/>
     Ian Lee <IanLee1521@gmail.com>
     Ibon <ibonso@gmail.com>
@@ -504,6 +510,7 @@ answer newbie questions, and generally made Django that much better:
     Joe Topjian <http://joe.terrarum.net/geek/code/python/django/>
     Johan C. Stöver <johan@nilling.nl>
     Johann Queuniet <johann.queuniet@adh.naellia.eu>
+    Johannes Westphal <jojo@w-hat.de>
     john@calixto.net
     John D'Agostino <john.dagostino@gmail.com>
     John D'Ambrosio <dambrosioj@gmail.com>
@@ -560,6 +567,7 @@ answer newbie questions, and generally made Django that much better:
     Karderio <karderio@gmail.com>
     Karen Tracey <kmtracey@gmail.com>
     Karol Sikora <elektrrrus@gmail.com>
+    Kasun Herath <kasunh01@gmail.com>
     Katherine “Kati” Michel <kthrnmichel@gmail.com>
     Kathryn Killebrew <kathryn.killebrew@gmail.com>
     Katie Miller <katie@sub50.com>
@@ -761,6 +769,7 @@ answer newbie questions, and generally made Django that much better:
     Nicolas Noé <nicolas@niconoe.eu>
     Nikita Marchant <nikita.marchant@gmail.com>
     Nikita Sobolev <mail@sobolevn.me>
+    Nina Menezes <https://github.com/nmenezes0>
     Niran Babalola <niran@niran.org>
     Nis Jørgensen <nis@superlativ.dk>
     Nowell Strite <https://nowell.strite.org/>
@@ -918,6 +927,7 @@ answer newbie questions, and generally made Django that much better:
     Sergey Fedoseev <fedoseev.sergey@gmail.com>
     Sergey Kolosov <m17.admin@gmail.com>
     Seth Hill <sethrh@gmail.com>
+    Shafiya Adzhani <adz.arsym@gmail.com>
     Shai Berger <shai@platonix.com>
     Shannon -jj Behrens <https://www.jjinux.com/>
     Shawn Milochik <shawn@milochik.com>
diff --git a/django/__init__.py b/django/__init__.py
index af19c36b41..67d6ecc45d 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -1,6 +1,6 @@
 from django.utils.version import get_version
 
-VERSION = (5, 1, 0, "alpha", 0)
+VERSION = (5, 2, 0, "alpha", 0)
 
 __version__ = get_version(VERSION)
 
diff --git a/django/conf/locale/__init__.py b/django/conf/locale/__init__.py
index 1b21ffd1f1..6ac7bd3bdb 100644
--- a/django/conf/locale/__init__.py
+++ b/django/conf/locale/__init__.py
@@ -480,7 +480,7 @@ LANG_INFO = {
         "bidi": False,
         "code": "sk",
         "name": "Slovak",
-        "name_local": "Slovensky",
+        "name_local": "slovensky",
     },
     "sl": {
         "bidi": False,
diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po
index cb9e747144..b47726e67a 100644
--- a/django/conf/locale/en/LC_MESSAGES/django.po
+++ b/django/conf/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Django\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-09-18 11:41-0300\n"
+"POT-Creation-Date: 2024-05-22 11:46-0300\n"
 "PO-Revision-Date: 2010-05-13 15:35+0200\n"
 "Last-Translator: Django team\n"
 "Language-Team: English <en@li.org>\n"
@@ -448,6 +448,10 @@ msgstr ""
 msgid "Enter a valid value."
 msgstr ""
 
+#: core/validators.py:70
+msgid "Enter a valid domain name."
+msgstr ""
+
 #: core/validators.py:104 forms/fields.py:759
 msgid "Enter a valid URL."
 msgstr ""
@@ -472,16 +476,22 @@ msgid ""
 "hyphens."
 msgstr ""
 
-#: core/validators.py:279 core/validators.py:306
-msgid "Enter a valid IPv4 address."
+#: core/validators.py:327 core/validators.py:336 core/validators.py:350
+#: db/models/fields/__init__.py:2219
+#, python-format
+msgid "Enter a valid %(protocol)s address."
 msgstr ""
 
-#: core/validators.py:286 core/validators.py:307
-msgid "Enter a valid IPv6 address."
+#: core/validators.py:329
+msgid "IPv4"
 msgstr ""
 
-#: core/validators.py:298 core/validators.py:305
-msgid "Enter a valid IPv4 or IPv6 address."
+#: core/validators.py:338 utils/ipv6.py:30
+msgid "IPv6"
+msgstr ""
+
+#: core/validators.py:352
+msgid "IPv4 or IPv6"
 msgstr ""
 
 #: core/validators.py:341
diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py
index aa43718cd6..94e700cf68 100644
--- a/django/contrib/admin/checks.py
+++ b/django/contrib/admin/checks.py
@@ -816,8 +816,7 @@ class ModelAdminChecks(BaseModelAdminChecks):
             *self._check_list_editable(admin_obj),
             *self._check_search_fields(admin_obj),
             *self._check_date_hierarchy(admin_obj),
-            *self._check_action_permission_methods(admin_obj),
-            *self._check_actions_uniqueness(admin_obj),
+            *self._check_actions(admin_obj),
         ]
 
     def _check_save_as(self, obj):
@@ -915,21 +914,19 @@ class ModelAdminChecks(BaseModelAdminChecks):
             try:
                 field = getattr(obj.model, item)
             except AttributeError:
-                return [
-                    checks.Error(
-                        "The value of '%s' refers to '%s', which is not a "
-                        "callable, an attribute of '%s', or an attribute or "
-                        "method on '%s'."
-                        % (
-                            label,
-                            item,
-                            obj.__class__.__name__,
-                            obj.model._meta.label,
-                        ),
-                        obj=obj.__class__,
-                        id="admin.E108",
-                    )
-                ]
+                try:
+                    field = get_fields_from_path(obj.model, item)[-1]
+                except (FieldDoesNotExist, NotRelationField):
+                    return [
+                        checks.Error(
+                            f"The value of '{label}' refers to '{item}', which is not "
+                            f"a callable or attribute of '{obj.__class__.__name__}', "
+                            "or an attribute, method, or field on "
+                            f"'{obj.model._meta.label}'.",
+                            obj=obj.__class__,
+                            id="admin.E108",
+                        )
+                    ]
         if (
             getattr(field, "is_relation", False)
             and (field.many_to_many or field.one_to_many)
@@ -1197,13 +1194,12 @@ class ModelAdminChecks(BaseModelAdminChecks):
                 else:
                     return []
 
-    def _check_action_permission_methods(self, obj):
-        """
-        Actions with an allowed_permission attribute require the ModelAdmin to
-        implement a has_<perm>_permission() method for each permission.
-        """
-        actions = obj._get_base_actions()
+    def _check_actions(self, obj):
         errors = []
+        actions = obj._get_base_actions()
+
+        # Actions with an allowed_permission attribute require the ModelAdmin
+        # to implement a has_<perm>_permission() method for each permission.
         for func, name, _ in actions:
             if not hasattr(func, "allowed_permissions"):
                 continue
@@ -1222,12 +1218,8 @@ class ModelAdminChecks(BaseModelAdminChecks):
                             id="admin.E129",
                         )
                     )
-        return errors
-
-    def _check_actions_uniqueness(self, obj):
-        """Check that every action has a unique __name__."""
-        errors = []
-        names = collections.Counter(name for _, name, _ in obj._get_base_actions())
+        # Names need to be unique.
+        names = collections.Counter(name for _, name, _ in actions)
         for name, count in names.items():
             if count > 1:
                 errors.append(
diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
index 675c4a5d49..10a039af2a 100644
--- a/django/contrib/admin/filters.py
+++ b/django/contrib/admin/filters.py
@@ -140,7 +140,7 @@ class SimpleListFilter(FacetsMixin, ListFilter):
             if lookup_qs is not None:
                 counts[f"{i}__c"] = models.Count(
                     pk_attname,
-                    filter=lookup_qs.query.where,
+                    filter=models.Q(pk__in=lookup_qs),
                 )
         self.used_parameters[self.parameter_name] = original_value
         return counts
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 90ca7affc8..d28a382814 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -18,6 +18,7 @@ from django.db.models.fields.related import (
 from django.forms.utils import flatatt
 from django.template.defaultfilters import capfirst, linebreaksbr
 from django.urls import NoReverseMatch, reverse
+from django.utils.functional import cached_property
 from django.utils.html import conditional_escape, format_html
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext
@@ -116,10 +117,14 @@ class Fieldset:
 
     @property
     def media(self):
-        if "collapse" in self.classes:
-            return forms.Media(js=["admin/js/collapse.js"])
         return forms.Media()
 
+    @cached_property
+    def is_collapsible(self):
+        if any([field in self.fields for field in self.form.errors]):
+            return False
+        return "collapse" in self.classes
+
     def __iter__(self):
         for field in self.fields:
             yield Fieldline(
@@ -438,6 +443,12 @@ class InlineAdminFormSet:
     def forms(self):
         return self.formset.forms
 
+    @cached_property
+    def is_collapsible(self):
+        if any(self.formset.errors):
+            return False
+        return "collapse" in self.classes
+
     def non_form_errors(self):
         return self.formset.non_form_errors()
 
@@ -498,13 +509,18 @@ class InlineAdminForm(AdminForm):
             # Auto fields are editable, so check for auto or non-editable pk.
             self.form._meta.model._meta.auto_field
             or not self.form._meta.model._meta.pk.editable
+            # The pk can be editable, but excluded from the inline.
+            or (
+                self.form._meta.exclude
+                and self.form._meta.model._meta.pk.name in self.form._meta.exclude
+            )
             or
             # Also search any parents for an auto field. (The pk info is
             # propagated to child models so that does not need to be checked
             # in parents.)
             any(
                 parent._meta.auto_field or not parent._meta.model._meta.pk.editable
-                for parent in self.form._meta.model._meta.get_parent_list()
+                for parent in self.form._meta.model._meta.all_parents
             )
         )
 
diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/django.po b/django/contrib/admin/locale/en/LC_MESSAGES/django.po
index d771ecbcad..c216532a03 100644
--- a/django/contrib/admin/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/admin/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Django\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-09-18 11:41-0300\n"
+"POT-Creation-Date: 2024-05-22 11:46-0300\n"
 "PO-Revision-Date: 2010-05-13 15:35+0200\n"
 "Last-Translator: Django team\n"
 "Language-Team: English <en@li.org>\n"
@@ -247,11 +247,6 @@ msgid ""
 "The {name} “{obj}” was changed successfully. You may edit it again below."
 msgstr ""
 
-#: contrib/admin/options.py:1497
-#, python-brace-format
-msgid "The {name} “{obj}” was added successfully. You may edit it again below."
-msgstr ""
-
 #: contrib/admin/options.py:1516
 #, python-brace-format
 msgid ""
@@ -475,6 +470,10 @@ msgstr ""
 msgid "Change password"
 msgstr ""
 
+#: contrib/admin/templates/admin/auth/user/change_password.html:18
+msgid "Set password"
+msgstr ""
+
 #: contrib/admin/templates/admin/auth/user/change_password.html:25
 #: contrib/admin/templates/admin/change_form.html:43
 #: contrib/admin/templates/admin/change_list.html:52
@@ -490,6 +489,20 @@ msgstr[1] ""
 msgid "Enter a new password for the user <strong>%(username)s</strong>."
 msgstr ""
 
+#: contrib/admin/templates/admin/auth/user/change_password.html:35
+msgid ""
+"This action will <strong>enable</strong> password-based authentication for "
+"this user."
+msgstr ""
+
+#: contrib/admin/templates/admin/auth/user/change_password.html:71
+msgid "Disable password-based authentication"
+msgstr ""
+
+#: contrib/admin/templates/admin/auth/user/change_password.html:73
+msgid "Enable password-based authentication"
+msgstr ""
+
 #: contrib/admin/templates/admin/base.html:28
 msgid "Skip to main content"
 msgstr ""
diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po b/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
index b0b92fb140..443c0b9558 100644
--- a/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
+++ b/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
@@ -4,7 +4,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Django\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-09-18 15:04-0300\n"
+"POT-Creation-Date: 2024-05-22 11:46-0300\n"
 "PO-Revision-Date: 2010-05-13 15:35+0200\n"
 "Last-Translator: Django team\n"
 "Language-Team: English <en@li.org>\n"
@@ -381,12 +381,3 @@ msgstr ""
 msgctxt "one letter Saturday"
 msgid "S"
 msgstr ""
-
-#: contrib/admin/static/admin/js/collapse.js:16
-#: contrib/admin/static/admin/js/collapse.js:34
-msgid "Show"
-msgstr ""
-
-#: contrib/admin/static/admin/js/collapse.js:30
-msgid "Hide"
-msgstr ""
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index d97597fe66..9cc891d807 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -6,7 +6,7 @@ import warnings
 from functools import partial, update_wrapper
 from urllib.parse import parse_qsl
 from urllib.parse import quote as urlquote
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 
 from django import forms
 from django.conf import settings
@@ -475,24 +475,25 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
                 # Lookups on nonexistent fields are ok, since they're ignored
                 # later.
                 break
+            if not prev_field or (
+                prev_field.is_relation
+                and field not in model._meta.parents.values()
+                and field is not model._meta.auto_field
+                and (
+                    model._meta.auto_field is None
+                    or part not in getattr(prev_field, "to_fields", [])
+                )
+                and (field.is_relation or not field.primary_key)
+            ):
+                relation_parts.append(part)
             if not getattr(field, "path_infos", None):
                 # This is not a relational field, so further parts
                 # must be transforms.
                 break
-            if (
-                not prev_field
-                or (field.is_relation and field not in model._meta.parents.values())
-                or (
-                    prev_field.is_relation
-                    and model._meta.auto_field is None
-                    and part not in getattr(prev_field, "to_fields", [])
-                )
-            ):
-                relation_parts.append(part)
             prev_field = field
             model = field.path_infos[-1].to_opts.model
 
-        if not relation_parts or len(parts) == 1:
+        if len(relation_parts) <= 1:
             # Either a local field filter, or no fields at all.
             return True
         valid_lookups = {self.date_hierarchy}
@@ -1032,7 +1033,10 @@ class ModelAdmin(BaseModelAdmin):
 
     @staticmethod
     def _get_action_description(func, name):
-        return getattr(func, "short_description", capfirst(name.replace("_", " ")))
+        try:
+            return func.short_description
+        except AttributeError:
+            return capfirst(name.replace("_", " "))
 
     def _get_base_actions(self):
         """Return the list of actions, prior to any request-based filtering."""
@@ -1380,7 +1384,7 @@ class ModelAdmin(BaseModelAdmin):
         )
 
     def _get_preserved_qsl(self, request, preserved_filters):
-        query_string = urlparse(request.build_absolute_uri()).query
+        query_string = urlsplit(request.build_absolute_uri()).query
         return parse_qsl(query_string.replace(preserved_filters, ""))
 
     def response_add(self, request, obj, post_url_continue=None):
@@ -2394,8 +2398,6 @@ class InlineModelAdmin(BaseModelAdmin):
         js = ["vendor/jquery/jquery%s.js" % extra, "jquery.init.js", "inlines.js"]
         if self.filter_vertical or self.filter_horizontal:
             js.extend(["SelectBox.js", "SelectFilter2.js"])
-        if self.classes and "collapse" in self.classes:
-            js.append("collapse.js")
         return forms.Media(js=["admin/js/%s" % url for url in js])
 
     def get_extra(self, request, obj=None, **kwargs):
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index bb02cb08ac..dc67262afc 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -7,11 +7,12 @@ from django.contrib.admin import ModelAdmin, actions
 from django.contrib.admin.exceptions import AlreadyRegistered, NotRegistered
 from django.contrib.admin.views.autocomplete import AutocompleteJsonView
 from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth.decorators import login_not_required
 from django.core.exceptions import ImproperlyConfigured
 from django.db.models.base import ModelBase
 from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
 from django.template.response import TemplateResponse
-from django.urls import NoReverseMatch, Resolver404, resolve, reverse
+from django.urls import NoReverseMatch, Resolver404, resolve, reverse, reverse_lazy
 from django.utils.decorators import method_decorator
 from django.utils.functional import LazyObject
 from django.utils.module_loading import import_string
@@ -259,6 +260,8 @@ class AdminSite:
                 return self.admin_view(view, cacheable)(*args, **kwargs)
 
             wrapper.admin_site = self
+            # Used by LoginRequiredMiddleware.
+            wrapper.login_url = reverse_lazy("admin:login", current_app=self.name)
             return update_wrapper(wrapper, view)
 
         # Admin-site-wide views.
@@ -402,6 +405,7 @@ class AdminSite:
         return LogoutView.as_view(**defaults)(request)
 
     @method_decorator(never_cache)
+    @login_not_required
     def login(self, request, extra_context=None):
         """
         Display the login form for the given HttpRequest.
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index 3a80e3a3c9..769195af13 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -84,6 +84,8 @@ html[data-theme="light"],
         "Segoe UI Emoji",
         "Segoe UI Symbol",
         "Noto Color Emoji";
+
+    color-scheme: light;
 }
 
 html, body {
@@ -217,6 +219,10 @@ fieldset {
     border-top: 1px solid var(--hairline-color);
 }
 
+details summary {
+    cursor: pointer;
+}
+
 blockquote {
     font-size: 0.6875rem;
     color: #777;
diff --git a/django/contrib/admin/static/admin/css/changelists.css b/django/contrib/admin/static/admin/css/changelists.css
index 72229082c4..005b7768c8 100644
--- a/django/contrib/admin/static/admin/css/changelists.css
+++ b/django/contrib/admin/static/admin/css/changelists.css
@@ -159,7 +159,6 @@
     font-weight: 400;
     padding: 0 15px;
     margin-bottom: 10px;
-    cursor: pointer;
 }
 
 #changelist-filter details summary > * {
diff --git a/django/contrib/admin/static/admin/css/dark_mode.css b/django/contrib/admin/static/admin/css/dark_mode.css
index c49b6bc26f..2123be05c4 100644
--- a/django/contrib/admin/static/admin/css/dark_mode.css
+++ b/django/contrib/admin/static/admin/css/dark_mode.css
@@ -29,6 +29,8 @@
   
       --close-button-bg: #333333;
       --close-button-hover-bg: #666666;
+
+      color-scheme: dark;
     }
   }
 
@@ -63,6 +65,8 @@ html[data-theme="dark"] {
 
     --close-button-bg: #333333;
     --close-button-hover-bg: #666666;
+
+    color-scheme: dark;
 }
 
 /* THEME SWITCH */
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css
index 1d9fa9858e..98f2f02acb 100644
--- a/django/contrib/admin/static/admin/css/forms.css
+++ b/django/contrib/admin/static/admin/css/forms.css
@@ -76,6 +76,20 @@ form ul.inline li {
     padding-right: 7px;
 }
 
+/* FIELDSETS */
+
+fieldset .fieldset-heading,
+fieldset .inline-heading,
+:not(.inline-related) .collapse summary {
+    border: 1px solid var(--header-bg);
+    margin: 0;
+    padding: 8px;
+    font-weight: 400;
+    font-size: 0.8125rem;
+    background: var(--header-bg);
+    color: var(--header-link-color);
+}
+
 /* ALIGNED FIELDSETS */
 
 .aligned label {
@@ -84,14 +98,12 @@ form ul.inline li {
     min-width: 160px;
     width: 160px;
     word-wrap: break-word;
-    line-height: 1;
 }
 
 .aligned label:not(.vCheckboxLabel):after {
     content: '';
     display: inline-block;
     vertical-align: middle;
-    height: 1.625rem;
 }
 
 .aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
@@ -168,11 +180,7 @@ form .aligned table p {
 }
 
 .aligned .vCheckboxLabel {
-    float: none;
-    width: auto;
-    display: inline-block;
-    vertical-align: -3px;
-    padding: 0 0 5px 5px;
+    padding: 1px 0 0 5px;
 }
 
 .aligned .vCheckboxLabel + p.help,
@@ -209,35 +217,16 @@ form div.help ul {
     width: 450px;
 }
 
-/* COLLAPSED FIELDSETS */
+/* COLLAPSIBLE FIELDSETS */
 
-fieldset.collapsed * {
-    display: none;
-}
-
-fieldset.collapsed h2, fieldset.collapsed {
-    display: block;
-}
-
-fieldset.collapsed {
-    border: 1px solid var(--hairline-color);
-    border-radius: 4px;
-    overflow: hidden;
-}
-
-fieldset.collapsed h2 {
-    background: var(--darkened-bg);
-    color: var(--body-quiet-color);
-}
-
-fieldset .collapse-toggle {
-    color: var(--header-link-color);
-}
-
-fieldset.collapsed .collapse-toggle {
+.collapse summary .fieldset-heading,
+.collapse summary .inline-heading {
     background: transparent;
+    border: none;
+    color: currentColor;
     display: inline;
-    color: var(--link-fg);
+    margin: 0;
+    padding: 0;
 }
 
 /* MONOSPACE TEXTAREAS */
@@ -389,14 +378,16 @@ body.popup .submit-row {
     position: relative;
 }
 
-.inline-related h3 {
+.inline-related h4,
+.inline-related:not(.tabular) .collapse summary {
     margin: 0;
     color: var(--body-quiet-color);
     padding: 5px;
     font-size: 0.8125rem;
     background: var(--darkened-bg);
-    border-top: 1px solid var(--hairline-color);
-    border-bottom: 1px solid var(--hairline-color);
+    border: 1px solid var(--hairline-color);
+    border-left-color: var(--darkened-bg);
+    border-right-color: var(--darkened-bg);
 }
 
 .inline-related h3 span.delete {
@@ -415,16 +406,6 @@ body.popup .submit-row {
     width: 100%;
 }
 
-.inline-related fieldset.module h3 {
-    margin: 0;
-    padding: 2px 5px 3px 5px;
-    font-size: 0.6875rem;
-    text-align: left;
-    font-weight: bold;
-    background: #bcd;
-    color: var(--body-bg);
-}
-
 .inline-group .tabular fieldset.module {
     border: none;
 }
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css
index df426ed991..932e824c1c 100644
--- a/django/contrib/admin/static/admin/css/responsive.css
+++ b/django/contrib/admin/static/admin/css/responsive.css
@@ -171,7 +171,7 @@ input[type="submit"], button {
     /* Forms */
 
     label {
-        font-size: 0.875rem;
+        font-size: 1rem;
     }
 
     /*
@@ -192,7 +192,7 @@ input[type="submit"], button {
         margin: 0;
         padding: 6px 8px;
         min-height: 2.25rem;
-        font-size: 0.875rem;
+        font-size: 1rem;
     }
 
     .form-row select {
@@ -565,10 +565,6 @@ input[type="submit"], button {
         padding-top: 15px;
     }
 
-    fieldset.collapsed .form-row {
-        display: none;
-    }
-
     .aligned label {
         width: 100%;
         min-width: auto;
diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css
index 1ab09fd10f..b8f60e0a34 100644
--- a/django/contrib/admin/static/admin/css/rtl.css
+++ b/django/contrib/admin/static/admin/css/rtl.css
@@ -282,6 +282,10 @@ form .form-row p.datetime {
     margin-right: 2px;
 }
 
+.inline-group .tabular td.original p {
+    right: 0;
+}
+
 .selector .selector-chooser {
     margin: 0;
 }
diff --git a/django/contrib/admin/static/admin/css/unusable_password_field.css b/django/contrib/admin/static/admin/css/unusable_password_field.css
new file mode 100644
index 0000000000..d46eb0384c
--- /dev/null
+++ b/django/contrib/admin/static/admin/css/unusable_password_field.css
@@ -0,0 +1,19 @@
+/* Hide warnings fields if usable password is selected */
+form:has(#id_usable_password input[value="true"]:checked) .messagelist {
+    display: none;
+}
+
+/* Hide password fields if unusable password is selected */
+form:has(#id_usable_password input[value="false"]:checked) .field-password1,
+form:has(#id_usable_password input[value="false"]:checked) .field-password2 {
+    display: none;
+}
+
+/* Select appropriate submit button */
+form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password {
+    display: none;
+}
+
+form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password {
+    display: none;
+}
diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js
index fc59eba7c4..6957412462 100644
--- a/django/contrib/admin/static/admin/js/SelectFilter2.js
+++ b/django/contrib/admin/static/admin/js/SelectFilter2.js
@@ -1,4 +1,4 @@
-/*global SelectBox, gettext, interpolate, quickElement, SelectFilter*/
+/*global SelectBox, gettext, ngettext, interpolate, quickElement, SelectFilter*/
 /*
 SelectFilter2 - Turns a multiple-select box into a filter interface.
 
diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js
index 6a2ae91a19..04b25e9684 100644
--- a/django/contrib/admin/static/admin/js/actions.js
+++ b/django/contrib/admin/static/admin/js/actions.js
@@ -1,4 +1,4 @@
-/*global gettext, interpolate, ngettext*/
+/*global gettext, interpolate, ngettext, Actions*/
 'use strict';
 {
     function show(selector) {
diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
index 32e3f5b840..bc3accea37 100644
--- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
+++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
@@ -96,8 +96,8 @@
         // Extract the model from the popup url '.../<model>/add/' or
         // '.../<model>/<id>/change/' depending the action (add or change).
         const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)];
-        // Exclude autocomplete selects.
-        const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] select:not(.admin-autocomplete)`);
+        // Select elements with a specific model reference and context of "available-source".
+        const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`);
 
         selectsRelated.forEach(function(select) {
             if (currentSelect === select) {
diff --git a/django/contrib/admin/static/admin/js/collapse.js b/django/contrib/admin/static/admin/js/collapse.js
deleted file mode 100644
index c6c7b0f68a..0000000000
--- a/django/contrib/admin/static/admin/js/collapse.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/*global gettext*/
-'use strict';
-{
-    window.addEventListener('load', function() {
-        // Add anchor tag for Show/Hide link
-        const fieldsets = document.querySelectorAll('fieldset.collapse');
-        for (const [i, elem] of fieldsets.entries()) {
-            // Don't hide if fields in this fieldset have errors
-            if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
-                elem.classList.add('collapsed');
-                const h2 = elem.querySelector('h2');
-                const link = document.createElement('a');
-                link.id = 'fieldsetcollapser' + i;
-                link.className = 'collapse-toggle';
-                link.href = '#';
-                link.textContent = gettext('Show');
-                h2.appendChild(document.createTextNode(' ('));
-                h2.appendChild(link);
-                h2.appendChild(document.createTextNode(')'));
-            }
-        }
-        // Add toggle to hide/show anchor tag
-        const toggleFunc = function(ev) {
-            if (ev.target.matches('.collapse-toggle')) {
-                ev.preventDefault();
-                ev.stopPropagation();
-                const fieldset = ev.target.closest('fieldset');
-                if (fieldset.classList.contains('collapsed')) {
-                    // Show
-                    ev.target.textContent = gettext('Hide');
-                    fieldset.classList.remove('collapsed');
-                } else {
-                    // Hide
-                    ev.target.textContent = gettext('Show');
-                    fieldset.classList.add('collapsed');
-                }
-            }
-        };
-        document.querySelectorAll('fieldset.module').forEach(function(el) {
-            el.addEventListener('click', toggleFunc);
-        });
-    });
-}
diff --git a/django/contrib/admin/static/admin/js/popup_response.js b/django/contrib/admin/static/admin/js/popup_response.js
index 2b1d3dd31d..fecf0f4798 100644
--- a/django/contrib/admin/static/admin/js/popup_response.js
+++ b/django/contrib/admin/static/admin/js/popup_response.js
@@ -1,4 +1,3 @@
-/*global opener */
 'use strict';
 {
     const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
diff --git a/django/contrib/admin/static/admin/js/unusable_password_field.js b/django/contrib/admin/static/admin/js/unusable_password_field.js
new file mode 100644
index 0000000000..ec26238c29
--- /dev/null
+++ b/django/contrib/admin/static/admin/js/unusable_password_field.js
@@ -0,0 +1,29 @@
+"use strict";
+// Fallback JS for browsers which do not support :has selector used in
+// admin/css/unusable_password_fields.css
+// Remove file once all supported browsers support :has selector
+try {
+    // If browser does not support :has selector this will raise an error
+    document.querySelector("form:has(input)");
+} catch (error) {
+    console.log("Defaulting to javascript for usable password form management: " + error);
+    // JS replacement for unsupported :has selector
+    document.querySelectorAll('input[name="usable_password"]').forEach(option => {
+        option.addEventListener('change', function() {
+            const usablePassword = (this.value === "true" ? this.checked : !this.checked);
+            const submit1 = document.querySelector('input[type="submit"].set-password');
+            const submit2 = document.querySelector('input[type="submit"].unset-password');
+            const messages = document.querySelector('#id_unusable_warning');
+            document.getElementById('id_password1').closest('.form-row').hidden = !usablePassword;
+            document.getElementById('id_password2').closest('.form-row').hidden = !usablePassword;
+            if (messages) {
+                messages.hidden = usablePassword;
+            }
+            if (submit1 && submit2) {
+                submit1.hidden = !usablePassword;
+                submit2.hidden = usablePassword;
+            }
+        });
+        option.dispatchEvent(new Event('change'));
+    });
+}
diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html
index 61cf5b1b40..48406f11a2 100644
--- a/django/contrib/admin/templates/admin/auth/user/add_form.html
+++ b/django/contrib/admin/templates/admin/auth/user/add_form.html
@@ -1,5 +1,5 @@
 {% extends "admin/change_form.html" %}
-{% load i18n %}
+{% load i18n static %}
 
 {% block form_top %}
   {% if not is_popup %}
@@ -8,3 +8,11 @@
     <p>{% translate "Enter a username and password." %}</p>
   {% endif %}
 {% endblock %}
+{% block extrahead %}
+  {{ block.super }}
+  <link rel="stylesheet" href="{% static 'admin/css/unusable_password_field.css' %}">
+{% endblock %}
+{% block admin_change_form_document_ready %}
+  {{ block.super }}
+  <script src="{% static 'admin/js/unusable_password_field.js' %}" defer></script>
+{% endblock %}
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 ebb24ef562..6801fe5fa7 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -2,7 +2,11 @@
 {% load i18n static %}
 {% load admin_urls %}
 
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
+{% block extrastyle %}
+  {{ block.super }}
+  <link rel="stylesheet" href="{% static "admin/css/forms.css" %}">
+  <link rel="stylesheet" href="{% static 'admin/css/unusable_password_field.css' %}">
+{% endblock %}
 {% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.model_name }} change-form{% endblock %}
 {% if not is_popup %}
 {% block breadcrumbs %}
@@ -11,7 +15,7 @@
 &rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
 &rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
 &rsaquo; <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
-&rsaquo; {% translate 'Change password' %}
+&rsaquo; {% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %}
 </div>
 {% endblock %}
 {% endif %}
@@ -27,10 +31,23 @@
 {% endif %}
 
 <p>{% blocktranslate with username=original %}Enter a new password for the user <strong>{{ username }}</strong>.{% endblocktranslate %}</p>
+{% if not form.user.has_usable_password %}
+  <p>{% blocktranslate %}This action will <strong>enable</strong> password-based authentication for this user.{% endblocktranslate %}</p>
+{% endif %}
 
 <fieldset class="module aligned">
 
 <div class="form-row">
+  {{ form.usable_password.errors }}
+  <div class="flex-container">{{ form.usable_password.label_tag }} {{ form.usable_password }}</div>
+  {% 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 %}
+</div>
+
+<div class="form-row field-password1">
   {{ form.password1.errors }}
   <div class="flex-container">{{ form.password1.label_tag }} {{ form.password1 }}</div>
   {% if form.password1.help_text %}
@@ -38,7 +55,7 @@
   {% endif %}
 </div>
 
-<div class="form-row">
+<div class="form-row field-password2">
   {{ form.password2.errors }}
   <div class="flex-container">{{ form.password2.label_tag }} {{ form.password2 }}</div>
   {% if form.password2.help_text %}
@@ -49,9 +66,15 @@
 </fieldset>
 
 <div class="submit-row">
-<input type="submit" value="{% translate 'Change password' %}" class="default">
+  {% if form.user.has_usable_password %}
+  <input type="submit" name="set-password" value="{% translate 'Change password' %}" class="default set-password">
+  <input type="submit" name="unset-password" value="{% translate 'Disable password-based authentication' %}" class="unset-password">
+  {% else %}
+  <input type="submit" name="set-password" value="{% translate 'Enable password-based authentication' %}" class="default set-password">
+  {% endif %}
 </div>
 
 </div>
 </form></div>
+<script src="{% static 'admin/js/unusable_password_field.js' %}" defer></script>
 {% endblock %}
diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
index f01a7ab61c..18e3a2a9fc 100644
--- a/django/contrib/admin/templates/admin/base.html
+++ b/django/contrib/admin/templates/admin/base.html
@@ -121,5 +121,6 @@
   <symbol viewBox="0 0 24 24" width="1rem" height="1rem" id="icon-sun"><path d="M0 0h24v24H0z" fill="currentColor"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol>
 </svg>
 <!-- END SVGs -->
+{% block extrabody %}{% endblock extrabody %}
 </body>
 </html>
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 20cc4a392c..31ff5d6c10 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -47,7 +47,7 @@
 
 {% block field_sets %}
 {% for fieldset in adminform %}
-  {% include "admin/includes/fieldset.html" %}
+  {% include "admin/includes/fieldset.html" with heading_level=2 id_suffix=forloop.counter0 %}
 {% endfor %}
 {% endblock %}
 
diff --git a/django/contrib/admin/templates/admin/color_theme_toggle.html b/django/contrib/admin/templates/admin/color_theme_toggle.html
index f5a326d501..2caa19edbf 100644
--- a/django/contrib/admin/templates/admin/color_theme_toggle.html
+++ b/django/contrib/admin/templates/admin/color_theme_toggle.html
@@ -1,8 +1,8 @@
 {% load i18n %}
 <button class="theme-toggle">
-  <div class="visually-hidden theme-label-when-auto">{% translate 'Toggle theme (current theme: auto)' %}</div>
-  <div class="visually-hidden theme-label-when-light">{% translate 'Toggle theme (current theme: light)' %}</div>
-  <div class="visually-hidden theme-label-when-dark">{% translate 'Toggle theme (current theme: dark)' %}</div>
+  <span class="visually-hidden theme-label-when-auto">{% translate 'Toggle theme (current theme: auto)' %}</span>
+  <span class="visually-hidden theme-label-when-light">{% translate 'Toggle theme (current theme: light)' %}</span>
+  <span class="visually-hidden theme-label-when-dark">{% translate 'Toggle theme (current theme: dark)' %}</span>
   <svg aria-hidden="true" class="theme-icon-when-auto">
     <use xlink:href="#icon-auto" />
   </svg>
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 31d917bff0..73f459ee47 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -3,12 +3,16 @@
      id="{{ inline_admin_formset.formset.prefix }}-group"
      data-inline-type="stacked"
      data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
-<fieldset class="module {{ inline_admin_formset.classes }}">
+<fieldset class="module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
+  {% if inline_admin_formset.is_collapsible %}<details><summary>{% endif %}
+  <h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
   {% if inline_admin_formset.formset.max_num == 1 %}
-    <h2>{{ inline_admin_formset.opts.verbose_name|capfirst }}</h2>
+    {{ inline_admin_formset.opts.verbose_name|capfirst }}
   {% else %}
-    <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+    {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}
   {% endif %}
+  </h2>
+  {% if inline_admin_formset.is_collapsible %}</summary>{% endif %}
 {{ inline_admin_formset.formset.management_form }}
 {{ inline_admin_formset.formset.non_form_errors }}
 
@@ -19,11 +23,16 @@
     {% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
   </h3>
   {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
-  {% for fieldset in inline_admin_form %}
-    {% include "admin/includes/fieldset.html" %}
-  {% endfor %}
+
+  {% with parent_counter=forloop.counter0 %}
+    {% for fieldset in inline_admin_form %}
+      {% include "admin/includes/fieldset.html" with heading_level=4 id_prefix=parent_counter id_suffix=forloop.counter0 %}
+    {% endfor %}
+  {% endwith %}
+
   {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
   {% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
 </div>{% endfor %}
+  {% if inline_admin_formset.is_collapsible %}</details>{% endif %}
 </fieldset>
 </div>
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index 84c9fe3696..7acfda7bd1 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -4,12 +4,16 @@
      data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
   <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
 {{ inline_admin_formset.formset.management_form }}
-<fieldset class="module {{ inline_admin_formset.classes }}">
-   {% if inline_admin_formset.formset.max_num == 1 %}
-     <h2>{{ inline_admin_formset.opts.verbose_name|capfirst }}</h2>
-   {% else %}
-     <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
-   {% endif %}
+<fieldset class="module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
+  {% if inline_admin_formset.is_collapsible %}<details><summary>{% endif %}
+  <h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
+  {% if inline_admin_formset.formset.max_num == 1 %}
+    {{ inline_admin_formset.opts.verbose_name|capfirst }}
+  {% else %}
+    {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}
+  {% endif %}
+  </h2>
+  {% if inline_admin_formset.is_collapsible %}</summary>{% endif %}
    {{ inline_admin_formset.formset.non_form_errors }}
    <table>
      <thead><tr>
@@ -61,6 +65,7 @@
      {% endfor %}
      </tbody>
    </table>
+  {% if inline_admin_formset.is_collapsible %}</details>{% endif %}
 </fieldset>
   </div>
 </div>
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
index 9a4372f791..b4eef47547 100644
--- a/django/contrib/admin/templates/admin/includes/fieldset.html
+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
@@ -1,5 +1,10 @@
-<fieldset class="module aligned {{ fieldset.classes }}">
-    {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
+{% with prefix=fieldset.formset.prefix|default:"fieldset" id_prefix=id_prefix|default:"0" id_suffix=id_suffix|default:"0" name=fieldset.name|default:""|slugify %}
+<fieldset class="module aligned {{ fieldset.classes }}"{% if name %} aria-labelledby="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading"{% endif %}>
+    {% if name %}
+        {% if fieldset.is_collapsible %}<details><summary>{% endif %}
+        <h{{ heading_level|default:2 }} id="{{ prefix }}-{{ id_prefix}}-{{ name }}-{{ id_suffix }}-heading" class="fieldset-heading">{{ fieldset.name }}</h{{ heading_level|default:2 }}>
+        {% if fieldset.is_collapsible %}</summary>{% endif %}
+    {% endif %}
     {% if fieldset.description %}
         <div class="description">{{ fieldset.description|safe }}</div>
     {% endif %}
@@ -9,7 +14,7 @@
             {% for field in line %}
                 <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 %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
+                        <div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% endif %}{% if field.is_checkbox %} checkbox-row{% endif %}">
                             {% if field.is_checkbox %}
                                 {{ field.field }}{{ field.label_tag }}
                             {% else %}
@@ -31,4 +36,6 @@
             {% if not line.fields|length == 1 %}</div>{% endif %}
         </div>
     {% endfor %}
+    {% if name and fieldset.is_collapsible %}</details>{% endif %}
 </fieldset>
+{% endwith %}
diff --git a/django/contrib/admin/templates/admin/widgets/clearable_file_input.html b/django/contrib/admin/templates/admin/widgets/clearable_file_input.html
index ab35253a0d..8b42f192f1 100644
--- a/django/contrib/admin/templates/admin/widgets/clearable_file_input.html
+++ b/django/contrib/admin/templates/admin/widgets/clearable_file_input.html
@@ -1,6 +1,6 @@
 {% if widget.is_initial %}<p class="file-upload">{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
 <span class="clearable-file-input">
-<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% include "django/forms/widgets/attrs.html" %}>
+<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}{% if widget.attrs.checked %} checked{% endif %}>
 <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label></span>{% endif %}<br>
 {{ widget.input_text }}:{% endif %}
 <input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.is_initial %}</p>{% endif %}
diff --git a/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html b/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html
index 8e4356a95c..99b20545af 100644
--- a/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html
+++ b/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html
@@ -1,5 +1,5 @@
 {% load i18n static %}
-<div class="related-widget-wrapper" {% if not model_has_limit_choices_to %}data-model-ref="{{ model }}"{% endif %}>
+<div class="related-widget-wrapper" {% if not model_has_limit_choices_to %}data-model-ref="{{ model_name }}"{% endif %}>
     {{ rendered_widget }}
     {% block links %}
         {% spaceless %}
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 0c32290b6c..fdf6e63f5f 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -18,6 +18,7 @@ from django.contrib.admin.views.main import (
 )
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
+from django.db.models.constants import LOOKUP_SEP
 from django.template import Library
 from django.template.loader import get_template
 from django.templatetags.static import static
@@ -112,7 +113,7 @@ def result_headers(cl):
             # Set ordering for attr that is a property, if defined.
             if isinstance(attr, property) and hasattr(attr, "fget"):
                 admin_order_field = getattr(attr.fget, "admin_order_field", None)
-            if not admin_order_field:
+            if not admin_order_field and LOOKUP_SEP not in field_name:
                 is_field_sortable = False
 
         if not is_field_sortable:
diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py
index 871b0d5f20..176e7a49ed 100644
--- a/django/contrib/admin/templatetags/admin_urls.py
+++ b/django/contrib/admin/templatetags/admin_urls.py
@@ -1,4 +1,4 @@
-from urllib.parse import parse_qsl, unquote, urlparse, urlunparse
+from urllib.parse import parse_qsl, unquote, urlsplit, urlunsplit
 
 from django import template
 from django.contrib.admin.utils import quote
@@ -24,8 +24,8 @@ def add_preserved_filters(context, url, popup=False, to_field=None):
     preserved_filters = context.get("preserved_filters")
     preserved_qsl = context.get("preserved_qsl")
 
-    parsed_url = list(urlparse(url))
-    parsed_qs = dict(parse_qsl(parsed_url[4]))
+    parsed_url = list(urlsplit(url))
+    parsed_qs = dict(parse_qsl(parsed_url[3]))
     merged_qs = {}
 
     if preserved_qsl:
@@ -66,5 +66,5 @@ def add_preserved_filters(context, url, popup=False, to_field=None):
 
     merged_qs.update(parsed_qs)
 
-    parsed_url[4] = urlencode(merged_qs)
-    return urlunparse(parsed_url)
+    parsed_url[3] = urlencode(merged_qs)
+    return urlunsplit(parsed_url)
diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
index 0bcf99ae85..c8e722bcc8 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admin/utils.py
@@ -289,8 +289,8 @@ def lookup_field(name, obj, model_admin=None):
     try:
         f = _get_non_gfk_field(opts, name)
     except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
-        # For non-field values, the value is either a method, property or
-        # returned via a callable.
+        # For non-regular field values, the value is either a method,
+        # property, related field, or returned via a callable.
         if callable(name):
             attr = name
             value = attr(obj)
@@ -298,10 +298,17 @@ def lookup_field(name, obj, model_admin=None):
             attr = getattr(model_admin, name)
             value = attr(obj)
         else:
-            attr = getattr(obj, name)
+            sentinel = object()
+            attr = getattr(obj, name, sentinel)
             if callable(attr):
                 value = attr()
             else:
+                if attr is sentinel:
+                    attr = obj
+                    for part in name.split(LOOKUP_SEP):
+                        attr = getattr(attr, part, sentinel)
+                        if attr is sentinel:
+                            return None, None, None
                 value = attr
             if hasattr(model_admin, "model") and hasattr(model_admin.model, name):
                 attr = getattr(model_admin.model, name)
@@ -345,9 +352,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False, form=None)
     """
     Return a sensible label for a field name. The name can be a callable,
     property (but not created with @property decorator), or the name of an
-    object's attribute, as well as a model field. If return_attr is True, also
-    return the resolved attribute (which could be a callable). This will be
-    None if (and only if) the name refers to a field.
+    object's attribute, as well as a model field, including across related
+    objects. If return_attr is True, also return the resolved attribute
+    (which could be a callable). This will be None if (and only if) the name
+    refers to a field.
     """
     attr = None
     try:
@@ -371,15 +379,15 @@ def label_for_field(name, model, model_admin=None, return_attr=False, form=None)
             elif form and name in form.fields:
                 attr = form.fields[name]
             else:
-                message = "Unable to lookup '%s' on %s" % (
-                    name,
-                    model._meta.object_name,
-                )
-                if model_admin:
-                    message += " or %s" % model_admin.__class__.__name__
-                if form:
-                    message += " or %s" % form.__class__.__name__
-                raise AttributeError(message)
+                try:
+                    attr = get_fields_from_path(model, name)[-1]
+                except (FieldDoesNotExist, NotRelationField):
+                    message = f"Unable to lookup '{name}' on {model._meta.object_name}"
+                    if model_admin:
+                        message += f" or {model_admin.__class__.__name__}"
+                    if form:
+                        message += f" or {form.__class__.__name__}"
+                    raise AttributeError(message)
 
             if hasattr(attr, "short_description"):
                 label = attr.short_description
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 44001f00f9..70b6590811 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -30,6 +30,7 @@ from django.core.exceptions import (
 )
 from django.core.paginator import InvalidPage
 from django.db.models import F, Field, ManyToOneRel, OrderBy
+from django.db.models.constants import LOOKUP_SEP
 from django.db.models.expressions import Combinable
 from django.urls import reverse
 from django.utils.deprecation import RemovedInDjango60Warning
@@ -356,9 +357,9 @@ class ChangeList:
         """
         Return the proper model field name corresponding to the given
         field_name to use for ordering. field_name may either be the name of a
-        proper model field or the name of a method (on the admin or model) or a
-        callable with the 'admin_order_field' attribute. Return None if no
-        proper model field name can be matched.
+        proper model field, possibly across relations, or the name of a method
+        (on the admin or model) or a callable with the 'admin_order_field'
+        attribute. Return None if no proper model field name can be matched.
         """
         try:
             field = self.lookup_opts.get_field(field_name)
@@ -371,7 +372,12 @@ class ChangeList:
             elif hasattr(self.model_admin, field_name):
                 attr = getattr(self.model_admin, field_name)
             else:
-                attr = getattr(self.model, field_name)
+                try:
+                    attr = getattr(self.model, field_name)
+                except AttributeError:
+                    if LOOKUP_SEP in field_name:
+                        return field_name
+                    raise
             if isinstance(attr, property) and hasattr(attr, "fget"):
                 attr = attr.fget
             return getattr(attr, "admin_order_field", None)
@@ -612,7 +618,7 @@ class ChangeList:
             else:
                 if isinstance(field.remote_field, ManyToOneRel):
                     # <FK>_id field names don't require a join.
-                    if field_name != field.get_attname():
+                    if field_name != field.attname:
                         return True
         return False
 
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 216b87671b..00e92bf42d 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -272,6 +272,8 @@ class RelatedFieldWidgetWrapper(forms.Widget):
         self.can_add_related = can_add_related
         # XXX: The UX does not support multiple selected values.
         multiple = getattr(widget, "allow_multiple_selected", False)
+        if not isinstance(widget, AutocompleteMixin):
+            self.attrs["data-context"] = "available-source"
         self.can_change_related = not multiple and can_change_related
         # XXX: The deletion UX can be confusing when dealing with cascading deletion.
         cascade = getattr(rel, "on_delete", None) is CASCADE
@@ -329,6 +331,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
             "name": name,
             "url_params": url_params,
             "model": rel_opts.verbose_name,
+            "model_name": rel_opts.model_name,
             "can_add_related": self.can_add_related,
             "can_change_related": self.can_change_related,
             "can_delete_related": self.can_delete_related,
diff --git a/django/contrib/admindocs/templates/admin_doc/model_detail.html b/django/contrib/admindocs/templates/admin_doc/model_detail.html
index d4a380aa38..1cbde0e44a 100644
--- a/django/contrib/admindocs/templates/admin_doc/model_detail.html
+++ b/django/contrib/admindocs/templates/admin_doc/model_detail.html
@@ -3,7 +3,7 @@
 
 {% block extrahead %}
 {{ block.super }}
-<style type="text/css">
+<style>
 .module table { width:100%; }
 .module table p { padding: 0; margin: 0; }
 </style>
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 5c18d676f2..38a2bb9286 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -359,7 +359,7 @@ class ModelDetailView(BaseAdminDocsView):
                 "app_label": rel.related_model._meta.app_label,
                 "object_name": rel.related_model._meta.object_name,
             }
-            accessor = rel.get_accessor_name()
+            accessor = rel.accessor_name
             fields.append(
                 {
                     "name": "%s.all" % accessor,
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 91257dd3da..3db1445d9e 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -269,4 +269,6 @@ def update_session_auth_hash(request, user):
 
 async def aupdate_session_auth_hash(request, user):
     """See update_session_auth_hash()."""
-    return await sync_to_async(update_session_auth_hash)(request, user)
+    await request.session.acycle_key()
+    if hasattr(user, "get_session_auth_hash") and request.user == user:
+        await request.session.aset(HASH_SESSION_KEY, user.get_session_auth_hash())
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index f9532abc14..90a53a142c 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -66,7 +66,7 @@ class UserAdmin(admin.ModelAdmin):
             None,
             {
                 "classes": ("wide",),
-                "fields": ("username", "password1", "password2"),
+                "fields": ("username", "usable_password", "password1", "password2"),
             },
         ),
     )
@@ -164,10 +164,27 @@ class UserAdmin(admin.ModelAdmin):
         if request.method == "POST":
             form = self.change_password_form(user, request.POST)
             if form.is_valid():
-                form.save()
+                # If disabling password-based authentication was requested
+                # (via the form field `usable_password`), the submit action
+                # must be "unset-password". This check is most relevant when
+                # the admin user has two submit buttons available (for example
+                # when Javascript is disabled).
+                valid_submission = (
+                    form.cleaned_data["set_usable_password"]
+                    or "unset-password" in request.POST
+                )
+                if not valid_submission:
+                    msg = gettext("Conflicting form data submitted. Please try again.")
+                    messages.error(request, msg)
+                    return HttpResponseRedirect(request.get_full_path())
+
+                user = form.save()
                 change_message = self.construct_change_message(request, form, None)
                 self.log_change(request, user, change_message)
-                msg = gettext("Password changed successfully.")
+                if user.has_usable_password():
+                    msg = gettext("Password changed successfully.")
+                else:
+                    msg = gettext("Password-based authentication was disabled.")
                 messages.success(request, msg)
                 update_session_auth_hash(request, form.user)
                 return HttpResponseRedirect(
@@ -187,8 +204,12 @@ class UserAdmin(admin.ModelAdmin):
         fieldsets = [(None, {"fields": list(form.base_fields)})]
         admin_form = admin.helpers.AdminForm(form, fieldsets, {})
 
+        if user.has_usable_password():
+            title = _("Change password: %s")
+        else:
+            title = _("Set password: %s")
         context = {
-            "title": _("Change password: %s") % escape(user.get_username()),
+            "title": title % escape(user.get_username()),
             "adminForm": admin_form,
             "form_url": form_url,
             "form": form,
diff --git a/django/contrib/auth/apps.py b/django/contrib/auth/apps.py
index 4882a27c42..ad6f816809 100644
--- a/django/contrib/auth/apps.py
+++ b/django/contrib/auth/apps.py
@@ -5,7 +5,7 @@ from django.db.models.signals import post_migrate
 from django.utils.translation import gettext_lazy as _
 
 from . import get_user_model
-from .checks import check_models_permissions, check_user_model
+from .checks import check_middleware, check_models_permissions, check_user_model
 from .management import create_permissions
 from .signals import user_logged_in
 
@@ -28,3 +28,4 @@ class AuthConfig(AppConfig):
             user_logged_in.connect(update_last_login, dispatch_uid="update_last_login")
         checks.register(check_user_model, checks.Tags.models)
         checks.register(check_models_permissions, checks.Tags.models)
+        checks.register(check_middleware)
diff --git a/django/contrib/auth/checks.py b/django/contrib/auth/checks.py
index ee8082524d..f2f9a74a6c 100644
--- a/django/contrib/auth/checks.py
+++ b/django/contrib/auth/checks.py
@@ -4,10 +4,27 @@ from types import MethodType
 from django.apps import apps
 from django.conf import settings
 from django.core import checks
+from django.utils.module_loading import import_string
 
 from .management import _get_builtin_permissions
 
 
+def _subclass_index(class_path, candidate_paths):
+    """
+    Return the index of dotted class path (or a subclass of that class) in a
+    list of candidate paths. If it does not exist, return -1.
+    """
+    cls = import_string(class_path)
+    for index, path in enumerate(candidate_paths):
+        try:
+            candidate_cls = import_string(path)
+            if issubclass(candidate_cls, cls):
+                return index
+        except (ImportError, TypeError):
+            continue
+    return -1
+
+
 def check_user_model(app_configs=None, **kwargs):
     if app_configs is None:
         cls = apps.get_model(settings.AUTH_USER_MODEL)
@@ -218,3 +235,28 @@ def check_models_permissions(app_configs=None, **kwargs):
             codenames.add(codename)
 
     return errors
+
+
+def check_middleware(app_configs, **kwargs):
+    errors = []
+
+    login_required_index = _subclass_index(
+        "django.contrib.auth.middleware.LoginRequiredMiddleware",
+        settings.MIDDLEWARE,
+    )
+
+    if login_required_index != -1:
+        auth_index = _subclass_index(
+            "django.contrib.auth.middleware.AuthenticationMiddleware",
+            settings.MIDDLEWARE,
+        )
+        if auth_index == -1 or auth_index > login_required_index:
+            errors.append(
+                checks.Error(
+                    "In order to use django.contrib.auth.middleware."
+                    "LoginRequiredMiddleware, django.contrib.auth.middleware."
+                    "AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
+                    id="auth.E013",
+                )
+            )
+    return errors
diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py
index cfcc4a2d3a..78e76a9ae9 100644
--- a/django/contrib/auth/decorators.py
+++ b/django/contrib/auth/decorators.py
@@ -1,5 +1,8 @@
+import asyncio
 from functools import wraps
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
+
+from asgiref.sync import async_to_sync, sync_to_async
 
 from django.conf import settings
 from django.contrib.auth import REDIRECT_FIELD_NAME
@@ -17,16 +20,13 @@ def user_passes_test(
     """
 
     def decorator(view_func):
-        @wraps(view_func)
-        def _wrapper_view(request, *args, **kwargs):
-            if test_func(request.user):
-                return view_func(request, *args, **kwargs)
+        def _redirect_to_login(request):
             path = request.build_absolute_uri()
             resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
             # If the login url is the same scheme and net location then just
             # use the path as the "next" url.
-            login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
-            current_scheme, current_netloc = urlparse(path)[:2]
+            login_scheme, login_netloc = urlsplit(resolved_login_url)[:2]
+            current_scheme, current_netloc = urlsplit(path)[:2]
             if (not login_scheme or login_scheme == current_scheme) and (
                 not login_netloc or login_netloc == current_netloc
             ):
@@ -35,7 +35,36 @@ def user_passes_test(
 
             return redirect_to_login(path, resolved_login_url, redirect_field_name)
 
-        return _wrapper_view
+        if asyncio.iscoroutinefunction(view_func):
+
+            async def _view_wrapper(request, *args, **kwargs):
+                auser = await request.auser()
+                if asyncio.iscoroutinefunction(test_func):
+                    test_pass = await test_func(auser)
+                else:
+                    test_pass = await sync_to_async(test_func)(auser)
+
+                if test_pass:
+                    return await view_func(request, *args, **kwargs)
+                return _redirect_to_login(request)
+
+        else:
+
+            def _view_wrapper(request, *args, **kwargs):
+                if asyncio.iscoroutinefunction(test_func):
+                    test_pass = async_to_sync(test_func)(request.user)
+                else:
+                    test_pass = test_func(request.user)
+
+                if test_pass:
+                    return view_func(request, *args, **kwargs)
+                return _redirect_to_login(request)
+
+        # Attributes used by LoginRequiredMiddleware.
+        _view_wrapper.login_url = login_url
+        _view_wrapper.redirect_field_name = redirect_field_name
+
+        return wraps(view_func)(_view_wrapper)
 
     return decorator
 
@@ -57,6 +86,14 @@ def login_required(
     return actual_decorator
 
 
+def login_not_required(view_func):
+    """
+    Decorator for views that allows access to unauthenticated requests.
+    """
+    view_func.login_required = False
+    return view_func
+
+
 def permission_required(perm, login_url=None, raise_exception=False):
     """
     Decorator for views that checks whether a user has a particular permission
@@ -64,19 +101,36 @@ def permission_required(perm, login_url=None, raise_exception=False):
     If the raise_exception parameter is given the PermissionDenied exception
     is raised.
     """
+    if isinstance(perm, str):
+        perms = (perm,)
+    else:
+        perms = perm
+
+    def decorator(view_func):
+        if asyncio.iscoroutinefunction(view_func):
+
+            async def check_perms(user):
+                # First check if the user has the permission (even anon users).
+                if await sync_to_async(user.has_perms)(perms):
+                    return True
+                # In case the 403 handler should be called raise the exception.
+                if raise_exception:
+                    raise PermissionDenied
+                # As the last resort, show the login form.
+                return False
 
-    def check_perms(user):
-        if isinstance(perm, str):
-            perms = (perm,)
         else:
-            perms = perm
-        # First check if the user has the permission (even anon users)
-        if user.has_perms(perms):
-            return True
-        # In case the 403 handler should be called raise the exception
-        if raise_exception:
-            raise PermissionDenied
-        # As the last resort, show the login form
-        return False
 
-    return user_passes_test(check_perms, login_url=login_url)
+            def check_perms(user):
+                # First check if the user has the permission (even anon users).
+                if user.has_perms(perms):
+                    return True
+                # In case the 403 handler should be called raise the exception.
+                if raise_exception:
+                    raise PermissionDenied
+                # As the last resort, show the login form.
+                return False
+
+        return user_passes_test(check_perms, login_url=login_url)(view_func)
+
+    return decorator
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 061dc81b42..31e96ff91c 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -36,10 +36,9 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
 
     def get_context(self, name, value, attrs):
         context = super().get_context(name, value, attrs)
+        usable_password = value and not value.startswith(UNUSABLE_PASSWORD_PREFIX)
         summary = []
-        if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
-            summary.append({"label": gettext("No password set.")})
-        else:
+        if usable_password:
             try:
                 hasher = identify_hasher(value)
             except ValueError:
@@ -53,7 +52,12 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
             else:
                 for key, value_ in hasher.safe_summary(value).items():
                     summary.append({"label": gettext(key), "value": value_})
+        else:
+            summary.append({"label": gettext("No password set.")})
         context["summary"] = summary
+        context["button_label"] = (
+            _("Reset password") if usable_password else _("Set password")
+        )
         return context
 
     def id_for_label(self, id_):
@@ -89,28 +93,115 @@ class UsernameField(forms.CharField):
         }
 
 
-class BaseUserCreationForm(forms.ModelForm):
+class SetPasswordMixin:
     """
-    A form that creates a user, with no privileges, from the given username and
-    password.
+    Form mixin that validates and sets a password for a user.
+
+    This mixin also support setting an unusable password for a user.
     """
 
     error_messages = {
         "password_mismatch": _("The two password fields didn’t match."),
     }
-    password1 = forms.CharField(
-        label=_("Password"),
-        strip=False,
-        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
-        help_text=password_validation.password_validators_help_text_html(),
-    )
-    password2 = forms.CharField(
-        label=_("Password confirmation"),
-        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
-        strip=False,
-        help_text=_("Enter the same password as before, for verification."),
+    usable_password_help_text = _(
+        "Whether the user will be able to authenticate using a password or not. "
+        "If disabled, they may still be able to authenticate using other backends, "
+        "such as Single Sign-On or LDAP."
     )
 
+    @staticmethod
+    def create_password_fields(label1=_("Password"), label2=_("Password confirmation")):
+        password1 = forms.CharField(
+            label=label1,
+            required=False,
+            strip=False,
+            widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
+            help_text=password_validation.password_validators_help_text_html(),
+        )
+        password2 = forms.CharField(
+            label=label2,
+            required=False,
+            widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
+            strip=False,
+            help_text=_("Enter the same password as before, for verification."),
+        )
+        return password1, password2
+
+    @staticmethod
+    def create_usable_password_field(help_text=usable_password_help_text):
+        return forms.ChoiceField(
+            label=_("Password-based authentication"),
+            required=False,
+            initial="true",
+            choices={"true": _("Enabled"), "false": _("Disabled")},
+            widget=forms.RadioSelect(attrs={"class": "radiolist inline"}),
+            help_text=help_text,
+        )
+
+    def validate_passwords(
+        self,
+        password1_field_name="password1",
+        password2_field_name="password2",
+        usable_password_field_name="usable_password",
+    ):
+        usable_password = (
+            self.cleaned_data.pop(usable_password_field_name, None) != "false"
+        )
+        self.cleaned_data["set_usable_password"] = usable_password
+        password1 = self.cleaned_data.get(password1_field_name)
+        password2 = self.cleaned_data.get(password2_field_name)
+
+        if not usable_password:
+            return self.cleaned_data
+
+        if not password1 and password1_field_name not in self.errors:
+            error = ValidationError(
+                self.fields[password1_field_name].error_messages["required"],
+                code="required",
+            )
+            self.add_error(password1_field_name, error)
+
+        if not password2 and password2_field_name not in self.errors:
+            error = ValidationError(
+                self.fields[password2_field_name].error_messages["required"],
+                code="required",
+            )
+            self.add_error(password2_field_name, error)
+
+        if password1 and password2 and password1 != password2:
+            error = ValidationError(
+                self.error_messages["password_mismatch"],
+                code="password_mismatch",
+            )
+            self.add_error(password2_field_name, error)
+
+    def validate_password_for_user(self, user, password_field_name="password2"):
+        password = self.cleaned_data.get(password_field_name)
+        if password and self.cleaned_data["set_usable_password"]:
+            try:
+                password_validation.validate_password(password, user)
+            except ValidationError as error:
+                self.add_error(password_field_name, error)
+
+    def set_password_and_save(self, user, password_field_name="password1", commit=True):
+        if self.cleaned_data["set_usable_password"]:
+            user.set_password(self.cleaned_data[password_field_name])
+        else:
+            user.set_unusable_password()
+        if commit:
+            user.save()
+        return user
+
+
+class BaseUserCreationForm(SetPasswordMixin, forms.ModelForm):
+    """
+    A form that creates a user, with no privileges, from the given username and
+    password.
+    """
+
+    password1, password2 = SetPasswordMixin.create_password_fields()
+    usable_password = SetPasswordMixin.create_usable_password_field()
+
     class Meta:
         model = User
         fields = ("username",)
@@ -123,34 +214,21 @@ class BaseUserCreationForm(forms.ModelForm):
                 "autofocus"
             ] = True
 
-    def clean_password2(self):
-        password1 = self.cleaned_data.get("password1")
-        password2 = self.cleaned_data.get("password2")
-        if password1 and password2 and password1 != password2:
-            raise ValidationError(
-                self.error_messages["password_mismatch"],
-                code="password_mismatch",
-            )
-        return password2
+    def clean(self):
+        self.validate_passwords()
+        return super().clean()
 
     def _post_clean(self):
         super()._post_clean()
         # Validate the password after self.instance is updated with form data
         # by super().
-        password = self.cleaned_data.get("password2")
-        if password:
-            try:
-                password_validation.validate_password(password, self.instance)
-            except ValidationError as error:
-                self.add_error("password2", error)
+        self.validate_password_for_user(self.instance)
 
     def save(self, commit=True):
         user = super().save(commit=False)
-        user.set_password(self.cleaned_data["password1"])
-        if commit:
-            user.save()
-            if hasattr(self, "save_m2m"):
-                self.save_m2m()
+        user = self.set_password_and_save(user, commit=commit)
+        if commit and hasattr(self, "save_m2m"):
+            self.save_m2m()
         return user
 
 
@@ -179,9 +257,8 @@ class UserChangeForm(forms.ModelForm):
     password = ReadOnlyPasswordHashField(
         label=_("Password"),
         help_text=_(
-            "Raw passwords are not stored, so there is no way to see this "
-            "user’s password, but you can change the password using "
-            '<a href="{}">this form</a>.'
+            "Raw passwords are not stored, so there is no way to see "
+            "the user’s password."
         ),
     )
 
@@ -194,9 +271,11 @@ class UserChangeForm(forms.ModelForm):
         super().__init__(*args, **kwargs)
         password = self.fields.get("password")
         if password:
-            password.help_text = password.help_text.format(
-                f"../../{self.instance.pk}/password/"
-            )
+            if self.instance and not self.instance.has_usable_password():
+                password.help_text = _(
+                    "Enable password-based authentication for this user by setting a "
+                    "password."
+                )
         user_permissions = self.fields.get("user_permissions")
         if user_permissions:
             user_permissions.queryset = user_permissions.queryset.select_related(
@@ -383,48 +462,27 @@ class PasswordResetForm(forms.Form):
             )
 
 
-class SetPasswordForm(forms.Form):
+class SetPasswordForm(SetPasswordMixin, forms.Form):
     """
     A form that lets a user set their password without entering the old
     password
     """
 
-    error_messages = {
-        "password_mismatch": _("The two password fields didn’t match."),
-    }
-    new_password1 = forms.CharField(
-        label=_("New password"),
-        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
-        strip=False,
-        help_text=password_validation.password_validators_help_text_html(),
-    )
-    new_password2 = forms.CharField(
-        label=_("New password confirmation"),
-        strip=False,
-        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
+    new_password1, new_password2 = SetPasswordMixin.create_password_fields(
+        label1=_("New password"), label2=_("New password confirmation")
     )
 
     def __init__(self, user, *args, **kwargs):
         self.user = user
         super().__init__(*args, **kwargs)
 
-    def clean_new_password2(self):
-        password1 = self.cleaned_data.get("new_password1")
-        password2 = self.cleaned_data.get("new_password2")
-        if password1 and password2 and password1 != password2:
-            raise ValidationError(
-                self.error_messages["password_mismatch"],
-                code="password_mismatch",
-            )
-        password_validation.validate_password(password2, self.user)
-        return password2
+    def clean(self):
+        self.validate_passwords("new_password1", "new_password2")
+        self.validate_password_for_user(self.user, "new_password2")
+        return super().clean()
 
     def save(self, commit=True):
-        password = self.cleaned_data["new_password1"]
-        self.user.set_password(password)
-        if commit:
-            self.user.save()
-        return self.user
+        return self.set_password_and_save(self.user, "new_password1", commit=commit)
 
 
 class PasswordChangeForm(SetPasswordForm):
@@ -462,57 +520,41 @@ class PasswordChangeForm(SetPasswordForm):
         return old_password
 
 
-class AdminPasswordChangeForm(forms.Form):
+class AdminPasswordChangeForm(SetPasswordMixin, forms.Form):
     """
     A form used to change the password of a user in the admin interface.
     """
 
-    error_messages = {
-        "password_mismatch": _("The two password fields didn’t match."),
-    }
     required_css_class = "required"
-    password1 = forms.CharField(
-        label=_("Password"),
-        widget=forms.PasswordInput(
-            attrs={"autocomplete": "new-password", "autofocus": True}
-        ),
-        strip=False,
-        help_text=password_validation.password_validators_help_text_html(),
-    )
-    password2 = forms.CharField(
-        label=_("Password (again)"),
-        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
-        strip=False,
-        help_text=_("Enter the same password as before, for verification."),
+    usable_password_help_text = SetPasswordMixin.usable_password_help_text + (
+        '<ul id="id_unusable_warning" class="messagelist"><li class="warning">'
+        "If disabled, the current password for this user will be lost.</li></ul>"
     )
+    password1, password2 = SetPasswordMixin.create_password_fields()
 
     def __init__(self, user, *args, **kwargs):
         self.user = user
         super().__init__(*args, **kwargs)
-
-    def clean_password2(self):
-        password1 = self.cleaned_data.get("password1")
-        password2 = self.cleaned_data.get("password2")
-        if password1 and password2 and password1 != password2:
-            raise ValidationError(
-                self.error_messages["password_mismatch"],
-                code="password_mismatch",
+        self.fields["password1"].widget.attrs["autofocus"] = True
+        if self.user.has_usable_password():
+            self.fields["usable_password"] = (
+                SetPasswordMixin.create_usable_password_field(
+                    self.usable_password_help_text
+                )
             )
-        password_validation.validate_password(password2, self.user)
-        return password2
+
+    def clean(self):
+        self.validate_passwords()
+        self.validate_password_for_user(self.user)
+        return super().clean()
 
     def save(self, commit=True):
         """Save the new password."""
-        password = self.cleaned_data["password1"]
-        self.user.set_password(password)
-        if commit:
-            self.user.save()
-        return self.user
+        return self.set_password_and_save(self.user, commit=commit)
 
     @property
     def changed_data(self):
         data = super().changed_data
-        for name in self.fields:
-            if name not in data:
-                return []
-        return ["password"]
+        if "set_usable_password" in data or "password1" in data and "password2" in data:
+            return ["password"]
+        return []
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index e23ae6243e..a2ef1dae11 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -312,7 +312,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
     """
 
     algorithm = "pbkdf2_sha256"
-    iterations = 870000
+    iterations = 1_000_000
     digest = hashlib.sha256
 
     def encode(self, password, salt, iterations=None):
@@ -570,7 +570,7 @@ class ScryptPasswordHasher(BasePasswordHasher):
     algorithm = "scrypt"
     block_size = 8
     maxmem = 0
-    parallelism = 1
+    parallelism = 5
     work_factor = 2**14
 
     def encode(self, password, salt, n=None, r=None, p=None):
diff --git a/django/contrib/auth/locale/en/LC_MESSAGES/django.po b/django/contrib/auth/locale/en/LC_MESSAGES/django.po
index 8b15915f9f..42929e9653 100644
--- a/django/contrib/auth/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/auth/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Django\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-03-17 03:19-0500\n"
+"POT-Creation-Date: 2024-05-22 11:46-0300\n"
 "PO-Revision-Date: 2010-05-13 15:35+0200\n"
 "Last-Translator: Django team\n"
 "Language-Team: English <en@li.org>\n"
@@ -31,15 +31,28 @@ msgstr ""
 msgid "%(name)s object with primary key %(key)r does not exist."
 msgstr ""
 
+#: contrib/auth/admin.py:177
+msgid "Conflicting form data submitted. Please try again."
+msgstr ""
+
 #: contrib/auth/admin.py:168
 msgid "Password changed successfully."
 msgstr ""
 
+#: contrib/auth/admin.py:187
+msgid "Password-based authentication was disabled."
+msgstr ""
+
 #: contrib/auth/admin.py:189
 #, python-format
 msgid "Change password: %s"
 msgstr ""
 
+#: contrib/auth/admin.py:210
+#, python-format
+msgid "Set password: %s"
+msgstr ""
+
 #: contrib/auth/apps.py:16
 msgid "Authentication and Authorization"
 msgstr ""
@@ -60,10 +73,25 @@ msgstr ""
 msgid "Invalid password format or unknown hashing algorithm."
 msgstr ""
 
+#: contrib/auth/forms.py:59
+msgid "Reset password"
+msgstr ""
+
+#: contrib/auth/forms.py:59
+msgid "Set password"
+msgstr ""
+
 #: contrib/auth/forms.py:91 contrib/auth/forms.py:379 contrib/auth/forms.py:457
 msgid "The two password fields didn’t match."
 msgstr ""
 
+#: contrib/auth/forms.py:107
+msgid ""
+"Whether the user will be able to authenticate using a password or not. If "
+"disabled, they may still be able to authenticate using other backends, such "
+"as Single Sign-On or LDAP."
+msgstr ""
+
 #: contrib/auth/forms.py:94 contrib/auth/forms.py:166 contrib/auth/forms.py:201
 #: contrib/auth/forms.py:461
 msgid "Password"
@@ -77,10 +105,26 @@ msgstr ""
 msgid "Enter the same password as before, for verification."
 msgstr ""
 
-#: contrib/auth/forms.py:168
+#: contrib/auth/forms.py:133
+msgid "Password-based authentication"
+msgstr ""
+
+#: contrib/auth/forms.py:136
+msgid "Enabled"
+msgstr ""
+
+#: contrib/auth/forms.py:136
+msgid "Disabled"
+msgstr ""
+
+#: contrib/auth/forms.py:260
 msgid ""
-"Raw passwords are not stored, so there is no way to see this user’s "
-"password, but you can change the password using <a href=\"{}\">this form</a>."
+"Raw passwords are not stored, so there is no way to see the user’s password."
+msgstr ""
+
+#: contrib/auth/forms.py:276
+msgid ""
+"Enable password-based authentication for this user by setting a password."
 msgstr ""
 
 #: contrib/auth/forms.py:208
@@ -114,10 +158,6 @@ msgstr ""
 msgid "Old password"
 msgstr ""
 
-#: contrib/auth/forms.py:469
-msgid "Password (again)"
-msgstr ""
-
 #: contrib/auth/hashers.py:327 contrib/auth/hashers.py:420
 #: contrib/auth/hashers.py:510 contrib/auth/hashers.py:605
 #: contrib/auth/hashers.py:665 contrib/auth/hashers.py:707
diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
index b29a980cb2..c40f2aa69d 100644
--- a/django/contrib/auth/management/__init__.py
+++ b/django/contrib/auth/management/__init__.py
@@ -46,6 +46,13 @@ def create_permissions(
     if not app_config.models_module:
         return
 
+    try:
+        Permission = apps.get_model("auth", "Permission")
+    except LookupError:
+        return
+    if not router.allow_migrate_model(using, Permission):
+        return
+
     # Ensure that contenttypes are created for this app. Needed if
     # 'django.contrib.auth' is in INSTALLED_APPS before
     # 'django.contrib.contenttypes'.
@@ -62,28 +69,15 @@ def create_permissions(
     try:
         app_config = apps.get_app_config(app_label)
         ContentType = apps.get_model("contenttypes", "ContentType")
-        Permission = apps.get_model("auth", "Permission")
     except LookupError:
         return
 
-    if not router.allow_migrate_model(using, Permission):
-        return
+    models = list(app_config.get_models())
 
-    # This will hold the permissions we're looking for as
-    # (content_type, (codename, name))
-    searched_perms = []
-    # The codenames and ctypes that should exist.
-    ctypes = set()
-    for klass in app_config.get_models():
-        # Force looking up the content types in the current database
-        # before creating foreign keys to them.
-        ctype = ContentType.objects.db_manager(using).get_for_model(
-            klass, for_concrete_model=False
-        )
-
-        ctypes.add(ctype)
-        for perm in _get_all_permissions(klass._meta):
-            searched_perms.append((ctype, perm))
+    # Grab all the ContentTypes.
+    ctypes = ContentType.objects.db_manager(using).get_for_models(
+        *models, for_concrete_models=False
+    )
 
     # Find all the Permissions that have a content_type for a model we're
     # looking for.  We don't need to check for codenames since we already have
@@ -91,20 +85,22 @@ def create_permissions(
     all_perms = set(
         Permission.objects.using(using)
         .filter(
-            content_type__in=ctypes,
+            content_type__in=set(ctypes.values()),
         )
         .values_list("content_type", "codename")
     )
 
     perms = []
-    for ct, (codename, name) in searched_perms:
-        if (ct.pk, codename) not in all_perms:
-            permission = Permission()
-            permission._state.db = using
-            permission.codename = codename
-            permission.name = name
-            permission.content_type = ct
-            perms.append(permission)
+    for model in models:
+        ctype = ctypes[model]
+        for codename, name in _get_all_permissions(model._meta):
+            if (ctype.pk, codename) not in all_perms:
+                permission = Permission()
+                permission._state.db = using
+                permission.codename = codename
+                permission.name = name
+                permission.content_type = ctype
+                perms.append(permission)
 
     Permission.objects.using(using).bulk_create(perms)
     if verbosity >= 2:
diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py
index 669d8cf5d0..db980c82ca 100644
--- a/django/contrib/auth/management/commands/changepassword.py
+++ b/django/contrib/auth/management/commands/changepassword.py
@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.password_validation import validate_password
 from django.core.exceptions import ValidationError
 from django.core.management.base import BaseCommand, CommandError
-from django.db import DEFAULT_DB_ALIAS
+from django.db import DEFAULT_DB_ALIAS, connections
 
 UserModel = get_user_model()
 
@@ -32,6 +32,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help='Specifies the database to use. Default is "default".',
         )
 
diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py
index 75ef68ff68..d5d5d193c8 100644
--- a/django/contrib/auth/management/commands/createsuperuser.py
+++ b/django/contrib/auth/management/commands/createsuperuser.py
@@ -11,7 +11,7 @@ from django.contrib.auth.management import get_default_username
 from django.contrib.auth.password_validation import validate_password
 from django.core import exceptions
 from django.core.management.base import BaseCommand, CommandError
-from django.db import DEFAULT_DB_ALIAS
+from django.db import DEFAULT_DB_ALIAS, connections
 from django.utils.functional import cached_property
 from django.utils.text import capfirst
 
@@ -56,6 +56,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help='Specifies the database to use. Default is "default".',
         )
         for field_name in self.UserModel.REQUIRED_FIELDS:
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index 6b8dd4340e..cb409ee778 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -1,9 +1,13 @@
 from functools import partial
+from urllib.parse import urlsplit
 
+from django.conf import settings
 from django.contrib import auth
-from django.contrib.auth import load_backend
+from django.contrib.auth import REDIRECT_FIELD_NAME, load_backend
 from django.contrib.auth.backends import RemoteUserBackend
+from django.contrib.auth.views import redirect_to_login
 from django.core.exceptions import ImproperlyConfigured
+from django.shortcuts import resolve_url
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.functional import SimpleLazyObject
 
@@ -34,6 +38,56 @@ class AuthenticationMiddleware(MiddlewareMixin):
         request.auser = partial(auser, request)
 
 
+class LoginRequiredMiddleware(MiddlewareMixin):
+    """
+    Middleware that redirects all unauthenticated requests to a login page.
+
+    Views using the login_not_required decorator will not be redirected.
+    """
+
+    redirect_field_name = REDIRECT_FIELD_NAME
+
+    def process_view(self, request, view_func, view_args, view_kwargs):
+        if request.user.is_authenticated:
+            return None
+
+        if not getattr(view_func, "login_required", True):
+            return None
+
+        return self.handle_no_permission(request, view_func)
+
+    def get_login_url(self, view_func):
+        login_url = getattr(view_func, "login_url", None) or settings.LOGIN_URL
+        if not login_url:
+            raise ImproperlyConfigured(
+                "No login URL to redirect to. Define settings.LOGIN_URL or "
+                "provide a login_url via the 'django.contrib.auth.decorators."
+                "login_required' decorator."
+            )
+        return str(login_url)
+
+    def get_redirect_field_name(self, view_func):
+        return getattr(view_func, "redirect_field_name", self.redirect_field_name)
+
+    def handle_no_permission(self, request, view_func):
+        path = request.build_absolute_uri()
+        resolved_login_url = resolve_url(self.get_login_url(view_func))
+        # If the login url is the same scheme and net location then use the
+        # path as the "next" url.
+        login_scheme, login_netloc = urlsplit(resolved_login_url)[:2]
+        current_scheme, current_netloc = urlsplit(path)[:2]
+        if (not login_scheme or login_scheme == current_scheme) and (
+            not login_netloc or login_netloc == current_netloc
+        ):
+            path = request.get_full_path()
+
+        return redirect_to_login(
+            path,
+            resolved_login_url,
+            self.get_redirect_field_name(view_func),
+        )
+
+
 class RemoteUserMiddleware(MiddlewareMixin):
     """
     Middleware for utilizing web-server-provided authentication.
diff --git a/django/contrib/auth/mixins.py b/django/contrib/auth/mixins.py
index 0e46000d97..1f2e95ff00 100644
--- a/django/contrib/auth/mixins.py
+++ b/django/contrib/auth/mixins.py
@@ -1,4 +1,4 @@
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 
 from django.conf import settings
 from django.contrib.auth import REDIRECT_FIELD_NAME
@@ -51,8 +51,8 @@ class AccessMixin:
         resolved_login_url = resolve_url(self.get_login_url())
         # If the login url is the same scheme and net location then use the
         # path as the "next" url.
-        login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
-        current_scheme, current_netloc = urlparse(path)[:2]
+        login_scheme, login_netloc = urlsplit(resolved_login_url)[:2]
+        current_scheme, current_netloc = urlsplit(path)[:2]
         if (not login_scheme or login_scheme == current_scheme) and (
             not login_netloc or login_netloc == current_netloc
         ):
diff --git a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
index c73042b18f..e95fa3e9da 100644
--- a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
+++ b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
@@ -1,5 +1,8 @@
 <div{% include 'django/forms/widgets/attrs.html' %}>
-{% for entry in summary %}
-<strong>{{ entry.label }}</strong>{% if entry.value %}: <bdi>{{ entry.value }}</bdi>{% endif %}
-{% endfor %}
+  <p>
+    {% for entry in summary %}
+    <strong>{{ entry.label }}</strong>{% if entry.value %}: <bdi>{{ entry.value }}</bdi>{% endif %}
+    {% endfor %}
+  </p>
+  <p><a class="button" href="{{ password_url|default:"../password/" }}">{{ button_label }}</a></p>
 </div>
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index 0d16104655..a18cfdb347 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -1,4 +1,4 @@
-from urllib.parse import urlparse, urlunparse
+from urllib.parse import urlsplit, urlunsplit
 
 from django.conf import settings
 
@@ -7,7 +7,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
 from django.contrib.auth import login as auth_login
 from django.contrib.auth import logout as auth_logout
 from django.contrib.auth import update_session_auth_hash
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import login_not_required, login_required
 from django.contrib.auth.forms import (
     AuthenticationForm,
     PasswordChangeForm,
@@ -62,6 +62,7 @@ class RedirectURLMixin:
         raise ImproperlyConfigured("No URL to redirect to. Provide a next_page.")
 
 
+@method_decorator(login_not_required, name="dispatch")
 class LoginView(RedirectURLMixin, FormView):
     """
     Display the login form and handle the login action.
@@ -182,13 +183,13 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
     """
     resolved_url = resolve_url(login_url or settings.LOGIN_URL)
 
-    login_url_parts = list(urlparse(resolved_url))
+    login_url_parts = list(urlsplit(resolved_url))
     if redirect_field_name:
-        querystring = QueryDict(login_url_parts[4], mutable=True)
+        querystring = QueryDict(login_url_parts[3], mutable=True)
         querystring[redirect_field_name] = next
-        login_url_parts[4] = querystring.urlencode(safe="/")
+        login_url_parts[3] = querystring.urlencode(safe="/")
 
-    return HttpResponseRedirect(urlunparse(login_url_parts))
+    return HttpResponseRedirect(urlunsplit(login_url_parts))
 
 
 # Class-based password reset views
@@ -210,6 +211,7 @@ class PasswordContextMixin:
         return context
 
 
+@method_decorator(login_not_required, name="dispatch")
 class PasswordResetView(PasswordContextMixin, FormView):
     email_template_name = "registration/password_reset_email.html"
     extra_email_context = None
@@ -244,11 +246,13 @@ class PasswordResetView(PasswordContextMixin, FormView):
 INTERNAL_RESET_SESSION_TOKEN = "_password_reset_token"
 
 
+@method_decorator(login_not_required, name="dispatch")
 class PasswordResetDoneView(PasswordContextMixin, TemplateView):
     template_name = "registration/password_reset_done.html"
     title = _("Password reset sent")
 
 
+@method_decorator(login_not_required, name="dispatch")
 class PasswordResetConfirmView(PasswordContextMixin, FormView):
     form_class = SetPasswordForm
     post_reset_login = False
@@ -335,6 +339,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
         return context
 
 
+@method_decorator(login_not_required, name="dispatch")
 class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
     template_name = "registration/password_reset_complete.html"
     title = _("Password reset complete")
diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py
index 1b6abb9818..a3e87f6ed4 100644
--- a/django/contrib/contenttypes/fields.py
+++ b/django/contrib/contenttypes/fields.py
@@ -11,6 +11,7 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
 from django.db import DEFAULT_DB_ALIAS, models, router, transaction
 from django.db.models import DO_NOTHING, ForeignObject, ForeignObjectRel
 from django.db.models.base import ModelBase, make_foreign_order_accessors
+from django.db.models.fields import Field
 from django.db.models.fields.mixins import FieldCacheMixin
 from django.db.models.fields.related import (
     ReverseManyToOneDescriptor,
@@ -24,7 +25,7 @@ from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.functional import cached_property
 
 
-class GenericForeignKey(FieldCacheMixin):
+class GenericForeignKey(FieldCacheMixin, Field):
     """
     Provide a generic many-to-one relation through the ``content_type`` and
     ``object_id`` fields.
@@ -33,35 +34,28 @@ class GenericForeignKey(FieldCacheMixin):
     ForwardManyToOneDescriptor) by adding itself as a model attribute.
     """
 
-    # Field flags
-    auto_created = False
-    concrete = False
-    editable = False
-    hidden = False
-
-    is_relation = True
     many_to_many = False
     many_to_one = True
     one_to_many = False
     one_to_one = False
-    related_model = None
-    remote_field = None
 
     def __init__(
         self, ct_field="content_type", fk_field="object_id", for_concrete_model=True
     ):
+        super().__init__(editable=False)
         self.ct_field = ct_field
         self.fk_field = fk_field
         self.for_concrete_model = for_concrete_model
-        self.editable = False
-        self.rel = None
-        self.column = None
+        self.is_relation = True
 
     def contribute_to_class(self, cls, name, **kwargs):
-        self.name = name
-        self.model = cls
-        cls._meta.add_field(self, private=True)
-        setattr(cls, name, self)
+        super().contribute_to_class(cls, name, private_only=True, **kwargs)
+        # GenericForeignKey is its own descriptor.
+        setattr(cls, self.attname, self)
+
+    def get_attname_column(self):
+        attname, column = super().get_attname_column()
+        return attname, None
 
     def get_filter_kwargs_for_object(self, obj):
         """See corresponding method on Field"""
@@ -77,10 +71,6 @@ class GenericForeignKey(FieldCacheMixin):
             self.ct_field: ContentType.objects.get_for_model(obj).pk,
         }
 
-    def __str__(self):
-        model = self.model
-        return "%s.%s" % (model._meta.label, self.name)
-
     def check(self, **kwargs):
         return [
             *self._check_field_name(),
@@ -88,18 +78,6 @@ class GenericForeignKey(FieldCacheMixin):
             *self._check_content_type_field(),
         ]
 
-    def _check_field_name(self):
-        if self.name.endswith("_"):
-            return [
-                checks.Error(
-                    "Field names must not end with an underscore.",
-                    obj=self,
-                    id="fields.E001",
-                )
-            ]
-        else:
-            return []
-
     def _check_object_id_field(self):
         try:
             self.model._meta.get_field(self.fk_field)
@@ -162,7 +140,8 @@ class GenericForeignKey(FieldCacheMixin):
             else:
                 return []
 
-    def get_cache_name(self):
+    @cached_property
+    def cache_name(self):
         return self.name
 
     def get_content_type(self, obj=None, id=None, using=None, model=None):
@@ -209,7 +188,7 @@ class GenericForeignKey(FieldCacheMixin):
         fk_dict = defaultdict(set)
         # We need one instance for each group in order to get the right db:
         instance_dict = {}
-        ct_attname = self.model._meta.get_field(self.ct_field).get_attname()
+        ct_attname = self.model._meta.get_field(self.ct_field).attname
         for instance in instances:
             # We avoid looking for values if either ct_id or fkey value is None
             ct_id = getattr(instance, ct_attname)
@@ -262,7 +241,7 @@ class GenericForeignKey(FieldCacheMixin):
         # content type ID here, and later when the actual instance is needed,
         # use ContentType.objects.get_for_id(), which has a global cache.
         f = self.model._meta.get_field(self.ct_field)
-        ct_id = getattr(instance, f.get_attname(), None)
+        ct_id = getattr(instance, f.attname, None)
         pk_val = getattr(instance, self.fk_field)
 
         rel_obj = self.get_cached_value(instance, default=None)
@@ -280,7 +259,9 @@ class GenericForeignKey(FieldCacheMixin):
         if ct_id is not None:
             ct = self.get_content_type(id=ct_id, using=instance._state.db)
             try:
-                rel_obj = ct.get_object_for_this_type(pk=pk_val)
+                rel_obj = ct.get_object_for_this_type(
+                    using=instance._state.db, pk=pk_val
+                )
             except ObjectDoesNotExist:
                 pass
         self.set_cached_value(instance, rel_obj)
diff --git a/django/contrib/contenttypes/forms.py b/django/contrib/contenttypes/forms.py
index c0ff4f7257..741824e2fc 100644
--- a/django/contrib/contenttypes/forms.py
+++ b/django/contrib/contenttypes/forms.py
@@ -67,10 +67,10 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
     def save_new(self, form, commit=True):
         setattr(
             form.instance,
-            self.ct_field.get_attname(),
+            self.ct_field.attname,
             ContentType.objects.get_for_model(self.instance).pk,
         )
-        setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
+        setattr(form.instance, self.ct_fk_field.attname, self.instance.pk)
         return form.save(commit=commit)
 
 
diff --git a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
index 950e615f0c..27aaf1d51b 100644
--- a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
+++ b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
@@ -3,7 +3,7 @@ import itertools
 from django.apps import apps
 from django.contrib.contenttypes.models import ContentType
 from django.core.management import BaseCommand
-from django.db import DEFAULT_DB_ALIAS, router
+from django.db import DEFAULT_DB_ALIAS, connections, router
 from django.db.models.deletion import Collector
 
 
@@ -21,6 +21,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help='Nominates the database to use. Defaults to the "default" database.',
         )
         parser.add_argument(
diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py
index 0d98ed3a4d..4f16e6eb69 100644
--- a/django/contrib/contenttypes/models.py
+++ b/django/contrib/contenttypes/models.py
@@ -174,20 +174,20 @@ class ContentType(models.Model):
         except LookupError:
             return None
 
-    def get_object_for_this_type(self, **kwargs):
+    def get_object_for_this_type(self, using=None, **kwargs):
         """
         Return an object of this type for the keyword arguments given.
         Basically, this is a proxy around this object_type's get_object() model
         method. The ObjectNotExist exception, if thrown, will not be caught,
         so code that calls this method should catch it.
         """
-        return self.model_class()._base_manager.using(self._state.db).get(**kwargs)
+        return self.model_class()._base_manager.using(using).get(**kwargs)
 
     def get_all_objects_for_this_type(self, **kwargs):
         """
         Return all objects of this type for the keyword arguments given.
         """
-        return self.model_class()._base_manager.using(self._state.db).filter(**kwargs)
+        return self.model_class()._base_manager.filter(**kwargs)
 
     def natural_key(self):
         return (self.app_label, self.model)
diff --git a/django/contrib/contenttypes/prefetch.py b/django/contrib/contenttypes/prefetch.py
index b02ed3bae5..97ac60295b 100644
--- a/django/contrib/contenttypes/prefetch.py
+++ b/django/contrib/contenttypes/prefetch.py
@@ -3,7 +3,7 @@ from django.db.models.query import ModelIterable, RawQuerySet
 
 
 class GenericPrefetch(Prefetch):
-    def __init__(self, lookup, querysets=None, to_attr=None):
+    def __init__(self, lookup, querysets, to_attr=None):
         for queryset in querysets:
             if queryset is not None and (
                 isinstance(queryset, RawQuerySet)
diff --git a/django/contrib/gis/db/backends/mysql/schema.py b/django/contrib/gis/db/backends/mysql/schema.py
index 27f6df174a..f3ddf14fa7 100644
--- a/django/contrib/gis/db/backends/mysql/schema.py
+++ b/django/contrib/gis/db/backends/mysql/schema.py
@@ -54,7 +54,7 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor):
         self.create_spatial_indexes()
 
     def remove_field(self, model, field):
-        if isinstance(field, GeometryField) and field.spatial_index:
+        if isinstance(field, GeometryField) and field.spatial_index and not field.null:
             index_name = self._create_spatial_index_name(model, field)
             sql = self._delete_index_sql(model, index_name)
             try:
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index 17d7b3213d..7a347c5287 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -203,7 +203,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
                 raise ImproperlyConfigured(
                     'Cannot determine PostGIS version for database "%s" '
                     'using command "SELECT postgis_lib_version()". '
-                    "GeoDjango requires at least PostGIS version 3.0. "
+                    "GeoDjango requires at least PostGIS version 3.1. "
                     "Was the database created from a spatial database "
                     "template?" % self.connection.settings_dict["NAME"]
                 )
diff --git a/django/contrib/gis/db/models/aggregates.py b/django/contrib/gis/db/models/aggregates.py
index 7ba2e756e7..abff1a5206 100644
--- a/django/contrib/gis/db/models/aggregates.py
+++ b/django/contrib/gis/db/models/aggregates.py
@@ -34,17 +34,14 @@ class GeoAggregate(Aggregate):
             tolerance = self.extra.get("tolerance") or getattr(self, "tolerance", 0.05)
             clone = self.copy()
             source_expressions = self.get_source_expressions()
-            if self.filter:
-                source_expressions.pop()
+            source_expressions.pop()  # Don't wrap filters with SDOAGGRTYPE().
             spatial_type_expr = Func(
                 *source_expressions,
                 Value(tolerance),
                 function="SDOAGGRTYPE",
                 output_field=self.output_field,
             )
-            source_expressions = [spatial_type_expr]
-            if self.filter:
-                source_expressions.append(self.filter)
+            source_expressions = [spatial_type_expr, self.filter]
             clone.set_source_expressions(source_expressions)
             return clone.as_sql(compiler, connection, **extra_context)
         return self.as_sql(compiler, connection, **extra_context)
diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py
index acbaa5ffc0..10272b7d68 100644
--- a/django/contrib/gis/db/models/functions.py
+++ b/django/contrib/gis/db/models/functions.py
@@ -367,15 +367,28 @@ class ForcePolygonCW(GeomOutputGeoFunc):
 
 
 class FromWKB(GeoFunc):
-    output_field = GeometryField(srid=0)
-    arity = 1
+    arity = 2
     geom_param_pos = ()
 
+    def __init__(self, expression, srid=0, **extra):
+        expressions = [
+            expression,
+            self._handle_param(srid, "srid", int),
+        ]
+        if "output_field" not in extra:
+            extra["output_field"] = GeometryField(srid=srid)
+        super().__init__(*expressions, **extra)
 
-class FromWKT(GeoFunc):
-    output_field = GeometryField(srid=0)
-    arity = 1
-    geom_param_pos = ()
+    def as_oracle(self, compiler, connection, **extra_context):
+        # Oracle doesn't support the srid parameter.
+        source_expressions = self.get_source_expressions()
+        clone = self.copy()
+        clone.set_source_expressions(source_expressions[:1])
+        return super(FromWKB, clone).as_sql(compiler, connection, **extra_context)
+
+
+class FromWKT(FromWKB):
+    pass
 
 
 class GeoHash(GeoFunc):
diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py
index 6ee98c412d..44e1026e3f 100644
--- a/django/contrib/gis/gdal/geometries.py
+++ b/django/contrib/gis/gdal/geometries.py
@@ -801,14 +801,22 @@ GEO_CLASSES = {
     2001: Point,  # POINT M
     2002: LineString,  # LINESTRING M
     2003: Polygon,  # POLYGON M
+    2004: MultiPoint,  # MULTIPOINT M
+    2005: MultiLineString,  # MULTILINESTRING M
+    2006: MultiPolygon,  # MULTIPOLYGON M
+    2007: GeometryCollection,  # GEOMETRYCOLLECTION M
     3001: Point,  # POINT ZM
     3002: LineString,  # LINESTRING ZM
     3003: Polygon,  # POLYGON ZM
+    3004: MultiPoint,  # MULTIPOINT ZM
+    3005: MultiLineString,  # MULTILINESTRING ZM
+    3006: MultiPolygon,  # MULTIPOLYGON ZM
+    3007: GeometryCollection,  # GEOMETRYCOLLECTION ZM
     1 + OGRGeomType.wkb25bit: Point,  # POINT Z
     2 + OGRGeomType.wkb25bit: LineString,  # LINESTRING Z
     3 + OGRGeomType.wkb25bit: Polygon,  # POLYGON Z
-    4 + OGRGeomType.wkb25bit: MultiPoint,
-    5 + OGRGeomType.wkb25bit: MultiLineString,
-    6 + OGRGeomType.wkb25bit: MultiPolygon,
-    7 + OGRGeomType.wkb25bit: GeometryCollection,
+    4 + OGRGeomType.wkb25bit: MultiPoint,  # MULTIPOINT Z
+    5 + OGRGeomType.wkb25bit: MultiLineString,  # MULTILINESTRING Z
+    6 + OGRGeomType.wkb25bit: MultiPolygon,  # MULTIPOLYGON Z
+    7 + OGRGeomType.wkb25bit: GeometryCollection,  # GEOMETRYCOLLECTION Z
 }
diff --git a/django/contrib/gis/static/gis/js/OLMapWidget.js b/django/contrib/gis/static/gis/js/OLMapWidget.js
index f3025f24ac..a545036c9f 100644
--- a/django/contrib/gis/static/gis/js/OLMapWidget.js
+++ b/django/contrib/gis/static/gis/js/OLMapWidget.js
@@ -54,7 +54,7 @@ class MapWidget {
 
         // Altering using user-provided options
         for (const property in options) {
-            if (options.hasOwnProperty(property)) {
+            if (Object.hasOwn(options, property)) {
                 this.options[property] = options[property];
             }
         }
diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py
index 23224779c5..174e367a69 100644
--- a/django/contrib/humanize/templatetags/humanize.py
+++ b/django/contrib/humanize/templatetags/humanize.py
@@ -24,12 +24,14 @@ register = template.Library()
 def ordinal(value):
     """
     Convert an integer to its ordinal as a string. 1 is '1st', 2 is '2nd',
-    3 is '3rd', etc. Works for any integer.
+    3 is '3rd', etc. Works for any non-negative integer.
     """
     try:
         value = int(value)
     except (TypeError, ValueError):
         return value
+    if value < 0:
+        return str(value)
     if value % 100 in (11, 12, 13):
         # Translators: Ordinal format for 11 (11th), 12 (12th), and 13 (13th).
         value = pgettext("ordinal 11, 12, 13", "{}th").format(value)
@@ -75,12 +77,15 @@ def intcomma(value, use_l10n=True):
             return intcomma(value, False)
         else:
             return number_format(value, use_l10n=True, force_grouping=True)
-    orig = str(value)
-    new = re.sub(r"^(-?\d+)(\d{3})", r"\g<1>,\g<2>", orig)
-    if orig == new:
-        return new
-    else:
-        return intcomma(new, use_l10n)
+    result = str(value)
+    match = re.match(r"-?\d+", result)
+    if match:
+        prefix = match[0]
+        prefix_with_commas = re.sub(r"\d{3}", r"\g<0>,", prefix[::-1])[::-1]
+        # Remove a leading comma, if needed.
+        prefix_with_commas = re.sub(r"^(-?),", r"\1", prefix_with_commas)
+        result = prefix_with_commas + result[len(prefix) :]
+    return result
 
 
 # A tuple of standard large number to their converters
diff --git a/django/contrib/postgres/aggregates/mixins.py b/django/contrib/postgres/aggregates/mixins.py
index 68f24a5ce3..d6ff535158 100644
--- a/django/contrib/postgres/aggregates/mixins.py
+++ b/django/contrib/postgres/aggregates/mixins.py
@@ -1,3 +1,4 @@
+from django.core.exceptions import FullResultSet
 from django.db.models.expressions import OrderByList
 
 
@@ -17,19 +18,30 @@ class OrderableAggMixin:
         return super().resolve_expression(*args, **kwargs)
 
     def get_source_expressions(self):
-        if self.order_by is not None:
-            return super().get_source_expressions() + [self.order_by]
-        return super().get_source_expressions()
+        return super().get_source_expressions() + [self.order_by]
 
     def set_source_expressions(self, exprs):
-        if isinstance(exprs[-1], OrderByList):
-            *exprs, self.order_by = exprs
+        *exprs, self.order_by = exprs
         return super().set_source_expressions(exprs)
 
     def as_sql(self, compiler, connection):
-        if self.order_by is not None:
-            order_by_sql, order_by_params = compiler.compile(self.order_by)
-        else:
-            order_by_sql, order_by_params = "", ()
-        sql, sql_params = super().as_sql(compiler, connection, ordering=order_by_sql)
-        return sql, (*sql_params, *order_by_params)
+        *source_exprs, filtering_expr, ordering_expr = self.get_source_expressions()
+
+        order_by_sql = ""
+        order_by_params = []
+        if ordering_expr is not None:
+            order_by_sql, order_by_params = compiler.compile(ordering_expr)
+
+        filter_params = []
+        if filtering_expr is not None:
+            try:
+                _, filter_params = compiler.compile(filtering_expr)
+            except FullResultSet:
+                pass
+
+        source_params = []
+        for source_expr in source_exprs:
+            source_params += compiler.compile(source_expr)[1]
+
+        sql, _ = super().as_sql(compiler, connection, ordering=order_by_sql)
+        return sql, (*source_params, *order_by_params, *filter_params)
diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py
index c61072b5a5..a6351dc008 100644
--- a/django/contrib/postgres/constraints.py
+++ b/django/contrib/postgres/constraints.py
@@ -1,8 +1,7 @@
 from types import NoneType
 
-from django.contrib.postgres.indexes import OpClass
 from django.core.exceptions import ValidationError
-from django.db import DEFAULT_DB_ALIAS, NotSupportedError
+from django.db import DEFAULT_DB_ALIAS
 from django.db.backends.ddl_references import Expressions, Statement, Table
 from django.db.models import BaseConstraint, Deferrable, F, Q
 from django.db.models.expressions import Exists, ExpressionList
@@ -77,6 +76,14 @@ class ExclusionConstraint(BaseConstraint):
             expressions.append(expression)
         return ExpressionList(*expressions).resolve_expression(query)
 
+    def _check(self, model, connection):
+        references = set()
+        for expr, _ in self.expressions:
+            if isinstance(expr, str):
+                expr = F(expr)
+            references.update(model._get_expr_references(expr))
+        return self._check_references(model, references)
+
     def _get_condition_sql(self, compiler, schema_editor, query):
         if self.condition is None:
             return None
@@ -107,7 +114,6 @@ class ExclusionConstraint(BaseConstraint):
         )
 
     def create_sql(self, model, schema_editor):
-        self.check_supported(schema_editor)
         return Statement(
             "ALTER TABLE %(table)s ADD %(constraint)s",
             table=Table(model._meta.db_table, schema_editor.quote_name),
@@ -121,17 +127,6 @@ class ExclusionConstraint(BaseConstraint):
             schema_editor.quote_name(self.name),
         )
 
-    def check_supported(self, schema_editor):
-        if (
-            self.include
-            and self.index_type.lower() == "spgist"
-            and not schema_editor.connection.features.supports_covering_spgist_indexes
-        ):
-            raise NotSupportedError(
-                "Covering exclusion constraints using an SP-GiST index "
-                "require PostgreSQL 14+."
-            )
-
     def deconstruct(self):
         path, args, kwargs = super().deconstruct()
         kwargs["expressions"] = self.expressions
@@ -200,12 +195,10 @@ class ExclusionConstraint(BaseConstraint):
                         if isinstance(expr, F) and expr.name in exclude:
                             return
             rhs_expression = expression.replace_expressions(replacements)
-            # Remove OpClass because it only has sense during the constraint
-            # creation.
-            if isinstance(expression, OpClass):
-                expression = expression.get_source_expressions()[0]
-            if isinstance(rhs_expression, OpClass):
-                rhs_expression = rhs_expression.get_source_expressions()[0]
+            if hasattr(expression, "get_expression_for_validation"):
+                expression = expression.get_expression_for_validation()
+            if hasattr(rhs_expression, "get_expression_for_validation"):
+                rhs_expression = rhs_expression.get_expression_for_validation()
             lookup = PostgresOperatorLookup(lhs=expression, rhs=rhs_expression)
             lookup.postgres_operator = operator
             lookups.append(lookup)
diff --git a/django/contrib/postgres/forms/hstore.py b/django/contrib/postgres/forms/hstore.py
index 6a20f7b729..f824f78c3e 100644
--- a/django/contrib/postgres/forms/hstore.py
+++ b/django/contrib/postgres/forms/hstore.py
@@ -20,7 +20,7 @@ class HStoreField(forms.CharField):
 
     def prepare_value(self, value):
         if isinstance(value, dict):
-            return json.dumps(value)
+            return json.dumps(value, ensure_ascii=False)
         return value
 
     def to_python(self, value):
diff --git a/django/contrib/postgres/indexes.py b/django/contrib/postgres/indexes.py
index cc944ed335..ce9e2cee14 100644
--- a/django/contrib/postgres/indexes.py
+++ b/django/contrib/postgres/indexes.py
@@ -1,4 +1,3 @@
-from django.db import NotSupportedError
 from django.db.models import Func, Index
 from django.utils.functional import cached_property
 
@@ -234,16 +233,10 @@ class SpGistIndex(PostgresIndex):
             with_params.append("fillfactor = %d" % self.fillfactor)
         return with_params
 
-    def check_supported(self, schema_editor):
-        if (
-            self.include
-            and not schema_editor.connection.features.supports_covering_spgist_indexes
-        ):
-            raise NotSupportedError("Covering SP-GiST indexes require PostgreSQL 14+.")
-
 
 class OpClass(Func):
     template = "%(expressions)s %(name)s"
+    constraint_validation_compatible = False
 
     def __init__(self, expression, name):
         super().__init__(expression, name=name)
diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py
index 050e7387be..69f756a228 100644
--- a/django/contrib/sessions/backends/base.py
+++ b/django/contrib/sessions/backends/base.py
@@ -2,6 +2,8 @@ import logging
 import string
 from datetime import datetime, timedelta
 
+from asgiref.sync import sync_to_async
+
 from django.conf import settings
 from django.core import signing
 from django.utils import timezone
@@ -56,6 +58,10 @@ class SessionBase:
         self._session[key] = value
         self.modified = True
 
+    async def aset(self, key, value):
+        (await self._aget_session())[key] = value
+        self.modified = True
+
     def __delitem__(self, key):
         del self._session[key]
         self.modified = True
@@ -67,28 +73,52 @@ class SessionBase:
     def get(self, key, default=None):
         return self._session.get(key, default)
 
+    async def aget(self, key, default=None):
+        return (await self._aget_session()).get(key, default)
+
     def pop(self, key, default=__not_given):
         self.modified = self.modified or key in self._session
         args = () if default is self.__not_given else (default,)
         return self._session.pop(key, *args)
 
+    async def apop(self, key, default=__not_given):
+        self.modified = self.modified or key in (await self._aget_session())
+        args = () if default is self.__not_given else (default,)
+        return (await self._aget_session()).pop(key, *args)
+
     def setdefault(self, key, value):
         if key in self._session:
             return self._session[key]
         else:
-            self.modified = True
-            self._session[key] = value
+            self[key] = value
+            return value
+
+    async def asetdefault(self, key, value):
+        session = await self._aget_session()
+        if key in session:
+            return session[key]
+        else:
+            await self.aset(key, value)
             return value
 
     def set_test_cookie(self):
         self[self.TEST_COOKIE_NAME] = self.TEST_COOKIE_VALUE
 
+    async def aset_test_cookie(self):
+        await self.aset(self.TEST_COOKIE_NAME, self.TEST_COOKIE_VALUE)
+
     def test_cookie_worked(self):
         return self.get(self.TEST_COOKIE_NAME) == self.TEST_COOKIE_VALUE
 
+    async def atest_cookie_worked(self):
+        return (await self.aget(self.TEST_COOKIE_NAME)) == self.TEST_COOKIE_VALUE
+
     def delete_test_cookie(self):
         del self[self.TEST_COOKIE_NAME]
 
+    async def adelete_test_cookie(self):
+        del (await self._aget_session())[self.TEST_COOKIE_NAME]
+
     def encode(self, session_dict):
         "Return the given session dictionary serialized and encoded as a string."
         return signing.dumps(
@@ -116,18 +146,34 @@ class SessionBase:
         self._session.update(dict_)
         self.modified = True
 
+    async def aupdate(self, dict_):
+        (await self._aget_session()).update(dict_)
+        self.modified = True
+
     def has_key(self, key):
         return key in self._session
 
+    async def ahas_key(self, key):
+        return key in (await self._aget_session())
+
     def keys(self):
         return self._session.keys()
 
+    async def akeys(self):
+        return (await self._aget_session()).keys()
+
     def values(self):
         return self._session.values()
 
+    async def avalues(self):
+        return (await self._aget_session()).values()
+
     def items(self):
         return self._session.items()
 
+    async def aitems(self):
+        return (await self._aget_session()).items()
+
     def clear(self):
         # To avoid unnecessary persistent storage accesses, we set up the
         # internals directly (loading data wastes time, since we are going to
@@ -150,11 +196,22 @@ class SessionBase:
             if not self.exists(session_key):
                 return session_key
 
+    async def _aget_new_session_key(self):
+        while True:
+            session_key = get_random_string(32, VALID_KEY_CHARS)
+            if not await self.aexists(session_key):
+                return session_key
+
     def _get_or_create_session_key(self):
         if self._session_key is None:
             self._session_key = self._get_new_session_key()
         return self._session_key
 
+    async def _aget_or_create_session_key(self):
+        if self._session_key is None:
+            self._session_key = await self._aget_new_session_key()
+        return self._session_key
+
     def _validate_session_key(self, key):
         """
         Key must be truthy and at least 8 characters long. 8 characters is an
@@ -192,6 +249,17 @@ class SessionBase:
                 self._session_cache = self.load()
         return self._session_cache
 
+    async def _aget_session(self, no_load=False):
+        self.accessed = True
+        try:
+            return self._session_cache
+        except AttributeError:
+            if self.session_key is None or no_load:
+                self._session_cache = {}
+            else:
+                self._session_cache = await self.aload()
+        return self._session_cache
+
     _session = property(_get_session)
 
     def get_session_cookie_age(self):
@@ -224,6 +292,25 @@ class SessionBase:
         delta = expiry - modification
         return delta.days * 86400 + delta.seconds
 
+    async def aget_expiry_age(self, **kwargs):
+        try:
+            modification = kwargs["modification"]
+        except KeyError:
+            modification = timezone.now()
+        try:
+            expiry = kwargs["expiry"]
+        except KeyError:
+            expiry = await self.aget("_session_expiry")
+
+        if not expiry:  # Checks both None and 0 cases
+            return self.get_session_cookie_age()
+        if not isinstance(expiry, (datetime, str)):
+            return expiry
+        if isinstance(expiry, str):
+            expiry = datetime.fromisoformat(expiry)
+        delta = expiry - modification
+        return delta.days * 86400 + delta.seconds
+
     def get_expiry_date(self, **kwargs):
         """Get session the expiry date (as a datetime object).
 
@@ -247,6 +334,23 @@ class SessionBase:
         expiry = expiry or self.get_session_cookie_age()
         return modification + timedelta(seconds=expiry)
 
+    async def aget_expiry_date(self, **kwargs):
+        try:
+            modification = kwargs["modification"]
+        except KeyError:
+            modification = timezone.now()
+        try:
+            expiry = kwargs["expiry"]
+        except KeyError:
+            expiry = await self.aget("_session_expiry")
+
+        if isinstance(expiry, datetime):
+            return expiry
+        elif isinstance(expiry, str):
+            return datetime.fromisoformat(expiry)
+        expiry = expiry or self.get_session_cookie_age()
+        return modification + timedelta(seconds=expiry)
+
     def set_expiry(self, value):
         """
         Set a custom expiration for the session. ``value`` can be an integer,
@@ -275,6 +379,20 @@ class SessionBase:
             value = value.isoformat()
         self["_session_expiry"] = value
 
+    async def aset_expiry(self, value):
+        if value is None:
+            # Remove any custom expiration for this session.
+            try:
+                await self.apop("_session_expiry")
+            except KeyError:
+                pass
+            return
+        if isinstance(value, timedelta):
+            value = timezone.now() + value
+        if isinstance(value, datetime):
+            value = value.isoformat()
+        await self.aset("_session_expiry", value)
+
     def get_expire_at_browser_close(self):
         """
         Return ``True`` if the session is set to expire when the browser
@@ -286,6 +404,11 @@ class SessionBase:
             return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
         return expiry == 0
 
+    async def aget_expire_at_browser_close(self):
+        if (expiry := await self.aget("_session_expiry")) is None:
+            return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
+        return expiry == 0
+
     def flush(self):
         """
         Remove the current session data from the database and regenerate the
@@ -295,6 +418,11 @@ class SessionBase:
         self.delete()
         self._session_key = None
 
+    async def aflush(self):
+        self.clear()
+        await self.adelete()
+        self._session_key = None
+
     def cycle_key(self):
         """
         Create a new session key, while retaining the current session data.
@@ -306,6 +434,17 @@ class SessionBase:
         if key:
             self.delete(key)
 
+    async def acycle_key(self):
+        """
+        Create a new session key, while retaining the current session data.
+        """
+        data = await self._aget_session()
+        key = self.session_key
+        await self.acreate()
+        self._session_cache = data
+        if key:
+            await self.adelete(key)
+
     # Methods that child classes must implement.
 
     def exists(self, session_key):
@@ -316,6 +455,9 @@ class SessionBase:
             "subclasses of SessionBase must provide an exists() method"
         )
 
+    async def aexists(self, session_key):
+        return await sync_to_async(self.exists)(session_key)
+
     def create(self):
         """
         Create a new session instance. Guaranteed to create a new object with
@@ -326,6 +468,9 @@ class SessionBase:
             "subclasses of SessionBase must provide a create() method"
         )
 
+    async def acreate(self):
+        return await sync_to_async(self.create)()
+
     def save(self, must_create=False):
         """
         Save the session data. If 'must_create' is True, create a new session
@@ -336,6 +481,9 @@ class SessionBase:
             "subclasses of SessionBase must provide a save() method"
         )
 
+    async def asave(self, must_create=False):
+        return await sync_to_async(self.save)(must_create)
+
     def delete(self, session_key=None):
         """
         Delete the session data under this key. If the key is None, use the
@@ -345,6 +493,9 @@ class SessionBase:
             "subclasses of SessionBase must provide a delete() method"
         )
 
+    async def adelete(self, session_key=None):
+        return await sync_to_async(self.delete)(session_key)
+
     def load(self):
         """
         Load the session data and return a dictionary.
@@ -353,6 +504,9 @@ class SessionBase:
             "subclasses of SessionBase must provide a load() method"
         )
 
+    async def aload(self):
+        return await sync_to_async(self.load)()
+
     @classmethod
     def clear_expired(cls):
         """
@@ -363,3 +517,7 @@ class SessionBase:
         a built-in expiration mechanism, it should be a no-op.
         """
         raise NotImplementedError("This backend does not support clear_expired().")
+
+    @classmethod
+    async def aclear_expired(cls):
+        return await sync_to_async(cls.clear_expired)()
diff --git a/django/contrib/sessions/backends/cache.py b/django/contrib/sessions/backends/cache.py
index 0c9d244f56..b87afd6f66 100644
--- a/django/contrib/sessions/backends/cache.py
+++ b/django/contrib/sessions/backends/cache.py
@@ -20,6 +20,9 @@ class SessionStore(SessionBase):
     def cache_key(self):
         return self.cache_key_prefix + self._get_or_create_session_key()
 
+    async def acache_key(self):
+        return self.cache_key_prefix + await self._aget_or_create_session_key()
+
     def load(self):
         try:
             session_data = self._cache.get(self.cache_key)
@@ -32,6 +35,16 @@ class SessionStore(SessionBase):
         self._session_key = None
         return {}
 
+    async def aload(self):
+        try:
+            session_data = await self._cache.aget(await self.acache_key())
+        except Exception:
+            session_data = None
+        if session_data is not None:
+            return session_data
+        self._session_key = None
+        return {}
+
     def create(self):
         # Because a cache can fail silently (e.g. memcache), we don't know if
         # we are failing to create a new session because of a key collision or
@@ -51,6 +64,20 @@ class SessionStore(SessionBase):
             "It is likely that the cache is unavailable."
         )
 
+    async def acreate(self):
+        for i in range(10000):
+            self._session_key = await self._aget_new_session_key()
+            try:
+                await self.asave(must_create=True)
+            except CreateError:
+                continue
+            self.modified = True
+            return
+        raise RuntimeError(
+            "Unable to create a new session key. "
+            "It is likely that the cache is unavailable."
+        )
+
     def save(self, must_create=False):
         if self.session_key is None:
             return self.create()
@@ -68,11 +95,33 @@ class SessionStore(SessionBase):
         if must_create and not result:
             raise CreateError
 
+    async def asave(self, must_create=False):
+        if self.session_key is None:
+            return await self.acreate()
+        if must_create:
+            func = self._cache.aadd
+        elif await self._cache.aget(await self.acache_key()) is not None:
+            func = self._cache.aset
+        else:
+            raise UpdateError
+        result = await func(
+            await self.acache_key(),
+            await self._aget_session(no_load=must_create),
+            await self.aget_expiry_age(),
+        )
+        if must_create and not result:
+            raise CreateError
+
     def exists(self, session_key):
         return (
             bool(session_key) and (self.cache_key_prefix + session_key) in self._cache
         )
 
+    async def aexists(self, session_key):
+        return bool(session_key) and await self._cache.ahas_key(
+            self.cache_key_prefix + session_key
+        )
+
     def delete(self, session_key=None):
         if session_key is None:
             if self.session_key is None:
@@ -80,6 +129,17 @@ class SessionStore(SessionBase):
             session_key = self.session_key
         self._cache.delete(self.cache_key_prefix + session_key)
 
+    async def adelete(self, session_key=None):
+        if session_key is None:
+            if self.session_key is None:
+                return
+            session_key = self.session_key
+        await self._cache.adelete(self.cache_key_prefix + session_key)
+
     @classmethod
     def clear_expired(cls):
         pass
+
+    @classmethod
+    async def aclear_expired(cls):
+        pass
diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py
index 3125a71cd0..2195f57acc 100644
--- a/django/contrib/sessions/backends/cached_db.py
+++ b/django/contrib/sessions/backends/cached_db.py
@@ -2,12 +2,16 @@
 Cached, database-backed sessions.
 """
 
+import logging
+
 from django.conf import settings
 from django.contrib.sessions.backends.db import SessionStore as DBStore
 from django.core.cache import caches
 
 KEY_PREFIX = "django.contrib.sessions.cached_db"
 
+logger = logging.getLogger("django.contrib.sessions")
+
 
 class SessionStore(DBStore):
     """
@@ -24,6 +28,9 @@ class SessionStore(DBStore):
     def cache_key(self):
         return self.cache_key_prefix + self._get_or_create_session_key()
 
+    async def acache_key(self):
+        return self.cache_key_prefix + await self._aget_or_create_session_key()
+
     def load(self):
         try:
             data = self._cache.get(self.cache_key)
@@ -43,6 +50,27 @@ class SessionStore(DBStore):
                 data = {}
         return data
 
+    async def aload(self):
+        try:
+            data = await self._cache.aget(await self.acache_key())
+        except Exception:
+            # Some backends (e.g. memcache) raise an exception on invalid
+            # cache keys. If this happens, reset the session. See #17810.
+            data = None
+
+        if data is None:
+            s = await self._aget_session_from_db()
+            if s:
+                data = self.decode(s.session_data)
+                await self._cache.aset(
+                    await self.acache_key(),
+                    data,
+                    await self.aget_expiry_age(expiry=s.expire_date),
+                )
+            else:
+                data = {}
+        return data
+
     def exists(self, session_key):
         return (
             session_key
@@ -50,9 +78,30 @@ class SessionStore(DBStore):
             or super().exists(session_key)
         )
 
+    async def aexists(self, session_key):
+        return (
+            session_key
+            and (self.cache_key_prefix + session_key) in self._cache
+            or await super().aexists(session_key)
+        )
+
     def save(self, must_create=False):
         super().save(must_create)
-        self._cache.set(self.cache_key, self._session, self.get_expiry_age())
+        try:
+            self._cache.set(self.cache_key, self._session, self.get_expiry_age())
+        except Exception:
+            logger.exception("Error saving to cache (%s)", self._cache)
+
+    async def asave(self, must_create=False):
+        await super().asave(must_create)
+        try:
+            await self._cache.aset(
+                await self.acache_key(),
+                self._session,
+                await self.aget_expiry_age(),
+            )
+        except Exception:
+            logger.exception("Error saving to cache (%s)", self._cache)
 
     def delete(self, session_key=None):
         super().delete(session_key)
@@ -62,6 +111,14 @@ class SessionStore(DBStore):
             session_key = self.session_key
         self._cache.delete(self.cache_key_prefix + session_key)
 
+    async def adelete(self, session_key=None):
+        await super().adelete(session_key)
+        if session_key is None:
+            if self.session_key is None:
+                return
+            session_key = self.session_key
+        await self._cache.adelete(self.cache_key_prefix + session_key)
+
     def flush(self):
         """
         Remove the current session data from the database and regenerate the
@@ -70,3 +127,9 @@ class SessionStore(DBStore):
         self.clear()
         self.delete(self.session_key)
         self._session_key = None
+
+    async def aflush(self):
+        """See flush()."""
+        self.clear()
+        await self.adelete(self.session_key)
+        self._session_key = None
diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py
index e1f6b69c55..6d6247d6c9 100644
--- a/django/contrib/sessions/backends/db.py
+++ b/django/contrib/sessions/backends/db.py
@@ -1,5 +1,7 @@
 import logging
 
+from asgiref.sync import sync_to_async
+
 from django.contrib.sessions.backends.base import CreateError, SessionBase, UpdateError
 from django.core.exceptions import SuspiciousOperation
 from django.db import DatabaseError, IntegrityError, router, transaction
@@ -38,13 +40,31 @@ class SessionStore(SessionBase):
                 logger.warning(str(e))
             self._session_key = None
 
+    async def _aget_session_from_db(self):
+        try:
+            return await self.model.objects.aget(
+                session_key=self.session_key, expire_date__gt=timezone.now()
+            )
+        except (self.model.DoesNotExist, SuspiciousOperation) as e:
+            if isinstance(e, SuspiciousOperation):
+                logger = logging.getLogger("django.security.%s" % e.__class__.__name__)
+                logger.warning(str(e))
+            self._session_key = None
+
     def load(self):
         s = self._get_session_from_db()
         return self.decode(s.session_data) if s else {}
 
+    async def aload(self):
+        s = await self._aget_session_from_db()
+        return self.decode(s.session_data) if s else {}
+
     def exists(self, session_key):
         return self.model.objects.filter(session_key=session_key).exists()
 
+    async def aexists(self, session_key):
+        return await self.model.objects.filter(session_key=session_key).aexists()
+
     def create(self):
         while True:
             self._session_key = self._get_new_session_key()
@@ -58,6 +78,19 @@ class SessionStore(SessionBase):
             self.modified = True
             return
 
+    async def acreate(self):
+        while True:
+            self._session_key = await self._aget_new_session_key()
+            try:
+                # Save immediately to ensure we have a unique entry in the
+                # database.
+                await self.asave(must_create=True)
+            except CreateError:
+                # Key wasn't unique. Try again.
+                continue
+            self.modified = True
+            return
+
     def create_model_instance(self, data):
         """
         Return a new instance of the session model object, which represents the
@@ -70,6 +103,14 @@ class SessionStore(SessionBase):
             expire_date=self.get_expiry_date(),
         )
 
+    async def acreate_model_instance(self, data):
+        """See create_model_instance()."""
+        return self.model(
+            session_key=await self._aget_or_create_session_key(),
+            session_data=self.encode(data),
+            expire_date=await self.aget_expiry_date(),
+        )
+
     def save(self, must_create=False):
         """
         Save the current session data to the database. If 'must_create' is
@@ -95,6 +136,36 @@ class SessionStore(SessionBase):
                 raise UpdateError
             raise
 
+    async def asave(self, must_create=False):
+        """See save()."""
+        if self.session_key is None:
+            return await self.acreate()
+        data = await self._aget_session(no_load=must_create)
+        obj = await self.acreate_model_instance(data)
+        using = router.db_for_write(self.model, instance=obj)
+        try:
+            # This code MOST run in a transaction, so it requires
+            # @sync_to_async wrapping until transaction.atomic() supports
+            # async.
+            @sync_to_async
+            def sync_transaction():
+                with transaction.atomic(using=using):
+                    obj.save(
+                        force_insert=must_create,
+                        force_update=not must_create,
+                        using=using,
+                    )
+
+            await sync_transaction()
+        except IntegrityError:
+            if must_create:
+                raise CreateError
+            raise
+        except DatabaseError:
+            if not must_create:
+                raise UpdateError
+            raise
+
     def delete(self, session_key=None):
         if session_key is None:
             if self.session_key is None:
@@ -105,6 +176,23 @@ class SessionStore(SessionBase):
         except self.model.DoesNotExist:
             pass
 
+    async def adelete(self, session_key=None):
+        if session_key is None:
+            if self.session_key is None:
+                return
+            session_key = self.session_key
+        try:
+            obj = await self.model.objects.aget(session_key=session_key)
+            await obj.adelete()
+        except self.model.DoesNotExist:
+            pass
+
     @classmethod
     def clear_expired(cls):
         cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()
+
+    @classmethod
+    async def aclear_expired(cls):
+        await cls.get_model_class().objects.filter(
+            expire_date__lt=timezone.now()
+        ).adelete()
diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py
index d3ba8b11dd..e15b3f0141 100644
--- a/django/contrib/sessions/backends/file.py
+++ b/django/contrib/sessions/backends/file.py
@@ -104,6 +104,9 @@ class SessionStore(SessionBase):
             self._session_key = None
         return session_data
 
+    async def aload(self):
+        return self.load()
+
     def create(self):
         while True:
             self._session_key = self._get_new_session_key()
@@ -114,6 +117,9 @@ class SessionStore(SessionBase):
             self.modified = True
             return
 
+    async def acreate(self):
+        return self.create()
+
     def save(self, must_create=False):
         if self.session_key is None:
             return self.create()
@@ -177,9 +183,15 @@ class SessionStore(SessionBase):
         except (EOFError, OSError):
             pass
 
+    async def asave(self, must_create=False):
+        return self.save(must_create=must_create)
+
     def exists(self, session_key):
         return os.path.exists(self._key_to_file(session_key))
 
+    async def aexists(self, session_key):
+        return self.exists(session_key)
+
     def delete(self, session_key=None):
         if session_key is None:
             if self.session_key is None:
@@ -190,8 +202,8 @@ class SessionStore(SessionBase):
         except OSError:
             pass
 
-    def clean(self):
-        pass
+    async def adelete(self, session_key=None):
+        return self.delete(session_key=session_key)
 
     @classmethod
     def clear_expired(cls):
@@ -208,3 +220,7 @@ class SessionStore(SessionBase):
             # the create() method.
             session.create = lambda: None
             session.load()
+
+    @classmethod
+    async def aclear_expired(cls):
+        cls.clear_expired()
diff --git a/django/contrib/sessions/backends/signed_cookies.py b/django/contrib/sessions/backends/signed_cookies.py
index dc41c6f12b..604cb99808 100644
--- a/django/contrib/sessions/backends/signed_cookies.py
+++ b/django/contrib/sessions/backends/signed_cookies.py
@@ -23,6 +23,9 @@ class SessionStore(SessionBase):
             self.create()
         return {}
 
+    async def aload(self):
+        return self.load()
+
     def create(self):
         """
         To create a new key, set the modified flag so that the cookie is set
@@ -30,6 +33,9 @@ class SessionStore(SessionBase):
         """
         self.modified = True
 
+    async def acreate(self):
+        return self.create()
+
     def save(self, must_create=False):
         """
         To save, get the session key as a securely signed string and then set
@@ -39,6 +45,9 @@ class SessionStore(SessionBase):
         self._session_key = self._get_session_key()
         self.modified = True
 
+    async def asave(self, must_create=False):
+        return self.save(must_create=must_create)
+
     def exists(self, session_key=None):
         """
         This method makes sense when you're talking to a shared resource, but
@@ -47,6 +56,9 @@ class SessionStore(SessionBase):
         """
         return False
 
+    async def aexists(self, session_key=None):
+        return self.exists(session_key=session_key)
+
     def delete(self, session_key=None):
         """
         To delete, clear the session key and the underlying data structure
@@ -57,6 +69,9 @@ class SessionStore(SessionBase):
         self._session_cache = {}
         self.modified = True
 
+    async def adelete(self, session_key=None):
+        return self.delete(session_key=session_key)
+
     def cycle_key(self):
         """
         Keep the same data but with a new key. Call save() and it will
@@ -64,6 +79,9 @@ class SessionStore(SessionBase):
         """
         self.save()
 
+    async def acycle_key(self):
+        return self.cycle_key()
+
     def _get_session_key(self):
         """
         Instead of generating a random string, generate a secure url-safe
@@ -79,3 +97,7 @@ class SessionStore(SessionBase):
     @classmethod
     def clear_expired(cls):
         pass
+
+    @classmethod
+    async def aclear_expired(cls):
+        pass
diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py
index 7394eff818..686718a355 100644
--- a/django/contrib/staticfiles/handlers.py
+++ b/django/contrib/staticfiles/handlers.py
@@ -36,13 +36,13 @@ class StaticFilesHandlerMixin:
         * the host is provided as part of the base_url
         * the request's path isn't under the media path (or equal)
         """
-        return path.startswith(self.base_url[2]) and not self.base_url[1]
+        return path.startswith(self.base_url.path) and not self.base_url.netloc
 
     def file_path(self, url):
         """
         Return the relative path to the media file on disk for the given URL.
         """
-        relative_url = url.removeprefix(self.base_url[2])
+        relative_url = url.removeprefix(self.base_url.path)
         return url2pathname(relative_url)
 
     def serve(self, request):
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index 85172ea42d..191fe3cbb5 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -221,7 +221,7 @@ class HashedFilesMixin:
             url = matches["url"]
 
             # Ignore absolute/protocol-relative and data-uri URLs.
-            if re.match(r"^[a-z]+:", url):
+            if re.match(r"^[a-z]+:", url) or url.startswith("//"):
                 return matched
 
             # Ignore absolute URLs that don't point to a static file (dynamic
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
index 2378a14874..0947ab212c 100644
--- a/django/contrib/syndication/views.py
+++ b/django/contrib/syndication/views.py
@@ -160,6 +160,7 @@ class Feed:
             feed_copyright=self._get_dynamic_attr("feed_copyright", obj),
             feed_guid=self._get_dynamic_attr("feed_guid", obj),
             ttl=self._get_dynamic_attr("ttl", obj),
+            stylesheets=self._get_dynamic_attr("stylesheets", obj),
             **self.feed_extra_kwargs(obj),
         )
 
diff --git a/django/core/checks/templates.py b/django/core/checks/templates.py
index 681aa1f317..72a3212e56 100644
--- a/django/core/checks/templates.py
+++ b/django/core/checks/templates.py
@@ -1,75 +1,12 @@
-import copy
-from collections import defaultdict
-
-from django.conf import settings
-from django.template.backends.django import get_template_tag_modules
-
-from . import Error, Tags, Warning, register
-
-E001 = Error(
-    "You have 'APP_DIRS': True in your TEMPLATES but also specify 'loaders' "
-    "in OPTIONS. Either remove APP_DIRS or remove the 'loaders' option.",
-    id="templates.E001",
-)
-E002 = Error(
-    "'string_if_invalid' in TEMPLATES OPTIONS must be a string but got: {} ({}).",
-    id="templates.E002",
-)
-W003 = Warning(
-    "{} is used for multiple template tag modules: {}",
-    id="templates.E003",
-)
+from . import Tags, register
 
 
 @register(Tags.templates)
-def check_setting_app_dirs_loaders(app_configs, **kwargs):
-    return (
-        [E001]
-        if any(
-            conf.get("APP_DIRS") and "loaders" in conf.get("OPTIONS", {})
-            for conf in settings.TEMPLATES
-        )
-        else []
-    )
+def check_templates(app_configs, **kwargs):
+    """Check all registered template engines."""
+    from django.template import engines
 
-
-@register(Tags.templates)
-def check_string_if_invalid_is_string(app_configs, **kwargs):
     errors = []
-    for conf in settings.TEMPLATES:
-        string_if_invalid = conf.get("OPTIONS", {}).get("string_if_invalid", "")
-        if not isinstance(string_if_invalid, str):
-            error = copy.copy(E002)
-            error.msg = error.msg.format(
-                string_if_invalid, type(string_if_invalid).__name__
-            )
-            errors.append(error)
-    return errors
-
-
-@register(Tags.templates)
-def check_for_template_tags_with_the_same_name(app_configs, **kwargs):
-    errors = []
-    libraries = defaultdict(set)
-
-    for conf in settings.TEMPLATES:
-        custom_libraries = conf.get("OPTIONS", {}).get("libraries", {})
-        for module_name, module_path in custom_libraries.items():
-            libraries[module_name].add(module_path)
-
-    for module_name, module_path in get_template_tag_modules():
-        libraries[module_name].add(module_path)
-
-    for library_name, items in libraries.items():
-        if len(items) > 1:
-            errors.append(
-                Warning(
-                    W003.msg.format(
-                        repr(library_name),
-                        ", ".join(repr(item) for item in sorted(items)),
-                    ),
-                    id=W003.id,
-                )
-            )
-
+    for engine in engines.all():
+        errors.extend(engine.check())
     return errors
diff --git a/django/core/checks/urls.py b/django/core/checks/urls.py
index 34eff9671d..aef2bfebb0 100644
--- a/django/core/checks/urls.py
+++ b/django/core/checks/urls.py
@@ -1,6 +1,8 @@
+import inspect
 from collections import Counter
 
 from django.conf import settings
+from django.core.exceptions import ViewDoesNotExist
 
 from . import Error, Tags, Warning, register
 
@@ -115,3 +117,43 @@ def E006(name):
         "The {} setting must end with a slash.".format(name),
         id="urls.E006",
     )
+
+
+@register(Tags.urls)
+def check_custom_error_handlers(app_configs, **kwargs):
+    if not getattr(settings, "ROOT_URLCONF", None):
+        return []
+
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+
+    errors = []
+    # All handlers take (request, exception) arguments except handler500
+    # which takes (request).
+    for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 1)]:
+        try:
+            handler = resolver.resolve_error_handler(status_code)
+        except (ImportError, ViewDoesNotExist) as e:
+            path = getattr(resolver.urlconf_module, "handler%s" % status_code)
+            msg = (
+                "The custom handler{status_code} view '{path}' could not be "
+                "imported."
+            ).format(status_code=status_code, path=path)
+            errors.append(Error(msg, hint=str(e), id="urls.E008"))
+            continue
+        signature = inspect.signature(handler)
+        args = [None] * num_parameters
+        try:
+            signature.bind(*args)
+        except TypeError:
+            msg = (
+                "The custom handler{status_code} view '{path}' does not "
+                "take the correct number of arguments ({args})."
+            ).format(
+                status_code=status_code,
+                path=handler.__module__ + "." + handler.__qualname__,
+                args="request, exception" if num_parameters == 2 else "request",
+            )
+            errors.append(Error(msg, id="urls.E007"))
+    return errors
diff --git a/django/core/files/move.py b/django/core/files/move.py
index 95d69f9d94..d7a9c7026e 100644
--- a/django/core/files/move.py
+++ b/django/core/files/move.py
@@ -13,20 +13,6 @@ from django.core.files import locks
 __all__ = ["file_move_safe"]
 
 
-def _samefile(src, dst):
-    # Macintosh, Unix.
-    if hasattr(os.path, "samefile"):
-        try:
-            return os.path.samefile(src, dst)
-        except OSError:
-            return False
-
-    # All other platforms: check for same pathname.
-    return os.path.normcase(os.path.abspath(src)) == os.path.normcase(
-        os.path.abspath(dst)
-    )
-
-
 def file_move_safe(
     old_file_name, new_file_name, chunk_size=1024 * 64, allow_overwrite=False
 ):
@@ -40,16 +26,18 @@ def file_move_safe(
     ``FileExistsError``.
     """
     # There's no reason to move if we don't have to.
-    if _samefile(old_file_name, new_file_name):
-        return
+    try:
+        if os.path.samefile(old_file_name, new_file_name):
+            return
+    except OSError:
+        pass
+
+    if not allow_overwrite and os.access(new_file_name, os.F_OK):
+        raise FileExistsError(
+            f"Destination file {new_file_name} exists and allow_overwrite is False."
+        )
 
     try:
-        if not allow_overwrite and os.access(new_file_name, os.F_OK):
-            raise FileExistsError(
-                "Destination file %s exists and allow_overwrite is False."
-                % new_file_name
-            )
-
         os.rename(old_file_name, new_file_name)
         return
     except OSError:
diff --git a/django/core/files/storage/base.py b/django/core/files/storage/base.py
index 16ac22f70a..6ce4ab2535 100644
--- a/django/core/files/storage/base.py
+++ b/django/core/files/storage/base.py
@@ -69,7 +69,8 @@ class Storage:
                 "Detected path traversal attempt in '%s'" % dir_name
             )
         validate_file_name(file_name)
-        file_root, file_ext = os.path.splitext(file_name)
+        file_ext = "".join(pathlib.PurePath(file_name).suffixes)
+        file_root = file_name.removesuffix(file_ext)
         # If the filename already exists, generate an alternative filename
         # until it doesn't exist.
         # Truncate original name if required, so the new filename does not
diff --git a/django/core/files/storage/filesystem.py b/django/core/files/storage/filesystem.py
index 85fc4eff9f..ed752cc062 100644
--- a/django/core/files/storage/filesystem.py
+++ b/django/core/files/storage/filesystem.py
@@ -1,13 +1,16 @@
 import os
+import warnings
 from datetime import datetime, timezone
 from urllib.parse import urljoin
 
 from django.conf import settings
+from django.core.exceptions import SuspiciousFileOperation
 from django.core.files import File, locks
 from django.core.files.move import file_move_safe
 from django.core.signals import setting_changed
 from django.utils._os import safe_join
 from django.utils.deconstruct import deconstructible
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.encoding import filepath_to_uri
 from django.utils.functional import cached_property
 
@@ -21,8 +24,7 @@ class FileSystemStorage(Storage, StorageSettingsMixin):
     Standard filesystem storage
     """
 
-    # The combination of O_CREAT and O_EXCL makes os.open() raise OSError if
-    # the file already exists before it's opened.
+    # RemovedInDjango60Warning: remove OS_OPEN_FLAGS.
     OS_OPEN_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
 
     def __init__(
@@ -31,12 +33,23 @@ class FileSystemStorage(Storage, StorageSettingsMixin):
         base_url=None,
         file_permissions_mode=None,
         directory_permissions_mode=None,
+        allow_overwrite=False,
     ):
         self._location = location
         self._base_url = base_url
         self._file_permissions_mode = file_permissions_mode
         self._directory_permissions_mode = directory_permissions_mode
+        self._allow_overwrite = allow_overwrite
         setting_changed.connect(self._clear_cached_properties)
+        # RemovedInDjango60Warning: remove this warning.
+        if self.OS_OPEN_FLAGS != os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(
+            os, "O_BINARY", 0
+        ):
+            warnings.warn(
+                "Overriding OS_OPEN_FLAGS is deprecated. Use "
+                "the allow_overwrite parameter instead.",
+                RemovedInDjango60Warning,
+            )
 
     @cached_property
     def base_location(self):
@@ -98,12 +111,30 @@ class FileSystemStorage(Storage, StorageSettingsMixin):
             try:
                 # This file has a file path that we can move.
                 if hasattr(content, "temporary_file_path"):
-                    file_move_safe(content.temporary_file_path(), full_path)
+                    file_move_safe(
+                        content.temporary_file_path(),
+                        full_path,
+                        allow_overwrite=self._allow_overwrite,
+                    )
 
                 # This is a normal uploadedfile that we can stream.
                 else:
-                    # The current umask value is masked out by os.open!
-                    fd = os.open(full_path, self.OS_OPEN_FLAGS, 0o666)
+                    # The combination of O_CREAT and O_EXCL makes os.open() raises an
+                    # OSError if the file already exists before it's opened.
+                    open_flags = (
+                        os.O_WRONLY
+                        | os.O_CREAT
+                        | os.O_EXCL
+                        | getattr(os, "O_BINARY", 0)
+                    )
+                    # RemovedInDjango60Warning: when the deprecation ends, replace with:
+                    # if self._allow_overwrite:
+                    #     open_flags = open_flags & ~os.O_EXCL
+                    if self.OS_OPEN_FLAGS != open_flags:
+                        open_flags = self.OS_OPEN_FLAGS
+                    elif self._allow_overwrite:
+                        open_flags = open_flags & ~os.O_EXCL
+                    fd = os.open(full_path, open_flags, 0o666)
                     _file = None
                     try:
                         locks.lock(fd, locks.LOCK_EX)
@@ -162,7 +193,13 @@ class FileSystemStorage(Storage, StorageSettingsMixin):
             pass
 
     def exists(self, name):
-        return os.path.lexists(self.path(name))
+        try:
+            exists = os.path.lexists(self.path(name))
+        except SuspiciousFileOperation:
+            raise
+        if self._allow_overwrite:
+            return False
+        return exists
 
     def listdir(self, path):
         path = self.path(path)
diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py
index 3af080599a..bb6a6bfb3c 100644
--- a/django/core/handlers/asgi.py
+++ b/django/core/handlers/asgi.py
@@ -50,21 +50,13 @@ class ASGIRequest(HttpRequest):
         self._post_parse_error = False
         self._read_started = False
         self.resolver_match = None
+        self.path = scope["path"]
         self.script_name = get_script_prefix(scope)
         if self.script_name:
             # TODO: Better is-prefix checking, slash handling?
             self.path_info = scope["path"].removeprefix(self.script_name)
         else:
             self.path_info = scope["path"]
-        # The Django path is different from ASGI scope path args, it should
-        # combine with script name.
-        if self.script_name:
-            self.path = "%s/%s" % (
-                self.script_name.rstrip("/"),
-                self.path_info.replace("/", "", 1),
-            )
-        else:
-            self.path = scope["path"]
         # HTTP basics.
         self.method = self.scope["method"].upper()
         # Ensure query string is encoded correctly.
diff --git a/django/core/mail/message.py b/django/core/mail/message.py
index 4f8c93e9e5..2eb8aa354b 100644
--- a/django/core/mail/message.py
+++ b/django/core/mail/message.py
@@ -1,4 +1,5 @@
 import mimetypes
+from collections import namedtuple
 from email import charset as Charset
 from email import encoders as Encoders
 from email import generator, message_from_string
@@ -168,7 +169,8 @@ class SafeMIMEText(MIMEMixin, MIMEText):
     def set_payload(self, payload, charset=None):
         if charset == "utf-8" and not isinstance(charset, Charset.Charset):
             has_long_lines = any(
-                len(line.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT
+                len(line.encode(errors="surrogateescape"))
+                > RFC5322_EMAIL_LINE_LENGTH_LIMIT
                 for line in payload.splitlines()
             )
             # Quoted-Printable encoding has the side effect of shortening long
@@ -189,6 +191,10 @@ class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
         MIMEMultipart.__setitem__(self, name, val)
 
 
+Alternative = namedtuple("Alternative", ["content", "mimetype"])
+EmailAttachment = namedtuple("Attachment", ["filename", "content", "mimetype"])
+
+
 class EmailMessage:
     """A container for email information."""
 
@@ -337,7 +343,7 @@ class EmailMessage:
                         # actually binary, read() raises a UnicodeDecodeError.
                         mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
 
-            self.attachments.append((filename, content, mimetype))
+            self.attachments.append(EmailAttachment(filename, content, mimetype))
 
     def attach_file(self, path, mimetype=None):
         """
@@ -470,13 +476,15 @@ class EmailMultiAlternatives(EmailMessage):
             cc,
             reply_to,
         )
-        self.alternatives = alternatives or []
+        self.alternatives = [
+            Alternative(*alternative) for alternative in (alternatives or [])
+        ]
 
     def attach_alternative(self, content, mimetype):
         """Attach an alternative content representation."""
         if content is None or mimetype is None:
             raise ValueError("Both content and mimetype must be provided.")
-        self.alternatives.append((content, mimetype))
+        self.alternatives.append(Alternative(content, mimetype))
 
     def _create_message(self, msg):
         return self._create_attachments(self._create_alternatives(msg))
@@ -491,5 +499,22 @@ class EmailMultiAlternatives(EmailMessage):
             if self.body:
                 msg.attach(body_msg)
             for alternative in self.alternatives:
-                msg.attach(self._create_mime_attachment(*alternative))
+                msg.attach(
+                    self._create_mime_attachment(
+                        alternative.content, alternative.mimetype
+                    )
+                )
         return msg
+
+    def body_contains(self, text):
+        """
+        Checks that ``text`` occurs in the email body and in all attached MIME
+        type text/* alternatives.
+        """
+        if text not in self.body:
+            return False
+
+        for content, mimetype in self.alternatives:
+            if mimetype.startswith("text/") and text not in content:
+                return False
+        return True
diff --git a/django/core/management/base.py b/django/core/management/base.py
index 4c47e1c6e5..41b17a24c8 100644
--- a/django/core/management/base.py
+++ b/django/core/management/base.py
@@ -345,7 +345,7 @@ class BaseCommand:
             parser,
             "--traceback",
             action="store_true",
-            help="Raise on CommandError exceptions.",
+            help="Display a full stack trace on CommandError exceptions.",
         )
         self.add_base_argument(
             parser,
diff --git a/django/core/management/commands/check.py b/django/core/management/commands/check.py
index 6348c9fc68..e61cff79f3 100644
--- a/django/core/management/commands/check.py
+++ b/django/core/management/commands/check.py
@@ -2,6 +2,7 @@ from django.apps import apps
 from django.core import checks
 from django.core.checks.registry import registry
 from django.core.management.base import BaseCommand, CommandError
+from django.db import connections
 
 
 class Command(BaseCommand):
@@ -43,6 +44,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             action="append",
+            choices=tuple(connections),
             dest="databases",
             help="Run database related checks against these aliases.",
         )
diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py
index 65ed1686d2..a8965b1edc 100644
--- a/django/core/management/commands/createcachetable.py
+++ b/django/core/management/commands/createcachetable.py
@@ -30,6 +30,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help="Nominates a database onto which the cache tables will be "
             'installed. Defaults to the "default" database.',
         )
diff --git a/django/core/management/commands/dbshell.py b/django/core/management/commands/dbshell.py
index bdb130594f..b177a7e083 100644
--- a/django/core/management/commands/dbshell.py
+++ b/django/core/management/commands/dbshell.py
@@ -16,6 +16,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 "Nominates a database onto which to open a shell. Defaults to the "
                 '"default" database.'
diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py
index 01ff8974dd..5a9ab83919 100644
--- a/django/core/management/commands/dumpdata.py
+++ b/django/core/management/commands/dumpdata.py
@@ -6,7 +6,7 @@ from django.apps import apps
 from django.core import serializers
 from django.core.management.base import BaseCommand, CommandError
 from django.core.management.utils import parse_apps_and_model_labels
-from django.db import DEFAULT_DB_ALIAS, router
+from django.db import DEFAULT_DB_ALIAS, connections, router
 
 try:
     import bz2
@@ -56,6 +56,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help="Nominates a specific database to dump fixtures from. "
             'Defaults to the "default" database.',
         )
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index e9d440dd86..a057393d53 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -25,6 +25,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help='Nominates a database to flush. Defaults to the "default" database.',
         )
 
diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py
index 5c2ed53db8..77605b178f 100644
--- a/django/core/management/commands/inspectdb.py
+++ b/django/core/management/commands/inspectdb.py
@@ -25,6 +25,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 'Nominates a database to introspect. Defaults to using the "default" '
                 "database."
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py
index bb46e8ae78..8c76e52633 100644
--- a/django/core/management/commands/loaddata.py
+++ b/django/core/management/commands/loaddata.py
@@ -55,6 +55,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 "Nominates a specific database to load fixtures into. Defaults to the "
                 '"default" database.'
diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py
index 1541843066..5e6b19c095 100644
--- a/django/core/management/commands/migrate.py
+++ b/django/core/management/commands/migrate.py
@@ -47,6 +47,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 'Nominates a database to synchronize. Defaults to the "default" '
                 "database."
@@ -195,8 +196,11 @@ class Command(BaseCommand):
                 )
             if self.verbosity > 0:
                 self.stdout.write("Pruning migrations:", self.style.MIGRATE_HEADING)
-            to_prune = set(executor.loader.applied_migrations) - set(
-                executor.loader.disk_migrations
+            to_prune = sorted(
+                migration
+                for migration in set(executor.loader.applied_migrations)
+                - set(executor.loader.disk_migrations)
+                if migration[0] == app_label
             )
             squashed_migrations_with_deleted_replaced_migrations = [
                 migration_key
@@ -222,9 +226,6 @@ class Command(BaseCommand):
                     )
                 )
             else:
-                to_prune = sorted(
-                    migration for migration in to_prune if migration[0] == app_label
-                )
                 if to_prune:
                     for migration in to_prune:
                         app, name = migration
diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py
index 26bbf29d68..132ee4c079 100644
--- a/django/core/management/commands/runserver.py
+++ b/django/core/management/commands/runserver.py
@@ -8,6 +8,7 @@ from datetime import datetime
 from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
 from django.core.servers.basehttp import WSGIServer, get_internal_wsgi_application, run
+from django.db import connections
 from django.utils import autoreload
 from django.utils.regex_helper import _lazy_re_compile
 
@@ -134,6 +135,9 @@ class Command(BaseCommand):
         # Need to check migrations here, so can't use the
         # requires_migrations_check attribute.
         self.check_migrations()
+        # Close all connections opened during migration checking.
+        for conn in connections.all(initialized_only=True):
+            conn.close()
 
         try:
             handler = self.get_handler(*args, **options)
diff --git a/django/core/management/commands/showmigrations.py b/django/core/management/commands/showmigrations.py
index 203f92151d..e88f83f273 100644
--- a/django/core/management/commands/showmigrations.py
+++ b/django/core/management/commands/showmigrations.py
@@ -19,6 +19,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 "Nominates a database to show migrations for. Defaults to the "
                 '"default" database.'
diff --git a/django/core/management/commands/sqlflush.py b/django/core/management/commands/sqlflush.py
index bc82e1f05f..d0512a68a8 100644
--- a/django/core/management/commands/sqlflush.py
+++ b/django/core/management/commands/sqlflush.py
@@ -16,6 +16,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 'Nominates a database to print the SQL for. Defaults to the "default" '
                 "database."
diff --git a/django/core/management/commands/sqlmigrate.py b/django/core/management/commands/sqlmigrate.py
index 2f6993682f..3e3151f0cf 100644
--- a/django/core/management/commands/sqlmigrate.py
+++ b/django/core/management/commands/sqlmigrate.py
@@ -19,6 +19,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 'Nominates a database to create SQL for. Defaults to the "default" '
                 "database."
diff --git a/django/core/management/commands/sqlsequencereset.py b/django/core/management/commands/sqlsequencereset.py
index 9653fa59d0..cc0d100499 100644
--- a/django/core/management/commands/sqlsequencereset.py
+++ b/django/core/management/commands/sqlsequencereset.py
@@ -14,6 +14,7 @@ class Command(AppCommand):
         parser.add_argument(
             "--database",
             default=DEFAULT_DB_ALIAS,
+            choices=tuple(connections),
             help=(
                 'Nominates a database to print the SQL for. Defaults to the "default" '
                 "database."
diff --git a/django/core/management/commands/squashmigrations.py b/django/core/management/commands/squashmigrations.py
index 6571cbc2e2..6b5ddeeba5 100644
--- a/django/core/management/commands/squashmigrations.py
+++ b/django/core/management/commands/squashmigrations.py
@@ -123,7 +123,7 @@ class Command(BaseCommand):
             if self.interactive:
                 answer = None
                 while not answer or answer not in "yn":
-                    answer = input("Do you wish to proceed? [yN] ")
+                    answer = input("Do you wish to proceed? [y/N] ")
                     if not answer:
                         answer = "n"
                         break
diff --git a/django/core/management/commands/test.py b/django/core/management/commands/test.py
index 2df6dbbecf..7452b3fc6a 100644
--- a/django/core/management/commands/test.py
+++ b/django/core/management/commands/test.py
@@ -40,11 +40,6 @@ class Command(BaseCommand):
             dest="interactive",
             help="Tells Django to NOT prompt the user for input of any kind.",
         )
-        parser.add_argument(
-            "--failfast",
-            action="store_true",
-            help="Tells Django to stop running the test suite after first failed test.",
-        )
         parser.add_argument(
             "--testrunner",
             help="Tells Django to use specified test runner class instead of "
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index 16b69770f6..5818bfaa84 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -106,7 +106,7 @@ class Serializer(base.Serializer):
         differently from regular fields).
         """
         self._start_relational_field(field)
-        related_att = getattr(obj, field.get_attname())
+        related_att = getattr(obj, field.attname)
         if related_att is not None:
             if self.use_natural_foreign_keys and hasattr(
                 field.remote_field.model, "natural_key"
diff --git a/django/core/validators.py b/django/core/validators.py
index 57940a59da..b1c5c053b8 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -65,6 +65,64 @@ class RegexValidator:
         )
 
 
+@deconstructible
+class DomainNameValidator(RegexValidator):
+    message = _("Enter a valid domain name.")
+    ul = "\u00a1-\uffff"  # Unicode letters range (must not be a raw string).
+    # Host patterns.
+    hostname_re = (
+        r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
+    )
+    # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1.
+    domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
+    # Top-level domain.
+    tld_re = (
+        r"\."  # dot
+        r"(?!-)"  # can't start with a dash
+        r"(?:[a-z" + ul + "-]{2,63}"  # domain label
+        r"|xn--[a-z0-9]{1,59})"  # or punycode label
+        r"(?<!-)"  # can't end with a dash
+        r"\.?"  # may have a trailing dot
+    )
+    ascii_only_hostname_re = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
+    ascii_only_domain_re = r"(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*"
+    ascii_only_tld_re = (
+        r"\."  # dot
+        r"(?!-)"  # can't start with a dash
+        r"(?:[a-zA-Z0-9-]{2,63})"  # domain label
+        r"(?<!-)"  # can't end with a dash
+        r"\.?"  # may have a trailing dot
+    )
+
+    max_length = 255
+
+    def __init__(self, **kwargs):
+        self.accept_idna = kwargs.pop("accept_idna", True)
+
+        if self.accept_idna:
+            self.regex = _lazy_re_compile(
+                self.hostname_re + self.domain_re + self.tld_re, re.IGNORECASE
+            )
+        else:
+            self.regex = _lazy_re_compile(
+                self.ascii_only_hostname_re
+                + self.ascii_only_domain_re
+                + self.ascii_only_tld_re,
+                re.IGNORECASE,
+            )
+        super().__init__(**kwargs)
+
+    def __call__(self, value):
+        if not isinstance(value, str) or len(value) > self.max_length:
+            raise ValidationError(self.message, code=self.code, params={"value": value})
+        if not self.accept_idna and not value.isascii():
+            raise ValidationError(self.message, code=self.code, params={"value": value})
+        super().__call__(value)
+
+
+validate_domain_name = DomainNameValidator()
+
+
 @deconstructible
 class URLValidator(RegexValidator):
     ul = "\u00a1-\uffff"  # Unicode letters range (must not be a raw string).
@@ -76,20 +134,10 @@ class URLValidator(RegexValidator):
     )
     ipv6_re = r"\[[0-9a-f:.]+\]"  # (simple regex, validated later)
 
-    # Host patterns
-    hostname_re = (
-        r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
-    )
-    # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
-    domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
-    tld_re = (
-        r"\."  # dot
-        r"(?!-)"  # can't start with a dash
-        r"(?:[a-z" + ul + "-]{2,63}"  # domain label
-        r"|xn--[a-z0-9]{1,59})"  # or punycode label
-        r"(?<!-)"  # can't end with a dash
-        r"\.?"  # may have a trailing dot
-    )
+    hostname_re = DomainNameValidator.hostname_re
+    domain_re = DomainNameValidator.domain_re
+    tld_re = DomainNameValidator.tld_re
+
     host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
 
     regex = _lazy_re_compile(
diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py
index 84b9974b40..7a1dfd30d1 100644
--- a/django/db/backends/base/base.py
+++ b/django/db/backends/base/base.py
@@ -17,7 +17,7 @@ from django.db.backends.base.validation import BaseDatabaseValidation
 from django.db.backends.signals import connection_created
 from django.db.backends.utils import debug_transaction
 from django.db.transaction import TransactionManagementError
-from django.db.utils import DatabaseErrorWrapper
+from django.db.utils import DatabaseErrorWrapper, ProgrammingError
 from django.utils.asyncio import async_unsafe
 from django.utils.functional import cached_property
 
@@ -271,6 +271,10 @@ class BaseDatabaseWrapper:
     def ensure_connection(self):
         """Guarantee that a connection to the database is established."""
         if self.connection is None:
+            if self.in_atomic_block and self.closed_in_transaction:
+                raise ProgrammingError(
+                    "Cannot open a new connection in an atomic block."
+                )
             with self.wrap_database_errors:
                 self.connect()
 
diff --git a/django/db/backends/base/creation.py b/django/db/backends/base/creation.py
index cbac8a7f76..6856fdb596 100644
--- a/django/db/backends/base/creation.py
+++ b/django/db/backends/base/creation.py
@@ -135,7 +135,10 @@ class BaseDatabaseCreation:
                             queryset = model._base_manager.using(
                                 self.connection.alias,
                             ).order_by(model._meta.pk.name)
-                            yield from queryset.iterator()
+                            chunk_size = (
+                                2000 if queryset._prefetch_related_lookups else None
+                            )
+                            yield from queryset.iterator(chunk_size=chunk_size)
 
         # Serialize to a string
         out = StringIO()
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
index 9f40ec5e4f..889e4d87b4 100644
--- a/django/db/backends/base/operations.py
+++ b/django/db/backends/base/operations.py
@@ -276,6 +276,11 @@ class BaseDatabaseOperations:
             if sql
         )
 
+    def bulk_insert_sql(self, fields, placeholder_rows):
+        placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
+        values_sql = ", ".join([f"({sql})" for sql in placeholder_rows_sql])
+        return f"VALUES {values_sql}"
+
     def last_executed_query(self, cursor, sql, params):
         """
         Return a string of the query last executed by the given cursor, with
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py
index 242083b850..e5f28d9c6a 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -3,6 +3,7 @@ import operator
 from datetime import datetime
 
 from django.conf import settings
+from django.core.exceptions import FieldError
 from django.db.backends.ddl_references import (
     Columns,
     Expressions,
@@ -129,7 +130,7 @@ class BaseDatabaseSchemaEditor:
     )
     sql_create_unique_index = (
         "CREATE UNIQUE INDEX %(name)s ON %(table)s "
-        "(%(columns)s)%(include)s%(condition)s%(nulls_distinct)s"
+        "(%(columns)s)%(include)s%(nulls_distinct)s%(condition)s"
     )
     sql_rename_index = "ALTER INDEX %(old_name)s RENAME TO %(new_name)s"
     sql_delete_index = "DROP INDEX %(name)s"
@@ -317,9 +318,9 @@ class BaseDatabaseSchemaEditor:
             if default_value is not None:
                 column_default = "DEFAULT " + self._column_default_sql(field)
                 if self.connection.features.requires_literal_defaults:
-                    # Some databases can't take defaults as a parameter (Oracle).
-                    # If this is the case, the individual schema backend should
-                    # implement prepare_default().
+                    # Some databases can't take defaults as a parameter
+                    # (Oracle, SQLite). If this is the case, the individual
+                    # schema backend should implement prepare_default().
                     yield column_default % self.prepare_default(default_value)
                 else:
                     yield column_default
@@ -333,7 +334,9 @@ class BaseDatabaseSchemaEditor:
         ):
             null = True
         if field.generated:
-            yield self._column_generated_sql(field)
+            generated_sql, generated_params = self._column_generated_sql(field)
+            params.extend(generated_params)
+            yield generated_sql
         elif not null:
             yield "NOT NULL"
         elif not self.connection.features.implied_column_null:
@@ -412,12 +415,15 @@ class BaseDatabaseSchemaEditor:
         """Return the sql and params for the field's database default."""
         from django.db.models.expressions import Value
 
-        sql = "%s" if isinstance(field.db_default, Value) else "(%s)"
+        db_default = field._db_default_expression
+        sql = (
+            self._column_default_sql(field) if isinstance(db_default, Value) else "(%s)"
+        )
         query = Query(model=field.model)
         compiler = query.get_compiler(connection=self.connection)
-        default_sql, params = compiler.compile(field.db_default)
+        default_sql, params = compiler.compile(db_default)
         if self.connection.features.requires_literal_defaults:
-            # Some databases doesn't support parameterized defaults (Oracle,
+            # Some databases don't support parameterized defaults (Oracle,
             # SQLite). If this is the case, the individual schema backend
             # should implement prepare_default().
             default_sql %= tuple(self.prepare_default(p) for p in params)
@@ -428,9 +434,10 @@ class BaseDatabaseSchemaEditor:
         """Return the SQL to use in a GENERATED ALWAYS clause."""
         expression_sql, params = field.generated_sql(self.connection)
         persistency_sql = "STORED" if field.db_persist else "VIRTUAL"
-        if params:
+        if self.connection.features.requires_literal_defaults:
             expression_sql = expression_sql % tuple(self.quote_value(p) for p in params)
-        return f"GENERATED ALWAYS AS ({expression_sql}) {persistency_sql}"
+            params = ()
+        return f"GENERATED ALWAYS AS ({expression_sql}) {persistency_sql}", params
 
     @staticmethod
     def _effective_default(field):
@@ -481,7 +488,7 @@ class BaseDatabaseSchemaEditor:
         """
         sql, params = self.table_sql(model)
         # Prevent using [] as params, in the case a literal '%' is used in the
-        # definition.
+        # definition on backends that don't support parametrized DDL.
         self.execute(sql, params or None)
 
         if self.connection.features.supports_comments:
@@ -743,7 +750,9 @@ class BaseDatabaseSchemaEditor:
             "column": self.quote_name(field.column),
             "definition": definition,
         }
-        self.execute(sql, params)
+        # Prevent using [] as params, in the case a literal '%' is used in the
+        # definition on backends that don't support parametrized DDL.
+        self.execute(sql, params or None)
         # Drop the default if we need to
         if (
             field.db_default is NOT_PROVIDED
@@ -823,6 +832,7 @@ class BaseDatabaseSchemaEditor:
         old_type = old_db_params["type"]
         new_db_params = new_field.db_parameters(connection=self.connection)
         new_type = new_db_params["type"]
+        modifying_generated_field = False
         if (old_type is None and old_field.remote_field is None) or (
             new_type is None and new_field.remote_field is None
         ):
@@ -861,13 +871,19 @@ class BaseDatabaseSchemaEditor:
                 "through= on M2M fields)" % (old_field, new_field)
             )
         elif old_field.generated != new_field.generated or (
-            new_field.generated
-            and (
-                old_field.db_persist != new_field.db_persist
-                or old_field.generated_sql(self.connection)
-                != new_field.generated_sql(self.connection)
-            )
+            new_field.generated and old_field.db_persist != new_field.db_persist
         ):
+            modifying_generated_field = True
+        elif new_field.generated:
+            try:
+                old_field_sql = old_field.generated_sql(self.connection)
+            except FieldError:
+                # Field used in a generated field was renamed.
+                modifying_generated_field = True
+            else:
+                new_field_sql = new_field.generated_sql(self.connection)
+                modifying_generated_field = old_field_sql != new_field_sql
+        if modifying_generated_field:
             raise ValueError(
                 f"Modifying GeneratedFields is not supported - the field {new_field} "
                 "must be removed and re-added with the new definition."
@@ -1566,12 +1582,23 @@ class BaseDatabaseSchemaEditor:
         )
 
     def _delete_index_sql(self, model, name, sql=None):
-        return Statement(
+        statement = Statement(
             sql or self.sql_delete_index,
             table=Table(model._meta.db_table, self.quote_name),
             name=self.quote_name(name),
         )
 
+        # Remove all deferred statements referencing the deleted index.
+        table_name = statement.parts["table"].table
+        index_name = statement.parts["name"]
+        for sql in list(self.deferred_sql):
+            if isinstance(sql, Statement) and sql.references_index(
+                table_name, index_name
+            ):
+                self.deferred_sql.remove(sql)
+
+        return statement
+
     def _rename_index_sql(self, model, old_name, new_name):
         return Statement(
             self.sql_rename_index,
@@ -1636,6 +1663,14 @@ class BaseDatabaseSchemaEditor:
         ):
             old_kwargs.pop("to", None)
             new_kwargs.pop("to", None)
+        # db_default can take many form but result in the same SQL.
+        if (
+            old_kwargs.get("db_default")
+            and new_kwargs.get("db_default")
+            and self.db_default_sql(old_field) == self.db_default_sql(new_field)
+        ):
+            old_kwargs.pop("db_default")
+            new_kwargs.pop("db_default")
         return self.quote_name(old_field.column) != self.quote_name(
             new_field.column
         ) or (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs)
diff --git a/django/db/backends/ddl_references.py b/django/db/backends/ddl_references.py
index 75787ef8ab..cb8d2defd2 100644
--- a/django/db/backends/ddl_references.py
+++ b/django/db/backends/ddl_references.py
@@ -21,6 +21,12 @@ class Reference:
         """
         return False
 
+    def references_index(self, table, index):
+        """
+        Return whether or not this instance references the specified index.
+        """
+        return False
+
     def rename_table_references(self, old_table, new_table):
         """
         Rename all references to the old_name to the new_table.
@@ -52,6 +58,9 @@ class Table(Reference):
     def references_table(self, table):
         return self.table == table
 
+    def references_index(self, table, index):
+        return self.references_table(table) and str(self) == index
+
     def rename_table_references(self, old_table, new_table):
         if self.table == old_table:
             self.table = new_table
@@ -207,6 +216,12 @@ class Statement(Reference):
             for part in self.parts.values()
         )
 
+    def references_index(self, table, index):
+        return any(
+            hasattr(part, "references_index") and part.references_index(table, index)
+            for part in self.parts.values()
+        )
+
     def rename_table_references(self, old_table, new_table):
         for part in self.parts.values():
             if hasattr(part, "rename_table_references"):
diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index cafc6702eb..21088544ac 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -158,6 +158,18 @@ class DatabaseFeatures(BaseDatabaseFeatures):
                     },
                 }
             )
+        if not self.connection.mysql_is_mariadb:
+            skips.update(
+                {
+                    "MySQL doesn't allow renaming columns referenced by generated "
+                    "columns": {
+                        "migrations.test_operations.OperationTests."
+                        "test_invalid_generated_field_changes_on_rename_stored",
+                        "migrations.test_operations.OperationTests."
+                        "test_invalid_generated_field_changes_on_rename_virtual",
+                    },
+                }
+            )
         return skips
 
     @cached_property
diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py
index 7fabb0e05e..9741e6a985 100644
--- a/django/db/backends/mysql/operations.py
+++ b/django/db/backends/mysql/operations.py
@@ -290,11 +290,6 @@ class DatabaseOperations(BaseDatabaseOperations):
     def pk_default_value(self):
         return "NULL"
 
-    def bulk_insert_sql(self, fields, placeholder_rows):
-        placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
-        values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
-        return "VALUES " + values_sql
-
     def combine_expression(self, connector, sub_expressions):
         if connector == "^":
             return "POW(%s)" % ",".join(sub_expressions)
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index f626c4d2d4..5606f90653 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -19,6 +19,7 @@ from django.db.backends.utils import debug_transaction
 from django.utils.asyncio import async_unsafe
 from django.utils.encoding import force_bytes, force_str
 from django.utils.functional import cached_property
+from django.utils.version import get_version_tuple
 
 
 def _setup_environment(environ):
@@ -345,6 +346,10 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         with self.temporary_connection():
             return tuple(int(x) for x in self.connection.version.split("."))
 
+    @cached_property
+    def oracledb_version(self):
+        return get_version_tuple(Database.__version__)
+
 
 class OracleParam:
     """
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index 47bdf37efa..aa657b3ba4 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -1,5 +1,6 @@
 from django.db import DatabaseError, InterfaceError
 from django.db.backends.base.features import BaseDatabaseFeatures
+from django.db.backends.oracle.oracledb_any import is_oracledb
 from django.utils.functional import cached_property
 
 
@@ -78,46 +79,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     supports_slicing_ordering_in_compound = True
     requires_compound_order_by_subquery = True
     allows_multiple_constraints_on_same_fields = False
-    supports_comparing_boolean_expr = False
     supports_json_field_contains = False
     supports_collation_on_textfield = False
     test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
-
-    django_test_skips = {
-        "Oracle doesn't support SHA224.": {
-            "db_functions.text.test_sha224.SHA224Tests.test_basic",
-            "db_functions.text.test_sha224.SHA224Tests.test_transform",
-        },
-        "Oracle doesn't correctly calculate ISO 8601 week numbering before "
-        "1583 (the Gregorian calendar was introduced in 1582).": {
-            "db_functions.datetime.test_extract_trunc.DateFunctionTests."
-            "test_trunc_week_before_1000",
-            "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests."
-            "test_trunc_week_before_1000",
-        },
-        "Oracle doesn't support bitwise XOR.": {
-            "expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor",
-            "expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor_null",
-            "expressions.tests.ExpressionOperatorTests."
-            "test_lefthand_bitwise_xor_right_null",
-        },
-        "Oracle requires ORDER BY in row_number, ANSI:SQL doesn't.": {
-            "expressions_window.tests.WindowFunctionTests.test_row_number_no_ordering",
-        },
-        "Raises ORA-00600: internal error code.": {
-            "model_fields.test_jsonfield.TestQuerying.test_usage_in_subquery",
-        },
-        "Oracle doesn't support changing collations on indexed columns (#33671).": {
-            "migrations.test_operations.OperationTests."
-            "test_alter_field_pk_fk_db_collation",
-        },
-        "Oracle doesn't support comparing NCLOB to NUMBER.": {
-            "generic_relations_regress.tests.GenericRelationTests.test_textlink_filter",
-        },
-        "Oracle doesn't support casting filters to NUMBER.": {
-            "lookup.tests.LookupQueryingTests.test_aggregate_combined_lookup",
-        },
-    }
     django_test_expected_failures = {
         # A bug in Django/oracledb with respect to string handling (#23843).
         "annotations.tests.NonAggregateAnnotationTestCase.test_custom_functions",
@@ -128,6 +92,64 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         "INSERT INTO {} VALUES (DEFAULT, DEFAULT, DEFAULT)"
     )
 
+    @cached_property
+    def django_test_skips(self):
+        skips = {
+            "Oracle doesn't support SHA224.": {
+                "db_functions.text.test_sha224.SHA224Tests.test_basic",
+                "db_functions.text.test_sha224.SHA224Tests.test_transform",
+            },
+            "Oracle doesn't correctly calculate ISO 8601 week numbering before "
+            "1583 (the Gregorian calendar was introduced in 1582).": {
+                "db_functions.datetime.test_extract_trunc.DateFunctionTests."
+                "test_trunc_week_before_1000",
+                "db_functions.datetime.test_extract_trunc."
+                "DateFunctionWithTimeZoneTests.test_trunc_week_before_1000",
+            },
+            "Oracle doesn't support bitwise XOR.": {
+                "expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor",
+                "expressions.tests.ExpressionOperatorTests."
+                "test_lefthand_bitwise_xor_null",
+                "expressions.tests.ExpressionOperatorTests."
+                "test_lefthand_bitwise_xor_right_null",
+            },
+            "Oracle requires ORDER BY in row_number, ANSI:SQL doesn't.": {
+                "expressions_window.tests.WindowFunctionTests."
+                "test_row_number_no_ordering",
+            },
+            "Oracle doesn't support changing collations on indexed columns (#33671).": {
+                "migrations.test_operations.OperationTests."
+                "test_alter_field_pk_fk_db_collation",
+            },
+            "Oracle doesn't support comparing NCLOB to NUMBER.": {
+                "generic_relations_regress.tests.GenericRelationTests."
+                "test_textlink_filter",
+            },
+            "Oracle doesn't support casting filters to NUMBER.": {
+                "lookup.tests.LookupQueryingTests.test_aggregate_combined_lookup",
+            },
+        }
+        if self.connection.oracle_version < (23,):
+            skips.update(
+                {
+                    "Raises ORA-00600 on Oracle < 23c: internal error code.": {
+                        "model_fields.test_jsonfield.TestQuerying."
+                        "test_usage_in_subquery",
+                    },
+                }
+            )
+        if is_oracledb and self.connection.oracledb_version >= (2, 1, 2):
+            skips.update(
+                {
+                    "python-oracledb 2.1.2+ no longer hides 'ORA-1403: no data found' "
+                    "exceptions raised in database triggers.": {
+                        "backends.oracle.tests.TransactionalTests."
+                        "test_hidden_no_data_found_exception"
+                    },
+                },
+            )
+        return skips
+
     @cached_property
     def introspected_field_types(self):
         return {
@@ -174,10 +196,18 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     def supports_boolean_expr_in_select_clause(self):
         return self.connection.oracle_version >= (23,)
 
+    @cached_property
+    def supports_comparing_boolean_expr(self):
+        return self.connection.oracle_version >= (23,)
+
     @cached_property
     def supports_aggregation_over_interval_types(self):
         return self.connection.oracle_version >= (23,)
 
+    @cached_property
+    def supports_bulk_insert_with_multiple_rows(self):
+        return self.connection.oracle_version >= (23,)
+
     @cached_property
     def bare_select_suffix(self):
         return "" if self.connection.oracle_version >= (23,) else " FROM DUAL"
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index 541128ec50..507c5fb369 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -295,6 +295,8 @@ END;
         columns = []
         for param in returning_params:
             value = param.get_value()
+            # Can be removed when cx_Oracle is no longer supported and
+            # python-oracle 2.1.2 becomes the minimum supported version.
             if value == []:
                 raise DatabaseError(
                     "The database did not return a new row id. Probably "
@@ -347,9 +349,10 @@ END;
     def lookup_cast(self, lookup_type, internal_type=None):
         if lookup_type in ("iexact", "icontains", "istartswith", "iendswith"):
             return "UPPER(%s)"
-        if (
-            lookup_type != "isnull" and internal_type in ("BinaryField", "TextField")
-        ) or (lookup_type == "exact" and internal_type == "JSONField"):
+        if lookup_type != "isnull" and internal_type in (
+            "BinaryField",
+            "TextField",
+        ):
             return "DBMS_LOB.SUBSTR(%s)"
         return "%s"
 
@@ -668,18 +671,38 @@ END;
         return self._get_no_autofield_sequence_name(table) if row is None else row[0]
 
     def bulk_insert_sql(self, fields, placeholder_rows):
+        field_placeholders = [
+            BulkInsertMapper.types.get(
+                getattr(field, "target_field", field).get_internal_type(), "%s"
+            )
+            for field in fields
+            if field
+        ]
+        if (
+            self.connection.features.supports_bulk_insert_with_multiple_rows
+            # A workaround with UNION of SELECTs is required for models without
+            # any fields.
+            and field_placeholders
+        ):
+            placeholder_rows_sql = []
+            for row in placeholder_rows:
+                placeholders_row = (
+                    field_placeholder % placeholder
+                    for field_placeholder, placeholder in zip(
+                        field_placeholders, row, strict=True
+                    )
+                )
+                placeholder_rows_sql.append(placeholders_row)
+            return super().bulk_insert_sql(fields, placeholder_rows_sql)
+        # Oracle < 23c doesn't support inserting multiple rows in a single
+        # statement, use UNION of SELECTs as a workaround.
         query = []
         for row in placeholder_rows:
             select = []
             for i, placeholder in enumerate(row):
                 # A model without any fields has fields=[None].
                 if fields[i]:
-                    internal_type = getattr(
-                        fields[i], "target_field", fields[i]
-                    ).get_internal_type()
-                    placeholder = (
-                        BulkInsertMapper.types.get(internal_type, "%s") % placeholder
-                    )
+                    placeholder = field_placeholders[i] % placeholder
                 # Add columns aliases to the first select to avoid "ORA-00918:
                 # column ambiguously defined" when two or more columns in the
                 # first select have the same value.
diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py
index c8dd64650f..0d70522a2a 100644
--- a/django/db/backends/oracle/schema.py
+++ b/django/db/backends/oracle/schema.py
@@ -198,9 +198,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
         return self.normalize_name(for_name + "_" + suffix)
 
     def prepare_default(self, value):
-        # Replace % with %% as %-formatting is applied in
-        # FormatStylePlaceholderCursor._fix_for_params().
-        return self.quote_value(value).replace("%", "%%")
+        return self.quote_value(value)
 
     def _field_should_be_indexed(self, model, field):
         create_index = super()._field_should_be_indexed(model, field)
diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py
index 8349d8f310..e97ab6aa89 100644
--- a/django/db/backends/postgresql/base.py
+++ b/django/db/backends/postgresql/base.py
@@ -13,7 +13,7 @@ from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db import DatabaseError as WrappedDatabaseError
 from django.db import connections
-from django.db.backends.base.base import BaseDatabaseWrapper
+from django.db.backends.base.base import NO_DB_ALIAS, BaseDatabaseWrapper
 from django.db.backends.utils import CursorDebugWrapper as BaseCursorDebugWrapper
 from django.utils.asyncio import async_unsafe
 from django.utils.functional import cached_property
@@ -86,6 +86,24 @@ def _get_varchar_column(data):
     return "varchar(%(max_length)s)" % data
 
 
+def ensure_timezone(connection, ops, timezone_name):
+    conn_timezone_name = connection.info.parameter_status("TimeZone")
+    if timezone_name and conn_timezone_name != timezone_name:
+        with connection.cursor() as cursor:
+            cursor.execute(ops.set_time_zone_sql(), [timezone_name])
+        return True
+    return False
+
+
+def ensure_role(connection, ops, role_name):
+    if role_name:
+        with connection.cursor() as cursor:
+            sql = ops.compose_sql("SET ROLE %s", [role_name])
+            cursor.execute(sql)
+        return True
+    return False
+
+
 class DatabaseWrapper(BaseDatabaseWrapper):
     vendor = "postgresql"
     display_name = "PostgreSQL"
@@ -179,6 +197,53 @@ class DatabaseWrapper(BaseDatabaseWrapper):
     ops_class = DatabaseOperations
     # PostgreSQL backend-specific attributes.
     _named_cursor_idx = 0
+    _connection_pools = {}
+
+    @property
+    def pool(self):
+        pool_options = self.settings_dict["OPTIONS"].get("pool")
+        if self.alias == NO_DB_ALIAS or not pool_options:
+            return None
+
+        if self.alias not in self._connection_pools:
+            if self.settings_dict.get("CONN_MAX_AGE", 0) != 0:
+                raise ImproperlyConfigured(
+                    "Pooling doesn't support persistent connections."
+                )
+            # Set the default options.
+            if pool_options is True:
+                pool_options = {}
+
+            try:
+                from psycopg_pool import ConnectionPool
+            except ImportError as err:
+                raise ImproperlyConfigured(
+                    "Error loading psycopg_pool module.\nDid you install psycopg[pool]?"
+                ) from err
+
+            connect_kwargs = self.get_connection_params()
+            # Ensure we run in autocommit, Django properly sets it later on.
+            connect_kwargs["autocommit"] = True
+            enable_checks = self.settings_dict["CONN_HEALTH_CHECKS"]
+            pool = ConnectionPool(
+                kwargs=connect_kwargs,
+                open=False,  # Do not open the pool during startup.
+                configure=self._configure_connection,
+                check=ConnectionPool.check_connection if enable_checks else None,
+                **pool_options,
+            )
+            # setdefault() ensures that multiple threads don't set this in
+            # parallel. Since we do not open the pool during it's init above,
+            # this means that at worst during startup multiple threads generate
+            # pool objects and the first to set it wins.
+            self._connection_pools.setdefault(self.alias, pool)
+
+        return self._connection_pools[self.alias]
+
+    def close_pool(self):
+        if self.pool:
+            self.pool.close()
+            del self._connection_pools[self.alias]
 
     def get_database_version(self):
         """
@@ -190,9 +255,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
     def get_connection_params(self):
         settings_dict = self.settings_dict
         # None may be used to connect to the default 'postgres' db
-        if settings_dict["NAME"] == "" and not settings_dict.get("OPTIONS", {}).get(
-            "service"
-        ):
+        if settings_dict["NAME"] == "" and not settings_dict["OPTIONS"].get("service"):
             raise ImproperlyConfigured(
                 "settings.DATABASES is improperly configured. "
                 "Please supply the NAME or OPTIONS['service'] value."
@@ -215,7 +278,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
             }
         elif settings_dict["NAME"] is None:
             # Connect to the default 'postgres' db.
-            settings_dict.get("OPTIONS", {}).pop("service", None)
+            settings_dict["OPTIONS"].pop("service", None)
             conn_params = {"dbname": "postgres", **settings_dict["OPTIONS"]}
         else:
             conn_params = {**settings_dict["OPTIONS"]}
@@ -223,6 +286,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
 
         conn_params.pop("assume_role", None)
         conn_params.pop("isolation_level", None)
+
+        pool_options = conn_params.pop("pool", None)
+        if pool_options and not is_psycopg3:
+            raise ImproperlyConfigured("Database pooling requires psycopg >= 3")
+
         server_side_binding = conn_params.pop("server_side_binding", None)
         conn_params.setdefault(
             "cursor_factory",
@@ -274,7 +342,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
                     f"Invalid transaction isolation level {isolation_level_value} "
                     f"specified. Use one of the psycopg.IsolationLevel values."
                 )
-        connection = self.Database.connect(**conn_params)
+        if self.pool:
+            # If nothing else has opened the pool, open it now.
+            self.pool.open()
+            connection = self.pool.getconn()
+        else:
+            connection = self.Database.connect(**conn_params)
         if set_isolation_level:
             connection.isolation_level = self.isolation_level
         if not is_psycopg3:
@@ -287,45 +360,58 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         return connection
 
     def ensure_timezone(self):
+        # Close the pool so new connections pick up the correct timezone.
+        self.close_pool()
         if self.connection is None:
             return False
-        conn_timezone_name = self.connection.info.parameter_status("TimeZone")
-        timezone_name = self.timezone_name
-        if timezone_name and conn_timezone_name != timezone_name:
-            with self.connection.cursor() as cursor:
-                cursor.execute(self.ops.set_time_zone_sql(), [timezone_name])
-            return True
-        return False
+        return ensure_timezone(self.connection, self.ops, self.timezone_name)
 
-    def ensure_role(self):
-        if self.connection is None:
-            return False
-        if new_role := self.settings_dict.get("OPTIONS", {}).get("assume_role"):
-            with self.connection.cursor() as cursor:
-                sql = self.ops.compose_sql("SET ROLE %s", [new_role])
-                cursor.execute(sql)
-            return True
-        return False
+    def _configure_connection(self, connection):
+        # This function is called from init_connection_state and from the
+        # psycopg pool itself after a connection is opened. Make sure that
+        # whatever is done here does not access anything on self aside from
+        # variables.
+
+        # Commit after setting the time zone.
+        commit_tz = ensure_timezone(connection, self.ops, self.timezone_name)
+        # Set the role on the connection. This is useful if the credential used
+        # to login is not the same as the role that owns database resources. As
+        # can be the case when using temporary or ephemeral credentials.
+        role_name = self.settings_dict["OPTIONS"].get("assume_role")
+        commit_role = ensure_role(connection, self.ops, role_name)
+
+        return commit_role or commit_tz
+
+    def _close(self):
+        if self.connection is not None:
+            # `wrap_database_errors` only works for `putconn` as long as there
+            # is no `reset` function set in the pool because it is deferred
+            # into a thread and not directly executed.
+            with self.wrap_database_errors:
+                if self.pool:
+                    # Ensure the correct pool is returned. This is a workaround
+                    # for tests so a pool can be changed on setting changes
+                    # (e.g. USE_TZ, TIME_ZONE).
+                    self.connection._pool.putconn(self.connection)
+                    # Connection can no longer be used.
+                    self.connection = None
+                else:
+                    return self.connection.close()
 
     def init_connection_state(self):
         super().init_connection_state()
 
-        # Commit after setting the time zone.
-        commit_tz = self.ensure_timezone()
-        # Set the role on the connection. This is useful if the credential used
-        # to login is not the same as the role that owns database resources. As
-        # can be the case when using temporary or ephemeral credentials.
-        commit_role = self.ensure_role()
+        if self.connection is not None and not self.pool:
+            commit = self._configure_connection(self.connection)
 
-        if (commit_role or commit_tz) and not self.get_autocommit():
-            self.connection.commit()
+            if commit and not self.get_autocommit():
+                self.connection.commit()
 
     @async_unsafe
     def create_cursor(self, name=None):
         if name:
             if is_psycopg3 and (
-                self.settings_dict.get("OPTIONS", {}).get("server_side_binding")
-                is not True
+                self.settings_dict["OPTIONS"].get("server_side_binding") is not True
             ):
                 # psycopg >= 3 forces the usage of server-side bindings for
                 # named cursors so a specialized class that implements
@@ -401,6 +487,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
             cursor.execute("SET CONSTRAINTS ALL DEFERRED")
 
     def is_usable(self):
+        if self.connection is None:
+            return False
         try:
             # Use a psycopg cursor directly, bypassing Django's utilities.
             with self.connection.cursor() as cursor:
@@ -410,6 +498,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         else:
             return True
 
+    def close_if_health_check_failed(self):
+        if self.pool:
+            # The pool only returns healthy connections.
+            return
+        return super().close_if_health_check_failed()
+
     @contextmanager
     def _nodb_cursor(self):
         cursor = None
diff --git a/django/db/backends/postgresql/client.py b/django/db/backends/postgresql/client.py
index 3b5ddafaca..4d79869e87 100644
--- a/django/db/backends/postgresql/client.py
+++ b/django/db/backends/postgresql/client.py
@@ -9,7 +9,7 @@ class DatabaseClient(BaseDatabaseClient):
     @classmethod
     def settings_to_cmd_args_env(cls, settings_dict, parameters):
         args = [cls.executable_name]
-        options = settings_dict.get("OPTIONS", {})
+        options = settings_dict["OPTIONS"]
 
         host = settings_dict.get("HOST")
         port = settings_dict.get("PORT")
diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py
index 9b562cec18..938be0f56f 100644
--- a/django/db/backends/postgresql/creation.py
+++ b/django/db/backends/postgresql/creation.py
@@ -58,6 +58,7 @@ class DatabaseCreation(BaseDatabaseCreation):
         # CREATE DATABASE ... WITH TEMPLATE ... requires closing connections
         # to the template database.
         self.connection.close()
+        self.connection.close_pool()
 
         source_database_name = self.connection.settings_dict["NAME"]
         target_database_name = self.get_test_db_clone_settings(suffix)["NAME"]
@@ -84,3 +85,7 @@ class DatabaseCreation(BaseDatabaseCreation):
                 except Exception as e:
                     self.log("Got an error cloning the test database: %s" % e)
                     sys.exit(2)
+
+    def _destroy_test_db(self, test_database_name, verbosity):
+        self.connection.close_pool()
+        return super()._destroy_test_db(test_database_name, verbosity)
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 7bcc356407..6170b5501a 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -7,7 +7,7 @@ from django.utils.functional import cached_property
 
 
 class DatabaseFeatures(BaseDatabaseFeatures):
-    minimum_database_version = (13,)
+    minimum_database_version = (14,)
     allows_group_by_selected_pks = True
     can_return_columns_from_insert = True
     can_return_rows_from_bulk_insert = True
@@ -83,15 +83,38 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     test_now_utc_template = "STATEMENT_TIMESTAMP() AT TIME ZONE 'UTC'"
     insert_test_table_with_defaults = "INSERT INTO {} DEFAULT VALUES"
 
-    django_test_skips = {
-        "opclasses are PostgreSQL only.": {
-            "indexes.tests.SchemaIndexesNotPostgreSQLTests."
-            "test_create_index_ignores_opclasses",
-        },
-        "PostgreSQL requires casting to text.": {
-            "lookup.tests.LookupTests.test_textfield_exact_null",
-        },
-    }
+    @cached_property
+    def django_test_skips(self):
+        skips = {
+            "opclasses are PostgreSQL only.": {
+                "indexes.tests.SchemaIndexesNotPostgreSQLTests."
+                "test_create_index_ignores_opclasses",
+            },
+            "PostgreSQL requires casting to text.": {
+                "lookup.tests.LookupTests.test_textfield_exact_null",
+            },
+        }
+        if self.connection.settings_dict["OPTIONS"].get("pool"):
+            skips.update(
+                {
+                    "Pool does implicit health checks": {
+                        "backends.base.test_base.ConnectionHealthChecksTests."
+                        "test_health_checks_enabled",
+                        "backends.base.test_base.ConnectionHealthChecksTests."
+                        "test_set_autocommit_health_checks_enabled",
+                    },
+                }
+            )
+        if self.uses_server_side_binding:
+            skips.update(
+                {
+                    "The actual query cannot be determined for server side bindings": {
+                        "backends.base.test_base.ExecuteWrapperTests."
+                        "test_wrapper_debug",
+                    }
+                },
+            )
+        return skips
 
     @cached_property
     def django_test_expected_failures(self):
@@ -129,10 +152,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
             "PositiveSmallIntegerField": "SmallIntegerField",
         }
 
-    @cached_property
-    def is_postgresql_14(self):
-        return self.connection.pg_version >= 140000
-
     @cached_property
     def is_postgresql_15(self):
         return self.connection.pg_version >= 150000
@@ -141,8 +160,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     def is_postgresql_16(self):
         return self.connection.pg_version >= 160000
 
-    has_bit_xor = property(operator.attrgetter("is_postgresql_14"))
-    supports_covering_spgist_indexes = property(operator.attrgetter("is_postgresql_14"))
     supports_unlimited_charfield = True
     supports_nulls_distinct_unique_constraints = property(
         operator.attrgetter("is_postgresql_15")
diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py
index 06981bc094..4b179ca83f 100644
--- a/django/db/backends/postgresql/operations.py
+++ b/django/db/backends/postgresql/operations.py
@@ -296,9 +296,14 @@ class DatabaseOperations(BaseDatabaseOperations):
     if is_psycopg3:
 
         def last_executed_query(self, cursor, sql, params):
-            try:
-                return self.compose_sql(sql, params)
-            except errors.DataError:
+            if self.connection.features.uses_server_side_binding:
+                try:
+                    return self.compose_sql(sql, params)
+                except errors.DataError:
+                    return None
+            else:
+                if cursor._query and cursor._query.query is not None:
+                    return cursor._query.query.decode()
                 return None
 
     else:
@@ -323,11 +328,6 @@ class DatabaseOperations(BaseDatabaseOperations):
         ]
         return "RETURNING %s" % ", ".join(columns), ()
 
-    def bulk_insert_sql(self, fields, placeholder_rows):
-        placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
-        values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
-        return "VALUES " + values_sql
-
     if is_psycopg3:
 
         def adapt_integerfield_value(self, value, internal_type):
diff --git a/django/db/backends/postgresql/psycopg_any.py b/django/db/backends/postgresql/psycopg_any.py
index 1fe6b15caf..700fc6ec83 100644
--- a/django/db/backends/postgresql/psycopg_any.py
+++ b/django/db/backends/postgresql/psycopg_any.py
@@ -75,9 +75,15 @@ except ImportError:
     from enum import IntEnum
 
     from psycopg2 import errors, extensions, sql  # NOQA
-    from psycopg2.extras import DateRange, DateTimeRange, DateTimeTZRange, Inet  # NOQA
-    from psycopg2.extras import Json as Jsonb  # NOQA
-    from psycopg2.extras import NumericRange, Range  # NOQA
+    from psycopg2.extras import (  # NOQA
+        DateRange,
+        DateTimeRange,
+        DateTimeTZRange,
+        Inet,
+        Json,
+        NumericRange,
+        Range,
+    )
 
     RANGE_TYPES = (DateRange, DateTimeRange, DateTimeTZRange, NumericRange)
 
@@ -101,3 +107,8 @@ except ImportError:
             return cursor.mogrify(sql, params).decode()
 
     is_psycopg3 = False
+
+    class Jsonb(Json):
+        def getquoted(self):
+            quoted = super().getquoted()
+            return quoted + b"::jsonb"
diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py
index 842830be30..0c8548a5d6 100644
--- a/django/db/backends/postgresql/schema.py
+++ b/django/db/backends/postgresql/schema.py
@@ -255,25 +255,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
                 model, old_field, new_field, new_type, old_collation, new_collation
             )
 
-    def _alter_column_collation_sql(
-        self, model, new_field, new_type, new_collation, old_field
-    ):
-        sql = self.sql_alter_column_collate
-        # Cast when data type changed.
-        if using_sql := self._using_sql(new_field, old_field):
-            sql += using_sql
-        return (
-            sql
-            % {
-                "column": self.quote_name(new_field.column),
-                "type": new_type,
-                "collation": (
-                    " " + self._collate_sql(new_collation) if new_collation else ""
-                ),
-            },
-            [],
-        )
-
     def _alter_field(
         self,
         model,
diff --git a/django/db/backends/sqlite3/_functions.py b/django/db/backends/sqlite3/_functions.py
index 0171b60f38..6d07d3d78b 100644
--- a/django/db/backends/sqlite3/_functions.py
+++ b/django/db/backends/sqlite3/_functions.py
@@ -118,7 +118,10 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None):
             hours, minutes = offset.split(":")
             offset_delta = timedelta(hours=int(hours), minutes=int(minutes))
             dt += offset_delta if sign == "+" else -offset_delta
-        dt = timezone.localtime(dt, zoneinfo.ZoneInfo(tzname))
+        # The tzname may originally be just the offset e.g. "+3:00",
+        # which becomes an empty string after splitting the sign and offset.
+        # In this case, use the conn_tzname as fallback.
+        dt = timezone.localtime(dt, zoneinfo.ZoneInfo(tzname or conn_tzname))
     return dt
 
 
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 8e17ea3d44..c7cf947800 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -187,6 +187,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
                 f"{allowed_transaction_modes}, or None."
             )
         self.transaction_mode = transaction_mode.upper() if transaction_mode else None
+
+        init_command = kwargs.pop("init_command", "")
+        self.init_commands = init_command.split(";")
         return kwargs
 
     def get_database_version(self):
@@ -201,6 +204,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         # The macOS bundled SQLite defaults legacy_alter_table ON, which
         # prevents atomic table renames.
         conn.execute("PRAGMA legacy_alter_table = OFF")
+        for init_command in self.init_commands:
+            if init_command := init_command.strip():
+                conn.execute(init_command)
         return conn
 
     def create_cursor(self, name=None):
diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py
index 713d8bd38f..502d8546e3 100644
--- a/django/db/backends/sqlite3/features.py
+++ b/django/db/backends/sqlite3/features.py
@@ -9,7 +9,7 @@ from .base import Database
 
 
 class DatabaseFeatures(BaseDatabaseFeatures):
-    minimum_database_version = (3, 27)
+    minimum_database_version = (3, 31)
     test_db_allows_multiple_connections = False
     supports_unspecified_pk = True
     supports_timezones = False
@@ -31,18 +31,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     supports_parentheses_in_compound = False
     can_defer_constraint_checks = True
     supports_over_clause = True
-    supports_frame_range_fixed_distance = Database.sqlite_version_info >= (3, 28, 0)
-    supports_frame_exclusion = Database.sqlite_version_info >= (3, 28, 0)
-    supports_aggregate_filter_clause = Database.sqlite_version_info >= (3, 30, 1)
-    supports_order_by_nulls_modifier = Database.sqlite_version_info >= (3, 30, 0)
-    # NULLS LAST/FIRST emulation on < 3.30 requires subquery wrapping.
-    requires_compound_order_by_subquery = Database.sqlite_version_info < (3, 30)
+    supports_frame_range_fixed_distance = True
+    supports_frame_exclusion = True
+    supports_aggregate_filter_clause = True
     order_by_nulls_first = True
     supports_json_field_contains = False
     supports_update_conflicts = True
     supports_update_conflicts_with_target = True
-    supports_stored_generated_columns = Database.sqlite_version_info >= (3, 31, 0)
-    supports_virtual_generated_columns = Database.sqlite_version_info >= (3, 31, 0)
+    supports_stored_generated_columns = True
+    supports_virtual_generated_columns = True
     test_collations = {
         "ci": "nocase",
         "cs": "binary",
@@ -89,6 +86,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
                 "db_functions.math.test_round.RoundTests."
                 "test_integer_with_negative_precision",
             },
+            "The actual query cannot be determined on SQLite": {
+                "backends.base.test_base.ExecuteWrapperTests.test_wrapper_debug",
+            },
         }
         if self.connection.is_in_memory_db():
             skips.update(
diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py
index 29a5c0391e..0078cc077a 100644
--- a/django/db/backends/sqlite3/operations.py
+++ b/django/db/backends/sqlite3/operations.py
@@ -353,11 +353,6 @@ class DatabaseOperations(BaseDatabaseOperations):
     def convert_booleanfield_value(self, value, expression, connection):
         return bool(value) if value in (1, 0) else value
 
-    def bulk_insert_sql(self, fields, placeholder_rows):
-        placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
-        values_sql = ", ".join(f"({sql})" for sql in placeholder_rows_sql)
-        return f"VALUES {values_sql}"
-
     def combine_expression(self, connector, sub_expressions):
         # SQLite doesn't have a ^ operator, so use the user-defined POWER
         # function that's registered in connect().
diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py
index d27a8bbd65..c5b428fc67 100644
--- a/django/db/backends/sqlite3/schema.py
+++ b/django/db/backends/sqlite3/schema.py
@@ -150,6 +150,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
             body.pop(old_field.name, None)
             mapping.pop(old_field.column, None)
             body[new_field.name] = new_field
+            rename_mapping[old_field.name] = new_field.name
+            if new_field.generated:
+                continue
             if old_field.null and not new_field.null:
                 if new_field.db_default is NOT_PROVIDED:
                     default = self.prepare_default(self.effective_default(new_field))
@@ -162,7 +165,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
                 mapping[new_field.column] = case_sql
             else:
                 mapping[new_field.column] = self.quote_name(old_field.column)
-            rename_mapping[old_field.name] = new_field.name
         # Remove any deleted fields
         if delete_field:
             del body[delete_field.name]
@@ -229,6 +231,14 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
         body_copy["__module__"] = model.__module__
         new_model = type("New%s" % model._meta.object_name, model.__bases__, body_copy)
 
+        # Remove the automatically recreated default primary key, if it has
+        # been deleted.
+        if delete_field and delete_field.attname == new_model._meta.pk.attname:
+            auto_pk = new_model._meta.pk
+            delattr(new_model, auto_pk.attname)
+            new_model._meta.local_fields.remove(auto_pk)
+            new_model.pk = None
+
         # Create a new table with the updated schema.
         self.create_model(new_model)
 
diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py
index f363253ca3..ab0ea8258b 100644
--- a/django/db/backends/utils.py
+++ b/django/db/backends/utils.py
@@ -200,6 +200,8 @@ def split_tzname_delta(tzname):
         if sign in tzname:
             name, offset = tzname.rsplit(sign, 1)
             if offset and parse_time(offset):
+                if ":" not in offset:
+                    offset = f"{offset}:00"
                 return name, sign, offset
     return tzname, None, None
 
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index f000d9fcaa..353b992258 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -1126,6 +1126,8 @@ class MigrationAutodetector:
                     self.to_state,
                 )
             )
+        if field.generated:
+            dependencies.extend(self._get_dependencies_for_generated_field(field))
         # You can't just add NOT NULL fields with no default or fields
         # which don't allow empty strings as default.
         time_fields = (models.DateField, models.DateTimeField, models.TimeField)
@@ -1547,6 +1549,27 @@ class MigrationAutodetector:
             )
         return dependencies
 
+    def _get_dependencies_for_generated_field(self, field):
+        dependencies = []
+        referenced_base_fields = models.Q(field.expression).referenced_base_fields
+        newly_added_fields = sorted(self.new_field_keys - self.old_field_keys)
+        for app_label, model_name, added_field_name in newly_added_fields:
+            added_field = self.to_state.models[app_label, model_name].get_field(
+                added_field_name
+            )
+            if (
+                added_field.remote_field and added_field.remote_field.model
+            ) or added_field.name in referenced_base_fields:
+                dependencies.append(
+                    OperationDependency(
+                        app_label,
+                        model_name,
+                        added_field.name,
+                        OperationDependency.Type.CREATE,
+                    )
+                )
+        return dependencies
+
     def _get_dependencies_for_model(self, app_label, model_name):
         """Return foreign key dependencies of the given model."""
         dependencies = []
diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py
index eb738cf457..13afa5988f 100644
--- a/django/db/migrations/executor.py
+++ b/django/db/migrations/executor.py
@@ -224,6 +224,9 @@ class MigrationExecutor:
         # remaining applied migrations.
         last_unapplied_migration = plan[-1][0]
         state = states[last_unapplied_migration]
+        # Avoid mutating state with apps rendered as it's an expensive
+        # operation.
+        del state.apps
         for index, (migration, _) in enumerate(full_plan):
             if migration == last_unapplied_migration:
                 for migration, _ in full_plan[index:]:
diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py
index 38c68f3ff3..9aad9c809e 100644
--- a/django/db/migrations/operations/models.py
+++ b/django/db/migrations/operations/models.py
@@ -342,6 +342,40 @@ class CreateModel(ModelOperation):
                         managers=self.managers,
                     ),
                 ]
+            elif isinstance(operation, AddConstraint):
+                return [
+                    CreateModel(
+                        self.name,
+                        fields=self.fields,
+                        options={
+                            **self.options,
+                            "constraints": [
+                                *self.options.get("constraints", []),
+                                operation.constraint,
+                            ],
+                        },
+                        bases=self.bases,
+                        managers=self.managers,
+                    ),
+                ]
+            elif isinstance(operation, RemoveConstraint):
+                options_constraints = [
+                    constraint
+                    for constraint in self.options.get("constraints", [])
+                    if constraint.name != operation.name
+                ]
+                return [
+                    CreateModel(
+                        self.name,
+                        fields=self.fields,
+                        options={
+                            **self.options,
+                            "constraints": options_constraints,
+                        },
+                        bases=self.bases,
+                        managers=self.managers,
+                    ),
+                ]
         return super().reduce(operation, app_label)
 
 
@@ -426,11 +460,11 @@ class RenameModel(ModelOperation):
                     model = new_model
                     related_key = (app_label, self.new_name_lower)
                 else:
-                    model = related_object.related_model
                     related_key = (
                         related_object.related_model._meta.app_label,
                         related_object.related_model._meta.model_name,
                     )
+                    model = to_state.apps.get_model(*related_key)
                 to_field = to_state.apps.get_model(*related_key)._meta.get_field(
                     related_object.field.name
                 )
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 3923cea591..fe81d92d36 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -46,7 +46,12 @@ from django.db.models.indexes import *  # NOQA
 from django.db.models.indexes import __all__ as indexes_all
 from django.db.models.lookups import Lookup, Transform
 from django.db.models.manager import Manager
-from django.db.models.query import Prefetch, QuerySet, prefetch_related_objects
+from django.db.models.query import (
+    Prefetch,
+    QuerySet,
+    aprefetch_related_objects,
+    prefetch_related_objects,
+)
 from django.db.models.query_utils import FilteredRelation, Q
 
 # Imports that would create circular imports if sorted
@@ -104,6 +109,7 @@ __all__ += [
     "Prefetch",
     "Q",
     "QuerySet",
+    "aprefetch_related_objects",
     "prefetch_related_objects",
     "DEFERRED",
     "Model",
diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py
index 0cbffacd1b..bf94decab7 100644
--- a/django/db/models/aggregates.py
+++ b/django/db/models/aggregates.py
@@ -50,12 +50,10 @@ class Aggregate(Func):
 
     def get_source_expressions(self):
         source_expressions = super().get_source_expressions()
-        if self.filter:
-            return source_expressions + [self.filter]
-        return source_expressions
+        return source_expressions + [self.filter]
 
     def set_source_expressions(self, exprs):
-        self.filter = self.filter and exprs.pop()
+        *exprs, self.filter = exprs
         return super().set_source_expressions(exprs)
 
     def resolve_expression(
@@ -63,8 +61,10 @@ class Aggregate(Func):
     ):
         # Aggregates are not allowed in UPDATE queries, so ignore for_save
         c = super().resolve_expression(query, allow_joins, reuse, summarize)
-        c.filter = c.filter and c.filter.resolve_expression(
-            query, allow_joins, reuse, summarize
+        c.filter = (
+            c.filter.resolve_expression(query, allow_joins, reuse, summarize)
+            if c.filter
+            else None
         )
         if summarize:
             # Summarized aggregates cannot refer to summarized aggregates.
@@ -104,7 +104,9 @@ class Aggregate(Func):
 
     @property
     def default_alias(self):
-        expressions = self.get_source_expressions()
+        expressions = [
+            expr for expr in self.get_source_expressions() if expr is not None
+        ]
         if len(expressions) == 1 and hasattr(expressions[0], "name"):
             return "%s__%s" % (expressions[0].name, self.name.lower())
         raise TypeError("Complex expressions require an alias")
diff --git a/django/db/models/base.py b/django/db/models/base.py
index c1f78d0632..cd300e47bc 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -28,9 +28,8 @@ from django.db import (
 )
 from django.db.models import NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value
 from django.db.models.constants import LOOKUP_SEP
-from django.db.models.constraints import CheckConstraint, UniqueConstraint
 from django.db.models.deletion import CASCADE, Collector
-from django.db.models.expressions import RawSQL
+from django.db.models.expressions import DatabaseDefault
 from django.db.models.fields.related import (
     ForeignObjectRel,
     OneToOneField,
@@ -692,8 +691,8 @@ class Model(AltersData, metaclass=ModelBase):
             self._prefetched_objects_cache = {}
         else:
             prefetched_objects_cache = getattr(self, "_prefetched_objects_cache", ())
-            fields = list(fields)
-            for field in list(fields):
+            fields = set(fields)
+            for field in fields.copy():
                 if field in prefetched_objects_cache:
                     del prefetched_objects_cache[field]
                     fields.remove(field)
@@ -718,11 +717,11 @@ class Model(AltersData, metaclass=ModelBase):
         if fields is not None:
             db_instance_qs = db_instance_qs.only(*fields)
         elif deferred_fields:
-            fields = [
+            fields = {
                 f.attname
                 for f in self._meta.concrete_fields
                 if f.attname not in deferred_fields
-            ]
+            }
             db_instance_qs = db_instance_qs.only(*fields)
 
         db_instance = db_instance_qs.get()
@@ -741,12 +740,16 @@ class Model(AltersData, metaclass=ModelBase):
 
         # Clear cached relations.
         for field in self._meta.related_objects:
-            if field.is_cached(self):
+            if (fields is None or field.name in fields) and field.is_cached(self):
                 field.delete_cached_value(self)
 
         # Clear cached private relations.
         for field in self._meta.private_fields:
-            if field.is_relation and field.is_cached(self):
+            if (
+                (fields is None or field.name in fields)
+                and field.is_relation
+                and field.is_cached(self)
+            ):
                 field.delete_cached_value(self)
 
         self._state.db = db_instance._state.db
@@ -1060,12 +1063,16 @@ class Model(AltersData, metaclass=ModelBase):
         for a single table.
         """
         meta = cls._meta
-        non_pks = [f for f in meta.local_concrete_fields if not f.primary_key]
+        non_pks_non_generated = [
+            f
+            for f in meta.local_concrete_fields
+            if not f.primary_key and not f.generated
+        ]
 
         if update_fields:
-            non_pks = [
+            non_pks_non_generated = [
                 f
-                for f in non_pks
+                for f in non_pks_non_generated
                 if f.name in update_fields or f.attname in update_fields
             ]
 
@@ -1081,6 +1088,7 @@ class Model(AltersData, metaclass=ModelBase):
         if (
             not raw
             and not force_insert
+            and not force_update
             and self._state.adding
             and (
                 (meta.pk.default and meta.pk.default is not NOT_PROVIDED)
@@ -1097,7 +1105,7 @@ class Model(AltersData, metaclass=ModelBase):
                     None,
                     (getattr(self, f.attname) if raw else f.pre_save(self, False)),
                 )
-                for f in non_pks
+                for f in non_pks_non_generated
             ]
             forced_update = update_fields or force_update
             updated = self._do_update(
@@ -1368,7 +1376,7 @@ class Model(AltersData, metaclass=ModelBase):
         constraints = []
         if include_meta_constraints:
             constraints = [(self.__class__, self._meta.total_unique_constraints)]
-        for parent_class in self._meta.get_parent_list():
+        for parent_class in self._meta.all_parents:
             if parent_class._meta.unique_together:
                 unique_togethers.append(
                     (parent_class, parent_class._meta.unique_together)
@@ -1397,7 +1405,7 @@ class Model(AltersData, metaclass=ModelBase):
         # the list of checks.
 
         fields_with_class = [(self.__class__, self._meta.local_fields)]
-        for parent_class in self._meta.get_parent_list():
+        for parent_class in self._meta.all_parents:
             fields_with_class.append((parent_class, parent_class._meta.local_fields))
 
         for model_class, fields in fields_with_class:
@@ -1546,7 +1554,7 @@ class Model(AltersData, metaclass=ModelBase):
 
     def get_constraints(self):
         constraints = [(self.__class__, self._meta.constraints)]
-        for parent_class in self._meta.get_parent_list():
+        for parent_class in self._meta.all_parents:
             if parent_class._meta.constraints:
                 constraints.append((parent_class, parent_class._meta.constraints))
         return constraints
@@ -1635,6 +1643,9 @@ class Model(AltersData, metaclass=ModelBase):
             raw_value = getattr(self, f.attname)
             if f.blank and raw_value in f.empty_values:
                 continue
+            # Skip validation for empty fields when db_default is used.
+            if isinstance(raw_value, DatabaseDefault):
+                continue
             try:
                 setattr(self, f.attname, f.clean(raw_value, self))
             except ValidationError as e:
@@ -1855,7 +1866,7 @@ class Model(AltersData, metaclass=ModelBase):
         used_fields = {}  # name or attname -> field
 
         # Check that multi-inheritance doesn't cause field name shadowing.
-        for parent in cls._meta.get_parent_list():
+        for parent in cls._meta.all_parents:
             for f in parent._meta.local_fields:
                 clash = used_fields.get(f.name) or used_fields.get(f.attname) or None
                 if clash:
@@ -1875,7 +1886,7 @@ class Model(AltersData, metaclass=ModelBase):
         # Check that fields defined in the model don't clash with fields from
         # parents, including auto-generated fields like multi-table inheritance
         # child accessors.
-        for parent in cls._meta.get_parent_list():
+        for parent in cls._meta.all_parents:
             for f in parent._meta.get_fields():
                 if f not in used_fields:
                     used_fields[f.name] = f
@@ -1925,7 +1936,7 @@ class Model(AltersData, metaclass=ModelBase):
         errors = []
 
         for f in cls._meta.local_fields:
-            _, column_name = f.get_attname_column()
+            column_name = f.column
 
             # Ensure the column name is not already in use.
             if column_name and column_name in used_column_names:
@@ -1972,7 +1983,7 @@ class Model(AltersData, metaclass=ModelBase):
         errors = []
         property_names = cls._meta._property_names
         related_field_accessors = (
-            f.get_attname()
+            f.attname
             for f in cls._meta._get_fields(reverse=False)
             if f.is_relation and f.related_model is not None
         )
@@ -2320,13 +2331,11 @@ class Model(AltersData, metaclass=ModelBase):
             return errors
 
         for f in cls._meta.local_fields:
-            _, column_name = f.get_attname_column()
-
             # Check if auto-generated name for the field is too long
             # for the database.
             if (
                 f.db_column is None
-                and column_name is not None
+                and (column_name := f.column) is not None
                 and len(column_name) > allowed_len
             ):
                 errors.append(
@@ -2348,10 +2357,9 @@ class Model(AltersData, metaclass=ModelBase):
             # Check if auto-generated name for the M2M field is too long
             # for the database.
             for m2m in f.remote_field.through._meta.local_fields:
-                _, rel_name = m2m.get_attname_column()
                 if (
                     m2m.db_column is None
-                    and rel_name is not None
+                    and (rel_name := m2m.column) is not None
                     and len(rel_name) > allowed_len
                 ):
                     errors.append(
@@ -2393,213 +2401,8 @@ class Model(AltersData, metaclass=ModelBase):
             if not router.allow_migrate_model(db, cls):
                 continue
             connection = connections[db]
-            if not (
-                connection.features.supports_table_check_constraints
-                or "supports_table_check_constraints" in cls._meta.required_db_features
-            ) and any(
-                isinstance(constraint, CheckConstraint)
-                for constraint in cls._meta.constraints
-            ):
-                errors.append(
-                    checks.Warning(
-                        "%s does not support check constraints."
-                        % connection.display_name,
-                        hint=(
-                            "A constraint won't be created. Silence this "
-                            "warning if you don't care about it."
-                        ),
-                        obj=cls,
-                        id="models.W027",
-                    )
-                )
-            if not (
-                connection.features.supports_partial_indexes
-                or "supports_partial_indexes" in cls._meta.required_db_features
-            ) and any(
-                isinstance(constraint, UniqueConstraint)
-                and constraint.condition is not None
-                for constraint in cls._meta.constraints
-            ):
-                errors.append(
-                    checks.Warning(
-                        "%s does not support unique constraints with "
-                        "conditions." % connection.display_name,
-                        hint=(
-                            "A constraint won't be created. Silence this "
-                            "warning if you don't care about it."
-                        ),
-                        obj=cls,
-                        id="models.W036",
-                    )
-                )
-            if not (
-                connection.features.supports_deferrable_unique_constraints
-                or "supports_deferrable_unique_constraints"
-                in cls._meta.required_db_features
-            ) and any(
-                isinstance(constraint, UniqueConstraint)
-                and constraint.deferrable is not None
-                for constraint in cls._meta.constraints
-            ):
-                errors.append(
-                    checks.Warning(
-                        "%s does not support deferrable unique constraints."
-                        % connection.display_name,
-                        hint=(
-                            "A constraint won't be created. Silence this "
-                            "warning if you don't care about it."
-                        ),
-                        obj=cls,
-                        id="models.W038",
-                    )
-                )
-            if not (
-                connection.features.supports_covering_indexes
-                or "supports_covering_indexes" in cls._meta.required_db_features
-            ) and any(
-                isinstance(constraint, UniqueConstraint) and constraint.include
-                for constraint in cls._meta.constraints
-            ):
-                errors.append(
-                    checks.Warning(
-                        "%s does not support unique constraints with non-key "
-                        "columns." % connection.display_name,
-                        hint=(
-                            "A constraint won't be created. Silence this "
-                            "warning if you don't care about it."
-                        ),
-                        obj=cls,
-                        id="models.W039",
-                    )
-                )
-            if not (
-                connection.features.supports_expression_indexes
-                or "supports_expression_indexes" in cls._meta.required_db_features
-            ) and any(
-                isinstance(constraint, UniqueConstraint)
-                and constraint.contains_expressions
-                for constraint in cls._meta.constraints
-            ):
-                errors.append(
-                    checks.Warning(
-                        "%s does not support unique constraints on "
-                        "expressions." % connection.display_name,
-                        hint=(
-                            "A constraint won't be created. Silence this "
-                            "warning if you don't care about it."
-                        ),
-                        obj=cls,
-                        id="models.W044",
-                    )
-                )
-            if not (
-                connection.features.supports_nulls_distinct_unique_constraints
-                or (
-                    "supports_nulls_distinct_unique_constraints"
-                    in cls._meta.required_db_features
-                )
-            ) and any(
-                isinstance(constraint, UniqueConstraint)
-                and constraint.nulls_distinct is not None
-                for constraint in cls._meta.constraints
-            ):
-                errors.append(
-                    checks.Warning(
-                        "%s does not support unique constraints with "
-                        "nulls distinct." % connection.display_name,
-                        hint=(
-                            "A constraint won't be created. Silence this "
-                            "warning if you don't care about it."
-                        ),
-                        obj=cls,
-                        id="models.W047",
-                    )
-                )
-            fields = set(
-                chain.from_iterable(
-                    (*constraint.fields, *constraint.include)
-                    for constraint in cls._meta.constraints
-                    if isinstance(constraint, UniqueConstraint)
-                )
-            )
-            references = set()
             for constraint in cls._meta.constraints:
-                if isinstance(constraint, UniqueConstraint):
-                    if (
-                        connection.features.supports_partial_indexes
-                        or "supports_partial_indexes"
-                        not in cls._meta.required_db_features
-                    ) and isinstance(constraint.condition, Q):
-                        references.update(
-                            cls._get_expr_references(constraint.condition)
-                        )
-                    if (
-                        connection.features.supports_expression_indexes
-                        or "supports_expression_indexes"
-                        not in cls._meta.required_db_features
-                    ) and constraint.contains_expressions:
-                        for expression in constraint.expressions:
-                            references.update(cls._get_expr_references(expression))
-                elif isinstance(constraint, CheckConstraint):
-                    if (
-                        connection.features.supports_table_check_constraints
-                        or "supports_table_check_constraints"
-                        not in cls._meta.required_db_features
-                    ):
-                        if isinstance(constraint.check, Q):
-                            references.update(
-                                cls._get_expr_references(constraint.check)
-                            )
-                        if any(
-                            isinstance(expr, RawSQL)
-                            for expr in constraint.check.flatten()
-                        ):
-                            errors.append(
-                                checks.Warning(
-                                    f"Check constraint {constraint.name!r} contains "
-                                    f"RawSQL() expression and won't be validated "
-                                    f"during the model full_clean().",
-                                    hint=(
-                                        "Silence this warning if you don't care about "
-                                        "it."
-                                    ),
-                                    obj=cls,
-                                    id="models.W045",
-                                ),
-                            )
-            for field_name, *lookups in references:
-                # pk is an alias that won't be found by opts.get_field.
-                if field_name != "pk":
-                    fields.add(field_name)
-                if not lookups:
-                    # If it has no lookups it cannot result in a JOIN.
-                    continue
-                try:
-                    if field_name == "pk":
-                        field = cls._meta.pk
-                    else:
-                        field = cls._meta.get_field(field_name)
-                    if not field.is_relation or field.many_to_many or field.one_to_many:
-                        continue
-                except FieldDoesNotExist:
-                    continue
-                # JOIN must happen at the first lookup.
-                first_lookup = lookups[0]
-                if (
-                    hasattr(field, "get_transform")
-                    and hasattr(field, "get_lookup")
-                    and field.get_transform(first_lookup) is None
-                    and field.get_lookup(first_lookup) is None
-                ):
-                    errors.append(
-                        checks.Error(
-                            "'constraints' refers to the joined field '%s'."
-                            % LOOKUP_SEP.join([field_name] + lookups),
-                            obj=cls,
-                            id="models.E041",
-                        )
-                    )
-            errors.extend(cls._check_local_fields(fields, "constraints"))
+                errors.extend(constraint._check(cls, connection))
         return errors
 
 
diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py
index 56d547e6b0..3e6c5205c6 100644
--- a/django/db/models/constraints.py
+++ b/django/db/models/constraints.py
@@ -2,9 +2,11 @@ import warnings
 from enum import Enum
 from types import NoneType
 
-from django.core.exceptions import FieldError, ValidationError
+from django.core import checks
+from django.core.exceptions import FieldDoesNotExist, FieldError, ValidationError
 from django.db import connections
-from django.db.models.expressions import Exists, ExpressionList, F, OrderBy
+from django.db.models.constants import LOOKUP_SEP
+from django.db.models.expressions import Exists, ExpressionList, F, RawSQL
 from django.db.models.indexes import IndexExpression
 from django.db.models.lookups import Exact
 from django.db.models.query_utils import Q
@@ -72,6 +74,47 @@ class BaseConstraint:
     def get_violation_error_message(self):
         return self.violation_error_message % {"name": self.name}
 
+    def _check(self, model, connection):
+        return []
+
+    def _check_references(self, model, references):
+        errors = []
+        fields = set()
+        for field_name, *lookups in references:
+            # pk is an alias that won't be found by opts.get_field.
+            if field_name != "pk":
+                fields.add(field_name)
+            if not lookups:
+                # If it has no lookups it cannot result in a JOIN.
+                continue
+            try:
+                if field_name == "pk":
+                    field = model._meta.pk
+                else:
+                    field = model._meta.get_field(field_name)
+                if not field.is_relation or field.many_to_many or field.one_to_many:
+                    continue
+            except FieldDoesNotExist:
+                continue
+            # JOIN must happen at the first lookup.
+            first_lookup = lookups[0]
+            if (
+                hasattr(field, "get_transform")
+                and hasattr(field, "get_lookup")
+                and field.get_transform(first_lookup) is None
+                and field.get_lookup(first_lookup) is None
+            ):
+                errors.append(
+                    checks.Error(
+                        "'constraints' refers to the joined field '%s'."
+                        % LOOKUP_SEP.join([field_name] + lookups),
+                        obj=model,
+                        id="models.E041",
+                    )
+                )
+        errors.extend(model._check_local_fields(fields, "constraints"))
+        return errors
+
     def deconstruct(self):
         path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
         path = path.replace("django.db.models.constraints", "django.db.models")
@@ -91,13 +134,30 @@ class BaseConstraint:
 
 
 class CheckConstraint(BaseConstraint):
+    # RemovedInDjango60Warning: when the deprecation ends, replace with
+    # def __init__(
+    #  self, *, condition, name, violation_error_code=None, violation_error_message=None
+    # )
     def __init__(
-        self, *, check, name, violation_error_code=None, violation_error_message=None
+        self,
+        *,
+        name,
+        condition=None,
+        check=None,
+        violation_error_code=None,
+        violation_error_message=None,
     ):
-        self.check = check
-        if not getattr(check, "conditional", False):
+        if check is not None:
+            warnings.warn(
+                "CheckConstraint.check is deprecated in favor of `.condition`.",
+                RemovedInDjango60Warning,
+                stacklevel=2,
+            )
+            condition = check
+        self.condition = condition
+        if not getattr(condition, "conditional", False):
             raise TypeError(
-                "CheckConstraint.check must be a Q instance or boolean expression."
+                "CheckConstraint.condition must be a Q instance or boolean expression."
             )
         super().__init__(
             name=name,
@@ -105,9 +165,66 @@ class CheckConstraint(BaseConstraint):
             violation_error_message=violation_error_message,
         )
 
+    def _get_check(self):
+        warnings.warn(
+            "CheckConstraint.check is deprecated in favor of `.condition`.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
+        return self.condition
+
+    def _set_check(self, value):
+        warnings.warn(
+            "CheckConstraint.check is deprecated in favor of `.condition`.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
+        self.condition = value
+
+    check = property(_get_check, _set_check)
+
+    def _check(self, model, connection):
+        errors = []
+        if not (
+            connection.features.supports_table_check_constraints
+            or "supports_table_check_constraints" in model._meta.required_db_features
+        ):
+            errors.append(
+                checks.Warning(
+                    f"{connection.display_name} does not support check constraints.",
+                    hint=(
+                        "A constraint won't be created. Silence this warning if you "
+                        "don't care about it."
+                    ),
+                    obj=model,
+                    id="models.W027",
+                )
+            )
+        elif (
+            connection.features.supports_table_check_constraints
+            or "supports_table_check_constraints"
+            not in model._meta.required_db_features
+        ):
+            references = set()
+            condition = self.condition
+            if isinstance(condition, Q):
+                references.update(model._get_expr_references(condition))
+            if any(isinstance(expr, RawSQL) for expr in condition.flatten()):
+                errors.append(
+                    checks.Warning(
+                        f"Check constraint {self.name!r} contains RawSQL() expression "
+                        "and won't be validated during the model full_clean().",
+                        hint="Silence this warning if you don't care about it.",
+                        obj=model,
+                        id="models.W045",
+                    ),
+                )
+            errors.extend(self._check_references(model, references))
+        return errors
+
     def _get_check_sql(self, model, schema_editor):
         query = Query(model=model, alias_cols=False)
-        where = query.build_where(self.check)
+        where = query.build_where(self.condition)
         compiler = query.get_compiler(connection=schema_editor.connection)
         sql, params = where.as_sql(compiler, schema_editor.connection)
         return sql % tuple(schema_editor.quote_value(p) for p in params)
@@ -126,7 +243,7 @@ class CheckConstraint(BaseConstraint):
     def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
         against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
         try:
-            if not Q(self.check).check(against, using=using):
+            if not Q(self.condition).check(against, using=using):
                 raise ValidationError(
                     self.get_violation_error_message(), code=self.violation_error_code
                 )
@@ -134,9 +251,9 @@ class CheckConstraint(BaseConstraint):
             pass
 
     def __repr__(self):
-        return "<%s: check=%s name=%s%s%s>" % (
+        return "<%s: condition=%s name=%s%s%s>" % (
             self.__class__.__qualname__,
-            self.check,
+            self.condition,
             repr(self.name),
             (
                 ""
@@ -155,7 +272,7 @@ class CheckConstraint(BaseConstraint):
         if isinstance(other, CheckConstraint):
             return (
                 self.name == other.name
-                and self.check == other.check
+                and self.condition == other.condition
                 and self.violation_error_code == other.violation_error_code
                 and self.violation_error_message == other.violation_error_message
             )
@@ -163,7 +280,7 @@ class CheckConstraint(BaseConstraint):
 
     def deconstruct(self):
         path, args, kwargs = super().deconstruct()
-        kwargs["check"] = self.check
+        kwargs["condition"] = self.condition
         return path, args, kwargs
 
 
@@ -251,6 +368,104 @@ class UniqueConstraint(BaseConstraint):
     def contains_expressions(self):
         return bool(self.expressions)
 
+    def _check(self, model, connection):
+        errors = model._check_local_fields({*self.fields, *self.include}, "constraints")
+        required_db_features = model._meta.required_db_features
+        if self.condition is not None and not (
+            connection.features.supports_partial_indexes
+            or "supports_partial_indexes" in required_db_features
+        ):
+            errors.append(
+                checks.Warning(
+                    f"{connection.display_name} does not support unique constraints "
+                    "with conditions.",
+                    hint=(
+                        "A constraint won't be created. Silence this warning if you "
+                        "don't care about it."
+                    ),
+                    obj=model,
+                    id="models.W036",
+                )
+            )
+        if self.deferrable is not None and not (
+            connection.features.supports_deferrable_unique_constraints
+            or "supports_deferrable_unique_constraints" in required_db_features
+        ):
+            errors.append(
+                checks.Warning(
+                    f"{connection.display_name} does not support deferrable unique "
+                    "constraints.",
+                    hint=(
+                        "A constraint won't be created. Silence this warning if you "
+                        "don't care about it."
+                    ),
+                    obj=model,
+                    id="models.W038",
+                )
+            )
+        if self.include and not (
+            connection.features.supports_covering_indexes
+            or "supports_covering_indexes" in required_db_features
+        ):
+            errors.append(
+                checks.Warning(
+                    f"{connection.display_name} does not support unique constraints "
+                    "with non-key columns.",
+                    hint=(
+                        "A constraint won't be created. Silence this warning if you "
+                        "don't care about it."
+                    ),
+                    obj=model,
+                    id="models.W039",
+                )
+            )
+        if self.contains_expressions and not (
+            connection.features.supports_expression_indexes
+            or "supports_expression_indexes" in required_db_features
+        ):
+            errors.append(
+                checks.Warning(
+                    f"{connection.display_name} does not support unique constraints on "
+                    "expressions.",
+                    hint=(
+                        "A constraint won't be created. Silence this warning if you "
+                        "don't care about it."
+                    ),
+                    obj=model,
+                    id="models.W044",
+                )
+            )
+        if self.nulls_distinct is not None and not (
+            connection.features.supports_nulls_distinct_unique_constraints
+            or "supports_nulls_distinct_unique_constraints" in required_db_features
+        ):
+            errors.append(
+                checks.Warning(
+                    f"{connection.display_name} does not support unique constraints "
+                    "with nulls distinct.",
+                    hint=(
+                        "A constraint won't be created. Silence this warning if you "
+                        "don't care about it."
+                    ),
+                    obj=model,
+                    id="models.W047",
+                )
+            )
+        references = set()
+        if (
+            connection.features.supports_partial_indexes
+            or "supports_partial_indexes" not in required_db_features
+        ) and isinstance(self.condition, Q):
+            references.update(model._get_expr_references(self.condition))
+        if self.contains_expressions and (
+            connection.features.supports_expression_indexes
+            or "supports_expression_indexes" not in required_db_features
+        ):
+            for expression in self.expressions:
+                references.update(model._get_expr_references(expression))
+        errors.extend(self._check_references(model, references))
+        return errors
+
     def _get_condition_sql(self, model, schema_editor):
         if self.condition is None:
             return None
@@ -429,9 +644,8 @@ class UniqueConstraint(BaseConstraint):
             }
             expressions = []
             for expr in self.expressions:
-                # Ignore ordering.
-                if isinstance(expr, OrderBy):
-                    expr = expr.expression
+                if hasattr(expr, "get_expression_for_validation"):
+                    expr = expr.get_expression_for_validation()
                 expressions.append(Exact(expr, expr.replace_expressions(replacements)))
             queryset = queryset.filter(*expressions)
         model_class_pk = instance._get_pk_val(model._meta)
diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py
index bc26d82e93..fd3d290a96 100644
--- a/django/db/models/deletion.py
+++ b/django/db/models/deletion.py
@@ -60,8 +60,9 @@ def SET(value):
         def set_on_delete(collector, field, sub_objs, using):
             collector.add_field_update(field, value, sub_objs)
 
+        set_on_delete.lazy_sub_objs = True
+
     set_on_delete.deconstruct = lambda: ("django.db.models.SET", (value,), {})
-    set_on_delete.lazy_sub_objs = True
     return set_on_delete
 
 
@@ -76,9 +77,6 @@ def SET_DEFAULT(collector, field, sub_objs, using):
     collector.add_field_update(field, field.get_default(), sub_objs)
 
 
-SET_DEFAULT.lazy_sub_objs = True
-
-
 def DO_NOTHING(collector, field, sub_objs, using):
     pass
 
@@ -307,13 +305,11 @@ class Collector:
         if not collect_related:
             return
 
-        if keep_parents:
-            parents = set(model._meta.get_parent_list())
         model_fast_deletes = defaultdict(list)
         protected_objects = defaultdict(list)
         for related in get_candidate_relations_to_delete(model._meta):
             # Preserve parent reverse relationships if keep_parents=True.
-            if keep_parents and related.model in parents:
+            if keep_parents and related.model in model._meta.all_parents:
                 continue
             field = related.field
             on_delete = field.remote_field.on_delete
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 6d329ae85d..4ee22420d9 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -5,6 +5,7 @@ import inspect
 from collections import defaultdict
 from decimal import Decimal
 from enum import Enum
+from itertools import chain
 from types import NoneType
 from uuid import UUID
 
@@ -175,10 +176,12 @@ class BaseExpression:
     _output_field_resolved_to_none = False
     # Can the expression be used in a WHERE clause?
     filterable = True
-    # Can the expression can be used as a source expression in Window?
+    # Can the expression be used as a source expression in Window?
     window_compatible = False
     # Can the expression be used as a database default value?
     allowed_default = False
+    # Can the expression be used during a constraint validation?
+    constraint_validation_compatible = True
 
     def __init__(self, output_field=None):
         if output_field is not None:
@@ -483,6 +486,20 @@ class BaseExpression:
             return self.output_field.select_format(compiler, sql, params)
         return sql, params
 
+    def get_expression_for_validation(self):
+        # Ignore expressions that cannot be used during a constraint validation.
+        if not getattr(self, "constraint_validation_compatible", True):
+            try:
+                (expression,) = self.get_source_expressions()
+            except ValueError as e:
+                raise ValueError(
+                    "Expressions with constraint_validation_compatible set to False "
+                    "must have only one source expression."
+                ) from e
+            else:
+                return expression
+        return self
+
 
 @deconstructible
 class Expression(BaseExpression, Combinable):
@@ -597,10 +614,16 @@ _connector_combinations = [
     },
     # Numeric with NULL.
     {
-        connector: [
-            (field_type, NoneType, field_type),
-            (NoneType, field_type, field_type),
-        ]
+        connector: list(
+            chain.from_iterable(
+                [(field_type, NoneType, field_type), (NoneType, field_type, field_type)]
+                for field_type in (
+                    fields.IntegerField,
+                    fields.DecimalField,
+                    fields.FloatField,
+                )
+            )
+        )
         for connector in (
             Combinable.ADD,
             Combinable.SUB,
@@ -609,7 +632,6 @@ _connector_combinations = [
             Combinable.MOD,
             Combinable.POW,
         )
-        for field_type in (fields.IntegerField, fields.DecimalField, fields.FloatField)
     },
     # Date/DateTimeField/DurationField/TimeField.
     {
@@ -1202,10 +1224,9 @@ class RawSQL(Expression):
     ):
         # Resolve parents fields used in raw SQL.
         if query.model:
-            for parent in query.model._meta.get_parent_list():
+            for parent in query.model._meta.all_parents:
                 for parent_field in parent._meta.local_fields:
-                    _, column_name = parent_field.get_attname_column()
-                    if column_name.lower() in self.sql.lower():
+                    if parent_field.column.lower() in self.sql.lower():
                         query.resolve_ref(
                             parent_field.name, allow_joins, reuse, summarize
                         )
@@ -1711,6 +1732,7 @@ class Exists(Subquery):
 class OrderBy(Expression):
     template = "%(expression)s %(ordering)s"
     conditional = False
+    constraint_validation_compatible = False
 
     def __init__(self, expression, descending=False, nulls_first=None, nulls_last=None):
         if nulls_first and nulls_last:
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 5186f0c414..796c4d23c4 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -219,12 +219,6 @@ class Field(RegisterLookupMixin):
         self.remote_field = rel
         self.is_relation = self.remote_field is not None
         self.default = default
-        if db_default is not NOT_PROVIDED and not hasattr(
-            db_default, "resolve_expression"
-        ):
-            from django.db.models.expressions import Value
-
-            db_default = Value(db_default)
         self.db_default = db_default
         self.editable = editable
         self.serialize = serialize
@@ -408,7 +402,7 @@ class Field(RegisterLookupMixin):
                 continue
             connection = connections[db]
 
-            if not getattr(self.db_default, "allowed_default", False) and (
+            if not getattr(self._db_default_expression, "allowed_default", False) and (
                 connection.features.supports_expression_defaults
             ):
                 msg = f"{self.db_default} cannot be used in db_default."
@@ -922,7 +916,7 @@ class Field(RegisterLookupMixin):
             return [self.from_db_value]
         return []
 
-    @property
+    @cached_property
     def unique(self):
         return self._unique or self.primary_key
 
@@ -994,7 +988,7 @@ class Field(RegisterLookupMixin):
             from django.db.models.expressions import DatabaseDefault
 
             if isinstance(value, DatabaseDefault):
-                return self.db_default
+                return self._db_default_expression
         return value
 
     def get_prep_value(self, value):
@@ -1047,6 +1041,17 @@ class Field(RegisterLookupMixin):
             return return_None
         return str  # return empty string
 
+    @cached_property
+    def _db_default_expression(self):
+        db_default = self.db_default
+        if db_default is not NOT_PROVIDED and not hasattr(
+            db_default, "resolve_expression"
+        ):
+            from django.db.models.expressions import Value
+
+            db_default = Value(db_default, self)
+        return db_default
+
     def get_choices(
         self,
         include_blank=True,
diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
index 95985684ee..7c911f4b23 100644
--- a/django/db/models/fields/files.py
+++ b/django/db/models/fields/files.py
@@ -3,7 +3,8 @@ import posixpath
 
 from django import forms
 from django.core import checks
-from django.core.files.base import File
+from django.core.exceptions import FieldError
+from django.core.files.base import ContentFile, File
 from django.core.files.images import ImageFile
 from django.core.files.storage import Storage, default_storage
 from django.core.files.utils import validate_file_name
@@ -12,6 +13,7 @@ from django.db.models.fields import Field
 from django.db.models.query_utils import DeferredAttribute
 from django.db.models.utils import AltersData
 from django.utils.translation import gettext_lazy as _
+from django.utils.version import PY311
 
 
 class FieldFile(File, AltersData):
@@ -88,10 +90,13 @@ class FieldFile(File, AltersData):
     # to further manipulate the underlying file, as well as update the
     # associated model instance.
 
+    def _set_instance_attribute(self, name, content):
+        setattr(self.instance, self.field.attname, name)
+
     def save(self, name, content, save=True):
         name = self.field.generate_filename(self.instance, name)
         self.name = self.storage.save(name, content, max_length=self.field.max_length)
-        setattr(self.instance, self.field.attname, self.name)
+        self._set_instance_attribute(self.name, content)
         self._committed = True
 
         # Save the object because it has changed, unless save is False
@@ -312,6 +317,15 @@ class FileField(Field):
 
     def pre_save(self, model_instance, add):
         file = super().pre_save(model_instance, add)
+        if file.name is None and file._file is not None:
+            exc = FieldError(
+                f"File for {self.name} must have "
+                "the name attribute specified to be saved."
+            )
+            if PY311 and isinstance(file._file, ContentFile):
+                exc.add_note("Pass a 'name' argument to ContentFile.")
+            raise exc
+
         if file and not file._committed:
             # Commit the file to storage prior to saving the model
             file.save(file.name, file.file, save=False)
@@ -380,6 +394,12 @@ class ImageFileDescriptor(FileDescriptor):
 
 
 class ImageFieldFile(ImageFile, FieldFile):
+    def _set_instance_attribute(self, name, content):
+        setattr(self.instance, self.field.attname, content)
+        # Update the name in case generate_filename() or storage.save() changed
+        # it, but bypass the descriptor to avoid re-reading the file.
+        self.instance.__dict__[self.field.attname] = self.name
+
     def delete(self, save=True):
         # Clear the image dimensions cache
         if hasattr(self, "_dimensions_cache"):
diff --git a/django/db/models/fields/generated.py b/django/db/models/fields/generated.py
index 257feeeba2..5b6b188df0 100644
--- a/django/db/models/fields/generated.py
+++ b/django/db/models/fields/generated.py
@@ -39,7 +39,7 @@ class GeneratedField(Field):
         return Col(self.model._meta.db_table, self, self.output_field)
 
     def get_col(self, alias, output_field=None):
-        if alias != self.model._meta.db_table and output_field is None:
+        if alias != self.model._meta.db_table and output_field in (None, self):
             output_field = self.output_field
         return super().get_col(alias, output_field)
 
diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py
index 571e6e79f3..1b219e620c 100644
--- a/django/db/models/fields/json.py
+++ b/django/db/models/fields/json.py
@@ -310,6 +310,14 @@ class JSONExact(lookups.Exact):
             rhs %= tuple(func)
         return rhs, rhs_params
 
+    def as_oracle(self, compiler, connection):
+        lhs, lhs_params = self.process_lhs(compiler, connection)
+        rhs, rhs_params = self.process_rhs(compiler, connection)
+        if connection.features.supports_primitives_in_json_field:
+            lhs = f"JSON({lhs})"
+            rhs = f"JSON({rhs})"
+        return f"JSON_EQUAL({lhs}, {rhs} ERROR ON ERROR)", (*lhs_params, *rhs_params)
+
 
 class JSONIContains(CaseInsensitiveMixin, lookups.IContains):
     pass
diff --git a/django/db/models/fields/mixins.py b/django/db/models/fields/mixins.py
index e7f282210e..9f2809dfc8 100644
--- a/django/db/models/fields/mixins.py
+++ b/django/db/models/fields/mixins.py
@@ -1,31 +1,52 @@
+import warnings
+
 from django.core import checks
+from django.utils.deprecation import RemovedInDjango60Warning
+from django.utils.functional import cached_property
 
 NOT_PROVIDED = object()
 
 
 class FieldCacheMixin:
-    """Provide an API for working with the model's fields value cache."""
+    """
+    An API for working with the model's fields value cache.
 
+    Subclasses must set self.cache_name to a unique entry for the cache -
+    typically the field’s name.
+    """
+
+    # RemovedInDjango60Warning.
     def get_cache_name(self):
         raise NotImplementedError
 
-    def get_cached_value(self, instance, default=NOT_PROVIDED):
+    @cached_property
+    def cache_name(self):
+        # RemovedInDjango60Warning: when the deprecation ends, replace with:
+        # raise NotImplementedError
         cache_name = self.get_cache_name()
+        warnings.warn(
+            f"Override {self.__class__.__qualname__}.cache_name instead of "
+            "get_cache_name().",
+            RemovedInDjango60Warning,
+        )
+        return cache_name
+
+    def get_cached_value(self, instance, default=NOT_PROVIDED):
         try:
-            return instance._state.fields_cache[cache_name]
+            return instance._state.fields_cache[self.cache_name]
         except KeyError:
             if default is NOT_PROVIDED:
                 raise
             return default
 
     def is_cached(self, instance):
-        return self.get_cache_name() in instance._state.fields_cache
+        return self.cache_name in instance._state.fields_cache
 
     def set_cached_value(self, instance, value):
-        instance._state.fields_cache[self.get_cache_name()] = value
+        instance._state.fields_cache[self.cache_name] = value
 
     def delete_cached_value(self, instance):
-        del instance._state.fields_cache[self.get_cache_name()]
+        del instance._state.fields_cache[self.cache_name]
 
 
 class CheckFieldDefaultMixin:
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 8564e6366b..7d42d1ea38 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -154,7 +154,7 @@ class RelatedField(FieldCacheMixin, Field):
         return []
 
     def _check_related_query_name_is_valid(self):
-        if self.remote_field.is_hidden():
+        if self.remote_field.hidden:
             return []
         rel_query_name = self.related_query_name()
         errors = []
@@ -253,8 +253,8 @@ class RelatedField(FieldCacheMixin, Field):
         # If the field doesn't install a backward relation on the target model
         # (so `is_hidden` returns True), then there are no clashes to check
         # and we can skip these fields.
-        rel_is_hidden = self.remote_field.is_hidden()
-        rel_name = self.remote_field.get_accessor_name()  # i. e. "model_set"
+        rel_is_hidden = self.remote_field.hidden
+        rel_name = self.remote_field.accessor_name  # i. e. "model_set"
         rel_query_name = self.related_query_name()  # i. e. "model"
         # i.e. "app_label.Model.field".
         field_name = "%s.%s" % (opts.label, self.name)
@@ -264,9 +264,8 @@ class RelatedField(FieldCacheMixin, Field):
         # model_set and it clashes with Target.model_set.
         potential_clashes = rel_opts.fields + rel_opts.many_to_many
         for clash_field in potential_clashes:
-            # i.e. "app_label.Target.model_set".
-            clash_name = "%s.%s" % (rel_opts.label, clash_field.name)
             if not rel_is_hidden and clash_field.name == rel_name:
+                clash_name = f"{rel_opts.label}.{clash_field.name}"
                 errors.append(
                     checks.Error(
                         f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
@@ -283,6 +282,7 @@ class RelatedField(FieldCacheMixin, Field):
                 )
 
             if clash_field.name == rel_query_name:
+                clash_name = f"{rel_opts.label}.{clash_field.name}"
                 errors.append(
                     checks.Error(
                         "Reverse query name for '%s' clashes with field name '%s'."
@@ -302,12 +302,10 @@ class RelatedField(FieldCacheMixin, Field):
         # Model.m2m accessor.
         potential_clashes = (r for r in rel_opts.related_objects if r.field is not self)
         for clash_field in potential_clashes:
-            # i.e. "app_label.Model.m2m".
-            clash_name = "%s.%s" % (
-                clash_field.related_model._meta.label,
-                clash_field.field.name,
-            )
-            if not rel_is_hidden and clash_field.get_accessor_name() == rel_name:
+            if not rel_is_hidden and clash_field.accessor_name == rel_name:
+                clash_name = (
+                    f"{clash_field.related_model._meta.label}.{clash_field.field.name}"
+                )
                 errors.append(
                     checks.Error(
                         f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
@@ -323,7 +321,10 @@ class RelatedField(FieldCacheMixin, Field):
                     )
                 )
 
-            if clash_field.get_accessor_name() == rel_query_name:
+            if clash_field.accessor_name == rel_query_name:
+                clash_name = (
+                    f"{clash_field.related_model._meta.label}.{clash_field.field.name}"
+                )
                 errors.append(
                     checks.Error(
                         "Reverse query name for '%s' clashes with reverse query name "
@@ -508,7 +509,8 @@ class RelatedField(FieldCacheMixin, Field):
             )
         return target_fields[0]
 
-    def get_cache_name(self):
+    @cached_property
+    def cache_name(self):
         return self.name
 
 
@@ -614,60 +616,56 @@ class ForeignObject(RelatedField):
         if not self.foreign_related_fields:
             return []
 
-        unique_foreign_fields = {
-            frozenset([f.name])
-            for f in self.remote_field.model._meta.get_fields()
-            if getattr(f, "unique", False)
-        }
-        unique_foreign_fields.update(
-            {frozenset(ut) for ut in self.remote_field.model._meta.unique_together}
+        has_unique_constraint = any(
+            rel_field.unique for rel_field in self.foreign_related_fields
         )
-        unique_foreign_fields.update(
-            {
-                frozenset(uc.fields)
-                for uc in self.remote_field.model._meta.total_unique_constraints
-            }
-        )
-        foreign_fields = {f.name for f in self.foreign_related_fields}
-        has_unique_constraint = any(u <= foreign_fields for u in unique_foreign_fields)
-
-        if not has_unique_constraint and len(self.foreign_related_fields) > 1:
-            field_combination = ", ".join(
-                "'%s'" % rel_field.name for rel_field in self.foreign_related_fields
+        if not has_unique_constraint:
+            foreign_fields = {f.name for f in self.foreign_related_fields}
+            remote_opts = self.remote_field.model._meta
+            has_unique_constraint = any(
+                frozenset(ut) <= foreign_fields for ut in remote_opts.unique_together
+            ) or any(
+                frozenset(uc.fields) <= foreign_fields
+                for uc in remote_opts.total_unique_constraints
             )
-            model_name = self.remote_field.model.__name__
-            return [
-                checks.Error(
-                    "No subset of the fields %s on model '%s' is unique."
-                    % (field_combination, model_name),
-                    hint=(
-                        "Mark a single field as unique=True or add a set of "
-                        "fields to a unique constraint (via unique_together "
-                        "or a UniqueConstraint (without condition) in the "
-                        "model Meta.constraints)."
-                    ),
-                    obj=self,
-                    id="fields.E310",
+
+        if not has_unique_constraint:
+            if len(self.foreign_related_fields) > 1:
+                field_combination = ", ".join(
+                    f"'{rel_field.name}'" for rel_field in self.foreign_related_fields
                 )
-            ]
-        elif not has_unique_constraint:
-            field_name = self.foreign_related_fields[0].name
-            model_name = self.remote_field.model.__name__
-            return [
-                checks.Error(
-                    "'%s.%s' must be unique because it is referenced by "
-                    "a foreign key." % (model_name, field_name),
-                    hint=(
-                        "Add unique=True to this field or add a "
-                        "UniqueConstraint (without condition) in the model "
-                        "Meta.constraints."
-                    ),
-                    obj=self,
-                    id="fields.E311",
-                )
-            ]
-        else:
-            return []
+                model_name = self.remote_field.model.__name__
+                return [
+                    checks.Error(
+                        f"No subset of the fields {field_combination} on model "
+                        f"'{model_name}' is unique.",
+                        hint=(
+                            "Mark a single field as unique=True or add a set of "
+                            "fields to a unique constraint (via unique_together "
+                            "or a UniqueConstraint (without condition) in the "
+                            "model Meta.constraints)."
+                        ),
+                        obj=self,
+                        id="fields.E310",
+                    )
+                ]
+            else:
+                field_name = self.foreign_related_fields[0].name
+                model_name = self.remote_field.model.__name__
+                return [
+                    checks.Error(
+                        f"'{model_name}.{field_name}' must be unique because it is "
+                        "referenced by a foreign key.",
+                        hint=(
+                            "Add unique=True to this field or add a "
+                            "UniqueConstraint (without condition) in the model "
+                            "Meta.constraints."
+                        ),
+                        obj=self,
+                        id="fields.E311",
+                    )
+                ]
+        return []
 
     def deconstruct(self):
         name, path, args, kwargs = super().deconstruct()
@@ -887,13 +885,10 @@ class ForeignObject(RelatedField):
     def contribute_to_related_class(self, cls, related):
         # Internal FK's - i.e., those with a related name ending with '+' -
         # and swapped models don't get a related descriptor.
-        if (
-            not self.remote_field.is_hidden()
-            and not related.related_model._meta.swapped
-        ):
+        if not self.remote_field.hidden and not related.related_model._meta.swapped:
             setattr(
                 cls._meta.concrete_model,
-                related.get_accessor_name(),
+                related.accessor_name,
                 self.related_accessor_class(related),
             )
             # While 'limit_choices_to' might be a callable, simply pass
@@ -1901,7 +1896,7 @@ class ManyToManyField(RelatedField):
             or self.remote_field.model == cls._meta.object_name
         ):
             self.remote_field.related_name = "%s_rel_+" % name
-        elif self.remote_field.is_hidden():
+        elif self.remote_field.hidden:
             # If the backwards relation is disabled, replace the original
             # related_name with one generated from the m2m field name. Django
             # still uses backwards relations internally and we need to avoid
@@ -1941,13 +1936,10 @@ class ManyToManyField(RelatedField):
     def contribute_to_related_class(self, cls, related):
         # Internal M2Ms (i.e., those with a related name ending with '+')
         # and swapped models don't get a related descriptor.
-        if (
-            not self.remote_field.is_hidden()
-            and not related.related_model._meta.swapped
-        ):
+        if not self.remote_field.hidden and not related.related_model._meta.swapped:
             setattr(
                 cls,
-                related.get_accessor_name(),
+                related.accessor_name,
                 ManyToManyDescriptor(self.remote_field, reverse=True),
             )
 
diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py
index 62ddfc60b3..bc288c47ec 100644
--- a/django/db/models/fields/related_descriptors.py
+++ b/django/db/models/fields/related_descriptors.py
@@ -75,7 +75,7 @@ from django.db import (
     router,
     transaction,
 )
-from django.db.models import Q, Window, signals
+from django.db.models import Manager, Q, Window, signals
 from django.db.models.functions import RowNumber
 from django.db.models.lookups import GreaterThan, LessThanOrEqual
 from django.db.models.query import QuerySet
@@ -187,7 +187,7 @@ class ForwardManyToOneDescriptor:
         # (related_name ends with a '+'). Refs #21410.
         # The check for len(...) == 1 is a special case that allows the query
         # to be join-less and smaller. Refs #21760.
-        if remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
+        if remote_field.hidden or len(self.field.foreign_related_fields) == 1:
             query = {
                 "%s__in"
                 % related_field.name: {instance_attr(inst)[0] for inst in instances}
@@ -195,6 +195,9 @@ class ForwardManyToOneDescriptor:
         else:
             query = {"%s__in" % self.field.related_query_name(): instances}
         queryset = queryset.filter(**query)
+        # There can be only one object prefetched for each instance so clear
+        # ordering if the query allows it without side effects.
+        queryset.query.clear_ordering()
 
         # Since we're going to assign directly in the cache,
         # we must manage the reverse relation cache manually.
@@ -207,7 +210,7 @@ class ForwardManyToOneDescriptor:
             rel_obj_attr,
             instance_attr,
             True,
-            self.field.get_cache_name(),
+            self.field.cache_name,
             False,
         )
 
@@ -469,6 +472,9 @@ class ReverseOneToOneDescriptor:
         instances_dict = {instance_attr(inst): inst for inst in instances}
         query = {"%s__in" % self.related.field.name: instances}
         queryset = queryset.filter(**query)
+        # There can be only one object prefetched for each instance so clear
+        # ordering if the query allows it without side effects.
+        queryset.query.clear_ordering()
 
         # Since we're going to assign directly in the cache,
         # we must manage the reverse relation cache manually.
@@ -480,7 +486,7 @@ class ReverseOneToOneDescriptor:
             rel_obj_attr,
             instance_attr,
             True,
-            self.related.get_cache_name(),
+            self.related.cache_name,
             False,
         )
 
@@ -524,7 +530,7 @@ class ReverseOneToOneDescriptor:
         if rel_obj is None:
             raise self.RelatedObjectDoesNotExist(
                 "%s has no %s."
-                % (instance.__class__.__name__, self.related.get_accessor_name())
+                % (instance.__class__.__name__, self.related.accessor_name)
             )
         else:
             return rel_obj
@@ -564,7 +570,7 @@ class ReverseOneToOneDescriptor:
                 % (
                     value,
                     instance._meta.object_name,
-                    self.related.get_accessor_name(),
+                    self.related.accessor_name,
                     self.related.related_model._meta.object_name,
                 )
             )
@@ -652,7 +658,7 @@ class ReverseManyToOneDescriptor:
     def _get_set_deprecation_msg_params(self):
         return (
             "reverse side of a related set",
-            self.rel.get_accessor_name(),
+            self.rel.accessor_name,
         )
 
     def __set__(self, instance, value):
@@ -738,7 +744,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
         def _remove_prefetched_objects(self):
             try:
                 self.instance._prefetched_objects_cache.pop(
-                    self.field.remote_field.get_cache_name()
+                    self.field.remote_field.cache_name
                 )
             except (AttributeError, KeyError):
                 pass  # nothing to clear from cache
@@ -754,7 +760,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
                 )
             try:
                 return self.instance._prefetched_objects_cache[
-                    self.field.remote_field.get_cache_name()
+                    self.field.remote_field.cache_name
                 ]
             except (AttributeError, KeyError):
                 queryset = super().get_queryset()
@@ -792,7 +798,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
                 if not self.field.is_cached(rel_obj):
                     instance = instances_dict[rel_obj_attr(rel_obj)]
                     setattr(rel_obj, self.field.name, instance)
-            cache_name = self.field.remote_field.get_cache_name()
+            cache_name = self.field.remote_field.cache_name
             return queryset, rel_obj_attr, instance_attr, False, cache_name, False
 
         def add(self, *objs, bulk=True):
@@ -1019,7 +1025,7 @@ class ManyToManyDescriptor(ReverseManyToOneDescriptor):
         return (
             "%s side of a many-to-many set"
             % ("reverse" if self.reverse else "forward"),
-            self.rel.get_accessor_name() if self.reverse else self.field.name,
+            self.rel.accessor_name if self.reverse else self.field.name,
         )
 
 
@@ -1121,6 +1127,12 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
             queryset._defer_next_filter = True
             return queryset._next_is_sticky().filter(**self.core_filters)
 
+        def get_prefetch_cache(self):
+            try:
+                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
+            except (AttributeError, KeyError):
+                return None
+
         def _remove_prefetched_objects(self):
             try:
                 self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
@@ -1128,9 +1140,9 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
                 pass  # nothing to clear from cache
 
         def get_queryset(self):
-            try:
-                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
-            except (AttributeError, KeyError):
+            if (cache := self.get_prefetch_cache()) is not None:
+                return cache
+            else:
                 queryset = super().get_queryset()
                 return self._apply_rel_filters(queryset)
 
@@ -1195,6 +1207,45 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
                 False,
             )
 
+        @property
+        def constrained_target(self):
+            # If the through relation's target field's foreign integrity is
+            # enforced, the query can be performed solely against the through
+            # table as the INNER JOIN'ing against target table is unnecessary.
+            if not self.target_field.db_constraint:
+                return None
+            db = router.db_for_read(self.through, instance=self.instance)
+            if not connections[db].features.supports_foreign_keys:
+                return None
+            hints = {"instance": self.instance}
+            manager = self.through._base_manager.db_manager(db, hints=hints)
+            filters = {self.source_field_name: self.instance.pk}
+            # Nullable target rows must be excluded as well as they would have
+            # been filtered out from an INNER JOIN.
+            if self.target_field.null:
+                filters["%s__isnull" % self.target_field_name] = False
+            return manager.filter(**filters)
+
+        def exists(self):
+            if (
+                superclass is Manager
+                and self.get_prefetch_cache() is None
+                and (constrained_target := self.constrained_target) is not None
+            ):
+                return constrained_target.exists()
+            else:
+                return super().exists()
+
+        def count(self):
+            if (
+                superclass is Manager
+                and self.get_prefetch_cache() is None
+                and (constrained_target := self.constrained_target) is not None
+            ):
+                return constrained_target.count()
+            else:
+                return super().count()
+
         def add(self, *objs, through_defaults=None):
             self._remove_prefetched_objects()
             db = router.db_for_write(self.through, instance=self.instance)
diff --git a/django/db/models/fields/reverse_related.py b/django/db/models/fields/reverse_related.py
index 144cce6142..a39bfd07da 100644
--- a/django/db/models/fields/reverse_related.py
+++ b/django/db/models/fields/reverse_related.py
@@ -66,7 +66,8 @@ class ForeignObjectRel(FieldCacheMixin):
     # AttributeError
     @cached_property
     def hidden(self):
-        return self.is_hidden()
+        """Should the related object be hidden?"""
+        return bool(self.related_name) and self.related_name[-1] == "+"
 
     @cached_property
     def name(self):
@@ -191,10 +192,6 @@ class ForeignObjectRel(FieldCacheMixin):
             qs = qs.order_by(*ordering)
         return (blank_choice if include_blank else []) + [(x.pk, str(x)) for x in qs]
 
-    def is_hidden(self):
-        """Should the related object be hidden?"""
-        return bool(self.related_name) and self.related_name[-1] == "+"
-
     def get_joining_columns(self):
         warnings.warn(
             "ForeignObjectRel.get_joining_columns() is deprecated. Use "
@@ -219,6 +216,10 @@ class ForeignObjectRel(FieldCacheMixin):
         # example custom multicolumn joins currently have no remote field).
         self.field_name = None
 
+    @cached_property
+    def accessor_name(self):
+        return self.get_accessor_name()
+
     def get_accessor_name(self, model=None):
         # This method encapsulates the logic that decides what name to give an
         # accessor descriptor that retrieves related many-to-one or
@@ -247,12 +248,13 @@ class ForeignObjectRel(FieldCacheMixin):
     def path_infos(self):
         return self.get_path_info()
 
-    def get_cache_name(self):
+    @cached_property
+    def cache_name(self):
         """
         Return the name of the cache key to use for storing an instance of the
         forward model on the reverse model.
         """
-        return self.get_accessor_name()
+        return self.accessor_name
 
 
 class ManyToOneRel(ForeignObjectRel):
diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py
index 139875eed5..18c4f2ca08 100644
--- a/django/db/models/lookups.py
+++ b/django/db/models/lookups.py
@@ -122,7 +122,7 @@ class Lookup(Expression):
             # Ensure expression is wrapped in parentheses to respect operator
             # precedence but avoid double wrapping as it can be misinterpreted
             # on some backends (e.g. subqueries on SQLite).
-            if sql and sql[0] != "(":
+            if not isinstance(value, Value) and sql and sql[0] != "(":
                 sql = "(%s)" % sql
             return sql, params
         else:
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 9b3106f67e..68a7228cbe 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -1,11 +1,11 @@
 import bisect
 import copy
-import inspect
 from collections import defaultdict
 
 from django.apps import apps
 from django.conf import settings
 from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
+from django.core.signals import setting_changed
 from django.db import connections
 from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint
 from django.db.models.query_utils import PathInfo
@@ -203,10 +203,9 @@ class Options:
             self.unique_together = normalize_together(self.unique_together)
             # App label/class name interpolation for names of constraints and
             # indexes.
-            if not getattr(cls._meta, "abstract", False):
-                for attr_name in {"constraints", "indexes"}:
-                    objs = getattr(self, attr_name, [])
-                    setattr(self, attr_name, self._format_names_with_class(cls, objs))
+            if not self.abstract:
+                self.constraints = self._format_names(self.constraints)
+                self.indexes = self._format_names(self.indexes)
 
             # verbose_name_plural is a special case because it uses a 's'
             # by default.
@@ -232,15 +231,16 @@ class Options:
                 self.db_table, connection.ops.max_name_length()
             )
 
-    def _format_names_with_class(self, cls, objs):
+        if self.swappable:
+            setting_changed.connect(self.setting_changed)
+
+    def _format_names(self, objs):
         """App label/class name interpolation for object names."""
+        names = {"app_label": self.app_label.lower(), "class": self.model_name}
         new_objs = []
         for obj in objs:
             obj = obj.clone()
-            obj.name = obj.name % {
-                "app_label": cls._meta.app_label.lower(),
-                "class": cls.__name__.lower(),
-            }
+            obj.name %= names
             new_objs.append(obj)
         return new_objs
 
@@ -395,13 +395,15 @@ class Options:
             )
         return True
 
-    @property
+    @cached_property
     def verbose_name_raw(self):
         """Return the untranslated verbose name."""
+        if isinstance(self.verbose_name, str):
+            return self.verbose_name
         with override(None):
             return str(self.verbose_name)
 
-    @property
+    @cached_property
     def swapped(self):
         """
         Has this model been swapped out for another? If so, return the model
@@ -429,6 +431,10 @@ class Options:
                     return swapped_for
         return None
 
+    def setting_changed(self, *, setting, **kwargs):
+        if setting == self.swappable and "swapped" in self.__dict__:
+            del self.swapped
+
     @cached_property
     def managers(self):
         managers = []
@@ -690,16 +696,24 @@ class Options:
                 return res
         return []
 
-    def get_parent_list(self):
+    @cached_property
+    def all_parents(self):
         """
-        Return all the ancestors of this model as a list ordered by MRO.
+        Return all the ancestors of this model as a tuple ordered by MRO.
         Useful for determining if something is an ancestor, regardless of lineage.
         """
         result = OrderedSet(self.parents)
         for parent in self.parents:
-            for ancestor in parent._meta.get_parent_list():
+            for ancestor in parent._meta.all_parents:
                 result.add(ancestor)
-        return list(result)
+        return tuple(result)
+
+    def get_parent_list(self):
+        """
+        Return all the ancestors of this model as a list ordered by MRO.
+        Backward compatibility method.
+        """
+        return list(self.all_parents)
 
     def get_ancestor_link(self, ancestor):
         """
@@ -962,11 +976,15 @@ class Options:
     @cached_property
     def _property_names(self):
         """Return a set of the names of the properties defined on the model."""
-        names = []
-        for name in dir(self.model):
-            attr = inspect.getattr_static(self.model, name)
-            if isinstance(attr, property):
-                names.append(name)
+        names = set()
+        seen = set()
+        for klass in self.model.__mro__:
+            names |= {
+                name
+                for name, value in klass.__dict__.items()
+                if isinstance(value, property) and name not in seen
+            }
+            seen |= set(klass.__dict__)
         return frozenset(names)
 
     @cached_property
diff --git a/django/db/models/query.py b/django/db/models/query.py
index b9e2d1f7e5..cb5c63c0d1 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -788,7 +788,7 @@ class QuerySet(AltersData):
         # model to detect the inheritance pattern ConcreteGrandParent ->
         # MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy
         # would not identify that case as involving multiple tables.
-        for parent in self.model._meta.get_parent_list():
+        for parent in self.model._meta.all_parents:
             if parent._meta.concrete_model is not self.model._meta.concrete_model:
                 raise ValueError("Can't bulk create a multi-table inherited model")
         if not objs:
@@ -2186,8 +2186,7 @@ class RawQuerySet:
         converter = connections[self.db].introspection.identifier_converter
         model_fields = {}
         for field in self.model._meta.fields:
-            name, column = field.get_attname_column()
-            model_fields[converter(column)] = field
+            model_fields[converter(field.column)] = field
         return model_fields
 
 
diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
index e1041b9653..1bf396723e 100644
--- a/django/db/models/query_utils.py
+++ b/django/db/models/query_utils.py
@@ -175,6 +175,19 @@ class Q(tree.Node):
     def __hash__(self):
         return hash(self.identity)
 
+    @cached_property
+    def referenced_base_fields(self):
+        """
+        Retrieve all base fields referenced directly or through F expressions
+        excluding any fields referenced through joins.
+        """
+        # Avoid circular imports.
+        from django.db.models.sql import query
+
+        return {
+            child.split(LOOKUP_SEP, 1)[0] for child in query.get_children_from_q(self)
+        }
+
 
 class DeferredAttribute:
     """
@@ -340,38 +353,37 @@ class RegisterLookupMixin:
     _unregister_class_lookup = classmethod(_unregister_class_lookup)
 
 
-def select_related_descend(field, restricted, requested, select_mask, reverse=False):
+def select_related_descend(field, restricted, requested, select_mask):
     """
-    Return True if this field should be used to descend deeper for
-    select_related() purposes. Used by both the query construction code
-    (compiler.get_related_selections()) and the model instance creation code
-    (compiler.klass_info).
+    Return whether `field` should be used to descend deeper for
+    `select_related()` purposes.
 
     Arguments:
-     * field - the field to be checked
-     * restricted - a boolean field, indicating if the field list has been
-       manually restricted using a requested clause)
-     * requested - The select_related() dictionary.
-     * select_mask - the dictionary of selected fields.
-     * reverse - boolean, True if we are checking a reverse select related
+     * `field` - the field to be checked. Can be either a `Field` or
+       `ForeignObjectRel` instance.
+     * `restricted` - a boolean field, indicating if the field list has been
+       manually restricted using a select_related() clause.
+     * `requested` - the select_related() dictionary.
+     * `select_mask` - the dictionary of selected fields.
     """
+    # Only relationships can be descended.
     if not field.remote_field:
         return False
-    if field.remote_field.parent_link and not reverse:
+    # Forward MTI parent links should not be explicitly descended as they are
+    # always JOIN'ed against (unless excluded by `select_mask`).
+    if getattr(field.remote_field, "parent_link", False):
         return False
-    if restricted:
-        if reverse and field.related_query_name() not in requested:
-            return False
-        if not reverse and field.name not in requested:
-            return False
-    if not restricted and field.null:
+    # When `select_related()` is used without a `*requested` mask all
+    # relationships are descended unless they are nullable.
+    if not restricted:
+        return not field.null
+    # When `select_related(*requested)` is used only fields that are part of
+    # `requested` should be descended.
+    if field.name not in requested:
         return False
-    if (
-        restricted
-        and select_mask
-        and field.name in requested
-        and field not in select_mask
-    ):
+    # Prevent invalid usages of `select_related()` and `only()`/`defer()` such
+    # as `select_related("a").only("b")` and `select_related("a").defer("a")`.
+    if select_mask and field not in select_mask:
         raise FieldError(
             f"Field {field.model._meta.object_name}.{field.name} cannot be both "
             "deferred and traversed using select_related at the same time."
@@ -403,8 +415,8 @@ def check_rel_lookup_compatibility(model, target_opts, field):
     def check(opts):
         return (
             model._meta.concrete_model == opts.concrete_model
-            or opts.concrete_model in model._meta.get_parent_list()
-            or model in opts.get_parent_list()
+            or opts.concrete_model in model._meta.all_parents
+            or model in opts.all_parents
         )
 
     # If the field is a primary key, then doing a query against the field's
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 9a0d2eb4e7..7377e555c3 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -1253,21 +1253,20 @@ class SQLCompiler:
 
         if restricted:
             related_fields = [
-                (o.field, o.related_model)
+                (o, o.field, o.related_model)
                 for o in opts.related_objects
                 if o.field.unique and not o.many_to_many
             ]
-            for related_field, model in related_fields:
-                related_select_mask = select_mask.get(related_field) or {}
+            for related_object, related_field, model in related_fields:
                 if not select_related_descend(
-                    related_field,
+                    related_object,
                     restricted,
                     requested,
-                    related_select_mask,
-                    reverse=True,
+                    select_mask,
                 ):
                     continue
 
+                related_select_mask = select_mask.get(related_object) or {}
                 related_field_name = related_field.related_query_name()
                 fields_found.add(related_field_name)
 
@@ -1280,7 +1279,7 @@ class SQLCompiler:
                     "model": model,
                     "field": related_field,
                     "reverse": True,
-                    "local_setter": related_field.remote_field.set_cached_value,
+                    "local_setter": related_object.set_cached_value,
                     "remote_setter": related_field.set_cached_value,
                     "from_parent": from_parent,
                 }
@@ -1296,7 +1295,7 @@ class SQLCompiler:
                     select_fields.append(len(select))
                     select.append((col, None))
                 klass_info["select_fields"] = select_fields
-                next = requested.get(related_field.related_query_name(), {})
+                next = requested.get(related_field_name, {})
                 next_klass_infos = self.get_related_selections(
                     select,
                     related_select_mask,
@@ -1391,7 +1390,7 @@ class SQLCompiler:
         def _get_parent_klass_info(klass_info):
             concrete_model = klass_info["model"]._meta.concrete_model
             for parent_model, parent_link in concrete_model._meta.parents.items():
-                parent_list = parent_model._meta.get_parent_list()
+                all_parents = parent_model._meta.all_parents
                 yield {
                     "model": parent_model,
                     "field": parent_link,
@@ -1402,7 +1401,7 @@ class SQLCompiler:
                         # Selected columns from a model or its parents.
                         if (
                             self.select[select_index][0].target.model == parent_model
-                            or self.select[select_index][0].target.model in parent_list
+                            or self.select[select_index][0].target.model in all_parents
                         )
                     ],
                 }
@@ -1621,11 +1620,12 @@ class SQLCompiler:
         # tuples with integers and strings. Flatten them out into strings.
         format_ = self.query.explain_info.format
         output_formatter = json.dumps if format_ and format_.lower() == "json" else str
-        for row in result[0]:
-            if not isinstance(row, str):
-                yield " ".join(output_formatter(c) for c in row)
-            else:
-                yield row
+        for row in result:
+            for value in row:
+                if not isinstance(value, str):
+                    yield " ".join([output_formatter(c) for c in value])
+                else:
+                    yield value
 
 
 class SQLInsertCompiler(SQLCompiler):
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 5100869b34..a7bc0610c8 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -696,6 +696,7 @@ class Query(BaseExpression):
         # except if the alias is the base table since it must be present in the
         # query on both sides.
         initial_alias = self.get_initial_alias()
+        rhs = rhs.clone()
         rhs.bump_prefix(self, exclude={initial_alias})
 
         # Work out how to relabel the rhs aliases, if necessary.
@@ -790,46 +791,44 @@ class Query(BaseExpression):
         if select_mask is None:
             select_mask = {}
         select_mask[opts.pk] = {}
-        # All concrete fields that are not part of the defer mask must be
-        # loaded. If a relational field is encountered it gets added to the
-        # mask for it be considered if `select_related` and the cycle continues
-        # by recursively calling this function.
-        for field in opts.concrete_fields:
+        # All concrete fields and related objects that are not part of the
+        # defer mask must be included. If a relational field is encountered it
+        # gets added to the mask for it be considered if `select_related` and
+        # the cycle continues by recursively calling this function.
+        for field in opts.concrete_fields + opts.related_objects:
             field_mask = mask.pop(field.name, None)
-            field_att_mask = mask.pop(field.attname, None)
+            field_att_mask = None
+            if field_attname := getattr(field, "attname", None):
+                field_att_mask = mask.pop(field_attname, None)
             if field_mask is None and field_att_mask is None:
                 select_mask.setdefault(field, {})
             elif field_mask:
                 if not field.is_relation:
                     raise FieldError(next(iter(field_mask)))
+                # Virtual fields such as many-to-many and generic foreign keys
+                # cannot be effectively deferred. Historically, they were
+                # allowed to be passed to QuerySet.defer(). Ignore such field
+                # references until a layer of validation at mask alteration
+                # time is eventually implemented.
+                if field.many_to_many:
+                    continue
                 field_select_mask = select_mask.setdefault(field, {})
-                related_model = field.remote_field.model._meta.concrete_model
+                related_model = field.related_model._meta.concrete_model
                 self._get_defer_select_mask(
                     related_model._meta, field_mask, field_select_mask
                 )
-        # Remaining defer entries must be references to reverse relationships.
-        # The following code is expected to raise FieldError if it encounters
-        # a malformed defer entry.
+        # Remaining defer entries must be references to filtered relations
+        # otherwise they are surfaced as missing field errors.
         for field_name, field_mask in mask.items():
             if filtered_relation := self._filtered_relations.get(field_name):
                 relation = opts.get_field(filtered_relation.relation_name)
                 field_select_mask = select_mask.setdefault((field_name, relation), {})
-                field = relation.field
+                related_model = relation.related_model._meta.concrete_model
+                self._get_defer_select_mask(
+                    related_model._meta, field_mask, field_select_mask
+                )
             else:
-                reverse_rel = opts.get_field(field_name)
-                # While virtual fields such as many-to-many and generic foreign
-                # keys cannot be effectively deferred we've historically
-                # allowed them to be passed to QuerySet.defer(). Ignore such
-                # field references until a layer of validation at mask
-                # alteration time will be implemented eventually.
-                if not hasattr(reverse_rel, "field"):
-                    continue
-                field = reverse_rel.field
-                field_select_mask = select_mask.setdefault(field, {})
-            related_model = field.model._meta.concrete_model
-            self._get_defer_select_mask(
-                related_model._meta, field_mask, field_select_mask
-            )
+                opts.get_field(field_name)
         return select_mask
 
     def _get_only_select_mask(self, opts, mask, select_mask=None):
@@ -839,13 +838,7 @@ class Query(BaseExpression):
         # Only include fields mentioned in the mask.
         for field_name, field_mask in mask.items():
             field = opts.get_field(field_name)
-            # Retrieve the actual field associated with reverse relationships
-            # as that's what is expected in the select mask.
-            if field in opts.related_objects:
-                field_key = field.field
-            else:
-                field_key = field
-            field_select_mask = select_mask.setdefault(field_key, {})
+            field_select_mask = select_mask.setdefault(field, {})
             if field_mask:
                 if not field.is_relation:
                     raise FieldError(next(iter(field_mask)))
diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py
index 26ef09ce49..fe0e1fa599 100644
--- a/django/dispatch/dispatcher.py
+++ b/django/dispatch/dispatcher.py
@@ -244,7 +244,9 @@ class Signal:
                 return responses
 
         else:
-            sync_send = list
+
+            async def sync_send():
+                return []
 
         responses, async_responses = await asyncio.gather(
             sync_send(),
@@ -380,7 +382,9 @@ class Signal:
                 return responses
 
         else:
-            sync_send = list
+
+            async def sync_send():
+                return []
 
         async def asend_and_wrap_exception(receiver):
             try:
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
index d56fa0ea3e..d0d5c13b3d 100644
--- a/django/forms/boundfield.py
+++ b/django/forms/boundfield.py
@@ -298,6 +298,7 @@ class BoundField(RenderableFieldMixin):
             and self.field.help_text
             and not self.use_fieldset
             and self.auto_id
+            and not self.is_hidden
         ):
             attrs["aria-describedby"] = f"{self.auto_id}_helptext"
         return attrs
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 4ec7b7aee7..1a58a60743 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -792,13 +792,13 @@ class URLField(CharField):
     def to_python(self, value):
         def split_url(url):
             """
-            Return a list of url parts via urlparse.urlsplit(), or raise
+            Return a list of url parts via urlsplit(), or raise
             ValidationError for some malformed URLs.
             """
             try:
                 return list(urlsplit(url))
             except ValueError:
-                # urlparse.urlsplit can raise a ValueError with some
+                # urlsplit can raise a ValueError with some
                 # misformatted URLs.
                 raise ValidationError(self.error_messages["invalid"], code="invalid")
 
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index e279751601..c8e5893f19 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -1,6 +1,6 @@
 from django.core.exceptions import ValidationError
-from django.forms import Form
 from django.forms.fields import BooleanField, IntegerField
+from django.forms.forms import Form
 from django.forms.renderers import get_default_renderer
 from django.forms.utils import ErrorList, RenderableFormMixin
 from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput
diff --git a/django/forms/jinja2/django/forms/widgets/clearable_file_input.html b/django/forms/jinja2/django/forms/widgets/clearable_file_input.html
index 93812837f0..4f3a93627f 100644
--- a/django/forms/jinja2/django/forms/widgets/clearable_file_input.html
+++ b/django/forms/jinja2/django/forms/widgets/clearable_file_input.html
@@ -1,5 +1,5 @@
 {% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
-<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
+<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}{% if widget.attrs.checked %} checked{% endif %}>
 <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
 {{ widget.input_text }}:{% endif %}
 <input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
diff --git a/django/forms/models.py b/django/forms/models.py
index cd6986d72a..8084e16c8d 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -11,6 +11,7 @@ from django.core.exceptions import (
     ImproperlyConfigured,
     ValidationError,
 )
+from django.core.validators import ProhibitNullCharactersValidator
 from django.db.models.utils import AltersData
 from django.forms.fields import ChoiceField, Field
 from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass
@@ -23,6 +24,7 @@ from django.forms.widgets import (
     SelectMultiple,
 )
 from django.utils.choices import BaseChoiceIterator
+from django.utils.hashable import make_hashable
 from django.utils.text import capfirst, get_text_list
 from django.utils.translation import gettext
 from django.utils.translation import gettext_lazy as _
@@ -834,8 +836,8 @@ class BaseModelFormSet(BaseFormSet, AltersData):
                     (
                         d._get_pk_val()
                         if hasattr(d, "_get_pk_val")
-                        # Prevent "unhashable type: list" errors later on.
-                        else tuple(d) if isinstance(d, list) else d
+                        # Prevent "unhashable type" errors later on.
+                        else make_hashable(d)
                     )
                     for d in row_data
                 )
@@ -1140,7 +1142,7 @@ class BaseInlineFormSet(BaseModelFormSet):
         if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name:
             fk_value = getattr(self.instance, self.fk.remote_field.field_name)
             fk_value = getattr(fk_value, "pk", fk_value)
-        setattr(form.instance, self.fk.get_attname(), fk_value)
+        setattr(form.instance, self.fk.attname, fk_value)
         return form
 
     @classmethod
@@ -1214,20 +1216,19 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
         if len(fks_to_parent) == 1:
             fk = fks_to_parent[0]
-            parent_list = parent_model._meta.get_parent_list()
-            parent_list.append(parent_model)
+            all_parents = (*parent_model._meta.all_parents, parent_model)
             if (
                 not isinstance(fk, ForeignKey)
                 or (
                     # ForeignKey to proxy models.
                     fk.remote_field.model._meta.proxy
-                    and fk.remote_field.model._meta.proxy_for_model not in parent_list
+                    and fk.remote_field.model._meta.proxy_for_model not in all_parents
                 )
                 or (
                     # ForeignKey to concrete models.
                     not fk.remote_field.model._meta.proxy
                     and fk.remote_field.model != parent_model
-                    and fk.remote_field.model not in parent_list
+                    and fk.remote_field.model not in all_parents
                 )
             ):
                 raise ValueError(
@@ -1240,18 +1241,17 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
             )
     else:
         # Try to discover what the ForeignKey from model to parent_model is
-        parent_list = parent_model._meta.get_parent_list()
-        parent_list.append(parent_model)
+        all_parents = (*parent_model._meta.all_parents, parent_model)
         fks_to_parent = [
             f
             for f in opts.fields
             if isinstance(f, ForeignKey)
             and (
                 f.remote_field.model == parent_model
-                or f.remote_field.model in parent_list
+                or f.remote_field.model in all_parents
                 or (
                     f.remote_field.model._meta.proxy
-                    and f.remote_field.model._meta.proxy_for_model in parent_list
+                    and f.remote_field.model._meta.proxy_for_model in all_parents
                 )
             )
         ]
@@ -1488,6 +1488,10 @@ class ModelChoiceField(ChoiceField):
         self.limit_choices_to = limit_choices_to  # limit the queryset later.
         self.to_field_name = to_field_name
 
+    def validate_no_null_characters(self, value):
+        non_null_character_validator = ProhibitNullCharactersValidator()
+        return non_null_character_validator(value)
+
     def get_limit_choices_to(self):
         """
         Return ``limit_choices_to`` for this form field.
@@ -1552,6 +1556,7 @@ class ModelChoiceField(ChoiceField):
     def to_python(self, value):
         if value in self.empty_values:
             return None
+        self.validate_no_null_characters(value)
         try:
             key = self.to_field_name or "pk"
             if isinstance(value, self.queryset.model):
@@ -1632,6 +1637,7 @@ class ModelMultipleChoiceField(ModelChoiceField):
                 code="invalid_list",
             )
         for pk in value:
+            self.validate_no_null_characters(pk)
             try:
                 self.queryset.filter(**{key: pk})
             except (ValueError, TypeError):
diff --git a/django/forms/templates/django/forms/widgets/clearable_file_input.html b/django/forms/templates/django/forms/widgets/clearable_file_input.html
index 93812837f0..4f3a93627f 100644
--- a/django/forms/templates/django/forms/widgets/clearable_file_input.html
+++ b/django/forms/templates/django/forms/widgets/clearable_file_input.html
@@ -1,5 +1,5 @@
 {% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
-<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
+<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}{% if widget.attrs.checked %} checked{% endif %}>
 <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
 {{ widget.input_text }}:{% endif %}
 <input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
diff --git a/django/http/response.py b/django/http/response.py
index eecd972cd6..0d756403db 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -9,7 +9,7 @@ import time
 import warnings
 from email.header import Header
 from http.client import responses
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 
 from asgiref.sync import async_to_sync, sync_to_async
 
@@ -616,7 +616,7 @@ class HttpResponseRedirectBase(HttpResponse):
     def __init__(self, redirect_to, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self["Location"] = iri_to_uri(redirect_to)
-        parsed = urlparse(str(redirect_to))
+        parsed = urlsplit(str(redirect_to))
         if parsed.scheme and parsed.scheme not in self.allowed_schemes:
             raise DisallowedRedirect(
                 "Unsafe redirect to URL with protocol '%s'" % parsed.scheme
diff --git a/django/middleware/cache.py b/django/middleware/cache.py
index 0fdffe1bbe..196b1995ff 100644
--- a/django/middleware/cache.py
+++ b/django/middleware/cache.py
@@ -43,6 +43,8 @@ More details about how the caching works:
 
 """
 
+import time
+
 from django.conf import settings
 from django.core.cache import DEFAULT_CACHE_ALIAS, caches
 from django.utils.cache import (
@@ -53,6 +55,7 @@ from django.utils.cache import (
     patch_response_headers,
 )
 from django.utils.deprecation import MiddlewareMixin
+from django.utils.http import parse_http_date_safe
 
 
 class UpdateCacheMiddleware(MiddlewareMixin):
@@ -171,6 +174,15 @@ class FetchFromCacheMiddleware(MiddlewareMixin):
             request._cache_update_cache = True
             return None  # No cache information available, need to rebuild.
 
+        # Derive the age estimation of the cached response.
+        if (max_age_seconds := get_max_age(response)) is not None and (
+            expires_timestamp := parse_http_date_safe(response["Expires"])
+        ) is not None:
+            now_timestamp = int(time.time())
+            remaining_seconds = expires_timestamp - now_timestamp
+            # Use Age: 0 if local clock got turned back.
+            response["Age"] = max(0, max_age_seconds - remaining_seconds)
+
         # hit, return cached response
         request._cache_update_cache = False
         return response
diff --git a/django/middleware/common.py b/django/middleware/common.py
index 9f71b9d278..bf22d00f01 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -1,5 +1,5 @@
 import re
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 
 from django.conf import settings
 from django.core.exceptions import PermissionDenied
@@ -171,7 +171,7 @@ class BrokenLinkEmailsMiddleware(MiddlewareMixin):
 
         # The referer is equal to the current URL, ignoring the scheme (assumed
         # to be a poorly implemented bot).
-        parsed_referer = urlparse(referer)
+        parsed_referer = urlsplit(referer)
         if parsed_referer.netloc in ["", domain] and parsed_referer.path == uri:
             return True
 
diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
index f7943494ba..5ae1aae5c6 100644
--- a/django/middleware/csrf.py
+++ b/django/middleware/csrf.py
@@ -8,7 +8,7 @@ against request forgeries from other sites.
 import logging
 import string
 from collections import defaultdict
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 
 from django.conf import settings
 from django.core.exceptions import DisallowedHost, ImproperlyConfigured
@@ -174,7 +174,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
     @cached_property
     def csrf_trusted_origins_hosts(self):
         return [
-            urlparse(origin).netloc.lstrip("*")
+            urlsplit(origin).netloc.lstrip("*")
             for origin in settings.CSRF_TRUSTED_ORIGINS
         ]
 
@@ -190,7 +190,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
         """
         allowed_origin_subdomains = defaultdict(list)
         for parsed in (
-            urlparse(origin)
+            urlsplit(origin)
             for origin in settings.CSRF_TRUSTED_ORIGINS
             if "*" in origin
         ):
@@ -284,7 +284,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
         if request_origin in self.allowed_origins_exact:
             return True
         try:
-            parsed_origin = urlparse(request_origin)
+            parsed_origin = urlsplit(request_origin)
         except ValueError:
             return False
         request_scheme = parsed_origin.scheme
@@ -300,7 +300,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
             raise RejectRequest(REASON_NO_REFERER)
 
         try:
-            referer = urlparse(referer)
+            referer = urlsplit(referer)
         except ValueError:
             raise RejectRequest(REASON_MALFORMED_REFERER)
 
diff --git a/django/template/backends/base.py b/django/template/backends/base.py
index 991ce64cb7..f08eb2464b 100644
--- a/django/template/backends/base.py
+++ b/django/template/backends/base.py
@@ -23,6 +23,9 @@ class BaseEngine:
                 "Unknown parameters: {}".format(", ".join(params))
             )
 
+    def check(self, **kwargs):
+        return []
+
     @property
     def app_dirname(self):
         raise ImproperlyConfigured(
diff --git a/django/template/backends/django.py b/django/template/backends/django.py
index ba561bba9f..cf6874c408 100644
--- a/django/template/backends/django.py
+++ b/django/template/backends/django.py
@@ -1,8 +1,10 @@
+from collections import defaultdict
 from importlib import import_module
 from pkgutil import walk_packages
 
 from django.apps import apps
 from django.conf import settings
+from django.core.checks import Error, Warning
 from django.template import TemplateDoesNotExist
 from django.template.context import make_context
 from django.template.engine import Engine
@@ -25,6 +27,50 @@ class DjangoTemplates(BaseEngine):
         super().__init__(params)
         self.engine = Engine(self.dirs, self.app_dirs, **options)
 
+    def check(self, **kwargs):
+        return [
+            *self._check_string_if_invalid_is_string(),
+            *self._check_for_template_tags_with_the_same_name(),
+        ]
+
+    def _check_string_if_invalid_is_string(self):
+        value = self.engine.string_if_invalid
+        if not isinstance(value, str):
+            return [
+                Error(
+                    "'string_if_invalid' in TEMPLATES OPTIONS must be a string but "
+                    "got: %r (%s)." % (value, type(value)),
+                    obj=self,
+                    id="templates.E002",
+                )
+            ]
+        return []
+
+    def _check_for_template_tags_with_the_same_name(self):
+        libraries = defaultdict(set)
+
+        for module_name, module_path in get_template_tag_modules():
+            libraries[module_name].add(module_path)
+
+        for module_name, module_path in self.engine.libraries.items():
+            libraries[module_name].add(module_path)
+
+        errors = []
+
+        for library_name, items in libraries.items():
+            if len(items) > 1:
+                items = ", ".join(repr(item) for item in sorted(items))
+                errors.append(
+                    Warning(
+                        f"{library_name!r} is used for multiple template tag modules: "
+                        f"{items}",
+                        obj=self,
+                        id="templates.W003",
+                    )
+                )
+
+        return errors
+
     def from_string(self, template_code):
         return Template(self.engine.from_string(template_code), self)
 
diff --git a/django/template/context.py b/django/template/context.py
index 080a2dd9c0..0c28b479cd 100644
--- a/django/template/context.py
+++ b/django/template/context.py
@@ -31,7 +31,9 @@ class BaseContext:
     def _reset_dicts(self, value=None):
         builtins = {"True": True, "False": False, "None": None}
         self.dicts = [builtins]
-        if value is not None:
+        if isinstance(value, BaseContext):
+            self.dicts += value.dicts[1:]
+        elif value is not None:
             self.dicts.append(value)
 
     def __copy__(self):
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 8345c0a06c..fbe0856b64 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -646,7 +646,7 @@ def slice_filter(value, arg):
                 bits.append(int(x))
         return value[slice(*bits)]
 
-    except (ValueError, TypeError):
+    except (ValueError, TypeError, KeyError):
         return value  # Fail silently.
 
 
diff --git a/django/template/smartif.py b/django/template/smartif.py
index 0a917c2aaa..6a6a03dd84 100644
--- a/django/template/smartif.py
+++ b/django/template/smartif.py
@@ -3,7 +3,7 @@ Parser and utilities for the smart 'if' tag
 """
 
 # Using a simple top down parser, as described here:
-#    http://effbot.org/zone/simple-top-down-parsing.htm.
+#    https://11l-lang.org/archive/simple-top-down-parsing/
 # 'led' = left denotation
 # 'nud' = null denotation
 # 'bp' = binding power (left = lbp, right = rbp)
diff --git a/django/test/client.py b/django/test/client.py
index d1fd428ea8..a755aae05c 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -8,7 +8,7 @@ from functools import partial
 from http import HTTPStatus
 from importlib import import_module
 from io import BytesIO, IOBase
-from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
+from urllib.parse import unquote_to_bytes, urljoin, urlsplit
 
 from asgiref.sync import sync_to_async
 
@@ -458,11 +458,7 @@ class RequestFactory:
         return json.dumps(data, cls=self.json_encoder) if should_encode else data
 
     def _get_path(self, parsed):
-        path = parsed.path
-        # If there are parameters, add them
-        if parsed.params:
-            path += ";" + parsed.params
-        path = unquote_to_bytes(path)
+        path = unquote_to_bytes(parsed.path)
         # Replace the behavior where non-ASCII values in the WSGI environ are
         # arbitrarily decoded with ISO-8859-1.
         # Refs comment in `get_bytes_from_wsgi()`.
@@ -647,7 +643,7 @@ class RequestFactory:
         **extra,
     ):
         """Construct an arbitrary HTTP request."""
-        parsed = urlparse(str(path))  # path can be lazy
+        parsed = urlsplit(str(path))  # path can be lazy
         data = force_bytes(data, settings.DEFAULT_CHARSET)
         r = {
             "PATH_INFO": self._get_path(parsed),
@@ -671,8 +667,7 @@ class RequestFactory:
         # If QUERY_STRING is absent or empty, we want to extract it from the URL.
         if not r.get("QUERY_STRING"):
             # WSGI requires latin-1 encoded strings. See get_path_info().
-            query_string = parsed[4].encode().decode("iso-8859-1")
-            r["QUERY_STRING"] = query_string
+            r["QUERY_STRING"] = parsed.query.encode().decode("iso-8859-1")
         return self.request(**r)
 
 
@@ -688,7 +683,7 @@ class AsyncRequestFactory(RequestFactory):
     Once you have a request object you can pass it to any view function,
     including synchronous ones. The reason we have a separate class here is:
     a) this makes ASGIRequest subclasses, and
-    b) AsyncTestClient can subclass it.
+    b) AsyncClient can subclass it.
     """
 
     def _base_scope(self, **request):
@@ -748,7 +743,7 @@ class AsyncRequestFactory(RequestFactory):
         **extra,
     ):
         """Construct an arbitrary HTTP request."""
-        parsed = urlparse(str(path))  # path can be lazy.
+        parsed = urlsplit(str(path))  # path can be lazy.
         data = force_bytes(data, settings.DEFAULT_CHARSET)
         s = {
             "method": method,
@@ -772,7 +767,7 @@ class AsyncRequestFactory(RequestFactory):
         else:
             # If QUERY_STRING is absent or empty, we want to extract it from
             # the URL.
-            s["query_string"] = parsed[4]
+            s["query_string"] = parsed.query
         if headers:
             extra.update(HttpHeaders.to_asgi_names(headers))
         s["headers"] += [
@@ -817,7 +812,14 @@ class ClientMixin:
         return session
 
     async def asession(self):
-        return await sync_to_async(lambda: self.session)()
+        engine = import_module(settings.SESSION_ENGINE)
+        cookie = self.cookies.get(settings.SESSION_COOKIE_NAME)
+        if cookie:
+            return engine.SessionStore(cookie.value)
+        session = engine.SessionStore()
+        await session.asave()
+        self.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
+        return session
 
     def login(self, **credentials):
         """
@@ -893,7 +895,7 @@ class ClientMixin:
 
         await alogin(request, user, backend)
         # Save the session values.
-        await sync_to_async(request.session.save)()
+        await request.session.asave()
         self._set_login_cookies(request)
 
     def _set_login_cookies(self, request):
diff --git a/django/test/runner.py b/django/test/runner.py
index 8bb40a3413..27eb9613e9 100644
--- a/django/test/runner.py
+++ b/django/test/runner.py
@@ -706,6 +706,11 @@ class DiscoverRunner:
 
     @classmethod
     def add_arguments(cls, parser):
+        parser.add_argument(
+            "--failfast",
+            action="store_true",
+            help="Stops the test suite after the first failure.",
+        )
         parser.add_argument(
             "-t",
             "--top-level-directory",
diff --git a/django/test/selenium.py b/django/test/selenium.py
index 07df34ae1f..4ef5015556 100644
--- a/django/test/selenium.py
+++ b/django/test/selenium.py
@@ -83,7 +83,7 @@ class SeleniumTestCaseBase(type(LiveServerTestCase)):
         options = self.import_options(self.browser)()
         if self.headless:
             match self.browser:
-                case "chrome":
+                case "chrome" | "edge":
                     options.add_argument("--headless=new")
                 case "firefox":
                     options.add_argument("-headless")
@@ -142,6 +142,8 @@ class SeleniumTestCase(LiveServerTestCase, metaclass=SeleniumTestCaseBase):
 
                 test.__name__ = f"{name}_{screenshot_case}"
                 test.__qualname__ = f"{test.__qualname__}_{screenshot_case}"
+                test._screenshot_name = name
+                test._screenshot_case = screenshot_case
                 setattr(cls, test.__name__, test)
 
     @classproperty
@@ -191,10 +193,43 @@ class SeleniumTestCase(LiveServerTestCase, metaclass=SeleniumTestCaseBase):
             finally:
                 self.selenium.execute_script("localStorage.removeItem('theme');")
 
+    def set_emulated_media(self, *, media=None, features=None):
+        if self.browser not in {"chrome", "edge"}:
+            self.skipTest(
+                "Emulation.setEmulatedMedia is only supported on Chromium and "
+                "Chrome-based browsers. See https://chromedevtools.github.io/devtools-"
+                "protocol/1-3/Emulation/#method-setEmulatedMedia for more details."
+            )
+        params = {}
+        if media is not None:
+            params["media"] = media
+        if features is not None:
+            params["features"] = features
+
+        # Not using .execute_cdp_cmd() as it isn't supported by the remote web driver
+        # when using --selenium-hub.
+        self.selenium.execute(
+            driver_command="executeCdpCommand",
+            params={"cmd": "Emulation.setEmulatedMedia", "params": params},
+        )
+
+    @contextmanager
+    def high_contrast(self):
+        self.set_emulated_media(features=[{"name": "forced-colors", "value": "active"}])
+        with self.desktop_size():
+            try:
+                yield
+            finally:
+                self.set_emulated_media(
+                    features=[{"name": "forced-colors", "value": "none"}]
+                )
+
     def take_screenshot(self, name):
         if not self.screenshots:
             return
-        path = Path.cwd() / "screenshots" / f"{self._testMethodName}-{name}.png"
+        test = getattr(self, self._testMethodName)
+        filename = f"{test._screenshot_name}--{name}--{test._screenshot_case}.png"
+        path = Path.cwd() / "screenshots" / filename
         path.parent.mkdir(exist_ok=True, parents=True)
         self.selenium.save_screenshot(path)
 
diff --git a/django/test/testcases.py b/django/test/testcases.py
index ce681f287c..f1c6b5ae9c 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -1,6 +1,7 @@
 import difflib
 import json
 import logging
+import pickle
 import posixpath
 import sys
 import threading
@@ -20,7 +21,7 @@ from urllib.parse import (
     urljoin,
     urlparse,
     urlsplit,
-    urlunparse,
+    urlunsplit,
 )
 from urllib.request import url2pathname
 
@@ -92,6 +93,18 @@ def to_list(value):
     return value
 
 
+def is_pickable(obj):
+    """
+    Returns true if the object can be dumped and loaded through the pickle
+    module.
+    """
+    try:
+        pickle.loads(pickle.dumps(obj))
+    except (AttributeError, TypeError, pickle.PickleError):
+        return False
+    return True
+
+
 def assert_and_parse_html(self, html, user_msg, msg):
     try:
         dom = parse_html(html)
@@ -280,6 +293,8 @@ class SimpleTestCase(unittest.TestCase):
                 self.connection is None
                 and self.alias not in cls.databases
                 and self.alias != NO_DB_ALIAS
+                # Dynamically created connections are always allowed.
+                and self.alias in connections
             ):
                 # Connection has not yet been established, but the alias is not allowed.
                 message = cls._disallowed_database_msg % {
@@ -301,6 +316,23 @@ class SimpleTestCase(unittest.TestCase):
         """
         self._setup_and_call(result)
 
+    def __getstate__(self):
+        """
+        Make SimpleTestCase picklable for parallel tests using subtests.
+        """
+        state = super().__dict__
+        # _outcome and _subtest cannot be tested on picklability, since they
+        # contain the TestCase itself, leading to an infinite recursion.
+        if state["_outcome"]:
+            pickable_state = {"_outcome": None, "_subtest": None}
+            for key, value in state.items():
+                if key in pickable_state or not is_pickable(value):
+                    continue
+                pickable_state[key] = value
+            return pickable_state
+
+        return state
+
     def debug(self):
         """Perform the same as __call__(), without catching the exception."""
         debug_result = _DebugResult()
@@ -509,11 +541,9 @@ class SimpleTestCase(unittest.TestCase):
         def normalize(url):
             """Sort the URL's query string parameters."""
             url = str(url)  # Coerce reverse_lazy() URLs.
-            scheme, netloc, path, params, query, fragment = urlparse(url)
+            scheme, netloc, path, query, fragment = urlsplit(url)
             query_parts = sorted(parse_qsl(query))
-            return urlunparse(
-                (scheme, netloc, path, params, urlencode(query_parts), fragment)
-            )
+            return urlunsplit((scheme, netloc, path, urlencode(query_parts), fragment))
 
         if msg_prefix:
             msg_prefix += ": "
@@ -1251,6 +1281,18 @@ def connections_support_transactions(aliases=None):
     return all(conn.features.supports_transactions for conn in conns)
 
 
+def connections_support_savepoints(aliases=None):
+    """
+    Return whether or not all (or specified) connections support savepoints.
+    """
+    conns = (
+        connections.all()
+        if aliases is None
+        else (connections[alias] for alias in aliases)
+    )
+    return all(conn.features.uses_savepoints for conn in conns)
+
+
 class TestData:
     """
     Descriptor to provide TestCase instance isolation for attributes assigned
@@ -1325,10 +1367,17 @@ class TestCase(TransactionTestCase):
     def _databases_support_transactions(cls):
         return connections_support_transactions(cls.databases)
 
+    @classmethod
+    def _databases_support_savepoints(cls):
+        return connections_support_savepoints(cls.databases)
+
     @classmethod
     def setUpClass(cls):
         super().setUpClass()
-        if not cls._databases_support_transactions():
+        if not (
+            cls._databases_support_transactions()
+            and cls._databases_support_savepoints()
+        ):
             return
         cls.cls_atomics = cls._enter_atomics()
 
@@ -1356,7 +1405,10 @@ class TestCase(TransactionTestCase):
 
     @classmethod
     def tearDownClass(cls):
-        if cls._databases_support_transactions():
+        if (
+            cls._databases_support_transactions()
+            and cls._databases_support_savepoints()
+        ):
             cls._rollback_atomics(cls.cls_atomics)
             for conn in connections.all(initialized_only=True):
                 conn.close()
@@ -1382,6 +1434,15 @@ class TestCase(TransactionTestCase):
         if self.reset_sequences:
             raise TypeError("reset_sequences cannot be used on TestCase instances")
         self.atomics = self._enter_atomics()
+        if not self._databases_support_savepoints():
+            if self.fixtures:
+                for db_name in self._databases_names(include_mirrors=False):
+                    call_command(
+                        "loaddata",
+                        *self.fixtures,
+                        **{"verbosity": 0, "database": db_name},
+                    )
+            self.setUpTestData()
 
     def _fixture_teardown(self):
         if not self._databases_support_transactions():
@@ -1574,11 +1635,11 @@ class FSFilesHandler(WSGIHandler):
         * the host is provided as part of the base_url
         * the request's path isn't under the media path (or equal)
         """
-        return path.startswith(self.base_url[2]) and not self.base_url[1]
+        return path.startswith(self.base_url.path) and not self.base_url.netloc
 
     def file_path(self, url):
         """Return the relative path to the file on disk for the given URL."""
-        relative_url = url.removeprefix(self.base_url[2])
+        relative_url = url.removeprefix(self.base_url.path)
         return url2pathname(relative_url)
 
     def get_response(self, request):
diff --git a/django/urls/converters.py b/django/urls/converters.py
index 9652823508..b36cde1497 100644
--- a/django/urls/converters.py
+++ b/django/urls/converters.py
@@ -1,5 +1,8 @@
 import functools
 import uuid
+import warnings
+
+from django.utils.deprecation import RemovedInDjango60Warning
 
 
 class IntConverter:
@@ -53,14 +56,23 @@ REGISTERED_CONVERTERS = {}
 
 
 def register_converter(converter, type_name):
+    if type_name in REGISTERED_CONVERTERS or type_name in DEFAULT_CONVERTERS:
+        # RemovedInDjango60Warning: when the deprecation ends, replace with
+        # raise ValueError(f"Converter {type_name} is already registered.")
+        warnings.warn(
+            f"Converter {type_name!r} is already registered. Support for overriding "
+            "registered converters is deprecated and will be removed in Django 6.0.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
     REGISTERED_CONVERTERS[type_name] = converter()
     get_converters.cache_clear()
 
+    from django.urls.resolvers import _route_to_regex
+
+    _route_to_regex.cache_clear()
+
 
 @functools.cache
 def get_converters():
     return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS}
-
-
-def get_converter(raw_converter):
-    return get_converters()[raw_converter]
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index 3607c84228..c667d7f268 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -19,14 +19,14 @@ from asgiref.local import Local
 from django.conf import settings
 from django.core.checks import Error, Warning
 from django.core.checks.urls import check_resolver
-from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
+from django.core.exceptions import ImproperlyConfigured
 from django.utils.datastructures import MultiValueDict
 from django.utils.functional import cached_property
 from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
 from django.utils.regex_helper import _lazy_re_compile, normalize
 from django.utils.translation import get_language
 
-from .converters import get_converter
+from .converters import get_converters
 from .exceptions import NoReverseMatch, Resolver404
 from .utils import get_callable
 
@@ -128,9 +128,6 @@ def get_ns_resolver(ns_pattern, resolver, converters):
 
 
 class LocaleRegexDescriptor:
-    def __init__(self, attr):
-        self.attr = attr
-
     def __get__(self, instance, cls=None):
         """
         Return a compiled regular expression based on the active language.
@@ -140,15 +137,23 @@ class LocaleRegexDescriptor:
         # As a performance optimization, if the given regex string is a regular
         # string (not a lazily-translated string proxy), compile it once and
         # avoid per-language compilation.
-        pattern = getattr(instance, self.attr)
+        pattern = instance._regex
         if isinstance(pattern, str):
-            instance.__dict__["regex"] = instance._compile(pattern)
+            instance.__dict__["regex"] = self._compile(pattern)
             return instance.__dict__["regex"]
         language_code = get_language()
         if language_code not in instance._regex_dict:
-            instance._regex_dict[language_code] = instance._compile(str(pattern))
+            instance._regex_dict[language_code] = self._compile(str(pattern))
         return instance._regex_dict[language_code]
 
+    def _compile(self, regex):
+        try:
+            return re.compile(regex)
+        except re.error as e:
+            raise ImproperlyConfigured(
+                f'"{regex}" is not a valid regular expression: {e}'
+            ) from e
+
 
 class CheckURLMixin:
     def describe(self):
@@ -164,12 +169,11 @@ class CheckURLMixin:
         """
         Check that the pattern does not begin with a forward slash.
         """
-        regex_pattern = self.regex.pattern
         if not settings.APPEND_SLASH:
             # Skip check as it can be useful to start a URL pattern with a slash
             # when APPEND_SLASH=False.
             return []
-        if regex_pattern.startswith(("/", "^/", "^\\/")) and not regex_pattern.endswith(
+        if self._regex.startswith(("/", "^/", "^\\/")) and not self._regex.endswith(
             "/"
         ):
             warning = Warning(
@@ -186,7 +190,7 @@ class CheckURLMixin:
 
 
 class RegexPattern(CheckURLMixin):
-    regex = LocaleRegexDescriptor("_regex")
+    regex = LocaleRegexDescriptor()
 
     def __init__(self, regex, name=None, is_endpoint=False):
         self._regex = regex
@@ -219,8 +223,7 @@ class RegexPattern(CheckURLMixin):
         return warnings
 
     def _check_include_trailing_dollar(self):
-        regex_pattern = self.regex.pattern
-        if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
+        if self._regex.endswith("$") and not self._regex.endswith(r"\$"):
             return [
                 Warning(
                     "Your URL pattern {} uses include with a route ending with a '$'. "
@@ -232,15 +235,6 @@ class RegexPattern(CheckURLMixin):
         else:
             return []
 
-    def _compile(self, regex):
-        """Compile and return the given regular expression."""
-        try:
-            return re.compile(regex)
-        except re.error as e:
-            raise ImproperlyConfigured(
-                '"%s" is not a valid regular expression: %s' % (regex, e)
-            ) from e
-
     def __str__(self):
         return str(self._regex)
 
@@ -249,62 +243,83 @@ _PATH_PARAMETER_COMPONENT_RE = _lazy_re_compile(
     r"<(?:(?P<converter>[^>:]+):)?(?P<parameter>[^>]+)>"
 )
 
+whitespace_set = frozenset(string.whitespace)
 
-def _route_to_regex(route, is_endpoint=False):
+
+@functools.lru_cache
+def _route_to_regex(route, is_endpoint):
     """
     Convert a path pattern into a regular expression. Return the regular
     expression and a dictionary mapping the capture names to the converters.
     For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
     and {'pk': <django.urls.converters.IntConverter>}.
     """
-    original_route = route
     parts = ["^"]
+    all_converters = get_converters()
     converters = {}
-    while True:
-        match = _PATH_PARAMETER_COMPONENT_RE.search(route)
-        if not match:
-            parts.append(re.escape(route))
-            break
-        elif not set(match.group()).isdisjoint(string.whitespace):
+    previous_end = 0
+    for match_ in _PATH_PARAMETER_COMPONENT_RE.finditer(route):
+        if not whitespace_set.isdisjoint(match_[0]):
             raise ImproperlyConfigured(
-                "URL route '%s' cannot contain whitespace in angle brackets "
-                "<…>." % original_route
+                f"URL route {route!r} cannot contain whitespace in angle brackets <…>."
             )
-        parts.append(re.escape(route[: match.start()]))
-        route = route[match.end() :]
-        parameter = match["parameter"]
+        # Default to make converter "str" if unspecified (parameter always
+        # matches something).
+        raw_converter, parameter = match_.groups(default="str")
         if not parameter.isidentifier():
             raise ImproperlyConfigured(
-                "URL route '%s' uses parameter name %r which isn't a valid "
-                "Python identifier." % (original_route, parameter)
+                f"URL route {route!r} uses parameter name {parameter!r} which "
+                "isn't a valid Python identifier."
             )
-        raw_converter = match["converter"]
-        if raw_converter is None:
-            # If a converter isn't specified, the default is `str`.
-            raw_converter = "str"
         try:
-            converter = get_converter(raw_converter)
+            converter = all_converters[raw_converter]
         except KeyError as e:
             raise ImproperlyConfigured(
-                "URL route %r uses invalid converter %r."
-                % (original_route, raw_converter)
+                f"URL route {route!r} uses invalid converter {raw_converter!r}."
             ) from e
         converters[parameter] = converter
-        parts.append("(?P<" + parameter + ">" + converter.regex + ")")
+
+        start, end = match_.span()
+        parts.append(re.escape(route[previous_end:start]))
+        previous_end = end
+        parts.append(f"(?P<{parameter}>{converter.regex})")
+
+    parts.append(re.escape(route[previous_end:]))
     if is_endpoint:
         parts.append(r"\Z")
     return "".join(parts), converters
 
 
+class LocaleRegexRouteDescriptor:
+    def __get__(self, instance, cls=None):
+        """
+        Return a compiled regular expression based on the active language.
+        """
+        if instance is None:
+            return self
+        # As a performance optimization, if the given route is a regular string
+        # (not a lazily-translated string proxy), compile it once and avoid
+        # per-language compilation.
+        if isinstance(instance._route, str):
+            instance.__dict__["regex"] = re.compile(instance._regex)
+            return instance.__dict__["regex"]
+        language_code = get_language()
+        if language_code not in instance._regex_dict:
+            instance._regex_dict[language_code] = re.compile(
+                _route_to_regex(str(instance._route), instance._is_endpoint)[0]
+            )
+        return instance._regex_dict[language_code]
+
+
 class RoutePattern(CheckURLMixin):
-    regex = LocaleRegexDescriptor("_route")
+    regex = LocaleRegexRouteDescriptor()
 
     def __init__(self, route, name=None, is_endpoint=False):
         self._route = route
+        self._regex, self.converters = _route_to_regex(str(route), is_endpoint)
         self._regex_dict = {}
         self._is_endpoint = is_endpoint
         self.name = name
-        self.converters = _route_to_regex(str(route), is_endpoint)[1]
 
     def match(self, path):
         match = self.regex.search(path)
@@ -356,9 +371,6 @@ class RoutePattern(CheckURLMixin):
             warnings.append(Warning(msg % (self.describe(), "<"), id="urls.W010"))
         return warnings
 
-    def _compile(self, route):
-        return re.compile(_route_to_regex(route, self._is_endpoint)[0])
-
     def __str__(self):
         return str(self._route)
 
@@ -518,40 +530,8 @@ class URLResolver:
         messages = []
         for pattern in self.url_patterns:
             messages.extend(check_resolver(pattern))
-        messages.extend(self._check_custom_error_handlers())
         return messages or self.pattern.check()
 
-    def _check_custom_error_handlers(self):
-        messages = []
-        # All handlers take (request, exception) arguments except handler500
-        # which takes (request).
-        for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 1)]:
-            try:
-                handler = self.resolve_error_handler(status_code)
-            except (ImportError, ViewDoesNotExist) as e:
-                path = getattr(self.urlconf_module, "handler%s" % status_code)
-                msg = (
-                    "The custom handler{status_code} view '{path}' could not be "
-                    "imported."
-                ).format(status_code=status_code, path=path)
-                messages.append(Error(msg, hint=str(e), id="urls.E008"))
-                continue
-            signature = inspect.signature(handler)
-            args = [None] * num_parameters
-            try:
-                signature.bind(*args)
-            except TypeError:
-                msg = (
-                    "The custom handler{status_code} view '{path}' does not "
-                    "take the correct number of arguments ({args})."
-                ).format(
-                    status_code=status_code,
-                    path=handler.__module__ + "." + handler.__qualname__,
-                    args="request, exception" if num_parameters == 2 else "request",
-                )
-                messages.append(Error(msg, id="urls.E007"))
-        return messages
-
     def _populate(self):
         # Short-circuit if called recursively in this thread to prevent
         # infinite recursion. Concurrent threads may call this at the same
diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py
index 4d136dfa16..9d3c628f66 100644
--- a/django/utils/deprecation.py
+++ b/django/utils/deprecation.py
@@ -4,15 +4,16 @@ import warnings
 from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
 
 
-class RemovedInNextVersionWarning(DeprecationWarning):
+class RemovedInDjango60Warning(DeprecationWarning):
     pass
 
 
-class RemovedInDjango60Warning(PendingDeprecationWarning):
+class RemovedInDjango61Warning(PendingDeprecationWarning):
     pass
 
 
-RemovedAfterNextVersionWarning = RemovedInDjango60Warning
+RemovedInNextVersionWarning = RemovedInDjango60Warning
+RemovedAfterNextVersionWarning = RemovedInDjango61Warning
 
 
 class warn_about_renamed_method:
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
index 3bd456ca68..fae3271430 100644
--- a/django/utils/feedgenerator.py
+++ b/django/utils/feedgenerator.py
@@ -24,6 +24,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
 
 import datetime
 import email
+import mimetypes
 from io import StringIO
 from urllib.parse import urlparse
 
@@ -57,6 +58,53 @@ def get_tag_uri(url, date):
     return "tag:%s%s:%s/%s" % (bits.hostname, d, bits.path, bits.fragment)
 
 
+def _guess_stylesheet_mimetype(url):
+    """
+    Return the given stylesheet's mimetype tuple, using a slightly custom
+    version of Python's mimetypes.guess_type().
+    """
+    mimetypedb = mimetypes.MimeTypes()
+
+    # The official mimetype for XSLT files is technically `application/xslt+xml`
+    # but as of 2024 almost no browser supports that (they all expect text/xsl).
+    # On top of that, windows seems to assume that the type for xsl is text/xml.
+    mimetypedb.readfp(StringIO("text/xsl\txsl\ntext/xsl\txslt"))
+
+    return mimetypedb.guess_type(url)
+
+
+class Stylesheet:
+    """An RSS stylesheet"""
+
+    def __init__(self, url, mimetype="", media="screen"):
+        self._url = url
+        self._mimetype = mimetype
+        self.media = media
+
+    # Using a property to delay the evaluation of self._url as late as possible
+    # in case of a lazy object (like reverse_lazy(...) for example).
+    @property
+    def url(self):
+        return iri_to_uri(self._url)
+
+    @property
+    def mimetype(self):
+        if self._mimetype == "":
+            return _guess_stylesheet_mimetype(self.url)[0]
+        return self._mimetype
+
+    def __str__(self):
+        data = [f'href="{self.url}"']
+        if self.mimetype is not None:
+            data.append(f'type="{self.mimetype}"')
+        if self.media is not None:
+            data.append(f'media="{self.media}"')
+        return " ".join(data)
+
+    def __repr__(self):
+        return repr((self.url, self.mimetype, self.media))
+
+
 class SyndicationFeed:
     "Base class for all syndication feeds. Subclasses should provide write()"
 
@@ -75,12 +123,24 @@ class SyndicationFeed:
         feed_copyright=None,
         feed_guid=None,
         ttl=None,
+        stylesheets=None,
         **kwargs,
     ):
         def to_str(s):
             return str(s) if s is not None else s
 
+        def to_stylesheet(s):
+            return s if isinstance(s, Stylesheet) else Stylesheet(s)
+
         categories = categories and [str(c) for c in categories]
+
+        if stylesheets is not None:
+            if isinstance(stylesheets, (Stylesheet, str)):
+                raise TypeError(
+                    f"stylesheets should be a list, not {stylesheets.__class__}"
+                )
+            stylesheets = [to_stylesheet(s) for s in stylesheets]
+
         self.feed = {
             "title": to_str(title),
             "link": iri_to_uri(link),
@@ -95,6 +155,7 @@ class SyndicationFeed:
             "feed_copyright": to_str(feed_copyright),
             "id": feed_guid or link,
             "ttl": to_str(ttl),
+            "stylesheets": stylesheets,
             **kwargs,
         }
         self.items = []
@@ -166,6 +227,12 @@ class SyndicationFeed:
         """
         pass
 
+    def add_stylesheets(self, handler):
+        """
+        Add stylesheet(s) to the feed. Called from write().
+        """
+        pass
+
     def item_attributes(self, item):
         """
         Return extra attributes to place on each item (i.e. item/entry) element.
@@ -228,6 +295,9 @@ class RssFeed(SyndicationFeed):
     def write(self, outfile, encoding):
         handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
         handler.startDocument()
+        # Any stylesheet must come after the start of the document but before any tag.
+        # https://www.w3.org/Style/styling-XML.en.html
+        self.add_stylesheets(handler)
         handler.startElement("rss", self.rss_attributes())
         handler.startElement("channel", self.root_attributes())
         self.add_root_elements(handler)
@@ -247,6 +317,10 @@ class RssFeed(SyndicationFeed):
             self.add_item_elements(handler, item)
             handler.endElement("item")
 
+    def add_stylesheets(self, handler):
+        for stylesheet in self.feed["stylesheets"] or []:
+            handler.processingInstruction("xml-stylesheet", stylesheet)
+
     def add_root_elements(self, handler):
         handler.addQuickElement("title", self.feed["title"])
         handler.addQuickElement("link", self.feed["link"])
diff --git a/django/utils/html.py b/django/utils/html.py
index 06e8824f56..22d3ae42fa 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -16,25 +16,27 @@ from django.utils.safestring import SafeData, SafeString, mark_safe
 from django.utils.text import normalize_newlines
 
 # https://html.spec.whatwg.org/#void-elements
-VOID_ELEMENTS = {
-    "area",
-    "base",
-    "br",
-    "col",
-    "embed",
-    "hr",
-    "img",
-    "input",
-    "link",
-    "meta",
-    "param",
-    "source",
-    "track",
-    "wbr",
-    # Deprecated tags.
-    "frame",
-    "spacer",
-}
+VOID_ELEMENTS = frozenset(
+    (
+        "area",
+        "base",
+        "br",
+        "col",
+        "embed",
+        "hr",
+        "img",
+        "input",
+        "link",
+        "meta",
+        "param",
+        "source",
+        "track",
+        "wbr",
+        # Deprecated tags.
+        "frame",
+        "spacer",
+    )
+)
 
 
 @keep_lazy(SafeString)
diff --git a/django/utils/http.py b/django/utils/http.py
index 78dfee7fee..bf783562dd 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
 from email.utils import formatdate
 from urllib.parse import quote, unquote
 from urllib.parse import urlencode as original_urlencode
-from urllib.parse import urlparse
+from urllib.parse import urlsplit
 
 from django.utils.datastructures import MultiValueDict
 from django.utils.regex_helper import _lazy_re_compile
@@ -271,11 +271,11 @@ def url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
 
 def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
     # Chrome considers any URL with more than two slashes to be absolute, but
-    # urlparse is not so flexible. Treat any url with three slashes as unsafe.
+    # urlsplit is not so flexible. Treat any url with three slashes as unsafe.
     if url.startswith("///"):
         return False
     try:
-        url_info = urlparse(url)
+        url_info = urlsplit(url)
     except ValueError:  # e.g. invalid IPv6 addresses
         return False
     # Forbid URLs like http:///example.com - with a scheme, but without a hostname.
diff --git a/django/utils/inspect.py b/django/utils/inspect.py
index 28418f7312..4e065f0347 100644
--- a/django/utils/inspect.py
+++ b/django/utils/inspect.py
@@ -16,13 +16,18 @@ def _get_callable_parameters(meth_or_func):
     return _get_func_parameters(func, remove_first=is_method)
 
 
+ARG_KINDS = frozenset(
+    {
+        inspect.Parameter.POSITIONAL_ONLY,
+        inspect.Parameter.KEYWORD_ONLY,
+        inspect.Parameter.POSITIONAL_OR_KEYWORD,
+    }
+)
+
+
 def get_func_args(func):
     params = _get_callable_parameters(func)
-    return [
-        param.name
-        for param in params
-        if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
-    ]
+    return [param.name for param in params if param.kind in ARG_KINDS]
 
 
 def get_func_full_args(func):
@@ -63,9 +68,7 @@ def func_accepts_var_args(func):
 
 def method_has_no_args(meth):
     """Return True if a method only accepts 'self'."""
-    count = len(
-        [p for p in _get_callable_parameters(meth) if p.kind == p.POSITIONAL_OR_KEYWORD]
-    )
+    count = len([p for p in _get_callable_parameters(meth) if p.kind in ARG_KINDS])
     return count == 0 if inspect.ismethod(meth) else count == 1
 
 
diff --git a/django/utils/log.py b/django/utils/log.py
index fd0cc1bdc1..a25b97a7d5 100644
--- a/django/utils/log.py
+++ b/django/utils/log.py
@@ -92,6 +92,13 @@ class AdminEmailHandler(logging.Handler):
         )
 
     def emit(self, record):
+        # Early return when no email will be sent.
+        if (
+            not settings.ADMINS
+            # Method not overridden.
+            and self.send_mail.__func__ is AdminEmailHandler.send_mail
+        ):
+            return
         try:
             request = record.request
             subject = "%s (%s IP): %s" % (
diff --git a/django/utils/text.py b/django/utils/text.py
index e20e852178..f08fb2458a 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -5,12 +5,20 @@ import reprlib
 import secrets
 import unicodedata
 from collections.abc import Sized
+from collections import deque
 from gzip import GzipFile
 from gzip import compress as gzip_compress
+from html import escape
+from html.parser import HTMLParser
 from io import BytesIO
 
 from django.core.exceptions import SuspiciousFileOperation
-from django.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
+from django.utils.functional import (
+    SimpleLazyObject,
+    cached_property,
+    keep_lazy_text,
+    lazy,
+)
 from django.utils.regex_helper import _lazy_re_compile
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy, pgettext
@@ -27,9 +35,6 @@ def capfirst(x):
 
 
 # Set up regular expressions
-re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
-re_chars = _lazy_re_compile(r"<[^>]+?>|(.)", re.S)
-re_tag = _lazy_re_compile(r"<(/)?(\S+?)(?:(\s*/)|\s.*?)?>", re.S)
 re_newlines = _lazy_re_compile(r"\r\n|\r")  # Used in normalize_newlines
 re_camel_case = _lazy_re_compile(r"(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))")
 
@@ -83,6 +88,101 @@ def add_truncation_text(text, truncate=None):
     return f"{text}{truncate}"
 
 
+def calculate_truncate_chars_length(length, replacement):
+    truncate_len = length
+    for char in add_truncation_text("", replacement):
+        if not unicodedata.combining(char):
+            truncate_len -= 1
+            if truncate_len == 0:
+                break
+    return truncate_len
+
+
+class TruncateHTMLParser(HTMLParser):
+    class TruncationCompleted(Exception):
+        pass
+
+    def __init__(self, *, length, replacement, convert_charrefs=True):
+        super().__init__(convert_charrefs=convert_charrefs)
+        self.tags = deque()
+        self.output = ""
+        self.remaining = length
+        self.replacement = replacement
+
+    @cached_property
+    def void_elements(self):
+        from django.utils.html import VOID_ELEMENTS
+
+        return VOID_ELEMENTS
+
+    def handle_startendtag(self, tag, attrs):
+        self.handle_starttag(tag, attrs)
+        if tag not in self.void_elements:
+            self.handle_endtag(tag)
+
+    def handle_starttag(self, tag, attrs):
+        self.output += self.get_starttag_text()
+        if tag not in self.void_elements:
+            self.tags.appendleft(tag)
+
+    def handle_endtag(self, tag):
+        if tag not in self.void_elements:
+            self.output += f"</{tag}>"
+            try:
+                self.tags.remove(tag)
+            except ValueError:
+                pass
+
+    def handle_data(self, data):
+        data, output = self.process(data)
+        data_len = len(data)
+        if self.remaining < data_len:
+            self.remaining = 0
+            self.output += add_truncation_text(output, self.replacement)
+            raise self.TruncationCompleted
+        self.remaining -= data_len
+        self.output += output
+
+    def feed(self, data):
+        try:
+            super().feed(data)
+        except self.TruncationCompleted:
+            self.output += "".join([f"</{tag}>" for tag in self.tags])
+            self.tags.clear()
+            self.reset()
+        else:
+            # No data was handled.
+            self.reset()
+
+
+class TruncateCharsHTMLParser(TruncateHTMLParser):
+    def __init__(self, *, length, replacement, convert_charrefs=True):
+        self.length = length
+        self.processed_chars = 0
+        super().__init__(
+            length=calculate_truncate_chars_length(length, replacement),
+            replacement=replacement,
+            convert_charrefs=convert_charrefs,
+        )
+
+    def process(self, data):
+        self.processed_chars += len(data)
+        if (self.processed_chars == self.length) and (
+            len(self.output) + len(data) == len(self.rawdata)
+        ):
+            self.output += data
+            raise self.TruncationCompleted
+        output = escape("".join(data[: self.remaining]))
+        return data, output
+
+
+class TruncateWordsHTMLParser(TruncateHTMLParser):
+    def process(self, data):
+        data = re.split(r"(?<=\S)\s+(?=\S)", data)
+        output = escape(" ".join(data[: self.remaining]))
+        return data, output
+
+
 class Truncator(SimpleLazyObject):
     """
     An object used to truncate text, either by characters or words.
@@ -107,21 +207,20 @@ class Truncator(SimpleLazyObject):
         """
         self._setup()
         length = int(num)
+        if length <= 0:
+            return ""
         text = unicodedata.normalize("NFC", self._wrapped)
 
-        # Calculate the length to truncate to (max length - end_text length)
-        truncate_len = length
-        for char in add_truncation_text("", truncate):
-            if not unicodedata.combining(char):
-                truncate_len -= 1
-                if truncate_len == 0:
-                    break
         if html:
-            return self._truncate_html(length, truncate, text, truncate_len, False)
-        return self._text_chars(length, truncate, text, truncate_len)
+            parser = TruncateCharsHTMLParser(length=length, replacement=truncate)
+            parser.feed(text)
+            parser.close()
+            return parser.output
+        return self._text_chars(length, truncate, text)
 
-    def _text_chars(self, length, truncate, text, truncate_len):
+    def _text_chars(self, length, truncate, text):
         """Truncate a string after a certain number of chars."""
+        truncate_len = calculate_truncate_chars_length(length, truncate)
         s_len = 0
         end_index = None
         for i, char in enumerate(text):
@@ -147,8 +246,13 @@ class Truncator(SimpleLazyObject):
         """
         self._setup()
         length = int(num)
+        if length <= 0:
+            return ""
         if html:
-            return self._truncate_html(length, truncate, self._wrapped, length, True)
+            parser = TruncateWordsHTMLParser(length=length, replacement=truncate)
+            parser.feed(self._wrapped)
+            parser.close()
+            return parser.output
         return self._text_words(length, truncate)
 
     def _text_words(self, length, truncate):
@@ -163,94 +267,6 @@ class Truncator(SimpleLazyObject):
             return add_truncation_text(" ".join(words), truncate)
         return " ".join(words)
 
-    def _truncate_html(self, length, truncate, text, truncate_len, words):
-        """
-        Truncate HTML to a certain number of chars (not counting tags and
-        comments), or, if words is True, then to a certain number of words.
-        Close opened tags if they were correctly closed in the given HTML.
-
-        Preserve newlines in the HTML.
-        """
-        if words and length <= 0:
-            return ""
-
-        size_limited = False
-        if len(text) > self.MAX_LENGTH_HTML:
-            text = text[: self.MAX_LENGTH_HTML]
-            size_limited = True
-
-        html4_singlets = (
-            "br",
-            "col",
-            "link",
-            "base",
-            "img",
-            "param",
-            "area",
-            "hr",
-            "input",
-        )
-
-        # Count non-HTML chars/words and keep note of open tags
-        pos = 0
-        end_text_pos = 0
-        current_len = 0
-        open_tags = []
-
-        regex = re_words if words else re_chars
-
-        while current_len <= length:
-            m = regex.search(text, pos)
-            if not m:
-                # Checked through whole string
-                break
-            pos = m.end(0)
-            if m[1]:
-                # It's an actual non-HTML word or char
-                current_len += 1
-                if current_len == truncate_len:
-                    end_text_pos = pos
-                continue
-            # Check for tag
-            tag = re_tag.match(m[0])
-            if not tag or current_len >= truncate_len:
-                # Don't worry about non tags or tags after our truncate point
-                continue
-            closing_tag, tagname, self_closing = tag.groups()
-            # Element names are always case-insensitive
-            tagname = tagname.lower()
-            if self_closing or tagname in html4_singlets:
-                pass
-            elif closing_tag:
-                # Check for match in open tags list
-                try:
-                    i = open_tags.index(tagname)
-                except ValueError:
-                    pass
-                else:
-                    # SGML: An end tag closes, back to the matching start tag,
-                    # all unclosed intervening start tags with omitted end tags
-                    open_tags = open_tags[i + 1 :]
-            else:
-                # Add it to the start of the open tags list
-                open_tags.insert(0, tagname)
-
-        truncate_text = add_truncation_text("", truncate)
-
-        if current_len <= length:
-            if size_limited and truncate_text:
-                text += truncate_text
-            return text
-
-        out = text[:end_text_pos]
-        if truncate_text:
-            out += truncate_text
-        # Close any tags still open
-        for tag in open_tags:
-            out += "</%s>" % tag
-        # Return string
-        return out
-
 
 @keep_lazy_text
 def get_valid_filename(name):
@@ -491,7 +507,7 @@ class DebugRepr(reprlib.Repr):
         self.maxother = limit
         self.limit = limit
 
-        self.fillvalue = ""
+        self.fillvalue = "..."
         self.indent = 2
 
     def repr_str(self, x, level):
@@ -507,6 +523,8 @@ class DebugRepr(reprlib.Repr):
         if isinstance(value, Sized) and len(value) > self.limit:
             length = len(value)
             self.fillvalue = self.gen_trim_msg(length)
+        else:
+            self.fillvalue = "..."
         return self.repr(value)
 
     def gen_trim_msg(self, length):
diff --git a/django/utils/version.py b/django/utils/version.py
index ecd41dac89..55509f4c85 100644
--- a/django/utils/version.py
+++ b/django/utils/version.py
@@ -13,13 +13,12 @@ PYPY = sys.implementation.name == "pypy"
 # or later". So that third-party apps can use these values, each constant
 # should remain as long as the oldest supported Django version supports that
 # Python version.
-PY36 = sys.version_info >= (3, 6)
-PY37 = sys.version_info >= (3, 7)
 PY38 = sys.version_info >= (3, 8)
 PY39 = sys.version_info >= (3, 9)
 PY310 = sys.version_info >= (3, 10)
 PY311 = sys.version_info >= (3, 11)
 PY312 = sys.version_info >= (3, 12)
+PY313 = sys.version_info >= (3, 13)
 
 
 def get_version(version=None):
diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py
index 7ea8a540de..3b868bcf29 100644
--- a/django/views/decorators/debug.py
+++ b/django/views/decorators/debug.py
@@ -47,7 +47,6 @@ def sensitive_variables(*variables):
 
             try:
                 file_path = inspect.getfile(wrapped_func)
-                _, first_file_line = inspect.getsourcelines(wrapped_func)
             except TypeError:  # Raises for builtins or native functions.
                 raise ValueError(
                     f"{func.__name__} cannot safely be wrapped by "
@@ -55,7 +54,10 @@ def sensitive_variables(*variables):
                     "Python file (not a builtin or from a native extension)."
                 )
             else:
-                key = hash(f"{file_path}:{first_file_line}")
+                # A source file may not be available (e.g. in .pyc-only builds),
+                # use the first line number instead.
+                first_line_number = wrapped_func.__code__.co_firstlineno
+                key = hash(f"{file_path}:{first_line_number}")
 
             if variables:
                 coroutine_functions_to_sensitive_variables[key] = variables
diff --git a/django/views/templates/csrf_403.html b/django/views/templates/csrf_403.html
index 402a2c6cdd..ee81b04bcc 100644
--- a/django/views/templates/csrf_403.html
+++ b/django/views/templates/csrf_403.html
@@ -4,11 +4,11 @@
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <meta name="robots" content="NONE,NOARCHIVE">
   <title>403 Forbidden</title>
-  <style type="text/css">
+  <style>
     html * { padding:0; margin:0; }
     body * { padding:10px 20px; }
     body * * { padding:0; }
-    body { font:small sans-serif; background:#eee; color:#000; }
+    body { font-family: sans-serif; background:#eee; color:#000; }
     body>div { border-bottom:1px solid #ddd; }
     h1 { font-weight:normal; margin-bottom:.4em; }
     h1 span { font-size:60%; color:#666; font-weight:normal; }
@@ -53,7 +53,7 @@
     <li>Your browser is accepting cookies.</li>
 
     <li>The view function passes a <code>request</code> to the template’s <a
-    href="https://docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
+    href="https://docs.djangoproject.com/en/{{ docs_version }}/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
     method.</li>
 
     <li>In the template, there is a <code>{% templatetag openblock %} csrf_token
diff --git a/django/views/templates/default_urlconf.html b/django/views/templates/default_urlconf.html
index f9e278006d..8a8a2b1e17 100644
--- a/django/views/templates/default_urlconf.html
+++ b/django/views/templates/default_urlconf.html
@@ -2,251 +2,240 @@
 <!doctype html>
 {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
 <html lang="{{ LANGUAGE_CODE|default:'en-us' }}" dir="{{ LANGUAGE_BIDI|yesno:'rtl,ltr,auto' }}">
-    <head>
-        <meta charset="utf-8">
-        <title>{% translate "The install worked successfully! Congratulations!" %}</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1">
-        <style>
-          html {
-            line-height: 1.15;
-          }
-          a {
-            color: #19865C;
-          }
-          header {
-            border-bottom: 1px solid #efefef;
-          }
-          body {
-            max-width: 960px;
-            color: #525252;
-            font-family: "Segoe UI", system-ui, sans-serif;
-            margin: 0 auto;
-          }
-          main {
-            text-align: center;
-          }
-          h1, h2, h3, h4, h5, p, ul {
-            padding: 0;
-            margin: 0;
-            font-weight: 400;
-          }
-          header {
-            display: grid;
-            grid-template-columns: auto auto;
-            align-items: self-end;
-            justify-content: space-between;
-            gap: 7px;
-            padding-top: 20px;
-            padding-bottom: 10px;
-          }
-          .logo {
-            font-weight: 700;
-            font-size: 1.375rem;
-            text-decoration: none;
-          }
-          .figure {
-            margin-top: 19vh;
-            max-width: 265px;
-            position: relative;
-            z-index: -9;
-            overflow: visible;
-          }
-          .exhaust__line {
-            animation: thrust 70ms 100 ease-in-out alternate;
-          }
-          .smoke {
-            animation: smoke .1s 70 ease-in-out alternate;
-          }
-          @keyframes smoke {
-            0% {
-              transform: translate3d(-5px, 0, 0);
-            }
-            100% {
-              transform: translate3d(5px, 0, 0);
-            }
-          }
-          .flame {
-            animation: burnInner2 .1s 70 ease-in-out alternate;
-          }
-          @keyframes burnInner2 {
-            0% {
-              transform: translate3d(0, 0, 0);
-            }
-            100% {
-              transform: translate3d(0, 3px, 0);
-            }
-          }
-          @keyframes thrust {
-            0% {
-              opacity: 1;
-            }
-            100% {
-              opacity: .5;
-            }
-          }
-          @media (prefers-reduced-motion: reduce) {
-            .exhaust__line,
-            .smoke,
-            .flame {
-              animation: none;
-            }
-          }
-          h1 {
-            font-size: 1.375rem;
-            max-width: 32rem;
-            margin: 5px auto 0;
-          }
-          main p {
-            line-height: 1.25;
-            max-width: 26rem;
-            margin: 15px auto 0;
-          }
-          footer {
-            display: grid;
-            grid-template-columns: 1fr 1fr 1fr;
-            gap: 5px;
-            padding: 25px 0;
-            position: fixed;
-            box-sizing: border-box;
-            left: 50%;
-            bottom: 0;
-            width: 960px;
-            transform: translateX(-50%);
-            transform-style: preserve-3d;
-            border-top: 1px solid #efefef;
-          }
-          .option {
-            display: grid;
-            grid-template-columns: min-content 1fr;
-            gap: 10px;
-            box-sizing: border-box;
-            text-decoration: none;
-          }
-          .option svg {
-            width: 1.5rem;
-            height: 1.5rem;
-            fill: gray;
-            border: 1px solid #d6d6d6;
-            padding: 5px;
-            border-radius: 100%;
-          }
-          .option p {
-            font-weight: 300;
-            line-height: 1.25;
-            color: #525252;
-            display: table;
-          }
-          .option .option__heading {
-            color: #19865C;
-            font-size: 1.25rem;
-            font-weight: 400;
-          }
-          @media (max-width: 996px) {
-            body, footer {
-              max-width: 780px;
-            }
-          }
-          @media (max-width: 800px) {
-            footer {
-              height: 100%;
-              grid-template-columns: 1fr;
-              gap: 60px;
-              position: relative;
-              padding: 25px;
-            }
-            .figure {
-              margin-top: 10px;
-            }
-            main {
-              padding: 0 25px;
-            }
-            main h1 {
-              font-size: 1.25rem;
-            }
-            header {
-              grid-template-columns: 1fr;
-              padding-left: 20px;
-              padding-right: 20px;
-            }
-            footer {
-              width: 100%;
-              margin-top: 50px;
-            }
-          }
-          @media (min-width: 801px) and (max-height: 730px) {
-            .figure {
-              margin-top: 80px;
-            }
-          }
-          @media (min-width: 801px) and (max-height: 600px) {
-            footer {
-              position: relative;
-              margin: 135px auto 0;
-            }
-            .figure {
-              margin-top: 50px;
-            }
-          }
-          .sr-only {
-            clip: rect(1px, 1px, 1px, 1px);
-            clip-path: inset(50%);
-            height: 1px;
-            overflow: hidden;
-            position: absolute;
-            white-space: nowrap;
-            width: 1px;
-          }
-        </style>
-    </head>
-    <body>
-      <header>
-          <a class="logo" href="https://www.djangoproject.com/" target="_blank" rel="noopener">
-            django
-          </a>
-          <p>{% blocktranslate %}View <a href="https://docs.djangoproject.com/en/{{ version }}/releases/" target="_blank" rel="noopener">release notes</a> for Django {{ version }}{% endblocktranslate %}</p>
-      </header>
-      <main>
-        <svg class="figure" viewBox="0 0 508 268" aria-hidden="true">
-          <path d="M305.2 156.6c0 4.6-.5 9-1.6 13.2-2.5-4.4-5.6-8.4-9.2-12-4.6-4.6-10-8.4-16-11.2 2.8-11.2 4.5-22.9 5-34.6 1.8 1.4 3.5 2.9 5 4.5 10.5 10.3 16.8 24.5 16.8 40.1zm-75-10c-6 2.8-11.4 6.6-16 11.2-3.5 3.6-6.6 7.6-9.1 12-1-4.3-1.6-8.7-1.6-13.2 0-15.7 6.3-29.9 16.6-40.1 1.6-1.6 3.3-3.1 5.1-4.5.6 11.8 2.2 23.4 5 34.6z" fill="#2E3B39" fill-rule="nonzero"/>
-          <path d="M282.981 152.6c16.125-48.1 6.375-104-29.25-142.6-35.625 38.5-45.25 94.5-29.25 142.6h58.5z" stroke="#FFF" stroke-width="3.396" fill="#6DDCBD"/>
-          <path d="M271 29.7c-4.4-10.6-9.9-20.6-16.6-29.7-6.7 9-12.2 19-16.6 29.7H271z" stroke="#FFF" stroke-width="3" fill="#2E3B39"/>
-          <circle stroke="#FFF" stroke-width="7" fill="none" cx="254.3" cy="76.8" r="12.2"/>
-          <path class="smoke" d="M507.812 234.24c0-2.16-.632-4.32-1.58-6.24-3.318-6.72-11.85-11.52-21.804-11.52-1.106 0-2.212.12-3.318.24-.474-11.52-12.956-20.76-28.282-20.76-3.318 0-6.636.48-9.638 1.32-4.74-6.72-14.062-11.28-24.806-11.28-.79 0-1.58 0-2.37.12-.79 0-1.58-.12-2.37-.12-10.744 0-20.066 4.56-24.806 11.28a35.326 35.326 0 00-9.638-1.32c-15.642 0-28.282 9.6-28.282 21.48 0 1.32.158 2.76.474 3.96a26.09 26.09 0 00-4.424-.36c-8.058 0-15.01 3.12-19.118 7.8-3.476-1.68-7.742-2.76-12.324-2.76-12.008 0-21.804 7.08-22.752 15.96h-.158c-9.322 0-17.38 4.32-20.856 10.44-4.108-3.6-10.27-6-17.222-6h-1.264c-6.794 0-12.956 2.28-17.222 6-3.476-6.12-11.534-10.44-20.856-10.44h-.158c-.948-9-10.744-15.96-22.752-15.96-4.582 0-8.69.96-12.324 2.76-4.108-4.68-11.06-7.8-19.118-7.8-1.422 0-3.002.12-4.424.36.316-1.32.474-2.64.474-3.96 0-11.88-12.64-21.48-28.282-21.48-3.318 0-6.636.48-9.638 1.32-4.74-6.72-14.062-11.28-24.806-11.28-.79 0-1.58 0-2.37.12-.79 0-1.58-.12-2.37-.12-10.744 0-20.066 4.56-24.806 11.28a35.326 35.326 0 00-9.638-1.32c-15.326 0-27.808 9.24-28.282 20.76-1.106-.12-2.212-.24-3.318-.24-9.954 0-18.486 4.8-21.804 11.52-.948 1.92-1.58 4.08-1.58 6.24 0 4.8 2.528 9.12 6.636 12.36-.79 1.44-1.264 3.12-1.264 4.8 0 7.2 7.742 13.08 17.222 13.08h462.15c9.48 0 17.222-5.88 17.222-13.08 0-1.68-.474-3.36-1.264-4.8 4.582-3.24 7.11-7.56 7.11-12.36z" fill="#E6E9EE"/>
-          <path fill="#6DDCBD" d="M239 152h30v8h-30z"/>
-          <path class="exhaust__line" fill="#E6E9EE" d="M250 172h7v90h-7z"/>
-          <path class="flame" d="M250.27 178.834l-5.32-8.93s-2.47-5.7 3.458-6.118h10.26s6.232.266 3.306 6.194l-5.244 8.93s-3.23 4.37-6.46 0v-.076z" fill="#AA2247"/>
+  <head>
+    <meta charset="utf-8">
+    <title>{% translate "The install worked successfully! Congratulations!" %}</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <style>
+      html {
+        line-height: 1.15;
+      }
+      a {
+        color: #092e20;
+      }
+      body {
+        max-width: 960px;
+        color: #525252;
+        font-family: "Segoe UI", system-ui, sans-serif;
+        margin: 0 auto;
+      }
+      main {
+        text-align: center;
+      }
+      h1, h2, h3, h4, h5, p, ul {
+        padding: 0;
+        margin: 0;
+        font-weight: 400;
+      }
+      .logo {
+        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19.16 6.696'%3E%3Cg fill='rgb(9 46 32)'%3E%3Cpath d='m2.259 3.55e-8h1.048v4.851c-0.5377 0.1021-0.9324 0.1429-1.361 0.1429-1.279 0-1.946-0.5784-1.946-1.688 0-1.068 0.7078-1.763 1.804-1.763 0.1701 0 0.2994 0.01365 0.456 0.0544v-1.598zm0 2.442c-0.1225-0.04079-0.2246-0.0544-0.3539-0.0544-0.5308 0-0.8371 0.3267-0.8371 0.8983 0 0.5582 0.2927 0.8644 0.8303 0.8644 0.1156 0 0.211-0.00681 0.3607-0.02713z'/%3E%3Cpath d='m4.975 1.618v2.43c0 0.837-0.06125 1.239-0.245 1.586-0.1702 0.3335-0.3948 0.5444-0.8575 0.7758l-0.9732-0.4628c0.4628-0.2178 0.6874-0.4082 0.8303-0.701 0.1498-0.2994 0.1974-0.6466 0.1974-1.559v-2.069zm-1.048-1.613h1.048v1.075h-1.048z'/%3E%3Cpath d='m5.608 1.857c0.4628-0.2178 0.9052-0.313 1.388-0.313 0.5377 0 0.8915 0.1429 1.048 0.422 0.08842 0.1565 0.1156 0.3606 0.1156 0.7963v2.13c-0.4696 0.06814-1.062 0.1157-1.497 0.1157-0.8779 0-1.273-0.3063-1.273-0.9868 0-0.7351 0.524-1.075 1.81-1.184v-0.2314c0-0.1905-0.09527-0.2585-0.3607-0.2585-0.3879 0-0.8235 0.1088-1.232 0.3198v-0.8099zm1.64 1.667c-0.6942 0.06814-0.9188 0.177-0.9188 0.4492 0 0.2042 0.1293 0.2995 0.4152 0.2995 0.1566 0 0.2994-0.01365 0.5036-0.04759z'/%3E%3Cpath d='m8.671 1.782c0.6193-0.1634 1.13-0.2382 1.647-0.2382 0.5377 0 0.9256 0.1224 1.157 0.3607 0.2178 0.2245 0.2858 0.4695 0.2858 0.9936v2.055h-1.048v-2.015c0-0.4015-0.1361-0.5513-0.5104-0.5513-0.1429 0-0.2722 0.01365-0.4833 0.0749v2.491h-1.048v-3.171z'/%3E%3Cpath d='m12.17 5.525c0.3676 0.1905 0.735 0.279 1.123 0.279 0.6873 0 0.98-0.279 0.98-0.946v-0.0205c-0.2042 0.1021-0.4084 0.143-0.6805 0.143-0.9188 0-1.504-0.6058-1.504-1.565 0-1.191 0.8644-1.865 2.396-1.865 0.4492 0 0.8644 0.04759 1.368 0.1496l-0.3589 0.7561c-0.2791-0.05449-0.02235-0.00733-0.2332-0.02775v0.1089l0.01357 0.4423 0.0068 0.5717c0.0068 0.1428 0.0068 0.2858 0.01365 0.4287v0.2859c0 0.8984-0.07486 1.32-0.2994 1.667-0.3267 0.5105-0.8916 0.7623-1.695 0.7623-0.4084 0-0.7622-0.06129-1.13-0.2042v-0.9663zm2.083-3.131h-0.03398-0.0749c-0.2041-0.00681-0.4423 0.04759-0.6057 0.1497-0.2517 0.143-0.3811 0.4016-0.3811 0.7691 0 0.5241 0.2587 0.8235 0.7214 0.8235 0.1429 0 0.2586-0.02722 0.3947-0.06805v-0.3607c0-0.1225-0.0068-0.2587-0.0068-0.4016l-0.0068-0.4832-0.0068-0.3471v-0.08171z'/%3E%3Cpath d='m17.48 1.53c1.048 0 1.688 0.6602 1.688 1.729 0 1.096-0.6669 1.783-1.729 1.783-1.048 0-1.695-0.6601-1.695-1.722 4.4e-5 -1.103 0.667-1.79 1.736-1.79zm-0.0205 2.668c0.4016 0 0.6398-0.3335 0.6398-0.912 0-0.5716-0.2314-0.9119-0.6329-0.9119-0.4152 0-0.6535 0.3336-0.6535 0.9119 4.4e-5 0.5786 0.2383 0.912 0.6465 0.912z'/%3E%3C/g%3E%3C/svg%3E");
+        color: #092e20;
+        background-position-x: center;
+        background-repeat: no-repeat;
+        font-size: 2rem;
+        font-weight: 700;
+        margin-top: 16px;
+        overflow: hidden;
+        text-decoration: none;
+        text-indent: 100%;
+        display: inline-block;
+      }
+      .figure {
+        margin-top: 22vh;
+        max-width: 265px;
+        position: relative;
+        z-index: -9;
+        overflow: visible;
+      }
+      .exhaust__line {
+        animation: thrust 70ms 100 ease-in-out alternate;
+      }
+      .smoke {
+        animation: smoke .1s 70 ease-in-out alternate;
+      }
+      @keyframes smoke {
+        0% {
+          transform: translate3d(-5px, 0, 0);
+        }
+        100% {
+          transform: translate3d(5px, 0, 0);
+        }
+      }
+      .flame {
+        animation: burnInner2 .1s 70 ease-in-out alternate;
+      }
+      @keyframes burnInner2 {
+        0% {
+          transform: translate3d(0, 0, 0);
+        }
+        100% {
+          transform: translate3d(0, 3px, 0);
+        }
+      }
+      @keyframes thrust {
+        0% {
+          opacity: 1;
+        }
+        100% {
+          opacity: .5;
+        }
+      }
+      @media (prefers-reduced-motion: reduce) {
+        .exhaust__line,
+        .smoke,
+        .flame {
+          animation: none;
+        }
+      }
+      h1 {
+        font-size: 1.375rem;
+        max-width: 32rem;
+        margin: 5px auto 0;
+      }
+      main p {
+        line-height: 1.25;
+        max-width: 26rem;
+        margin: 15px auto 0;
+      }
+      footer {
+        display: grid;
+        grid-template-columns: 1fr 1fr 1fr;
+        gap: 5px;
+        padding: 25px 0;
+        position: fixed;
+        box-sizing: border-box;
+        left: 50%;
+        bottom: 0;
+        width: 960px;
+        transform: translateX(-50%);
+        transform-style: preserve-3d;
+        border-top: 1px solid #efefef;
+      }
+      .option {
+        display: grid;
+        grid-template-columns: min-content 1fr;
+        gap: 10px;
+        box-sizing: border-box;
+        text-decoration: none;
+      }
+      .option svg {
+        width: 1.5rem;
+        height: 1.5rem;
+        fill: gray;
+        border: 1px solid #d6d6d6;
+        padding: 5px;
+        border-radius: 100%;
+      }
+      .option p {
+        font-weight: 300;
+        line-height: 1.25;
+        color: #525252;
+        display: table;
+      }
+      .option .option__heading {
+        color: #092e20;
+        font-size: 1.25rem;
+        font-weight: 400;
+      }
+      @media (max-width: 996px) {
+        body, footer {
+          max-width: 780px;
+        }
+      }
+      @media (max-width: 800px) {
+        footer {
+          height: 100%;
+          grid-template-columns: 1fr;
+          gap: 60px;
+          position: relative;
+          padding: 25px;
+        }
+        .figure {
+          margin-top: 10px;
+        }
+        main {
+          padding: 0 25px;
+        }
+        main h1 {
+          font-size: 1.25rem;
+        }
+        footer {
+          width: 100%;
+          margin-top: 50px;
+        }
+      }
+      @media (min-width: 801px) and (max-height: 730px) {
+        .figure {
+          margin-top: 80px;
+        }
+      }
+      @media (min-width: 801px) and (max-height: 600px) {
+        footer {
+          position: relative;
+          margin: 135px auto 0;
+        }
+        .figure {
+          margin-top: 50px;
+        }
+      }
+      .sr-only {
+        clip: rect(1px, 1px, 1px, 1px);
+        clip-path: inset(50%);
+        height: 1px;
+        overflow: hidden;
+        position: absolute;
+        white-space: nowrap;
+        width: 1px;
+      }
+    </style>
+  </head>
+  <body>
+    <main>
+      <svg class="figure" viewBox="0 0 508 268" aria-hidden="true">
+        <path d="M305.2 156.6c0 4.6-.5 9-1.6 13.2-2.5-4.4-5.6-8.4-9.2-12-4.6-4.6-10-8.4-16-11.2 2.8-11.2 4.5-22.9 5-34.6 1.8 1.4 3.5 2.9 5 4.5 10.5 10.3 16.8 24.5 16.8 40.1zm-75-10c-6 2.8-11.4 6.6-16 11.2-3.5 3.6-6.6 7.6-9.1 12-1-4.3-1.6-8.7-1.6-13.2 0-15.7 6.3-29.9 16.6-40.1 1.6-1.6 3.3-3.1 5.1-4.5.6 11.8 2.2 23.4 5 34.6z" fill="#2E3B39" fill-rule="nonzero"/>
+        <path d="M282.981 152.6c16.125-48.1 6.375-104-29.25-142.6-35.625 38.5-45.25 94.5-29.25 142.6h58.5z" stroke="#FFF" stroke-width="3.396" fill="#6DDCBD"/>
+        <path d="M271 29.7c-4.4-10.6-9.9-20.6-16.6-29.7-6.7 9-12.2 19-16.6 29.7H271z" stroke="#FFF" stroke-width="3" fill="#2E3B39"/>
+        <circle stroke="#FFF" stroke-width="7" fill="none" cx="254.3" cy="76.8" r="12.2"/>
+        <path class="smoke" d="M507.812 234.24c0-2.16-.632-4.32-1.58-6.24-3.318-6.72-11.85-11.52-21.804-11.52-1.106 0-2.212.12-3.318.24-.474-11.52-12.956-20.76-28.282-20.76-3.318 0-6.636.48-9.638 1.32-4.74-6.72-14.062-11.28-24.806-11.28-.79 0-1.58 0-2.37.12-.79 0-1.58-.12-2.37-.12-10.744 0-20.066 4.56-24.806 11.28a35.326 35.326 0 00-9.638-1.32c-15.642 0-28.282 9.6-28.282 21.48 0 1.32.158 2.76.474 3.96a26.09 26.09 0 00-4.424-.36c-8.058 0-15.01 3.12-19.118 7.8-3.476-1.68-7.742-2.76-12.324-2.76-12.008 0-21.804 7.08-22.752 15.96h-.158c-9.322 0-17.38 4.32-20.856 10.44-4.108-3.6-10.27-6-17.222-6h-1.264c-6.794 0-12.956 2.28-17.222 6-3.476-6.12-11.534-10.44-20.856-10.44h-.158c-.948-9-10.744-15.96-22.752-15.96-4.582 0-8.69.96-12.324 2.76-4.108-4.68-11.06-7.8-19.118-7.8-1.422 0-3.002.12-4.424.36.316-1.32.474-2.64.474-3.96 0-11.88-12.64-21.48-28.282-21.48-3.318 0-6.636.48-9.638 1.32-4.74-6.72-14.062-11.28-24.806-11.28-.79 0-1.58 0-2.37.12-.79 0-1.58-.12-2.37-.12-10.744 0-20.066 4.56-24.806 11.28a35.326 35.326 0 00-9.638-1.32c-15.326 0-27.808 9.24-28.282 20.76-1.106-.12-2.212-.24-3.318-.24-9.954 0-18.486 4.8-21.804 11.52-.948 1.92-1.58 4.08-1.58 6.24 0 4.8 2.528 9.12 6.636 12.36-.79 1.44-1.264 3.12-1.264 4.8 0 7.2 7.742 13.08 17.222 13.08h462.15c9.48 0 17.222-5.88 17.222-13.08 0-1.68-.474-3.36-1.264-4.8 4.582-3.24 7.11-7.56 7.11-12.36z" fill="#E6E9EE"/>
+        <path fill="#6DDCBD" d="M239 152h30v8h-30z"/>
+        <path class="exhaust__line" fill="#E6E9EE" d="M250 172h7v90h-7z"/>
+        <path class="flame" d="M250.27 178.834l-5.32-8.93s-2.47-5.7 3.458-6.118h10.26s6.232.266 3.306 6.194l-5.244 8.93s-3.23 4.37-6.46 0v-.076z" fill="#AA2247"/>
+      </svg>
+      <h1>{% translate "The install worked successfully! Congratulations!" %}</h1>
+      <p>
+        {% blocktranslate %}View <a href="https://docs.djangoproject.com/en/{{ version }}/releases/" target="_blank" rel="noopener">release notes</a> for Django {{ version }}{% endblocktranslate %}
+      </p>
+      <p>{% blocktranslate %}You are seeing this page because <a href="https://docs.djangoproject.com/en/{{ version }}/ref/settings/#debug" target="_blank" rel="noopener">DEBUG=True</a> is in your settings file and you have not configured any URLs.{% endblocktranslate %}</p>
+      <a class="logo" href="https://www.djangoproject.com/" target="_blank" rel="noopener">Django</a>
+    </main>
+    <footer>
+      <a class="option" href="https://docs.djangoproject.com/en/{{ version }}/" target="_blank" rel="noopener">
+        <svg viewBox="0 0 24 24" aria-hidden="true">
+          <path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6A4.997 4.997 0 017 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"></path>
         </svg>
-        <h1>{% translate "The install worked successfully! Congratulations!" %}</h1>
-        <p>{% blocktranslate %}You are seeing this page because <a href="https://docs.djangoproject.com/en/{{ version }}/ref/settings/#debug" target="_blank" rel="noopener">DEBUG=True</a> is in your settings file and you have not configured any URLs.{% endblocktranslate %}</p>
-      </main>
-      <footer>
-        <a class="option" href="https://docs.djangoproject.com/en/{{ version }}/" target="_blank" rel="noopener">
-          <svg viewBox="0 0 24 24" aria-hidden="true">
-            <path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6A4.997 4.997 0 017 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"></path>
-          </svg>
-          <p>
-            <span class="option__heading">{% translate "Django Documentation" %}</span><span class="sr-only">.</span><br>
-            {% translate 'Topics, references, &amp; how-to’s' %}
-          </p>
-        </a>
-        <a class="option" href="https://docs.djangoproject.com/en/{{ version }}/intro/tutorial01/" target="_blank" rel="noopener">
-          <svg viewBox="0 0 24 24" aria-hidden="true">
-            <path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"></path>
-          </svg>
-          <p>
-            <span class="option__heading">{% translate "Tutorial: A Polling App" %}</span><span class="sr-only">.</span><br>
-            {% translate "Get started with Django" %}
-          </p>
-        </a>
-        <a class="option" href="https://www.djangoproject.com/community/" target="_blank" rel="noopener">
-          <svg viewBox="0 0 24 24" aria-hidden="true">
-            <path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"></path>
-          </svg>
-          <p>
-            <span class="option__heading">{% translate "Django Community" %}</span><span class="sr-only">.</span><br>
-            {% translate "Connect, get help, or contribute" %}
-          </p>
-        </a>
-      </footer>
-    </body>
+        <p>
+          <span class="option__heading">{% translate "Django Documentation" %}</span><span class="sr-only">.</span><br>
+          {% translate 'Topics, references, &amp; how-to’s' %}
+        </p>
+      </a>
+      <a class="option" href="https://docs.djangoproject.com/en/{{ version }}/intro/tutorial01/" target="_blank" rel="noopener">
+        <svg viewBox="0 0 24 24" aria-hidden="true">
+          <path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"></path>
+        </svg>
+        <p>
+          <span class="option__heading">{% translate "Tutorial: A Polling App" %}</span><span class="sr-only">.</span><br>
+          {% translate "Get started with Django" %}
+        </p>
+      </a>
+      <a class="option" href="https://www.djangoproject.com/community/" target="_blank" rel="noopener">
+        <svg viewBox="0 0 24 24" aria-hidden="true">
+          <path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"></path>
+        </svg>
+        <p>
+          <span class="option__heading">{% translate "Django Community" %}</span><span class="sr-only">.</span><br>
+          {% translate "Connect, get help, or contribute" %}
+        </p>
+      </a>
+    </footer>
+  </body>
 </html>
diff --git a/django/views/templates/technical_404.html b/django/views/templates/technical_404.html
index f2bfe49372..f8d4e92c08 100644
--- a/django/views/templates/technical_404.html
+++ b/django/views/templates/technical_404.html
@@ -4,11 +4,11 @@
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <title>Page not found at {{ request.path_info }}</title>
   <meta name="robots" content="NONE,NOARCHIVE">
-  <style type="text/css">
+  <style>
     html * { padding:0; margin:0; }
     body * { padding:10px 20px; }
     body * * { padding:0; }
-    body { font:small sans-serif; background:#eee; color:#000; }
+    body { font-family: sans-serif; background:#eee; color:#000; }
     body > :where(header, main, footer) { border-bottom:1px solid #ddd; }
     h1 { font-weight:normal; margin-bottom:.4em; }
     h1 small { font-size:60%; color:#666; font-weight:normal; }
diff --git a/django/views/templates/technical_500.html b/django/views/templates/technical_500.html
index 305c4655ad..31f0dfe1b9 100644
--- a/django/views/templates/technical_500.html
+++ b/django/views/templates/technical_500.html
@@ -5,11 +5,11 @@
   <meta name="robots" content="NONE,NOARCHIVE">
   <title>{% if exception_type %}{{ exception_type }}{% else %}Report{% endif %}
          {% if request %} at {{ request.path_info }}{% endif %}</title>
-  <style type="text/css">
+  <style>
     html * { padding:0; margin:0; }
     body * { padding:10px 20px; }
     body * * { padding:0; }
-    body { font:small sans-serif; background-color:#fff; color:#000; }
+    body { font-family: sans-serif; background-color:#fff; color:#000; }
     body > :where(header, main, footer) { border-bottom:1px solid #ddd; }
     h1 { font-weight:normal; }
     h2 { margin-bottom:.8em; }
@@ -21,7 +21,7 @@
     tbody td, tbody th { vertical-align:top; padding:2px 3px; }
     thead th {
       padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
-      font-weight:normal; font-size:11px; border:1px solid #ddd;
+      font-weight:normal; font-size: 0.6875rem; border:1px solid #ddd;
     }
     tbody th { width:12em; text-align:right; color:#666; padding-right:.5em; }
     table.vars { margin:5px 10px 2px 40px; width: auto; }
@@ -61,9 +61,9 @@
     #requestinfo h3 { margin-bottom:-1em; }
     .error { background: #ffc; }
     .specific { color:#cc3300; font-weight:bold; }
-    h2 span.commands { font-size:.7em; font-weight:normal; }
+    h2 span.commands { font-size: 0.7rem; font-weight:normal; }
     span.commands a:link {color:#5E5694;}
-    pre.exception_value { font-family: sans-serif; color: #575757; font-size: 1.5em; margin: 10px 0 10px 0; }
+    pre.exception_value { font-family: sans-serif; color: #575757; font-size: 1.5rem; margin: 10px 0 10px 0; }
     .append-bottom { margin-bottom: 10px; }
     .fname { user-select: all; }
   </style>
diff --git a/docs/_ext/github_links.py b/docs/_ext/github_links.py
new file mode 100644
index 0000000000..08f4161a01
--- /dev/null
+++ b/docs/_ext/github_links.py
@@ -0,0 +1,149 @@
+import ast
+import functools
+import importlib.util
+import pathlib
+
+
+class CodeLocator(ast.NodeVisitor):
+    def __init__(self):
+        super().__init__()
+        self.current_path = []
+        self.node_line_numbers = {}
+        self.import_locations = {}
+
+    @classmethod
+    def from_code(cls, code):
+        tree = ast.parse(code)
+        locator = cls()
+        locator.visit(tree)
+        return locator
+
+    def visit_node(self, node):
+        self.current_path.append(node.name)
+        self.node_line_numbers[".".join(self.current_path)] = node.lineno
+        self.generic_visit(node)
+        self.current_path.pop()
+
+    def visit_FunctionDef(self, node):
+        self.visit_node(node)
+
+    def visit_ClassDef(self, node):
+        self.visit_node(node)
+
+    def visit_ImportFrom(self, node):
+        for alias in node.names:
+            if alias.asname:
+                # Exclude linking aliases (`import x as y`) to avoid confusion
+                # when clicking a source link to a differently named entity.
+                continue
+            if alias.name == "*":
+                # Resolve wildcard imports.
+                file = module_name_to_file_path(node.module)
+                file_contents = file.read_text(encoding="utf-8")
+                locator = CodeLocator.from_code(file_contents)
+                self.import_locations.update(locator.import_locations)
+                self.import_locations.update(
+                    {n: node.module for n in locator.node_line_numbers if "." not in n}
+                )
+            else:
+                self.import_locations[alias.name] = ("." * node.level) + (
+                    node.module or ""
+                )
+
+
+@functools.lru_cache(maxsize=1024)
+def get_locator(file):
+    file_contents = file.read_text(encoding="utf-8")
+    return CodeLocator.from_code(file_contents)
+
+
+class CodeNotFound(Exception):
+    pass
+
+
+def module_name_to_file_path(module_name):
+    # Avoid importlib machinery as locating a module involves importing its
+    # parent, which would trigger import side effects.
+
+    for suffix in [".py", "/__init__.py"]:
+        file_path = pathlib.Path(__file__).parents[2] / (
+            module_name.replace(".", "/") + suffix
+        )
+        if file_path.exists():
+            return file_path
+
+    raise CodeNotFound
+
+
+def get_path_and_line(module, fullname):
+    path = module_name_to_file_path(module_name=module)
+
+    locator = get_locator(path)
+
+    lineno = locator.node_line_numbers.get(fullname)
+
+    if lineno is not None:
+        return path, lineno
+
+    imported_object = fullname.split(".", maxsplit=1)[0]
+    try:
+        imported_path = locator.import_locations[imported_object]
+    except KeyError:
+        raise CodeNotFound
+
+    # From a statement such as:
+    # from . import y.z
+    # - either y.z might be an object in the parent module
+    # - or y might be a module, and z be an object in y
+    # also:
+    # - either the current file is x/__init__.py, and z would be in x.y
+    # - or the current file is x/a.py, and z would be in x.a.y
+    if path.name != "__init__.py":
+        # Look in parent module
+        module = module.rsplit(".", maxsplit=1)[0]
+    try:
+        imported_module = importlib.util.resolve_name(
+            name=imported_path, package=module
+        )
+    except ImportError as error:
+        raise ImportError(
+            f"Could not import '{imported_path}' in '{module}'."
+        ) from error
+    try:
+        return get_path_and_line(module=imported_module, fullname=fullname)
+    except CodeNotFound:
+        if "." not in fullname:
+            raise
+
+        first_element, remainder = fullname.rsplit(".", maxsplit=1)
+        # Retrying, assuming the first element of the fullname is a module.
+        return get_path_and_line(
+            module=f"{imported_module}.{first_element}", fullname=remainder
+        )
+
+
+def get_branch(version, next_version):
+    if version == next_version:
+        return "main"
+    else:
+        return f"stable/{version}.x"
+
+
+def github_linkcode_resolve(domain, info, *, version, next_version):
+    if domain != "py":
+        return None
+
+    if not (module := info["module"]):
+        return None
+
+    try:
+        path, lineno = get_path_and_line(module=module, fullname=info["fullname"])
+    except CodeNotFound:
+        return None
+
+    branch = get_branch(version=version, next_version=next_version)
+    relative_path = path.relative_to(pathlib.Path(__file__).parents[2])
+    # Use "/" explicitely to join the path parts since str(file), on Windows,
+    # uses the Windows path separator which is incorrect for URLs.
+    url_path = "/".join(relative_path.parts)
+    return f"https://github.com/django/django/blob/{branch}/{url_path}#L{lineno}"
diff --git a/docs/conf.py b/docs/conf.py
index 3c71ced6bf..c36a9a2022 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -9,6 +9,7 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
+import functools
 import sys
 from os.path import abspath, dirname, join
 
@@ -29,6 +30,10 @@ sys.path.insert(1, dirname(dirname(abspath(__file__))))
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 sys.path.append(abspath(join(dirname(__file__), "_ext")))
 
+# Use the module to GitHub url resolver, but import it after the _ext directoy
+# it lives in has been added to sys.path.
+import github_links  # NOQA
+
 # -- General configuration -----------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
@@ -40,8 +45,8 @@ extensions = [
     "djangodocs",
     "sphinx.ext.extlinks",
     "sphinx.ext.intersphinx",
-    "sphinx.ext.viewcode",
     "sphinx.ext.autosectionlabel",
+    "sphinx.ext.linkcode",
 ]
 
 # AutosectionLabel settings.
@@ -111,7 +116,7 @@ copyright = "Django Software Foundation and contributors"
 # built documents.
 #
 # The short X.Y version.
-version = "5.1"
+version = "5.2"
 # The full version, including alpha/beta/rc tags.
 try:
     from django import VERSION, get_version
@@ -128,7 +133,7 @@ else:
     release = django_release()
 
 # The "development version" of Django
-django_next_version = "5.1"
+django_next_version = "5.2"
 
 extlinks = {
     "bpo": ("https://bugs.python.org/issue?@action=redirect&bpo=%s", "bpo-%s"),
@@ -432,3 +437,9 @@ epub_cover = ("", "epub-cover.html")
 
 # If false, no index is generated.
 # epub_use_index = True
+
+linkcode_resolve = functools.partial(
+    github_links.github_linkcode_resolve,
+    version=version,
+    next_version=django_next_version,
+)
diff --git a/docs/faq/contributing.txt b/docs/faq/contributing.txt
index 769cddc488..71a6a7a476 100644
--- a/docs/faq/contributing.txt
+++ b/docs/faq/contributing.txt
@@ -10,8 +10,8 @@ How can I get started contributing code to Django?
 Thanks for asking! We've written an entire document devoted to this question.
 It's titled :doc:`Contributing to Django </internals/contributing/index>`.
 
-I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch?
-============================================================================================
+I submitted a bug fix several weeks ago. Why are you ignoring my contribution?
+==============================================================================
 
 Don't worry: We're not ignoring you!
 
@@ -34,21 +34,21 @@ that area of the code, to understand the problem and verify the fix:
   database, are those instructions clear enough even for someone not
   familiar with it?
 
-* If there are several patches attached to the ticket, is it clear what
-  each one does, which ones can be ignored and which matter?
+* If there are several branches linked to the ticket, is it clear what each one
+  does, which ones can be ignored and which matter?
 
-* Does the patch include a unit test? If not, is there a very clear
+* Does the change include a unit test? If not, is there a very clear
   explanation why not? A test expresses succinctly what the problem is,
-  and shows that the patch actually fixes it.
+  and shows that the branch actually fixes it.
 
-If your patch stands no chance of inclusion in Django, we won't ignore it --
-we'll just close the ticket. So if your ticket is still open, it doesn't mean
+If your contribution is not suitable for inclusion in Django, we won't ignore
+it -- we'll close the ticket. So if your ticket is still open, it doesn't mean
 we're ignoring you; it just means we haven't had time to look at it yet.
 
-When and how might I remind the team of a patch I care about?
-=============================================================
+When and how might I remind the team of a change I care about?
+==============================================================
 
-A polite, well-timed message to the mailing list is one way to get attention.
+A polite, well-timed message in the forum/branch is one way to get attention.
 To determine the right time, you need to keep an eye on the schedule. If you
 post your message right before a release deadline, you're not likely to get the
 sort of attention you require.
@@ -68,11 +68,11 @@ issue over and over again. This sort of behavior will not gain you any
 additional attention -- certainly not the attention that you need in order to
 get your issue addressed.
 
-But I've reminded you several times and you keep ignoring my patch!
-===================================================================
+But I've reminded you several times and you keep ignoring my contribution!
+==========================================================================
 
-Seriously - we're not ignoring you. If your patch stands no chance of
-inclusion in Django, we'll close the ticket. For all the other tickets, we
+Seriously - we're not ignoring you. If your contribution is not suitable for
+inclusion in Django, we will close the ticket. For all the other tickets, we
 need to prioritize our efforts, which means that some tickets will be
 addressed before others.
 
@@ -83,7 +83,7 @@ are edge cases.
 
 Another reason that a bug might be ignored for a while is if the bug is a
 symptom of a larger problem. While we can spend time writing, testing and
-applying lots of little patches, sometimes the right solution is to rebuild. If
+applying lots of little changes, sometimes the right solution is to rebuild. If
 a rebuild or refactor of a particular component has been proposed or is
 underway, you may find that bugs affecting that component will not get as much
 attention. Again, this is a matter of prioritizing scarce resources. By
diff --git a/docs/faq/general.txt b/docs/faq/general.txt
index 41be777b3b..3861ac7814 100644
--- a/docs/faq/general.txt
+++ b/docs/faq/general.txt
@@ -88,9 +88,10 @@ under a permissive open source license. :source:`A copy of the Python license
 Which sites use Django?
 =======================
 
-`DjangoSites.org`_ features a constantly growing list of Django-powered sites.
+`BuiltWithDjango.com`_ features a constantly growing list of Django-powered
+sites.
 
-.. _DjangoSites.org: https://djangosites.org
+.. _BuiltWithDjango.com: https://builtwithdjango.com/projects/
 
 .. _faq-mtv:
 
diff --git a/docs/faq/install.txt b/docs/faq/install.txt
index a89da571a9..ddb84d6d9c 100644
--- a/docs/faq/install.txt
+++ b/docs/faq/install.txt
@@ -50,12 +50,10 @@ What Python version can I use with Django?
 ============== ===============
 Django version Python versions
 ============== ===============
-3.2            3.6, 3.7, 3.8, 3.9, 3.10 (added in 3.2.9)
-4.0            3.8, 3.9, 3.10
-4.1            3.8, 3.9, 3.10, 3.11 (added in 4.1.3)
 4.2            3.8, 3.9, 3.10, 3.11, 3.12 (added in 4.2.8)
 5.0            3.10, 3.11, 3.12
 5.1            3.10, 3.11, 3.12
+5.2            3.10, 3.11, 3.12, 3.13
 ============== ===============
 
 For each version of Python, only the latest micro release (A.B.C) is officially
diff --git a/docs/howto/custom-file-storage.txt b/docs/howto/custom-file-storage.txt
index 3cc96cce84..4c9d164a0a 100644
--- a/docs/howto/custom-file-storage.txt
+++ b/docs/howto/custom-file-storage.txt
@@ -131,7 +131,7 @@ to refer to a specific storage throughout Django, to a dictionary of settings
 for that specific storage backend. The settings in the inner dictionaries are
 described fully in the :setting:`STORAGES` documentation.
 
-Storages are then accessed by alias from from the
+Storages are then accessed by alias from the
 :data:`django.core.files.storage.storages` dictionary::
 
     from django.core.files.storage import storages
diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt
index b36f884096..61450dfe7a 100644
--- a/docs/howto/error-reporting.txt
+++ b/docs/howto/error-reporting.txt
@@ -192,10 +192,6 @@ filtered out of error reports in a production environment (that is, where
             @another_decorator
             def process_info(user): ...
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping ``async`` functions was added.
-
 .. function:: sensitive_post_parameters(*parameters)
 
     If one of your views receives an :class:`~django.http.HttpRequest` object
@@ -235,10 +231,6 @@ filtered out of error reports in a production environment (that is, where
     ``user_change_password`` in the ``auth`` admin) to prevent the leaking of
     sensitive information such as user passwords.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping ``async`` functions was added.
-
 .. _custom-error-reports:
 
 Custom error reports
diff --git a/docs/howto/windows.txt b/docs/howto/windows.txt
index 5dd40915d9..0ab976f039 100644
--- a/docs/howto/windows.txt
+++ b/docs/howto/windows.txt
@@ -6,7 +6,7 @@ This document will guide you through installing Python 3.12 and Django on
 Windows. It also provides instructions for setting up a virtual environment,
 which makes it easier to work on Python projects. This is meant as a beginner's
 guide for users working on Django projects and does not reflect how Django
-should be installed when developing patches for Django itself.
+should be installed when developing changes for Django itself.
 
 The steps in this guide have been tested with Windows 10. In other
 versions, the steps would be similar. You will need to be familiar with using
diff --git a/docs/index.txt b/docs/index.txt
index 00d62f9f11..358c465df5 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -27,7 +27,7 @@ Are you new to Django or to programming? This is the place to start!
 
 * **Advanced Tutorials:**
   :doc:`How to write reusable apps <intro/reusable-apps>` |
-  :doc:`Writing your first patch for Django <intro/contributing>`
+  :doc:`Writing your first contribution to Django <intro/contributing>`
 
 Getting help
 ============
diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt
index 49b69bf066..7f825da90a 100644
--- a/docs/internals/contributing/writing-code/coding-style.txt
+++ b/docs/internals/contributing/writing-code/coding-style.txt
@@ -132,8 +132,7 @@ Python style
 Imports
 =======
 
-* Use `isort <https://github.com/PyCQA/isort#readme>`_ to automate import
-  sorting using the guidelines below.
+* Use :pypi:`isort` to automate import sorting using the guidelines below.
 
   Quick start:
 
@@ -215,20 +214,149 @@ Imports
 Template style
 ==============
 
-* In Django template code, put one (and only one) space between the curly
-  brackets and the tag contents.
+Follow the below rules in Django template code.
+
+* ``{% extends %}`` should be the first non-comment line.
 
   Do this:
 
   .. code-block:: html+django
 
-      {{ foo }}
+      {% extends "base.html" %}
+
+      {% block content %}
+        <h1 class="font-semibold text-xl">
+          {{ pages.title }}
+        </h1>
+      {% endblock content %}
+
+  Or this:
+
+  .. code-block:: html+django
+
+      {# This is a comment #}
+      {% extends "base.html" %}
+
+      {% block content %}
+        <h1 class="font-semibold text-xl">
+          {{ pages.title }}
+        </h1>
+      {% endblock content %}
 
   Don't do this:
 
   .. code-block:: html+django
 
-      {{foo}}
+      {% load i18n %}
+      {% extends "base.html" %}
+
+      {% block content %}
+        <h1 class="font-semibold text-xl">
+          {{ pages.title }}
+        </h1>
+      {% endblock content %}
+
+* Put exactly one space between ``{{``, variable contents, and ``}}``.
+
+  Do this:
+
+  .. code-block:: html+django
+
+      {{ user }}
+
+  Don't do this:
+
+  .. code-block:: html+django
+
+      {{user}}
+
+* In ``{% load ... %}``, list libraries in alphabetical order.
+
+  Do this:
+
+  .. code-block:: html+django
+
+      {% load i18n l10 tz %}
+
+  Don't do this:
+
+  .. code-block:: html+django
+
+      {% load l10 i18n tz %}
+
+* Put exactly one space between ``{%``, tag contents, and ``%}``.
+
+  Do this:
+
+  .. code-block:: html+django
+
+      {% load humanize %}
+
+  Don't do this:
+
+  .. code-block:: html+django
+
+      {%load humanize%}
+
+* Put the ``{% block %}`` tag name in the ``{% endblock %}`` tag if it is not
+  on the same line.
+
+  Do this:
+
+  .. code-block:: html+django
+
+      {% block header %}
+
+        Code goes here
+
+      {% endblock header %}
+
+  Don't do this:
+
+  .. code-block:: html+django
+
+      {% block header %}
+
+        Code goes here
+
+      {% endblock %}
+
+* Inside curly braces, separate tokens by single spaces, except for around the
+  ``.`` for attribute access and the ``|`` for a filter.
+
+  Do this:
+
+  .. code-block:: html+django
+
+    {% if user.name|lower == "admin" %}
+
+  Don't do this:
+
+  .. code-block:: html+django
+
+    {% if user . name | lower  ==  "admin" %}
+
+    {{ user.name | upper }}
+
+* Within a template using ``{% extends %}``, avoid indenting top-level
+  ``{% block %}`` tags.
+
+  Do this:
+
+  .. code-block:: html+django
+
+      {% extends "base.html" %}
+
+      {% block content %}
+
+  Don't do this:
+
+  .. code-block:: html+django
+
+      {% extends "base.html" %}
+
+        {% block content %}
+        ...
 
 View style
 ==========
@@ -310,8 +438,8 @@ Model style
 
     class MyModel(models.Model):
         class Direction(models.TextChoices):
-            UP = U, "Up"
-            DOWN = D, "Down"
+            UP = "U", "Up"
+            DOWN = "D", "Down"
 
 Use of ``django.conf.settings``
 ===============================
diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
index 1e86a5802d..ca4029dbfa 100644
--- a/docs/internals/contributing/writing-code/unit-tests.txt
+++ b/docs/internals/contributing/writing-code/unit-tests.txt
@@ -278,16 +278,16 @@ For testing changes to the admin UI, the selenium tests can be run with the
 To define when screenshots should be taken during a selenium test, the test
 class must use the ``@django.test.selenium.screenshot_cases`` decorator with a
 list of supported screenshot types (``"desktop_size"``, ``"mobile_size"``,
-``"small_screen_size"``, ``"rtl"``, and ``"dark"``). It can then call
-``self.take_screenshot("unique-screenshot-name")`` at the desired point to
-generate the screenshots. For example::
+``"small_screen_size"``, ``"rtl"``, ``"dark"``, and ``"high_contrast"``). It
+can then call ``self.take_screenshot("unique-screenshot-name")`` at the desired
+point to generate the screenshots. For example::
 
     from django.test.selenium import SeleniumTestCase, screenshot_cases
     from django.urls import reverse
 
 
     class SeleniumTests(SeleniumTestCase):
-        @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark"])
+        @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
         def test_login_button_centered(self):
             self.selenium.get(self.live_server_url + reverse("admin:login"))
             self.take_screenshot("login")
@@ -295,7 +295,8 @@ generate the screenshots. For example::
 
 This generates multiple screenshots of the login page - one for a desktop
 screen, one for a mobile screen, one for right-to-left languages on desktop,
-and one for the dark mode on desktop.
+one for the dark mode on desktop, and one for high contrast mode on desktop
+when using chrome.
 
 .. versionchanged:: 5.1
 
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index e91ac062cb..4b14b404fc 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -7,6 +7,14 @@ in a backward incompatible way, following their deprecation, as per the
 :ref:`deprecation policy <internal-release-deprecation-policy>`. More details
 about each item can often be found in the release notes of two versions prior.
 
+.. _deprecation-removed-in-6.1:
+
+6.1
+---
+
+See the :ref:`Django 5.2 release notes <deprecated-features-5.2>` for more
+details on these changes.
+
 .. _deprecation-removed-in-6.0:
 
 6.0
@@ -74,6 +82,16 @@ details on these changes.
 * The setter for ``django.contrib.gis.gdal.OGRGeometry.coord_dim`` will be
   removed.
 
+* ``django.urls.register_converter()`` will no longer allow overriding existing
+  converters.
+
+* The ``check`` keyword argument of ``CheckConstraint`` will be removed.
+
+* The ``OS_OPEN_FLAGS`` attribute of
+  :class:`~django.core.files.storage.FileSystemStorage` will be removed.
+
+* The ``get_cache_name()`` method of ``FieldCacheMixin`` will be removed.
+
 .. _deprecation-removed-in-5.1:
 
 5.1
diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt
index 869c83088e..4fb0df4c73 100644
--- a/docs/internals/howto-release-django.txt
+++ b/docs/internals/howto-release-django.txt
@@ -50,22 +50,46 @@ There are a lot of details, so please read on.
 Prerequisites
 =============
 
-You'll need a few things before getting started:
+You'll need a few things before getting started. If this is your first release,
+you'll need to coordinate with another releaser to get all these things lined
+up, and write to the Ops mailing list requesting the required access and
+permissions.
 
-* A GPG key. If the key you want to use is not your default signing key, you'll
-  need to add ``-u you@example.com`` to every GPG signing command below, where
-  ``you@example.com`` is the email address associated with the key you want to
-  use. You will also need to add ``-i you@example.com`` to the ``twine`` call.
+* A Unix environment with these tools installed (in alphabetical order):
 
-* An install of some required Python packages:
+  * bash
+  * git
+  * GPG
+  * make
+  * man
+  * hashing tools (typically ``md5sum``, ``sha1sum``, and ``sha256sum`` on
+    Linux, or ``md5`` and ``shasum`` on macOS)
+  * python
+  * ssh
+
+* A GPG key pair. Ensure that the private part of this key is securely stored.
+  The public part needs to be uploaded to your GitHub account, and also to the
+  Jenkins server running the "confirm release" job.
+
+  .. admonition:: More than one GPG key
+
+    If the key you want to use is not your default signing key, you'll need to
+    add ``-u you@example.com`` to every GPG signing command shown below, where
+    ``you@example.com`` is the email address associated with the key you want
+    to use.
+
+* A clean Python virtual environment per Django version being released, with
+  these required Python packages installed:
 
   .. code-block:: shell
 
       $ python -m pip install wheel twine
 
-* Access to Django's project on PyPI. Create a project-scoped token following
-  the `official documentation <https://pypi.org/help/#apitoken>`_ and set up
-  your ``$HOME/.pypirc`` file like this:
+* Access to `Django's project on PyPI <https://pypi.org/project/Django/>`_ to
+  upload binaries, ideally with extra permissions to `yank a release
+  <https://pypi.org/help/#yanked>`_ if necessary. Create a project-scoped token
+  following the `official documentation <https://pypi.org/help/#apitoken>`_
+  and set up your ``$HOME/.pypirc`` file like this:
 
   .. code-block:: ini
      :caption: ``~/.pypirc``
@@ -84,33 +108,82 @@ You'll need a few things before getting started:
        username = __token__
        password = # A project token.
 
-* Access to the ``djangoproject.com`` server to upload files.
+* Access to `Django's project on Transifex
+  <https://app.transifex.com/django/django/>`_, with a Manager role. Generate
+  an API Token in the `user setting section
+  <https://app.transifex.com/user/settings/api/>`_ and set up your
+  ``$HOME/.transifexrc`` file like this:
 
-* Access to the admin on ``djangoproject.com`` as a "Site maintainer".
+  .. code-block:: ini
+     :caption: ``~/.transifexrc``
 
-* Access to post to ``django-announce``.
+     [https://www.transifex.com]
+       rest_hostname = https://rest.api.transifex.com
+       token = # API token
 
-* If this is a security release, access to the pre-notification distribution
-  list.
+* Access to the ``djangoproject.com`` server to upload files (using ``scp``).
 
-If this is your first release, you'll need to coordinate with another releaser
-to get all these things lined up.
+* Access to the Django admin on ``djangoproject.com`` as a "Site maintainer".
+
+* Access to create a post in the `Django Forum - Announcements category
+  <https://forum.djangoproject.com/c/announcements/7>`_ and to  send emails to
+  the following mailing lists:
+
+  * `django-users <https://groups.google.com/g/django-users/>`_
+  * `django-developers <https://groups.google.com/g/django-developers/>`_
+  * `django-announce <https://groups.google.com/g/django-announce/>`_
+
+* Access to the ``django-security`` repo in GitHub. Among other things, this
+  provides access to the pre-notification distribution list (needed for
+  security release preparation tasks).
 
 Pre-release tasks
 =================
 
 A few items need to be taken care of before even beginning the release process.
 This stuff starts about a week before the release; most of it can be done
-any time leading up to the actual release:
+any time leading up to the actual release.
 
-#. If this is a security release, send out pre-notification **one week** before
-   the release. The template for that email and a list of the recipients are in
-   the private ``django-security`` GitHub wiki. BCC the pre-notification
-   recipients. Sign the email with the key you'll use for the release and
-   include `CVE IDs <https://cveform.mitre.org/>`_ (requested with Vendor:
-   djangoproject, Product: django) and patches for each issue being fixed.
-   Also, :ref:`notify django-announce <security-disclosure>` of the upcoming
-   security release.
+10 (or more) days before a security release
+-------------------------------------------
+
+#. Request the `CVE IDs <https://cveform.mitre.org/>`_  for the security
+   issue(s) being released. One CVE ID per issue, requested with
+   ``Vendor: djangoproject`` and ``Product: django``.
+
+#. Generate the relevant (private) patch(es) using ``git format-patch``, one
+   for the ``main`` branch and one for each stable branch being patched.
+
+A week before a security release
+--------------------------------
+
+#. Send out pre-notification exactly **one week** before the security release.
+   The template for that email and a list of the recipients are in the private
+   ``django-security`` GitHub wiki. BCC the pre-notification recipients and be
+   sure to include the relevant CVE IDs. Attach all the relevant patches
+   (targeting ``main`` and the stable branches) and sign the email text with
+   the key you'll use for the release, with a command like:
+
+   .. code-block:: shell
+
+      $ gpg --clearsign --digest-algo SHA256 prenotification-email.txt
+
+#. :ref:`Notify django-announce <security-disclosure>` of the upcoming
+   security release with a general message such as:
+
+   .. code-block:: text
+
+    Notice of upcoming Django security releases (3.2.24, 4.2.10 and 5.0.2)
+
+    Django versions 5.0.2, 4.2.10, and 3.2.24 will be released on Tuesday,
+    February 6th, 2024 around 1500 UTC. They will fix one security defect
+    with severity "moderate".
+
+    For details of severity levels, see:
+    https://docs.djangoproject.com/en/dev/internals/security/#how-django-discloses-security-issues
+
+A few days before any release
+-----------------------------
 
 #. As the release approaches, watch Trac to make sure no release blockers
    are left for the upcoming release.
@@ -129,18 +202,43 @@ any time leading up to the actual release:
 #. Double-check that the release notes index has a link to the notes
    for the new release; this will be in ``docs/releases/index.txt``.
 
-#. If this is a feature release, ensure translations from Transifex have been
-   integrated. This is typically done by a separate translation's manager
-   rather than the releaser, but here are the steps. Provided you have an
-   account on Transifex:
+#. If this is a :term:`feature release`, ensure translations from Transifex
+   have been integrated. This is typically done by a separate translation's
+   manager rather than the releaser, but here are the steps. This process is a
+   bit lengthy so be sure to set aside 4-10 hours to do this, and ideally plan
+   for this task one or two days ahead of the release day.
+
+   In addition to having a configured Transifex account, the
+   `tx CLI <https://developers.transifex.com/docs/cli>`_ should be available in
+   your ``PATH``. Then, you can fetch all the translations by running:
 
    .. code-block:: shell
 
         $ python scripts/manage_translations.py fetch
 
-   and then commit the changed/added files (both ``.po`` and ``.mo``).
-   Sometimes there are validation errors which need to be debugged, so avoid
-   doing this task immediately before a release is needed.
+   This command takes some time to run. When done, carefully inspect the output
+   for potential errors and/or warnings. If there are some, you will need to
+   debug and resolve them on a case by case basis.
+
+   The recently fetched translations need some manual adjusting. First of all,
+   the ``PO-Revision-Date`` values must be manually bumped to be later than
+   ``POT-Creation-Date``. You can use a command similar to this to bulk update
+   all the ``.po`` files (compare the diff against the relevant stable branch):
+
+   .. code-block:: shell
+
+        $ git diff --name-only stable/5.0.x | grep "\.po"  | xargs sed -ri "s/PO-Revision-Date: [0-9\-]+ /PO-Revision-Date: $(date -I) /g"
+
+   All the new ``.po`` files should be manually and carefully inspected to
+   avoid committing a change in a file without any new translations. Also,
+   there shouldn't be any changes in the "plural forms": if there are any
+   (usually Spanish and French report changes for this) those will need
+   reverting.
+
+   Lastly, commit the changed/added files (both ``.po`` and ``.mo``) and create
+   a new PR targeting the stable branch of the corresponding release (example
+   `PR updating translations for 4.2
+   <https://github.com/django/django/pull/16715>`_).
 
 #. :ref:`Update the django-admin manual page <django-admin-manpage>`:
 
@@ -176,13 +274,10 @@ any time leading up to the actual release:
     $ git checkout -b stable/4.2.x origin/stable/4.1.x
     $ git push origin stable/4.2.x:stable/4.2.x
 
-Preparing for release
-=====================
-
-Write the announcement blog post for the release. You can enter it into the
-admin at any time and mark it as inactive. Here are a few examples: `example
-security release announcement`__, `example regular release announcement`__,
-`example pre-release announcement`__.
+#. Write the announcement blog post for the release. You can enter it into the
+   admin at any time and mark it as inactive. Here are a few examples: `example
+   security release announcement`__, `example regular release announcement`__,
+   `example pre-release announcement`__.
 
 __ https://www.djangoproject.com/weblog/2013/feb/19/security/
 __ https://www.djangoproject.com/weblog/2012/mar/23/14/
@@ -191,15 +286,32 @@ __ https://www.djangoproject.com/weblog/2012/nov/27/15-beta-1/
 Actually rolling the release
 ============================
 
-OK, this is the fun part, where we actually push out a release!
+OK, this is the fun part, where we actually push out a release! If you're
+issuing **multiple releases**, repeat these steps for each release.
 
 #. Check `Jenkins`__ is green for the version(s) you're putting out. You
-   probably shouldn't issue a release until it's green.
+   probably shouldn't issue a release until it's green, and you should make
+   sure that the latest green run includes the changes that you are releasing.
 
    __ https://djangoci.com
 
+#. Cleanup the release notes for this release. Make these changes in ``main``
+   and backport to all branches where the release notes for a particular
+   version are located.
+
+   #. For a feature release, remove the ``UNDER DEVELOPMENT`` header at the top
+      of the release notes, remove the ``Expected`` prefix and update the
+      release date, if necessary (:commit:`example commit
+      <1994a2643881a9e3f9fa8d3e0794c1a9933a1831>`).
+
+   #. For a patch release, remove the ``Expected`` prefix and update the
+      release date for all releases, if necessary (:commit:`example commit
+      <34a503162fe222033a1cd3249bccad014fcd1d20>`).
+
 #. A release always begins from a release branch, so you should make sure
-   you're on a stable branch and up-to-date. For example:
+   you're on an up-to-date stable branch. Also, you should have available a
+   clean and dedicated virtual environment per version being released. For
+   example:
 
    .. code-block:: shell
 
@@ -227,19 +339,19 @@ OK, this is the fun part, where we actually push out a release!
    that the commit is a security fix and that an announcement will follow
    (:commit:`example security commit <bf39978a53f117ca02e9a0c78b76664a41a54745>`).
 
-#. For a feature release, remove the ``UNDER DEVELOPMENT`` header at the
-   top of the release notes and add the release date on the next line. For a
-   patch release, remove the ``Expected`` prefix and update the release date,
-   if necessary. Make this change on all branches where the release notes for a
-   particular version are located.
-
 #. Update the version number in ``django/__init__.py`` for the release.
    Please see `notes on setting the VERSION tuple`_ below for details
-   on ``VERSION``.
+   on ``VERSION`` (:commit:`example commit
+   <2719a7f8c161233f45d34b624a9df9392c86cc1b>`).
 
-#. If this is a pre-release package, update the "Development Status" trove
-   classifier in ``setup.cfg`` to reflect this. Otherwise, make sure the
-   classifier is set to ``Development Status :: 5 - Production/Stable``.
+   #. If this is a pre-release package also update the "Development Status"
+      trove classifier in ``setup.cfg`` to reflect this. An ``rc`` pre-release
+      should not change the trove classifier (:commit:`example commit for alpha
+      release <eeeacc52a967234e920c001b7908c4acdfd7a848>`, :commit:`example
+      commit for beta release <25fec8940b24107e21314ab6616e18ce8dec1c1c>`).
+
+   #. Otherwise, make sure the classifier is set to
+      ``Development Status :: 5 - Production/Stable``.
 
 #. Tag the release using ``git tag``. For example:
 
@@ -247,9 +359,14 @@ OK, this is the fun part, where we actually push out a release!
 
         $ git tag --sign --message="Tag 4.1.1" 4.1.1
 
-   You can check your work by running ``git tag --verify <tag>``.
+   You can check your work running ``git tag --verify <tag>``.
 
-#. Push your work, including the tag: ``git push --tags``.
+#. Push your work and the new tag:
+
+   .. code-block:: shell
+
+        $ git push
+        $ git push --tags
 
 #. Make sure you have an absolutely clean tree by running ``git clean -dfx``.
 
@@ -326,13 +443,20 @@ OK, this is the fun part, where we actually push out a release!
    ``Django-<version>.checksum.txt.asc`` which you can then verify using ``gpg
    --verify Django-<version>.checksum.txt.asc``.
 
-If you're issuing multiple releases, repeat these steps for each release.
-
 Making the release(s) available to the public
 =============================================
 
 Now you're ready to actually put the release out there. To do this:
 
+#. Upload the checksum file(s):
+
+   .. code-block:: shell
+
+        $ scp Django-A.B.C.checksum.txt.asc djangoproject.com:/home/www/www/media/pgp/Django-A.B.C.checksum.txt
+
+   (If this is a security release, what follows should be done 15 minutes
+   before the announced release time, no sooner.)
+
 #. Upload the release package(s) to the djangoproject server, replacing
    A.B. with the appropriate version number, e.g. 4.1 for a 4.1.x release:
 
@@ -340,34 +464,42 @@ Now you're ready to actually put the release out there. To do this:
 
         $ scp Django-* djangoproject.com:/home/www/www/media/releases/A.B
 
-   If this is the alpha release of a new series, you will need to create the
-   directory A.B.
-
-#. Upload the checksum file(s):
-
-   .. code-block:: shell
-
-        $ scp Django-A.B.C.checksum.txt.asc djangoproject.com:/home/www/www/media/pgp/Django-A.B.C.checksum.txt
+   If this is the alpha release of a new series, you will need to create
+   **first** the directory A.B.
 
 #. Test that the release packages install correctly using ``pip``. Here's one
-   method:
+   simple method (this just tests that the binaries are available, that they
+   install correctly, and that migrations and the development server start, but
+   it'll catch silly mistakes):
 
    .. code-block:: shell
 
         $ RELEASE_VERSION='4.1.1'
         $ MAJOR_VERSION=`echo $RELEASE_VERSION| cut -c 1-3`
 
-        $ python -m venv django-pip
-        $ . django-pip/bin/activate
+        $ python -m venv django-pip-tarball
+        $ . django-pip-tarball/bin/activate
         $ python -m pip install https://www.djangoproject.com/m/releases/$MAJOR_VERSION/Django-$RELEASE_VERSION.tar.gz
+        $ django-admin startproject test_tarball
+        $ cd test_tarball
+        $ ./manage.py --help  # Ensure executable bits
+        $ python manage.py migrate
+        $ python manage.py runserver
+        <CTRL+C>
         $ deactivate
+        $ cd .. && rm -rf test_tarball && rm -rf django-pip-tarball
+
         $ python -m venv django-pip-wheel
         $ . django-pip-wheel/bin/activate
         $ python -m pip install https://www.djangoproject.com/m/releases/$MAJOR_VERSION/Django-$RELEASE_VERSION-py3-none-any.whl
+        $ django-admin startproject test_wheel
+        $ cd test_wheel
+        $ ./manage.py --help  # Ensure executable bits
+        $ python manage.py migrate
+        $ python manage.py runserver
+        <CTRL+C>
         $ deactivate
-
-   This just tests that the tarballs are available (i.e. redirects are up) and
-   that they install correctly, but it'll catch silly mistakes.
+        $ cd .. && rm -rf test_wheel && rm -rf django-pip-wheel
 
 #. Run the `confirm-release`__ build on Jenkins to verify the checksum file(s)
    (e.g. use ``4.2rc1`` for
@@ -380,7 +512,7 @@ Now you're ready to actually put the release out there. To do this:
 
    .. code-block:: shell
 
-       $ twine upload -s dist/*
+       $ twine upload dist/*
 
 #. Go to the `Add release page in the admin`__, enter the new release number
    exactly as it appears in the name of the tarball
@@ -404,9 +536,10 @@ Now you're ready to actually put the release out there. To do this:
 
    Create new ``DocumentRelease`` objects for each language that has an entry
    for the previous release. Update djangoproject.com's `robots.docs.txt`__
-   file by copying entries from ``manage_translations.py robots_txt`` from the
-   current stable branch in the ``django-docs-translations`` repository. For
-   example, when releasing Django 4.2:
+   file by copying the result generated from running the command
+   ``manage_translations.py robots_txt`` in the current stable branch from the
+   `django-docs-translations repository`__. For example, when releasing Django
+   4.2:
 
    .. code-block:: shell
 
@@ -415,6 +548,7 @@ Now you're ready to actually put the release out there. To do this:
         $ python manage_translations.py robots_txt
 
    __ https://github.com/django/djangoproject.com/blob/main/djangoproject/static/robots.docs.txt
+   __ https://github.com/django/django-docs-translations
 
 #. Post the release announcement to the |django-announce|, |django-developers|,
    |django-users| mailing lists, and the Django Forum. This should include a
@@ -447,9 +581,14 @@ You're almost done! All that's left to do now is:
 
    __ https://github.com/django/code.djangoproject.com/blob/main/trac-env/conf/trac.ini
 
-#. If it's a final release, update the current stable branch and remove the
-   pre-release branch in the `Django release process
-   <https://code.djangoproject.com/#Djangoreleaseprocess>`_ on Trac.
+#. If this was a final release:
+
+   #. Update the current stable branch and remove the pre-release branch in the
+      `Django release process
+      <https://code.djangoproject.com/#Djangoreleaseprocess>`_ on Trac.
+
+   #. Update djangoproject.com's download page (`example PR
+      <https://github.com/django/djangoproject.com/pull/1444>`__).
 
 #. If this was a security release, update :doc:`/releases/security` with
    details of the issues addressed.
@@ -466,7 +605,8 @@ need to be done by the releaser.
 #. Create a new ``DocumentRelease`` object in the ``docs.djangoproject.com``
    database for the new version's docs, and update the
    ``docs/fixtures/doc_releases.json`` JSON fixture, so people without access
-   to the production DB can still run an up-to-date copy of the docs site.
+   to the production DB can still run an up-to-date copy of the docs site
+   (`example PR <https://github.com/django/djangoproject.com/pull/1446>`__).
 
 #. Create a stub release note for the new feature version. Use the stub from
    the previous feature release version or copy the contents from the previous
diff --git a/docs/internals/organization.txt b/docs/internals/organization.txt
index 5fd8115b5a..53bac152d5 100644
--- a/docs/internals/organization.txt
+++ b/docs/internals/organization.txt
@@ -55,8 +55,8 @@ Mergers hold the following prerogatives:
   enough to require the use of the `DEP process`_) reaches one of its
   implementation milestones and is intended to merge.
 
-.. _`minor change`: https://github.com/django/deps/blob/main/accepted/0010-new-governance.rst#terminology
-.. _`major change`: https://github.com/django/deps/blob/main/accepted/0010-new-governance.rst#terminology
+.. _`minor change`: https://github.com/django/deps/blob/main/final/0010-new-governance.rst#terminology
+.. _`major change`: https://github.com/django/deps/blob/main/final/0010-new-governance.rst#terminology
 
 Membership
 ----------
@@ -294,4 +294,4 @@ Changes to this document require the use of the `DEP process`_, with
 modifications described in `DEP 0010`_.
 
 .. _`DEP process`: https://github.com/django/deps/blob/main/final/0001-dep-process.rst
-.. _`DEP 0010`: https://github.com/django/deps/blob/main/accepted/0010-new-governance.rst#changing-this-governance-process
+.. _`DEP 0010`: https://github.com/django/deps/blob/main/final/0010-new-governance.rst#changing-this-governance-process
diff --git a/docs/internals/security.txt b/docs/internals/security.txt
index 373012b707..55300b01e1 100644
--- a/docs/internals/security.txt
+++ b/docs/internals/security.txt
@@ -84,24 +84,24 @@ upcoming security release, as well as the severity of the issues. This is to
 aid organizations that need to ensure they have staff available to handle
 triaging our announcement and upgrade Django as needed. Severity levels are:
 
-**High**:
+* **High**
 
-* Remote code execution
-* SQL injection
+  * Remote code execution
+  * SQL injection
 
-**Moderate**:
+* **Moderate**
 
-* Cross site scripting (XSS)
-* Cross site request forgery (CSRF)
-* Denial-of-service attacks
-* Broken authentication
+  * Cross site scripting (XSS)
+  * Cross site request forgery (CSRF)
+  * Denial-of-service attacks
+  * Broken authentication
 
-**Low**:
+* **Low**
 
-* Sensitive data exposure
-* Broken session management
-* Unvalidated redirects/forwards
-* Issues requiring an uncommon configuration option
+  * Sensitive data exposure
+  * Broken session management
+  * Unvalidated redirects/forwards
+  * Issues requiring an uncommon configuration option
 
 Second, we notify a list of :ref:`people and organizations
 <security-notifications>`, primarily composed of operating-system vendors and
diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt
index 06230b8ee3..7d590e76a2 100644
--- a/docs/intro/contributing.txt
+++ b/docs/intro/contributing.txt
@@ -1,6 +1,6 @@
-===================================
-Writing your first patch for Django
-===================================
+==========================================
+Writing your first contribution for Django
+==========================================
 
 Introduction
 ============
@@ -52,16 +52,16 @@ __ https://web.libera.chat/#django-dev
 What does this tutorial cover?
 ------------------------------
 
-We'll be walking you through contributing a patch to Django for the first time.
+We'll be walking you through contributing to Django for the first time.
 By the end of this tutorial, you should have a basic understanding of both the
 tools and the processes involved. Specifically, we'll be covering the following:
 
 * Installing Git.
 * Downloading a copy of Django's development version.
 * Running Django's test suite.
-* Writing a test for your patch.
-* Writing the code for your patch.
-* Testing your patch.
+* Writing a test for your changes.
+* Writing the code for your changes.
+* Testing your changes.
 * Submitting a pull request.
 * Where to look for more information.
 
@@ -91,7 +91,7 @@ Installing Git
 ==============
 
 For this tutorial, you'll need Git installed to download the current
-development version of Django and to generate patch files for the changes you
+development version of Django and to generate a branch for the changes you
 make.
 
 To check whether or not you have Git installed, enter ``git`` into the command
@@ -178,7 +178,7 @@ Go ahead and install the previously cloned copy of Django:
 
 The installed version of Django is now pointing at your local copy by installing
 in editable mode. You will immediately see any changes you make to it, which is
-of great help when writing your first patch.
+of great help when writing your first contribution.
 
 Creating projects with a local copy of Django
 ---------------------------------------------
@@ -188,8 +188,8 @@ have to create a new virtual environment, :ref:`install the previously cloned
 local copy of Django in editable mode <intro-contributing-install-local-copy>`,
 and create a new Django project outside of your local copy of Django. You will
 immediately see any changes you make to Django in your new project, which is
-of great help when writing your first patch, especially if testing any changes
-to the UI.
+of great help when writing your first contribution, especially if testing
+any changes to the UI.
 
 You can follow the :doc:`tutorial</intro/tutorial01>` for help in creating a
 Django project.
@@ -279,8 +279,8 @@ imaginary details:
 
 We'll now implement this feature and associated tests.
 
-Creating a branch for your patch
-================================
+Creating a branch
+=================
 
 Before making any changes, create a new branch for the ticket:
 
@@ -295,19 +295,19 @@ won't affect the main copy of the code that we cloned earlier.
 Writing some tests for your ticket
 ==================================
 
-In most cases, for a patch to be accepted into Django it has to include tests.
-For bug fix patches, this means writing a regression test to ensure that the
-bug is never reintroduced into Django later on. A regression test should be
-written in such a way that it will fail while the bug still exists and pass
-once the bug has been fixed. For patches containing new features, you'll need
-to include tests which ensure that the new features are working correctly.
-They too should fail when the new feature is not present, and then pass once it
-has been implemented.
+In most cases, for a contribution to be accepted into Django it has to include
+tests. For bug fix contributions, this means writing a regression test to
+ensure that  the bug is never reintroduced into Django later on. A regression
+test should be written in such a way that it will fail while the bug still
+exists and pass once the bug has been fixed. For contributions containing new
+features, you'll need to include tests which ensure that the new features are
+working correctly. They too should fail when the new feature is not present,
+and then pass once it has been implemented.
 
 A good way to do this is to write your new tests first, before making any
 changes to the code. This style of development is called
 `test-driven development`__ and can be applied to both entire projects and
-single patches. After writing your tests, you then run them to make sure that
+single changes. After writing your tests, you then run them to make sure that
 they do indeed fail (since you haven't fixed that bug or added that feature
 yet). If your new tests don't fail, you'll need to fix them so that they do.
 After all, a regression test that passes regardless of whether a bug is present
@@ -398,7 +398,7 @@ function to the correct file.
 Running Django's test suite for the second time
 ===============================================
 
-Once you've verified that your patch and your test are working correctly, it's
+Once you've verified that your changes and test are working correctly, it's
 a good idea to run the entire Django test suite to verify that your change
 hasn't introduced any bugs into other areas of Django. While successfully
 passing the entire test suite doesn't guarantee your code is bug free, it does
@@ -450,7 +450,7 @@ preview the HTML that will be generated.
 Previewing your changes
 =======================
 
-Now it's time to go through all the changes made in our patch. To stage all the
+Now it's time to review the changes made in the branch. To stage all the
 changes ready for commit, run:
 
 .. console::
@@ -528,12 +528,11 @@ Use the arrow keys to move up and down.
     +    def test_make_toast(self):
     +        self.assertEqual(make_toast(), 'toast')
 
-When you're done previewing the patch, hit the ``q`` key to return to the
-command line. If the patch's content looked okay, it's time to commit the
-changes.
+When you're done previewing the changes, hit the ``q`` key to return to the
+command line. If the diff looked okay, it's time to commit the changes.
 
-Committing the changes in the patch
-===================================
+Committing the changes
+======================
 
 To commit the changes:
 
@@ -551,7 +550,7 @@ message guidelines <committing-guidelines>` and write a message like:
 Pushing the commit and making a pull request
 ============================================
 
-After committing the patch, send it to your fork on GitHub (substitute
+After committing the changes, send it to your fork on GitHub (substitute
 "ticket_99999" with the name of your branch if it's different):
 
 .. console::
@@ -563,7 +562,7 @@ You can create a pull request by visiting the `Django GitHub page
 recently pushed branches". Click "Compare & pull request" next to it.
 
 Please don't do it for this tutorial, but on the next page that displays a
-preview of the patch, you would click "Create pull request".
+preview of the changes, you would click "Create pull request".
 
 Next steps
 ==========
@@ -578,14 +577,14 @@ codebase.
 More information for new contributors
 -------------------------------------
 
-Before you get too into writing patches for Django, there's a little more
+Before you get too into contributing to Django, there's a little more
 information on contributing that you should probably take a look at:
 
 * You should make sure to read Django's documentation on
-  :doc:`claiming tickets and submitting patches
+  :doc:`claiming tickets and submitting pull requests
   </internals/contributing/writing-code/submitting-patches>`.
   It covers Trac etiquette, how to claim tickets for yourself, expected
-  coding style for patches, and many other important details.
+  coding style (both for code and docs), and many other important details.
 * First time contributors should also read Django's :doc:`documentation
   for first time contributors</internals/contributing/new-contributors/>`.
   It has lots of good advice for those of us who are new to helping out
@@ -600,19 +599,19 @@ Finding your first real ticket
 ------------------------------
 
 Once you've looked through some of that information, you'll be ready to go out
-and find a ticket of your own to write a patch for. Pay special attention to
+and find a ticket of your own to contribute to. Pay special attention to
 tickets with the "easy pickings" criterion. These tickets are often much
 simpler in nature and are great for first time contributors. Once you're
-familiar with contributing to Django, you can move on to writing patches for
-more difficult and complicated tickets.
+familiar with contributing to Django, you can start working on more difficult
+and complicated tickets.
 
 If you just want to get started already (and nobody would blame you!), try
-taking a look at the list of `easy tickets that need patches`__ and the
-`easy tickets that have patches which need improvement`__. If you're familiar
+taking a look at the list of `easy tickets without a branch`__ and the
+`easy tickets that have branches which need improvement`__. If you're familiar
 with writing tests, you can also look at the list of
 `easy tickets that need tests`__. Remember to follow the guidelines about
 claiming tickets that were mentioned in the link to Django's documentation on
-:doc:`claiming tickets and submitting patches
+:doc:`claiming tickets and submitting branches
 </internals/contributing/writing-code/submitting-patches>`.
 
 __ https://code.djangoproject.com/query?status=new&status=reopened&has_patch=0&easy=1&col=id&col=summary&col=status&col=owner&col=type&col=milestone&order=priority
@@ -622,9 +621,9 @@ __ https://code.djangoproject.com/query?status=new&status=reopened&needs_tests=1
 What's next after creating a pull request?
 ------------------------------------------
 
-After a ticket has a patch, it needs to be reviewed by a second set of eyes.
+After a ticket has a branch, it needs to be reviewed by a second set of eyes.
 After submitting a pull request, update the ticket metadata by setting the
 flags on the ticket to say "has patch", "doesn't need tests", etc, so others
-can find it for review. Contributing doesn't necessarily always mean writing a
-patch from scratch. Reviewing existing patches is also a very helpful
+can find it for review. Contributing doesn't necessarily always mean writing
+code from scratch. Reviewing open pull requests is also a very helpful
 contribution. See :doc:`/internals/contributing/triaging-tickets` for details.
diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt
index cb296129c0..f506fc605d 100644
--- a/docs/intro/tutorial01.txt
+++ b/docs/intro/tutorial01.txt
@@ -64,18 +64,6 @@ work, see :ref:`troubleshooting-django-admin`.
     ``django`` (which will conflict with Django itself) or ``test`` (which
     conflicts with a built-in Python package).
 
-.. admonition:: Where should this code live?
-
-    If your background is in plain old PHP (with no use of modern frameworks),
-    you're probably used to putting code under the web server's document root
-    (in a place such as ``/var/www``). With Django, you don't do that. It's
-    not a good idea to put any of this Python code within your web server's
-    document root, because it risks the possibility that people may be able
-    to view your code over the web. That's not good for security.
-
-    Put your code in some directory **outside** of the document root, such as
-    :file:`/home/mycode`.
-
 Let's look at what :djadmin:`startproject` created:
 
 .. code-block:: text
@@ -150,6 +138,10 @@ You'll see the following output on the command line:
     Ignore the warning about unapplied database migrations for now; we'll deal
     with the database shortly.
 
+Now that the server's running, visit http://127.0.0.1:8000/ with your web
+browser. You'll see a "Congratulations!" page, with a rocket taking off.
+It worked!
+
 You've started the Django development server, a lightweight web server written
 purely in Python. We've included this with Django so you can develop things
 rapidly, without having to deal with configuring a production server -- such as
@@ -159,34 +151,7 @@ Now's a good time to note: **don't** use this server in anything resembling a
 production environment. It's intended only for use while developing. (We're in
 the business of making web frameworks, not web servers.)
 
-Now that the server's running, visit http://127.0.0.1:8000/ with your web
-browser. You'll see a "Congratulations!" page, with a rocket taking off.
-It worked!
-
-.. admonition:: Changing the port
-
-    By default, the :djadmin:`runserver` command starts the development server
-    on the internal IP at port 8000.
-
-    If you want to change the server's port, pass
-    it as a command-line argument. For instance, this command starts the server
-    on port 8080:
-
-    .. console::
-
-        $ python manage.py runserver 8080
-
-    If you want to change the server's IP, pass it along with the port. For
-    example, to listen on all available public IPs (which is useful if you are
-    running Vagrant or want to show off your work on other computers on the
-    network), use:
-
-    .. console::
-
-        $ python manage.py runserver 0.0.0.0:8000
-
-    Full docs for the development server can be found in the
-    :djadmin:`runserver` reference.
+(To serve the site on a different port, see the :djadmin:`runserver` reference.)
 
 .. admonition:: Automatic reloading of :djadmin:`runserver`
 
diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt
index d558a3eb1d..3cda0d38d6 100644
--- a/docs/intro/tutorial02.txt
+++ b/docs/intro/tutorial02.txt
@@ -515,8 +515,8 @@ Save these changes and start a new Python interactive shell by running
     # Give the Question a couple of Choices. The create call constructs a new
     # Choice object, does the INSERT statement, adds the choice to the set
     # of available choices and returns the new Choice object. Django creates
-    # a set to hold the "other side" of a ForeignKey relation
-    # (e.g. a question's choice) which can be accessed via the API.
+    # a set (defined as "choice_set") to hold the "other side" of a ForeignKey
+    # relation (e.g. a question's choice) which can be accessed via the API.
     >>> q = Question.objects.get(pk=1)
 
     # Display any choices from the related object set -- none so far.
diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt
index 65dc132a94..149b01f338 100644
--- a/docs/intro/tutorial04.txt
+++ b/docs/intro/tutorial04.txt
@@ -74,6 +74,7 @@ create a real version. Add the following to ``polls/views.py``:
 .. code-block:: python
     :caption: ``polls/views.py``
 
+    from django.db.models import F
     from django.http import HttpResponse, HttpResponseRedirect
     from django.shortcuts import get_object_or_404, render
     from django.urls import reverse
@@ -97,7 +98,7 @@ create a real version. Add the following to ``polls/views.py``:
                 },
             )
         else:
-            selected_choice.votes += 1
+            selected_choice.votes = F("votes") + 1
             selected_choice.save()
             # Always return an HttpResponseRedirect after successfully dealing
             # with POST data. This prevents data from being posted twice if a
@@ -123,6 +124,9 @@ This code includes a few things we haven't covered yet in this tutorial:
   :exc:`KeyError` and redisplays the question form with an error
   message if ``choice`` isn't given.
 
+* ``F("votes") + 1`` :ref:`instructs the database
+  <avoiding-race-conditions-using-f>` to increase the vote count by 1.
+
 * After incrementing the choice count, the code returns an
   :class:`~django.http.HttpResponseRedirect` rather than a normal
   :class:`~django.http.HttpResponse`.
@@ -190,19 +194,6 @@ Now, go to ``/polls/1/`` in your browser and vote in the question. You should se
 results page that gets updated each time you vote. If you submit the form
 without having chosen a choice, you should see the error message.
 
-.. note::
-    The code for our ``vote()`` view does have a small problem. It first gets
-    the ``selected_choice`` object from the database, then computes the new
-    value of ``votes``, and then saves it back to the database. If two users of
-    your website try to vote at *exactly the same time*, this might go wrong:
-    The same value, let's say 42, will be retrieved for ``votes``. Then, for
-    both users the new value of 43 is computed and saved, but 44 would be the
-    expected value.
-
-    This is called a *race condition*. If you are interested, you can read
-    :ref:`avoiding-race-conditions-using-f` to learn how you can solve this
-    issue.
-
 Use generic views: Less code is better
 ======================================
 
@@ -281,6 +272,7 @@ views and use Django's generic views instead. To do so, open the
 .. code-block:: python
     :caption: ``polls/views.py``
 
+    from django.db.models import F
     from django.http import HttpResponseRedirect
     from django.shortcuts import get_object_or_404, render
     from django.urls import reverse
diff --git a/docs/intro/tutorial07.txt b/docs/intro/tutorial07.txt
index 8cb5dd5d5a..b47ae14bc2 100644
--- a/docs/intro/tutorial07.txt
+++ b/docs/intro/tutorial07.txt
@@ -199,7 +199,7 @@ Here's what it looks like at this point:
 By default, Django displays the ``str()`` of each object. But sometimes it'd be
 more helpful if we could display individual fields. To do that, use the
 :attr:`~django.contrib.admin.ModelAdmin.list_display` admin option, which is a
-tuple of field names to display, as columns, on the change list page for the
+list of field names to display, as columns, on the change list page for the
 object:
 
 .. code-block:: python
diff --git a/docs/intro/tutorial08.txt b/docs/intro/tutorial08.txt
index 5a20e29b9d..c184eec47a 100644
--- a/docs/intro/tutorial08.txt
+++ b/docs/intro/tutorial08.txt
@@ -48,11 +48,11 @@ The steps are not duplicated in this tutorial, because as a third-party
 package, it may change separately to Django's schedule.
 
 Once installed, you should be able to see the DjDT "handle" on the right side
-of the browser window when you refresh the polls application. Click it to open
-the debug toolbar and use the tools in each panel. See the `panels
-documentation page
-<https://django-debug-toolbar.readthedocs.io/en/latest/panels.html>`__ for more
-information on what the panels show.
+of the browser window when you browse to ``http://localhost:8000/admin/``.
+Click it to open the debug toolbar and use the tools in each panel. See the
+`panels documentation page`__ for more information on what the panels show.
+
+__ https://django-debug-toolbar.readthedocs.io/en/latest/panels.html
 
 Getting help from others
 ========================
diff --git a/docs/man/django-admin.1 b/docs/man/django-admin.1
index 95d34931eb..06912769c8 100644
--- a/docs/man/django-admin.1
+++ b/docs/man/django-admin.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "DJANGO-ADMIN" "1" "September 18, 2023" "5.0" "Django"
+.TH "DJANGO-ADMIN" "1" "May 22, 2024" "5.1" "Django"
 .SH NAME
 django-admin \- Utility script for the Django web framework
 .sp
@@ -36,7 +36,7 @@ This document outlines all it can do.
 .sp
 In addition, \fBmanage.py\fP is automatically created in each Django project. It
 does the same thing as \fBdjango\-admin\fP but also sets the
-\fI\%DJANGO_SETTINGS_MODULE\fP environment variable so that it points to your
+\X'tty: link #envvar-DJANGO_SETTINGS_MODULE'\fI\%DJANGO_SETTINGS_MODULE\fP\X'tty: link' environment variable so that it points to your
 project\(aqs \fBsettings.py\fP file.
 .sp
 The \fBdjango\-admin\fP script should be on your system path if you installed
@@ -46,7 +46,7 @@ environment activated.
 Generally, when working on a single Django project, it\(aqs easier to use
 \fBmanage.py\fP than \fBdjango\-admin\fP\&. If you need to switch between multiple
 Django settings files, use \fBdjango\-admin\fP with
-\fI\%DJANGO_SETTINGS_MODULE\fP or the \fI\%\-\-settings\fP command line
+\X'tty: link #envvar-DJANGO_SETTINGS_MODULE'\fI\%DJANGO_SETTINGS_MODULE\fP\X'tty: link' or the \fI\%\-\-settings\fP command line
 option.
 .sp
 The command\-line examples throughout this document use \fBdjango\-admin\fP to
@@ -56,13 +56,11 @@ just as well.
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 $ django\-admin <command> [options]
 $ manage.py <command> [options]
 $ python \-m django <command> [options]
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -86,7 +84,7 @@ command and a list of its available options.
 .SS App names
 .sp
 Many commands take a list of \(dqapp names.\(dq An \(dqapp name\(dq is the basename of
-the package containing your models. For example, if your \fI\%INSTALLED_APPS\fP
+the package containing your models. For example, if your \X'tty: link #std-setting-INSTALLED_APPS'\fI\%INSTALLED_APPS\fP\X'tty: link'
 contains the string \fB\(aqmysite.blog\(aq\fP, the app name is \fBblog\fP\&.
 .SS Determining the version
 .INDENT 0.0
@@ -96,17 +94,15 @@ contains the string \fB\(aqmysite.blog\(aq\fP, the app name is \fBblog\fP\&.
 .sp
 Run \fBdjango\-admin version\fP to display the current Django version.
 .sp
-The output follows the schema described in \fI\%PEP 440\fP:
+The output follows the schema described in \X'tty: link https://peps.python.org/pep-0440/'\fI\%PEP 440\fP\X'tty: link':
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 1.4.dev17026
 1.4a1
 1.4
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SS Displaying debug output
@@ -128,11 +124,9 @@ providing a list of app labels as arguments:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin check auth admin myapp
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -141,17 +135,15 @@ django\-admin check auth admin myapp
 .UNINDENT
 .sp
 The system check framework performs many different types of checks that are
-\fI\%categorized with tags\fP\&. You can use these
+\X'tty: link #system-check-builtin-tags'\fI\%categorized with tags\fP\X'tty: link'\&. You can use these
 tags to restrict the checks performed to just those in a particular category.
 For example, to perform only models and compatibility checks, run:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin check \-\-tag models \-\-tag compatibility
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -163,11 +155,9 @@ Specifies the database to run checks requiring database access:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin check \-\-database default \-\-database other
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -188,16 +178,14 @@ Activates some additional checks that are only relevant in a deployment setting.
 You can use this option in your local development environment, but since your
 local development settings module may not have many of your production settings,
 you will probably want to point the \fBcheck\fP command at a different settings
-module, either by setting the \fI\%DJANGO_SETTINGS_MODULE\fP environment
+module, either by setting the \X'tty: link #envvar-DJANGO_SETTINGS_MODULE'\fI\%DJANGO_SETTINGS_MODULE\fP\X'tty: link' environment
 variable, or by passing the \fB\-\-settings\fP option:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin check \-\-deploy \-\-settings=production_settings
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -237,14 +225,13 @@ are excluded.
 .B \-\-use\-fuzzy, \-f
 .UNINDENT
 .sp
-Includes \fI\%fuzzy translations\fP into compiled files.
+Includes \X'tty: link https://www.gnu.org/software/gettext/manual/html_node/Fuzzy-Entries.html'\fI\%fuzzy translations\fP\X'tty: link' into compiled files.
 .sp
 Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin compilemessages \-\-locale=pt_BR
 django\-admin compilemessages \-\-locale=pt_BR \-\-locale=fr \-f
 django\-admin compilemessages \-l pt_BR
@@ -253,8 +240,7 @@ django\-admin compilemessages \-\-exclude=pt_BR
 django\-admin compilemessages \-\-exclude=pt_BR \-\-exclude=fr
 django\-admin compilemessages \-x pt_BR
 django\-admin compilemessages \-x pt_BR \-x fr
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -262,18 +248,16 @@ django\-admin compilemessages \-x pt_BR \-x fr
 .B \-\-ignore PATTERN, \-i PATTERN
 .UNINDENT
 .sp
-Ignores directories matching the given \fI\%glob\fP\-style pattern. Use
+Ignores directories matching the given \X'tty: link https://docs.python.org/3/library/glob.html#module-glob'\fI\%glob\fP\X'tty: link'\-style pattern. Use
 multiple times to ignore more.
 .sp
 Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin compilemessages \-\-ignore=cache \-\-ignore=outdated/*/locale
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SS \fBcreatecachetable\fP
@@ -306,8 +290,8 @@ customize it or use the migrations framework.
 .UNINDENT
 .sp
 Runs the command\-line client for the database engine specified in your
-\fI\%ENGINE\fP setting, with the connection parameters
-specified in your \fI\%USER\fP, \fI\%PASSWORD\fP, etc., settings.
+\X'tty: link #std-setting-DATABASE-ENGINE'\fI\%ENGINE\fP\X'tty: link' setting, with the connection parameters
+specified in your \X'tty: link #std-setting-USER'\fI\%USER\fP\X'tty: link', \X'tty: link #std-setting-PASSWORD'\fI\%PASSWORD\fP\X'tty: link', etc., settings.
 .INDENT 0.0
 .IP \(bu 2
 For PostgreSQL, this runs the \fBpsql\fP command\-line client.
@@ -340,15 +324,13 @@ command\(aqs \fB\-c\fP flag to execute a raw SQL query directly:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 $ django\-admin dbshell \-\- \-c \(aqselect current_user\(aq
  current_user
 \-\-\-\-\-\-\-\-\-\-\-\-\-\-
  postgres
 (1 row)
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -356,24 +338,22 @@ On MySQL/MariaDB, you can do this with the \fBmysql\fP command\(aqs \fB\-e\fP fl
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 $ django\-admin dbshell \-\- \-e \(dqselect user()\(dq
 +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
 | user()               |
 +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
 | djangonaut@localhost |
 +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
 \fBNOTE:\fP
 .INDENT 0.0
 .INDENT 3.5
-Be aware that not all options set in the \fI\%OPTIONS\fP part of your
-database configuration in \fI\%DATABASES\fP are passed to the
+Be aware that not all options set in the \X'tty: link #std-setting-OPTIONS'\fI\%OPTIONS\fP\X'tty: link' part of your
+database configuration in \X'tty: link #std-setting-DATABASES'\fI\%DATABASES\fP\X'tty: link' are passed to the
 command\-line client, e.g. \fB\(aqisolation_level\(aq\fP\&.
 .UNINDENT
 .UNINDENT
@@ -387,8 +367,8 @@ Displays differences between the current settings file and Django\(aqs default
 settings (or another settings file specified by \fI\%\-\-default\fP).
 .sp
 Settings that don\(aqt appear in the defaults are followed by \fB\(dq###\(dq\fP\&. For
-example, the default settings don\(aqt define \fI\%ROOT_URLCONF\fP, so
-\fI\%ROOT_URLCONF\fP is followed by \fB\(dq###\(dq\fP in the output of
+example, the default settings don\(aqt define \X'tty: link #std-setting-ROOT_URLCONF'\fI\%ROOT_URLCONF\fP\X'tty: link', so
+\X'tty: link #std-setting-ROOT_URLCONF'\fI\%ROOT_URLCONF\fP\X'tty: link' is followed by \fB\(dq###\(dq\fP in the output of
 \fBdiffsettings\fP\&.
 .INDENT 0.0
 .TP
@@ -428,12 +408,12 @@ If no application name is provided, all installed applications will be dumped.
 The output of \fBdumpdata\fP can be used as input for \fI\%loaddata\fP\&.
 .sp
 When result of \fBdumpdata\fP is saved as a file, it can serve as a
-\fI\%fixture\fP for
-\fI\%tests\fP or as an
-\fI\%initial data\fP\&.
+\X'tty: link #fixtures-explanation'\fI\%fixture\fP\X'tty: link' for
+\X'tty: link #topics-testing-fixtures'\fI\%tests\fP\X'tty: link' or as an
+\X'tty: link #initial-data-via-fixtures'\fI\%initial data\fP\X'tty: link'\&.
 .sp
 Note that \fBdumpdata\fP uses the default manager on the model for selecting the
-records to dump. If you\(aqre using a \fI\%custom manager\fP as
+records to dump. If you\(aqre using a \X'tty: link #custom-managers'\fI\%custom manager\fP\X'tty: link' as
 the default manager and it filters some of the available records, not all of the
 objects will be dumped.
 .INDENT 0.0
@@ -449,7 +429,7 @@ or modified by a custom manager.
 .UNINDENT
 .sp
 Specifies the serialization format of the output. Defaults to JSON. Supported
-formats are listed in \fI\%Serialization formats\fP\&.
+formats are listed in \X'tty: link #serialization-formats'\fI\%Serialization formats\fP\X'tty: link'\&.
 .INDENT 0.0
 .TP
 .B \-\-indent INDENT
@@ -472,11 +452,9 @@ once:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin dumpdata \-\-exclude=auth \-\-exclude=contenttypes
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -494,7 +472,7 @@ Uses the \fBnatural_key()\fP model method to serialize any foreign key and
 many\-to\-many relationship to objects of the type that defines the method. If
 you\(aqre dumping \fBcontrib.auth\fP \fBPermission\fP objects or
 \fBcontrib.contenttypes\fP \fBContentType\fP objects, you should probably use this
-flag. See the \fI\%natural keys\fP
+flag. See the \X'tty: link #topics-serialization-natural-keys'\fI\%natural keys\fP\X'tty: link'
 documentation for more details on this and the next option.
 .INDENT 0.0
 .TP
@@ -529,11 +507,9 @@ For example, to output the data as a compressed JSON file:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin dumpdata \-o mydata.json.gz
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SS \fBflush\fP
@@ -566,7 +542,7 @@ Specifies the database to flush. Defaults to \fBdefault\fP\&.
 .UNINDENT
 .sp
 Introspects the database tables in the database pointed\-to by the
-\fI\%NAME\fP setting and outputs a Django model module (a \fBmodels.py\fP
+\X'tty: link #std-setting-NAME'\fI\%NAME\fP\X'tty: link' setting and outputs a Django model module (a \fBmodels.py\fP
 file) to standard output.
 .sp
 You may choose what tables or views to inspect by passing their names as
@@ -587,7 +563,7 @@ If \fBinspectdb\fP cannot map a column\(aqs type to a model field type, it\(aqll
 use \fBTextField\fP and will insert the Python comment
 \fB\(aqThis field type is a guess.\(aq\fP next to the field in the generated
 model. The recognized fields may depend on apps listed in
-\fI\%INSTALLED_APPS\fP\&. For example, \fI\%django.contrib.postgres\fP adds
+\X'tty: link #std-setting-INSTALLED_APPS'\fI\%INSTALLED_APPS\fP\X'tty: link'\&. For example, \X'tty: link #module-django.contrib.postgres'\fI\%django.contrib.postgres\fP\X'tty: link' adds
 recognition for several PostgreSQL\-specific field types.
 .IP \(bu 2
 If the database column name is a Python reserved word (such as
@@ -606,7 +582,7 @@ customizations. In particular, you\(aqll need to rearrange models\(aq order, so
 models that refer to other models are ordered properly.
 .sp
 Django doesn\(aqt create database defaults when a
-\fI\%default\fP is specified on a model field.
+\X'tty: link #django.db.models.Field.default'\fI\%default\fP\X'tty: link' is specified on a model field.
 Similarly, database defaults aren\(aqt translated to model field defaults or
 detected in any fashion by \fBinspectdb\fP\&.
 .sp
@@ -614,7 +590,7 @@ By default, \fBinspectdb\fP creates unmanaged models. That is, \fBmanaged = Fals
 in the model\(aqs \fBMeta\fP class tells Django not to manage each table\(aqs creation,
 modification, and deletion. If you do want to allow Django to manage the
 table\(aqs lifecycle, you\(aqll need to change the
-\fI\%managed\fP option to \fBTrue\fP (or remove
+\X'tty: link #django.db.models.Options.managed'\fI\%managed\fP\X'tty: link' option to \fBTrue\fP (or remove
 it because \fBTrue\fP is its default value).
 .SS Database\-specific notes
 .SS Oracle
@@ -661,7 +637,7 @@ If this option is provided, models are also created for database views.
 .UNINDENT
 .sp
 Searches for and loads the contents of the named
-\fI\%fixture\fP into the database.
+\X'tty: link #fixtures-explanation'\fI\%fixture\fP\X'tty: link' into the database.
 .INDENT 0.0
 .TP
 .B \-\-database DATABASE
@@ -687,7 +663,7 @@ Specifies a single app to look for fixtures in rather than looking in all apps.
 .B \-\-format FORMAT
 .UNINDENT
 .sp
-Specifies the \fI\%serialization format\fP (e.g.,
+Specifies the \X'tty: link #serialization-formats'\fI\%serialization format\fP\X'tty: link' (e.g.,
 \fBjson\fP or \fBxml\fP) for fixtures \fI\%read from stdin\fP\&.
 .INDENT 0.0
 .TP
@@ -704,16 +680,14 @@ example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin loaddata \-\-format=json \-
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
 When reading from \fBstdin\fP, the \fI\%\-\-format\fP option
-is required to specify the \fI\%serialization format\fP
+is required to specify the \X'tty: link #serialization-formats'\fI\%serialization format\fP\X'tty: link'
 of the input (e.g., \fBjson\fP or \fBxml\fP).
 .sp
 Loading from \fBstdin\fP is useful with standard input and output redirections.
@@ -721,11 +695,9 @@ For example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin dumpdata \-\-format=json \-\-database=test app_label.ModelName | django\-admin loaddata \-\-format=json \-\-database=prod \-
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -734,7 +706,7 @@ The \fI\%dumpdata\fP command can be used to generate input for \fBloaddata\fP\&.
 \fBSEE ALSO:\fP
 .INDENT 0.0
 .INDENT 3.5
-For more detail about fixtures see the \fI\%Fixtures\fP topic.
+For more detail about fixtures see the \X'tty: link #fixtures-explanation'\fI\%Fixtures\fP\X'tty: link' topic.
 .UNINDENT
 .UNINDENT
 .SS \fBmakemessages\fP
@@ -748,11 +720,11 @@ strings marked for translation. It creates (or updates) a message file in the
 conf/locale (in the Django tree) or locale (for project and application)
 directory. After making changes to the messages files you need to compile them
 with \fI\%compilemessages\fP for use with the builtin gettext support. See
-the \fI\%i18n documentation\fP for details.
+the \X'tty: link #how-to-create-language-files'\fI\%i18n documentation\fP\X'tty: link' for details.
 .sp
 This command doesn\(aqt require configured settings. However, when settings aren\(aqt
-configured, the command can\(aqt ignore the \fI\%MEDIA_ROOT\fP and
-\fI\%STATIC_ROOT\fP directories or include \fI\%LOCALE_PATHS\fP\&.
+configured, the command can\(aqt ignore the \X'tty: link #std-setting-MEDIA_ROOT'\fI\%MEDIA_ROOT\fP\X'tty: link' and
+\X'tty: link #std-setting-STATIC_ROOT'\fI\%STATIC_ROOT\fP\X'tty: link' directories or include \X'tty: link #std-setting-LOCALE_PATHS'\fI\%LOCALE_PATHS\fP\X'tty: link'\&.
 .INDENT 0.0
 .TP
 .B \-\-all, \-a
@@ -765,17 +737,15 @@ Updates the message files for all available languages.
 .UNINDENT
 .sp
 Specifies a list of file extensions to examine (default: \fBhtml\fP, \fBtxt\fP,
-\fBpy\fP or \fBjs\fP if \fI\%\-\-domain\fP is \fBjs\fP).
+\fBpy\fP or \fBjs\fP if \fI\%\-\-domain\fP is \fBdjangojs\fP).
 .sp
 Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin makemessages \-\-locale=de \-\-extension xhtml
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -784,11 +754,9 @@ multiple times:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin makemessages \-\-locale=de \-\-extension=html,txt \-\-extension xml
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -809,8 +777,7 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin makemessages \-\-locale=pt_BR
 django\-admin makemessages \-\-locale=pt_BR \-\-locale=fr
 django\-admin makemessages \-l pt_BR
@@ -819,8 +786,7 @@ django\-admin makemessages \-\-exclude=pt_BR
 django\-admin makemessages \-\-exclude=pt_BR \-\-exclude=fr
 django\-admin makemessages \-x pt_BR
 django\-admin makemessages \-x pt_BR \-x fr
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -846,11 +812,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin makemessages \-\-locale=de \-\-symlinks
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -858,7 +822,7 @@ django\-admin makemessages \-\-locale=de \-\-symlinks
 .B \-\-ignore PATTERN, \-i PATTERN
 .UNINDENT
 .sp
-Ignores files or directories matching the given \fI\%glob\fP\-style pattern. Use
+Ignores files or directories matching the given \X'tty: link https://docs.python.org/3/library/glob.html#module-glob'\fI\%glob\fP\X'tty: link'\-style pattern. Use
 multiple times to ignore more.
 .sp
 These patterns are used by default: \fB\(aqCVS\(aq\fP, \fB\(aq.*\(aq\fP, \fB\(aq*~\(aq\fP, \fB\(aq*.pyc\(aq\fP\&.
@@ -867,11 +831,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin makemessages \-\-locale=en_US \-\-ignore=apps/* \-\-ignore=secret/*.html
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -930,7 +892,7 @@ language files from being created.
 \fBSEE ALSO:\fP
 .INDENT 0.0
 .INDENT 3.5
-See \fI\%Customizing the makemessages command\fP for instructions on how to customize
+See \X'tty: link #customizing-makemessages'\fI\%Customizing the makemessages command\fP\X'tty: link' for instructions on how to customize
 the keywords that \fI\%makemessages\fP passes to \fBxgettext\fP\&.
 .UNINDENT
 .UNINDENT
@@ -986,7 +948,7 @@ Enables fixing of migration conflicts.
 .UNINDENT
 .sp
 Allows naming the generated migration(s) instead of using a generated name. The
-name must be a valid Python \fI\%identifier\fP\&.
+name must be a valid Python \X'tty: link https://docs.python.org/3/reference/lexical_analysis.html#identifiers'\fI\%identifier\fP\X'tty: link'\&.
 .INDENT 0.0
 .TP
 .B \-\-no\-header
@@ -999,11 +961,7 @@ Generate migration files without Django version and timestamp header.
 .UNINDENT
 .sp
 Makes \fBmakemigrations\fP exit with a non\-zero status when model changes without
-migrations are detected.
-.sp
-In older versions, the missing migrations were also created when using the
-\fB\-\-check\fP option.
-
+migrations are detected. Implies \fB\-\-dry\-run\fP\&.
 .INDENT 0.0
 .TP
 .B \-\-scriptable
@@ -1015,8 +973,6 @@ generated migration files to \fBstdout\fP\&.
 .TP
 .B \-\-update
 .UNINDENT
-.sp
-
 .sp
 Merges model changes into the latest migration and optimize the resulting
 operations.
@@ -1085,7 +1041,7 @@ run correctly.
 .sp
 Allows Django to skip an app\(aqs initial migration if all database tables with
 the names of all models created by all
-\fI\%CreateModel\fP operations in that
+\X'tty: link #django.db.migrations.operations.CreateModel'\fI\%CreateModel\fP\X'tty: link' operations in that
 migration already exist. This option is intended for use when first running
 migrations against a database that preexisted the use of migrations. This
 option does not, however, check for matching database schema beyond matching
@@ -1127,7 +1083,7 @@ detected.
 .sp
 Deletes nonexistent migrations from the \fBdjango_migrations\fP table. This is
 useful when migration files replaced by a squashed migration have been removed.
-See \fI\%Squashing migrations\fP for more details.
+See \X'tty: link #migration-squashing'\fI\%Squashing migrations\fP\X'tty: link' for more details.
 .SS \fBoptimizemigration\fP
 .INDENT 0.0
 .TP
@@ -1160,7 +1116,7 @@ might not have access to start a port on a low port number. Low port numbers
 are reserved for the superuser (root).
 .sp
 This server uses the WSGI application object specified by the
-\fI\%WSGI_APPLICATION\fP setting.
+\X'tty: link #std-setting-WSGI_APPLICATION'\fI\%WSGI_APPLICATION\fP\X'tty: link' setting.
 .sp
 DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through
 security audits or performance tests. (And that\(aqs how it\(aqs gonna stay. We\(aqre in
@@ -1173,8 +1129,8 @@ needed. You don\(aqt need to restart the server for code changes to take effect.
 However, some actions like adding files don\(aqt trigger a restart, so you\(aqll
 have to restart the server in these cases.
 .sp
-If you\(aqre using Linux or MacOS and install both \fI\%pywatchman\fP and the
-\fI\%Watchman\fP service, kernel signals will be used to autoreload the server
+If you\(aqre using Linux or MacOS and install both \X'tty: link https://pypi.org/project/pywatchman/'\fI\%pywatchman\fP\X'tty: link' and the
+\X'tty: link https://facebook.github.io/watchman/'\fI\%Watchman\fP\X'tty: link' service, kernel signals will be used to autoreload the server
 (rather than polling file modification timestamps each second). This offers
 better performance on large projects, reduced response time after code changes,
 more robust change detection, and a reduction in power usage. Django supports
@@ -1185,7 +1141,7 @@ more robust change detection, and a reduction in power usage. Django supports
 .sp
 When using Watchman with a project that includes large non\-Python
 directories like \fBnode_modules\fP, it\(aqs advisable to ignore this directory
-for optimal performance. See the \fI\%watchman documentation\fP for information
+for optimal performance. See the \X'tty: link https://facebook.github.io/watchman/docs/config#ignore_dirs'\fI\%watchman documentation\fP\X'tty: link' for information
 on how to do this.
 .UNINDENT
 .UNINDENT
@@ -1223,10 +1179,10 @@ A hostname containing ASCII\-only characters can also be used.
 .sp
 If the \fI\%staticfiles\fP contrib app is enabled
 (default in new projects) the \fI\%runserver\fP command will be overridden
-with its own \fI\%runserver\fP command.
+with its own \X'tty: link #staticfiles-runserver'\fI\%runserver\fP\X'tty: link' command.
 .sp
 Logging of each request and response of the server is sent to the
-\fI\%django.server\fP logger.
+\X'tty: link #django-server-logger'\fI\%django.server\fP\X'tty: link' logger.
 .INDENT 0.0
 .TP
 .B \-\-noreload
@@ -1255,11 +1211,9 @@ Port 8000 on IP address \fB127.0.0.1\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1267,11 +1221,9 @@ Port 8000 on IP address \fB1.2.3.4\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver 1.2.3.4:8000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1279,11 +1231,9 @@ Port 7000 on IP address \fB127.0.0.1\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver 7000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1291,11 +1241,9 @@ Port 7000 on IP address \fB1.2.3.4\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver 1.2.3.4:7000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1303,11 +1251,9 @@ Port 8000 on IPv6 address \fB::1\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver \-6
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1315,11 +1261,9 @@ Port 7000 on IPv6 address \fB::1\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver \-6 7000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1327,11 +1271,9 @@ Port 7000 on IPv6 address \fB2001:0db8:1234:5678::9\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver [2001:0db8:1234:5678::9]:7000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1339,11 +1281,9 @@ Port 8000 on IPv4 address of host \fBlocalhost\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver localhost:8000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1351,24 +1291,22 @@ Port 8000 on IPv6 address of host \fBlocalhost\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver \-6 localhost:8000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SS Serving static files with the development server
 .sp
 By default, the development server doesn\(aqt serve any static files for your site
-(such as CSS files, images, things under \fI\%MEDIA_URL\fP and so forth). If
+(such as CSS files, images, things under \X'tty: link #std-setting-MEDIA_URL'\fI\%MEDIA_URL\fP\X'tty: link' and so forth). If
 you want to configure Django to serve static media, read
 \fI\%How to manage static files (e.g. images, JavaScript, CSS)\fP\&.
 .SS Serving with ASGI in development
 .sp
 Django\(aqs \fBrunserver\fP command provides a WSGI server. In order to run under
 ASGI you will need to use an \fI\%ASGI server\fP\&.
-The Django Daphne project provides \fI\%Integration with runserver\fP that you can use.
+The Django Daphne project provides \X'tty: link #daphne-runserver'\fI\%Integration with runserver\fP\X'tty: link' that you can use.
 .SS \fBsendtestemail\fP
 .INDENT 0.0
 .TP
@@ -1380,11 +1318,9 @@ recipient(s) specified. For example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin sendtestemail foo@example.com bar@example.com
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1395,15 +1331,15 @@ together:
 .B \-\-managers
 .UNINDENT
 .sp
-Mails the email addresses specified in \fI\%MANAGERS\fP using
-\fI\%mail_managers()\fP\&.
+Mails the email addresses specified in \X'tty: link #std-setting-MANAGERS'\fI\%MANAGERS\fP\X'tty: link' using
+\X'tty: link #django.core.mail.mail_managers'\fI\%mail_managers()\fP\X'tty: link'\&.
 .INDENT 0.0
 .TP
 .B \-\-admins
 .UNINDENT
 .sp
-Mails the email addresses specified in \fI\%ADMINS\fP using
-\fI\%mail_admins()\fP\&.
+Mails the email addresses specified in \X'tty: link #std-setting-ADMINS'\fI\%ADMINS\fP\X'tty: link' using
+\X'tty: link #django.core.mail.mail_admins'\fI\%mail_admins()\fP\X'tty: link'\&.
 .SS \fBshell\fP
 .INDENT 0.0
 .TP
@@ -1416,18 +1352,16 @@ Starts the Python interactive interpreter.
 .B \-\-interface {ipython,bpython,python}, \-i {ipython,bpython,python}
 .UNINDENT
 .sp
-Specifies the shell to use. By default, Django will use \fI\%IPython\fP or \fI\%bpython\fP if
+Specifies the shell to use. By default, Django will use \X'tty: link https://ipython.org/'\fI\%IPython\fP\X'tty: link' or \X'tty: link https://bpython-interpreter.org/'\fI\%bpython\fP\X'tty: link' if
 either is installed. If both are installed, specify which one you want like so:
 .sp
 IPython:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin shell \-i ipython
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1435,11 +1369,9 @@ bpython:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin shell \-i bpython
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1448,11 +1380,9 @@ Python interpreter, use \fBpython\fP as the interface name, like so:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin shell \-i python
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -1461,7 +1391,7 @@ django\-admin shell \-i python
 .UNINDENT
 .sp
 Disables reading the startup script for the \(dqplain\(dq Python interpreter. By
-default, the script pointed to by the \fI\%PYTHONSTARTUP\fP environment
+default, the script pointed to by the \X'tty: link https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSTARTUP'\fI\%PYTHONSTARTUP\fP\X'tty: link' environment
 variable or the \fB~/.pythonrc.py\fP script is read.
 .INDENT 0.0
 .TP
@@ -1472,11 +1402,9 @@ Lets you pass a command as a string to execute it as Django, like so:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin shell \-\-command=\(dqimport django; print(django.__version__)\(dq
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1484,19 +1412,17 @@ You can also pass code in on standard input to execute it. For example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 $ django\-admin shell <<EOF
 > import django
 > print(django.__version__)
 > EOF
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
 On Windows, the REPL is output due to implementation limits of
-\fI\%select.select()\fP on that platform.
+\X'tty: link https://docs.python.org/3/library/select.html#select.select'\fI\%select.select()\fP\X'tty: link' on that platform.
 .SS \fBshowmigrations\fP
 .INDENT 0.0
 .TP
@@ -1601,12 +1527,12 @@ Specifies the database for which to print the SQL. Defaults to \fBdefault\fP\&.
 Squashes the migrations for \fBapp_label\fP up to and including \fBmigration_name\fP
 down into fewer migrations, if possible. The resulting squashed migrations
 can live alongside the unsquashed ones safely. For more information,
-please read \fI\%Squashing migrations\fP\&.
+please read \X'tty: link #migration-squashing'\fI\%Squashing migrations\fP\X'tty: link'\&.
 .sp
 When \fBstart_migration_name\fP is given, Django will only include migrations
 starting from and including this migration. This helps to mitigate the
-squashing limitation of \fI\%RunPython\fP and
-\fI\%django.db.migrations.operations.RunSQL\fP migration operations.
+squashing limitation of \X'tty: link #django.db.migrations.operations.RunPython'\fI\%RunPython\fP\X'tty: link' and
+\X'tty: link #django.db.migrations.operations.RunSQL'\fI\%django.db.migrations.operations.RunSQL\fP\X'tty: link' migration operations.
 .INDENT 0.0
 .TP
 .B \-\-no\-optimize
@@ -1645,7 +1571,7 @@ Generate squashed migration file without Django version and timestamp header.
 Creates a Django app directory structure for the given app name in the current
 directory or the given destination.
 .sp
-By default, \fI\%the new directory\fP contains a
+By default, \X'tty: link https://github.com/django/django/blob/main/django/conf/app_template'\fI\%the new directory\fP\X'tty: link' contains a
 \fBmodels.py\fP file and other app template files. If only the app name is given,
 the app directory will be created in the current working directory.
 .sp
@@ -1657,11 +1583,9 @@ For example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin startapp myapp /Users/jezdez/Code/myapp
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -1679,11 +1603,9 @@ creating the \fBmyapp\fP app:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin startapp \-\-template=/Users/jezdez/Code/my_app_template myapp
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -1696,11 +1618,9 @@ zip files, you can use a URL like:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin startapp \-\-template=https://github.com/githubuser/django\-app\-template/archive/main.zip myapp
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -1727,7 +1647,7 @@ Specifies which directories in the app template should be excluded, in addition
 to \fB\&.git\fP and \fB__pycache__\fP\&. If this option is not provided, directories
 named \fB__pycache__\fP or starting with \fB\&.\fP will be excluded.
 .sp
-The \fI\%template context\fP used for all matching
+The \X'tty: link #django.template.Context'\fI\%template context\fP\X'tty: link' used for all matching
 files is:
 .INDENT 0.0
 .IP \(bu 2
@@ -1754,7 +1674,7 @@ stray template variables contained. For example, if one of the Python files
 contains a docstring explaining a particular feature related
 to template rendering, it might result in an incorrect example.
 .sp
-To work around this problem, you can use the \fI\%templatetag\fP
+To work around this problem, you can use the \X'tty: link #std-templatetag-templatetag'\fI\%templatetag\fP\X'tty: link'
 template tag to \(dqescape\(dq the various parts of the template syntax.
 .sp
 In addition, to allow Python template files that contain Django template
@@ -1786,7 +1706,7 @@ so make sure any custom template you use is worthy of your trust.
 Creates a Django project directory structure for the given project name in
 the current directory or the given destination.
 .sp
-By default, \fI\%the new directory\fP contains
+By default, \X'tty: link https://github.com/django/django/blob/main/django/conf/project_template'\fI\%the new directory\fP\X'tty: link' contains
 \fBmanage.py\fP and a project package (containing a \fBsettings.py\fP and other
 files).
 .sp
@@ -1802,11 +1722,9 @@ For example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin startproject myproject /Users/jezdez/Code/myproject_repo
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -1840,7 +1758,7 @@ Specifies which directories in the project template should be excluded, in
 addition to \fB\&.git\fP and \fB__pycache__\fP\&. If this option is not provided,
 directories named \fB__pycache__\fP or starting with \fB\&.\fP will be excluded.
 .sp
-The \fI\%template context\fP used is:
+The \X'tty: link #django.template.Context'\fI\%template context\fP\X'tty: link' used is:
 .INDENT 0.0
 .IP \(bu 2
 Any option passed to the \fBstartproject\fP command (among the command\(aqs
@@ -1850,7 +1768,7 @@ supported options)
 .IP \(bu 2
 \fBproject_directory\fP \-\- the full path of the newly created project
 .IP \(bu 2
-\fBsecret_key\fP \-\- a random key for the \fI\%SECRET_KEY\fP setting
+\fBsecret_key\fP \-\- a random key for the \X'tty: link #std-setting-SECRET_KEY'\fI\%SECRET_KEY\fP\X'tty: link' setting
 .IP \(bu 2
 \fBdocs_version\fP \-\- the version of the documentation: \fB\(aqdev\(aq\fP or \fB\(aq1.x\(aq\fP
 .IP \(bu 2
@@ -1880,7 +1798,7 @@ Stops running tests and reports the failure immediately after a test fails.
 .UNINDENT
 .sp
 Controls the test runner class that is used to execute tests. This value
-overrides the value provided by the \fI\%TEST_RUNNER\fP setting.
+overrides the value provided by the \X'tty: link #std-setting-TEST_RUNNER'\fI\%TEST_RUNNER\fP\X'tty: link' setting.
 .INDENT 0.0
 .TP
 .B \-\-noinput, \-\-no\-input
@@ -1892,7 +1810,7 @@ existing test database.
 .sp
 The \fBtest\fP command receives options on behalf of the specified
 \fI\%\-\-testrunner\fP\&. These are the options of the default test runner:
-\fI\%DiscoverRunner\fP\&.
+\X'tty: link #django.test.runner.DiscoverRunner'\fI\%DiscoverRunner\fP\X'tty: link'\&.
 .INDENT 0.0
 .TP
 .B \-\-keepdb
@@ -1902,7 +1820,7 @@ Preserves the test database between test runs. This has the advantage of
 skipping both the create and destroy actions which can greatly decrease the
 time to run tests, especially those in a large test suite. If the test database
 does not exist, it will be created on the first run and then preserved for each
-subsequent run. Unless the \fI\%MIGRATE\fP test setting is
+subsequent run. Unless the \X'tty: link #std-setting-TEST_MIGRATE'\fI\%MIGRATE\fP\X'tty: link' test setting is
 \fBFalse\fP, any unapplied migrations will also be applied to the test database
 before running the test suite.
 .INDENT 0.0
@@ -1915,7 +1833,7 @@ that aren\(aqt properly isolated. The test order generated by this option is a
 deterministic function of the integer seed given. When no seed is passed, a
 seed is chosen randomly and printed to the console. To repeat a particular test
 order, pass a seed. The test orders generated by this option preserve Django\(aqs
-\fI\%guarantees on test order\fP\&. They also keep tests grouped
+\X'tty: link #order-of-tests'\fI\%guarantees on test order\fP\X'tty: link'\&. They also keep tests grouped
 by test case class.
 .sp
 The shuffled orderings also have a special consistency property useful when
@@ -1929,22 +1847,22 @@ order of the original tests will be the same in the new order.
 .UNINDENT
 .sp
 Sorts test cases in the opposite execution order. This may help in debugging
-the side effects of tests that aren\(aqt properly isolated. \fI\%Grouping by test
-class\fP is preserved when using this option. This can be used
+the side effects of tests that aren\(aqt properly isolated. \X'tty: link #order-of-tests'\fI\%Grouping by test
+class\fP\X'tty: link' is preserved when using this option. This can be used
 in conjunction with \fB\-\-shuffle\fP to reverse the order for a particular seed.
 .INDENT 0.0
 .TP
 .B \-\-debug\-mode
 .UNINDENT
 .sp
-Sets the \fI\%DEBUG\fP setting to \fBTrue\fP prior to running tests. This may
+Sets the \X'tty: link #std-setting-DEBUG'\fI\%DEBUG\fP\X'tty: link' setting to \fBTrue\fP prior to running tests. This may
 help troubleshoot test failures.
 .INDENT 0.0
 .TP
 .B \-\-debug\-sql, \-d
 .UNINDENT
 .sp
-Enables \fI\%SQL logging\fP for failing tests. If
+Enables \X'tty: link #django-db-logger'\fI\%SQL logging\fP\X'tty: link' for failing tests. If
 \fB\-\-verbosity\fP is \fB2\fP, then queries in passing tests are also output.
 .INDENT 0.0
 .TP
@@ -1959,25 +1877,25 @@ Runs tests in separate parallel processes. Since modern processors have
 multiple cores, this allows running tests significantly faster.
 .sp
 Using \fB\-\-parallel\fP without a value, or with the value \fBauto\fP, runs one test
-process per core according to \fI\%multiprocessing.cpu_count()\fP\&. You can
+process per core according to \X'tty: link https://docs.python.org/3/library/multiprocessing.html#multiprocessing.cpu_count'\fI\%multiprocessing.cpu_count()\fP\X'tty: link'\&. You can
 override this by passing the desired number of processes, e.g.
 \fB\-\-parallel 4\fP, or by setting the \fI\%DJANGO_TEST_PROCESSES\fP environment
 variable.
 .sp
-Django distributes test cases — \fI\%unittest.TestCase\fP subclasses — to
-subprocesses. If there are fewer test cases than configured processes, Django
-will reduce the number of processes accordingly.
+Django distributes test cases — \X'tty: link https://docs.python.org/3/library/unittest.html#unittest.TestCase'\fI\%unittest.TestCase\fP\X'tty: link' subclasses — to
+subprocesses. If there are fewer test case classes than configured processes,
+Django will reduce the number of processes accordingly.
 .sp
-Each process gets its own database. You must ensure that different test cases
-don\(aqt access the same resources. For instance, test cases that touch the
-filesystem should create a temporary directory for their own use.
+Each process gets its own database. You must ensure that different test case
+classes don\(aqt access the same resources. For instance, test case classes that
+touch the filesystem should create a temporary directory for their own use.
 .sp
 \fBNOTE:\fP
 .INDENT 0.0
 .INDENT 3.5
 If you have test classes that cannot be run in parallel, you can use
-\fBSerializeMixin\fP to run them sequentially. See \fI\%Enforce running test
-classes sequentially\fP\&.
+\fBSerializeMixin\fP to run them sequentially. See \X'tty: link #topics-testing-enforce-run-sequentially'\fI\%Enforce running test
+classes sequentially\fP\X'tty: link'\&.
 .UNINDENT
 .UNINDENT
 .sp
@@ -1986,18 +1904,16 @@ correctly:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 $ python \-m pip install tblib
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
 This feature isn\(aqt available on Windows. It doesn\(aqt work with the Oracle
 database backend either.
 .sp
-If you want to use \fI\%pdb\fP while debugging tests, you must disable parallel
+If you want to use \X'tty: link https://docs.python.org/3/library/pdb.html#module-pdb'\fI\%pdb\fP\X'tty: link' while debugging tests, you must disable parallel
 execution (\fB\-\-parallel=1\fP). You\(aqll see something like \fBbdb.BdbQuit\fP if you
 don\(aqt.
 .sp
@@ -2011,7 +1927,7 @@ parallelization to see the traceback of the failure.
 .sp
 This is a known limitation. It arises from the need to serialize objects
 in order to exchange them between processes. See
-\fI\%What can be pickled and unpickled?\fP for details.
+\X'tty: link https://docs.python.org/3/library/pickle.html#pickle-picklable'\fI\%What can be pickled and unpickled?\fP\X'tty: link' for details.
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2019,7 +1935,7 @@ in order to exchange them between processes. See
 .B \-\-tag TAGS
 .UNINDENT
 .sp
-Runs only tests \fI\%marked with the specified tags\fP\&.
+Runs only tests \X'tty: link #topics-tagging-tests'\fI\%marked with the specified tags\fP\X'tty: link'\&.
 May be specified multiple times and combined with \fI\%test \-\-exclude\-tag\fP\&.
 .sp
 Tests that fail to load are always considered matching.
@@ -2028,7 +1944,7 @@ Tests that fail to load are always considered matching.
 .B \-\-exclude\-tag EXCLUDE_TAGS
 .UNINDENT
 .sp
-Excludes tests \fI\%marked with the specified tags\fP\&.
+Excludes tests \X'tty: link #topics-tagging-tests'\fI\%marked with the specified tags\fP\X'tty: link'\&.
 May be specified multiple times and combined with \fI\%test \-\-tag\fP\&.
 .INDENT 0.0
 .TP
@@ -2036,7 +1952,7 @@ May be specified multiple times and combined with \fI\%test \-\-tag\fP\&.
 .UNINDENT
 .sp
 Runs test methods and classes matching test name patterns, in the same way as
-\fI\%unittest\(aqs \-k option\fP\&. Can be specified multiple times.
+\X'tty: link https://docs.python.org/3/library/unittest.html#cmdoption-unittest-k'\fI\%unittest\(aqs \-k option\fP\X'tty: link'\&. Can be specified multiple times.
 .INDENT 0.0
 .TP
 .B \-\-pdb
@@ -2050,13 +1966,13 @@ installed, \fBipdb\fP is used instead.
 .UNINDENT
 .sp
 Discards output (\fBstdout\fP and \fBstderr\fP) for passing tests, in the same way
-as \fI\%unittest\(aqs \-\-buffer option\fP\&.
+as \X'tty: link https://docs.python.org/3/library/unittest.html#cmdoption-unittest-b'\fI\%unittest\(aqs \-\-buffer option\fP\X'tty: link'\&.
 .INDENT 0.0
 .TP
 .B \-\-no\-faulthandler
 .UNINDENT
 .sp
-Django automatically calls \fI\%faulthandler.enable()\fP when starting the
+Django automatically calls \X'tty: link https://docs.python.org/3/library/faulthandler.html#faulthandler.enable'\fI\%faulthandler.enable()\fP\X'tty: link' when starting the
 tests, which allows it to print a traceback if the interpreter crashes. Pass
 \fB\-\-no\-faulthandler\fP to disable this behavior.
 .INDENT 0.0
@@ -2093,18 +2009,16 @@ For example, this command:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin testserver mydata.json
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
 \&...would perform the following steps:
 .INDENT 0.0
 .IP 1. 3
-Create a test database, as described in \fI\%The test database\fP\&.
+Create a test database, as described in \X'tty: link #the-test-database'\fI\%The test database\fP\X'tty: link'\&.
 .IP 2. 3
 Populate the test database with fixture data from the given fixtures.
 (For more on fixtures, see the documentation for \fI\%loaddata\fP above.)
@@ -2122,7 +2036,7 @@ the views in a web browser, manually.
 .IP \(bu 2
 Let\(aqs say you\(aqre developing your Django application and have a \(dqpristine\(dq
 copy of a database that you\(aqd like to interact with. You can dump your
-database to a \fI\%fixture\fP (using the
+database to a \X'tty: link #fixtures-explanation'\fI\%fixture\fP\X'tty: link' (using the
 \fI\%dumpdata\fP command, explained above), then use \fBtestserver\fP to run
 your web application with that data.  With this arrangement, you have the
 flexibility of messing up your data in any way, knowing that whatever data
@@ -2147,12 +2061,10 @@ To run the test server on port 7000 with \fBfixture1\fP and \fBfixture2\fP:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin testserver \-\-addrport 7000 fixture1 fixture2
 django\-admin testserver fixture1 fixture2 \-\-addrport 7000
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2164,11 +2076,9 @@ To run on 1.2.3.4:7000 with a \fBtest\fP fixture:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin testserver \-\-addrport 1.2.3.4:7000 test
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2182,7 +2092,7 @@ existing test database.
 .sp
 Some commands are only available when the \fBdjango.contrib\fP application that
 \fI\%implements\fP them has been
-\fI\%enabled\fP\&. This section describes them grouped by
+\X'tty: link #std-setting-INSTALLED_APPS'\fI\%enabled\fP\X'tty: link'\&. This section describes them grouped by
 their application.
 .SS \fBdjango.contrib.auth\fP
 .SS \fBchangepassword\fP
@@ -2208,11 +2118,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin changepassword ringo
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SS \fBcreatesuperuser\fP
@@ -2238,9 +2146,9 @@ variable. Otherwise, no password will be set, and the superuser account will
 not be able to log in until a password has been manually set for it.
 .sp
 In non\-interactive mode, the
-\fI\%USERNAME_FIELD\fP and required
+\X'tty: link #django.contrib.auth.models.CustomUser.USERNAME_FIELD'\fI\%USERNAME_FIELD\fP\X'tty: link' and required
 fields (listed in
-\fI\%REQUIRED_FIELDS\fP) fall back to
+\X'tty: link #django.contrib.auth.models.CustomUser.REQUIRED_FIELDS'\fI\%REQUIRED_FIELDS\fP\X'tty: link') fall back to
 \fBDJANGO_SUPERUSER_<uppercase_field_name>\fP environment variables, unless they
 are overridden by a command line argument. For example, to provide an \fBemail\fP
 field, you can use \fBDJANGO_SUPERUSER_EMAIL\fP environment variable.
@@ -2275,7 +2183,7 @@ You can subclass the management command and override \fBget_input_data()\fP if y
 want to customize data input and validation. Consult the source code for
 details on the existing implementation and the method\(aqs parameters. For example,
 it could be useful if you have a \fBForeignKey\fP in
-\fI\%REQUIRED_FIELDS\fP and want to
+\X'tty: link #django.contrib.auth.models.CustomUser.REQUIRED_FIELDS'\fI\%REQUIRED_FIELDS\fP\X'tty: link' and want to
 allow creating an instance instead of entering the primary key of an existing
 instance.
 .SS \fBdjango.contrib.contenttypes\fP
@@ -2285,7 +2193,7 @@ instance.
 .B django\-admin remove_stale_contenttypes
 .UNINDENT
 .sp
-This command is only available if Django\(aqs \fI\%contenttypes app\fP (\fI\%django.contrib.contenttypes\fP) is installed.
+This command is only available if Django\(aqs \fI\%contenttypes app\fP (\X'tty: link #module-django.contrib.contenttypes'\fI\%django.contrib.contenttypes\fP\X'tty: link') is installed.
 .sp
 Deletes stale content types (from deleted models) in your database. Any objects
 that depend on the deleted content types will also be deleted. A list of
@@ -2303,14 +2211,14 @@ Specifies the database to use. Defaults to \fBdefault\fP\&.
 .UNINDENT
 .sp
 Deletes stale content types including ones from previously installed apps that
-have been removed from \fI\%INSTALLED_APPS\fP\&. Defaults to \fBFalse\fP\&.
+have been removed from \X'tty: link #std-setting-INSTALLED_APPS'\fI\%INSTALLED_APPS\fP\X'tty: link'\&. Defaults to \fBFalse\fP\&.
 .SS \fBdjango.contrib.gis\fP
 .SS \fBogrinspect\fP
 .sp
 This command is only available if \fI\%GeoDjango\fP
 (\fBdjango.contrib.gis\fP) is installed.
 .sp
-Please refer to its \fI\%description\fP in the GeoDjango
+Please refer to its \X'tty: link #django-admin-ogrinspect'\fI\%description\fP\X'tty: link' in the GeoDjango
 documentation.
 .SS \fBdjango.contrib.sessions\fP
 .SS \fBclearsessions\fP
@@ -2325,13 +2233,13 @@ Can be run as a cron job or directly to clean out expired sessions.
 .sp
 This command is only available if the \fI\%static files application\fP (\fBdjango.contrib.staticfiles\fP) is installed.
 .sp
-Please refer to its \fI\%description\fP in the
+Please refer to its \X'tty: link #django-admin-collectstatic'\fI\%description\fP\X'tty: link' in the
 \fI\%staticfiles\fP documentation.
 .SS \fBfindstatic\fP
 .sp
 This command is only available if the \fI\%static files application\fP (\fBdjango.contrib.staticfiles\fP) is installed.
 .sp
-Please refer to its \fI\%description\fP in the \fI\%staticfiles\fP documentation.
+Please refer to its \X'tty: link #django-admin-findstatic'\fI\%description\fP\X'tty: link' in the \fI\%staticfiles\fP documentation.
 .SH DEFAULT OPTIONS
 .sp
 Although some commands may allow their own custom options, every command
@@ -2341,9 +2249,9 @@ allows for the following options by default:
 .B \-\-pythonpath PYTHONPATH
 .UNINDENT
 .sp
-Adds the given filesystem path to the Python \fI\%import search path\fP\&. If this
-isn\(aqt provided, \fBdjango\-admin\fP will use the \fI\%PYTHONPATH\fP environment
-variable.
+Adds the given filesystem path to the Python \X'tty: link https://docs.python.org/3/library/sys.html#sys.path'\fI\%sys.path\fP\X'tty: link' module
+attribute. If this isn\(aqt provided, \fBdjango\-admin\fP will use the
+\X'tty: link https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH'\fI\%PYTHONPATH\fP\X'tty: link' environment variable.
 .sp
 This option is unnecessary in \fBmanage.py\fP, because it takes care of setting
 the Python path for you.
@@ -2352,11 +2260,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin migrate \-\-pythonpath=\(aq/home/djangoprojects/myproject\(aq
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2366,7 +2272,7 @@ django\-admin migrate \-\-pythonpath=\(aq/home/djangoprojects/myproject\(aq
 .sp
 Specifies the settings module to use. The settings module should be in Python
 package syntax, e.g. \fBmysite.settings\fP\&. If this isn\(aqt provided,
-\fBdjango\-admin\fP will use the \fI\%DJANGO_SETTINGS_MODULE\fP environment
+\fBdjango\-admin\fP will use the \X'tty: link #envvar-DJANGO_SETTINGS_MODULE'\fI\%DJANGO_SETTINGS_MODULE\fP\X'tty: link' environment
 variable.
 .sp
 This option is unnecessary in \fBmanage.py\fP, because it uses
@@ -2376,11 +2282,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin migrate \-\-settings=mysite.settings
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2388,7 +2292,7 @@ django\-admin migrate \-\-settings=mysite.settings
 .B \-\-traceback
 .UNINDENT
 .sp
-Displays a full stack trace when a \fI\%CommandError\fP
+Displays a full stack trace when a \X'tty: link #django.core.management.CommandError'\fI\%CommandError\fP\X'tty: link'
 is raised. By default, \fBdjango\-admin\fP will show an error message when a
 \fBCommandError\fP occurs and a full stack trace for any other exception.
 .sp
@@ -2398,11 +2302,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin migrate \-\-traceback
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2429,11 +2331,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin migrate \-\-verbosity 2
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2449,11 +2349,9 @@ Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin runserver \-\-no\-color
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2471,18 +2369,16 @@ colored output to another command.
 .sp
 Skips running system checks prior to running the command. This option is only
 available if the
-\fI\%requires_system_checks\fP command
+\X'tty: link #django.core.management.BaseCommand.requires_system_checks'\fI\%requires_system_checks\fP\X'tty: link' command
 attribute is not an empty list or tuple.
 .sp
 Example usage:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin migrate \-\-skip\-checks
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SH EXTRA NICETIES
@@ -2498,7 +2394,7 @@ won\(aqt use the color codes if you\(aqre piping the command\(aqs output to
 another program unless the \fI\%\-\-force\-color\fP option is used.
 .SS Windows support
 .sp
-On Windows 10, the \fI\%Windows Terminal\fP application, \fI\%VS Code\fP, and PowerShell
+On Windows 10, the \X'tty: link https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701'\fI\%Windows Terminal\fP\X'tty: link' application, \X'tty: link https://code.visualstudio.com'\fI\%VS Code\fP\X'tty: link', and PowerShell
 (where virtual terminal processing is enabled) allow colored output, and are
 supported by default.
 .sp
@@ -2507,22 +2403,20 @@ escape sequences so by default there is no color output. In this case either of
 two third\-party libraries are needed:
 .INDENT 0.0
 .IP \(bu 2
-Install \fI\%colorama\fP, a Python package that translates ANSI color codes
+Install \X'tty: link https://pypi.org/project/colorama/'\fI\%colorama\fP\X'tty: link', a Python package that translates ANSI color codes
 into Windows API calls. Django commands will detect its presence and will
 make use of its services to color output just like on Unix\-based platforms.
 \fBcolorama\fP can be installed via pip:
 .INDENT 2.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 \&...\e> py \-m pip install \(dqcolorama >= 0.4.6\(dq
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .IP \(bu 2
-Install \fI\%ANSICON\fP, a third\-party tool that allows \fBcmd.exe\fP to process
+Install \X'tty: link http://adoxa.altervista.org/ansicon/'\fI\%ANSICON\fP\X'tty: link', a third\-party tool that allows \fBcmd.exe\fP to process
 ANSI color codes. Django commands will detect its presence and will make use
 of its services to color output just like on Unix\-based platforms.
 .UNINDENT
@@ -2553,11 +2447,9 @@ would run the following at a command prompt:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 export DJANGO_COLORS=\(dqlight\(dq
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2655,11 +2547,9 @@ are then separated by a semicolon. For example:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 export DJANGO_COLORS=\(dqerror=yellow/blue,blink;notice=magenta\(dq
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2673,11 +2563,9 @@ palette will be loaded. So:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 export DJANGO_COLORS=\(dqlight;error=yellow/blue,blink;notice=magenta\(dq
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2687,7 +2575,7 @@ overridden as specified.
 .SS Bash completion
 .sp
 If you use the Bash shell, consider installing the Django bash completion
-script, which lives in \fI\%extras/django_bash_completion\fP in the Django source
+script, which lives in \X'tty: link https://github.com/django/django/blob/main/extras/django_bash_completion'\fI\%extras/django_bash_completion\fP\X'tty: link' in the Django source
 distribution. It enables tab\-completion of \fBdjango\-admin\fP and
 \fBmanage.py\fP commands, so you can, for instance...
 .INDENT 0.0
@@ -2713,11 +2601,9 @@ current project, you can set the \fBPATH\fP explicitly:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 PATH=path/to/venv/bin django\-admin makemigrations
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2725,11 +2611,9 @@ For commands using \fBstdout\fP you can pipe the output to \fBblack\fP if needed
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 django\-admin inspectdb | black \-
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .INDENT 0.0
@@ -2760,16 +2644,14 @@ Examples:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 from django.core import management
 from django.core.management.commands import loaddata
 
 management.call_command(\(dqflush\(dq, verbosity=0, interactive=False)
 management.call_command(\(dqloaddata\(dq, \(dqtest_data\(dq, verbosity=0)
 management.call_command(loaddata.Command(), \(dqtest_data\(dq, verbosity=0)
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2780,8 +2662,7 @@ Named arguments can be passed by using either one of the following syntaxes:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 # Similar to the command line
 management.call_command(\(dqdumpdata\(dq, \(dq\-\-natural\-foreign\(dq)
 
@@ -2791,8 +2672,7 @@ management.call_command(\(dqdumpdata\(dq, natural_foreign=True)
 
 # \(gause_natural_foreign_keys\(ga is the option destination variable
 management.call_command(\(dqdumpdata\(dq, use_natural_foreign_keys=True)
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2807,11 +2687,9 @@ Command options which take multiple options are passed a list:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 management.call_command(\(dqdumpdata\(dq, exclude=[\(dqcontenttypes\(dq, \(dqauth\(dq])
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .sp
@@ -2824,12 +2702,10 @@ support the \fBstdout\fP and \fBstderr\fP options. For example, you could write:
 .INDENT 0.0
 .INDENT 3.5
 .sp
-.nf
-.ft C
+.EX
 with open(\(dq/path/to/command_output\(dq, \(dqw\(dq) as f:
     management.call_command(\(dqdumpdata\(dq, stdout=f)
-.ft P
-.fi
+.EE
 .UNINDENT
 .UNINDENT
 .SH AUTHOR
diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt
index 03063f2086..69d04380ce 100644
--- a/docs/ref/applications.txt
+++ b/docs/ref/applications.txt
@@ -431,11 +431,6 @@ application registry.
     It must be called explicitly in other cases, for instance in plain Python
     scripts.
 
-    .. versionchanged:: 5.0
-
-        Raises a ``RuntimeWarning`` when apps interact with the database before
-        the app registry has been fully populated.
-
 .. currentmodule:: django.apps
 
 The application registry is initialized in three stages. At each stage, Django
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index f0eeaca268..d78a6f76b2 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -575,7 +575,9 @@ configured:
 
 * **templates.E001**: You have ``'APP_DIRS': True`` in your
   :setting:`TEMPLATES` but also specify ``'loaders'`` in ``OPTIONS``. Either
-  remove ``APP_DIRS`` or remove the ``'loaders'`` option.
+  remove ``APP_DIRS`` or remove the ``'loaders'`` option. *This check is
+  removed in Django 5.1 as system checks may now raise*
+  ``ImproperlyConfigured`` *instead.*
 * **templates.E002**: ``string_if_invalid`` in :setting:`TEMPLATES`
   :setting:`OPTIONS <TEMPLATES-OPTIONS>` must be a string but got: ``{value}``
   (``{type}``).
@@ -726,9 +728,9 @@ with the admin site:
 * **admin.E106**: The value of ``<InlineModelAdmin class>.model`` must be a
   ``Model``.
 * **admin.E107**: The value of ``list_display`` must be a list or tuple.
-* **admin.E108**: The value of ``list_display[n]`` refers to ``<label>``,
-  which is not a callable, an attribute of ``<ModelAdmin class>``, or an
-  attribute or method on ``<model>``.
+* **admin.E108**: The value of ``list_display[n]`` refers to ``<label>``, which
+  is not a callable or attribute of ``<ModelAdmin class>``, or an attribute,
+  method, or field on ``<model>``.
 * **admin.E109**: The value of ``list_display[n]`` must not be a many-to-many
   field or a reverse foreign key.
 * **admin.E110**: The value of ``list_display_links`` must be a list, a tuple,
@@ -866,6 +868,10 @@ The following checks are performed on the default
   for its builtin permission names to be at most 100 characters.
 * **auth.E012**: The permission codenamed ``<codename>`` of model ``<model>``
   is longer than 100 characters.
+* **auth.E013**: In order to use
+  :class:`django.contrib.auth.middleware.LoginRequiredMiddleware`,
+  :class:`django.contrib.auth.middleware.AuthenticationMiddleware` must be
+  defined before it in MIDDLEWARE.
 
 ``contenttypes``
 ----------------
diff --git a/docs/ref/clickjacking.txt b/docs/ref/clickjacking.txt
index 3a81bdbdb0..f9bec591a7 100644
--- a/docs/ref/clickjacking.txt
+++ b/docs/ref/clickjacking.txt
@@ -90,11 +90,6 @@ that tells the middleware not to set the header::
     iframe, you may need to modify the :setting:`CSRF_COOKIE_SAMESITE` or
     :setting:`SESSION_COOKIE_SAMESITE` settings.
 
-.. versionchanged:: 5.0
-
-    Support for wrapping asynchronous view functions was added to the
-    ``@xframe_options_exempt`` decorator.
-
 Setting ``X-Frame-Options`` per view
 ------------------------------------
 
@@ -118,11 +113,6 @@ decorators::
 Note that you can use the decorators in conjunction with the middleware. Use of
 a decorator overrides the middleware.
 
-.. versionchanged:: 5.0
-
-    Support for wrapping asynchronous view functions was added to the
-    ``@xframe_options_deny`` and ``@xframe_options_sameorigin`` decorators.
-
 Limitations
 ===========
 
diff --git a/docs/ref/contrib/admin/_images/fieldsets.png b/docs/ref/contrib/admin/_images/fieldsets.png
index 2e00302b59..ae1a415e73 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/filters.txt b/docs/ref/contrib/admin/filters.txt
index fc70a1d6b2..d55e6fb946 100644
--- a/docs/ref/contrib/admin/filters.txt
+++ b/docs/ref/contrib/admin/filters.txt
@@ -216,8 +216,6 @@ concrete example.
 Facets
 ======
 
-.. versionadded:: 5.0
-
 By default, counts for each filter, known as facets, can be shown by toggling
 on via the admin UI. These counts will update according to the currently
 applied filters. See :attr:`ModelAdmin.show_facets` for more details.
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index e85ba9c36a..504ab62368 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -315,9 +315,9 @@ subclass::
     For more complex layout needs, see the :attr:`~ModelAdmin.fieldsets` option.
 
     The ``fields`` option accepts the same types of values as
-    :attr:`~ModelAdmin.list_display`, except that callables aren't accepted.
-    Names of model and model admin methods will only be used if they're listed
-    in :attr:`~ModelAdmin.readonly_fields`.
+    :attr:`~ModelAdmin.list_display`, except that callables and ``__`` lookups
+    for related fields aren't accepted. Names of model and model admin methods
+    will only be used if they're listed in :attr:`~ModelAdmin.readonly_fields`.
 
     To display multiple fields on the same line, wrap those fields in their own
     tuple. In this example, the ``url`` and ``title`` fields will display on the
@@ -413,18 +413,27 @@ 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``.
 
         Example::
 
             {
-                "classes": ["wide", "extrapretty"],
+                "classes": ["wide", "collapse"],
             }
 
-        Two useful classes defined by the default admin site stylesheet are
-        ``collapse`` and ``wide``. Fieldsets with the ``collapse`` style
-        will be initially collapsed in the admin and replaced with a small
-        "click to expand" link. Fieldsets with the ``wide`` style will be
-        given extra horizontal space.
+        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:: 5.1
+
+            ``fieldsets`` using the ``collapse`` class now use ``<details>``
+            and ``<summary>`` elements, provided they define a ``name``.
 
     * ``description``
         A string of optional extra text to be displayed at the top of each
@@ -565,7 +574,7 @@ subclass::
     If you don't set ``list_display``, the admin site will display a single
     column that displays the ``__str__()`` representation of each object.
 
-    There are four types of values that can be used in ``list_display``. All
+    There are five types of values that can be used in ``list_display``. All
     but the simplest may use the  :func:`~django.contrib.admin.display`
     decorator, which is used to customize how the field is presented:
 
@@ -574,6 +583,11 @@ subclass::
           class PersonAdmin(admin.ModelAdmin):
               list_display = ["first_name", "last_name"]
 
+    * The name of a related field, using the ``__`` notation. For example::
+
+          class PersonAdmin(admin.ModelAdmin):
+              list_display = ["city__name"]
+
     * A callable that accepts one argument, the model instance. For example::
 
           @admin.display(description="Name")
@@ -614,6 +628,11 @@ subclass::
           class PersonAdmin(admin.ModelAdmin):
               list_display = ["name", "decade_born_in"]
 
+    .. versionchanged:: 5.1
+
+        Support for using ``__`` lookups was added, when targeting related
+        fields.
+
     A few special cases to note about ``list_display``:
 
     * If the field is a ``ForeignKey``, Django will display the
@@ -820,10 +839,6 @@ subclass::
 
           full_name = property(my_property)
 
-      .. versionchanged:: 5.0
-
-        Support for ``boolean`` attribute on properties was added.
-
     * The field names in ``list_display`` will also appear as CSS classes in
       the HTML output, in the form of ``column-<field_name>`` on each ``<th>``
       element. This can be used to set column widths in a CSS file for example.
@@ -831,7 +846,7 @@ subclass::
     * Django will try to interpret every element of ``list_display`` in this
       order:
 
-      * A field of the model.
+      * A field of the model or from a related field.
       * A callable.
       * A string representing a ``ModelAdmin`` attribute.
       * A string representing a model attribute.
@@ -1009,8 +1024,6 @@ subclass::
 
 .. attribute:: ModelAdmin.show_facets
 
-    .. versionadded:: 5.0
-
     Controls whether facet counts are displayed for filters in the admin
     changelist. Defaults to :attr:`.ShowFacets.ALLOW`.
 
@@ -1018,8 +1031,6 @@ subclass::
 
     .. class:: ShowFacets
 
-        .. versionadded:: 5.0
-
         Enum of allowed values for :attr:`.ModelAdmin.show_facets`.
 
         .. attribute:: ALWAYS
@@ -1268,7 +1279,7 @@ subclass::
     ======  ====================
     Prefix  Lookup
     ======  ====================
-    ^       :lookup:`startswith`
+    ^       :lookup:`istartswith`
     =       :lookup:`iexact`
     @       :lookup:`search`
     None    :lookup:`icontains`
@@ -1876,10 +1887,6 @@ templates used by the :class:`ModelAdmin` views:
     Override this method to customize the lookups permitted for your
     :class:`~django.contrib.admin.ModelAdmin` subclass.
 
-    .. versionchanged:: 5.0
-
-        The ``request`` argument was added.
-
 .. method:: ModelAdmin.has_view_permission(request, obj=None)
 
     Should return ``True`` if viewing ``obj`` is permitted, ``False`` otherwise.
@@ -2141,10 +2148,6 @@ forms or widgets depending on ``django.jQuery`` must specify
 ``js=['admin/js/jquery.init.js', …]`` when :ref:`declaring form media assets
 <assets-as-a-static-definition>`.
 
-.. versionchanged:: 5.0
-
-    jQuery was upgraded from 3.6.4 to 3.7.1.
-
 The :class:`ModelAdmin` class requires jQuery by default, so there is no need
 to add jQuery to your ``ModelAdmin``’s list of media resources unless you have
 a specific need. For example, if you require the jQuery library to be in the
@@ -2296,8 +2299,12 @@ The ``InlineModelAdmin`` class adds or customizes:
     A list or tuple containing extra CSS classes to apply to the fieldset that
     is rendered for the inlines. Defaults to ``None``. As with classes
     configured in :attr:`~ModelAdmin.fieldsets`, inlines with a ``collapse``
-    class will be initially collapsed and their header will have a small "show"
-    link.
+    class will be initially collapsed using an expandable widget.
+
+    .. versionchanged:: 5.1
+
+        ``fieldsets`` using the ``collapse`` class now use ``<details>`` and
+        ``<summary>`` elements, provided they define a ``name``.
 
 .. attribute:: InlineModelAdmin.extra
 
@@ -2820,6 +2827,32 @@ linked to the document in ``{% block dark-mode-vars %}``.
 
 .. _prefers-color-scheme: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
 
+.. _extrabody:
+
+``extrabody`` block
+===================
+
+.. versionadded:: 5.2
+
+You can add custom HTML, JavaScript, or other content to appear just before the
+closing ``</body>`` tag of templates that extend ``admin/base.html`` by
+extending the ``extrabody`` block. For example, if you want an alert to appear
+on page load you could add a ``admin/base.html`` template override to your
+project:
+
+.. code-block:: html+django
+
+    {% extends 'admin/base.html' %}
+
+    {% block extrabody %}
+        {{ block.super }}
+        <script>
+            document.addEventListener('DOMContentLoaded', function() {
+                window.alert('Welcome!');
+            });
+        </script>
+    {% endblock extrabody %}
+
 ``AdminSite`` objects
 =====================
 
@@ -2856,10 +2889,6 @@ Templates can override or extend base admin templates as described in
     The text to put at the top of each admin page, as a ``<div>`` (a string).
     By default, this is "Django administration".
 
-    .. versionchanged:: 5.0
-
-        In older versions, ``site_header`` was using an ``<h1>`` tag.
-
 .. attribute:: AdminSite.site_title
 
     The text to put at the end of each admin page's ``<title>`` (a string). By
@@ -3031,15 +3060,11 @@ Templates can override or extend base admin templates as described in
 
 .. method:: AdminSite.get_model_admin(model)
 
-    .. versionadded:: 5.0
-
     Returns an admin class for the given model class. Raises
     ``django.contrib.admin.exceptions.NotRegistered`` if a model isn't registered.
 
 .. method:: AdminSite.get_log_entries(request)
 
-    .. versionadded:: 5.0
-
     Returns a queryset for the related
     :class:`~django.contrib.admin.models.LogEntry` instances, shown on the site
     index page. This method can be overridden to filter the log entries by
diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt
index 036f8d9f76..d5fc724b54 100644
--- a/docs/ref/contrib/auth.txt
+++ b/docs/ref/contrib/auth.txt
@@ -173,10 +173,6 @@ Methods
         the user. (This takes care of the password hashing in making the
         comparison.)
 
-        .. versionchanged:: 5.0
-
-            ``acheck_password()`` method was added.
-
     .. method:: set_unusable_password()
 
         Marks the user as having no password set.  This isn't the same as
@@ -721,6 +717,3 @@ Utility functions
     backend's ``get_user()`` method, or if the session auth hash doesn't
     validate.
 
-    .. versionchanged:: 5.0
-
-        ``aget_user()`` function was added.
diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt
index 71feee63e0..ff0688d4ac 100644
--- a/docs/ref/contrib/contenttypes.txt
+++ b/docs/ref/contrib/contenttypes.txt
@@ -106,13 +106,18 @@ methods that allow you to get from a
 :class:`~django.contrib.contenttypes.models.ContentType` instance to the
 model it represents, or to retrieve objects from that model:
 
-.. method:: ContentType.get_object_for_this_type(**kwargs)
+.. method:: ContentType.get_object_for_this_type(using=None, **kwargs)
 
     Takes a set of valid :ref:`lookup arguments <field-lookups-intro>` for the
     model the :class:`~django.contrib.contenttypes.models.ContentType`
     represents, and does
     :meth:`a get() lookup <django.db.models.query.QuerySet.get>`
-    on that model, returning the corresponding object.
+    on that model, returning the corresponding object. The ``using`` argument
+    can be used to specify a different database than the default one.
+
+    .. versionchanged:: 5.1
+
+        The ``using`` argument was added.
 
 .. method:: ContentType.model_class()
 
@@ -596,9 +601,7 @@ information.
 ``GenericPrefetch()``
 ---------------------
 
-.. versionadded:: 5.0
-
-.. class:: GenericPrefetch(lookup, querysets=None, to_attr=None)
+.. class:: GenericPrefetch(lookup, querysets, to_attr=None)
 
 This lookup is similar to ``Prefetch()`` and it should only be used on
 ``GenericForeignKey``. The ``querysets`` argument accepts a list of querysets,
diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt
index f55d314b3f..ff05d0ec96 100644
--- a/docs/ref/contrib/gis/functions.txt
+++ b/docs/ref/contrib/gis/functions.txt
@@ -257,8 +257,6 @@ value of the geometry.
 ``ClosestPoint``
 ================
 
-.. versionadded:: 5.0
-
 .. class:: ClosestPoint(expr1, expr2, **extra)
 
 *Availability*: `PostGIS <https://postgis.net/docs/ST_ClosestPoint.html>`__,
@@ -359,24 +357,36 @@ are returned unchanged.
 ``FromWKB``
 ===========
 
-.. class:: FromWKB(expression, **extra)
+.. class:: FromWKB(expression, srid=0, **extra)
 
 *Availability*: MariaDB, `MySQL
 <https://dev.mysql.com/doc/refman/en/gis-wkb-functions.html#function_st-geomfromwkb>`__,
 Oracle, `PostGIS <https://postgis.net/docs/ST_GeomFromWKB.html>`__, SpatiaLite
 
-Creates geometry from `Well-known binary (WKB)`_ representation.
+Creates geometry from `Well-known binary (WKB)`_ representation. The optional
+``srid`` argument allows to specify the SRID of the resulting geometry.
+``srid`` is ignored on Oracle.
+
+.. versionchanged:: 5.1
+
+    The ``srid`` argument was added.
 
 ``FromWKT``
 ===========
 
-.. class:: FromWKT(expression, **extra)
+.. class:: FromWKT(expression, srid=0, **extra)
 
 *Availability*: MariaDB, `MySQL
 <https://dev.mysql.com/doc/refman/en/gis-wkt-functions.html#function_st-geomfromtext>`__,
 Oracle, `PostGIS <https://postgis.net/docs/ST_GeomFromText.html>`__, SpatiaLite
 
-Creates geometry from `Well-known text (WKT)`_ representation.
+Creates geometry from `Well-known text (WKT)`_ representation. The optional
+``srid`` argument allows to specify the SRID of the resulting geometry.
+``srid`` is ignored on Oracle.
+
+.. versionchanged:: 5.1
+
+    The ``srid`` argument was added.
 
 ``GeoHash``
 ===========
diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt
index c0dd8d71c8..b639c5271e 100644
--- a/docs/ref/contrib/gis/geoquerysets.txt
+++ b/docs/ref/contrib/gis/geoquerysets.txt
@@ -879,10 +879,6 @@ aggregate, except it can be several orders of magnitude faster than performing
 a union because it rolls up geometries into a collection or multi object, not
 caring about dissolving boundaries.
 
-.. versionchanged:: 5.0
-
-    Support for using the ``filter`` argument was added.
-
 .. versionchanged:: 5.1
 
     MySQL 8.0.24+ support was added.
@@ -906,10 +902,6 @@ Example:
     >>> print(qs["poly__extent"])
     (-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
 
-.. versionchanged:: 5.0
-
-    Support for using the ``filter`` argument was added.
-
 ``Extent3D``
 ~~~~~~~~~~~~
 
@@ -929,10 +921,6 @@ Example:
     >>> print(qs["poly__extent3d"])
     (-96.8016128540039, 29.7633724212646, 0, -95.3631439208984, 32.782058715820, 0)
 
-.. versionchanged:: 5.0
-
-    Support for using the ``filter`` argument was added.
-
 ``MakeLine``
 ~~~~~~~~~~~~
 
@@ -952,10 +940,6 @@ Example:
     >>> print(qs["poly__makeline"])
     LINESTRING (-95.3631510000000020 29.7633739999999989, -96.8016109999999941 32.7820570000000018)
 
-.. versionchanged:: 5.0
-
-    Support for using the ``filter`` argument was added.
-
 ``Union``
 ~~~~~~~~~
 
@@ -983,10 +967,6 @@ Example:
     ...     Union(poly)
     ... )  # A more sensible approach.
 
-.. versionchanged:: 5.0
-
-    Support for using the ``filter`` argument was added.
-
 .. rubric:: Footnotes
 .. [#fnde9im] *See* `OpenGIS Simple Feature Specification For SQL <https://portal.ogc.org/files/?artifact_id=829>`_, at Ch. 2.1.13.2, p. 2-13 (The Dimensionally Extended Nine-Intersection Model).
 .. [#fnsdorelate] *See* `SDO_RELATE documentation <https://docs.oracle.com/en/
diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt
index fed3847424..8b1ae62c2f 100644
--- a/docs/ref/contrib/gis/geos.txt
+++ b/docs/ref/contrib/gis/geos.txt
@@ -19,7 +19,7 @@ maintained by `Refractions Research`__ of Victoria, Canada.
 
 __ https://libgeos.org/
 __ https://sourceforge.net/projects/jts-topo-suite/
-__ https://www.ogc.org/standards/sfs
+__ https://www.ogc.org/standard/sfs/
 __ http://www.refractions.net/
 
 Features
@@ -484,8 +484,6 @@ return a boolean.
 
 .. method:: GEOSGeometry.equals_identical(other)
 
-    .. versionadded:: 5.0
-
     Returns ``True`` if the two geometries are point-wise equivalent by
     checking that the structure, ordering, and values of all vertices are
     identical in all dimensions. ``NaN`` values are considered to be equal to
diff --git a/docs/ref/contrib/gis/install/geolibs.txt b/docs/ref/contrib/gis/install/geolibs.txt
index c12f65f34a..a0a66c0dc6 100644
--- a/docs/ref/contrib/gis/install/geolibs.txt
+++ b/docs/ref/contrib/gis/install/geolibs.txt
@@ -12,7 +12,7 @@ Program                   Description                           Required
 `PROJ`_                   Cartographic Projections library      Yes (PostgreSQL and SQLite only)  9.x, 8.x, 7.x, 6.x
 :doc:`GDAL <../gdal>`     Geospatial Data Abstraction Library   Yes                               3.8, 3.7, 3.6, 3.5, 3.4, 3.3, 3.2, 3.1, 3.0
 :doc:`GeoIP <../geoip2>`  IP-based geolocation library          No                                2
-`PostGIS`__               Spatial extensions for PostgreSQL     Yes (PostgreSQL only)             3.4, 3.3, 3.2, 3.1, 3.0
+`PostGIS`__               Spatial extensions for PostgreSQL     Yes (PostgreSQL only)             3.4, 3.3, 3.2, 3.1
 `SpatiaLite`__            Spatial extensions for SQLite         Yes (SQLite only)                 5.1, 5.0, 4.3
 ========================  ====================================  ================================  ===========================================
 
@@ -35,7 +35,6 @@ totally fine with GeoDjango. Your mileage may vary.
     GDAL 3.6.0 2022-11-03
     GDAL 3.7.0 2023-05-10
     GDAL 3.8.0 2023-11-13
-    PostGIS 3.0.0 2019-10-20
     PostGIS 3.1.0 2020-12-18
     PostGIS 3.2.0 2021-12-18
     PostGIS 3.3.0 2022-08-27
@@ -291,10 +290,10 @@ the GDAL library.  For example:
 .. [#] The datum shifting files are needed for converting data to and from
        certain projections.
        For example, the PROJ string for the `Google projection (900913 or 3857)
-       <https://spatialreference.org/ref/sr-org/6864/prj/>`_ requires the
-       ``null`` grid file only included in the extra datum shifting files.
-       It is easier to install the shifting files now, then to have debug a
-       problem caused by their absence later.
+       <https://spatialreference.org/ref/epsg/3857/>`_ requires the ``null``
+       grid file only included in the extra datum shifting files. It is easier
+       to install the shifting files now, then to have debug a problem caused
+       by their absence later.
 .. [#] Specifically, GeoDjango provides support for the `OGR
        <https://gdal.org/user/vector_data_model.html>`_ library, a component of
        GDAL.
diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt
index e5c2c17bd5..e7bc885d4b 100644
--- a/docs/ref/contrib/gis/install/index.txt
+++ b/docs/ref/contrib/gis/install/index.txt
@@ -56,10 +56,10 @@ supported versions, and any notes for each of the supported database backends:
 ==================  ==============================  ==================  =========================================
 Database            Library Requirements            Supported Versions  Notes
 ==================  ==============================  ==================  =========================================
-PostgreSQL          GEOS, GDAL, PROJ, PostGIS       13+                 Requires PostGIS.
+PostgreSQL          GEOS, GDAL, PROJ, PostGIS       14+                 Requires PostGIS.
 MySQL               GEOS, GDAL                      8.0.11+             :ref:`Limited functionality <mysql-spatial-limitations>`.
 Oracle              GEOS, GDAL                      19+                 XE not supported.
-SQLite              GEOS, GDAL, PROJ, SpatiaLite    3.27.0+             Requires SpatiaLite 4.3+
+SQLite              GEOS, GDAL, PROJ, SpatiaLite    3.31.0+             Requires SpatiaLite 4.3+
 ==================  ==============================  ==================  =========================================
 
 See also `this comparison matrix`__ on the OSGeo Wiki for
@@ -107,9 +107,6 @@ Troubleshooting
 If you can't find the solution to your problem here then participate in the
 community!  You can:
 
-* Join the ``#django-geo`` IRC channel on Libera.Chat. Please be patient and
-  polite -- while you may not get an immediate response, someone will attempt
-  to answer your question as soon as they see it.
 * Ask your question on the `GeoDjango`__ forum.
 * File a ticket on the `Django trac`__ if you think there's a bug.  Make
   sure to provide a complete description of the problem, versions used,
@@ -303,7 +300,7 @@ Summary:
 
 .. code-block:: shell
 
-    $ sudo port install postgresql13-server
+    $ sudo port install postgresql14-server
     $ sudo port install geos
     $ sudo port install proj6
     $ sudo port install postgis3
@@ -317,14 +314,14 @@ Summary:
 
     .. code-block:: shell
 
-        export PATH=/opt/local/bin:/opt/local/lib/postgresql13/bin
+        export PATH=/opt/local/bin:/opt/local/lib/postgresql14/bin
 
     In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that
     the libraries can be found by Python:
 
     .. code-block:: shell
 
-        export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql13
+        export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql14
 
 __ https://www.macports.org/
 
diff --git a/docs/ref/contrib/gis/install/spatialite.txt b/docs/ref/contrib/gis/install/spatialite.txt
index 6606b8cff0..dd468fda26 100644
--- a/docs/ref/contrib/gis/install/spatialite.txt
+++ b/docs/ref/contrib/gis/install/spatialite.txt
@@ -111,9 +111,17 @@ including SQLite, SpatiaLite, PROJ, and GEOS. Install them like this:
     $ brew install spatialite-tools
     $ brew install gdal
 
-Finally, for GeoDjango to be able to find the SpatiaLite library, add the
-following to your ``settings.py``::
+Finally, for GeoDjango to be able to find the SpatiaLite library, set
+the ``SPATIALITE_LIBRARY_PATH`` setting to its path. This will be within
+your brew install path, which you can check with:
 
-    SPATIALITE_LIBRARY_PATH = "/usr/local/lib/mod_spatialite.dylib"
+.. code-block:: console
+
+    $ brew --prefix
+    /opt/homebrew
+
+Using this brew install path, the full path can be constructed like this::
+
+    SPATIALITE_LIBRARY_PATH = "/opt/homebrew/lib/mod_spatialite.dylib"
 
 .. _Homebrew: https://brew.sh/
diff --git a/docs/ref/contrib/gis/model-api.txt b/docs/ref/contrib/gis/model-api.txt
index e46d862707..981581cbf2 100644
--- a/docs/ref/contrib/gis/model-api.txt
+++ b/docs/ref/contrib/gis/model-api.txt
@@ -265,7 +265,7 @@ determining `when to use geography data type over geometry data type
 <https://postgis.net/docs/using_postgis_dbmanagement.html#PostGIS_GeographyVSGeometry>`_.
 
 .. rubric:: Footnotes
-.. [#fnogc] OpenGIS Consortium, Inc., `Simple Feature Specification For SQL <https://www.ogc.org/standards/sfs>`_.
+.. [#fnogc] OpenGIS Consortium, Inc., `Simple Feature Specification For SQL <https://www.ogc.org/standard/sfs/>`_.
 .. [#fnogcsrid] *See id.* at Ch. 2.3.8, p. 39 (Geometry Values and Spatial Reference Systems).
 .. [#fnsrid] Typically, SRID integer corresponds to an EPSG (`European Petroleum Survey Group <https://epsg.org/>`_) identifier.  However, it may also be associated with custom projections defined in spatial database's spatial reference systems table.
 .. [#fnthematic] Terry A. Slocum, Robert B. McMaster, Fritz C. Kessler, & Hugh H. Howard, *Thematic Cartography and Geographic Visualization* (Prentice Hall, 2nd edition), at Ch. 7.1.3.
diff --git a/docs/ref/contrib/gis/sitemaps.txt b/docs/ref/contrib/gis/sitemaps.txt
index c81760a2af..32680d6f39 100644
--- a/docs/ref/contrib/gis/sitemaps.txt
+++ b/docs/ref/contrib/gis/sitemaps.txt
@@ -19,4 +19,4 @@ Reference
 --------------
 
 .. rubric:: Footnotes
-.. [#] https://www.ogc.org/standards/kml
+.. [#] https://www.ogc.org/standard/kml/
diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt
index 53c961561c..b051a57a0f 100644
--- a/docs/ref/contrib/gis/tutorial.txt
+++ b/docs/ref/contrib/gis/tutorial.txt
@@ -37,7 +37,7 @@ basic apps`_ project. [#]_
     instructions.
 
 .. _OGC: https://www.ogc.org/
-.. _world borders: https://thematicmapping.org/downloads/world_borders.php
+.. _world borders: https://web.archive.org/web/20240123190237/https://thematicmapping.org/downloads/world_borders.php
 .. _GeoDjango basic apps: https://code.google.com/archive/p/geodjango-basic-apps
 
 Setting Up
@@ -115,7 +115,7 @@ unzip. On GNU/Linux platforms, use the following commands:
 
     $ mkdir world/data
     $ cd world/data
-    $ wget https://thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip
+    $ wget https://web.archive.org/web/20231220150759/https://thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip
     $ unzip TM_WORLD_BORDERS-0.3.zip
     $ cd ../..
 
@@ -131,7 +131,7 @@ extensions:
 * ``.prj``: Contains the spatial reference information for the geographic
   data stored in the shapefile.
 
-__ https://thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip
+__ https://web.archive.org/web/20231220150759/https://thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip
 __ https://en.wikipedia.org/wiki/Shapefile
 
 Use ``ogrinfo`` to examine spatial data
@@ -807,9 +807,8 @@ in your ``admin.py`` file::
 
 .. rubric:: Footnotes
 
-.. [#] Special thanks to Bjørn Sandvik of `thematicmapping.org
-       <https://thematicmapping.org/>`_ for providing and maintaining this
-       dataset.
+.. [#] Special thanks to Bjørn Sandvik of `mastermaps.net
+       <https://mastermaps.net/>`_ for providing and maintaining this dataset.
 .. [#] GeoDjango basic apps was written by Dane Springmeyer, Josh Livni, and
        Christopher Schmidt.
 .. [#] This point is the `University of Houston Law Center
diff --git a/docs/ref/contrib/humanize.txt b/docs/ref/contrib/humanize.txt
index 7c1af53ed3..1596f30b97 100644
--- a/docs/ref/contrib/humanize.txt
+++ b/docs/ref/contrib/humanize.txt
@@ -143,3 +143,4 @@ Examples:
 * ``3`` becomes ``3rd``.
 
 You can pass in either an integer or a string representation of an integer.
+Negative integers are returned unchanged.
diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt
index b85f425277..7fdf40f47b 100644
--- a/docs/ref/contrib/messages.txt
+++ b/docs/ref/contrib/messages.txt
@@ -456,8 +456,6 @@ the session cookie settings:
 Testing
 =======
 
-.. versionadded:: 5.0
-
 This module offers a tailored test assertion method, for testing messages
 attached to an :class:`~.HttpResponse`.
 
diff --git a/docs/ref/contrib/postgres/aggregates.txt b/docs/ref/contrib/postgres/aggregates.txt
index 2675a90af2..fd4fabe853 100644
--- a/docs/ref/contrib/postgres/aggregates.txt
+++ b/docs/ref/contrib/postgres/aggregates.txt
@@ -55,12 +55,6 @@ General-purpose aggregation functions
 
             F("some_field").desc()
 
-    .. versionchanged:: 5.0
-
-        In older versions, if there are no rows and ``default`` is not
-        provided, ``ArrayAgg`` returned an empty list instead of ``None``. If
-        you need it, explicitly set ``default`` to ``Value([])``.
-
 ``BitAnd``
 ----------
 
@@ -182,12 +176,6 @@ General-purpose aggregation functions
             {'parking': True, 'double_bed': True}
         ]}]>
 
-    .. versionchanged:: 5.0
-
-        In older versions, if there are no rows and ``default`` is not
-        provided, ``JSONBAgg`` returned an empty list instead of ``None``. If
-        you need it, explicitly set ``default`` to ``Value([])``.
-
 ``StringAgg``
 -------------
 
@@ -243,12 +231,6 @@ General-purpose aggregation functions
             'headline': 'NASA uses Python', 'publication_names': 'Science News, The Python Journal'
         }]>
 
-    .. versionchanged:: 5.0
-
-        In older versions, if there are no rows and ``default`` is not
-        provided, ``StringAgg`` returned an empty string instead of ``None``.
-        If you need it, explicitly set ``default`` to ``Value("")``.
-
 Aggregate functions for statistics
 ==================================
 
diff --git a/docs/ref/contrib/postgres/constraints.txt b/docs/ref/contrib/postgres/constraints.txt
index ce9f0cf78f..4d13eddd24 100644
--- a/docs/ref/contrib/postgres/constraints.txt
+++ b/docs/ref/contrib/postgres/constraints.txt
@@ -136,8 +136,6 @@ used for queries that select only included fields
 ``violation_error_code``
 ------------------------
 
-.. versionadded:: 5.0
-
 .. attribute:: ExclusionConstraint.violation_error_code
 
 The error code used when ``ValidationError`` is raised during
diff --git a/docs/ref/contrib/postgres/functions.txt b/docs/ref/contrib/postgres/functions.txt
index f5d9cdd873..4602f7fd9d 100644
--- a/docs/ref/contrib/postgres/functions.txt
+++ b/docs/ref/contrib/postgres/functions.txt
@@ -14,12 +14,6 @@ All of these functions are available from the
 
 Returns a version 4 UUID.
 
-On PostgreSQL < 13, the `pgcrypto extension`_ must be installed. You can use
-the :class:`~django.contrib.postgres.operations.CryptoExtension` migration
-operation to install it.
-
-.. _pgcrypto extension: https://www.postgresql.org/docs/current/pgcrypto.html
-
 Usage example:
 
 .. code-block:: pycon
diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt
index 4f32c06bd9..5628edd183 100644
--- a/docs/ref/contrib/sitemaps.txt
+++ b/docs/ref/contrib/sitemaps.txt
@@ -249,11 +249,6 @@ Note:
         sitemap was requested is used. If the sitemap is built outside the
         context of a request, the default is ``'https'``.
 
-        .. versionchanged:: 5.0
-
-            In older versions, the default protocol for sitemaps built outside
-            the context of a request was ``'http'``.
-
     .. attribute:: Sitemap.limit
 
         **Optional.**
diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt
index b84660db5f..f4b81edf77 100644
--- a/docs/ref/contrib/staticfiles.txt
+++ b/docs/ref/contrib/staticfiles.txt
@@ -412,7 +412,7 @@ hashing algorithm.
 .. _`@import`: https://www.w3.org/TR/CSS2/cascade.html#at-import
 .. _`url()`: https://www.w3.org/TR/CSS2/syndata.html#uri
 .. _`Cascading Style Sheets`: https://www.w3.org/Style/CSS/
-.. _`source map`: https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map
+.. _`source map`: https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/
 .. _`modules import`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script
 .. _`modules aggregation`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aggregating_modules
 
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
index d9672c5b00..d0a3cc41f7 100644
--- a/docs/ref/contrib/syndication.txt
+++ b/docs/ref/contrib/syndication.txt
@@ -596,6 +596,24 @@ This example illustrates all possible attributes and methods for a
 
         ttl = 600  # Hard-coded Time To Live.
 
+        # STYLESHEETS -- Optional. To set, provide one of the following three.
+        # The framework looks for them in this order.
+
+        def stylesheets(self, obj):
+            """
+            Takes the object returned by get_object() and returns the feed's
+            stylesheets (as URL strings or as Stylesheet instances).
+            """
+
+        def stylesheets(self):
+            """
+            Returns the feed's stylesheets (as URL strings or Stylesheet
+            instances).
+            """
+
+        # Hardcoded stylesheets.
+        stylesheets = ["/stylesheet1.xsl", "stylesheet2.xsl"]
+
         # ITEMS -- One of the following three is required. The framework looks
         # for them in this order.
 
@@ -961,16 +979,26 @@ They share this interface:
     * ``feed_copyright``
     * ``feed_guid``
     * ``ttl``
+    * ``stylesheets``
 
     Any extra keyword arguments you pass to ``__init__`` will be stored in
     ``self.feed`` for use with `custom feed generators`_.
 
-    All parameters should be strings, except ``categories``, which should be a
-    sequence of strings. Beware that some control characters
-    are `not allowed <https://www.w3.org/International/questions/qa-controls>`_
-    in XML documents. If your content has some of them, you might encounter a
+    All parameters should be strings, except for two:
+
+    * ``categories`` should be a sequence of strings.
+    * ``stylesheets`` should be a sequence of either strings or
+      :class:`~django.utils.feedgenerator.Stylesheet` instances.
+
+    Beware that some control characters are
+    `not allowed <https://www.w3.org/International/questions/qa-controls>`_ in
+    XML documents. If your content has some of them, you might encounter a
     :exc:`ValueError` when producing the feed.
 
+    .. versionchanged:: 5.2
+
+        The ``stylesheets`` argument was added.
+
 :meth:`.SyndicationFeed.add_item`
     Add an item to the feed with the given parameters.
 
@@ -1095,3 +1123,90 @@ For example, you might start implementing an iTunes RSS feed generator like so::
 
 There's a lot more work to be done for a complete custom feed class, but the
 above example should demonstrate the basic idea.
+
+.. _feed-stylesheets:
+
+Feed stylesheets
+----------------
+
+.. versionadded:: 5.2
+
+If you wish to have your RSS feed render nicely in a browser, you will need to
+provide styling information for the XML file, typically in XSLT_ or CSS
+formats.
+
+You can add this to your RSS feed by setting the ``stylesheets`` attribute on
+the feed class.
+
+This can be a hardcoded URL::
+
+    from django.contrib.syndication.views import Feed
+
+
+    class FeedWithHardcodedStylesheet(Feed):
+        stylesheets = [
+            "https://example.com/rss_stylesheet.xslt",
+        ]
+
+You can also use Django's static files system::
+
+    from django.contrib.syndication.views import Feed
+    from django.templatetags.static import static
+
+
+    class FeedWithStaticFileStylesheet(Feed):
+        stylesheets = [
+            static("rss_styles.xslt"),
+        ]
+
+Another option is to have a view in your project that renders the XSLT
+document. You can then link it like so::
+
+    from django.contrib.syndication.views import Feed
+    from django.urls import reverse_lazy
+
+
+    class FeedWithStylesheetView(Feed):
+        stylesheets = [
+            reverse_lazy("your-custom-view-name"),
+        ]
+
+Django will normally try to guess the MIME type of the given URL based on its
+extension, but if that fails you can specify it using the
+:class:`~django.utils.feedgenerator.Stylesheet` class::
+
+    from django.contrib.syndication.views import Feed
+    from django.utils.feedgenerator import Stylesheet
+
+
+    class FeedWithHardcodedStylesheet(Feed):
+        stylesheets = [
+            Stylesheet("https://example.com/rss_stylesheet", mimetype="text/xsl"),
+        ]
+
+Similarly, if you'd like to use a different ``media`` attribute than ``screen``
+(Django's default), you can use the
+:class:`~django.utils.feedgenerator.Stylesheet` class again::
+
+    from django.contrib.syndication.views import Feed
+    from django.utils.feedgenerator import Stylesheet
+
+
+    class FeedWithHardcodedStylesheet(Feed):
+        stylesheets = [
+            Stylesheet("https://example.com/rss_stylesheet.xslt", media="print"),
+        ]
+
+Any of these options can be combined when using multiple stylesheets::
+
+    from django.contrib.syndication.views import Feed
+    from django.utils.feedgenerator import Stylesheet
+
+
+    class MultiStylesheetFeed(Feed):
+        stylesheets = [
+            "/stylesheet1.xsl",
+            Stylesheet("/stylesheet2.xsl"),
+        ]
+
+.. _xslt: https://developer.mozilla.org/en-US/docs/Web/XSLT/Transforming_XML_with_XSLT
diff --git a/docs/ref/csrf.txt b/docs/ref/csrf.txt
index 12a14091d0..e963c1e627 100644
--- a/docs/ref/csrf.txt
+++ b/docs/ref/csrf.txt
@@ -43,7 +43,7 @@ The CSRF protection is based on the following things:
    a mask. The mask is generated randomly on every call to ``get_token()``, so
    the form field value is different each time.
 
-   This part is done by the template tag.
+   This part is done by the :ttag:`csrf_token` template tag.
 
 #. For all incoming requests that are not using HTTP GET, HEAD, OPTIONS or
    TRACE, a CSRF cookie must be present, and the 'csrfmiddlewaretoken' field
@@ -150,13 +150,10 @@ class-based views<decorating-class-based-views>`.
         def my_view(request):
             return HttpResponse("Hello world")
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: csrf_protect(view)
 
-    Decorator that provides the protection of ``CsrfViewMiddleware`` to a view.
+    Decorator that provides the protection of
+    :class:`~django.middleware.csrf.CsrfViewMiddleware` to a view.
 
     Usage::
 
@@ -170,10 +167,6 @@ class-based views<decorating-class-based-views>`.
             # ...
             return render(request, "a_template.html", c)
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: requires_csrf_token(view)
 
     Normally the :ttag:`csrf_token` template tag will not work if
@@ -194,18 +187,10 @@ class-based views<decorating-class-based-views>`.
             # ...
             return render(request, "a_template.html", c)
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: ensure_csrf_cookie(view)
 
     This decorator forces a view to send the CSRF cookie.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 Settings
 ========
 
diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt
index d853647730..3e50d2e46a 100644
--- a/docs/ref/databases.txt
+++ b/docs/ref/databases.txt
@@ -115,7 +115,7 @@ below for information on how to set up your database correctly.
 PostgreSQL notes
 ================
 
-Django supports PostgreSQL 13 and higher. `psycopg`_ 3.1.8+ or `psycopg2`_
+Django supports PostgreSQL 14 and higher. `psycopg`_ 3.1.8+ or `psycopg2`_
 2.8.4+ is required, though the latest `psycopg`_ 3.1.8+ is recommended.
 
 .. note::
@@ -245,6 +245,31 @@ database configuration in :setting:`DATABASES`::
         },
     }
 
+.. _postgresql-pool:
+
+Connection pool
+---------------
+
+.. versionadded:: 5.1
+
+To use a connection pool with `psycopg`_, you can either set ``"pool"`` in the
+:setting:`OPTIONS` part of your database configuration in :setting:`DATABASES`
+to be a dict to be passed to :class:`~psycopg:psycopg_pool.ConnectionPool`, or
+to ``True`` to use the ``ConnectionPool`` defaults::
+
+    DATABASES = {
+        "default": {
+            "ENGINE": "django.db.backends.postgresql",
+            # ...
+            "OPTIONS": {
+                "pool": True,
+            },
+        },
+    }
+
+This option requires ``psycopg[pool]`` or :pypi:`psycopg-pool` to be installed
+and is ignored with ``psycopg2``.
+
 .. _database-server-side-parameters-binding:
 
 Server-side parameters binding
@@ -787,7 +812,7 @@ appropriate typecasting.
 SQLite notes
 ============
 
-Django supports SQLite 3.27.0 and later.
+Django supports SQLite 3.31.0 and later.
 
 SQLite_ provides an excellent development alternative for applications that
 are predominantly read-only or require a smaller installation footprint. As
@@ -941,6 +966,30 @@ To enable the JSON1 extension you can follow the instruction on
 .. _JSON1 extension: https://www.sqlite.org/json1.html
 .. _the wiki page: https://code.djangoproject.com/wiki/JSON1Extension
 
+.. _sqlite-init-command:
+
+Setting pragma options
+----------------------
+
+.. versionadded:: 5.1
+
+`Pragma options`_ can be set upon connection by using the ``init_command`` in
+the :setting:`OPTIONS` part of your database configuration in
+:setting:`DATABASES`. The example below shows how to enable extra durability of
+synchronous writes and change the ``cache_size``::
+
+    DATABASES = {
+        "default": {
+            "ENGINE": "django.db.backends.sqlite3",
+            # ...
+            "OPTIONS": {
+                "init_command": "PRAGMA synchronous=3; PRAGMA cache_size=2000;",
+            },
+        }
+    }
+
+.. _Pragma options: https://www.sqlite.org/pragma.html
+
 .. _oracle-notes:
 
 Oracle notes
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 3fba67bf20..0546555a30 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -584,7 +584,7 @@ Updates the message files for all available languages.
 .. django-admin-option:: --extension EXTENSIONS, -e EXTENSIONS
 
 Specifies a list of file extensions to examine (default: ``html``, ``txt``,
-``py`` or ``js`` if :option:`--domain` is ``js``).
+``py`` or ``js`` if :option:`--domain` is ``djangojs``).
 
 Example usage:
 
@@ -1554,8 +1554,6 @@ Outputs timings, including database setup and total run time.
 
 .. django-admin-option:: --durations N
 
-.. versionadded:: 5.0
-
 Shows the N slowest test cases (N=0 for all).
 
 .. admonition:: Python 3.12 and later
diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt
index ea9bf0968e..d0b0cdd786 100644
--- a/docs/ref/files/file.txt
+++ b/docs/ref/files/file.txt
@@ -59,10 +59,6 @@ The ``File`` class
 
         It can be used as a context manager, e.g. ``with file.open() as f:``.
 
-        .. versionchanged:: 5.0
-
-            Support for passing ``*args`` and ``**kwargs`` was added.
-
     .. method:: __iter__()
 
         Iterate over the file yielding one line at a time.
diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt
index 1b38fb696e..e912bcc412 100644
--- a/docs/ref/files/storage.txt
+++ b/docs/ref/files/storage.txt
@@ -28,13 +28,18 @@ Django provides convenient ways to access the default storage class:
 The ``FileSystemStorage`` class
 ===============================
 
-.. class:: FileSystemStorage(location=None, base_url=None, file_permissions_mode=None, directory_permissions_mode=None)
+.. class:: FileSystemStorage(location=None, base_url=None, file_permissions_mode=None, directory_permissions_mode=None, allow_overwrite=False)
 
     The :class:`~django.core.files.storage.FileSystemStorage` class implements
     basic file storage on a local filesystem. It inherits from
     :class:`~django.core.files.storage.Storage` and provides implementations
     for all the public methods thereof.
 
+    .. note::
+
+        The ``FileSystemStorage.delete()`` method will not raise an exception
+        if the given file name does not exist.
+
     .. attribute:: location
 
         Absolute path to the directory that will hold the files.
@@ -55,10 +60,12 @@ The ``FileSystemStorage`` class
         The file system permissions that the directory will receive when it is
         saved. Defaults to :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS`.
 
-    .. note::
+    .. attribute:: allow_overwrite
 
-        The ``FileSystemStorage.delete()`` method will not raise
-        an exception if the given file name does not exist.
+        .. versionadded:: 5.1
+
+        Flag to control allowing saving a new file over an existing one.
+        Defaults to ``False``.
 
     .. method:: get_created_time(name)
 
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 28cd452c4e..33d0806859 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -1192,10 +1192,6 @@ Attributes of ``BoundField``
     When rendering a field with errors, ``aria-invalid="true"`` will be set on
     the field's widget to indicate there is an error to screen reader users.
 
-    .. versionchanged:: 5.0
-
-        The ``aria-invalid="true"`` was added when a field has errors.
-
 .. attribute:: BoundField.field
 
     The form :class:`~django.forms.Field` instance from the form class that
@@ -1289,8 +1285,6 @@ Attributes of ``BoundField``
 
 .. attribute:: BoundField.template_name
 
-    .. versionadded:: 5.0
-
     The name of the template rendered with :meth:`.BoundField.as_field_group`.
 
     A property returning the value of the
@@ -1323,8 +1317,6 @@ Methods of ``BoundField``
 
 .. method:: BoundField.as_field_group()
 
-    .. versionadded:: 5.0
-
     Renders the field using :meth:`.BoundField.render` with default values
     which renders the ``BoundField``, including its label, help text and errors
     using the template's :attr:`~django.forms.Field.template_name` if set
@@ -1372,8 +1364,6 @@ Methods of ``BoundField``
 
 .. method:: BoundField.get_context()
 
-    .. versionadded:: 5.0
-
     Return the template context for rendering the field. The available context
     is ``field`` being the instance of the bound field.
 
@@ -1426,8 +1416,6 @@ Methods of ``BoundField``
 
 .. method:: BoundField.render(template_name=None, context=None, renderer=None)
 
-    .. versionadded:: 5.0
-
     The render method is called by ``as_field_group``. All arguments are 
     optional and default to:
 
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index 8b07c6e2b2..2ae4fe2be6 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -7,7 +7,7 @@ Form fields
 
 .. currentmodule:: django.forms
 
-.. class:: Field(**kwargs)
+.. class:: Field
 
 When you create a ``Form`` class, the most important part is defining the
 fields of the form. Each field has custom validation logic, along with a few
@@ -322,10 +322,6 @@ inside ``aria-describedby``:
     >>> print(f["username"])
     <input type="text" name="username" aria-describedby="custom-description id_username_helptext" maxlength="255" id="id_username" required>
 
-.. versionchanged:: 5.0
-
-    ``aria-describedby`` was added to associate ``help_text`` with its input.
-
 .. versionchanged:: 5.1
 
     ``aria-describedby`` support was added for ``<fieldset>``.
@@ -397,8 +393,6 @@ be ignored in favor of the value from the form's initial data.
 
 .. attribute:: Field.template_name
 
-.. versionadded:: 5.0
-
 The ``template_name`` argument allows a custom template to be used when the
 field is rendered with :meth:`~django.forms.BoundField.as_field_group`. By 
 default this value is set to ``"django/forms/field.html"``. Can be changed per
@@ -513,12 +507,6 @@ For each field, we describe the default widget used if you don't specify
         other data types, such as integers or booleans, consider using
         :class:`TypedChoiceField` instead.
 
-    .. versionchanged:: 5.0
-
-        Support for mappings and using
-        :ref:`enumeration types <field-choices-enum-types>` directly in
-        ``choices`` was added.
-
 ``DateField``
 -------------
 
@@ -1145,8 +1133,6 @@ For each field, we describe the default widget used if you don't specify
 
     .. attribute:: assume_scheme
 
-        .. versionadded:: 5.0
-
         The scheme assumed for URLs provided without one. Defaults to
         ``"http"``. For example, if ``assume_scheme`` is ``"https"`` and the
         provided value is ``"example.com"``, the normalized value will be
diff --git a/docs/ref/forms/renderers.txt b/docs/ref/forms/renderers.txt
index 02b3cac7fb..e527a70c57 100644
--- a/docs/ref/forms/renderers.txt
+++ b/docs/ref/forms/renderers.txt
@@ -61,8 +61,6 @@ should return a rendered templates (as a string) or raise
 
     .. attribute:: field_template_name
 
-        .. versionadded:: 5.0
-
         The default name of the template used to render a ``BoundField``.
 
         Defaults to ``"django/forms/field.html"``
@@ -173,8 +171,6 @@ forms receive a dictionary with the following values:
 Context available in field templates
 ====================================
 
-.. versionadded:: 5.0
-
 Field templates receive a context from :meth:`.BoundField.get_context`. By
 default, fields receive a dictionary with the following values:
 
@@ -224,8 +220,6 @@ renderer. Then overriding form templates works :doc:`the same as
 Overriding built-in field templates
 ===================================
 
-.. versionadded:: 5.0
-
 :attr:`.Field.template_name`
 
 To override field templates, you must use the :class:`TemplatesSetting`
diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt
index a2b3fb4885..7a037eaf75 100644
--- a/docs/ref/forms/validation.txt
+++ b/docs/ref/forms/validation.txt
@@ -370,7 +370,7 @@ example::
                 # Only do something if both fields are valid so far.
                 if "help" not in subject:
                     raise ValidationError(
-                        "Did not send for 'help' in the subject despite " "CC'ing yourself."
+                        "Did not send for 'help' in the subject despite CC'ing yourself."
                     )
 
 In this code, if the validation error is raised, the form will display an
diff --git a/docs/ref/logging.txt b/docs/ref/logging.txt
index 6d8861299f..8a7e58997e 100644
--- a/docs/ref/logging.txt
+++ b/docs/ref/logging.txt
@@ -286,6 +286,17 @@ Messages to this logger have ``params`` and ``sql`` in their extra context (but
 unlike ``django.db.backends``, not duration). The values have the same meaning
 as explained in :ref:`django-db-logger`.
 
+.. _django-contrib-sessions-logger:
+
+``django.contrib.sessions``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Log messages related to the :doc:`session framework</topics/http/sessions>`.
+
+* Non-fatal errors occurring when using the
+  :class:`django.contrib.sessions.backends.cached_db.SessionStore` engine are
+  logged as ``ERROR`` messages with the corresponding traceback.
+
 Handlers
 --------
 
diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt
index 63b38da0a0..c762457839 100644
--- a/docs/ref/middleware.txt
+++ b/docs/ref/middleware.txt
@@ -153,6 +153,9 @@ header, the middleware adds one if needed. If the response has an ``ETag`` or
 ``If-Modified-Since``, the response is replaced by an
 :class:`~django.http.HttpResponseNotModified`.
 
+You can handle conditional GET operations with individual views using the
+:func:`~django.views.decorators.http.conditional_page()` decorator.
+
 Locale middleware
 -----------------
 
@@ -495,6 +498,58 @@ Adds the ``user`` attribute, representing the currently-logged-in user, to
 every incoming ``HttpRequest`` object. See :ref:`Authentication in web requests
 <auth-web-requests>`.
 
+.. class:: LoginRequiredMiddleware
+
+.. versionadded:: 5.1
+
+Redirects all unauthenticated requests to a login page. For admin views, this
+redirects to the admin login. For all other views, this will redirect to
+:setting:`settings.LOGIN_URL <LOGIN_URL>`. This can be customized by using the
+:func:`~.django.contrib.auth.decorators.login_required` decorator and setting
+``login_url`` or ``redirect_field_name`` for the view. For example::
+
+    @method_decorator(
+        login_required(login_url="/login/", redirect_field_name="redirect_to"),
+        name="dispatch",
+    )
+    class MyView(View):
+        pass
+
+
+    @login_required(login_url="/login/", redirect_field_name="redirect_to")
+    def my_view(request): ...
+
+Views using the :func:`~django.contrib.auth.decorators.login_not_required`
+decorator are exempt from this requirement.
+
+.. admonition:: Ensure that your login view does not require a login.
+
+    To prevent infinite redirects, ensure you have
+    :ref:`enabled unauthenticated requests
+    <disable-login-required-middleware-for-views>` to your login view.
+
+**Methods and Attributes**
+
+.. attribute:: redirect_field_name
+
+    Defaults to ``"next"``.
+
+.. method:: get_login_url()
+
+    Returns the URL that unauthenticated requests will be redirected to. If
+    defined, this returns the ``login_url`` set on the
+    :func:`~.django.contrib.auth.decorators.login_required` decorator. Defaults
+    to :setting:`settings.LOGIN_URL <LOGIN_URL>`.
+
+.. method:: get_redirect_field_name()
+
+    Returns the name of the query parameter that contains the URL the user
+    should be redirected to after a successful login. If defined, this returns
+    the ``redirect_field_name`` set on the
+    :func:`~.django.contrib.auth.decorators.login_required` decorator. Defaults
+    to :attr:`redirect_field_name`. If ``None`` is returned, a query parameter
+    won't be added.
+
 .. class:: RemoteUserMiddleware
 
 Middleware for utilizing web server provided authentication. See
@@ -517,6 +572,9 @@ Adds protection against Cross Site Request Forgeries by adding hidden form
 fields to POST forms and checking requests for the correct value. See the
 :doc:`Cross Site Request Forgery protection documentation </ref/csrf>`.
 
+You can add Cross Site Request Forgery protection to individual views using the
+:func:`~django.views.decorators.csrf.csrf_protect()` decorator.
+
 ``X-Frame-Options`` middleware
 ------------------------------
 
@@ -597,6 +655,12 @@ Here are some hints about the ordering of various Django middleware classes:
 
    After ``SessionMiddleware``: uses session storage.
 
+#. :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware`
+
+   .. versionadded:: 5.1
+
+   After ``AuthenticationMiddleware``: uses user object.
+
 #. :class:`~django.contrib.messages.middleware.MessageMiddleware`
 
    After ``SessionMiddleware``: can use session-based storage.
diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt
index cd852f537d..9e90b78623 100644
--- a/docs/ref/migration-operations.txt
+++ b/docs/ref/migration-operations.txt
@@ -195,6 +195,16 @@ if the field is nullable or if it has a default value that can be used to
 populate the recreated column. If the field is not nullable and does not have a
 default value, the operation is irreversible.
 
+.. admonition:: PostgreSQL
+
+    ``RemoveField`` will also delete any additional database objects that are
+    related to the removed field (like views, for example). This is because the
+    resulting ``DROP COLUMN`` statement will include the ``CASCADE`` clause to
+    ensure `dependent objects outside the table are also dropped`_.
+
+.. _dependent objects outside the table are also dropped: https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-PARMS-CASCADE
+
+
 ``AlterField``
 --------------
 
diff --git a/docs/ref/models/constraints.txt b/docs/ref/models/constraints.txt
index cc308cedf2..7dfc3b7d28 100644
--- a/docs/ref/models/constraints.txt
+++ b/docs/ref/models/constraints.txt
@@ -26,7 +26,7 @@ option.
     (including ``name``) each time. To work around name collisions, part of the
     name may contain ``'%(app_label)s'`` and ``'%(class)s'``, which are
     replaced, respectively, by the lowercased app label and class name of the
-    concrete model. For example ``CheckConstraint(check=Q(age__gte=18),
+    concrete model. For example ``CheckConstraint(condition=Q(age__gte=18),
     name='%(app_label)s_%(class)s_is_adult')``.
 
 .. admonition:: Validation of Constraints
@@ -34,17 +34,6 @@ option.
     Constraints are checked during the :ref:`model validation
     <validating-objects>`.
 
-.. admonition:: Validation of Constraints with ``JSONField``
-
-    Constraints containing :class:`~django.db.models.JSONField` may not raise
-    validation errors as key, index, and path transforms have many
-    database-specific caveats. This :ticket:`may be fully supported later
-    <34059>`.
-
-    You should always check that there are no log messages, in the
-    ``django.db.models`` logger, like *"Got a database error calling check() on
-    …"* to confirm it's validated properly.
-
 ``BaseConstraint``
 ==================
 
@@ -71,8 +60,6 @@ constraint.
 ``violation_error_code``
 ------------------------
 
-.. versionadded:: 5.0
-
 .. attribute:: BaseConstraint.violation_error_code
 
 The error code used when ``ValidationError`` is raised during
@@ -104,29 +91,45 @@ This method must be implemented by a subclass.
 ``CheckConstraint``
 ===================
 
-.. class:: CheckConstraint(*, check, name, violation_error_code=None, violation_error_message=None)
+.. class:: CheckConstraint(*, condition, name, violation_error_code=None, violation_error_message=None)
 
     Creates a check constraint in the database.
 
-``check``
----------
+``condition``
+-------------
 
-.. attribute:: CheckConstraint.check
+.. attribute:: CheckConstraint.condition
 
 A :class:`Q` object or boolean :class:`~django.db.models.Expression` that
-specifies the check you want the constraint to enforce.
+specifies the conditional check you want the constraint to enforce.
 
-For example, ``CheckConstraint(check=Q(age__gte=18), name='age_gte_18')``
+For example, ``CheckConstraint(condition=Q(age__gte=18), name='age_gte_18')``
 ensures the age field is never less than 18.
 
-.. admonition:: Oracle
+.. admonition:: Expression order
 
-    Checks with nullable fields on Oracle must include a condition allowing for
-    ``NULL`` values in order for :meth:`validate() <BaseConstraint.validate>`
+    ``Q`` argument order is not necessarily preserved, however the order of
+    ``Q`` expressions themselves are preserved. This may be important for
+    databases that preserve check constraint expression order for performance
+    reasons. For example, use the following format if order matters::
+
+        CheckConstraint(
+            condition=Q(age__gte=18) & Q(expensive_check=condition),
+            name="age_gte_18_and_others",
+        )
+
+.. admonition:: Oracle < 23c
+
+    Checks with nullable fields on Oracle < 23c must include a condition
+    allowing for ``NULL`` values in order for :meth:`~BaseConstraint.validate`
     to behave the same as check constraints validation. For example, if ``age``
     is a nullable field::
 
-        CheckConstraint(check=Q(age__gte=18) | Q(age__isnull=True), name="age_gte_18")
+        CheckConstraint(condition=Q(age__gte=18) | Q(age__isnull=True), name="age_gte_18")
+
+.. deprecated:: 5.1
+
+    The ``check`` attribute is deprecated in favor of ``condition``.
 
 ``UniqueConstraint``
 ====================
@@ -258,8 +261,6 @@ creates a unique index on ``username`` using ``varchar_pattern_ops``.
 ``nulls_distinct``
 ------------------
 
-.. versionadded:: 5.0
-
 .. attribute:: UniqueConstraint.nulls_distinct
 
 Whether rows containing ``NULL`` values covered by the unique constraint should
@@ -279,8 +280,6 @@ PostgreSQL 15+.
 ``violation_error_code``
 ------------------------
 
-.. versionadded:: 5.0
-
 .. attribute:: UniqueConstraint.violation_error_code
 
 The error code used when ``ValidationError`` is raised during
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index ca4016e2b6..eb08e160f7 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -569,11 +569,6 @@ Usage example:
     On Oracle, the SQL ``LOCALTIMESTAMP`` is used to avoid issues with casting
     ``CURRENT_TIMESTAMP`` to ``DateTimeField``.
 
-.. versionchanged:: 5.0
-
-    In older versions, the SQL ``CURRENT_TIMESTAMP`` was used on Oracle instead
-    of ``LOCALTIMESTAMP``.
-
 ``Trunc``
 ---------
 
@@ -1975,7 +1970,7 @@ Row # Value Rank Calculation  Relative Rank
 
 .. class:: Rank(*expressions, **extra)
 
-Comparable to ``RowNumber``, this function ranks rows in the window. The
+Comparable to :class:`RowNumber`, this function ranks rows in the window. The
 computed rank contains gaps. Use :class:`DenseRank` to compute rank without
 gaps.
 
diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt
index 67baef7dfc..1b6a208d01 100644
--- a/docs/ref/models/expressions.txt
+++ b/docs/ref/models/expressions.txt
@@ -1053,11 +1053,18 @@ calling the appropriate methods on the wrapped expression.
 
     .. attribute:: allowed_default
 
-        .. versionadded:: 5.0
-
         Tells Django that this expression can be used in
         :attr:`Field.db_default`. Defaults to ``False``.
 
+    .. attribute:: constraint_validation_compatible
+
+        .. versionadded:: 5.1
+
+        Tells Django that this expression can be used during a constraint
+        validation. Expressions with ``constraint_validation_compatible`` set
+        to ``False`` must have only one source expression. Defaults to
+        ``True``.
+
     .. attribute:: contains_aggregate
 
         Tells Django that this expression contains an aggregate and that a
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index a29d06c00a..e8b1cf8cba 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -138,10 +138,6 @@ the choices are:
   provide a well-known inventory of values, such as currencies, countries,
   languages, time zones, etc.
 
-.. versionchanged:: 5.0
-
-    Support for mappings and callables was added.
-
 Generally, it's best to define choices inside a model class, and to
 define a suitably-named constant for each value::
 
@@ -372,10 +368,6 @@ There are some additional caveats to be aware of:
 
         __empty__ = _("(Unknown)")
 
-.. versionchanged:: 5.0
-
-    Support for using enumeration types directly in the ``choices`` was added.
-
 ``db_column``
 -------------
 
@@ -405,8 +397,6 @@ looking at your Django code. For example::
 ``db_default``
 --------------
 
-.. versionadded:: 5.0
-
 .. attribute:: Field.db_default
 
 The database-computed default value for this field. This can be a literal value
@@ -808,6 +798,15 @@ Any combination of these options will result in an error.
     instead of a ``DateField`` and deciding how to handle the conversion from
     datetime to date at display time.
 
+.. warning:: Always use :class:`DateField` with a ``datetime.date`` instance.
+
+    If you have a ``datetime.datetime`` instance, it's recommended to convert
+    it to a ``datetime.date`` first. If you don't, :class:`DateField` will
+    localize the ``datetime.datetime`` to the :ref:`default timezone
+    <default-current-time-zone>` and convert it to a ``datetime.date``
+    instance, removing its time component. This is true for both storage and
+    comparison.
+
 ``DateTimeField``
 -----------------
 
@@ -820,6 +819,16 @@ The default form widget for this field is a single
 :class:`~django.forms.DateTimeInput`. The admin uses two separate
 :class:`~django.forms.TextInput` widgets with JavaScript shortcuts.
 
+.. warning:: Always use :class:`DateTimeField` with a ``datetime.datetime``
+    instance.
+
+    If you have a ``datetime.date`` instance, it's recommended to convert it to
+    a ``datetime.datetime`` first. If you don't, :class:`DateTimeField` will
+    use midnight in the :ref:`default timezone <default-current-time-zone>` for
+    the time component. This is true for both storage and comparison. To
+    compare the date portion of a :class:`DateTimeField` with a
+    ``datetime.date`` instance, use the :lookup:`date` lookup.
+
 ``DecimalField``
 ----------------
 
@@ -1235,8 +1244,6 @@ when :attr:`~django.forms.Field.localize` is ``False`` or
 ``GeneratedField``
 ------------------
 
-.. versionadded:: 5.0
-
 .. class:: GeneratedField(expression, output_field, db_persist=None, **kwargs)
 
 A field that is always computed based on other fields in the model. This field
@@ -1342,13 +1349,13 @@ following optional arguments:
 
 .. attribute:: ImageField.height_field
 
-    Name of a model field which will be auto-populated with the height of the
-    image each time the model instance is saved.
+    Name of a model field which is auto-populated with the height of the image
+    each time an image object is set.
 
 .. attribute:: ImageField.width_field
 
-    Name of a model field which will be auto-populated with the width of the
-    image each time the model instance is saved.
+    Name of a model field which is auto-populated with the width of the image
+    each time an image object is set.
 
 Requires the `Pillow`_ library.
 
@@ -2489,18 +2496,11 @@ Attributes for fields
 
 .. attribute:: Field.hidden
 
-    Boolean flag that indicates if a field is used to back another non-hidden
-    field's functionality (e.g. the ``content_type`` and ``object_id`` fields
-    that make up a ``GenericForeignKey``). The ``hidden`` flag is used to
-    distinguish what constitutes the public subset of fields on the model from
-    all the fields on the model.
-
-    .. note::
-
-        :meth:`Options.get_fields()
-        <django.db.models.options.Options.get_fields()>`
-        excludes hidden fields by default. Pass in ``include_hidden=True`` to
-        return hidden fields in the results.
+    Boolean flag that indicates if a field is hidden and should not be returned
+    by :meth:`Options.get_fields()
+    <django.db.models.options.Options.get_fields>` by default. An example is
+    the reverse field for a :class:`~django.db.models.ForeignKey` with a
+    ``related_name`` that starts with ``'+'``.
 
 .. attribute:: Field.is_relation
 
diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
index 6d1a7e5db4..e1011ded66 100644
--- a/docs/ref/models/instances.txt
+++ b/docs/ref/models/instances.txt
@@ -246,17 +246,6 @@ need to call a model's :meth:`~Model.full_clean()` method if you plan to handle
 validation errors yourself, or if you have excluded fields from the
 :class:`~django.forms.ModelForm` that require validation.
 
-.. warning::
-
-    Constraints containing :class:`~django.db.models.JSONField` may not raise
-    validation errors as key, index, and path transforms have many
-    database-specific caveats. This :ticket:`may be fully supported later
-    <34059>`.
-
-    You should always check that there are no log messages, in the
-    ``django.db.models`` logger, like *"Got a database error calling check() on
-    …"* to confirm it's validated properly.
-
 .. method:: Model.full_clean(exclude=None, validate_unique=True, validate_constraints=True)
 
 This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`,
@@ -391,7 +380,7 @@ Then, ``full_clean()`` will check unique constraints on your model.
                         raise ValidationError(
                             {
                                 "status": _(
-                                    "Set status to draft if there is not a " "publication date."
+                                    "Set status to draft if there is not a publication date."
                                 ),
                             }
                         )
@@ -591,10 +580,6 @@ which returns ``NULL``. In such cases it is possible to revert to the old
 algorithm by setting the :attr:`~django.db.models.Options.select_on_save`
 option to ``True``.
 
-.. versionchanged:: 5.0
-
-    The ``Field.db_default`` parameter was added.
-
 .. _ref-models-force-insert:
 
 Forcing an INSERT or UPDATE
@@ -627,11 +612,6 @@ only.
 
 Using ``update_fields`` will force an update similarly to ``force_update``.
 
-.. versionchanged:: 5.0
-
-    Support for passing a tuple of parent classes to ``force_insert`` was
-    added.
-
 .. _ref-models-field-updates-using-f-expressions:
 
 Updating attributes based on existing fields
diff --git a/docs/ref/models/meta.txt b/docs/ref/models/meta.txt
index a96c563d49..b3ec23f218 100644
--- a/docs/ref/models/meta.txt
+++ b/docs/ref/models/meta.txt
@@ -79,10 +79,7 @@ Retrieving all field instances of a model
 
     ``include_hidden``
         ``False`` by default. If set to ``True``, ``get_fields()`` will include
-        fields that are used to back other field's functionality. This will
-        also include any fields that have a ``related_name`` (such
-        as :class:`~django.db.models.ManyToManyField`, or
-        :class:`~django.db.models.ForeignKey`) that start with a "+".
+        :attr:`hidden fields <django.db.models.Field.hidden>`.
 
     .. code-block:: pycon
 
diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
index 909577be6c..3433d0730f 100644
--- a/docs/ref/models/options.txt
+++ b/docs/ref/models/options.txt
@@ -467,7 +467,7 @@ not be looking at your Django code. For example::
 
             class Meta:
                 constraints = [
-                    models.CheckConstraint(check=models.Q(age__gte=18), name="age_gte_18"),
+                    models.CheckConstraint(condition=models.Q(age__gte=18), name="age_gte_18"),
                 ]
 
 ``verbose_name``
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 0118cf4e5b..7a0d086bfe 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -1160,12 +1160,6 @@ supports prefetching of
 queryset for each ``ContentType`` must be provided in the ``querysets``
 parameter of :class:`~django.contrib.contenttypes.prefetch.GenericPrefetch`.
 
-.. versionchanged:: 5.0
-
-    Support for prefetching
-    :class:`~django.contrib.contenttypes.fields.GenericForeignKey` with
-    non-homogeneous set of results was added.
-
 For example, suppose you have these models::
 
     from django.db import models
@@ -2114,13 +2108,6 @@ SQL equivalent:
             2
         )
 
-   .. versionchanged:: 5.0
-
-        In older versions, on databases without native support for the SQL
-        ``XOR`` operator, ``XOR`` returned rows that were matched by exactly
-        one operand. The previous behavior was not consistent with MySQL,
-        MariaDB, and Python behavior.
-
 Methods that do not return ``QuerySet``\s
 -----------------------------------------
 
@@ -2402,10 +2389,6 @@ Like :meth:`get_or_create` and :meth:`create`, if you're using manually
 specified primary keys and an object needs to be created but the key already
 exists in the database, an :exc:`~django.db.IntegrityError` is raised.
 
-.. versionchanged:: 5.0
-
-    The ``create_defaults`` argument was added.
-
 ``bulk_create()``
 ~~~~~~~~~~~~~~~~~
 
@@ -2470,11 +2453,6 @@ be in conflict must be provided.
 Enabling the ``ignore_conflicts`` parameter disables setting the primary key on
 each model instance (if the database normally supports it).
 
-.. versionchanged:: 5.0
-
-    In older versions, enabling the ``update_conflicts`` parameter prevented
-    setting the primary key on each model instance.
-
 .. warning::
 
     On MySQL and MariaDB, setting the ``ignore_conflicts`` parameter to
@@ -2629,11 +2607,6 @@ evaluated will force it to evaluate again, repeating the query.
 long as ``chunk_size`` is given. Larger values will necessitate fewer queries
 to accomplish the prefetching at the cost of greater memory usage.
 
-.. versionchanged:: 5.0
-
-    Support for ``aiterator()`` with previous calls to ``prefetch_related()``
-    was added.
-
 On some databases (e.g. Oracle, `SQLite
 <https://www.sqlite.org/limits.html#max_variable_number>`_), the maximum number
 of terms in an SQL ``IN`` clause might be limited. Hence values below this
@@ -4078,9 +4051,10 @@ elsewhere.
 A ``Q()`` object represents an SQL condition that can be used in
 database-related operations. It's similar to how an
 :class:`F() <django.db.models.F>` object represents the value of a model field
-or annotation. They make it possible to define and reuse conditions, and
-combine them using operators such as ``|`` (``OR``), ``&`` (``AND``), and ``^``
-(``XOR``). See :ref:`complex-lookups-with-q`.
+or annotation. They make it possible to define and reuse conditions. These can
+be negated using the ``~`` (``NOT``) operator, and combined using operators
+such as ``|`` (``OR``), ``&`` (``AND``), and ``^`` (``XOR``). See
+:ref:`complex-lookups-with-q`.
 
 ``Prefetch()`` objects
 ----------------------
@@ -4161,10 +4135,6 @@ When using multiple databases with ``prefetch_related_objects``, the prefetch
 query will use the database associated with the model instance. This can be
 overridden by using a custom queryset in a related lookup.
 
-.. versionchanged:: 5.0
-
-    ``aprefetch_related_objects()`` function was added.
-
 ``FilteredRelation()`` objects
 ------------------------------
 
diff --git a/docs/ref/paginator.txt b/docs/ref/paginator.txt
index 8afadbe6f8..03084e5d40 100644
--- a/docs/ref/paginator.txt
+++ b/docs/ref/paginator.txt
@@ -58,8 +58,6 @@ For examples, see the :doc:`Pagination topic guide </topics/pagination>`.
 
 .. attribute:: Paginator.error_messages
 
-    .. versionadded:: 5.0
-
     The ``error_messages`` argument lets you override the default messages that
     the paginator will raise. Pass in a dictionary with keys matching the error
     messages you want to override. Available error message keys are:
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index 15e0639fae..20c04279b2 100644
--- a/docs/ref/request-response.txt
+++ b/docs/ref/request-response.txt
@@ -289,8 +289,6 @@ Methods
 
 .. method:: HttpRequest.auser()
 
-    .. versionadded:: 5.0
-
     From the :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`:
     Coroutine. Returns an instance of :setting:`AUTH_USER_MODEL` representing
     the currently logged-in user. If the user isn't currently logged in,
@@ -948,7 +946,7 @@ Methods
       sent with all same-site and cross-site requests.
 
     .. _HttpOnly: https://owasp.org/www-community/HttpOnly
-    .. _SameSite: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
+    .. _SameSite: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
 
     .. warning::
 
@@ -1290,8 +1288,6 @@ Attributes
 Handling disconnects
 --------------------
 
-.. versionadded:: 5.0
-
 If the client disconnects during a streaming response, Django will cancel the
 coroutine that is handling the response. If you want to clean up resources
 manually, you can do so by catching the ``asyncio.CancelledError``::
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 98434c8833..88ccf6106e 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -1667,7 +1667,7 @@ the server-provided value of ``SCRIPT_NAME``, which may be a rewritten version
 of the preferred value or not supplied at all. It is also used by
 :func:`django.setup()` to set the URL resolver script prefix outside of the
 request/response cycle (e.g. in management commands and standalone scripts) to
-generate correct URLs when ``SCRIPT_NAME`` is not ``/``.
+generate correct URLs when ``FORCE_SCRIPT_NAME`` is provided.
 
 .. setting:: FORM_RENDERER
 
@@ -1689,7 +1689,6 @@ renderers are:
 ``FORMS_URLFIELD_ASSUME_HTTPS``
 -------------------------------
 
-.. versionadded:: 5.0
 .. deprecated:: 5.0
 
 Default: ``False``
@@ -1710,6 +1709,10 @@ for project locales. If not ``None``, Django will check for a ``formats.py``
 file, under the directory named as the current locale, and will use the
 formats defined in this file.
 
+The name of the directory containing the format definitions is expected to be
+named using :term:`locale name` notation, for example ``de``, ``pt_BR``,
+``en_US``, etc.
+
 For example, if :setting:`FORMAT_MODULE_PATH` is set to ``mysite.formats``,
 and current language is ``en`` (English), Django will expect a directory tree
 like:
@@ -2919,10 +2922,6 @@ be retained if present.
 
 See also :setting:`TIME_ZONE` and :setting:`USE_I18N`.
 
-.. versionchanged:: 5.0
-
-    In older versions, the default value is ``False``.
-
 .. setting:: USE_X_FORWARDED_HOST
 
 ``USE_X_FORWARDED_HOST``
@@ -3056,8 +3055,9 @@ Default: ``'/accounts/login/'``
 The URL or :ref:`named URL pattern <naming-url-patterns>` where requests are
 redirected for login when using the
 :func:`~django.contrib.auth.decorators.login_required` decorator,
-:class:`~django.contrib.auth.mixins.LoginRequiredMixin`, or
-:class:`~django.contrib.auth.mixins.AccessMixin`.
+:class:`~django.contrib.auth.mixins.LoginRequiredMixin`,
+:class:`~django.contrib.auth.mixins.AccessMixin`, or when
+:class:`~django.contrib.auth.middleware.LoginRequiredMiddleware` is installed.
 
 .. setting:: LOGOUT_REDIRECT_URL
 
@@ -3345,7 +3345,7 @@ Possible values for the setting are:
     Modern browsers provide a more secure default policy for the ``SameSite``
     flag and will assume ``Lax`` for cookies without an explicit value set.
 
-.. _SameSite: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
+.. _SameSite: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
 
 .. setting:: SESSION_COOKIE_SECURE
 
@@ -3664,14 +3664,14 @@ Forms
 
 Globalization (``i18n``/``l10n``)
 ---------------------------------
-* :setting:`DATE_FORMAT`
-* :setting:`DATE_INPUT_FORMATS`
-* :setting:`DATETIME_FORMAT`
-* :setting:`DATETIME_INPUT_FORMATS`
-* :setting:`DECIMAL_SEPARATOR`
+
+.. _settings-i18n:
+
+Internationalization (``i18n``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 * :setting:`FIRST_DAY_OF_WEEK`
 * :setting:`FORMAT_MODULE_PATH`
-* :setting:`LANGUAGE_CODE`
 * :setting:`LANGUAGE_COOKIE_AGE`
 * :setting:`LANGUAGE_COOKIE_DOMAIN`
 * :setting:`LANGUAGE_COOKIE_HTTPONLY`
@@ -3682,6 +3682,21 @@ Globalization (``i18n``/``l10n``)
 * :setting:`LANGUAGES`
 * :setting:`LANGUAGES_BIDI`
 * :setting:`LOCALE_PATHS`
+* :setting:`TIME_ZONE`
+* :setting:`USE_I18N`
+* :setting:`USE_TZ`
+
+.. _settings-l10n:
+
+Localization (``l10n``)
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* :setting:`DATE_FORMAT`
+* :setting:`DATE_INPUT_FORMATS`
+* :setting:`DATETIME_FORMAT`
+* :setting:`DATETIME_INPUT_FORMATS`
+* :setting:`DECIMAL_SEPARATOR`
+* :setting:`LANGUAGE_CODE`
 * :setting:`MONTH_DAY_FORMAT`
 * :setting:`NUMBER_GROUPING`
 * :setting:`SHORT_DATE_FORMAT`
@@ -3689,10 +3704,7 @@ Globalization (``i18n``/``l10n``)
 * :setting:`THOUSAND_SEPARATOR`
 * :setting:`TIME_FORMAT`
 * :setting:`TIME_INPUT_FORMATS`
-* :setting:`TIME_ZONE`
-* :setting:`USE_I18N`
 * :setting:`USE_THOUSAND_SEPARATOR`
-* :setting:`USE_TZ`
 * :setting:`YEAR_MONTH_FORMAT`
 
 HTTP
diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt
index 26e4ff4c23..a46717b8d7 100644
--- a/docs/ref/templates/api.txt
+++ b/docs/ref/templates/api.txt
@@ -201,12 +201,37 @@ Once you have a compiled :class:`Template` object, you can render a context
 with it. You can reuse the same template to render it several times with
 different contexts.
 
-.. class:: Context(dict_=None)
+.. class:: Context(dict_=None, autoescape=True, use_l10n=None, use_tz=None)
 
     The constructor of ``django.template.Context`` takes an optional argument —
     a dictionary mapping variable names to variable values.
 
-    For details, see :ref:`playing-with-context` below.
+    Three optional keyword arguments can also be specified:
+
+    * ``autoescape`` controls whether HTML autoescaping is enabled.
+
+      It defaults to ``True``.
+
+      .. warning::
+
+          Only set it to ``False`` if you're rendering non-HTML templates!
+
+    * ``use_l10n`` overrides whether values will be localized by default. If
+      set to ``True`` numbers and dates will be formatted based on locale.
+
+      It defaults to ``None``.
+
+      See :ref:`topic-l10n-templates` for details.
+
+    * ``use_tz`` overrides whether dates are converted to the local time when
+      rendered in a template. If set to ``True`` all dates will be rendered
+      using the local timezone. This takes precedence over :setting:`USE_TZ`.
+
+      It defaults to ``None``.
+
+      See :ref:`time-zones-in-templates` for details.
+
+    For example usage, see :ref:`playing-with-context` below.
 
 .. method:: Template.render(context)
 
@@ -610,9 +635,9 @@ against ``dict``::
 Using ``RequestContext``
 ------------------------
 
-.. class:: RequestContext(request, dict_=None, processors=None)
+.. class:: RequestContext(request, dict_=None, processors=None, use_l10n=None, use_tz=None, autoescape=True)
 
-Django comes with a special ``Context`` class,
+Django comes with a special :class:`~django.template.Context` class,
 ``django.template.RequestContext``, that acts slightly differently from the
 normal ``django.template.Context``. The first difference is that it takes an
 :class:`~django.http.HttpRequest` as its first argument. For example::
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index a10af9310f..4cfd1d8f71 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -1957,8 +1957,6 @@ For example:
 ``escapeseq``
 -------------
 
-.. versionadded:: 5.0
-
 Applies the :tfilter:`escape` filter to each element of a sequence. Useful in
 conjunction with other filters that operate on sequences, such as
 :tfilter:`join`. For example:
@@ -2141,7 +2139,8 @@ For example:
 
     {{ value|iriencode }}
 
-If ``value`` is ``"?test=1&me=2"``, the output will be ``"?test=1&amp;me=2"``.
+If ``value`` is ``"?test=I ♥ Django"``, the output will be
+``"?test=I%20%E2%99%A5%20Django"``.
 
 .. templatefilter:: join
 
@@ -2725,10 +2724,6 @@ Newlines in the HTML content will be preserved.
     resource-intensive and impact service performance. ``truncatechars_html``
     limits input to the first five million characters.
 
-.. versionchanged:: 3.2.22
-
-    In older versions, strings over five million characters were processed.
-
 .. templatefilter:: truncatewords
 
 ``truncatewords``
@@ -2777,10 +2772,6 @@ Newlines in the HTML content will be preserved.
     resource-intensive and impact service performance. ``truncatewords_html``
     limits input to the first five million characters.
 
-.. versionchanged:: 3.2.22
-
-    In older versions, strings over five million characters were processed.
-
 .. templatefilter:: unordered_list
 
 ``unordered_list``
diff --git a/docs/ref/unicode.txt b/docs/ref/unicode.txt
index 81a0b08aad..dfd10b19db 100644
--- a/docs/ref/unicode.txt
+++ b/docs/ref/unicode.txt
@@ -28,7 +28,7 @@ able to store certain characters in the database, and information will be lost.
   for internal encoding.
 
 .. _MySQL manual: https://dev.mysql.com/doc/refman/en/charset-database.html
-.. _PostgreSQL manual: https://www.postgresql.org/docs/current/multibyte.html#id-1.6.11.5.6
+.. _PostgreSQL manual: https://www.postgresql.org/docs/current/multibyte.html#MULTIBYTE-SETTING
 .. _Oracle manual: https://docs.oracle.com/en/database/oracle/oracle-database/21/nlspg/index.html
 .. _section 2: https://docs.oracle.com/en/database/oracle/oracle-database/21/nlspg/choosing-character-set.html
 .. _section 11: https://docs.oracle.com/en/database/oracle/oracle-database/21/nlspg/character-set-migration.html
diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt
index eb0b991f1b..b335d1fc39 100644
--- a/docs/ref/urlresolvers.txt
+++ b/docs/ref/urlresolvers.txt
@@ -203,7 +203,7 @@ A :class:`ResolverMatch` object can also be assigned to a triple::
 One possible use of :func:`~django.urls.resolve` would be to test whether a
 view would raise a ``Http404`` error before redirecting to it::
 
-    from urllib.parse import urlparse
+    from urllib.parse import urlsplit
     from django.urls import resolve
     from django.http import Http404, HttpResponseRedirect
 
@@ -215,7 +215,7 @@ view would raise a ``Http404`` error before redirecting to it::
         # modify the request and response as required, e.g. change locale
         # and set corresponding locale cookie
 
-        view, args, kwargs = resolve(urlparse(next)[2])
+        view, args, kwargs = resolve(urlsplit(next).path)
         kwargs["request"] = request
         try:
             view(*args, **kwargs)
diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt
index e8d51eeda2..2ef873d348 100644
--- a/docs/ref/urls.txt
+++ b/docs/ref/urls.txt
@@ -120,6 +120,10 @@ The ``converter`` argument is a converter class, and ``type_name`` is the
 converter name to use in path patterns. See
 :ref:`registering-custom-path-converters` for an example.
 
+.. deprecated:: 5.1
+
+    Overriding existing converters is deprecated.
+
 ==================================================
 ``django.conf.urls`` functions for use in URLconfs
 ==================================================
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 3e357cba17..9fb1e83e9e 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -331,6 +331,32 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
 
     See https://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
 
+``Stylesheet``
+--------------
+
+.. versionadded:: 5.2
+
+.. class:: Stylesheet(url, mimetype="", media="screen")
+
+    Represents an RSS stylesheet.
+
+    .. attribute:: url
+
+        Required argument. The URL where the stylesheet is located.
+
+    .. attribute:: mimetype
+
+        An optional string containing the MIME type of the stylesheet. If not
+        specified, Django will attempt to guess it by using Python's
+        :py:func:`mimetypes.guess_type()`. Use ``mimetype=None`` if you don't
+        want your stylesheet to have a MIME type specified.
+
+    .. attribute:: media
+
+        An optional string which will be used as the ``media`` attribute of
+        the stylesheet. Defaults to ``"screen"``. Use ``media=None`` if you
+        don't want your stylesheet to have a ``media`` attribute.
+
 ``SyndicationFeed``
 -------------------
 
@@ -339,7 +365,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
     Base class for all syndication feeds. Subclasses should provide
     ``write()``.
 
-    .. method:: __init__(title, link, description, language=None, author_email=None, author_name=None, author_link=None, subtitle=None, categories=None, feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs)
+    .. method:: __init__(title, link, description, language=None, author_email=None, author_name=None, author_link=None, subtitle=None, categories=None, feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, stylesheets=None, **kwargs)
 
         Initialize the feed with the given dictionary of metadata, which applies
         to the entire feed.
@@ -347,8 +373,15 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
         Any extra keyword arguments you pass to ``__init__`` will be stored in
         ``self.feed``.
 
-        All parameters should be strings, except ``categories``, which should
-        be a sequence of strings.
+        All parameters should be strings, except for two:
+
+        * ``categories`` should be a sequence of strings.
+        * ``stylesheets`` should be a sequence of either strings or
+          :class:`Stylesheet` instances.
+
+        .. versionchanged:: 5.2
+
+            The ``stylesheets`` argument was added.
 
     .. method:: add_item(title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, categories=(), item_copyright=None, ttl=None, updateddate=None, enclosures=None, **kwargs)
 
@@ -368,6 +401,13 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
         Add elements in the root (i.e. feed/channel) element.
         Called from ``write()``.
 
+    .. method:: add_stylesheets(self, handler)
+
+        .. versionadded:: 5.2
+
+        Add stylesheet information to the document.
+        Called from ``write()``.
+
     .. method:: item_attributes(item)
 
         Return extra attributes to place on each item (i.e. item/entry)
diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt
index 789d47d935..846a3c7157 100644
--- a/docs/ref/validators.txt
+++ b/docs/ref/validators.txt
@@ -159,6 +159,25 @@ to, or in lieu of custom ``field.clean()`` methods.
         validation, so you'd need to add them to the ``allowlist`` as
         necessary.
 
+``DomainNameValidator``
+-----------------------
+
+.. versionadded:: 5.1
+
+.. class:: DomainNameValidator(accept_idna=True, message=None, code=None)
+
+    A :class:`RegexValidator` subclass that ensures a value looks like a domain
+    name. Values longer than 255 characters are always considered invalid. IP
+    addresses are not accepted as valid domain names.
+
+    In addition to the optional arguments of its parent :class:`RegexValidator`
+    class, ``DomainNameValidator`` accepts an extra optional attribute:
+
+    .. attribute:: accept_idna
+
+        Determines whether to accept internationalized domain names, that is,
+        domain names that contain non-ASCII characters. Defaults to ``True``.
+
 ``URLValidator``
 ----------------
 
@@ -183,6 +202,12 @@ to, or in lieu of custom ``field.clean()`` methods.
 
         .. _valid URI schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
 
+        .. warning::
+
+            Values starting with ``file:///`` will not pass validation even
+            when the ``file`` scheme is provided. Valid values must contain a
+            host.
+
     .. attribute:: max_length
 
         The maximum length of values that could be considered valid. Defaults
@@ -195,6 +220,15 @@ to, or in lieu of custom ``field.clean()`` methods.
 
     An :class:`EmailValidator` instance without any customizations.
 
+``validate_domain_name``
+------------------------
+
+.. versionadded:: 5.1
+
+.. data:: validate_domain_name
+
+    A :class:`DomainNameValidator` instance without any customizations.
+
 ``validate_slug``
 -----------------
 
@@ -361,6 +395,3 @@ to, or in lieu of custom ``field.clean()`` methods.
     ``StepValueValidator(3, offset=1.4)`` valid values include ``1.4``,
     ``4.4``, ``7.4``, ``10.4``, and so on.
 
-    .. versionchanged:: 5.0
-
-        The ``offset`` argument was added.
diff --git a/docs/releases/0.96.txt b/docs/releases/0.96.txt
index 0be19b1ddf..032eb1009a 100644
--- a/docs/releases/0.96.txt
+++ b/docs/releases/0.96.txt
@@ -119,7 +119,7 @@ The ``ENABLE_PSYCO`` setting no longer exists. If your settings file includes
 ``ENABLE_PSYCO`` it will have no effect; to use Psyco_, we recommend
 writing a middleware class to activate it.
 
-.. _psyco: http://psyco.sourceforge.net/
+.. _psyco: https://psyco.sourceforge.net/
 
 What's new in 0.96?
 ===================
diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt
index 4dfb6aae5a..eb043710c1 100644
--- a/docs/releases/3.0.txt
+++ b/docs/releases/3.0.txt
@@ -362,14 +362,8 @@ Tests
   references, and entity references that refer to the same character as
   equivalent.
 
-* Django test runner now supports headless mode for selenium tests on supported
-  browsers. Add the ``--headless`` option to enable this mode.
-
-* Django test runner now supports ``--start-at`` and ``--start-after`` options
-  to run tests starting from a specific top-level module.
-
-* Django test runner now supports a ``--pdb`` option to spawn a debugger at
-  each error or failure.
+* :class:`~django.test.runner.DiscoverRunner` can now spawn a debugger at each
+  error or failure using the :option:`test --pdb` option.
 
 .. _backwards-incompatible-3.0:
 
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 0b13fc37d2..704cf3e6d1 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -388,7 +388,7 @@ Models
   ``OneToOneField`` emulates the behavior of the SQL constraint ``ON DELETE
   RESTRICT``.
 
-* :attr:`.CheckConstraint.check` now supports boolean expressions.
+* ``CheckConstraint.check`` now supports boolean expressions.
 
 * The :meth:`.RelatedManager.add`, :meth:`~.RelatedManager.create`, and
   :meth:`~.RelatedManager.set` methods now accept callables as values in the
@@ -475,8 +475,8 @@ Tests
 * The new :setting:`MIGRATE <TEST_MIGRATE>` test database setting allows
   disabling of migrations during a test database creation.
 
-* Django test runner now supports a :option:`test --buffer` option to discard
-  output for passing tests.
+* :class:`~django.test.runner.DiscoverRunner` can now discard output for
+  passing tests using the :option:`test --buffer` option.
 
 * :class:`~django.test.runner.DiscoverRunner` now skips running the system
   checks on databases not :ref:`referenced by tests<testing-multi-db>`.
diff --git a/docs/releases/3.2.24.txt b/docs/releases/3.2.24.txt
index 1ab7024f73..67be0f65d1 100644
--- a/docs/releases/3.2.24.txt
+++ b/docs/releases/3.2.24.txt
@@ -6,4 +6,8 @@ Django 3.2.24 release notes
 
 Django 3.2.24 fixes a security issue with severity "moderate" in 3.2.23.
 
-...
+CVE-2024-24680: Potential denial-of-service in ``intcomma`` template filter
+===========================================================================
+
+The ``intcomma`` template filter was subject to a potential denial-of-service
+attack when used with very long strings.
diff --git a/docs/releases/3.2.25.txt b/docs/releases/3.2.25.txt
new file mode 100644
index 0000000000..a3a90986ff
--- /dev/null
+++ b/docs/releases/3.2.25.txt
@@ -0,0 +1,22 @@
+===========================
+Django 3.2.25 release notes
+===========================
+
+*March 4, 2024*
+
+Django 3.2.25 fixes a security issue with severity "moderate" and a regression
+in 3.2.24.
+
+CVE-2024-27351: Potential regular expression denial-of-service in ``django.utils.text.Truncator.words()``
+=========================================================================================================
+
+``django.utils.text.Truncator.words()`` method (with ``html=True``) and
+:tfilter:`truncatewords_html` template filter were subject to a potential
+regular expression denial-of-service attack using a suitably crafted string
+(follow up to :cve:`2019-14232` and :cve:`2023-43665`).
+
+Bugfixes
+========
+
+* Fixed a regression in Django 3.2.24 where ``intcomma`` template filter could
+  return a leading comma for string representation of floats (:ticket:`35172`).
diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt
index 05f199e4fe..8fb11451a6 100644
--- a/docs/releases/4.0.txt
+++ b/docs/releases/4.0.txt
@@ -367,8 +367,7 @@ Tests
   serialized to allow usage of the
   :ref:`serialized_rollback <test-case-serialized-rollback>` feature.
 
-* Django test runner now supports a :option:`--buffer <test --buffer>` option
-  with parallel tests.
+* The :option:`test --buffer` option now supports parallel tests.
 
 * The new ``logger`` argument to :class:`~django.test.runner.DiscoverRunner`
   allows a Python :py:ref:`logger <logger>` to be used for logging.
@@ -376,8 +375,8 @@ Tests
 * The new :meth:`.DiscoverRunner.log` method provides a way to log messages
   that uses the ``DiscoverRunner.logger``, or prints to the console if not set.
 
-* Django test runner now supports a :option:`--shuffle <test --shuffle>` option
-  to execute tests in a random order.
+* :class:`~django.test.runner.DiscoverRunner` can now execute tests in a random
+  order using the :option:`test --shuffle` option.
 
 * The :option:`test --parallel` option now supports the value ``auto`` to run
   one test process for each processor core.
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index c840db4a7f..3986774013 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -534,8 +534,9 @@ Miscellaneous
   on the :class:`~django.db.models.Model` instance to which they belong. *This
   change was reverted in Django 4.1.2.*
 
-* The Django test runner now returns a non-zero error code for unexpected
-  successes from tests marked with :py:func:`unittest.expectedFailure`.
+* :class:`~django.test.runner.DiscoverRunner` now returns a non-zero error code
+  for unexpected successes from tests marked with
+  :py:func:`unittest.expectedFailure`.
 
 * :class:`~django.middleware.csrf.CsrfViewMiddleware` no longer masks the CSRF
   cookie like it does the CSRF token in the DOM.
diff --git a/docs/releases/4.2.10.txt b/docs/releases/4.2.10.txt
index c039f6840f..7cdfa69814 100644
--- a/docs/releases/4.2.10.txt
+++ b/docs/releases/4.2.10.txt
@@ -6,4 +6,8 @@ Django 4.2.10 release notes
 
 Django 4.2.10 fixes a security issue with severity "moderate" in 4.2.9.
 
-...
+CVE-2024-24680: Potential denial-of-service in ``intcomma`` template filter
+===========================================================================
+
+The ``intcomma`` template filter was subject to a potential denial-of-service
+attack when used with very long strings.
diff --git a/docs/releases/4.2.11.txt b/docs/releases/4.2.11.txt
new file mode 100644
index 0000000000..c562e47866
--- /dev/null
+++ b/docs/releases/4.2.11.txt
@@ -0,0 +1,22 @@
+===========================
+Django 4.2.11 release notes
+===========================
+
+*March 4, 2024*
+
+Django 4.2.11 fixes a security issue with severity "moderate" and a regression
+in 4.2.10.
+
+CVE-2024-27351: Potential regular expression denial-of-service in ``django.utils.text.Truncator.words()``
+=========================================================================================================
+
+``django.utils.text.Truncator.words()`` method (with ``html=True``) and
+:tfilter:`truncatewords_html` template filter were subject to a potential
+regular expression denial-of-service attack using a suitably crafted string
+(follow up to :cve:`2019-14232` and :cve:`2023-43665`).
+
+Bugfixes
+========
+
+* Fixed a regression in Django 4.2.10 where ``intcomma`` template filter could
+  return a leading comma for string representation of floats (:ticket:`35172`).
diff --git a/docs/releases/4.2.12.txt b/docs/releases/4.2.12.txt
new file mode 100644
index 0000000000..01abac19c2
--- /dev/null
+++ b/docs/releases/4.2.12.txt
@@ -0,0 +1,14 @@
+===========================
+Django 4.2.12 release notes
+===========================
+
+*May 6, 2024*
+
+Django 4.2.12 fixes a compatibility issue with Python 3.11.9+ and 3.12.3+.
+
+Bugfixes
+========
+
+* Fixed a crash in Django 4.2 when validating email max line lengths with
+  content decoded using the ``surrogateescape`` error handling scheme
+  (:ticket:`35361`).
diff --git a/docs/releases/4.2.13.txt b/docs/releases/4.2.13.txt
new file mode 100644
index 0000000000..510e587279
--- /dev/null
+++ b/docs/releases/4.2.13.txt
@@ -0,0 +1,7 @@
+===========================
+Django 4.2.13 release notes
+===========================
+
+*May 7, 2024*
+
+Django 4.2.13 fixes a packaging error in 4.2.12.
diff --git a/docs/releases/5.0.2.txt b/docs/releases/5.0.2.txt
index 64ffcb88bd..1da6dc02d9 100644
--- a/docs/releases/5.0.2.txt
+++ b/docs/releases/5.0.2.txt
@@ -7,6 +7,12 @@ Django 5.0.2 release notes
 Django 5.0.2 fixes a security issue with severity "moderate" and several bugs
 in 5.0.1. Also, the latest string translations from Transifex are incorporated.
 
+CVE-2024-24680: Potential denial-of-service in ``intcomma`` template filter
+===========================================================================
+
+The ``intcomma`` template filter was subject to a potential denial-of-service
+attack when used with very long strings.
+
 Bugfixes
 ========
 
@@ -32,3 +38,12 @@ Bugfixes
 * Fixed a regression in Django 5.0 that caused the ``request_finished`` signal to
   sometimes not be fired when running Django through an ASGI server, resulting
   in potential resource leaks (:ticket:`35059`).
+
+* Fixed a bug in Django 5.0 that caused a migration crash on MySQL when adding
+  a ``BinaryField``, ``TextField``, ``JSONField``, or ``GeometryField`` with a
+  ``db_default`` (:ticket:`35162`).
+
+* Fixed a bug in Django 5.0 that caused a migration crash on models with a
+  literal ``db_default`` of a complex type such as ``dict`` instance of a
+  ``JSONField``. Running ``makemigrations`` might generate no-op ``AlterField``
+  operations for fields using ``db_default`` (:ticket:`35149`).
diff --git a/docs/releases/5.0.3.txt b/docs/releases/5.0.3.txt
new file mode 100644
index 0000000000..bd3c6b5004
--- /dev/null
+++ b/docs/releases/5.0.3.txt
@@ -0,0 +1,42 @@
+==========================
+Django 5.0.3 release notes
+==========================
+
+*March 4, 2024*
+
+Django 5.0.3 fixes a security issue with severity "moderate" and several bugs
+in 5.0.2.
+
+CVE-2024-27351: Potential regular expression denial-of-service in ``django.utils.text.Truncator.words()``
+=========================================================================================================
+
+``django.utils.text.Truncator.words()`` method (with ``html=True``) and
+:tfilter:`truncatewords_html` template filter were subject to a potential
+regular expression denial-of-service attack using a suitably crafted string
+(follow up to :cve:`2019-14232` and :cve:`2023-43665`).
+
+Bugfixes
+========
+
+* Fixed a regression in Django 5.0.2 where ``intcomma`` template filter could
+  return a leading comma for string representation of floats (:ticket:`35172`).
+
+* Fixed a bug in Django 5.0 that caused a crash of ``Signal.asend()`` and
+  ``asend_robust()`` when all receivers were asynchronous functions
+  (:ticket:`35174`).
+
+* Fixed a regression in Django 5.0.1 where :meth:`.ModelAdmin.lookup_allowed`
+  would prevent filtering against foreign keys using lookups like ``__isnull``
+  when the field was not included in :attr:`.ModelAdmin.list_filter`
+  (:ticket:`35173`).
+
+* Fixed a regression in Django 5.0 that caused a crash of
+  ``@sensitive_variables`` and ``@sensitive_post_parameters`` decorators on
+  functions loaded from ``.pyc`` files (:ticket:`35187`).
+
+* Fixed a regression in Django 5.0 that caused a crash when reloading a test
+  database and a base queryset for a base manager used ``prefetch_related()``
+  (:ticket:`35238`).
+
+* Fixed a bug in Django 5.0 where facet filters in the admin would crash on a
+  ``SimpleListFilter`` using a queryset without primary keys (:ticket:`35198`).
diff --git a/docs/releases/5.0.4.txt b/docs/releases/5.0.4.txt
new file mode 100644
index 0000000000..1cbdc0c8c4
--- /dev/null
+++ b/docs/releases/5.0.4.txt
@@ -0,0 +1,30 @@
+==========================
+Django 5.0.4 release notes
+==========================
+
+*April 3, 2024*
+
+Django 5.0.4 fixes several bugs in 5.0.3.
+
+Bugfixes
+========
+
+* Fixed a bug in Django 5.0 that caused a crash of ``Model.full_clean()`` on
+  fields with expressions in ``db_default``. As a consequence,
+  ``Model.full_clean()`` no longer validates for empty values in fields with
+  ``db_default`` (:ticket:`35223`).
+
+* Fixed a regression in Django 5.0 where the ``AdminFileWidget`` could be
+  rendered with two ``id`` attributes on the "Clear" checkbox
+  (:ticket:`35273`).
+
+* Fixed a bug in Django 5.0 that caused a migration crash on PostgreSQL 15+
+  when adding a partial ``UniqueConstraint`` with ``nulls_distinct``
+  (:ticket:`35329`).
+
+* Fixed a crash in Django 5.0 when performing queries involving table aliases
+  and lookups on a ``GeneratedField`` of the aliased table (:ticket:`35344`).
+
+* Fixed a bug in Django 5.0 that caused a migration crash when adding a
+  ``GeneratedField`` relying on the ``__contains`` or ``__icontains``
+  lookups or using a ``Value`` containing a ``"%"`` (:ticket:`35336`).
diff --git a/docs/releases/5.0.5.txt b/docs/releases/5.0.5.txt
new file mode 100644
index 0000000000..17127eeaa2
--- /dev/null
+++ b/docs/releases/5.0.5.txt
@@ -0,0 +1,35 @@
+==========================
+Django 5.0.5 release notes
+==========================
+
+*May 6, 2024*
+
+Django 5.0.5 fixes several bugs in 5.0.4.
+
+Bugfixes
+========
+
+* Fixed a bug in Django 5.0 that caused a crash of ``Model.save()`` when
+  creating an instance of a model with a ``GeneratedField`` and providing a
+  primary key (:ticket:`35350`).
+
+* Fixed a compatibility issue encountered in Python 3.11.9+ and 3.12.3+ when
+  validating email max line lengths with content decoded using the
+  ``surrogateescape`` error handling scheme (:ticket:`35361`).
+
+* Fixed a bug in Django 5.0 that caused a crash when applying migrations
+  including alterations to ``GeneratedField`` such as setting ``db_index=True``
+  on SQLite (:ticket:`35373`).
+
+* Allowed importing ``aprefetch_related_objects`` from ``django.db.models``
+  (:ticket:`35392`).
+
+* Fixed a bug in Django 5.0 that caused a migration crash when a
+  ``GeneratedField`` was added before any of the referenced fields from its
+  ``expression`` definition (:ticket:`35359`).
+
+* Fixed a bug in Django 5.0 that caused a migration crash when altering a
+  ``GeneratedField`` referencing a renamed field (:ticket:`35422`).
+
+* Fixed a bug in Django 5.0 where the ``querysets`` argument of
+  ``GenericPrefetch`` was not required (:ticket:`35426`).
diff --git a/docs/releases/5.0.6.txt b/docs/releases/5.0.6.txt
new file mode 100644
index 0000000000..d75d9ffede
--- /dev/null
+++ b/docs/releases/5.0.6.txt
@@ -0,0 +1,7 @@
+==========================
+Django 5.0.6 release notes
+==========================
+
+*May 7, 2024*
+
+Django 5.0.6 fixes a packaging error in 5.0.5.
diff --git a/docs/releases/5.0.7.txt b/docs/releases/5.0.7.txt
new file mode 100644
index 0000000000..cdaa57f766
--- /dev/null
+++ b/docs/releases/5.0.7.txt
@@ -0,0 +1,12 @@
+==========================
+Django 5.0.7 release notes
+==========================
+
+*Expected July 9, 2024*
+
+Django 5.0.7 fixes several bugs in 5.0.6.
+
+Bugfixes
+========
+
+* ...
diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt
index a10c9d280a..303ee88078 100644
--- a/docs/releases/5.0.txt
+++ b/docs/releases/5.0.txt
@@ -432,8 +432,9 @@ Tests
 
 * :class:`~django.test.AsyncClient` now supports the ``follow`` parameter.
 
-* The new :option:`test --durations` option allows showing the duration of the
-  slowest tests on Python 3.12+.
+* :class:`~django.test.runner.DiscoverRunner` now allows showing the duration
+  of the slowest tests using the :option:`test --durations` option (available
+  on Python 3.12+).
 
 Validators
 ~~~~~~~~~~
diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt
index 3fefaa9f0e..49741ca81c 100644
--- a/docs/releases/5.1.txt
+++ b/docs/releases/5.1.txt
@@ -7,8 +7,8 @@ Django 5.1 release notes - UNDER DEVELOPMENT
 Welcome to Django 5.1!
 
 These release notes cover the :ref:`new features <whats-new-5.1>`, as well as
-some :ref:`backwards incompatible changes <backwards-incompatible-5.1>` you'll
-want to be aware of when upgrading from Django 5.0 or earlier. We've
+some :ref:`backwards incompatible changes <backwards-incompatible-5.1>` you
+should be aware of when upgrading from Django 5.0 or earlier. We've
 :ref:`begun the deprecation process for some features
 <deprecated-features-5.1>`.
 
@@ -26,18 +26,85 @@ only officially support the latest release of each series.
 What's new in Django 5.1
 ========================
 
+``{% query_string %}`` template tag
+-----------------------------------
+
+Django 5.1 introduces the :ttag:`{% query_string %} <query_string>` template
+tag, simplifying the modification of query parameters in URLs, making it easier
+to generate links that maintain existing query parameters while adding or
+changing specific ones.
+
+For instance, navigating pagination and query strings in templates can be
+cumbersome. Consider this template fragment that dynamically generates a URL
+for navigating to the next page within a paginated view:
+
+.. code-block:: html+django
+
+    {# Linebreaks added for readability, this should be one, long line. #}
+    <a href="?{% for key, values in request.GET.iterlists %}
+      {% if key != "page" %}
+        {% for value in values %}
+          {{ key }}={{ value }}&amp;
+        {% endfor %}
+      {% endif %}
+    {% endfor %}page={{ page.next_page_number }}">Next page</a>
+
+When switching to using this new template tag, the above magically becomes:
+
+.. code-block:: html+django
+
+    <a href="{% query_string page=page.next_page_number %}">Next page</a>
+
+PostgreSQL Connection Pools
+---------------------------
+
+Django 5.1 also introduces :ref:`connection pool <postgresql-pool>` support for
+PostgreSQL. As the time to establish a new connection can be relatively long,
+keeping connections open can reduce latency.
+
+To use a connection pool with `psycopg`_, you can set the ``"pool"`` option
+inside :setting:`OPTIONS` to be a dict to be passed to
+:class:`~psycopg:psycopg_pool.ConnectionPool`, or to ``True`` to use the
+``ConnectionPool`` defaults::
+
+    DATABASES = {
+        "default": {
+            "ENGINE": "django.db.backends.postgresql",
+            # ...
+            "OPTIONS": {
+                "pool": {
+                    "min_size": 2,
+                    "max_size": 4,
+                    "timeout": 10,
+                }
+            },
+        },
+    }
+
+.. _psycopg: https://www.psycopg.org/
+
+Middleware to require authentication by default
+-----------------------------------------------
+
+The new :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware`
+redirects all unauthenticated requests to a login page. Views can allow
+unauthenticated requests by using the new
+:func:`~django.contrib.auth.decorators.login_not_required` decorator.
+
+The :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware` respects
+the ``login_url`` and ``redirect_field_name`` values set via the
+:func:`~.django.contrib.auth.decorators.login_required` decorator, but does not
+support setting ``login_url`` or ``redirect_field_name`` via the
+:class:`~django.contrib.auth.mixins.LoginRequiredMixin`.
+
 Minor features
 --------------
 
 :mod:`django.contrib.admin`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
-
-:mod:`django.contrib.admindocs`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* ...
+* :attr:`.ModelAdmin.list_display` now supports using ``__`` lookups to list
+  fields from related models.
 
 :mod:`django.contrib.auth`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -45,10 +112,25 @@ Minor features
 * The default iteration count for the PBKDF2 password hasher is increased from
   720,000 to 870,000.
 
-:mod:`django.contrib.contenttypes`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* The default ``parallelism`` of the ``ScryptPasswordHasher`` is 
+  increased from 1 to 5, to follow OWASP recommendations.
 
-* ...
+* :class:`~django.contrib.auth.forms.BaseUserCreationForm` and
+  :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` now support
+  disabling password-based authentication by setting an unusable password on
+  form save. This is now available in the admin when visiting the user creation
+  and password change pages.
+
+* :func:`~.django.contrib.auth.decorators.login_required`,
+  :func:`~.django.contrib.auth.decorators.permission_required`, and
+  :func:`~.django.contrib.auth.decorators.user_passes_test` decorators now
+  support wrapping asynchronous view functions.
+
+* ``ReadOnlyPasswordHashWidget`` now includes a button to reset the user's
+  password, which replaces the link previously embedded in the
+  ``ReadOnlyPasswordHashField``'s help text, improving the overall
+  accessibility of the
+  :class:`~django.contrib.auth.forms.UserChangeForm`.
 
 :mod:`django.contrib.gis`
 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -66,9 +148,9 @@ Minor features
   ``continent_name``, and ``is_in_european_union`` values.
 
 * :meth:`.GeoIP2.city` now exposes the ``accuracy_radius`` and ``region_name``
-  values. In addition the ``dma_code`` and ``region`` values are now exposed as
-  ``metro_code`` and ``region_code``, but the previous keys are also retained
-  for backward compatibility.
+  values. In addition, the ``dma_code`` and ``region`` values are now exposed
+  as ``metro_code`` and ``region_code``, but the previous keys are also
+  retained for backward compatibility.
 
 * :class:`~django.contrib.gis.measure.Area` now supports the ``ha`` unit.
 
@@ -80,18 +162,19 @@ Minor features
 
 * :class:`~django.contrib.gis.gdal.OGRGeometry`,
   :class:`~django.contrib.gis.gdal.Point`,
-  :class:`~django.contrib.gis.gdal.LineString`, and
-  :class:`~django.contrib.gis.gdal.Polygon` now support measured geometries
-  via the new :attr:`.OGRGeometry.is_measured` and ``m`` properties, and the
-  :meth:`.OGRGeometry.set_measured` method.
+  :class:`~django.contrib.gis.gdal.LineString`,
+  :class:`~django.contrib.gis.gdal.Polygon`, and
+  :class:`~django.contrib.gis.gdal.GeometryCollection` and its subclasses now
+  support measured geometries via the new :attr:`.OGRGeometry.is_measured` and
+  ``m`` properties, and the :meth:`.OGRGeometry.set_measured` method.
 
 * :attr:`.OGRGeometry.centroid` is now available on all supported geometry
   types.
 
-:mod:`django.contrib.messages`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* ...
+* :class:`FromWKB() <django.contrib.gis.db.models.functions.FromWKB>` and
+  :class:`FromWKT() <django.contrib.gis.db.models.functions.FromWKT>` functions
+  now support the optional ``srid`` argument (except for Oracle where it is
+  ignored).
 
 :mod:`django.contrib.postgres`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -99,60 +182,30 @@ Minor features
 * :class:`~django.contrib.postgres.indexes.BTreeIndex` now supports the
   ``deduplicate_items`` parameter.
 
-:mod:`django.contrib.redirects`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* ...
-
 :mod:`django.contrib.sessions`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* :class:`django.contrib.sessions.backends.cached_db.SessionStore` now handles
+  exceptions when storing session information in the cache, logging proper
+  error messages with their traceback via the newly added
+  :ref:`sessions logger <django-contrib-sessions-logger>`.
 
-:mod:`django.contrib.sitemaps`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* :class:`django.contrib.sessions.backends.base.SessionBase` and all built-in
+  session engines now provide async API. The new asynchronous methods all have
+  ``a`` prefixed names, e.g. ``aget()``, ``akeys()``, or ``acycle_key()``.
 
-* ...
+Database backends
+~~~~~~~~~~~~~~~~~
 
-:mod:`django.contrib.sites`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* ``"init_command"`` option is now supported in :setting:`OPTIONS` on SQLite
+  to allow specifying :ref:`pragma options <sqlite-init-command>` to set upon
+  connection.
 
-* ...
+* ``"transaction_mode"`` option is now supported in :setting:`OPTIONS` on
+  SQLite to allow specifying the :ref:`sqlite-transaction-behavior`.
 
-:mod:`django.contrib.staticfiles`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* ...
-
-:mod:`django.contrib.syndication`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* ...
-
-Asynchronous views
-~~~~~~~~~~~~~~~~~~
-
-* ...
-
-Cache
-~~~~~
-
-* ...
-
-CSRF
-~~~~
-
-* ...
-
-Decorators
-~~~~~~~~~~
-
-* ...
-
-Email
-~~~~~
-
-* ...
+* ``"pool"`` option is now supported in :setting:`OPTIONS` on PostgreSQL to
+  allow using :ref:`connection pools <postgresql-pool>`.
 
 Error Reporting
 ~~~~~~~~~~~~~~~
@@ -163,12 +216,9 @@ Error Reporting
 File Storage
 ~~~~~~~~~~~~
 
-* ...
-
-File Uploads
-~~~~~~~~~~~~
-
-* ...
+* The :attr:`~django.core.files.storage.FileSystemStorage.allow_overwrite`
+  parameter of :class:`~django.core.files.storage.FileSystemStorage` now allows
+  saving new files over existing ones.
 
 Forms
 ~~~~~
@@ -177,26 +227,11 @@ Forms
   fieldsets with their help text, the form fieldset now includes the
   ``aria-describedby`` HTML attribute.
 
-Generic Views
-~~~~~~~~~~~~~
-
-* ...
-
-Internationalization
-~~~~~~~~~~~~~~~~~~~~
-
-* ...
-
-Logging
-~~~~~~~
-
-* ...
-
 Management Commands
 ~~~~~~~~~~~~~~~~~~~
 
-* :djadmin:`makemigrations` command now displays meaningful symbols for each
-  operation to highlight :class:`operation categories
+* The :djadmin:`makemigrations` command now displays meaningful symbols for
+  each operation to highlight :class:`operation categories
   <django.db.migrations.operations.base.OperationCategory>`.
 
 Migrations
@@ -237,28 +272,9 @@ Models
   reload a model's value. This can be used to lock the row before reloading or
   to select related objects.
 
-* The new ``"transaction_mode"`` option is now supported in :setting:`OPTIONS`
-  on SQLite to allow specifying the :ref:`sqlite-transaction-behavior`.
-
-Requests and Responses
-~~~~~~~~~~~~~~~~~~~~~~
-
-* ...
-
-Security
-~~~~~~~~
-
-* ...
-
-Serialization
-~~~~~~~~~~~~~
-
-* ...
-
-Signals
-~~~~~~~
-
-* ...
+* The new :attr:`.Expression.constraint_validation_compatible` attribute allows
+  specifying that the expression should be ignored during a constraint
+  validation.
 
 Templates
 ~~~~~~~~~
@@ -267,10 +283,8 @@ Templates
   be made available on the ``Template`` instance. Such data may be used, for
   example, by the template loader, or other template clients.
 
-* The new :ttag:`{% query_string %} <query_string>` template tag allows
-  changing a :class:`~django.http.QueryDict` instance for use in links, for
-  example, to generate a link to the next page while keeping any filtering
-  options in place.
+* :ref:`Template engines <field-checking>` now implement a ``check()`` method
+  that is already registered with the check framework.
 
 Tests
 ~~~~~
@@ -280,9 +294,6 @@ Tests
   :meth:`~django.test.SimpleTestCase.assertInHTML` assertions now add haystacks
   to assertion error messages.
 
-* The Django test runner now supports a ``--screenshots`` option to save
-  screenshots for Selenium tests.
-
 * The :class:`~django.test.RequestFactory`,
   :class:`~django.test.AsyncRequestFactory`, :class:`~django.test.Client`, and
   :class:`~django.test.AsyncClient` classes now support the ``query_params``
@@ -300,34 +311,19 @@ Tests
 * In order to enforce test isolation, database connections inside threads are
   no longer allowed in :class:`~django.test.SimpleTestCase`.
 
-URLs
-~~~~
-
-* ...
-
-Utilities
-~~~~~~~~~
-
-* ...
-
 Validators
 ~~~~~~~~~~
 
-* ...
+* The new :class:`~django.core.validators.DomainNameValidator` validates domain
+  names, including internationalized domain names. The new
+  :func:`~django.core.validators.validate_domain_name` function returns an
+  instance of :class:`~django.core.validators.DomainNameValidator`.
 
 .. _backwards-incompatible-5.1:
 
 Backwards incompatible changes in 5.1
 =====================================
 
-Database backend API
---------------------
-
-This section describes changes that may be needed in third-party database
-backends.
-
-* ...
-
 :mod:`django.contrib.gis`
 -------------------------
 
@@ -366,10 +362,38 @@ Miscellaneous
   a ``<footer>`` tag instead of a ``<div>``, and also moved below the
   ``<div id="main">`` element.
 
+* In order to improve accessibility, the expandable widget used for
+  :attr:`ModelAdmin.fieldsets <django.contrib.admin.ModelAdmin.fieldsets>` and
+  :attr:`InlineModelAdmin.fieldsets <django.contrib.admin.InlineModelAdmin>`,
+  when the fieldset has a name and use the ``collapse`` class, now includes
+  ``<details>`` and ``<summary>`` elements.
+
+* The JavaScript file ``collapse.js`` is removed since it is no longer needed
+  in the Django admin site.
+
 * :meth:`.SimpleTestCase.assertURLEqual` and
   :meth:`~django.test.SimpleTestCase.assertInHTML` now add ``": "`` to the
   ``msg_prefix``. This is consistent with the behavior of other assertions.
 
+* ``django.utils.text.Truncator`` used by :tfilter:`truncatechars_html` and
+  :tfilter:`truncatewords_html` template filters now uses
+  :py:class:`html.parser.HTMLParser` subclasses. This results in a more robust
+  and faster operation, but there may be small differences in the output.
+
+* The undocumented ``django.urls.converters.get_converter()`` function is
+  removed.
+
+* The minimum supported version of SQLite is increased from 3.27.0 to 3.31.0.
+
+* :class:`~django.db.models.FileField` now raises a
+  :class:`~django.core.exceptions.FieldError` when saving a file without a
+  ``name``.
+
+* ``ImageField.update_dimension_fields(force=True)`` is no longer called after
+  saving the image to storage. If your storage backend resizes images, the
+  ``width_field`` and ``height_field`` will not match the width and height of
+  the image.
+
 .. _deprecated-features-5.1:
 
 Features deprecated in 5.1
@@ -399,6 +423,21 @@ Miscellaneous
 * Setting ``django.contrib.gis.gdal.OGRGeometry.coord_dim`` is deprecated. Use
   :meth:`~django.contrib.gis.gdal.OGRGeometry.set_3d` instead.
 
+* Overriding existing converters with ``django.urls.register_converter()`` is
+  deprecated.
+
+* The ``check`` keyword argument of ``CheckConstraint`` is deprecated in favor
+  of ``condition``.
+
+* The undocumented ``OS_OPEN_FLAGS`` property of
+  :class:`~django.core.files.storage.FileSystemStorage` is deprecated. To allow
+  overwriting files in storage, set the new
+  :attr:`~django.core.files.storage.FileSystemStorage.allow_overwrite` option
+  to ``True`` instead.
+
+* The ``get_cache_name()`` method of ``FieldCacheMixin`` is deprecated in favor
+  of the ``cache_name`` cached property.
+
 Features removed in 5.1
 =======================
 
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt
new file mode 100644
index 0000000000..8b77ecc482
--- /dev/null
+++ b/docs/releases/5.2.txt
@@ -0,0 +1,282 @@
+============================================
+Django 5.2 release notes - UNDER DEVELOPMENT
+============================================
+
+*Expected April 2025*
+
+Welcome to Django 5.2!
+
+These release notes cover the :ref:`new features <whats-new-5.2>`, as well as
+some :ref:`backwards incompatible changes <backwards-incompatible-5.2>` you
+should be aware of when upgrading from Django 5.1 or earlier. We've
+:ref:`begun the deprecation process for some features
+<deprecated-features-5.2>`.
+
+See the :doc:`/howto/upgrade-version` guide if you're updating an existing
+project.
+
+Django 5.2 is designated as a :term:`long-term support release
+<Long-term support release>`. It will receive security updates for at least
+three years after its release. Support for the previous LTS, Django 4.2, will
+end in April 2026.
+
+Python compatibility
+====================
+
+Django 5.2 supports Python 3.10, 3.11, 3.12, and 3.13. We **highly recommend**
+and only officially support the latest release of each series.
+
+.. _whats-new-5.2:
+
+What's new in Django 5.2
+========================
+
+Minor features
+--------------
+
+:mod:`django.contrib.admin`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* The ``admin/base.html`` template now has a new block
+  :ref:`extrabody <extrabody>` for adding custom code before the closing
+  ``</body>`` tag.
+
+:mod:`django.contrib.admindocs`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.auth`
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* The default iteration count for the PBKDF2 password hasher is increased from
+  870,000 to 1,000,000.
+
+:mod:`django.contrib.contenttypes`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.gis`
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.messages`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.postgres`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.redirects`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.sessions`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.sitemaps`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.sites`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.staticfiles`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.syndication`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* All :class:`~django.utils.feedgenerator.SyndicationFeed` classes now support
+  a ``stylesheets`` attribute. If specified, an ``<? xml-stylesheet ?>``
+  processing instruction will be added to the top of the document for each
+  stylesheet in the given list. See :ref:`feed-stylesheets` for more details.
+
+Asynchronous views
+~~~~~~~~~~~~~~~~~~
+
+* ...
+
+Cache
+~~~~~
+
+* ...
+
+CSRF
+~~~~
+
+* ...
+
+Database backends
+~~~~~~~~~~~~~~~~~
+
+* ...
+
+Decorators
+~~~~~~~~~~
+
+* ...
+
+Email
+~~~~~
+
+* Tuple items of :class:`EmailMessage.attachments
+  <django.core.mail.EmailMessage>` and
+  :class:`EmailMultiAlternatives.attachments
+  <django.core.mail.EmailMultiAlternatives>` are now named tuples, as opposed
+  to regular tuples.
+
+* :attr:`EmailMultiAlternatives.alternatives
+  <django.core.mail.EmailMultiAlternatives.alternatives>` is now a list of
+  named tuples, as opposed to regular tuples.
+
+* The new :meth:`~django.core.mail.EmailMultiAlternatives.body_contains` method
+  returns a boolean indicating whether a provided text is contained in the
+  email ``body`` and in all attached MIME type ``text/*`` alternatives.
+
+Error Reporting
+~~~~~~~~~~~~~~~
+
+* ...
+
+File Storage
+~~~~~~~~~~~~
+
+* ...
+
+File Uploads
+~~~~~~~~~~~~
+
+* ...
+
+Forms
+~~~~~
+
+* ...
+
+Generic Views
+~~~~~~~~~~~~~
+
+* ...
+
+Internationalization
+~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+Logging
+~~~~~~~
+
+* ...
+
+Management Commands
+~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+Migrations
+~~~~~~~~~~
+
+* ...
+
+Models
+~~~~~~
+
+* ...
+
+Requests and Responses
+~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+Security
+~~~~~~~~
+
+* ...
+
+Serialization
+~~~~~~~~~~~~~
+
+* ...
+
+Signals
+~~~~~~~
+
+* ...
+
+Templates
+~~~~~~~~~
+
+* ...
+
+Tests
+~~~~~
+
+* ...
+
+URLs
+~~~~
+
+* ...
+
+Utilities
+~~~~~~~~~
+
+* ...
+
+Validators
+~~~~~~~~~~
+
+* ...
+
+.. _backwards-incompatible-5.2:
+
+Backwards incompatible changes in 5.2
+=====================================
+
+Database backend API
+--------------------
+
+This section describes changes that may be needed in third-party database
+backends.
+
+* ...
+
+:mod:`django.contrib.gis`
+-------------------------
+
+* Support for PostGIS 3.0 is removed.
+
+Dropped support for PostgreSQL 13
+---------------------------------
+
+Upstream support for PostgreSQL 13 ends in November 2025. Django 5.2 supports
+PostgreSQL 14 and higher.
+
+Miscellaneous
+-------------
+
+* ...
+
+.. _deprecated-features-5.2:
+
+Features deprecated in 5.2
+==========================
+
+Miscellaneous
+-------------
+
+* ...
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index db0741a136..820456fa7a 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -20,6 +20,13 @@ versions of the documentation contain the release notes for any later releases.
 
 .. _development_release_notes:
 
+5.2 release
+-----------
+.. toctree::
+   :maxdepth: 1
+
+   5.2
+
 5.1 release
 -----------
 .. toctree::
@@ -32,6 +39,11 @@ versions of the documentation contain the release notes for any later releases.
 .. toctree::
    :maxdepth: 1
 
+   5.0.7
+   5.0.6
+   5.0.5
+   5.0.4
+   5.0.3
    5.0.2
    5.0.1
    5.0
@@ -42,6 +54,9 @@ versions of the documentation contain the release notes for any later releases.
 .. toctree::
    :maxdepth: 1
 
+   4.2.13
+   4.2.12
+   4.2.11
    4.2.10
    4.2.9
    4.2.8
@@ -96,6 +111,7 @@ versions of the documentation contain the release notes for any later releases.
 .. toctree::
    :maxdepth: 1
 
+   3.2.25
    3.2.24
    3.2.23
    3.2.22
diff --git a/docs/releases/security.txt b/docs/releases/security.txt
index cf63dafa0d..5ded7966f1 100644
--- a/docs/releases/security.txt
+++ b/docs/releases/security.txt
@@ -36,6 +36,28 @@ Issues under Django's security process
 All security issues have been handled under versions of Django's security
 process. These are listed below.
 
+March 4, 2024 - :cve:`2024-27351`
+---------------------------------
+
+Potential regular expression denial-of-service in
+``django.utils.text.Truncator.words()``. `Full description
+<https://www.djangoproject.com/weblog/2024/mar/04/security-releases/>`__
+
+* Django 5.0 :commit:`(patch) <3394fc6132436eca89e997083bae9985fb7e761e>`
+* Django 4.2 :commit:`(patch) <3c9a2771cc80821e041b16eb36c1c37af5349d4a>`
+* Django 3.2 :commit:`(patch) <072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521>`
+
+February 6, 2024 - :cve:`2024-24680`
+------------------------------------
+
+Potential denial-of-service in ``intcomma`` template filter.
+`Full description
+<https://www.djangoproject.com/weblog/2024/feb/06/security-releases/>`__
+
+* Django 5.0 :commit:`(patch) <16a8fe18a3b81250f4fa57e3f93f0599dc4895bc>`
+* Django 4.2 :commit:`(patch) <572ea07e84b38ea8de0551f4b4eda685d91d09d2>`
+* Django 3.2 :commit:`(patch) <c1171ffbd570db90ca206c30f8e2b9f691243820>`
+
 November 1, 2023 - :cve:`2023-46695`
 ------------------------------------
 
@@ -1359,7 +1381,7 @@ Versions affected
 
 * Django 1.2 :commit:`(patch) <7f84657b6b2243cc787bdb9f296710c8d13ad0bd>`
 
-October 9, 2009 - :cve:`2009-3965`
+October 9, 2009 - :cve:`2009-3695`
 ----------------------------------
 
 Denial-of-service via pathological regular expression performance. `Full
diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist
index 5828b24253..1044cd80eb 100644
--- a/docs/spelling_wordlist
+++ b/docs/spelling_wordlist
@@ -361,6 +361,7 @@ postfix
 postgis
 postgres
 postgresql
+pragma
 pre
 precisions
 precomputation
diff --git a/docs/topics/async.txt b/docs/topics/async.txt
index 87550ff46d..a289344f6b 100644
--- a/docs/topics/async.txt
+++ b/docs/topics/async.txt
@@ -76,8 +76,6 @@ corruption.
 Decorators
 ----------
 
-.. versionadded:: 5.0
-
 The following decorators can be used with both synchronous and asynchronous
 view functions:
 
@@ -181,8 +179,6 @@ mode if you have asynchronous code in your project.
 Handling disconnects
 --------------------
 
-.. versionadded:: 5.0
-
 For long-lived requests, a client may disconnect before the view returns a
 response. In this case, an ``asyncio.CancelledError`` will be raised in the
 view. You can catch this error and handle it if you need to perform any
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index 52fa3515b8..3f8be983f8 100644
--- a/docs/topics/auth/customizing.txt
+++ b/docs/topics/auth/customizing.txt
@@ -703,10 +703,6 @@ The following attributes and methods are available on any subclass of
         the user. (This takes care of the password hashing in making the
         comparison.)
 
-        .. versionchanged:: 5.0
-
-            ``acheck_password()`` method was added.
-
     .. method:: models.AbstractBaseUser.set_unusable_password()
 
         Marks the user as having no password set.  This isn't the same as
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index de2bc51cda..56f867ede5 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -155,10 +155,6 @@ Authenticating users
         this. Rather if you're looking for a way to login a user, use the
         :class:`~django.contrib.auth.views.LoginView`.
 
-    .. versionchanged:: 5.0
-
-        ``aauthenticate()`` function was added.
-
 .. _topic-authorization:
 
 Permissions and Authorization
@@ -401,10 +397,6 @@ Or in an asynchronous view::
         # Do something for anonymous users.
         ...
 
-.. versionchanged:: 5.0
-
-    The :meth:`.HttpRequest.auser` method was added.
-
 .. _how-to-log-a-user-in:
 
 How to log a user in
@@ -446,10 +438,6 @@ If you have an authenticated user you want to attach to the current session
                 # Return an 'invalid login' error message.
                 ...
 
-    .. versionchanged:: 5.0
-
-        ``alogin()`` function was added.
-
 Selecting the authentication backend
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -505,10 +493,6 @@ How to log a user out
     immediately after logging out, do that *after* calling
     :func:`django.contrib.auth.logout()`.
 
-    .. versionchanged:: 5.0
-
-        ``alogout()`` function was added.
-
 Limiting access to logged-in users
 ----------------------------------
 
@@ -617,6 +601,10 @@ The ``login_required`` decorator
     :func:`django.contrib.admin.views.decorators.staff_member_required`
     decorator a useful alternative to ``login_required()``.
 
+.. versionchanged:: 5.1
+
+    Support for wrapping asynchronous view functions was added.
+
 .. currentmodule:: django.contrib.auth.mixins
 
 The ``LoginRequiredMixin`` mixin
@@ -652,8 +640,25 @@ inheritance list.
     ``is_active`` flag on a user, but the default
     :setting:`AUTHENTICATION_BACKENDS` reject inactive users.
 
+.. _disable-login-required-middleware-for-views:
+
 .. currentmodule:: django.contrib.auth.decorators
 
+The ``login_not_required`` decorator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 5.1
+
+When :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware` is
+installed, all views require authentication by default. Some views, such as the
+login view, may need to disable this behavior.
+
+.. function:: login_not_required()
+
+    Allows unauthenticated requests without redirecting to the login page when
+    :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware` is
+    installed.
+
 Limiting access to logged-in users that pass a test
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -714,6 +719,11 @@ email in the desired domain and if not, redirects to the login page::
         @user_passes_test(email_check, login_url="/login/")
         def my_view(request): ...
 
+    .. versionchanged:: 5.1
+
+        Support for wrapping asynchronous view functions and using asynchronous
+        test callables was added.
+
 .. currentmodule:: django.contrib.auth.mixins
 
 .. class:: UserPassesTestMixin
@@ -818,6 +828,10 @@ The ``permission_required`` decorator
     ``redirect_authenticated_user=True`` and the logged-in user doesn't have
     all of the required permissions.
 
+.. versionchanged:: 5.1
+
+    Support for wrapping asynchronous view functions was added.
+
 .. currentmodule:: django.contrib.auth.mixins
 
 The ``PermissionRequiredMixin`` mixin
@@ -970,10 +984,6 @@ function.
             else:
                 ...
 
-    .. versionchanged:: 5.0
-
-        ``aupdate_session_auth_hash()`` function was added.
-
 .. note::
 
     Since
@@ -1623,10 +1633,18 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
 
 .. class:: AdminPasswordChangeForm
 
-    A form used in the admin interface to change a user's password.
+    A form used in the admin interface to change a user's password, including
+    the ability to set an :meth:`unusable password
+    <django.contrib.auth.models.User.set_unusable_password>`, which blocks the
+    user from logging in with password-based authentication.
 
     Takes the ``user`` as the first positional argument.
 
+    .. versionchanged:: 5.1
+
+        Option to disable (or reenable) password-based authentication was
+        added.
+
 .. class:: AuthenticationForm
 
     A form for logging a user in.
@@ -1717,12 +1735,21 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
     A :class:`~django.forms.ModelForm` for creating a new user. This is the
     recommended base class if you need to customize the user creation form.
 
-    It has three fields: ``username`` (from the user model), ``password1``,
-    and ``password2``. It verifies that ``password1`` and ``password2`` match,
-    validates the password using
+    It has four fields: ``username`` (from the user model), ``password1``,
+    ``password2``, and ``usable_password`` (the latter is enabled by default).
+    If ``usable_password`` is enabled, it verifies that ``password1`` and
+    ``password2`` are non empty and match, validates the password using
     :func:`~django.contrib.auth.password_validation.validate_password`, and
     sets the user's password using
     :meth:`~django.contrib.auth.models.User.set_password()`.
+    If ``usable_password`` is disabled, no password validation is done, and
+    password-based authentication is disabled for the user by calling
+    :meth:`~django.contrib.auth.models.User.set_unusable_password()`.
+
+    .. versionchanged:: 5.1
+
+        Option to create users with disabled password-based authentication was
+        added.
 
 .. class:: UserCreationForm
 
@@ -1837,6 +1864,8 @@ You should see a link to "Users" in the "Auth"
 section of the main admin index page. The "Add user" admin page is different
 than standard admin pages in that it requires you to choose a username and
 password before allowing you to edit the rest of the user's fields.
+Alternatively, on this page, you can choose a username and disable
+password-based authentication for the user.
 
 Also note: if you want a user account to be able to create users using the
 Django admin site, you'll need to give them permission to add users *and*
@@ -1858,4 +1887,4 @@ Changing passwords
 User passwords are not displayed in the admin (nor stored in the database), but
 the :doc:`password storage details </topics/auth/passwords>` are displayed.
 Included in the display of this information is a link to
-a password change form that allows admins to change user passwords.
+a password change form that allows admins to change or unset user passwords.
diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
index 54a5e069d0..4d5f845a57 100644
--- a/docs/topics/auth/passwords.txt
+++ b/docs/topics/auth/passwords.txt
@@ -493,10 +493,6 @@ from the ``User`` model.
     to use the default (first entry of ``PASSWORD_HASHERS`` setting). See
     :ref:`auth-included-hashers` for the algorithm name of each hasher.
 
-    .. versionchanged:: 5.0
-
-        ``acheck_password()`` method was added.
-
 .. function:: make_password(password, salt=None, hasher='default')
 
     Creates a hashed password in the format used by this application. It takes
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index 3d67f9b03a..1fe9d335fb 100644
--- a/docs/topics/cache.txt
+++ b/docs/topics/cache.txt
@@ -562,10 +562,8 @@ flag on the connection's socket::
     }
 
 Here's an example configuration for a ``redis`` based backend that selects
-database ``10`` (by default Redis ships with 16 logical databases), specifies a
-`parser class`_ (``redis.connection.HiredisParser`` will be used by default if
-the ``hiredis-py`` package is installed), and sets a custom `connection pool
-class`_ (``redis.ConnectionPool`` is used by default)::
+database ``10`` (by default Redis ships with 16 logical databases), and sets a
+custom `connection pool class`_ (``redis.ConnectionPool`` is used by default)::
 
     CACHES = {
         "default": {
@@ -573,13 +571,11 @@ class`_ (``redis.ConnectionPool`` is used by default)::
             "LOCATION": "redis://127.0.0.1:6379",
             "OPTIONS": {
                 "db": "10",
-                "parser_class": "redis.connection.PythonParser",
                 "pool_class": "redis.BlockingConnectionPool",
             },
         }
     }
 
-.. _`parser class`: https://github.com/redis/redis-py#parsers
 .. _`connection pool class`: https://github.com/redis/redis-py#connection-pools
 
 .. _the-per-site-cache:
diff --git a/docs/topics/checks.txt b/docs/topics/checks.txt
index 3e3bbe19d6..94ba66f0db 100644
--- a/docs/topics/checks.txt
+++ b/docs/topics/checks.txt
@@ -130,18 +130,18 @@ The code below is equivalent to the code above::
 
 .. _field-checking:
 
-Field, model, manager, and database checks
-------------------------------------------
+Field, model, manager, template engine, and database checks
+-----------------------------------------------------------
 
 In some cases, you won't need to register your check function -- you can
 piggyback on an existing registration.
 
-Fields, models, model managers, and database backends all implement a
-``check()`` method that is already registered with the check framework. If you
-want to add extra checks, you can extend the implementation on the base class,
-perform any extra checks you need, and append any messages to those generated
-by the base class. It's recommended that you delegate each check to separate
-methods.
+Fields, models, model managers, template engines, and database backends all
+implement a ``check()`` method that is already registered with the check
+framework. If you want to add extra checks, you can extend the implementation
+on the base class, perform any extra checks you need, and append any messages
+to those generated by the base class. It's recommended that you delegate each
+check to separate methods.
 
 Consider an example where you are implementing a custom field named
 ``RangedIntegerField``. This field adds ``min`` and ``max`` arguments to the
@@ -195,6 +195,10 @@ the only difference is that the check is a classmethod, not an instance method::
             # ... your own checks ...
             return errors
 
+.. versionchanged:: 5.1
+
+    In older versions, template engines didn't implement a ``check()`` method.
+
 Writing tests
 -------------
 
diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt
index 244e9bbb16..aefb35ed9c 100644
--- a/docs/topics/db/models.txt
+++ b/docs/topics/db/models.txt
@@ -219,10 +219,6 @@ ones:
     Further examples are available in the :ref:`model field reference
     <field-choices>`.
 
-    .. versionchanged:: 5.0
-
-        Support for mappings and callables was added.
-
 :attr:`~Field.default`
     The default value for the field. This can be a value or a callable
     object. If callable it will be called every time a new object is
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
index 6f2c22c297..e5d4f277f5 100644
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -282,8 +282,13 @@ All parameters are optional and can be set at any time prior to calling the
   new connection is created when ``send()`` is called.
 
 * ``attachments``: A list of attachments to put on the message. These can
-  be either :class:`~email.mime.base.MIMEBase` instances, or ``(filename,
-  content, mimetype)`` triples.
+  be either :class:`~email.mime.base.MIMEBase` instances, or a named tuple
+  with attributes ``(filename, content, mimetype)``.
+
+  .. versionchanged:: 5.2
+
+    In older versions, tuple items of ``attachments`` were regular tuples,
+    as opposed to named tuples.
 
 * ``headers``: A dictionary of extra headers to put on the message. The
   keys are the header name, values are the header values. It's up to the
@@ -380,26 +385,79 @@ The class has the following methods:
   ``attach()``.
 
 Sending alternative content types
+---------------------------------
+
+Sending multiple content versions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 It can be useful to include multiple versions of the content in an email; the
 classic example is to send both text and HTML versions of a message. With
-Django's email library, you can do this using the ``EmailMultiAlternatives``
-class. This subclass of :class:`~django.core.mail.EmailMessage` has an
-``attach_alternative()`` method for including extra versions of the message
-body in the email. All the other methods (including the class initialization)
-are inherited directly from :class:`~django.core.mail.EmailMessage`.
+Django's email library, you can do this using the
+:class:`~django.core.mail.EmailMultiAlternatives` class.
 
-To send a text and HTML combination, you could write::
+.. class:: EmailMultiAlternatives
 
-    from django.core.mail import EmailMultiAlternatives
+    A subclass of :class:`EmailMessage` that allows additional versions of the
+    message body in the email via the :meth:`attach_alternative` method. This
+    directly inherits all methods (including the class initialization) from
+    :class:`EmailMessage`.
 
-    subject, from_email, to = "hello", "from@example.com", "to@example.com"
-    text_content = "This is an important message."
-    html_content = "<p>This is an <strong>important</strong> message.</p>"
-    msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
-    msg.attach_alternative(html_content, "text/html")
-    msg.send()
+    .. attribute:: alternatives
+
+        A list of named tuples with attributes ``(content, mimetype)``. This is
+        particularly useful in tests::
+
+            self.assertEqual(len(msg.alternatives), 1)
+            self.assertEqual(msg.alternatives[0].content, html_content)
+            self.assertEqual(msg.alternatives[0].mimetype, "text/html")
+
+        Alternatives should only be added using the :meth:`attach_alternative`
+        method.
+
+        .. versionchanged:: 5.2
+
+            In older versions, ``alternatives`` was a list of regular tuples,
+            as opposed to named tuples.
+
+    .. method:: attach_alternative(content, mimetype)
+
+        Attach an alternative representation of the message body in the email.
+
+        For example, to send a text and HTML combination, you could write::
+
+            from django.core.mail import EmailMultiAlternatives
+
+            subject = "hello"
+            from_email = "from@example.com"
+            to = "to@example.com"
+            text_content = "This is an important message."
+            html_content = "<p>This is an <strong>important</strong> message.</p>"
+            msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
+            msg.attach_alternative(html_content, "text/html")
+            msg.send()
+
+    .. method:: body_contains(text)
+
+        .. versionadded:: 5.2
+
+        Returns a boolean indicating whether the provided ``text`` is
+        contained in the email ``body`` and in all attached MIME type
+        ``text/*`` alternatives.
+
+        This can be useful when testing emails. For example::
+
+            def test_contains_email_content(self):
+                subject = "Hello World"
+                from_email = "from@example.com"
+                to = "to@example.com"
+                msg = EmailMultiAlternatives(subject, "I am content.", from_email, [to])
+                msg.attach_alternative("<p>I am content.</p>", "text/html")
+
+                self.assertIs(msg.body_contains("I am content"), True)
+                self.assertIs(msg.body_contains("<p>I am content.</p>"), False)
+
+Updating the default content type
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 By default, the MIME type of the ``body`` parameter in an
 :class:`~django.core.mail.EmailMessage` is ``"text/plain"``. It is good
diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt
index d09a1a063f..1f49044e6e 100644
--- a/docs/topics/forms/formsets.txt
+++ b/docs/topics/forms/formsets.txt
@@ -432,16 +432,18 @@ If ``validate_max=True`` is passed to
 that the number of forms in the data set, minus those marked for
 deletion, is less than or equal to ``max_num``.
 
+.. code-block:: pycon
+
     >>> from django.forms import formset_factory
     >>> from myapp.forms import ArticleForm
     >>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)
     >>> data = {
-    ...     'form-TOTAL_FORMS': '2',
-    ...     'form-INITIAL_FORMS': '0',
-    ...     'form-0-title': 'Test',
-    ...     'form-0-pub_date': '1904-06-16',
-    ...     'form-1-title': 'Test 2',
-    ...     'form-1-pub_date': '1912-06-23',
+    ...     "form-TOTAL_FORMS": "2",
+    ...     "form-INITIAL_FORMS": "0",
+    ...     "form-0-title": "Test",
+    ...     "form-0-pub_date": "1904-06-16",
+    ...     "form-1-title": "Test 2",
+    ...     "form-1-pub_date": "1912-06-23",
     ... }
     >>> formset = ArticleFormSet(data)
     >>> formset.is_valid()
@@ -475,16 +477,18 @@ If ``validate_min=True`` is passed to
 that the number of forms in the data set, minus those marked for
 deletion, is greater than or equal to ``min_num``.
 
+.. code-block:: pycon
+
     >>> from django.forms import formset_factory
     >>> from myapp.forms import ArticleForm
     >>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
     >>> data = {
-    ...     'form-TOTAL_FORMS': '2',
-    ...     'form-INITIAL_FORMS': '0',
-    ...     'form-0-title': 'Test',
-    ...     'form-0-pub_date': '1904-06-16',
-    ...     'form-1-title': 'Test 2',
-    ...     'form-1-pub_date': '1912-06-23',
+    ...     "form-TOTAL_FORMS": "2",
+    ...     "form-INITIAL_FORMS": "0",
+    ...     "form-0-title": "Test",
+    ...     "form-0-pub_date": "1904-06-16",
+    ...     "form-1-title": "Test 2",
+    ...     "form-1-pub_date": "1912-06-23",
     ... }
     >>> formset = ArticleFormSet(data)
     >>> formset.is_valid()
diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
index 55d032c9bb..71d443f7d1 100644
--- a/docs/topics/forms/index.txt
+++ b/docs/topics/forms/index.txt
@@ -564,8 +564,6 @@ See :ref:`ref-forms-api-outputting-html` for more details.
 Reusable field group templates
 ------------------------------
 
-.. versionadded:: 5.0
-
 Each field is available as an attribute of the form, using
 ``{{ form.name_of_field }}`` in a template. A field has a
 :meth:`~django.forms.BoundField.as_field_group` method which renders the
@@ -781,12 +779,10 @@ Useful attributes on ``{{ field }}`` include:
         <label for="id_email">Email address:</label>
 
 ``{{ field.legend_tag }}``
-
     Similar to ``field.label_tag`` but uses a ``<legend>`` tag in place of
     ``<label>``, for widgets with multiple inputs wrapped in a ``<fieldset>``.
 
 ``{{ field.use_fieldset }}``
-
     This attribute is ``True`` if the form field's widget contains multiple
     inputs that should be semantically grouped in a ``<fieldset>`` with a
     ``<legend>`` to improve accessibility. An example use in a template:
diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt
index 6e7bfdcbd4..3fddf2d4bb 100644
--- a/docs/topics/forms/media.txt
+++ b/docs/topics/forms/media.txt
@@ -287,7 +287,7 @@ outputting the complete HTML ``<script>`` or ``<link>`` tag content:
     >>> @html_safe
     ... class JSPath:
     ...     def __str__(self):
-    ...         return '<script src="https://example.org/asset.js" rel="stylesheet">'
+    ...         return '<script src="https://example.org/asset.js" defer>'
     ...
 
     >>> class SomeWidget(forms.TextInput):
diff --git a/docs/topics/http/decorators.txt b/docs/topics/http/decorators.txt
index fa84e8d9b7..1c48e667f1 100644
--- a/docs/topics/http/decorators.txt
+++ b/docs/topics/http/decorators.txt
@@ -33,26 +33,14 @@ a :class:`django.http.HttpResponseNotAllowed` if the conditions are not met.
 
     Note that request methods should be in uppercase.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: require_GET()
 
     Decorator to require that a view only accepts the GET method.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: require_POST()
 
     Decorator to require that a view only accepts the POST method.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: require_safe()
 
     Decorator to require that a view only accepts the GET and HEAD methods.
@@ -67,10 +55,6 @@ a :class:`django.http.HttpResponseNotAllowed` if the conditions are not met.
         such as link checkers, rely on HEAD requests, you might prefer
         using ``require_safe`` instead of ``require_GET``.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 Conditional view processing
 ===========================
 
@@ -79,6 +63,11 @@ control caching behavior on particular views.
 
 .. function:: condition(etag_func=None, last_modified_func=None)
 
+.. function:: conditional_page()
+
+    This decorator provides the conditional GET operation handling of
+    :class:`~django.middleware.http.ConditionalGetMiddleware` to a view.
+
 .. function:: etag(etag_func)
 
 .. function:: last_modified(last_modified_func)
@@ -87,10 +76,6 @@ control caching behavior on particular views.
     headers; see
     :doc:`conditional view processing </topics/conditional-view-processing>`.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. module:: django.views.decorators.gzip
 
 GZip compression
@@ -105,10 +90,6 @@ compression on a per-view basis.
     It sets the ``Vary`` header accordingly, so that caches will base their
     storage on the ``Accept-Encoding`` header.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. module:: django.views.decorators.vary
 
 Vary headers
@@ -119,10 +100,6 @@ caching based on specific request headers.
 
 .. function:: vary_on_cookie(func)
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: vary_on_headers(*headers)
 
     The ``Vary`` header defines which request headers a cache mechanism should take
@@ -130,10 +107,6 @@ caching based on specific request headers.
 
     See :ref:`using vary headers <using-vary-headers>`.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. module:: django.views.decorators.cache
 
 Caching
@@ -149,10 +122,6 @@ client-side caching.
     :func:`~django.utils.cache.patch_cache_control` for the details of the
     transformation.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. function:: never_cache(view_func)
 
     This decorator adds an ``Expires`` header to the current date/time.
@@ -163,10 +132,6 @@ client-side caching.
 
     Each header is only added if it isn't already set.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
-
 .. module:: django.views.decorators.common
 
 Common
@@ -180,6 +145,3 @@ customization of :class:`~django.middleware.common.CommonMiddleware` behavior.
     This decorator allows individual views to be excluded from
     :setting:`APPEND_SLASH` URL normalization.
 
-    .. versionchanged:: 5.0
-
-        Support for wrapping asynchronous view functions was added.
diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt
index 911c282157..b9a9bbf416 100644
--- a/docs/topics/http/file-uploads.txt
+++ b/docs/topics/http/file-uploads.txt
@@ -154,7 +154,7 @@ Uploading multiple files
     should be updated after any changes in the following snippets.
 
 If you want to upload multiple files using one form field, create a subclass
-of the field's widget and set the ``allow_multiple_selected`` attribute on it
+of the field's widget and set its ``allow_multiple_selected`` class attribute
 to ``True``.
 
 In order for such files to be all validated by your form (and have the value of
@@ -186,14 +186,14 @@ below for an example.
             if isinstance(data, (list, tuple)):
                 result = [single_file_clean(d, initial) for d in data]
             else:
-                result = single_file_clean(data, initial)
+                result = [single_file_clean(data, initial)]
             return result
 
 
     class FileFieldForm(forms.Form):
         file_field = MultipleFileField()
 
-Then override the ``post`` method of your
+Then override the ``form_valid()`` method of your
 :class:`~django.views.generic.edit.FormView` subclass to handle multiple file
 uploads:
 
@@ -209,19 +209,11 @@ uploads:
         template_name = "upload.html"  # Replace with your template.
         success_url = "..."  # Replace with your URL or reverse().
 
-        def post(self, request, *args, **kwargs):
-            form_class = self.get_form_class()
-            form = self.get_form(form_class)
-            if form.is_valid():
-                return self.form_valid(form)
-            else:
-                return self.form_invalid(form)
-
         def form_valid(self, form):
             files = form.cleaned_data["file_field"]
             for f in files:
                 ...  # Do something with each file.
-            return super().form_valid()
+            return super().form_valid(form)
 
 .. warning::
 
diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt
index 4f635f1704..e670292ca8 100644
--- a/docs/topics/http/sessions.txt
+++ b/docs/topics/http/sessions.txt
@@ -76,9 +76,17 @@ Once your cache is configured, you have to choose between a database-backed
 cache or a non-persistent cache.
 
 The cached database backend (``cached_db``) uses a write-through cache --
-session writes are applied to both the cache and the database. Session reads
-use the cache, or the database if the data has been evicted from the cache. To
-use this backend, set :setting:`SESSION_ENGINE` to
+session writes are applied to both the database and cache, in that order. If
+writing to the cache fails, the exception is handled and logged via the
+:ref:`sessions logger <django-contrib-sessions-logger>`, to avoid failing an
+otherwise successful write operation.
+
+.. versionchanged:: 5.1
+
+    Handling and logging of exceptions when writing to the cache was added.
+
+Session reads use the cache, or the database if the data has been evicted from
+the cache. To use this backend, set :setting:`SESSION_ENGINE` to
 ``"django.contrib.sessions.backends.cached_db"``, and follow the configuration
 instructions for the `using database-backed sessions`_.
 
@@ -188,54 +196,156 @@ You can edit it multiple times.
       Example: ``'fav_color' in request.session``
 
     .. method:: get(key, default=None)
+    .. method:: aget(key, default=None)
+
+      *Asynchronous version*: ``aget()``
 
       Example: ``fav_color = request.session.get('fav_color', 'red')``
 
+      .. versionchanged:: 5.1
+
+        ``aget()`` function was added.
+
+    .. method:: aset(key, value)
+
+      .. versionadded:: 5.1
+
+      Example: ``await request.session.aset('fav_color', 'red')``
+
+    .. method:: update(dict)
+    .. method:: aupdate(dict)
+
+      *Asynchronous version*: ``aupdate()``
+
+      Example: ``request.session.update({'fav_color': 'red'})``
+
+      .. versionchanged:: 5.1
+
+        ``aupdate()`` function was added.
+
     .. method:: pop(key, default=__not_given)
+    .. method:: apop(key, default=__not_given)
+
+      *Asynchronous version*: ``apop()``
 
       Example: ``fav_color = request.session.pop('fav_color', 'blue')``
 
+      .. versionchanged:: 5.1
+
+        ``apop()`` function was added.
+
     .. method:: keys()
+    .. method:: akeys()
+
+      *Asynchronous version*: ``akeys()``
+
+      .. versionchanged:: 5.1
+
+        ``akeys()`` function was added.
+
+    .. method:: values()
+    .. method:: avalues()
+
+      *Asynchronous version*: ``avalues()``
+
+      .. versionchanged:: 5.1
+
+        ``avalues()`` function was added.
+
+    .. method:: has_key(key)
+    .. method:: ahas_key(key)
+
+      *Asynchronous version*: ``ahas_key()``
+
+      .. versionchanged:: 5.1
+
+        ``ahas_key()`` function was added.
 
     .. method:: items()
+    .. method:: aitems()
+
+      *Asynchronous version*: ``aitems()``
+
+      .. versionchanged:: 5.1
+
+        ``aitems()`` function was added.
 
     .. method:: setdefault()
+    .. method:: asetdefault()
+
+      *Asynchronous version*: ``asetdefault()``
+
+      .. versionchanged:: 5.1
+
+        ``asetdefault()`` function was added.
 
     .. method:: clear()
 
     It also has these methods:
 
     .. method:: flush()
+    .. method:: aflush()
+
+      *Asynchronous version*: ``aflush()``
 
       Deletes the current session data from the session and deletes the session
       cookie. This is used if you want to ensure that the previous session data
       can't be accessed again from the user's browser (for example, the
       :func:`django.contrib.auth.logout()` function calls it).
 
+      .. versionchanged:: 5.1
+
+        ``aflush()`` function was added.
+
     .. method:: set_test_cookie()
+    .. method:: aset_test_cookie()
+
+      *Asynchronous version*: ``aset_test_cookie()``
 
       Sets a test cookie to determine whether the user's browser supports
       cookies. Due to the way cookies work, you won't be able to test this
       until the user's next page request. See `Setting test cookies`_ below for
       more information.
 
+      .. versionchanged:: 5.1
+
+        ``aset_test_cookie()`` function was added.
+
     .. method:: test_cookie_worked()
+    .. method:: atest_cookie_worked()
+
+      *Asynchronous version*: ``atest_cookie_worked()``
 
       Returns either ``True`` or ``False``, depending on whether the user's
       browser accepted the test cookie. Due to the way cookies work, you'll
-      have to call ``set_test_cookie()`` on a previous, separate page request.
+      have to call ``set_test_cookie()`` or ``aset_test_cookie()`` on a
+      previous, separate page request.
       See `Setting test cookies`_ below for more information.
 
+      .. versionchanged:: 5.1
+
+        ``atest_cookie_worked()`` function was added.
+
     .. method:: delete_test_cookie()
+    .. method:: adelete_test_cookie()
+
+      *Asynchronous version*: ``adelete_test_cookie()``
 
       Deletes the test cookie. Use this to clean up after yourself.
 
+      .. versionchanged:: 5.1
+
+        ``adelete_test_cookie()`` function was added.
+
     .. method:: get_session_cookie_age()
 
       Returns the value of the setting :setting:`SESSION_COOKIE_AGE`. This can
       be overridden in a custom session backend.
 
     .. method:: set_expiry(value)
+    .. method:: aset_expiry(value)
+
+      *Asynchronous version*: ``aset_expiry()``
 
       Sets the expiration time for the session. You can pass a number of
       different values:
@@ -258,7 +368,14 @@ You can edit it multiple times.
       purposes. Session expiration is computed from the last time the
       session was *modified*.
 
+      .. versionchanged:: 5.1
+
+        ``aset_expiry()`` function was added.
+
     .. method:: get_expiry_age()
+    .. method:: aget_expiry_age()
+
+      *Asynchronous version*: ``aget_expiry_age()``
 
       Returns the number of seconds until this session expires. For sessions
       with no custom expiration (or those set to expire at browser close), this
@@ -271,7 +388,7 @@ You can edit it multiple times.
       - ``expiry``: expiry information for the session, as a
         :class:`~datetime.datetime` object, an :class:`int` (in seconds), or
         ``None``. Defaults to the value stored in the session by
-        :meth:`set_expiry`, if there is one, or ``None``.
+        :meth:`set_expiry`/:meth:`aset_expiry`, if there is one, or ``None``.
 
       .. note::
 
@@ -287,7 +404,14 @@ You can edit it multiple times.
 
             expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
 
+      .. versionchanged:: 5.1
+
+        ``aget_expiry_age()`` function was added.
+
     .. method:: get_expiry_date()
+    .. method:: aget_expiry_date()
+
+      *Asynchronous version*: ``aget_expiry_date()``
 
       Returns the date this session will expire. For sessions with no custom
       expiration (or those set to expire at browser close), this will equal the
@@ -296,22 +420,47 @@ You can edit it multiple times.
       This function accepts the same keyword arguments as
       :meth:`get_expiry_age`, and similar notes on usage apply.
 
+      .. versionchanged:: 5.1
+
+        ``aget_expiry_date()`` function was added.
+
     .. method:: get_expire_at_browser_close()
+    .. method:: aget_expire_at_browser_close()
+
+      *Asynchronous version*: ``aget_expire_at_browser_close()``
 
       Returns either ``True`` or ``False``, depending on whether the user's
       session cookie will expire when the user's web browser is closed.
 
+      .. versionchanged:: 5.1
+
+        ``aget_expire_at_browser_close()`` function was added.
+
     .. method:: clear_expired()
+    .. method:: aclear_expired()
+
+      *Asynchronous version*: ``aclear_expired()``
 
       Removes expired sessions from the session store. This class method is
       called by :djadmin:`clearsessions`.
 
+      .. versionchanged:: 5.1
+
+        ``aclear_expired()`` function was added.
+
     .. method:: cycle_key()
+    .. method:: acycle_key()
+
+      *Asynchronous version*: ``acycle_key()``
 
       Creates a new session key while retaining the current session data.
       :func:`django.contrib.auth.login()` calls this method to mitigate against
       session fixation.
 
+      .. versionchanged:: 5.1
+
+        ``acycle_key()`` function was added.
+
 .. _session_serialization:
 
 Session serialization
@@ -467,6 +616,10 @@ Here's a typical usage example::
         request.session.set_test_cookie()
         return render(request, "foo/login_form.html")
 
+.. versionchanged:: 5.1
+
+    Support for setting test cookies in asynchronous view functions was added.
+
 Using sessions out of views
 ===========================
 
@@ -686,16 +839,26 @@ the corresponding session engine. By convention, the session store object class
 is named ``SessionStore`` and is located in the module designated by
 :setting:`SESSION_ENGINE`.
 
-All ``SessionStore`` classes available in Django inherit from
-:class:`~backends.base.SessionBase` and implement data manipulation methods,
-namely:
+All ``SessionStore`` subclasses available in Django implement the following
+data manipulation methods:
 
 * ``exists()``
 * ``create()``
 * ``save()``
 * ``delete()``
 * ``load()``
-* :meth:`~backends.base.SessionBase.clear_expired`
+* :meth:`~.SessionBase.clear_expired`
+
+An asynchronous interface for these methods is provided by wrapping them with
+``sync_to_async()``. They can be implemented directly if an async-native
+implementation is available:
+
+* ``aexists()``
+* ``acreate()``
+* ``asave()``
+* ``adelete()``
+* ``aload()``
+* :meth:`~.SessionBase.aclear_expired`
 
 In order to build a custom session engine or to customize an existing one, you
 may create a new class inheriting from :class:`~backends.base.SessionBase` or
@@ -705,6 +868,11 @@ You can extend the session engines, but doing so with database-backed session
 engines generally requires some extra effort (see the next section for
 details).
 
+.. versionchanged:: 5.1
+
+    ``aexists()``, ``acreate()``, ``asave()``, ``adelete()``, ``aload()``, and
+    ``aclear_expired()`` methods were added.
+
 .. _extending-database-backed-session-engines:
 
 Extending database-backed session engines
diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt
index 3e4778f0f2..171cfc3c93 100644
--- a/docs/topics/http/shortcuts.txt
+++ b/docs/topics/http/shortcuts.txt
@@ -239,10 +239,6 @@ Note: As with ``get()``, a
 :class:`~django.core.exceptions.MultipleObjectsReturned` exception
 will be raised if more than one object is found.
 
-.. versionchanged:: 5.0
-
-    ``aget_object_or_404()`` function was added.
-
 ``get_list_or_404()``
 =====================
 
@@ -291,6 +287,3 @@ This example is equivalent to::
         if not my_objects:
             raise Http404("No MyModel matches the given query.")
 
-.. versionchanged:: 5.0
-
-    ``aget_list_or_404()`` function was added.
diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt
index d8de9635ec..8e57732725 100644
--- a/docs/topics/http/urls.txt
+++ b/docs/topics/http/urls.txt
@@ -183,6 +183,11 @@ Register custom converter classes in your URLconf using
         ...,
     ]
 
+.. deprecated:: 5.1
+
+    Overriding existing converters with ``django.urls.register_converter()`` is
+    deprecated.
+
 Using regular expressions
 =========================
 
diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt
index 30ec916ce8..0d866dac6c 100644
--- a/docs/topics/i18n/timezones.txt
+++ b/docs/topics/i18n/timezones.txt
@@ -9,7 +9,10 @@ Overview
 
 When support for time zones is enabled, Django stores datetime information in
 UTC in the database, uses time-zone-aware datetime objects internally, and
-translates them to the end user's time zone in templates and forms.
+converts them to the end user's time zone in forms. Templates will use the
+:ref:`default time zone <default-current-time-zone>`, but this can be updated
+to the end user's time zone through the use of :ref:`filters and tags
+<time-zones-in-templates>`.
 
 This is handy if your users live in more than one time zone and you want to
 display datetime information according to each user's wall clock.
@@ -27,10 +30,6 @@ interacting with end users.
 Time zone support is enabled by default. To disable it, set :setting:`USE_TZ =
 False <USE_TZ>` in your settings file.
 
-.. versionchanged:: 5.0
-
-    In older version, time zone support was disabled by default.
-
 Time zone support uses :mod:`zoneinfo`, which is part of the Python standard
 library from Python 3.9.
 
@@ -393,9 +392,15 @@ UTC on storage, and from UTC to the connection's time zone on retrieval.
 
 As a consequence, if you're using PostgreSQL, you can switch between ``USE_TZ
 = False`` and ``USE_TZ = True`` freely. The database connection's time zone
-will be set to :setting:`TIME_ZONE` or ``UTC`` respectively, so that Django
-obtains correct datetimes in all cases. You don't need to perform any data
-conversions.
+will be set to :setting:`DATABASE-TIME_ZONE` or ``UTC`` respectively, so that
+Django obtains correct datetimes in all cases. You don't need to perform any
+data conversions.
+
+.. admonition:: Time zone settings
+
+    The :setting:`time zone <DATABASE-TIME_ZONE>` configured for the connection
+    in the :setting:`DATABASES` setting is distinct from the general
+    :setting:`TIME_ZONE` setting.
 
 Other databases
 ~~~~~~~~~~~~~~~
diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt
index 5fb5019c1e..f9748f0d9d 100644
--- a/docs/topics/migrations.txt
+++ b/docs/topics/migrations.txt
@@ -703,7 +703,7 @@ app label and migration name you want to squash up to, and it'll get to work:
    - 0002_some_change
    - 0003_another_change
    - 0004_undo_something
-  Do you wish to proceed? [yN] y
+  Do you wish to proceed? [y/N] y
   Optimizing...
     Optimized from 12 operations to 7 operations.
   Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_something.py
@@ -796,11 +796,6 @@ Django can serialize the following:
 - Any class reference (must be in module's top-level scope)
 - Anything with a custom ``deconstruct()`` method (:ref:`see below <custom-deconstruct-method>`)
 
-.. versionchanged:: 5.0
-
-    Serialization support for functions decorated with :func:`functools.cache`
-    or :func:`functools.lru_cache` was added.
-
 Django cannot serialize:
 
 - Nested classes
diff --git a/docs/topics/signals.txt b/docs/topics/signals.txt
index 6a577dd8ad..339626c799 100644
--- a/docs/topics/signals.txt
+++ b/docs/topics/signals.txt
@@ -107,10 +107,6 @@ Signals can be sent either synchronously or asynchronously, and receivers will
 automatically be adapted to the correct call-style. See :ref:`sending signals
 <sending-signals>` for more information.
 
-.. versionchanged:: 5.0
-
-    Support for asynchronous receivers was added.
-
 .. _connecting-receiver-functions:
 
 Connecting receiver functions
@@ -307,7 +303,7 @@ error instance is returned in the tuple pair for the receiver that raised the er
 The tracebacks are present on the ``__traceback__`` attribute of the errors
 returned when calling ``send_robust()``.
 
-``asend()`` is similar as ``send()``, but it is coroutine that must be
+``asend()`` is similar to ``send()``, but it is a coroutine that must be
 awaited::
 
     async def asend_pizza(self, toppings, size):
@@ -330,10 +326,6 @@ receiver. In addition, async receivers are executed concurrently using
 All built-in signals, except those in the async request-response cycle, are
 dispatched using :meth:`Signal.send`.
 
-.. versionchanged:: 5.0
-
-    Support for asynchronous signals was added.
-
 Disconnecting signals
 =====================
 
diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt
index d889bd02ee..6b03f0f82b 100644
--- a/docs/topics/testing/advanced.txt
+++ b/docs/topics/testing/advanced.txt
@@ -628,10 +628,6 @@ and tear down the test suite.
     custom arguments by calling ``parser.add_argument()`` inside the method, so
     that the :djadmin:`test` command will be able to use those arguments.
 
-    .. versionadded:: 5.0
-
-        The ``durations`` argument was added.
-
 Attributes
 ~~~~~~~~~~
 
diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt
index 14a58c4c00..363505a0fc 100644
--- a/docs/topics/testing/tools.txt
+++ b/docs/topics/testing/tools.txt
@@ -497,10 +497,6 @@ Use the ``django.test.Client`` class to make requests.
         :meth:`~django.contrib.auth.models.UserManager.create_user` helper
         method to create a new user with a correctly hashed password.
 
-        .. versionchanged:: 5.0
-
-            ``alogin()`` method was added.
-
     .. method:: Client.force_login(user, backend=None)
     .. method:: Client.aforce_login(user, backend=None)
 
@@ -528,10 +524,6 @@ Use the ``django.test.Client`` class to make requests.
         ``login()`` by :ref:`using a weaker hasher while testing
         <speeding-up-tests-auth-hashers>`.
 
-        .. versionchanged:: 5.0
-
-            ``aforce_login()`` method was added.
-
     .. method:: Client.logout()
     .. method:: Client.alogout()
 
@@ -545,10 +537,6 @@ Use the ``django.test.Client`` class to make requests.
         and session data cleared to defaults. Subsequent requests will appear
         to come from an :class:`~django.contrib.auth.models.AnonymousUser`.
 
-        .. versionchanged:: 5.0
-
-            ``alogout()`` method was added.
-
 Testing responses
 -----------------
 
@@ -735,8 +723,6 @@ access these properties as part of a test condition.
 
 .. method:: Client.asession()
 
-    .. versionadded:: 5.0
-
     This is similar to the :attr:`session` attribute but it works in async
     contexts.
 
@@ -1163,7 +1149,7 @@ out the `full reference`_ for more details.
     for more information.
 
     .. _Selenium FAQ: https://web.archive.org/web/20160129132110/http://code.google.com/p/selenium/wiki/FrequentlyAskedQuestions#Q:_WebDriver_fails_to_find_elements_/_Does_not_block_on_page_loa
-    .. _Selenium documentation: https://www.selenium.dev/documentation/webdriver/waits/#explicit-wait
+    .. _Selenium documentation: https://www.selenium.dev/documentation/webdriver/waits/#explicit-waits
 
 Test cases features
 ===================
@@ -2062,10 +2048,6 @@ test client, with the following exceptions:
     >>> c = AsyncClient()
     >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
 
-.. versionchanged:: 5.0
-
-    Support for the ``follow`` parameter was added to the ``AsyncClient``.
-
 .. versionchanged:: 5.1
 
     The ``query_params`` argument was added.
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000000..306adb3aa5
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,63 @@
+import globals from "globals";
+import js from "@eslint/js";
+
+export default [
+    js.configs.recommended,
+    {
+        files: ["**/*.js"],
+        rules: {
+            "camelcase": ["off", {"properties": "always"}],
+            "comma-spacing": ["error", {"before": false, "after": true}],
+            "curly": ["error", "all"],
+            "dot-notation": ["error", {"allowKeywords": true}],
+            "eqeqeq": ["error"],
+            "indent": ["error", 4],
+            "key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
+            "linebreak-style": ["error", "unix"],
+            "new-cap": ["off", {"newIsCap": true, "capIsNew": true}],
+            "no-alert": ["off"],
+            "no-eval": ["error"],
+            "no-extend-native": ["error", {"exceptions": ["Date", "String"]}],
+            "no-multi-spaces": ["error"],
+            "no-octal-escape": ["error"],
+            "no-script-url": ["error"],
+            "no-shadow": ["error", {"hoist": "functions"}],
+            "no-underscore-dangle": ["error"],
+            "no-unused-vars": ["error", {"vars": "local", "args": "none"}],
+            "no-var": ["error"],
+            "prefer-const": ["error"],
+            "quotes": ["off", "single"],
+            "semi": ["error", "always"],
+            "space-before-blocks": ["error", "always"],
+            "space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}],
+            "space-infix-ops": ["error", {"int32Hint": false}],
+            "strict": ["error", "global"]
+        },
+        languageOptions: {
+            ecmaVersion: 6,
+            sourceType: "script",
+            globals: {
+                ...globals.browser,
+                ...globals.commonjs,
+                "django": false
+            }
+        }
+    },
+    {
+        files: ["**/*.mjs"],
+        languageOptions: {
+            sourceType: "module"
+        }
+    },
+    {
+        ignores: [
+            "**/*.min.js",
+            "**/vendor/**/*.js",
+            "django/contrib/gis/templates/**/*.js",
+            "django/views/templates/*.js",
+            "docs/_build/**/*.js",
+            "node_modules/**.js",
+            "tests/**/*.js",
+        ]
+    }
+];
diff --git a/js_tests/admin/actions.test.js b/js_tests/admin/actions.test.js
index 0077dd6ff3..bcc10debaa 100644
--- a/js_tests/admin/actions.test.js
+++ b/js_tests/admin/actions.test.js
@@ -1,4 +1,4 @@
-/* global QUnit */
+/* global QUnit, Actions */
 'use strict';
 
 QUnit.module('admin.actions', {
diff --git a/js_tests/admin/navigation.test.js b/js_tests/admin/navigation.test.js
index 262f08c9ae..0da50e757c 100644
--- a/js_tests/admin/navigation.test.js
+++ b/js_tests/admin/navigation.test.js
@@ -1,4 +1,4 @@
-/* global QUnit */
+/* global QUnit, initSidebarQuickFilter */
 'use strict';
 
 QUnit.module('admin.sidebar: filter', {
diff --git a/js_tests/tests.html b/js_tests/tests.html
index adeac66125..011998e5a7 100644
--- a/js_tests/tests.html
+++ b/js_tests/tests.html
@@ -143,7 +143,6 @@
     <script src='./admin/inlines.test.js'></script>
 
     <script src='../django/contrib/admin/static/admin/js/actions.js' data-cover></script>
-    <script src='../django/contrib/admin/static/admin/js/collapse.js' data-cover></script>
     <script src='../django/contrib/admin/static/admin/js/prepopulate.js' data-cover></script>
     <script src='../django/contrib/admin/static/admin/js/urlify.js' data-cover></script>
     <script src='./admin/URLify.test.js'></script>
diff --git a/package.json b/package.json
index 0258997340..88d154cd28 100644
--- a/package.json
+++ b/package.json
@@ -9,11 +9,12 @@
     "npm": ">=1.3.0"
   },
   "devDependencies": {
-    "eslint": "^8.56.0",
-    "puppeteer": "^21.7.0",
+    "eslint": "^9.2.0",
+    "puppeteer": "^22.2.0",
+    "globals": "^15.1.0",
     "grunt": "^1.6.1",
     "grunt-cli": "^1.4.3",
     "grunt-contrib-qunit": "^8.0.1",
-    "qunit": "^2.20.0"
+    "qunit": "^2.20.1"
   }
 }
diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py
index 8ffc45e391..349ef7d465 100644
--- a/tests/admin_changelist/admin.py
+++ b/tests/admin_changelist/admin.py
@@ -3,7 +3,7 @@ from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import User
 from django.core.paginator import Paginator
 
-from .models import Band, Child, Event, Parent, ProxyUser, Swallow
+from .models import Band, Child, Event, GrandChild, Parent, ProxyUser, Swallow
 
 site = admin.AdminSite(name="admin")
 
@@ -53,6 +53,13 @@ class ChildAdmin(admin.ModelAdmin):
         return super().get_queryset(request).select_related("parent")
 
 
+class GrandChildAdmin(admin.ModelAdmin):
+    list_display = ["name", "parent__name", "parent__parent__name"]
+
+
+site.register(GrandChild, GrandChildAdmin)
+
+
 class CustomPaginationAdmin(ChildAdmin):
     paginator = CustomPaginator
 
diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py
index aa4656e93e..290a3ea4ec 100644
--- a/tests/admin_changelist/models.py
+++ b/tests/admin_changelist/models.py
@@ -19,6 +19,11 @@ class Child(models.Model):
     age = models.IntegerField(null=True, blank=True)
 
 
+class GrandChild(models.Model):
+    parent = models.ForeignKey(Child, models.SET_NULL, editable=False, null=True)
+    name = models.CharField(max_length=30, blank=True)
+
+
 class Genre(models.Model):
     name = models.CharField(max_length=20)
 
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index b4739b572d..bf85cf038f 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -42,6 +42,7 @@ from .admin import (
     EmptyValueChildAdmin,
     EventAdmin,
     FilteredChildAdmin,
+    GrandChildAdmin,
     GroupAdmin,
     InvitationAdmin,
     NoListDisplayLinksParentAdmin,
@@ -61,6 +62,7 @@ from .models import (
     CustomIdUser,
     Event,
     Genre,
+    GrandChild,
     Group,
     Invitation,
     Membership,
@@ -1441,7 +1443,7 @@ class ChangeListTests(TestCase):
                             ["field_3", "related_4_id"],
                         )
                     ],
-                    models.CheckConstraint(check=models.Q(id__gt=0), name="foo"),
+                    models.CheckConstraint(condition=models.Q(id__gt=0), name="foo"),
                     models.UniqueConstraint(
                         fields=["field_5"],
                         condition=models.Q(id__gt=10),
@@ -1634,6 +1636,62 @@ class ChangeListTests(TestCase):
                     response, f'0 results (<a href="{href}">1 total</a>)'
                 )
 
+    def test_list_display_related_field(self):
+        parent = Parent.objects.create(name="I am your father")
+        child = Child.objects.create(name="I am your child", parent=parent)
+        GrandChild.objects.create(name="I am your grandchild", parent=child)
+        request = self._mocked_authenticated_request("/grandchild/", self.superuser)
+
+        m = GrandChildAdmin(GrandChild, custom_site)
+        response = m.changelist_view(request)
+        self.assertContains(response, parent.name)
+        self.assertContains(response, child.name)
+
+    def test_list_display_related_field_null(self):
+        GrandChild.objects.create(name="I am parentless", parent=None)
+        request = self._mocked_authenticated_request("/grandchild/", self.superuser)
+
+        m = GrandChildAdmin(GrandChild, custom_site)
+        response = m.changelist_view(request)
+        self.assertContains(response, '<td class="field-parent__name">-</td>')
+        self.assertContains(response, '<td class="field-parent__parent__name">-</td>')
+
+    def test_list_display_related_field_ordering(self):
+        parent_a = Parent.objects.create(name="Alice")
+        parent_z = Parent.objects.create(name="Zara")
+        Child.objects.create(name="Alice's child", parent=parent_a)
+        Child.objects.create(name="Zara's child", parent=parent_z)
+
+        class ChildAdmin(admin.ModelAdmin):
+            list_display = ["name", "parent__name"]
+            list_per_page = 1
+
+        m = ChildAdmin(Child, custom_site)
+
+        # Order ascending.
+        request = self._mocked_authenticated_request("/grandchild/?o=1", self.superuser)
+        response = m.changelist_view(request)
+        self.assertContains(response, parent_a.name)
+        self.assertNotContains(response, parent_z.name)
+
+        # Order descending.
+        request = self._mocked_authenticated_request(
+            "/grandchild/?o=-1", self.superuser
+        )
+        response = m.changelist_view(request)
+        self.assertNotContains(response, parent_a.name)
+        self.assertContains(response, parent_z.name)
+
+    def test_list_display_related_field_ordering_fields(self):
+        class ChildAdmin(admin.ModelAdmin):
+            list_display = ["name", "parent__name"]
+            ordering = ["parent__name"]
+
+        m = ChildAdmin(Child, custom_site)
+        request = self._mocked_authenticated_request("/", self.superuser)
+        cl = m.get_changelist_instance(request)
+        self.assertEqual(cl.get_ordering_field_columns(), {2: "asc"})
+
 
 class GetAdminLogTests(TestCase):
     def test_custom_user_pk_not_named_id(self):
@@ -2015,3 +2073,58 @@ class SeleniumTests(AdminSeleniumTestCase):
                 By.CSS_SELECTOR, "[data-filter-title='It\\'s OK']"
             ).get_attribute("open")
         )
+
+    def test_list_display_ordering(self):
+        from selenium.webdriver.common.by import By
+
+        parent_a = Parent.objects.create(name="Parent A")
+        child_l = Child.objects.create(name="Child L", parent=None)
+        child_m = Child.objects.create(name="Child M", parent=parent_a)
+        GrandChild.objects.create(name="Grandchild X", parent=child_m)
+        GrandChild.objects.create(name="Grandchild Y", parent=child_l)
+        GrandChild.objects.create(name="Grandchild Z", parent=None)
+
+        self.admin_login(username="super", password="secret")
+        changelist_url = reverse("admin:admin_changelist_grandchild_changelist")
+        self.selenium.get(self.live_server_url + changelist_url)
+
+        def find_result_row_texts():
+            table = self.selenium.find_element(By.ID, "result_list")
+            # Drop header from the result list
+            return [row.text for row in table.find_elements(By.TAG_NAME, "tr")][1:]
+
+        def expected_from_queryset(qs):
+            return [
+                " ".join("-" if i is None else i for i in item)
+                for item in qs.values_list(
+                    "name", "parent__name", "parent__parent__name"
+                )
+            ]
+
+        cases = [
+            # Order ascending by `name`.
+            ("th.sortable.column-name", ("name",)),
+            # Order descending by `name`.
+            ("th.sortable.column-name", ("-name",)),
+            # Order ascending by `parent__name`.
+            ("th.sortable.column-parent__name", ("parent__name", "-name")),
+            # Order descending by `parent__name`.
+            ("th.sortable.column-parent__name", ("-parent__name", "-name")),
+            # Order ascending by `parent__parent__name`.
+            (
+                "th.sortable.column-parent__parent__name",
+                ("parent__parent__name", "-parent__name", "-name"),
+            ),
+            # Order descending by `parent__parent__name`.
+            (
+                "th.sortable.column-parent__parent__name",
+                ("-parent__parent__name", "-parent__name", "-name"),
+            ),
+        ]
+        for css_selector, ordering in cases:
+            with self.subTest(ordering=ordering):
+                self.selenium.find_element(By.CSS_SELECTOR, css_selector).click()
+                expected = expected_from_queryset(
+                    GrandChild.objects.all().order_by(*ordering)
+                )
+                self.assertEqual(find_result_row_texts(), expected)
diff --git a/tests/admin_checks/tests.py b/tests/admin_checks/tests.py
index d2d1eb219e..6ca5d6d925 100644
--- a/tests/admin_checks/tests.py
+++ b/tests/admin_checks/tests.py
@@ -1009,3 +1009,26 @@ class SystemChecksTestCase(SimpleTestCase):
             self.assertEqual(errors, [])
         finally:
             Book._meta.apps.ready = True
+
+    def test_related_field_list_display(self):
+        class SongAdmin(admin.ModelAdmin):
+            list_display = ["pk", "original_release", "album__title"]
+
+        errors = SongAdmin(Song, AdminSite()).check()
+        self.assertEqual(errors, [])
+
+    def test_related_field_list_display_wrong_field(self):
+        class SongAdmin(admin.ModelAdmin):
+            list_display = ["pk", "original_release", "album__hello"]
+
+        errors = SongAdmin(Song, AdminSite()).check()
+        expected = [
+            checks.Error(
+                "The value of 'list_display[2]' refers to 'album__hello', which is not "
+                "a callable or attribute of 'SongAdmin', or an attribute, method, or "
+                "field on 'admin_checks.Song'.",
+                obj=SongAdmin,
+                id="admin.E108",
+            )
+        ]
+        self.assertEqual(errors, expected)
diff --git a/tests/admin_docs/models.py b/tests/admin_docs/models.py
index a403259c6d..b4ef84caba 100644
--- a/tests/admin_docs/models.py
+++ b/tests/admin_docs/models.py
@@ -54,6 +54,12 @@ class Person(models.Model):
     def dummy_function(self, baz, rox, *some_args, **some_kwargs):
         return some_kwargs
 
+    def dummy_function_keyword_only_arg(self, *, keyword_only_arg):
+        return keyword_only_arg
+
+    def all_kinds_arg_function(self, position_only_arg, /, arg, *, kwarg):
+        return position_only_arg, arg, kwarg
+
     @property
     def a_property(self):
         return "a_property"
diff --git a/tests/admin_docs/test_views.py b/tests/admin_docs/test_views.py
index ef7fde1bf9..064ce27fb0 100644
--- a/tests/admin_docs/test_views.py
+++ b/tests/admin_docs/test_views.py
@@ -280,6 +280,8 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
         self.assertContains(self.response, "<h3>Methods with arguments</h3>")
         self.assertContains(self.response, "<td>rename_company</td>")
         self.assertContains(self.response, "<td>dummy_function</td>")
+        self.assertContains(self.response, "<td>dummy_function_keyword_only_arg</td>")
+        self.assertContains(self.response, "<td>all_kinds_arg_function</td>")
         self.assertContains(self.response, "<td>suffix_company_name</td>")
 
     def test_methods_with_arguments_display_arguments(self):
@@ -287,6 +289,7 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
         Methods with arguments should have their arguments displayed.
         """
         self.assertContains(self.response, "<td>new_name</td>")
+        self.assertContains(self.response, "<td>keyword_only_arg</td>")
 
     def test_methods_with_arguments_display_arguments_default_value(self):
         """
@@ -302,6 +305,7 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
         self.assertContains(
             self.response, "<td>baz, rox, *some_args, **some_kwargs</td>"
         )
+        self.assertContains(self.response, "<td>position_only_arg, arg, kwarg</td>")
 
     def test_instance_of_property_methods_are_displayed(self):
         """Model properties are displayed as fields."""
diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py
index a3af966005..558164f75c 100644
--- a/tests/admin_filters/tests.py
+++ b/tests/admin_filters/tests.py
@@ -17,7 +17,7 @@ from django.contrib.admin.options import IncorrectLookupParameters, ShowFacets
 from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import User
 from django.core.exceptions import ImproperlyConfigured
-from django.db import connection
+from django.db import connection, models
 from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
 
 from .models import Book, Bookmark, Department, Employee, ImprovedBook, TaggedItem
@@ -154,6 +154,30 @@ class EmployeeNameCustomDividerFilter(FieldListFilter):
         return [self.lookup_kwarg]
 
 
+class DepartmentOwnershipListFilter(SimpleListFilter):
+    title = "Department Ownership"
+    parameter_name = "department_ownership"
+
+    def lookups(self, request, model_admin):
+        return [
+            ("DEV_OWNED", "Owned by Dev Department"),
+            ("OTHER", "Other"),
+        ]
+
+    def queryset(self, request, queryset):
+        queryset = queryset.annotate(
+            owned_book_count=models.Count(
+                "employee__department",
+                filter=models.Q(employee__department__code="DEV"),
+            ),
+        )
+
+        if self.value() == "DEV_OWNED":
+            return queryset.filter(owned_book_count__gt=0)
+        elif self.value() == "OTHER":
+            return queryset.filter(owned_book_count=0)
+
+
 class CustomUserAdmin(UserAdmin):
     list_filter = ("books_authored", "books_contributed")
 
@@ -229,6 +253,7 @@ class DecadeFilterBookAdmin(ModelAdmin):
         ("author__email", AllValuesFieldListFilter),
         ("contributors", RelatedOnlyFieldListFilter),
         ("category", EmptyFieldListFilter),
+        DepartmentOwnershipListFilter,
     )
     ordering = ("-id",)
 
@@ -336,6 +361,14 @@ class ListFiltersTests(TestCase):
         cls.bob = User.objects.create_user("bob", "bob@example.com")
         cls.lisa = User.objects.create_user("lisa", "lisa@example.com")
 
+        # Departments
+        cls.dev = Department.objects.create(code="DEV", description="Development")
+        cls.design = Department.objects.create(code="DSN", description="Design")
+
+        # Employees
+        cls.john = Employee.objects.create(name="John Blue", department=cls.dev)
+        cls.jack = Employee.objects.create(name="Jack Red", department=cls.design)
+
         # Books
         cls.djangonaut_book = Book.objects.create(
             title="Djangonaut: an art of living",
@@ -345,6 +378,7 @@ class ListFiltersTests(TestCase):
             date_registered=cls.today,
             availability=True,
             category="non-fiction",
+            employee=cls.john,
         )
         cls.bio_book = Book.objects.create(
             title="Django: a biography",
@@ -354,6 +388,7 @@ class ListFiltersTests(TestCase):
             no=207,
             availability=False,
             category="fiction",
+            employee=cls.john,
         )
         cls.django_book = Book.objects.create(
             title="The Django Book",
@@ -363,6 +398,7 @@ class ListFiltersTests(TestCase):
             date_registered=cls.today,
             no=103,
             availability=True,
+            employee=cls.jack,
         )
         cls.guitar_book = Book.objects.create(
             title="Guitar for dummies",
@@ -374,14 +410,6 @@ class ListFiltersTests(TestCase):
         )
         cls.guitar_book.contributors.set([cls.bob, cls.lisa])
 
-        # Departments
-        cls.dev = Department.objects.create(code="DEV", description="Development")
-        cls.design = Department.objects.create(code="DSN", description="Design")
-
-        # Employees
-        cls.john = Employee.objects.create(name="John Blue", department=cls.dev)
-        cls.jack = Employee.objects.create(name="Jack Red", department=cls.design)
-
     def assertChoicesDisplay(self, choices, expected_displays):
         for choice, expected_display in zip(choices, expected_displays, strict=True):
             self.assertEqual(choice["display"], expected_display)
@@ -905,6 +933,7 @@ class ListFiltersTests(TestCase):
             filterspec.lookup_choices,
             [
                 (self.djangonaut_book.pk, "Djangonaut: an art of living"),
+                (self.bio_book.pk, "Django: a biography"),
                 (self.django_book.pk, "The Django Book"),
             ],
         )
@@ -1407,6 +1436,8 @@ class ListFiltersTests(TestCase):
             ["All", "bob (1)", "lisa (1)", "??? (3)"],
             # EmptyFieldListFilter.
             ["All", "Empty (2)", "Not empty (2)"],
+            # SimpleListFilter with join relations.
+            ["All", "Owned by Dev Department (2)", "Other (2)"],
         ]
         for filterspec, expected_displays in zip(filters, tests, strict=True):
             with self.subTest(filterspec.__class__.__name__):
@@ -1482,6 +1513,8 @@ class ListFiltersTests(TestCase):
             ["All", "bob (0)", "lisa (0)", "??? (2)"],
             # EmptyFieldListFilter.
             ["All", "Empty (0)", "Not empty (2)"],
+            # SimpleListFilter with join relations.
+            ["All", "Owned by Dev Department (2)", "Other (0)"],
         ]
         for filterspec, expected_displays in zip(filters, tests, strict=True):
             with self.subTest(filterspec.__class__.__name__):
@@ -1525,6 +1558,8 @@ class ListFiltersTests(TestCase):
             ["All", "bob", "lisa", "???"],
             # EmptyFieldListFilter.
             ["All", "Empty", "Not empty"],
+            # SimpleListFilter with join relations.
+            ["All", "Owned by Dev Department", "Other"],
         ]
         for filterspec, expected_displays in zip(filters, tests, strict=True):
             with self.subTest(filterspec.__class__.__name__):
diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py
index 0ec56d71b3..578142d192 100644
--- a/tests/admin_inlines/admin.py
+++ b/tests/admin_inlines/admin.py
@@ -40,6 +40,8 @@ from .models import (
     OutfitItem,
     ParentModelWithCustomPk,
     Person,
+    Photo,
+    Photographer,
     Poll,
     Profile,
     ProfileCollection,
@@ -55,6 +57,8 @@ from .models import (
     Teacher,
     Title,
     TitleCollection,
+    UUIDChild,
+    UUIDParent,
 )
 
 site = admin.AdminSite(name="admin")
@@ -98,6 +102,57 @@ class AuthorAdmin(admin.ModelAdmin):
     ]
 
 
+class PhotoInlineMixin:
+    model = Photo
+    extra = 2
+    fieldsets = [
+        (None, {"fields": ["image", "title"]}),
+        (
+            "Details",
+            {"fields": ["description", "creation_date"], "classes": ["collapse"]},
+        ),
+        (
+            "Details",  # Fieldset name intentionally duplicated
+            {"fields": ["update_date", "updated_by"]},
+        ),
+    ]
+
+
+class PhotoTabularInline(PhotoInlineMixin, admin.TabularInline):
+    pass
+
+
+class PhotoStackedExtra2Inline(PhotoInlineMixin, admin.StackedInline):
+    pass
+
+
+class PhotoStackedExtra3Inline(PhotoInlineMixin, admin.StackedInline):
+    extra = 3
+
+
+class PhotoStackedCollapsibleInline(PhotoInlineMixin, admin.StackedInline):
+    fieldsets = []
+    classes = ["collapse"]
+
+
+class PhotographerAdmin(admin.ModelAdmin):
+    fieldsets = [
+        (None, {"fields": ["firstname", "fullname"]}),
+        ("Advanced options", {"fields": ["nationality", "residency"]}),
+        (
+            "Advanced options",  # Fieldset name intentionally duplicated
+            {"fields": ["siblings", "children"], "classes": ["collapse"]},
+        ),
+    ]
+    inlines = [
+        PhotoTabularInline,
+        PhotoTabularInline,
+        PhotoStackedExtra2Inline,
+        PhotoStackedExtra3Inline,
+        PhotoStackedCollapsibleInline,
+    ]
+
+
 class InnerInline(admin.StackedInline):
     model = Inner
     can_delete = False
@@ -418,6 +473,16 @@ class ShowInlineChildInline(admin.StackedInline):
     model = ShowInlineChild
 
 
+class UUIDChildInline(admin.StackedInline):
+    model = UUIDChild
+    exclude = ("id",)
+
+
+class UUIDParentModelAdmin(admin.ModelAdmin):
+    model = UUIDParent
+    inlines = [UUIDChildInline]
+
+
 class ShowInlineParentAdmin(admin.ModelAdmin):
     def get_inlines(self, request, obj):
         if obj is not None and obj.show_inlines:
@@ -454,11 +519,13 @@ site.register(Teacher, TeacherAdmin)
 site.register(Chapter, inlines=[FootNoteNonEditableInlineCustomForm])
 site.register(OutfitItem, inlines=[WeaknessInlineCustomForm])
 site.register(Person, inlines=[AuthorTabularInline, FashonistaStackedInline])
+site.register(Photographer, PhotographerAdmin)
 site.register(Course, ClassAdminStackedHorizontal)
 site.register(CourseProxy, ClassAdminStackedVertical)
 site.register(CourseProxy1, ClassAdminTabularVertical)
 site.register(CourseProxy2, ClassAdminTabularHorizontal)
 site.register(ShowInlineParent, ShowInlineParentAdmin)
+site.register(UUIDParent, UUIDParentModelAdmin)
 # Used to test hidden fields in tabular and stacked inlines.
 site2 = admin.AdminSite(name="tabular_inline_hidden_field_admin")
 site2.register(SomeParentModel, inlines=[ChildHiddenFieldTabularInline])
diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py
index eca5bae422..64aaca8d14 100644
--- a/tests/admin_inlines/models.py
+++ b/tests/admin_inlines/models.py
@@ -3,6 +3,7 @@ Testing of admin inline formsets.
 """
 
 import random
+import uuid
 
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
@@ -180,6 +181,27 @@ class ShoppingWeakness(models.Model):
     item = models.ForeignKey(OutfitItem, models.CASCADE)
 
 
+# Models for #35189
+
+
+class Photographer(Person):
+    fullname = models.CharField(max_length=100)
+    nationality = models.CharField(max_length=100)
+    residency = models.CharField(max_length=100)
+    siblings = models.IntegerField()
+    children = models.IntegerField()
+
+
+class Photo(models.Model):
+    photographer = models.ForeignKey(Photographer, on_delete=models.CASCADE)
+    image = models.CharField(max_length=100)
+    title = models.CharField(max_length=100)
+    description = models.TextField()
+    creation_date = models.DateField()
+    update_date = models.DateField()
+    updated_by = models.CharField(max_length=100)
+
+
 # Models for #13510
 
 
@@ -378,3 +400,13 @@ class BothVerboseNameProfile(Profile):
     class Meta:
         verbose_name = "Model with both - name"
         verbose_name_plural = "Model with both - plural name"
+
+
+class UUIDParent(models.Model):
+    pass
+
+
+class UUIDChild(models.Model):
+    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
+    title = models.CharField(max_length=128)
+    parent = models.ForeignKey(UUIDParent, on_delete=models.CASCADE)
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 9533cc9af3..25512aede4 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -4,7 +4,9 @@ from django.contrib.admin.tests import AdminSeleniumTestCase
 from django.contrib.auth.models import Permission, User
 from django.contrib.contenttypes.models import ContentType
 from django.test import RequestFactory, TestCase, override_settings
+from django.test.selenium import screenshot_cases
 from django.urls import reverse
+from django.utils.translation import gettext
 
 from .admin import InnerInline
 from .admin import site as admin_site
@@ -42,6 +44,8 @@ from .models import (
     SomeChildModel,
     SomeParentModel,
     Teacher,
+    UUIDChild,
+    UUIDParent,
     VerboseNamePluralProfile,
     VerboseNameProfile,
 )
@@ -113,11 +117,31 @@ class TestInline(TestDataMixin, TestCase):
         )
         self.assertContains(response, "<label>Inner readonly label:</label>")
 
+    def test_excluded_id_for_inlines_uses_hidden_field(self):
+        parent = UUIDParent.objects.create()
+        child = UUIDChild.objects.create(title="foo", parent=parent)
+        response = self.client.get(
+            reverse("admin:admin_inlines_uuidparent_change", args=(parent.id,))
+        )
+        self.assertContains(
+            response,
+            f'<input type="hidden" name="uuidchild_set-0-id" value="{child.id}" '
+            'id="id_uuidchild_set-0-id">',
+            html=True,
+        )
+
     def test_many_to_many_inlines(self):
         "Autogenerated many-to-many inlines are displayed correctly (#13407)"
         response = self.client.get(reverse("admin:admin_inlines_author_add"))
         # The heading for the m2m inline block uses the right text
-        self.assertContains(response, "<h2>Author-book relationships</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         # The "add another" label is correct
         self.assertContains(response, "Add another Author-book relationship")
         # The '+' is dropped from the autogenerated form prefix (Author_books+)
@@ -737,13 +761,35 @@ class TestInline(TestDataMixin, TestCase):
 
     def test_inlines_plural_heading_foreign_key(self):
         response = self.client.get(reverse("admin:admin_inlines_holder4_add"))
-        self.assertContains(response, "<h2>Inner4 stackeds</h2>", html=True)
-        self.assertContains(response, "<h2>Inner4 tabulars</h2>", html=True)
+        self.assertContains(
+            response,
+            (
+                '<h2 id="inner4stacked_set-heading" class="inline-heading">'
+                "Inner4 stackeds</h2>"
+            ),
+            html=True,
+        )
+        self.assertContains(
+            response,
+            (
+                '<h2 id="inner4tabular_set-heading" class="inline-heading">'
+                "Inner4 tabulars</h2>"
+            ),
+            html=True,
+        )
 
     def test_inlines_singular_heading_one_to_one(self):
         response = self.client.get(reverse("admin:admin_inlines_person_add"))
-        self.assertContains(response, "<h2>Author</h2>", html=True)  # Tabular.
-        self.assertContains(response, "<h2>Fashionista</h2>", html=True)  # Stacked.
+        self.assertContains(
+            response,
+            '<h2 id="author-heading" class="inline-heading">Author</h2>',
+            html=True,
+        )  # Tabular.
+        self.assertContains(
+            response,
+            '<h2 id="fashionista-heading" class="inline-heading">Fashionista</h2>',
+            html=True,
+        )  # Stacked.
 
     def test_inlines_based_on_model_state(self):
         parent = ShowInlineParent.objects.create(show_inlines=False)
@@ -914,28 +960,50 @@ class TestInlinePermissions(TestCase):
     def test_inline_add_m2m_noperm(self):
         response = self.client.get(reverse("admin:admin_inlines_author_add"))
         # No change permission on books, so no inline
-        self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+        self.assertNotContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertNotContains(response, "Add another Author-Book Relationship")
         self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
 
     def test_inline_add_fk_noperm(self):
         response = self.client.get(reverse("admin:admin_inlines_holder2_add"))
         # No permissions on Inner2s, so no inline
-        self.assertNotContains(response, "<h2>Inner2s</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         self.assertNotContains(response, "Add another Inner2")
         self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
 
     def test_inline_change_m2m_noperm(self):
         response = self.client.get(self.author_change_url)
         # No change permission on books, so no inline
-        self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+        self.assertNotContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertNotContains(response, "Add another Author-Book Relationship")
         self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
 
     def test_inline_change_fk_noperm(self):
         response = self.client.get(self.holder_change_url)
         # No permissions on Inner2s, so no inline
-        self.assertNotContains(response, "<h2>Inner2s</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         self.assertNotContains(response, "Add another Inner2")
         self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
 
@@ -959,7 +1027,14 @@ class TestInlinePermissions(TestCase):
         self.assertIs(
             response.context["inline_admin_formset"].has_delete_permission, False
         )
-        self.assertContains(response, "<h2>Author-book relationships</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(
             response,
             '<input type="hidden" name="Author_books-TOTAL_FORMS" value="0" '
@@ -975,7 +1050,14 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(reverse("admin:admin_inlines_author_add"))
         # No change permission on Books, so no inline
-        self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+        self.assertNotContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertNotContains(response, "Add another Author-Book Relationship")
         self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
 
@@ -986,7 +1068,11 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(reverse("admin:admin_inlines_holder2_add"))
         # Add permission on inner2s, so we get the inline
-        self.assertContains(response, "<h2>Inner2s</h2>")
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         self.assertContains(response, "Add another Inner2")
         self.assertContains(
             response,
@@ -1002,7 +1088,14 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(self.author_change_url)
         # No change permission on books, so no inline
-        self.assertNotContains(response, "<h2>Author-book relationships</h2>")
+        self.assertNotContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertNotContains(response, "Add another Author-Book Relationship")
         self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
         self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
@@ -1026,7 +1119,14 @@ class TestInlinePermissions(TestCase):
         self.assertIs(
             response.context["inline_admin_formset"].has_delete_permission, False
         )
-        self.assertContains(response, "<h2>Author-book relationships</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(
             response,
             '<input type="hidden" name="Author_books-TOTAL_FORMS" value="1" '
@@ -1059,7 +1159,14 @@ class TestInlinePermissions(TestCase):
         self.assertIs(
             response.context["inline_admin_formset"].has_delete_permission, True
         )
-        self.assertContains(response, "<h2>Author-book relationships</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="Author_books-heading" class="inline-heading">'
+                "Author-book relationships</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Author-book relationship")
         self.assertContains(
             response,
@@ -1082,7 +1189,11 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(self.holder_change_url)
         # Add permission on inner2s, so we can add but not modify existing
-        self.assertContains(response, "<h2>Inner2s</h2>")
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         self.assertContains(response, "Add another Inner2")
         # 3 extra forms only, not the existing instance form
         self.assertContains(
@@ -1105,7 +1216,16 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(self.holder_change_url)
         # Change permission on inner2s, so we can change existing but not add new
-        self.assertContains(response, "<h2>Inner2s</h2>", count=2)
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         # Just the one form for existing instances
         self.assertContains(
             response,
@@ -1148,7 +1268,11 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(self.holder_change_url)
         # Add/change perm, so we can add new and change existing
-        self.assertContains(response, "<h2>Inner2s</h2>")
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         # One form for existing instance and three extra for new
         self.assertContains(
             response,
@@ -1174,7 +1298,11 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(self.holder_change_url)
         # Change/delete perm on inner2s, so we can change/delete existing
-        self.assertContains(response, "<h2>Inner2s</h2>")
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         # One form for existing instance only, no new
         self.assertContains(
             response,
@@ -1205,7 +1333,16 @@ class TestInlinePermissions(TestCase):
         self.user.user_permissions.add(permission)
         response = self.client.get(self.holder_change_url)
         # All perms on inner2s, so we can add/change/delete
-        self.assertContains(response, "<h2>Inner2s</h2>", count=2)
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
+        self.assertContains(
+            response,
+            '<h2 id="inner2_set-2-heading" class="inline-heading">Inner2s</h2>',
+            html=True,
+        )
         # One form for existing instance only, three for new
         self.assertContains(
             response,
@@ -1367,22 +1504,69 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
         response = modeladmin.changeform_view(request)
         self.assertNotContains(response, "Add another Profile")
         # Non-verbose model.
-        self.assertContains(response, "<h2>Non-verbose childss</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="profile_set-heading" class="inline-heading">'
+                "Non-verbose childss</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Non-verbose child")
-        self.assertNotContains(response, "<h2>Profiles</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
+            html=True,
+        )
         # Model with verbose name.
-        self.assertContains(response, "<h2>Childs with verbose names</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+                "Childs with verbose names</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Childs with verbose name")
-        self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+            "Model with verbose name onlys</h2>",
+            html=True,
+        )
         self.assertNotContains(response, "Add another Model with verbose name only")
         # Model with verbose name plural.
-        self.assertContains(response, "<h2>Childs with verbose name plurals</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+                "Childs with verbose name plurals</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Childs with verbose name plural")
-        self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+            "Model with verbose name plural only</h2>",
+            html=True,
+        )
         # Model with both verbose names.
-        self.assertContains(response, "<h2>Childs with both verbose namess</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+                "Childs with both verbose namess</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Childs with both verbose names")
-        self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+            "Model with both - plural name</h2>",
+            html=True,
+        )
         self.assertNotContains(response, "Add another Model with both - name")
 
     def test_verbose_name_plural_inline(self):
@@ -1415,21 +1599,68 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
         request.user = self.superuser
         response = modeladmin.changeform_view(request)
         # Non-verbose model.
-        self.assertContains(response, "<h2>Non-verbose childs</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="profile_set-heading" class="inline-heading">'
+                "Non-verbose childs</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Profile")
-        self.assertNotContains(response, "<h2>Profiles</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
+            html=True,
+        )
         # Model with verbose name.
-        self.assertContains(response, "<h2>Childs with verbose name</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+                "Childs with verbose name</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Model with verbose name only")
-        self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+            "Model with verbose name onlys</h2>",
+            html=True,
+        )
         # Model with verbose name plural.
-        self.assertContains(response, "<h2>Childs with verbose name plural</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+                "Childs with verbose name plural</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Profile")
-        self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+            "Model with verbose name plural only</h2>",
+            html=True,
+        )
         # Model with both verbose names.
-        self.assertContains(response, "<h2>Childs with both verbose names</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+                "Childs with both verbose names</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Model with both - name")
-        self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+            "Model with both - plural name</h2>",
+            html=True,
+        )
 
     def test_both_verbose_names_inline(self):
         class NonVerboseProfileInline(TabularInline):
@@ -1466,30 +1697,158 @@ class TestVerboseNameInlineForms(TestDataMixin, TestCase):
         response = modeladmin.changeform_view(request)
         self.assertNotContains(response, "Add another Profile")
         # Non-verbose model.
-        self.assertContains(response, "<h2>Non-verbose childs - plural name</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="profile_set-heading" class="inline-heading">'
+                "Non-verbose childs - plural name</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Non-verbose childs - name")
-        self.assertNotContains(response, "<h2>Profiles</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="profile_set-heading" class="inline-heading">Profiles</h2>',
+            html=True,
+        )
         # Model with verbose name.
-        self.assertContains(response, "<h2>Childs with verbose name - plural name</h2>")
+        self.assertContains(
+            response,
+            (
+                '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+                "Childs with verbose name - plural name</h2>"
+            ),
+            html=True,
+        )
         self.assertContains(response, "Add another Childs with verbose name - name")
-        self.assertNotContains(response, "<h2>Model with verbose name onlys</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="verbosenameprofile_set-heading" class="inline-heading">'
+            "Model with verbose name onlys</h2>",
+            html=True,
+        )
         # Model with verbose name plural.
         self.assertContains(
             response,
-            "<h2>Childs with verbose name plural - plural name</h2>",
+            (
+                '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+                "Childs with verbose name plural - plural name</h2>"
+            ),
+            html=True,
         )
         self.assertContains(
             response,
             "Add another Childs with verbose name plural - name",
         )
-        self.assertNotContains(response, "<h2>Model with verbose name plural only</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="verbosenamepluralprofile_set-heading" class="inline-heading">'
+            "Model with verbose name plural only</h2>",
+            html=True,
+        )
         # Model with both verbose names.
-        self.assertContains(response, "<h2>Childs with both - plural name</h2>")
+        self.assertContains(
+            response,
+            '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+            "Childs with both - plural name</h2>",
+            html=True,
+        )
         self.assertContains(response, "Add another Childs with both - name")
-        self.assertNotContains(response, "<h2>Model with both - plural name</h2>")
+        self.assertNotContains(
+            response,
+            '<h2 id="bothverbosenameprofile_set-heading" class="inline-heading">'
+            "Model with both - plural name</h2>",
+            html=True,
+        )
         self.assertNotContains(response, "Add another Model with both - name")
 
 
+@override_settings(ROOT_URLCONF="admin_inlines.urls")
+class TestInlineWithFieldsets(TestDataMixin, TestCase):
+    def setUp(self):
+        self.client.force_login(self.superuser)
+
+    def test_inline_headings(self):
+        response = self.client.get(reverse("admin:admin_inlines_photographer_add"))
+        # Page main title.
+        self.assertContains(response, "<h1>Add photographer</h1>", html=True)
+
+        # Headings for the toplevel fieldsets. The first one has no name.
+        self.assertContains(response, '<fieldset class="module aligned ">')
+        # The second and third have the same "Advanced options" name, but the
+        # second one has the "collapse" class.
+        for x, classes in ((1, ""), (2, "collapse")):
+            heading_id = f"fieldset-0-advanced-options-{x}-heading"
+            with self.subTest(heading_id=heading_id):
+                self.assertContains(
+                    response,
+                    f'<fieldset class="module aligned {classes}" '
+                    f'aria-labelledby="{heading_id}">',
+                )
+                self.assertContains(
+                    response,
+                    f'<h2 id="{heading_id}" class="fieldset-heading">'
+                    "Advanced options</h2>",
+                )
+                self.assertContains(response, f'id="{heading_id}"', count=1)
+
+        # Headings and subheadings for all the inlines.
+        for inline_admin_formset in response.context["inline_admin_formsets"]:
+            prefix = inline_admin_formset.formset.prefix
+            heading_id = f"{prefix}-heading"
+            formset_heading = (
+                f'<h2 id="{heading_id}" class="inline-heading">Photos</h2>'
+            )
+            self.assertContains(response, formset_heading, html=True)
+            self.assertContains(response, f'id="{heading_id}"', count=1)
+
+            # If this is a TabularInline, do not make further asserts since
+            # fieldsets are not shown as such in this table layout.
+            if "tabular" in inline_admin_formset.opts.template:
+                continue
+
+            if "collapse" in inline_admin_formset.classes:
+                formset_heading = f"<summary>{formset_heading}</summary>"
+                self.assertContains(response, formset_heading, html=True, count=1)
+
+            # Headings for every formset (the amount depends on `extra`).
+            for y, inline_admin_form in enumerate(inline_admin_formset):
+                y_plus_one = y + 1
+                form_heading = (
+                    f'<h3><b>Photo:</b> <span class="inline_label">#{y_plus_one}</span>'
+                    "</h3>"
+                )
+                self.assertContains(response, form_heading, html=True)
+
+                # Every fieldset defined for an inline's form.
+                for z, fieldset in enumerate(inline_admin_form):
+                    if fieldset.name:
+                        heading_id = f"{prefix}-{y}-details-{z}-heading"
+                        self.assertContains(
+                            response,
+                            f'<fieldset class="module aligned {fieldset.classes}" '
+                            f'aria-labelledby="{heading_id}">',
+                        )
+                        fieldset_heading = (
+                            f'<h4 id="{heading_id}" class="fieldset-heading">'
+                            f"Details</h4>"
+                        )
+                        self.assertContains(response, fieldset_heading)
+                        if "collapse" in fieldset.classes:
+                            self.assertContains(
+                                response,
+                                f"<summary>{fieldset_heading}</summary>",
+                                html=True,
+                            )
+                        self.assertContains(response, f'id="{heading_id}"', count=1)
+
+                    else:
+                        fieldset_html = (
+                            f'<fieldset class="module aligned {fieldset.classes}">'
+                        )
+                        self.assertContains(response, fieldset_html)
+
+
 @override_settings(ROOT_URLCONF="admin_inlines.urls")
 class SeleniumTests(AdminSeleniumTestCase):
     available_apps = ["admin_inlines"] + AdminSeleniumTestCase.available_apps
@@ -1850,10 +2209,11 @@ class SeleniumTests(AdminSeleniumTestCase):
             "form#profilecollection_form tr.dynamic-profile_set#profile_set-2", 1
         )
 
+    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
     def test_collapsed_inlines(self):
         from selenium.webdriver.common.by import By
 
-        # Collapsed inlines have SHOW/HIDE links.
+        # Collapsed inlines use details and summary elements.
         self.admin_login(username="super", password="secret")
         self.selenium.get(
             self.live_server_url + reverse("admin:admin_inlines_author_add")
@@ -1863,19 +2223,21 @@ class SeleniumTests(AdminSeleniumTestCase):
             "#id_nonautopkbook_set-0-title",
             "#id_nonautopkbook_set-2-0-title",
         ]
-        show_links = self.selenium.find_elements(By.LINK_TEXT, "SHOW")
-        self.assertEqual(len(show_links), 3)
+        summaries = self.selenium.find_elements(By.TAG_NAME, "summary")
+        self.assertEqual(len(summaries), 3)
+        self.take_screenshot("loaded")
         for show_index, field_name in enumerate(test_fields, 0):
             self.wait_until_invisible(field_name)
-            show_links[show_index].click()
+            summaries[show_index].click()
             self.wait_until_visible(field_name)
-        hide_links = self.selenium.find_elements(By.LINK_TEXT, "HIDE")
-        self.assertEqual(len(hide_links), 2)
+        self.take_screenshot("expanded")
         for hide_index, field_name in enumerate(test_fields, 0):
             self.wait_until_visible(field_name)
-            hide_links[hide_index].click()
+            summaries[hide_index].click()
             self.wait_until_invisible(field_name)
+        self.take_screenshot("collapsed")
 
+    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
     def test_added_stacked_inline_with_collapsed_fields(self):
         from selenium.webdriver.common.by import By
 
@@ -1883,20 +2245,22 @@ class SeleniumTests(AdminSeleniumTestCase):
         self.selenium.get(
             self.live_server_url + reverse("admin:admin_inlines_teacher_add")
         )
-        self.selenium.find_element(By.LINK_TEXT, "Add another Child").click()
+        add_text = gettext("Add another %(verbose_name)s") % {"verbose_name": "Child"}
+        self.selenium.find_element(By.LINK_TEXT, add_text).click()
         test_fields = ["#id_child_set-0-name", "#id_child_set-1-name"]
-        show_links = self.selenium.find_elements(By.LINK_TEXT, "SHOW")
-        self.assertEqual(len(show_links), 2)
+        summaries = self.selenium.find_elements(By.TAG_NAME, "summary")
+        self.assertEqual(len(summaries), 3)
+        self.take_screenshot("loaded")
         for show_index, field_name in enumerate(test_fields, 0):
             self.wait_until_invisible(field_name)
-            show_links[show_index].click()
+            summaries[show_index].click()
             self.wait_until_visible(field_name)
-        hide_links = self.selenium.find_elements(By.LINK_TEXT, "HIDE")
-        self.assertEqual(len(hide_links), 2)
+        self.take_screenshot("expanded")
         for hide_index, field_name in enumerate(test_fields, 0):
             self.wait_until_visible(field_name)
-            hide_links[hide_index].click()
+            summaries[hide_index].click()
             self.wait_until_invisible(field_name)
+        self.take_screenshot("collapsed")
 
     def assertBorder(self, element, border):
         width, style, color = border.split(" ")
@@ -1932,9 +2296,9 @@ class SeleniumTests(AdminSeleniumTestCase):
         self.wait_until_visible("#id_dummy")
         self.selenium.find_element(By.ID, "id_dummy").send_keys(1)
         fields = ["id_inner5stacked_set-0-dummy", "id_inner5tabular_set-0-dummy"]
-        show_links = self.selenium.find_elements(By.LINK_TEXT, "SHOW")
+        summaries = self.selenium.find_elements(By.TAG_NAME, "summary")
         for show_index, field_name in enumerate(fields):
-            show_links[show_index].click()
+            summaries[show_index].click()
             self.wait_until_visible("#" + field_name)
             self.selenium.find_element(By.ID, field_name).send_keys(1)
 
@@ -1972,49 +2336,40 @@ class SeleniumTests(AdminSeleniumTestCase):
         self.selenium.get(
             self.live_server_url + reverse("admin:admin_inlines_holder5_add")
         )
-        stacked_inline_formset_selector = (
-            "div#inner5stacked_set-group fieldset.module.collapse"
+        stacked_inline_details_selector = (
+            "div#inner5stacked_set-group fieldset.module.collapse details"
         )
-        tabular_inline_formset_selector = (
-            "div#inner5tabular_set-group fieldset.module.collapse"
+        tabular_inline_details_selector = (
+            "div#inner5tabular_set-group fieldset.module.collapse details"
         )
         # Inlines without errors, both inlines collapsed
         self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
         self.assertCountSeleniumElements(
-            stacked_inline_formset_selector + ".collapsed", 1
+            stacked_inline_details_selector + ":not([open])", 1
         )
         self.assertCountSeleniumElements(
-            tabular_inline_formset_selector + ".collapsed", 1
+            tabular_inline_details_selector + ":not([open])", 1
         )
-        show_links = self.selenium.find_elements(By.LINK_TEXT, "SHOW")
-        self.assertEqual(len(show_links), 2)
+        summaries = self.selenium.find_elements(By.TAG_NAME, "summary")
+        self.assertEqual(len(summaries), 2)
 
         # Inlines with errors, both inlines expanded
         test_fields = ["#id_inner5stacked_set-0-dummy", "#id_inner5tabular_set-0-dummy"]
         for show_index, field_name in enumerate(test_fields):
-            show_links[show_index].click()
+            summaries[show_index].click()
             self.wait_until_visible(field_name)
             self.selenium.find_element(By.ID, field_name[1:]).send_keys(1)
-        hide_links = self.selenium.find_elements(By.LINK_TEXT, "HIDE")
-        self.assertEqual(len(hide_links), 2)
         for hide_index, field_name in enumerate(test_fields):
-            hide_link = hide_links[hide_index]
+            summary = summaries[hide_index]
             self.selenium.execute_script(
-                "window.scrollTo(0, %s);" % hide_link.location["y"]
+                "window.scrollTo(0, %s);" % summary.location["y"]
             )
-            hide_link.click()
+            summary.click()
             self.wait_until_invisible(field_name)
         with self.wait_page_loaded():
             self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
-        with self.disable_implicit_wait():
-            self.assertCountSeleniumElements(
-                stacked_inline_formset_selector + ".collapsed", 0
-            )
-            self.assertCountSeleniumElements(
-                tabular_inline_formset_selector + ".collapsed", 0
-            )
-        self.assertCountSeleniumElements(stacked_inline_formset_selector, 1)
-        self.assertCountSeleniumElements(tabular_inline_formset_selector, 1)
+        self.assertCountSeleniumElements(stacked_inline_details_selector, 0)
+        self.assertCountSeleniumElements(tabular_inline_details_selector, 0)
 
     def test_inlines_verbose_name(self):
         """
diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py
index 50e8d4a3b1..2e77f2c97a 100644
--- a/tests/admin_scripts/tests.py
+++ b/tests/admin_scripts/tests.py
@@ -33,6 +33,7 @@ from django.db.migrations.recorder import MigrationRecorder
 from django.test import LiveServerTestCase, SimpleTestCase, TestCase, override_settings
 from django.test.utils import captured_stderr, captured_stdout
 from django.urls import path
+from django.utils.version import PY313
 from django.views.static import serve
 
 from . import urls
@@ -1901,10 +1902,16 @@ class CommandTypes(AdminScriptTestCase):
         ]
         for option in expected_options:
             self.assertOutput(out, f"[{option}]")
-        self.assertOutput(out, "--option_a OPTION_A, -a OPTION_A")
-        self.assertOutput(out, "--option_b OPTION_B, -b OPTION_B")
-        self.assertOutput(out, "--option_c OPTION_C, -c OPTION_C")
-        self.assertOutput(out, "-v {0,1,2,3}, --verbosity {0,1,2,3}")
+        if PY313:
+            self.assertOutput(out, "--option_a, -a OPTION_A")
+            self.assertOutput(out, "--option_b, -b OPTION_B")
+            self.assertOutput(out, "--option_c, -c OPTION_C")
+            self.assertOutput(out, "-v, --verbosity {0,1,2,3}")
+        else:
+            self.assertOutput(out, "--option_a OPTION_A, -a OPTION_A")
+            self.assertOutput(out, "--option_b OPTION_B, -b OPTION_B")
+            self.assertOutput(out, "--option_c OPTION_C, -c OPTION_C")
+            self.assertOutput(out, "-v {0,1,2,3}, --verbosity {0,1,2,3}")
 
     def test_color_style(self):
         style = color.no_style()
@@ -2294,6 +2301,35 @@ class Discovery(SimpleTestCase):
             self.assertEqual(out.getvalue().strip(), "simple_app")
 
 
+class CommandDBOptionChoiceTests(SimpleTestCase):
+    def test_invalid_choice_db_option(self):
+        expected_error = (
+            "Error: argument --database: invalid choice: "
+            "'deflaut' (choose from 'default', 'other')"
+        )
+        args = [
+            "changepassword",
+            "createsuperuser",
+            "remove_stale_contenttypes",
+            "check",
+            "createcachetable",
+            "dbshell",
+            "flush",
+            "dumpdata",
+            "inspectdb",
+            "loaddata",
+            "showmigrations",
+            "sqlflush",
+            "sqlmigrate",
+            "sqlsequencereset",
+            "migrate",
+        ]
+
+        for arg in args:
+            with self.assertRaisesMessage(CommandError, expected_error):
+                call_command(arg, "--database", "deflaut", verbosity=0)
+
+
 class ArgumentOrder(AdminScriptTestCase):
     """Tests for 2-stage argument parsing scheme.
 
@@ -2616,7 +2652,7 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
             urls.urlpatterns = old_urlpatterns
 
     def test_project_template_tarball_url(self):
-        """ "
+        """
         Startproject management command handles project template tar/zip balls
         from non-canonical urls.
         """
diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py
index 067b47198d..56d46324e0 100644
--- a/tests/admin_utils/tests.py
+++ b/tests/admin_utils/tests.py
@@ -137,6 +137,7 @@ class UtilsTests(SimpleTestCase):
             (simple_function, SIMPLE_FUNCTION),
             ("test_from_model", article.test_from_model()),
             ("non_field", INSTANCE_ATTRIBUTE),
+            ("site__domain", SITE_NAME),
         )
 
         mock_admin = MockModelAdmin()
@@ -294,6 +295,17 @@ class UtilsTests(SimpleTestCase):
 
         self.assertEqual(label_for_field(lambda x: "nothing", Article), "--")
         self.assertEqual(label_for_field("site_id", Article), "Site id")
+        # The correct name and attr are returned when `__` is in the field name.
+        self.assertEqual(label_for_field("site__domain", Article), "Site  domain")
+        self.assertEqual(
+            label_for_field("site__domain", Article, return_attr=True),
+            ("Site  domain", Site._meta.get_field("domain")),
+        )
+
+    def test_label_for_field_failed_lookup(self):
+        msg = "Unable to lookup 'site__unknown' on Article"
+        with self.assertRaisesMessage(AttributeError, msg):
+            label_for_field("site__unknown", Article)
 
         class MockModelAdmin:
             @admin.display(description="not Really the Model")
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index c157e70505..0ea64d594a 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -33,6 +33,8 @@ from .models import (
     Book,
     Bookmark,
     Box,
+    CamelCaseModel,
+    CamelCaseRelatedModel,
     Category,
     Chapter,
     ChapterXtra1,
@@ -1181,6 +1183,10 @@ class SquareAdmin(admin.ModelAdmin):
     readonly_fields = ("area",)
 
 
+class CamelCaseAdmin(admin.ModelAdmin):
+    filter_horizontal = ["m2m"]
+
+
 site = admin.AdminSite(name="admin")
 site.site_url = "/my-site-url/"
 site.register(Article, ArticleAdmin)
@@ -1305,6 +1311,8 @@ site.register(Box)
 site.register(Country, CountryAdmin)
 site.register(Traveler, TravelerAdmin)
 site.register(Square, SquareAdmin)
+site.register(CamelCaseModel)
+site.register(CamelCaseRelatedModel, CamelCaseAdmin)
 
 # Register core models we need in our tests
 site.register(User, UserAdmin)
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index bd2dc65d2e..812505de82 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -83,6 +83,9 @@ class Book(models.Model):
     def __str__(self):
         return self.name
 
+    def get_absolute_url(self):
+        return f"/books/{self.id}/"
+
 
 class Promo(models.Model):
     name = models.CharField(max_length=100, verbose_name="¿Name?")
@@ -1152,3 +1155,15 @@ class Square(models.Model):
 
     class Meta:
         required_db_features = {"supports_stored_generated_columns"}
+
+
+class CamelCaseModel(models.Model):
+    interesting_name = models.CharField(max_length=100)
+
+    def __str__(self):
+        return self.interesting_name
+
+
+class CamelCaseRelatedModel(models.Model):
+    m2m = models.ManyToManyField(CamelCaseModel, related_name="m2m")
+    fk = models.ForeignKey(CamelCaseModel, on_delete=models.CASCADE, related_name="fk")
diff --git a/tests/admin_views/templates/admin/base_site.html b/tests/admin_views/templates/admin/base_site.html
index 9714571289..27158e9e44 100644
--- a/tests/admin_views/templates/admin/base_site.html
+++ b/tests/admin_views/templates/admin/base_site.html
@@ -1,3 +1,5 @@
 {% extends "admin/base.html" %}
 
 {% block bodyclass %}bodyclass_consistency_check{% endblock %}
+
+{% block extrabody %}extrabody_check{% endblock extrabody %}
diff --git a/tests/admin_views/test_adminsite.py b/tests/admin_views/test_adminsite.py
index 68a32567d8..7c4841f916 100644
--- a/tests/admin_views/test_adminsite.py
+++ b/tests/admin_views/test_adminsite.py
@@ -11,8 +11,19 @@ site = admin.AdminSite(name="test_adminsite")
 site.register(User)
 site.register(Article)
 
+
+class CustomAdminSite(admin.AdminSite):
+    site_title = "Custom title"
+    site_header = "Custom site"
+
+
+custom_site = CustomAdminSite(name="test_custom_adminsite")
+custom_site.register(User)
+
+
 urlpatterns = [
     path("test_admin/admin/", site.urls),
+    path("test_custom_admin/admin/", custom_site.urls),
 ]
 
 
@@ -43,6 +54,13 @@ class SiteEachContextTest(TestCase):
         self.assertEqual(ctx["site_url"], "/")
         self.assertIs(ctx["has_permission"], True)
 
+    def test_custom_admin_titles(self):
+        request = self.request_factory.get(reverse("test_custom_adminsite:index"))
+        request.user = self.u1
+        ctx = custom_site.each_context(request)
+        self.assertEqual(ctx["site_title"], "Custom title")
+        self.assertEqual(ctx["site_header"], "Custom site")
+
     def test_each_context_site_url_with_script_name(self):
         request = self.request_factory.get(
             reverse("test_adminsite:index"), SCRIPT_NAME="/my-script-name/"
diff --git a/tests/admin_views/test_autocomplete_view.py b/tests/admin_views/test_autocomplete_view.py
index 8f56dd8314..dc3789fc5b 100644
--- a/tests/admin_views/test_autocomplete_view.py
+++ b/tests/admin_views/test_autocomplete_view.py
@@ -576,7 +576,8 @@ class SeleniumTests(AdminSeleniumTestCase):
 
         def assertNoResults(row):
             elem = row.find_element(By.CSS_SELECTOR, ".select2-selection")
-            elem.click()  # Open the autocomplete dropdown.
+            with self.select2_ajax_wait():
+                elem.click()  # Open the autocomplete dropdown.
             results = self.selenium.find_element(By.CSS_SELECTOR, ".select2-results")
             self.assertTrue(results.is_displayed())
             option = self.selenium.find_element(
diff --git a/tests/admin_views/test_multidb.py b/tests/admin_views/test_multidb.py
index d868321a4b..654161e11d 100644
--- a/tests/admin_views/test_multidb.py
+++ b/tests/admin_views/test_multidb.py
@@ -2,6 +2,8 @@ from unittest import mock
 
 from django.contrib import admin
 from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.http import HttpResponse
 from django.test import TestCase, override_settings
 from django.urls import path, reverse
 
@@ -23,8 +25,15 @@ class Router:
 site = admin.AdminSite(name="test_adminsite")
 site.register(Book)
 
+
+def book(request, book_id):
+    b = Book.objects.get(id=book_id)
+    return HttpResponse(b.title)
+
+
 urlpatterns = [
     path("admin/", site.urls),
+    path("books/<book_id>/", book),
 ]
 
 
@@ -88,3 +97,47 @@ class MultiDatabaseTests(TestCase):
                     {"post": "yes"},
                 )
                 mock.atomic.assert_called_with(using=db)
+
+
+class ViewOnSiteRouter:
+    def db_for_read(self, model, instance=None, **hints):
+        if model._meta.app_label in {"auth", "sessions", "contenttypes"}:
+            return "default"
+        return "other"
+
+    def db_for_write(self, model, **hints):
+        if model._meta.app_label in {"auth", "sessions", "contenttypes"}:
+            return "default"
+        return "other"
+
+    def allow_relation(self, obj1, obj2, **hints):
+        return obj1._state.db in {"default", "other"} and obj2._state.db in {
+            "default",
+            "other",
+        }
+
+    def allow_migrate(self, db, app_label, **hints):
+        return True
+
+
+@override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=[ViewOnSiteRouter()])
+class ViewOnSiteTests(TestCase):
+    databases = {"default", "other"}
+
+    def test_contenttype_in_separate_db(self):
+        ContentType.objects.using("other").all().delete()
+        book = Book.objects.using("other").create(name="other book")
+        user = User.objects.create_superuser(
+            username="super", password="secret", email="super@example.com"
+        )
+
+        book_type = ContentType.objects.get(app_label="admin_views", model="book")
+
+        self.client.force_login(user)
+
+        shortcut_url = reverse("admin:view_on_site", args=(book_type.pk, book.id))
+        response = self.client.get(shortcut_url, follow=False)
+        self.assertEqual(response.status_code, 302)
+        self.assertRegex(
+            response.url, f"http://(testserver|example.com)/books/{book.id}/"
+        )
diff --git a/tests/admin_views/test_password_form.py b/tests/admin_views/test_password_form.py
new file mode 100644
index 0000000000..d448943b04
--- /dev/null
+++ b/tests/admin_views/test_password_form.py
@@ -0,0 +1,144 @@
+from django.contrib.admin.tests import AdminSeleniumTestCase
+from django.contrib.auth.models import User
+from django.test import override_settings
+from django.urls import reverse
+
+
+@override_settings(ROOT_URLCONF="auth_tests.urls_admin")
+class SeleniumAuthTests(AdminSeleniumTestCase):
+    available_apps = AdminSeleniumTestCase.available_apps
+
+    def setUp(self):
+        self.superuser = User.objects.create_superuser(
+            username="super",
+            password="secret",
+            email="super@example.com",
+        )
+
+    def test_add_new_user(self):
+        """A user with no password can be added.
+
+        Enabling/disabling the usable password field shows/hides the password
+        fields when adding a user.
+        """
+        from selenium.common import NoSuchElementException
+        from selenium.webdriver.common.by import By
+
+        user_add_url = reverse("auth_test_admin:auth_user_add")
+        self.admin_login(username="super", password="secret")
+        self.selenium.get(self.live_server_url + user_add_url)
+
+        pw_switch_on = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="usable_password"][value="true"]'
+        )
+        pw_switch_off = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="usable_password"][value="false"]'
+        )
+        password1 = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="password1"]'
+        )
+        password2 = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="password2"]'
+        )
+
+        # Default is to set a password on user creation.
+        self.assertIs(pw_switch_on.is_selected(), True)
+        self.assertIs(pw_switch_off.is_selected(), False)
+
+        # The password fields are visible.
+        self.assertIs(password1.is_displayed(), True)
+        self.assertIs(password2.is_displayed(), True)
+
+        # Click to disable password-based authentication.
+        pw_switch_off.click()
+
+        # Radio buttons are updated accordingly.
+        self.assertIs(pw_switch_on.is_selected(), False)
+        self.assertIs(pw_switch_off.is_selected(), True)
+
+        # The password fields are hidden.
+        self.assertIs(password1.is_displayed(), False)
+        self.assertIs(password2.is_displayed(), False)
+
+        # The warning message should not be shown.
+        with self.assertRaises(NoSuchElementException):
+            self.selenium.find_element(By.ID, "id_unusable_warning")
+
+    def test_change_password_for_existing_user(self):
+        """A user can have their password changed or unset.
+
+        Enabling/disabling the usable password field shows/hides the password
+        fields and the warning about password lost.
+        """
+        from selenium.webdriver.common.by import By
+
+        user = User.objects.create_user(
+            username="ada", password="charles", email="ada@example.com"
+        )
+        user_url = reverse("auth_test_admin:auth_user_password_change", args=(user.pk,))
+        self.admin_login(username="super", password="secret")
+        self.selenium.get(self.live_server_url + user_url)
+
+        pw_switch_on = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="usable_password"][value="true"]'
+        )
+        pw_switch_off = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="usable_password"][value="false"]'
+        )
+        password1 = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="password1"]'
+        )
+        password2 = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[name="password2"]'
+        )
+        submit_set = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[type="submit"].set-password'
+        )
+        submit_unset = self.selenium.find_element(
+            By.CSS_SELECTOR, 'input[type="submit"].unset-password'
+        )
+
+        # By default password-based authentication is enabled.
+        self.assertIs(pw_switch_on.is_selected(), True)
+        self.assertIs(pw_switch_off.is_selected(), False)
+
+        # The password fields are visible.
+        self.assertIs(password1.is_displayed(), True)
+        self.assertIs(password2.is_displayed(), True)
+
+        # Only the set password submit button is visible.
+        self.assertIs(submit_set.is_displayed(), True)
+        self.assertIs(submit_unset.is_displayed(), False)
+
+        # Click to disable password-based authentication.
+        pw_switch_off.click()
+
+        # Radio buttons are updated accordingly.
+        self.assertIs(pw_switch_on.is_selected(), False)
+        self.assertIs(pw_switch_off.is_selected(), True)
+
+        # The password fields are hidden.
+        self.assertIs(password1.is_displayed(), False)
+        self.assertIs(password2.is_displayed(), False)
+
+        # Only the unset password submit button is visible.
+        self.assertIs(submit_unset.is_displayed(), True)
+        self.assertIs(submit_set.is_displayed(), False)
+
+        # The warning about password being lost is shown.
+        warning = self.selenium.find_element(By.ID, "id_unusable_warning")
+        self.assertIs(warning.is_displayed(), True)
+
+        # Click to enable password-based authentication.
+        pw_switch_on.click()
+
+        # The warning disappears.
+        self.assertIs(warning.is_displayed(), False)
+
+        # The password fields are shown.
+        self.assertIs(password1.is_displayed(), True)
+        self.assertIs(password2.is_displayed(), True)
+
+        # Only the set password submit button is visible.
+        self.assertIs(submit_set.is_displayed(), True)
+        self.assertIs(submit_unset.is_displayed(), False)
diff --git a/tests/admin_views/test_related_object_lookups.py b/tests/admin_views/test_related_object_lookups.py
index 145bf0d6de..761819a50f 100644
--- a/tests/admin_views/test_related_object_lookups.py
+++ b/tests/admin_views/test_related_object_lookups.py
@@ -76,3 +76,47 @@ class SeleniumTests(AdminSeleniumTestCase):
             with self.subTest(link_id):
                 link = self.selenium.find_element(By.XPATH, f'//*[@id="{link_id}"]')
                 self.assertIsNone(link.get_attribute("aria-disabled"))
+
+    def test_related_object_update_with_camel_casing(self):
+        from selenium.webdriver.common.by import By
+
+        add_url = reverse("admin:admin_views_camelcaserelatedmodel_add")
+        self.selenium.get(self.live_server_url + add_url)
+        interesting_name = "A test name"
+
+        # Add a new CamelCaseModel using the "+" icon next to the "fk" field.
+        self.selenium.find_element(By.ID, "add_id_fk").click()
+
+        # Switch to the add popup window.
+        self.wait_for_and_switch_to_popup()
+
+        # Find the "interesting_name" field and enter a value, then save it.
+        self.selenium.find_element(By.ID, "id_interesting_name").send_keys(
+            interesting_name
+        )
+        self.selenium.find_element(By.NAME, "_save").click()
+
+        # Return to the main window.
+        self.wait_until(lambda d: len(d.window_handles) == 1, 1)
+        self.selenium.switch_to.window(self.selenium.window_handles[0])
+
+        # Check that both the "Available" m2m box and the "Fk" dropdown now
+        # include the newly added CamelCaseModel instance.
+        fk_dropdown = self.selenium.find_element(By.ID, "id_fk")
+        self.assertHTMLEqual(
+            fk_dropdown.get_attribute("innerHTML"),
+            f"""
+            <option value="" selected="">---------</option>
+            <option value="1" selected>{interesting_name}</option>
+            """,
+        )
+        # Check the newly added instance is not also added in the "to" box.
+        m2m_to = self.selenium.find_element(By.ID, "id_m2m_to")
+        self.assertHTMLEqual(m2m_to.get_attribute("innerHTML"), "")
+        m2m_box = self.selenium.find_element(By.ID, "id_m2m_from")
+        self.assertHTMLEqual(
+            m2m_box.get_attribute("innerHTML"),
+            f"""
+            <option value="1">{interesting_name}</option>
+            """,
+        )
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 5877abf781..763fa44ce8 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -4,7 +4,7 @@ import re
 import unittest
 import zoneinfo
 from unittest import mock
-from urllib.parse import parse_qsl, urljoin, urlparse
+from urllib.parse import parse_qsl, urljoin, urlsplit
 
 from django import forms
 from django.contrib import admin
@@ -357,7 +357,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
                             **save_option,
                         },
                     )
-                    parsed_url = urlparse(response.url)
+                    parsed_url = urlsplit(response.url)
                     self.assertEqual(parsed_url.query, qsl)
 
     def test_change_query_string_persists(self):
@@ -386,7 +386,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
                             **save_option,
                         },
                     )
-                    parsed_url = urlparse(response.url)
+                    parsed_url = urlsplit(response.url)
                     self.assertEqual(parsed_url.query, qsl)
 
     def test_basic_edit_GET(self):
@@ -1746,6 +1746,10 @@ class AdminCustomTemplateTests(AdminViewBasicTestCase):
         response = self.client.get(reverse("admin:admin_views_section_add"))
         self.assertContains(response, "bodyclass_consistency_check ")
 
+    def test_extended_extrabody(self):
+        response = self.client.get(reverse("admin:admin_views_section_add"))
+        self.assertContains(response, "extrabody_check\n</body>")
+
     def test_change_password_template(self):
         user = User.objects.get(username="super")
         response = self.client.get(
@@ -1939,7 +1943,6 @@ class AdminJavaScriptTest(TestCase):
             self.assertContains(response, "vendor/jquery/jquery.min.js")
             self.assertContains(response, "prepopulate.js")
             self.assertContains(response, "actions.js")
-            self.assertContains(response, "collapse.js")
             self.assertContains(response, "inlines.js")
         with override_settings(DEBUG=True):
             response = self.client.get(reverse("admin:admin_views_section_add"))
@@ -1947,7 +1950,6 @@ class AdminJavaScriptTest(TestCase):
             self.assertNotContains(response, "vendor/jquery/jquery.min.js")
             self.assertContains(response, "prepopulate.js")
             self.assertContains(response, "actions.js")
-            self.assertContains(response, "collapse.js")
             self.assertContains(response, "inlines.js")
 
 
@@ -3927,7 +3929,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
                 )
 
     def test_deleteconfirmation_link(self):
-        """ "
+        """
         The link from the delete confirmation page referring back to the
         changeform of the object should be quoted.
         """
@@ -5764,7 +5766,7 @@ class SeleniumTests(AdminSeleniumTestCase):
             title="A Long Title", published=True, slug="a-long-title"
         )
 
-    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark"])
+    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
     def test_login_button_centered(self):
         from selenium.webdriver.common.by import By
 
@@ -6070,7 +6072,7 @@ class SeleniumTests(AdminSeleniumTestCase):
         self.assertEqual(slug1, "this-is-the-main-name-the-best-2012-02-18")
         self.assertEqual(slug2, "option-two-this-is-the-main-name-the-best")
 
-    @screenshot_cases(["desktop_size", "mobile_size", "dark"])
+    @screenshot_cases(["desktop_size", "mobile_size", "dark", "high_contrast"])
     def test_collapsible_fieldset(self):
         """
         The 'collapse' class in fieldsets definition allows to
@@ -6086,14 +6088,11 @@ class SeleniumTests(AdminSeleniumTestCase):
         )
         self.assertFalse(self.selenium.find_element(By.ID, "id_title").is_displayed())
         self.take_screenshot("collapsed")
-        self.selenium.find_elements(By.LINK_TEXT, "Show")[0].click()
+        self.selenium.find_elements(By.TAG_NAME, "summary")[0].click()
         self.assertTrue(self.selenium.find_element(By.ID, "id_title").is_displayed())
-        self.assertEqual(
-            self.selenium.find_element(By.ID, "fieldsetcollapser0").text, "Hide"
-        )
         self.take_screenshot("expanded")
 
-    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark"])
+    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
     def test_selectbox_height_collapsible_fieldset(self):
         from selenium.webdriver.common.by import By
 
@@ -6104,7 +6103,7 @@ class SeleniumTests(AdminSeleniumTestCase):
         )
         url = self.live_server_url + reverse("admin7:admin_views_pizza_add")
         self.selenium.get(url)
-        self.selenium.find_elements(By.ID, "fieldsetcollapser0")[0].click()
+        self.selenium.find_elements(By.TAG_NAME, "summary")[0].click()
         from_filter_box = self.selenium.find_element(By.ID, "id_toppings_filter")
         from_box = self.selenium.find_element(By.ID, "id_toppings_from")
         to_filter_box = self.selenium.find_element(By.ID, "id_toppings_filter_selected")
@@ -6121,7 +6120,7 @@ class SeleniumTests(AdminSeleniumTestCase):
         )
         self.take_screenshot("selectbox-collapsible")
 
-    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark"])
+    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
     def test_selectbox_height_not_collapsible_fieldset(self):
         from selenium.webdriver.common.by import By
 
@@ -6152,7 +6151,7 @@ class SeleniumTests(AdminSeleniumTestCase):
         )
         self.take_screenshot("selectbox-non-collapsible")
 
-    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark"])
+    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
     def test_first_field_focus(self):
         """JavaScript-assisted auto-focus on first usable form field."""
         from selenium.webdriver.common.by import By
@@ -8037,11 +8036,11 @@ class AdminKeepChangeListFiltersTests(TestCase):
         Assert that two URLs are equal despite the ordering
         of their querystring. Refs #22360.
         """
-        parsed_url1 = urlparse(url1)
+        parsed_url1 = urlsplit(url1)
         path1 = parsed_url1.path
         parsed_qs1 = dict(parse_qsl(parsed_url1.query))
 
-        parsed_url2 = urlparse(url2)
+        parsed_url2 = urlsplit(url2)
         path2 = parsed_url2.path
         parsed_qs2 = dict(parse_qsl(parsed_url2.query))
 
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 50c26095ff..6f009a6f3f 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -590,6 +590,19 @@ class AdminFileWidgetTests(TestDataMixin, TestCase):
             '<input type="file" name="test">',
         )
 
+    def test_render_with_attrs_id(self):
+        storage_url = default_storage.url("")
+        w = widgets.AdminFileWidget()
+        self.assertHTMLEqual(
+            w.render("test", self.album.cover_art, attrs={"id": "test_id"}),
+            f'<p class="file-upload">Currently: <a href="{storage_url}albums/'
+            r'hybrid_theory.jpg">albums\hybrid_theory.jpg</a> '
+            '<span class="clearable-file-input">'
+            '<input type="checkbox" name="test-clear" id="test-clear_id"> '
+            '<label for="test-clear_id">Clear</label></span><br>'
+            'Change: <input type="file" name="test" id="test_id"></p>',
+        )
+
     def test_render_required(self):
         widget = widgets.AdminFileWidget()
         widget.is_required = True
@@ -618,6 +631,20 @@ class AdminFileWidgetTests(TestDataMixin, TestCase):
             },
         )
 
+    def test_render_checked(self):
+        storage_url = default_storage.url("")
+        widget = widgets.AdminFileWidget()
+        widget.checked = True
+        self.assertHTMLEqual(
+            widget.render("test", self.album.cover_art),
+            f'<p class="file-upload">Currently: <a href="{storage_url}albums/'
+            r'hybrid_theory.jpg">albums\hybrid_theory.jpg</a> '
+            '<span class="clearable-file-input">'
+            '<input type="checkbox" name="test-clear" id="test-clear_id" checked>'
+            '<label for="test-clear_id">Clear</label></span><br>'
+            'Change: <input type="file" name="test" checked></p>',
+        )
+
     def test_readonly_fields(self):
         """
         File widgets should render as a link when they're marked "read only."
@@ -910,6 +937,28 @@ class RelatedFieldWidgetWrapperTests(SimpleTestCase):
         # Related item links are present.
         self.assertIn("<a ", output)
 
+    def test_data_model_ref_when_model_name_is_camel_case(self):
+        rel = VideoStream._meta.get_field("release_event").remote_field
+        widget = forms.Select()
+        wrapper = widgets.RelatedFieldWidgetWrapper(widget, rel, widget_admin_site)
+        self.assertIs(wrapper.is_hidden, False)
+        context = wrapper.get_context("release_event", None, {})
+        self.assertEqual(context["model"], "release event")
+        self.assertEqual(context["model_name"], "releaseevent")
+        output = wrapper.render("stream", "value")
+        expected = """
+        <div class="related-widget-wrapper" data-model-ref="releaseevent">
+          <select name="stream" data-context="available-source">
+          </select>
+          <a class="related-widget-wrapper-link add-related" id="add_id_stream"
+             data-popup="yes" title="Add another release event"
+             href="/admin_widgets/releaseevent/add/?_to_field=album&amp;_popup=1">
+            <img src="/static/admin/img/icon-addlink.svg" alt="" width="20" height="20">
+          </a>
+        </div>
+        """
+        self.assertHTMLEqual(output, expected)
+
 
 @override_settings(ROOT_URLCONF="admin_widgets.urls")
 class AdminWidgetSeleniumTestCase(AdminSeleniumTestCase):
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
index 4408535228..075e707102 100644
--- a/tests/aggregation/tests.py
+++ b/tests/aggregation/tests.py
@@ -1291,7 +1291,7 @@ class AggregateTestCase(TestCase):
 
             def as_sql(self, compiler, connection):
                 copy = self.copy()
-                copy.set_source_expressions(copy.get_source_expressions()[0:1])
+                copy.set_source_expressions(copy.get_source_expressions()[0:1] + [None])
                 return super(MyMax, copy).as_sql(compiler, connection)
 
         with self.assertRaisesMessage(TypeError, "Complex aggregates require an alias"):
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
index 963f45f798..658e9d853e 100644
--- a/tests/asgi/tests.py
+++ b/tests/asgi/tests.py
@@ -475,6 +475,7 @@ class ASGITest(SimpleTestCase):
         sync_waiter.active_threads.clear()
 
     async def test_asyncio_cancel_error(self):
+        view_started = asyncio.Event()
         # Flag to check if the view was cancelled.
         view_did_cancel = False
         # Track request_finished signal.
@@ -484,9 +485,10 @@ class ASGITest(SimpleTestCase):
 
         # A view that will listen for the cancelled error.
         async def view(request):
-            nonlocal view_did_cancel
+            nonlocal view_started, view_did_cancel
+            view_started.set()
             try:
-                await asyncio.sleep(0.2)
+                await asyncio.sleep(0.1)
                 return HttpResponse("Hello World!")
             except asyncio.CancelledError:
                 # Set the flag.
@@ -522,6 +524,7 @@ class ASGITest(SimpleTestCase):
         self.assertNotEqual(handler_call["thread"], threading.current_thread())
         # The signal sender is the handler class.
         self.assertEqual(handler_call["kwargs"], {"sender": TestASGIHandler})
+        view_started.clear()
 
         # Request cycle with a disconnect before the view can respond.
         application = TestASGIHandler()
@@ -529,7 +532,7 @@ class ASGITest(SimpleTestCase):
         communicator = ApplicationCommunicator(application, scope)
         await communicator.send_input({"type": "http.request"})
         # Let the view actually start.
-        await asyncio.sleep(0.1)
+        await view_started.wait()
         # Disconnect the client.
         await communicator.send_input({"type": "http.disconnect"})
         # The handler should not send a response.
diff --git a/tests/auth_tests/test_checks.py b/tests/auth_tests/test_checks.py
index 5757946f95..3d70451e9d 100644
--- a/tests/auth_tests/test_checks.py
+++ b/tests/auth_tests/test_checks.py
@@ -1,5 +1,14 @@
-from django.contrib.auth.checks import check_models_permissions, check_user_model
+from django.contrib.auth.checks import (
+    check_middleware,
+    check_models_permissions,
+    check_user_model,
+)
+from django.contrib.auth.middleware import (
+    AuthenticationMiddleware,
+    LoginRequiredMiddleware,
+)
 from django.contrib.auth.models import AbstractBaseUser
+from django.contrib.sessions.middleware import SessionMiddleware
 from django.core import checks
 from django.db import models
 from django.db.models import Q, UniqueConstraint
@@ -345,3 +354,102 @@ class ModelsPermissionsChecksTests(SimpleTestCase):
                 default_permissions = ()
 
         self.assertEqual(checks.run_checks(self.apps.get_app_configs()), [])
+
+
+class LoginRequiredMiddlewareSubclass(LoginRequiredMiddleware):
+    redirect_field_name = "redirect_to"
+
+
+class AuthenticationMiddlewareSubclass(AuthenticationMiddleware):
+    pass
+
+
+class SessionMiddlewareSubclass(SessionMiddleware):
+    pass
+
+
+@override_system_checks([check_middleware])
+class MiddlewareChecksTests(SimpleTestCase):
+    @override_settings(
+        MIDDLEWARE=[
+            "auth_tests.test_checks.SessionMiddlewareSubclass",
+            "auth_tests.test_checks.AuthenticationMiddlewareSubclass",
+            "auth_tests.test_checks.LoginRequiredMiddlewareSubclass",
+        ]
+    )
+    def test_middleware_subclasses(self):
+        errors = checks.run_checks()
+        self.assertEqual(errors, [])
+
+    @override_settings(
+        MIDDLEWARE=[
+            "auth_tests.test_checks",
+            "auth_tests.test_checks.NotExist",
+        ]
+    )
+    def test_invalid_middleware_skipped(self):
+        errors = checks.run_checks()
+        self.assertEqual(errors, [])
+
+    @override_settings(
+        MIDDLEWARE=[
+            "django.contrib.does.not.Exist",
+            "django.contrib.sessions.middleware.SessionMiddleware",
+            "django.contrib.auth.middleware.AuthenticationMiddleware",
+            "django.contrib.auth.middleware.LoginRequiredMiddleware",
+        ]
+    )
+    def test_check_ignores_import_error_in_middleware(self):
+        errors = checks.run_checks()
+        self.assertEqual(errors, [])
+
+    @override_settings(
+        MIDDLEWARE=[
+            "django.contrib.sessions.middleware.SessionMiddleware",
+            "django.contrib.auth.middleware.AuthenticationMiddleware",
+            "django.contrib.auth.middleware.LoginRequiredMiddleware",
+        ]
+    )
+    def test_correct_order_with_login_required_middleware(self):
+        errors = checks.run_checks()
+        self.assertEqual(errors, [])
+
+    @override_settings(
+        MIDDLEWARE=[
+            "django.contrib.auth.middleware.LoginRequiredMiddleware",
+            "django.contrib.auth.middleware.AuthenticationMiddleware",
+            "django.contrib.sessions.middleware.SessionMiddleware",
+        ]
+    )
+    def test_incorrect_order_with_login_required_middleware(self):
+        errors = checks.run_checks()
+        self.assertEqual(
+            errors,
+            [
+                checks.Error(
+                    "In order to use django.contrib.auth.middleware."
+                    "LoginRequiredMiddleware, django.contrib.auth.middleware."
+                    "AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
+                    id="auth.E013",
+                )
+            ],
+        )
+
+    @override_settings(
+        MIDDLEWARE=[
+            "django.contrib.auth.middleware.LoginRequiredMiddleware",
+        ]
+    )
+    def test_missing_authentication_with_login_required_middleware(self):
+        errors = checks.run_checks()
+        self.assertEqual(
+            errors,
+            [
+                checks.Error(
+                    "In order to use django.contrib.auth.middleware."
+                    "LoginRequiredMiddleware, django.contrib.auth.middleware."
+                    "AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
+                    id="auth.E013",
+                )
+            ],
+        )
diff --git a/tests/auth_tests/test_decorators.py b/tests/auth_tests/test_decorators.py
index ae43adcb0a..e585b28bd5 100644
--- a/tests/auth_tests/test_decorators.py
+++ b/tests/auth_tests/test_decorators.py
@@ -1,6 +1,15 @@
+from asyncio import iscoroutinefunction
+
+from asgiref.sync import sync_to_async
+
 from django.conf import settings
 from django.contrib.auth import models
-from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.decorators import (
+    login_not_required,
+    login_required,
+    permission_required,
+    user_passes_test,
+)
 from django.core.exceptions import PermissionDenied
 from django.http import HttpResponse
 from django.test import TestCase, override_settings
@@ -15,6 +24,22 @@ class LoginRequiredTestCase(AuthViewsTestCase):
     Tests the login_required decorators
     """
 
+    factory = RequestFactory()
+
+    def test_wrapped_sync_function_is_not_coroutine_function(self):
+        def sync_view(request):
+            return HttpResponse()
+
+        wrapped_view = login_required(sync_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), False)
+
+    def test_wrapped_async_function_is_coroutine_function(self):
+        async def async_view(request):
+            return HttpResponse()
+
+        wrapped_view = login_required(async_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), True)
+
     def test_callable(self):
         """
         login_required is assignable to callable objects.
@@ -59,6 +84,69 @@ class LoginRequiredTestCase(AuthViewsTestCase):
             view_url="/login_required_login_url/", login_url="/somewhere/"
         )
 
+    async def test_login_required_async_view(self, login_url=None):
+        async def async_view(request):
+            return HttpResponse()
+
+        async def auser_anonymous():
+            return models.AnonymousUser()
+
+        async def auser():
+            return self.u1
+
+        if login_url is None:
+            async_view = login_required(async_view)
+            login_url = settings.LOGIN_URL
+        else:
+            async_view = login_required(async_view, login_url=login_url)
+
+        request = self.factory.get("/rand")
+        request.auser = auser_anonymous
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 302)
+        self.assertIn(login_url, response.url)
+
+        request.auser = auser
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 200)
+
+    async def test_login_required_next_url_async_view(self):
+        await self.test_login_required_async_view(login_url="/somewhere/")
+
+
+class LoginNotRequiredTestCase(TestCase):
+    """
+    Tests the login_not_required decorators
+    """
+
+    def test_callable(self):
+        """
+        login_not_required is assignable to callable objects.
+        """
+
+        class CallableView:
+            def __call__(self, *args, **kwargs):
+                pass
+
+        login_not_required(CallableView())
+
+    def test_view(self):
+        """
+        login_not_required is assignable to normal views.
+        """
+
+        def normal_view(request):
+            pass
+
+        login_not_required(normal_view)
+
+    def test_decorator_marks_view_as_login_not_required(self):
+        @login_not_required
+        def view(request):
+            return HttpResponse()
+
+        self.assertFalse(view.login_required)
+
 
 class PermissionsRequiredDecoratorTest(TestCase):
     """
@@ -76,6 +164,24 @@ class PermissionsRequiredDecoratorTest(TestCase):
         )
         cls.user.user_permissions.add(*perms)
 
+    @classmethod
+    async def auser(cls):
+        return cls.user
+
+    def test_wrapped_sync_function_is_not_coroutine_function(self):
+        def sync_view(request):
+            return HttpResponse()
+
+        wrapped_view = permission_required([])(sync_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), False)
+
+    def test_wrapped_async_function_is_coroutine_function(self):
+        async def async_view(request):
+            return HttpResponse()
+
+        wrapped_view = permission_required([])(async_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), True)
+
     def test_many_permissions_pass(self):
         @permission_required(
             ["auth_tests.add_customuser", "auth_tests.change_customuser"]
@@ -142,3 +248,179 @@ class PermissionsRequiredDecoratorTest(TestCase):
         request.user = self.user
         with self.assertRaises(PermissionDenied):
             a_view(request)
+
+    async def test_many_permissions_pass_async_view(self):
+        @permission_required(
+            ["auth_tests.add_customuser", "auth_tests.change_customuser"]
+        )
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 200)
+
+    async def test_many_permissions_in_set_pass_async_view(self):
+        @permission_required(
+            {"auth_tests.add_customuser", "auth_tests.change_customuser"}
+        )
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 200)
+
+    async def test_single_permission_pass_async_view(self):
+        @permission_required("auth_tests.add_customuser")
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 200)
+
+    async def test_permissioned_denied_redirect_async_view(self):
+        @permission_required(
+            [
+                "auth_tests.add_customuser",
+                "auth_tests.change_customuser",
+                "nonexistent-permission",
+            ]
+        )
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 302)
+
+    async def test_permissioned_denied_exception_raised_async_view(self):
+        @permission_required(
+            [
+                "auth_tests.add_customuser",
+                "auth_tests.change_customuser",
+                "nonexistent-permission",
+            ],
+            raise_exception=True,
+        )
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser
+        with self.assertRaises(PermissionDenied):
+            await async_view(request)
+
+
+class UserPassesTestDecoratorTest(TestCase):
+    factory = RequestFactory()
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.user_pass = models.User.objects.create(username="joe", password="qwerty")
+        cls.user_deny = models.User.objects.create(username="jim", password="qwerty")
+        models.Group.objects.create(name="Joe group")
+        # Add permissions auth.add_customuser and auth.change_customuser
+        perms = models.Permission.objects.filter(
+            codename__in=("add_customuser", "change_customuser")
+        )
+        cls.user_pass.user_permissions.add(*perms)
+
+    @classmethod
+    async def auser_pass(cls):
+        return cls.user_pass
+
+    @classmethod
+    async def auser_deny(cls):
+        return cls.user_deny
+
+    def test_wrapped_sync_function_is_not_coroutine_function(self):
+        def sync_view(request):
+            return HttpResponse()
+
+        wrapped_view = user_passes_test(lambda user: True)(sync_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), False)
+
+    def test_wrapped_async_function_is_coroutine_function(self):
+        async def async_view(request):
+            return HttpResponse()
+
+        wrapped_view = user_passes_test(lambda user: True)(async_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), True)
+
+    def test_decorator(self):
+        def sync_test_func(user):
+            return bool(
+                models.Group.objects.filter(name__istartswith=user.username).exists()
+            )
+
+        @user_passes_test(sync_test_func)
+        def sync_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.user = self.user_pass
+        response = sync_view(request)
+        self.assertEqual(response.status_code, 200)
+
+        request.user = self.user_deny
+        response = sync_view(request)
+        self.assertEqual(response.status_code, 302)
+
+    def test_decorator_async_test_func(self):
+        async def async_test_func(user):
+            return await sync_to_async(user.has_perms)(["auth_tests.add_customuser"])
+
+        @user_passes_test(async_test_func)
+        def sync_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.user = self.user_pass
+        response = sync_view(request)
+        self.assertEqual(response.status_code, 200)
+
+        request.user = self.user_deny
+        response = sync_view(request)
+        self.assertEqual(response.status_code, 302)
+
+    async def test_decorator_async_view(self):
+        def sync_test_func(user):
+            return bool(
+                models.Group.objects.filter(name__istartswith=user.username).exists()
+            )
+
+        @user_passes_test(sync_test_func)
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser_pass
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 200)
+
+        request.auser = self.auser_deny
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 302)
+
+    async def test_decorator_async_view_async_test_func(self):
+        async def async_test_func(user):
+            return await sync_to_async(user.has_perms)(["auth_tests.add_customuser"])
+
+        @user_passes_test(async_test_func)
+        async def async_view(request):
+            return HttpResponse()
+
+        request = self.factory.get("/rand")
+        request.auser = self.auser_pass
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 200)
+
+        request.auser = self.auser_deny
+        response = await async_view(request)
+        self.assertEqual(response.status_code, 302)
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index 14604cd2e6..3dd9324304 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -16,6 +16,7 @@ from django.contrib.auth.forms import (
     UserCreationForm,
     UsernameField,
 )
+from django.contrib.auth.hashers import make_password
 from django.contrib.auth.models import User
 from django.contrib.auth.signals import user_login_failed
 from django.contrib.sites.models import Site
@@ -54,6 +55,24 @@ class TestDataMixin:
         cls.u4 = User.objects.create(username="empty_password", password="")
         cls.u5 = User.objects.create(username="unmanageable_password", password="$")
         cls.u6 = User.objects.create(username="unknown_password", password="foo$bar")
+        cls.u7 = User.objects.create(
+            username="unusable_password", password=make_password(None)
+        )
+
+
+class ExtraValidationFormMixin:
+    def __init__(self, *args, failing_fields=None, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.failing_fields = failing_fields or {}
+
+    def failing_helper(self, field_name):
+        if field_name in self.failing_fields:
+            errors = [
+                ValidationError(error, code="invalid")
+                for error in self.failing_fields[field_name]
+            ]
+            raise ValidationError(errors)
+        return self.cleaned_data[field_name]
 
 
 class BaseUserCreationFormTest(TestDataMixin, TestCase):
@@ -217,6 +236,16 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
             form["password2"].errors,
         )
 
+        # passwords are not validated if `usable_password` is unset
+        data = {
+            "username": "othertestclient",
+            "password1": "othertestclient",
+            "password2": "othertestclient",
+            "usable_password": "false",
+        }
+        form = BaseUserCreationForm(data)
+        self.assertIs(form.is_valid(), True, form.errors)
+
     def test_custom_form(self):
         class CustomUserCreationForm(BaseUserCreationForm):
             class Meta(BaseUserCreationForm.Meta):
@@ -310,6 +339,22 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
             "</li></ul>",
         )
 
+    def test_password_extra_validations(self):
+        class ExtraValidationForm(ExtraValidationFormMixin, BaseUserCreationForm):
+            def clean_password1(self):
+                return self.failing_helper("password1")
+
+            def clean_password2(self):
+                return self.failing_helper("password2")
+
+        data = {"username": "extra", "password1": "abc", "password2": "abc"}
+        for fields in (["password1"], ["password2"], ["password1", "password2"]):
+            with self.subTest(fields=fields):
+                errors = {field: [f"Extra validation for {field}."] for field in fields}
+                form = ExtraValidationForm(data, failing_fields=errors)
+                self.assertIs(form.is_valid(), False)
+                self.assertDictEqual(form.errors, errors)
+
     @override_settings(
         AUTH_PASSWORD_VALIDATORS=[
             {
@@ -345,6 +390,19 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
             ["The password is too similar to the first name."],
         )
 
+        # passwords are not validated if `usable_password` is unset
+        form = CustomUserCreationForm(
+            {
+                "username": "testuser",
+                "password1": "testpassword",
+                "password2": "testpassword",
+                "first_name": "testpassword",
+                "last_name": "lastname",
+                "usable_password": "false",
+            }
+        )
+        self.assertIs(form.is_valid(), True, form.errors)
+
     def test_username_field_autocapitalize_none(self):
         form = BaseUserCreationForm()
         self.assertEqual(
@@ -364,6 +422,17 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
                     form.fields[field_name].widget.attrs["autocomplete"], autocomplete
                 )
 
+    def test_unusable_password(self):
+        data = {
+            "username": "new-user-which-does-not-exist",
+            "usable_password": "false",
+        }
+        form = BaseUserCreationForm(data)
+        self.assertIs(form.is_valid(), True, form.errors)
+        u = form.save()
+        self.assertEqual(u.username, data["username"])
+        self.assertFalse(u.has_usable_password())
+
 
 class UserCreationFormTest(TestDataMixin, TestCase):
     def test_case_insensitive_username(self):
@@ -740,6 +809,23 @@ class SetPasswordFormTest(TestDataMixin, TestCase):
             form["new_password2"].errors,
         )
 
+        # SetPasswordForm does not consider usable_password for form validation
+        data = {
+            "new_password1": "testclient",
+            "new_password2": "testclient",
+            "usable_password": "false",
+        }
+        form = SetPasswordForm(user, data)
+        self.assertFalse(form.is_valid())
+        self.assertEqual(len(form["new_password2"].errors), 2)
+        self.assertIn(
+            "The password is too similar to the username.", form["new_password2"].errors
+        )
+        self.assertIn(
+            "This password is too short. It must contain at least 12 characters.",
+            form["new_password2"].errors,
+        )
+
     def test_no_password(self):
         user = User.objects.get(username="testclient")
         data = {"new_password1": "new-password"}
@@ -810,6 +896,27 @@ class SetPasswordFormTest(TestDataMixin, TestCase):
                     form.fields[field_name].widget.attrs["autocomplete"], autocomplete
                 )
 
+    def test_password_extra_validations(self):
+        class ExtraValidationForm(ExtraValidationFormMixin, SetPasswordForm):
+            def clean_new_password1(self):
+                return self.failing_helper("new_password1")
+
+            def clean_new_password2(self):
+                return self.failing_helper("new_password2")
+
+        user = User.objects.get(username="testclient")
+        data = {"new_password1": "abc", "new_password2": "abc"}
+        for fields in (
+            ["new_password1"],
+            ["new_password2"],
+            ["new_password1", "new_password2"],
+        ):
+            with self.subTest(fields=fields):
+                errors = {field: [f"Extra validation for {field}."] for field in fields}
+                form = ExtraValidationForm(user, data, failing_fields=errors)
+                self.assertIs(form.is_valid(), False)
+                self.assertDictEqual(form.errors, errors)
+
 
 class PasswordChangeFormTest(TestDataMixin, TestCase):
     def test_incorrect_password(self):
@@ -918,9 +1025,7 @@ class UserChangeFormTest(TestDataMixin, TestCase):
         MyUserForm({})
 
     def test_unusable_password(self):
-        user = User.objects.get(username="empty_password")
-        user.set_unusable_password()
-        user.save()
+        user = User.objects.get(username="unusable_password")
         form = UserChangeForm(instance=user)
         self.assertIn(_("No password set."), form.as_table())
 
@@ -970,24 +1075,42 @@ class UserChangeFormTest(TestDataMixin, TestCase):
         self.assertEqual(form.initial["password"], form["password"].value())
 
     @override_settings(ROOT_URLCONF="auth_tests.urls_admin")
-    def test_link_to_password_reset_in_helptext_via_to_field(self):
-        user = User.objects.get(username="testclient")
-        form = UserChangeForm(data={}, instance=user)
-        password_help_text = form.fields["password"].help_text
-        matches = re.search('<a href="(.*?)">', password_help_text)
+    def test_link_to_password_reset_in_user_change_form(self):
+        cases = [
+            (
+                "testclient",
+                "Raw passwords are not stored, so there is no way to see "
+                "the user’s password.",
+                "Reset password",
+            ),
+            (
+                "unusable_password",
+                "Enable password-based authentication for this user by setting a "
+                "password.",
+                "Set password",
+            ),
+        ]
+        password_reset_link = r'<a class="button" href="([^"]*)">([^<]*)</a>'
+        for username, expected_help_text, expected_button_label in cases:
+            with self.subTest(username=username):
+                user = User.objects.get(username=username)
+                form = UserChangeForm(data={}, instance=user)
+                password_help_text = form.fields["password"].help_text
+                self.assertEqual(password_help_text, expected_help_text)
 
-        # URL to UserChangeForm in admin via to_field (instead of pk).
-        admin_user_change_url = reverse(
-            f"admin:{user._meta.app_label}_{user._meta.model_name}_change",
-            args=(user.username,),
-        )
-        joined_url = urllib.parse.urljoin(admin_user_change_url, matches.group(1))
+                matches = re.search(password_reset_link, form.as_p())
+                self.assertIsNotNone(matches)
+                self.assertEqual(len(matches.groups()), 2)
+                url_prefix = f"admin:{user._meta.app_label}_{user._meta.model_name}"
+                # URL to UserChangeForm in admin via to_field (instead of pk).
+                user_change_url = reverse(f"{url_prefix}_change", args=(user.pk,))
+                joined_url = urllib.parse.urljoin(user_change_url, matches.group(1))
 
-        pw_change_url = reverse(
-            f"admin:{user._meta.app_label}_{user._meta.model_name}_password_change",
-            args=(user.pk,),
-        )
-        self.assertEqual(joined_url, pw_change_url)
+                pw_change_url = reverse(
+                    f"{url_prefix}_password_change", args=(user.pk,)
+                )
+                self.assertEqual(joined_url, pw_change_url)
+                self.assertEqual(matches.group(2), expected_button_label)
 
     def test_custom_form(self):
         class CustomUserChangeForm(UserChangeForm):
@@ -1282,11 +1405,14 @@ class ReadOnlyPasswordHashTest(SimpleTestCase):
         self.assertHTMLEqual(
             widget.render("name", value, {"id": "id_password"}),
             '<div id="id_password">'
+            "  <p>"
             "    <strong>algorithm</strong>: <bdi>pbkdf2_sha256</bdi>"
             "    <strong>iterations</strong>: <bdi>100000</bdi>"
             "    <strong>salt</strong>: <bdi>a6Pucb******</bdi>"
             "    <strong>hash</strong>: "
             "       <bdi>WmCkn9**************************************</bdi>"
+            "  </p>"
+            '  <p><a class="button" href="../password/">Reset password</a></p>'
             "</div>",
         )
 
@@ -1361,6 +1487,15 @@ class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
             form["password2"].errors,
         )
 
+        # passwords are not validated if `usable_password` is unset
+        data = {
+            "password1": "testclient",
+            "password2": "testclient",
+            "usable_password": "false",
+        }
+        form = AdminPasswordChangeForm(user, data)
+        self.assertIs(form.is_valid(), True, form.errors)
+
     def test_password_whitespace_not_stripped(self):
         user = User.objects.get(username="testclient")
         data = {
@@ -1373,6 +1508,23 @@ class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
         self.assertEqual(form.cleaned_data["password2"], data["password2"])
         self.assertEqual(form.changed_data, ["password"])
 
+    def test_password_extra_validations(self):
+        class ExtraValidationForm(ExtraValidationFormMixin, AdminPasswordChangeForm):
+            def clean_password1(self):
+                return self.failing_helper("password1")
+
+            def clean_password2(self):
+                return self.failing_helper("password2")
+
+        user = User.objects.get(username="testclient")
+        data = {"username": "extra", "password1": "abc", "password2": "abc"}
+        for fields in (["password1"], ["password2"], ["password1", "password2"]):
+            with self.subTest(fields=fields):
+                errors = {field: [f"Extra validation for {field}."] for field in fields}
+                form = ExtraValidationForm(user, data, failing_fields=errors)
+                self.assertIs(form.is_valid(), False)
+                self.assertDictEqual(form.errors, errors)
+
     def test_non_matching_passwords(self):
         user = User.objects.get(username="testclient")
         data = {"password1": "password1", "password2": "password2"}
@@ -1415,3 +1567,29 @@ class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
                 self.assertEqual(
                     form.fields[field_name].widget.attrs["autocomplete"], autocomplete
                 )
+
+    def test_enable_password_authentication(self):
+        user = User.objects.get(username="unusable_password")
+        form = AdminPasswordChangeForm(
+            user,
+            {"password1": "complexpassword", "password2": "complexpassword"},
+        )
+        self.assertNotIn("usable_password", form.fields)
+        self.assertIs(form.is_valid(), True)
+        user = form.save(commit=True)
+        self.assertIs(user.has_usable_password(), True)
+
+    def test_disable_password_authentication(self):
+        user = User.objects.get(username="testclient")
+        form = AdminPasswordChangeForm(
+            user,
+            {"usable_password": "false", "password1": "", "password2": "test"},
+        )
+        self.assertIn("usable_password", form.fields)
+        self.assertIn(
+            "If disabled, the current password for this user will be lost.",
+            form.fields["usable_password"].help_text,
+        )
+        self.assertIs(form.is_valid(), True)  # Valid despite password empty/mismatch.
+        user = form.save(commit=True)
+        self.assertIs(user.has_usable_password(), False)
diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py
index 1b0d2c65be..09d7056411 100644
--- a/tests/auth_tests/test_hashers.py
+++ b/tests/auth_tests/test_hashers.py
@@ -83,7 +83,8 @@ class TestUtilsHashPass(SimpleTestCase):
         encoded = make_password("lètmein", "seasalt", "pbkdf2_sha256")
         self.assertEqual(
             encoded,
-            "pbkdf2_sha256$870000$seasalt$wJSpLMQRQz0Dhj/pFpbyjMj71B2gUYp6HJS5AU+32Ac=",
+            "pbkdf2_sha256$1000000$"
+            "seasalt$r1uLUxoxpP2Ued/qxvmje7UH9PUJBkRrvf9gGPL7Cps=",
         )
         self.assertTrue(is_password_usable(encoded))
         self.assertTrue(check_password("lètmein", encoded))
@@ -276,8 +277,8 @@ class TestUtilsHashPass(SimpleTestCase):
         encoded = hasher.encode("lètmein", "seasalt2")
         self.assertEqual(
             encoded,
-            "pbkdf2_sha256$870000$"
-            "seasalt2$nxgnNHRsZWSmi4hRSKq2MRigfaRmjDhH1NH4g2sQRbU=",
+            "pbkdf2_sha256$1000000$"
+            "seasalt2$egbhFghgsJVDo5Tpg/k9ZnfbySKQ1UQnBYXhR97a7sk=",
         )
         self.assertTrue(hasher.verify("lètmein", encoded))
 
@@ -285,7 +286,7 @@ class TestUtilsHashPass(SimpleTestCase):
         hasher = PBKDF2SHA1PasswordHasher()
         encoded = hasher.encode("lètmein", "seasalt2")
         self.assertEqual(
-            encoded, "pbkdf2_sha1$870000$seasalt2$iFPKnrkYfxxyxaeIqxq+c3nJ/j4="
+            encoded, "pbkdf2_sha1$1000000$seasalt2$3R9hvSAiAy5ARspAFy5GJ/2rjXo="
         )
         self.assertTrue(hasher.verify("lètmein", encoded))
 
@@ -650,8 +651,8 @@ class TestUtilsHashPassScrypt(SimpleTestCase):
         encoded = make_password("lètmein", "seasalt", "scrypt")
         self.assertEqual(
             encoded,
-            "scrypt$16384$seasalt$8$1$Qj3+9PPyRjSJIebHnG81TMjsqtaIGxNQG/aEB/NY"
-            "afTJ7tibgfYz71m0ldQESkXFRkdVCBhhY8mx7rQwite/Pw==",
+            "scrypt$16384$seasalt$8$5$ECMIUp+LMxMSK8xB/IVyba+KYGTI7FTnet025q/1f"
+            "/vBAVnnP3hdYqJuRi+mJn6ji6ze3Fbb7JEFPKGpuEf5vw==",
         )
         self.assertIs(is_password_usable(encoded), True)
         self.assertIs(check_password("lètmein", encoded), True)
diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py
index 0cc56b6760..5765c50034 100644
--- a/tests/auth_tests/test_management.py
+++ b/tests/auth_tests/test_management.py
@@ -1528,7 +1528,7 @@ class CreatePermissionsMultipleDatabasesTests(TestCase):
 
     def test_set_permissions_fk_to_using_parameter(self):
         Permission.objects.using("other").delete()
-        with self.assertNumQueries(6, using="other") as captured_queries:
+        with self.assertNumQueries(4, using="other") as captured_queries:
             create_permissions(apps.get_app_config("auth"), verbosity=0, using="other")
         self.assertIn("INSERT INTO", captured_queries[-1]["sql"].upper())
         self.assertGreater(Permission.objects.using("other").count(), 0)
diff --git a/tests/auth_tests/test_middleware.py b/tests/auth_tests/test_middleware.py
index e7c5a525cd..a837eb8b96 100644
--- a/tests/auth_tests/test_middleware.py
+++ b/tests/auth_tests/test_middleware.py
@@ -1,8 +1,14 @@
-from django.contrib.auth.middleware import AuthenticationMiddleware
+from django.conf import settings
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth.middleware import (
+    AuthenticationMiddleware,
+    LoginRequiredMiddleware,
+)
 from django.contrib.auth.models import User
 from django.core.exceptions import ImproperlyConfigured
 from django.http import HttpRequest, HttpResponse
-from django.test import TestCase
+from django.test import TestCase, modify_settings, override_settings
+from django.urls import reverse
 
 
 class TestAuthenticationMiddleware(TestCase):
@@ -50,3 +56,134 @@ class TestAuthenticationMiddleware(TestCase):
         self.assertEqual(auser, self.user)
         auser_second = await self.request.auser()
         self.assertIs(auser, auser_second)
+
+
+@override_settings(ROOT_URLCONF="auth_tests.urls")
+@modify_settings(
+    MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
+)
+class TestLoginRequiredMiddleware(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.user = User.objects.create_user(
+            "test_user", "test@example.com", "test_password"
+        )
+
+    def setUp(self):
+        self.middleware = LoginRequiredMiddleware(lambda req: HttpResponse())
+        self.request = HttpRequest()
+
+    def test_public_paths(self):
+        paths = ["public_view", "public_function_view"]
+        for path in paths:
+            response = self.client.get(f"/{path}/")
+            self.assertEqual(response.status_code, 200)
+
+    def test_protected_paths(self):
+        paths = ["protected_view", "protected_function_view"]
+        for path in paths:
+            response = self.client.get(f"/{path}/")
+            self.assertRedirects(
+                response,
+                settings.LOGIN_URL + f"?next=/{path}/",
+                fetch_redirect_response=False,
+            )
+
+    def test_login_required_paths(self):
+        paths = ["login_required_cbv_view", "login_required_decorator_view"]
+        for path in paths:
+            response = self.client.get(f"/{path}/")
+            self.assertRedirects(
+                response,
+                "/custom_login/" + f"?step=/{path}/",
+                fetch_redirect_response=False,
+            )
+
+    def test_admin_path(self):
+        admin_url = reverse("admin:index")
+        response = self.client.get(admin_url)
+        self.assertRedirects(
+            response,
+            reverse("admin:login") + f"?next={admin_url}",
+            target_status_code=200,
+        )
+
+    def test_non_existent_path(self):
+        response = self.client.get("/non_existent/")
+        self.assertEqual(response.status_code, 404)
+
+    def test_paths_with_logged_in_user(self):
+        paths = [
+            "public_view",
+            "public_function_view",
+            "protected_view",
+            "protected_function_view",
+            "login_required_cbv_view",
+            "login_required_decorator_view",
+        ]
+        self.client.login(username="test_user", password="test_password")
+        for path in paths:
+            response = self.client.get(f"/{path}/")
+            self.assertEqual(response.status_code, 200)
+
+    def test_get_login_url_from_view_func(self):
+        def view_func(request):
+            return HttpResponse()
+
+        view_func.login_url = "/custom_login/"
+        login_url = self.middleware.get_login_url(view_func)
+        self.assertEqual(login_url, "/custom_login/")
+
+    @override_settings(LOGIN_URL="/settings_login/")
+    def test_get_login_url_from_settings(self):
+        login_url = self.middleware.get_login_url(lambda: None)
+        self.assertEqual(login_url, "/settings_login/")
+
+    @override_settings(LOGIN_URL=None)
+    def test_get_login_url_no_login_url(self):
+        with self.assertRaises(ImproperlyConfigured) as e:
+            self.middleware.get_login_url(lambda: None)
+        self.assertEqual(
+            str(e.exception),
+            "No login URL to redirect to. Define settings.LOGIN_URL or provide "
+            "a login_url via the 'django.contrib.auth.decorators.login_required' "
+            "decorator.",
+        )
+
+    def test_get_redirect_field_name_from_view_func(self):
+        def view_func(request):
+            return HttpResponse()
+
+        view_func.redirect_field_name = "next_page"
+        redirect_field_name = self.middleware.get_redirect_field_name(view_func)
+        self.assertEqual(redirect_field_name, "next_page")
+
+    @override_settings(
+        MIDDLEWARE=[
+            "django.contrib.sessions.middleware.SessionMiddleware",
+            "django.contrib.auth.middleware.AuthenticationMiddleware",
+            "auth_tests.test_checks.LoginRequiredMiddlewareSubclass",
+        ],
+        LOGIN_URL="/settings_login/",
+    )
+    def test_login_url_resolve_logic(self):
+        paths = ["login_required_cbv_view", "login_required_decorator_view"]
+        for path in paths:
+            response = self.client.get(f"/{path}/")
+            self.assertRedirects(
+                response,
+                "/custom_login/" + f"?step=/{path}/",
+                fetch_redirect_response=False,
+            )
+        paths = ["protected_view", "protected_function_view"]
+        for path in paths:
+            response = self.client.get(f"/{path}/")
+            self.assertRedirects(
+                response,
+                f"/settings_login/?redirect_to=/{path}/",
+                fetch_redirect_response=False,
+            )
+
+    def test_get_redirect_field_name_default(self):
+        redirect_field_name = self.middleware.get_redirect_field_name(lambda: None)
+        self.assertEqual(redirect_field_name, REDIRECT_FIELD_NAME)
diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
index f4cf6ed2f4..97d0448ab1 100644
--- a/tests/auth_tests/test_views.py
+++ b/tests/auth_tests/test_views.py
@@ -23,6 +23,8 @@ from django.contrib.auth.views import (
     redirect_to_login,
 )
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.messages import Message
+from django.contrib.messages.test import MessagesTestMixin
 from django.contrib.sessions.middleware import SessionMiddleware
 from django.contrib.sites.requests import RequestSite
 from django.core import mail
@@ -30,7 +32,7 @@ from django.core.exceptions import ImproperlyConfigured
 from django.db import connection
 from django.http import HttpRequest, HttpResponse
 from django.middleware.csrf import CsrfViewMiddleware, get_token
-from django.test import Client, TestCase, override_settings
+from django.test import Client, TestCase, modify_settings, override_settings
 from django.test.client import RedirectCycleError
 from django.urls import NoReverseMatch, reverse, reverse_lazy
 from django.utils.http import urlsafe_base64_encode
@@ -470,6 +472,29 @@ class PasswordResetTest(AuthViewsTestCase):
         with self.assertRaisesMessage(ImproperlyConfigured, msg):
             self.client.get("/reset/missing_parameters/")
 
+    @modify_settings(
+        MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
+    )
+    def test_access_under_login_required_middleware(self):
+        reset_urls = [
+            reverse("password_reset"),
+            reverse("password_reset_done"),
+            reverse("password_reset_confirm", kwargs={"uidb64": "abc", "token": "def"}),
+            reverse("password_reset_complete"),
+        ]
+
+        for url in reset_urls:
+            with self.subTest(url=url):
+                response = self.client.get(url)
+                self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            "/password_reset/", {"email": "staffmember@example.com"}
+        )
+        self.assertRedirects(
+            response, "/password_reset/done/", fetch_redirect_response=False
+        )
+
 
 @override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
 class CustomUserPasswordResetTest(AuthViewsTestCase):
@@ -659,6 +684,38 @@ class ChangePasswordTest(AuthViewsTestCase):
             response, "/password_reset/", fetch_redirect_response=False
         )
 
+    @modify_settings(
+        MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
+    )
+    def test_access_under_login_required_middleware(self):
+        response = self.client.post(
+            "/password_change/",
+            {
+                "old_password": "password",
+                "new_password1": "password1",
+                "new_password2": "password1",
+            },
+        )
+        self.assertRedirects(
+            response,
+            settings.LOGIN_URL + "?next=/password_change/",
+            fetch_redirect_response=False,
+        )
+
+        self.login()
+
+        response = self.client.post(
+            "/password_change/",
+            {
+                "old_password": "password",
+                "new_password1": "password1",
+                "new_password2": "password1",
+            },
+        )
+        self.assertRedirects(
+            response, "/password_change/done/", fetch_redirect_response=False
+        )
+
 
 class SessionAuthenticationTests(AuthViewsTestCase):
     def test_user_password_change_updates_session(self):
@@ -902,6 +959,13 @@ class LoginTest(AuthViewsTestCase):
         response = self.login(url="/login/get_default_redirect_url/?next=/test/")
         self.assertRedirects(response, "/test/", fetch_redirect_response=False)
 
+    @modify_settings(
+        MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
+    )
+    def test_access_under_login_required_middleware(self):
+        response = self.client.get(reverse("login"))
+        self.assertEqual(response.status_code, 200)
+
 
 class LoginURLSettings(AuthViewsTestCase):
     """Tests for settings.LOGIN_URL."""
@@ -1353,6 +1417,22 @@ class LogoutTest(AuthViewsTestCase):
         self.assertContains(response, "Logged out")
         self.confirm_logged_out()
 
+    @modify_settings(
+        MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
+    )
+    def test_access_under_login_required_middleware(self):
+        response = self.client.post("/logout/")
+        self.assertRedirects(
+            response,
+            settings.LOGIN_URL + "?next=/logout/",
+            fetch_redirect_response=False,
+        )
+
+        self.login()
+
+        response = self.client.post("/logout/")
+        self.assertEqual(response.status_code, 200)
+
 
 def get_perm(Model, perm):
     ct = ContentType.objects.get_for_model(Model)
@@ -1365,7 +1445,7 @@ def get_perm(Model, perm):
     ROOT_URLCONF="auth_tests.urls_admin",
     PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"],
 )
-class ChangelistTests(AuthViewsTestCase):
+class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
     @classmethod
     def setUpTestData(cls):
         super().setUpTestData()
@@ -1429,7 +1509,7 @@ class ChangelistTests(AuthViewsTestCase):
         row = LogEntry.objects.latest("id")
         self.assertEqual(row.get_change_message(), "No fields changed.")
 
-    def test_user_change_password(self):
+    def test_user_with_usable_password_change_password(self):
         user_change_url = reverse(
             "auth_test_admin:auth_user_change", args=(self.admin.pk,)
         )
@@ -1440,11 +1520,118 @@ class ChangelistTests(AuthViewsTestCase):
         response = self.client.get(user_change_url)
         # Test the link inside password field help_text.
         rel_link = re.search(
-            r'you can change the password using <a href="([^"]*)">this form</a>',
+            r'<a class="button" href="([^"]*)">Reset password</a>',
             response.content.decode(),
         )[1]
         self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)
 
+        response = self.client.get(password_change_url)
+        # Test the form title with original (usable) password
+        self.assertContains(
+            response, f"<h1>Change password: {self.admin.username}</h1>"
+        )
+        # Breadcrumb.
+        self.assertContains(
+            response, f"{self.admin.username}</a>\n&rsaquo; Change password"
+        )
+        # Submit buttons
+        self.assertContains(response, '<input type="submit" name="set-password"')
+        self.assertContains(response, '<input type="submit" name="unset-password"')
+
+        # Password change.
+        response = self.client.post(
+            password_change_url,
+            {
+                "password1": "password1",
+                "password2": "password1",
+            },
+        )
+        self.assertRedirects(response, user_change_url)
+        self.assertMessages(
+            response, [Message(level=25, message="Password changed successfully.")]
+        )
+        row = LogEntry.objects.latest("id")
+        self.assertEqual(row.get_change_message(), "Changed password.")
+        self.logout()
+        self.login(password="password1")
+
+        # Disable password-based authentication without proper submit button.
+        response = self.client.post(
+            password_change_url,
+            {
+                "password1": "password1",
+                "password2": "password1",
+                "usable_password": "false",
+            },
+        )
+        self.assertRedirects(response, password_change_url)
+        self.assertMessages(
+            response,
+            [
+                Message(
+                    level=40,
+                    message="Conflicting form data submitted. Please try again.",
+                )
+            ],
+        )
+        # No password change yet.
+        self.login(password="password1")
+
+        # Disable password-based authentication with proper submit button.
+        response = self.client.post(
+            password_change_url,
+            {
+                "password1": "password1",
+                "password2": "password1",
+                "usable_password": "false",
+                "unset-password": 1,
+            },
+        )
+        self.assertRedirects(response, user_change_url)
+        self.assertMessages(
+            response,
+            [Message(level=25, message="Password-based authentication was disabled.")],
+        )
+        row = LogEntry.objects.latest("id")
+        self.assertEqual(row.get_change_message(), "Changed password.")
+        self.logout()
+        # Password-based authentication was disabled.
+        with self.assertRaises(AssertionError):
+            self.login(password="password1")
+        self.admin.refresh_from_db()
+        self.assertIs(self.admin.has_usable_password(), False)
+
+    def test_user_with_unusable_password_change_password(self):
+        # Test for title with unusable password with a test user
+        test_user = User.objects.get(email="staffmember@example.com")
+        test_user.set_unusable_password()
+        test_user.save()
+        user_change_url = reverse(
+            "auth_test_admin:auth_user_change", args=(test_user.pk,)
+        )
+        password_change_url = reverse(
+            "auth_test_admin:auth_user_password_change", args=(test_user.pk,)
+        )
+
+        response = self.client.get(user_change_url)
+        # Test the link inside password field help_text.
+        rel_link = re.search(
+            r'<a class="button" href="([^"]*)">Set password</a>',
+            response.content.decode(),
+        )[1]
+        self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)
+
+        response = self.client.get(password_change_url)
+        # Test the form title with original (usable) password
+        self.assertContains(response, f"<h1>Set password: {test_user.username}</h1>")
+        # Breadcrumb.
+        self.assertContains(
+            response, f"{test_user.username}</a>\n&rsaquo; Set password"
+        )
+        # Submit buttons
+        self.assertContains(response, '<input type="submit" name="set-password"')
+        self.assertNotContains(response, '<input type="submit" name="unset-password"')
+
         response = self.client.post(
             password_change_url,
             {
@@ -1453,10 +1640,11 @@ class ChangelistTests(AuthViewsTestCase):
             },
         )
         self.assertRedirects(response, user_change_url)
+        self.assertMessages(
+            response, [Message(level=25, message="Password changed successfully.")]
+        )
         row = LogEntry.objects.latest("id")
         self.assertEqual(row.get_change_message(), "Changed password.")
-        self.logout()
-        self.login(password="password1")
 
     def test_user_change_different_user_password(self):
         u = User.objects.get(email="staffmember@example.com")
diff --git a/tests/auth_tests/urls.py b/tests/auth_tests/urls.py
index 99fa22e4f4..cb6a0ed1cf 100644
--- a/tests/auth_tests/urls.py
+++ b/tests/auth_tests/urls.py
@@ -1,6 +1,10 @@
 from django.contrib import admin
 from django.contrib.auth import views
-from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.decorators import (
+    login_not_required,
+    login_required,
+    permission_required,
+)
 from django.contrib.auth.forms import AuthenticationForm
 from django.contrib.auth.urls import urlpatterns as auth_urlpatterns
 from django.contrib.auth.views import LoginView
@@ -9,6 +13,8 @@ from django.http import HttpRequest, HttpResponse
 from django.shortcuts import render
 from django.template import RequestContext, Template
 from django.urls import path, re_path, reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views import View
 from django.views.decorators.cache import never_cache
 from django.views.i18n import set_language
 
@@ -88,6 +94,42 @@ class CustomDefaultRedirectURLLoginView(LoginView):
         return "/custom/"
 
 
+class EmptyResponseBaseView(View):
+    def get(self, request, *args, **kwargs):
+        return HttpResponse()
+
+
+@method_decorator(login_not_required, name="dispatch")
+class PublicView(EmptyResponseBaseView):
+    pass
+
+
+class ProtectedView(EmptyResponseBaseView):
+    pass
+
+
+@method_decorator(
+    login_required(login_url="/custom_login/", redirect_field_name="step"),
+    name="dispatch",
+)
+class ProtectedViewWithCustomLoginRequired(EmptyResponseBaseView):
+    pass
+
+
+@login_not_required
+def public_view(request):
+    return HttpResponse()
+
+
+def protected_view(request):
+    return HttpResponse()
+
+
+@login_required(login_url="/custom_login/", redirect_field_name="step")
+def protected_view_with_login_required_decorator(request):
+    return HttpResponse()
+
+
 # special urls for auth test cases
 urlpatterns = auth_urlpatterns + [
     path(
@@ -198,7 +240,14 @@ urlpatterns = auth_urlpatterns + [
         "login_and_permission_required_exception/",
         login_and_permission_required_exception,
     ),
+    path("public_view/", PublicView.as_view()),
+    path("public_function_view/", public_view),
+    path("protected_view/", ProtectedView.as_view()),
+    path("protected_function_view/", protected_view),
+    path(
+        "login_required_decorator_view/", protected_view_with_login_required_decorator
+    ),
+    path("login_required_cbv_view/", ProtectedViewWithCustomLoginRequired.as_view()),
     path("setlang/", set_language, name="set_language"),
-    # This line is only required to render the password reset with is_admin=True
     path("admin/", admin.site.urls),
 ]
diff --git a/tests/backends/base/test_base.py b/tests/backends/base/test_base.py
index 03356fbf10..4418d010ea 100644
--- a/tests/backends/base/test_base.py
+++ b/tests/backends/base/test_base.py
@@ -211,6 +211,16 @@ class ExecuteWrapperTests(TestCase):
         self.assertEqual(connection.execute_wrappers, [])
         self.assertEqual(connections["other"].execute_wrappers, [])
 
+    def test_wrapper_debug(self):
+        def wrap_with_comment(execute, sql, params, many, context):
+            return execute(f"/* My comment */ {sql}", params, many, context)
+
+        with CaptureQueriesContext(connection) as ctx:
+            with connection.execute_wrapper(wrap_with_comment):
+                list(Person.objects.all())
+        last_query = ctx.captured_queries[-1]["sql"]
+        self.assertTrue(last_query.startswith("/* My comment */"))
+
 
 class ConnectionHealthChecksTests(SimpleTestCase):
     databases = {"default"}
diff --git a/tests/backends/base/test_creation.py b/tests/backends/base/test_creation.py
index 9593e13462..7e760e8884 100644
--- a/tests/backends/base/test_creation.py
+++ b/tests/backends/base/test_creation.py
@@ -14,6 +14,7 @@ from ..models import (
     Object,
     ObjectReference,
     ObjectSelfReference,
+    SchoolBus,
     SchoolClass,
 )
 
@@ -250,6 +251,22 @@ class TestDeserializeDbFromString(TransactionTestCase):
         self.assertIn('"model": "backends.schoolclass"', data)
         self.assertIn('"year": 1000', data)
 
+    def test_serialize_db_to_string_base_manager_with_prefetch_related(self):
+        sclass = SchoolClass.objects.create(
+            year=2000, last_updated=datetime.datetime.now()
+        )
+        bus = SchoolBus.objects.create(number=1)
+        bus.schoolclasses.add(sclass)
+        with mock.patch("django.db.migrations.loader.MigrationLoader") as loader:
+            # serialize_db_to_string() serializes only migrated apps, so mark
+            # the backends app as migrated.
+            loader_instance = loader.return_value
+            loader_instance.migrated_apps = {"backends"}
+            data = connection.creation.serialize_db_to_string()
+        self.assertIn('"model": "backends.schoolbus"', data)
+        self.assertIn('"model": "backends.schoolclass"', data)
+        self.assertIn(f'"schoolclasses": [{sclass.pk}]', data)
+
 
 class SkipTestClass:
     def skip_function(self):
diff --git a/tests/backends/models.py b/tests/backends/models.py
index 99e9e86f44..1ed108c2b8 100644
--- a/tests/backends/models.py
+++ b/tests/backends/models.py
@@ -32,6 +32,20 @@ class SchoolClass(models.Model):
     objects = SchoolClassManager()
 
 
+class SchoolBusManager(models.Manager):
+    def get_queryset(self):
+        return super().get_queryset().prefetch_related("schoolclasses")
+
+
+class SchoolBus(models.Model):
+    number = models.IntegerField()
+    schoolclasses = models.ManyToManyField("SchoolClass")
+    objects = SchoolBusManager()
+
+    class Meta:
+        base_manager_name = "objects"
+
+
 class VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ(models.Model):
     primary_key_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.AutoField(
         primary_key=True
diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py
index a045195991..0b4f580612 100644
--- a/tests/backends/postgresql/tests.py
+++ b/tests/backends/postgresql/tests.py
@@ -8,6 +8,7 @@ from django.db import (
     DEFAULT_DB_ALIAS,
     DatabaseError,
     NotSupportedError,
+    ProgrammingError,
     connection,
     connections,
 )
@@ -20,6 +21,15 @@ except ImportError:
     is_psycopg3 = False
 
 
+def no_pool_connection(alias=None):
+    new_connection = connection.copy(alias)
+    new_connection.settings_dict = copy.deepcopy(connection.settings_dict)
+    # Ensure that the second connection circumvents the pool, this is kind
+    # of a hack, but we cannot easily change the pool connections.
+    new_connection.settings_dict["OPTIONS"]["pool"] = False
+    return new_connection
+
+
 @unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
 class Tests(TestCase):
     databases = {"default", "other"}
@@ -177,7 +187,7 @@ class Tests(TestCase):
         PostgreSQL shouldn't roll back SET TIME ZONE, even if the first
         transaction is rolled back (#17062).
         """
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         try:
             # Ensure the database default time zone is different than
             # the time zone in new_connection.settings_dict. We can
@@ -213,7 +223,7 @@ class Tests(TestCase):
         The connection wrapper shouldn't believe that autocommit is enabled
         after setting the time zone when AUTOCOMMIT is False (#21452).
         """
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         new_connection.settings_dict["AUTOCOMMIT"] = False
 
         try:
@@ -223,6 +233,126 @@ class Tests(TestCase):
         finally:
             new_connection.close()
 
+    @unittest.skipUnless(is_psycopg3, "psycopg3 specific test")
+    def test_connect_pool(self):
+        from psycopg_pool import PoolTimeout
+
+        new_connection = no_pool_connection(alias="default_pool")
+        new_connection.settings_dict["OPTIONS"]["pool"] = {
+            "min_size": 0,
+            "max_size": 2,
+            "timeout": 5,
+        }
+        self.assertIsNotNone(new_connection.pool)
+
+        connections = []
+
+        def get_connection():
+            # copy() reuses the existing alias and as such the same pool.
+            conn = new_connection.copy()
+            conn.connect()
+            connections.append(conn)
+            return conn
+
+        try:
+            connection_1 = get_connection()  # First connection.
+            connection_1_backend_pid = connection_1.connection.info.backend_pid
+            get_connection()  # Get the second connection.
+            with self.assertRaises(PoolTimeout):
+                # The pool has a maximum of 2 connections.
+                get_connection()
+
+            connection_1.close()  # Release back to the pool.
+            connection_3 = get_connection()
+            # Reuses the first connection as it is available.
+            self.assertEqual(
+                connection_3.connection.info.backend_pid, connection_1_backend_pid
+            )
+        finally:
+            # Release all connections back to the pool.
+            for conn in connections:
+                conn.close()
+            new_connection.close_pool()
+
+    @unittest.skipUnless(is_psycopg3, "psycopg3 specific test")
+    def test_connect_pool_set_to_true(self):
+        new_connection = no_pool_connection(alias="default_pool")
+        new_connection.settings_dict["OPTIONS"]["pool"] = True
+        try:
+            self.assertIsNotNone(new_connection.pool)
+        finally:
+            new_connection.close_pool()
+
+    @unittest.skipUnless(is_psycopg3, "psycopg3 specific test")
+    def test_connect_pool_with_timezone(self):
+        new_time_zone = "Africa/Nairobi"
+        new_connection = no_pool_connection(alias="default_pool")
+
+        try:
+            with new_connection.cursor() as cursor:
+                cursor.execute("SHOW TIMEZONE")
+                tz = cursor.fetchone()[0]
+                self.assertNotEqual(new_time_zone, tz)
+        finally:
+            new_connection.close()
+
+        del new_connection.timezone_name
+        new_connection.settings_dict["OPTIONS"]["pool"] = True
+        try:
+            with self.settings(TIME_ZONE=new_time_zone):
+                with new_connection.cursor() as cursor:
+                    cursor.execute("SHOW TIMEZONE")
+                    tz = cursor.fetchone()[0]
+                    self.assertEqual(new_time_zone, tz)
+        finally:
+            new_connection.close()
+            new_connection.close_pool()
+
+    @unittest.skipUnless(is_psycopg3, "psycopg3 specific test")
+    def test_pooling_health_checks(self):
+        new_connection = no_pool_connection(alias="default_pool")
+        new_connection.settings_dict["OPTIONS"]["pool"] = True
+        new_connection.settings_dict["CONN_HEALTH_CHECKS"] = False
+
+        try:
+            self.assertIsNone(new_connection.pool._check)
+        finally:
+            new_connection.close_pool()
+
+        new_connection.settings_dict["CONN_HEALTH_CHECKS"] = True
+        try:
+            self.assertIsNotNone(new_connection.pool._check)
+        finally:
+            new_connection.close_pool()
+
+    @unittest.skipUnless(is_psycopg3, "psycopg3 specific test")
+    def test_cannot_open_new_connection_in_atomic_block(self):
+        new_connection = no_pool_connection(alias="default_pool")
+        new_connection.settings_dict["OPTIONS"]["pool"] = True
+
+        msg = "Cannot open a new connection in an atomic block."
+        new_connection.in_atomic_block = True
+        new_connection.closed_in_transaction = True
+        with self.assertRaisesMessage(ProgrammingError, msg):
+            new_connection.ensure_connection()
+
+    @unittest.skipUnless(is_psycopg3, "psycopg3 specific test")
+    def test_pooling_not_support_persistent_connections(self):
+        new_connection = no_pool_connection(alias="default_pool")
+        new_connection.settings_dict["OPTIONS"]["pool"] = True
+        new_connection.settings_dict["CONN_MAX_AGE"] = 10
+        msg = "Pooling doesn't support persistent connections."
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            new_connection.pool
+
+    @unittest.skipIf(is_psycopg3, "psycopg2 specific test")
+    def test_connect_pool_setting_ignored_for_psycopg2(self):
+        new_connection = no_pool_connection()
+        new_connection.settings_dict["OPTIONS"]["pool"] = True
+        msg = "Database pooling requires psycopg >= 3"
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            new_connection.connect()
+
     def test_connect_isolation_level(self):
         """
         The transaction level can be configured with
@@ -236,7 +366,7 @@ class Tests(TestCase):
         # Check the level on the psycopg connection, not the Django wrapper.
         self.assertIsNone(connection.connection.isolation_level)
 
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         new_connection.settings_dict["OPTIONS"][
             "isolation_level"
         ] = IsolationLevel.SERIALIZABLE
@@ -253,7 +383,7 @@ class Tests(TestCase):
 
     def test_connect_invalid_isolation_level(self):
         self.assertIsNone(connection.connection.isolation_level)
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         new_connection.settings_dict["OPTIONS"]["isolation_level"] = -1
         msg = (
             "Invalid transaction isolation level -1 specified. Use one of the "
@@ -269,7 +399,7 @@ class Tests(TestCase):
         """
         try:
             custom_role = "django_nonexistent_role"
-            new_connection = connection.copy()
+            new_connection = no_pool_connection()
             new_connection.settings_dict["OPTIONS"]["assume_role"] = custom_role
             msg = f'role "{custom_role}" does not exist'
             with self.assertRaisesMessage(errors.InvalidParameterValue, msg):
@@ -285,7 +415,7 @@ class Tests(TestCase):
         """
         from django.db.backends.postgresql.base import ServerBindingCursor
 
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         new_connection.settings_dict["OPTIONS"]["server_side_binding"] = True
         try:
             new_connection.connect()
@@ -306,7 +436,7 @@ class Tests(TestCase):
         class MyCursor(Cursor):
             pass
 
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         new_connection.settings_dict["OPTIONS"]["cursor_factory"] = MyCursor
         try:
             new_connection.connect()
@@ -315,7 +445,7 @@ class Tests(TestCase):
             new_connection.close()
 
     def test_connect_no_is_usable_checks(self):
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         try:
             with mock.patch.object(new_connection, "is_usable") as is_usable:
                 new_connection.connect()
@@ -324,7 +454,7 @@ class Tests(TestCase):
             new_connection.close()
 
     def test_client_encoding_utf8_enforce(self):
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         new_connection.settings_dict["OPTIONS"]["client_encoding"] = "iso-8859-2"
         try:
             new_connection.connect()
@@ -417,19 +547,19 @@ class Tests(TestCase):
         self.assertEqual([q["sql"] for q in connection.queries], [copy_sql])
 
     def test_get_database_version(self):
-        new_connection = connection.copy()
-        new_connection.pg_version = 130009
-        self.assertEqual(new_connection.get_database_version(), (13, 9))
+        new_connection = no_pool_connection()
+        new_connection.pg_version = 140009
+        self.assertEqual(new_connection.get_database_version(), (14, 9))
 
-    @mock.patch.object(connection, "get_database_version", return_value=(12,))
+    @mock.patch.object(connection, "get_database_version", return_value=(13,))
     def test_check_database_version_supported(self, mocked_get_database_version):
-        msg = "PostgreSQL 13 or later is required (found 12)."
+        msg = "PostgreSQL 14 or later is required (found 13)."
         with self.assertRaisesMessage(NotSupportedError, msg):
             connection.check_database_version_supported()
         self.assertTrue(mocked_get_database_version.called)
 
     def test_compose_sql_when_no_connection(self):
-        new_connection = connection.copy()
+        new_connection = no_pool_connection()
         try:
             self.assertEqual(
                 new_connection.ops.compose_sql("SELECT %s", ["test"]),
diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py
index 42fee432f9..fafc0b182f 100644
--- a/tests/backends/sqlite/tests.py
+++ b/tests/backends/sqlite/tests.py
@@ -109,13 +109,36 @@ class Tests(TestCase):
             connections["default"].close()
             self.assertTrue(os.path.isfile(os.path.join(tmp, "test.db")))
 
-    @mock.patch.object(connection, "get_database_version", return_value=(3, 26))
+    @mock.patch.object(connection, "get_database_version", return_value=(3, 30))
     def test_check_database_version_supported(self, mocked_get_database_version):
-        msg = "SQLite 3.27 or later is required (found 3.26)."
+        msg = "SQLite 3.31 or later is required (found 3.30)."
         with self.assertRaisesMessage(NotSupportedError, msg):
             connection.check_database_version_supported()
         self.assertTrue(mocked_get_database_version.called)
 
+    def test_init_command(self):
+        settings_dict = {
+            "default": {
+                "ENGINE": "django.db.backends.sqlite3",
+                "NAME": ":memory:",
+                "OPTIONS": {
+                    "init_command": "PRAGMA synchronous=3; PRAGMA cache_size=2000;",
+                },
+            }
+        }
+        connections = ConnectionHandler(settings_dict)
+        connections["default"].ensure_connection()
+        try:
+            with connections["default"].cursor() as cursor:
+                cursor.execute("PRAGMA synchronous")
+                value = cursor.fetchone()[0]
+                self.assertEqual(value, 3)
+                cursor.execute("PRAGMA cache_size")
+                value = cursor.fetchone()[0]
+                self.assertEqual(value, 2000)
+        finally:
+            connections["default"]._close()
+
 
 @unittest.skipUnless(connection.vendor == "sqlite", "SQLite tests")
 @isolate_apps("backends")
@@ -298,4 +321,4 @@ class TestTransactionMode(SimpleTestCase):
         try:
             yield new_connection
         finally:
-            new_connection.close()
+            new_connection._close()
diff --git a/tests/backends/test_ddl_references.py b/tests/backends/test_ddl_references.py
index 86984ed3e8..8975b97124 100644
--- a/tests/backends/test_ddl_references.py
+++ b/tests/backends/test_ddl_references.py
@@ -166,10 +166,13 @@ class ForeignKeyNameTests(IndexNameTests):
 
 
 class MockReference:
-    def __init__(self, representation, referenced_tables, referenced_columns):
+    def __init__(
+        self, representation, referenced_tables, referenced_columns, referenced_indexes
+    ):
         self.representation = representation
         self.referenced_tables = referenced_tables
         self.referenced_columns = referenced_columns
+        self.referenced_indexes = referenced_indexes
 
     def references_table(self, table):
         return table in self.referenced_tables
@@ -177,6 +180,9 @@ class MockReference:
     def references_column(self, table, column):
         return (table, column) in self.referenced_columns
 
+    def references_index(self, table, index):
+        return (table, index) in self.referenced_indexes
+
     def rename_table_references(self, old_table, new_table):
         if old_table in self.referenced_tables:
             self.referenced_tables.remove(old_table)
@@ -195,32 +201,43 @@ class MockReference:
 class StatementTests(SimpleTestCase):
     def test_references_table(self):
         statement = Statement(
-            "", reference=MockReference("", {"table"}, {}), non_reference=""
+            "", reference=MockReference("", {"table"}, {}, {}), non_reference=""
         )
         self.assertIs(statement.references_table("table"), True)
         self.assertIs(statement.references_table("other"), False)
 
     def test_references_column(self):
         statement = Statement(
-            "", reference=MockReference("", {}, {("table", "column")}), non_reference=""
+            "",
+            reference=MockReference("", {}, {("table", "column")}, {}),
+            non_reference="",
         )
         self.assertIs(statement.references_column("table", "column"), True)
         self.assertIs(statement.references_column("other", "column"), False)
 
+    def test_references_index(self):
+        statement = Statement(
+            "",
+            reference=MockReference("", {}, {}, {("table", "index")}),
+            non_reference="",
+        )
+        self.assertIs(statement.references_index("table", "index"), True)
+        self.assertIs(statement.references_index("other", "index"), False)
+
     def test_rename_table_references(self):
-        reference = MockReference("", {"table"}, {})
+        reference = MockReference("", {"table"}, {}, {})
         statement = Statement("", reference=reference, non_reference="")
         statement.rename_table_references("table", "other")
         self.assertEqual(reference.referenced_tables, {"other"})
 
     def test_rename_column_references(self):
-        reference = MockReference("", {}, {("table", "column")})
+        reference = MockReference("", {}, {("table", "column")}, {})
         statement = Statement("", reference=reference, non_reference="")
         statement.rename_column_references("table", "column", "other")
         self.assertEqual(reference.referenced_columns, {("table", "other")})
 
     def test_repr(self):
-        reference = MockReference("reference", {}, {})
+        reference = MockReference("reference", {}, {}, {})
         statement = Statement(
             "%(reference)s - %(non_reference)s",
             reference=reference,
@@ -229,7 +246,7 @@ class StatementTests(SimpleTestCase):
         self.assertEqual(repr(statement), "<Statement 'reference - non_reference'>")
 
     def test_str(self):
-        reference = MockReference("reference", {}, {})
+        reference = MockReference("reference", {}, {}, {})
         statement = Statement(
             "%(reference)s - %(non_reference)s",
             reference=reference,
diff --git a/tests/basic/tests.py b/tests/basic/tests.py
index 8a304e9ace..38fb9ca200 100644
--- a/tests/basic/tests.py
+++ b/tests/basic/tests.py
@@ -186,6 +186,12 @@ class ModelInstanceCreationTests(TestCase):
         with self.assertNumQueries(1):
             PrimaryKeyWithDefault().save()
 
+    def test_save_primary_with_default_force_update(self):
+        # An UPDATE attempt is made if explicitly requested.
+        obj = PrimaryKeyWithDefault.objects.create()
+        with self.assertNumQueries(1):
+            PrimaryKeyWithDefault(uuid=obj.pk).save(force_update=True)
+
     def test_save_primary_with_db_default(self):
         # An UPDATE attempt is skipped when a primary key has db_default.
         with self.assertNumQueries(1):
@@ -966,6 +972,13 @@ class ModelRefreshTests(TestCase):
         article.refresh_from_db()
         self.assertTrue(hasattr(article, "featured"))
 
+    def test_refresh_clears_reverse_related_explicit_fields(self):
+        article = Article.objects.create(headline="Test", pub_date=datetime(2024, 2, 4))
+        self.assertFalse(hasattr(article, "featured"))
+        FeaturedArticle.objects.create(article_id=article.pk)
+        article.refresh_from_db(fields=["featured"])
+        self.assertTrue(hasattr(article, "featured"))
+
     def test_refresh_clears_one_to_one_field(self):
         article = Article.objects.create(
             headline="Parrot programs in Python",
diff --git a/tests/cache/failing_cache.py b/tests/cache/failing_cache.py
new file mode 100644
index 0000000000..1c9b5996d6
--- /dev/null
+++ b/tests/cache/failing_cache.py
@@ -0,0 +1,10 @@
+from django.core.cache.backends.locmem import LocMemCache
+
+
+class CacheClass(LocMemCache):
+
+    def set(self, *args, **kwargs):
+        raise Exception("Faked exception saving to cache")
+
+    async def aset(self, *args, **kwargs):
+        raise Exception("Faked exception saving to cache")
diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index e6ebb718f1..978efdd9d3 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -2752,6 +2752,37 @@ class CacheMiddlewareTest(SimpleTestCase):
 
         self.assertIsNot(thread_caches[0], thread_caches[1])
 
+    def test_cache_control_max_age(self):
+        view = cache_page(2)(hello_world_view)
+        request = self.factory.get("/view/")
+
+        # First request. Freshly created response gets returned with no Age
+        # header.
+        with mock.patch.object(
+            time, "time", return_value=1468749600
+        ):  # Sun, 17 Jul 2016 10:00:00 GMT
+            response = view(request, 1)
+            response.close()
+            self.assertIn("Expires", response)
+            self.assertEqual(response["Expires"], "Sun, 17 Jul 2016 10:00:02 GMT")
+            self.assertIn("Cache-Control", response)
+            self.assertEqual(response["Cache-Control"], "max-age=2")
+            self.assertNotIn("Age", response)
+
+        # Second request one second later. Response from the cache gets
+        # returned with an Age header set to 1 (second).
+        with mock.patch.object(
+            time, "time", return_value=1468749601
+        ):  # Sun, 17 Jul 2016 10:00:01 GMT
+            response = view(request, 1)
+            response.close()
+            self.assertIn("Expires", response)
+            self.assertEqual(response["Expires"], "Sun, 17 Jul 2016 10:00:02 GMT")
+            self.assertIn("Cache-Control", response)
+            self.assertEqual(response["Cache-Control"], "max-age=2")
+            self.assertIn("Age", response)
+            self.assertEqual(response["Age"], "1")
+
 
 @override_settings(
     CACHE_MIDDLEWARE_KEY_PREFIX="settingsprefix",
diff --git a/tests/check_framework/test_model_checks.py b/tests/check_framework/test_model_checks.py
index 3075a61be8..be504f9c2d 100644
--- a/tests/check_framework/test_model_checks.py
+++ b/tests/check_framework/test_model_checks.py
@@ -287,8 +287,8 @@ class ConstraintNameTests(TestCase):
         class Model(models.Model):
             class Meta:
                 constraints = [
-                    models.CheckConstraint(check=models.Q(id__gt=0), name="foo"),
-                    models.CheckConstraint(check=models.Q(id__lt=100), name="foo"),
+                    models.CheckConstraint(condition=models.Q(id__gt=0), name="foo"),
+                    models.CheckConstraint(condition=models.Q(id__lt=100), name="foo"),
                 ]
 
         self.assertEqual(
@@ -303,7 +303,7 @@ class ConstraintNameTests(TestCase):
         )
 
     def test_collision_in_different_models(self):
-        constraint = models.CheckConstraint(check=models.Q(id__gt=0), name="foo")
+        constraint = models.CheckConstraint(condition=models.Q(id__gt=0), name="foo")
 
         class Model1(models.Model):
             class Meta:
@@ -328,7 +328,7 @@ class ConstraintNameTests(TestCase):
         class AbstractModel(models.Model):
             class Meta:
                 constraints = [
-                    models.CheckConstraint(check=models.Q(id__gt=0), name="foo")
+                    models.CheckConstraint(condition=models.Q(id__gt=0), name="foo")
                 ]
                 abstract = True
 
@@ -354,7 +354,7 @@ class ConstraintNameTests(TestCase):
             class Meta:
                 constraints = [
                     models.CheckConstraint(
-                        check=models.Q(id__gt=0), name="%(app_label)s_%(class)s_foo"
+                        condition=models.Q(id__gt=0), name="%(app_label)s_%(class)s_foo"
                     ),
                 ]
                 abstract = True
@@ -370,7 +370,7 @@ class ConstraintNameTests(TestCase):
     @modify_settings(INSTALLED_APPS={"append": "basic"})
     @isolate_apps("basic", "check_framework", kwarg_name="apps")
     def test_collision_across_apps(self, apps):
-        constraint = models.CheckConstraint(check=models.Q(id__gt=0), name="foo")
+        constraint = models.CheckConstraint(condition=models.Q(id__gt=0), name="foo")
 
         class Model1(models.Model):
             class Meta:
@@ -397,7 +397,7 @@ class ConstraintNameTests(TestCase):
     @isolate_apps("basic", "check_framework", kwarg_name="apps")
     def test_no_collision_across_apps_interpolation(self, apps):
         constraint = models.CheckConstraint(
-            check=models.Q(id__gt=0), name="%(app_label)s_%(class)s_foo"
+            condition=models.Q(id__gt=0), name="%(app_label)s_%(class)s_foo"
         )
 
         class Model1(models.Model):
diff --git a/tests/check_framework/test_templates.py b/tests/check_framework/test_templates.py
index c8a2f83b8a..18f705ddb8 100644
--- a/tests/check_framework/test_templates.py
+++ b/tests/check_framework/test_templates.py
@@ -1,128 +1,105 @@
-from copy import copy, deepcopy
+from copy import deepcopy
+from itertools import chain
 
-from django.core.checks import Warning
-from django.core.checks.templates import (
-    E001,
-    E002,
-    W003,
-    check_for_template_tags_with_the_same_name,
-    check_setting_app_dirs_loaders,
-    check_string_if_invalid_is_string,
-)
+from django.core.checks import Error, Warning
+from django.core.checks.templates import check_templates
+from django.template import engines
+from django.template.backends.base import BaseEngine
 from django.test import SimpleTestCase
 from django.test.utils import override_settings
 
 
-class CheckTemplateSettingsAppDirsTest(SimpleTestCase):
-    TEMPLATES_APP_DIRS_AND_LOADERS = [
-        {
-            "BACKEND": "django.template.backends.django.DjangoTemplates",
-            "APP_DIRS": True,
-            "OPTIONS": {
-                "loaders": ["django.template.loaders.filesystem.Loader"],
-            },
-        },
-    ]
+class ErrorEngine(BaseEngine):
+    def __init__(self, params):
+        params.pop("OPTIONS")
+        super().__init__(params)
 
-    @override_settings(TEMPLATES=TEMPLATES_APP_DIRS_AND_LOADERS)
-    def test_app_dirs_and_loaders(self):
-        """
-        Error if template loaders are specified and APP_DIRS is True.
-        """
-        self.assertEqual(check_setting_app_dirs_loaders(None), [E001])
+    def check(self, **kwargs):
+        return [Error("Example")]
 
-    def test_app_dirs_removed(self):
-        TEMPLATES = deepcopy(self.TEMPLATES_APP_DIRS_AND_LOADERS)
-        del TEMPLATES[0]["APP_DIRS"]
-        with self.settings(TEMPLATES=TEMPLATES):
-            self.assertEqual(check_setting_app_dirs_loaders(None), [])
 
-    def test_loaders_removed(self):
-        TEMPLATES = deepcopy(self.TEMPLATES_APP_DIRS_AND_LOADERS)
-        del TEMPLATES[0]["OPTIONS"]["loaders"]
-        with self.settings(TEMPLATES=TEMPLATES):
-            self.assertEqual(check_setting_app_dirs_loaders(None), [])
+class CheckTemplatesTests(SimpleTestCase):
+    @override_settings(
+        TEMPLATES=[
+            {"BACKEND": f"{__name__}.{ErrorEngine.__qualname__}", "NAME": "backend_1"},
+            {"BACKEND": f"{__name__}.{ErrorEngine.__qualname__}", "NAME": "backend_2"},
+        ]
+    )
+    def test_errors_aggregated(self):
+        errors = check_templates(None)
+        self.assertEqual(errors, [Error("Example")] * 2)
 
 
 class CheckTemplateStringIfInvalidTest(SimpleTestCase):
     TEMPLATES_STRING_IF_INVALID = [
         {
             "BACKEND": "django.template.backends.django.DjangoTemplates",
+            "NAME": "backend_1",
             "OPTIONS": {
                 "string_if_invalid": False,
             },
         },
         {
             "BACKEND": "django.template.backends.django.DjangoTemplates",
+            "NAME": "backend_2",
             "OPTIONS": {
                 "string_if_invalid": 42,
             },
         },
     ]
 
-    @classmethod
-    def setUpClass(cls):
-        super().setUpClass()
-        cls.error1 = copy(E002)
-        cls.error2 = copy(E002)
-        string_if_invalid1 = cls.TEMPLATES_STRING_IF_INVALID[0]["OPTIONS"][
-            "string_if_invalid"
-        ]
-        string_if_invalid2 = cls.TEMPLATES_STRING_IF_INVALID[1]["OPTIONS"][
-            "string_if_invalid"
-        ]
-        cls.error1.msg = cls.error1.msg.format(
-            string_if_invalid1, type(string_if_invalid1).__name__
+    def _get_error_for_engine(self, engine):
+        value = engine.engine.string_if_invalid
+        return Error(
+            "'string_if_invalid' in TEMPLATES OPTIONS must be a string but got: %r "
+            "(%s)." % (value, type(value)),
+            obj=engine,
+            id="templates.E002",
         )
-        cls.error2.msg = cls.error2.msg.format(
-            string_if_invalid2, type(string_if_invalid2).__name__
+
+    def _check_engines(self, engines):
+        return list(
+            chain.from_iterable(e._check_string_if_invalid_is_string() for e in engines)
         )
 
     @override_settings(TEMPLATES=TEMPLATES_STRING_IF_INVALID)
     def test_string_if_invalid_not_string(self):
-        self.assertEqual(
-            check_string_if_invalid_is_string(None), [self.error1, self.error2]
-        )
+        _engines = engines.all()
+        errors = [
+            self._get_error_for_engine(_engines[0]),
+            self._get_error_for_engine(_engines[1]),
+        ]
+        self.assertEqual(self._check_engines(_engines), errors)
 
     def test_string_if_invalid_first_is_string(self):
         TEMPLATES = deepcopy(self.TEMPLATES_STRING_IF_INVALID)
         TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = "test"
         with self.settings(TEMPLATES=TEMPLATES):
-            self.assertEqual(check_string_if_invalid_is_string(None), [self.error2])
+            _engines = engines.all()
+            errors = [self._get_error_for_engine(_engines[1])]
+            self.assertEqual(self._check_engines(_engines), errors)
 
     def test_string_if_invalid_both_are_strings(self):
         TEMPLATES = deepcopy(self.TEMPLATES_STRING_IF_INVALID)
         TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = "test"
         TEMPLATES[1]["OPTIONS"]["string_if_invalid"] = "test"
         with self.settings(TEMPLATES=TEMPLATES):
-            self.assertEqual(check_string_if_invalid_is_string(None), [])
+            self.assertEqual(self._check_engines(engines.all()), [])
 
     def test_string_if_invalid_not_specified(self):
         TEMPLATES = deepcopy(self.TEMPLATES_STRING_IF_INVALID)
         del TEMPLATES[1]["OPTIONS"]["string_if_invalid"]
         with self.settings(TEMPLATES=TEMPLATES):
-            self.assertEqual(check_string_if_invalid_is_string(None), [self.error1])
+            _engines = engines.all()
+            errors = [self._get_error_for_engine(_engines[0])]
+            self.assertEqual(self._check_engines(_engines), errors)
 
 
 class CheckTemplateTagLibrariesWithSameName(SimpleTestCase):
-    @classmethod
-    def setUpClass(cls):
-        super().setUpClass()
-        cls.warning_same_tags = Warning(
-            W003.msg.format(
-                "'same_tags'",
-                "'check_framework.template_test_apps.same_tags_app_1."
-                "templatetags.same_tags', "
-                "'check_framework.template_test_apps.same_tags_app_2."
-                "templatetags.same_tags'",
-            ),
-            id=W003.id,
-        )
-
-    @staticmethod
-    def get_settings(module_name, module_path):
+    def get_settings(self, module_name, module_path, name="django"):
         return {
             "BACKEND": "django.template.backends.django.DjangoTemplates",
+            "NAME": name,
             "OPTIONS": {
                 "libraries": {
                     module_name: f"check_framework.template_test_apps.{module_path}",
@@ -130,6 +107,20 @@ class CheckTemplateTagLibrariesWithSameName(SimpleTestCase):
             },
         }
 
+    def _get_error_for_engine(self, engine, modules):
+        return Warning(
+            f"'same_tags' is used for multiple template tag modules: {modules}",
+            obj=engine,
+            id="templates.W003",
+        )
+
+    def _check_engines(self, engines):
+        return list(
+            chain.from_iterable(
+                e._check_for_template_tags_with_the_same_name() for e in engines
+            )
+        )
+
     @override_settings(
         INSTALLED_APPS=[
             "check_framework.template_test_apps.same_tags_app_1",
@@ -137,26 +128,32 @@ class CheckTemplateTagLibrariesWithSameName(SimpleTestCase):
         ]
     )
     def test_template_tags_with_same_name(self):
-        self.assertEqual(
-            check_for_template_tags_with_the_same_name(None),
-            [self.warning_same_tags],
+        _engines = engines.all()
+        modules = (
+            "'check_framework.template_test_apps.same_tags_app_1.templatetags"
+            ".same_tags', 'check_framework.template_test_apps.same_tags_app_2"
+            ".templatetags.same_tags'"
         )
+        errors = [self._get_error_for_engine(_engines[0], modules)]
+        self.assertEqual(self._check_engines(_engines), errors)
 
-    def test_template_tags_with_same_library_name(self):
+    def test_template_tags_for_separate_backends(self):
+        # The "libraries" names are the same, but the backends are different.
         with self.settings(
             TEMPLATES=[
                 self.get_settings(
-                    "same_tags", "same_tags_app_1.templatetags.same_tags"
+                    "same_tags",
+                    "same_tags_app_1.templatetags.same_tags",
+                    name="backend_1",
                 ),
                 self.get_settings(
-                    "same_tags", "same_tags_app_2.templatetags.same_tags"
+                    "same_tags",
+                    "same_tags_app_2.templatetags.same_tags",
+                    name="backend_2",
                 ),
             ]
         ):
-            self.assertEqual(
-                check_for_template_tags_with_the_same_name(None),
-                [self.warning_same_tags],
-            )
+            self.assertEqual(self._check_engines(engines.all()), [])
 
     @override_settings(
         INSTALLED_APPS=["check_framework.template_test_apps.same_tags_app_1"]
@@ -169,48 +166,44 @@ class CheckTemplateTagLibrariesWithSameName(SimpleTestCase):
                 ),
             ]
         ):
-            self.assertEqual(check_for_template_tags_with_the_same_name(None), [])
+            self.assertEqual(self._check_engines(engines.all()), [])
 
     @override_settings(
         INSTALLED_APPS=["check_framework.template_test_apps.same_tags_app_1"]
     )
     def test_template_tags_with_same_library_name_and_module_name(self):
+        modules = (
+            "'check_framework.template_test_apps.different_tags_app.templatetags"
+            ".different_tags', 'check_framework.template_test_apps.same_tags_app_1"
+            ".templatetags.same_tags'"
+        )
         with self.settings(
             TEMPLATES=[
                 self.get_settings(
-                    "same_tags",
-                    "different_tags_app.templatetags.different_tags",
+                    "same_tags", "different_tags_app.templatetags.different_tags"
                 ),
             ]
         ):
-            self.assertEqual(
-                check_for_template_tags_with_the_same_name(None),
-                [
-                    Warning(
-                        W003.msg.format(
-                            "'same_tags'",
-                            "'check_framework.template_test_apps.different_tags_app."
-                            "templatetags.different_tags', "
-                            "'check_framework.template_test_apps.same_tags_app_1."
-                            "templatetags.same_tags'",
-                        ),
-                        id=W003.id,
-                    )
-                ],
-            )
+            _engines = engines.all()
+            errors = [self._get_error_for_engine(_engines[0], modules)]
+            self.assertEqual(self._check_engines(_engines), errors)
 
     def test_template_tags_with_different_library_name(self):
         with self.settings(
             TEMPLATES=[
                 self.get_settings(
-                    "same_tags", "same_tags_app_1.templatetags.same_tags"
+                    "same_tags",
+                    "same_tags_app_1.templatetags.same_tags",
+                    name="backend_1",
                 ),
                 self.get_settings(
-                    "not_same_tags", "same_tags_app_2.templatetags.same_tags"
+                    "not_same_tags",
+                    "same_tags_app_2.templatetags.same_tags",
+                    name="backend_2",
                 ),
             ]
         ):
-            self.assertEqual(check_for_template_tags_with_the_same_name(None), [])
+            self.assertEqual(self._check_engines(engines.all()), [])
 
     @override_settings(
         INSTALLED_APPS=[
@@ -219,4 +212,4 @@ class CheckTemplateTagLibrariesWithSameName(SimpleTestCase):
         ]
     )
     def test_template_tags_with_different_name(self):
-        self.assertEqual(check_for_template_tags_with_the_same_name(None), [])
+        self.assertEqual(self._check_engines(engines.all()), [])
diff --git a/tests/check_framework/test_urls.py b/tests/check_framework/test_urls.py
index 4b6a4a6f3e..a31c5fd856 100644
--- a/tests/check_framework/test_urls.py
+++ b/tests/check_framework/test_urls.py
@@ -2,6 +2,7 @@ from django.conf import settings
 from django.core.checks.messages import Error, Warning
 from django.core.checks.urls import (
     E006,
+    check_custom_error_handlers,
     check_url_config,
     check_url_namespaces_unique,
     check_url_settings,
@@ -243,7 +244,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase):
         ROOT_URLCONF="check_framework.urls.bad_function_based_error_handlers",
     )
     def test_bad_function_based_handlers(self):
-        result = check_url_config(None)
+        result = check_custom_error_handlers(None)
         self.assertEqual(len(result), 4)
         for code, num_params, error in zip([400, 403, 404, 500], [2, 2, 2, 1], result):
             with self.subTest("handler{}".format(code)):
@@ -264,7 +265,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase):
         ROOT_URLCONF="check_framework.urls.bad_class_based_error_handlers",
     )
     def test_bad_class_based_handlers(self):
-        result = check_url_config(None)
+        result = check_custom_error_handlers(None)
         self.assertEqual(len(result), 4)
         for code, num_params, error in zip([400, 403, 404, 500], [2, 2, 2, 1], result):
             with self.subTest("handler%s" % code):
@@ -287,7 +288,7 @@ class CheckCustomErrorHandlersTests(SimpleTestCase):
         ROOT_URLCONF="check_framework.urls.bad_error_handlers_invalid_path"
     )
     def test_bad_handlers_invalid_path(self):
-        result = check_url_config(None)
+        result = check_custom_error_handlers(None)
         paths = [
             "django.views.bad_handler",
             "django.invalid_module.bad_handler",
@@ -318,14 +319,14 @@ class CheckCustomErrorHandlersTests(SimpleTestCase):
         ROOT_URLCONF="check_framework.urls.good_function_based_error_handlers",
     )
     def test_good_function_based_handlers(self):
-        result = check_url_config(None)
+        result = check_custom_error_handlers(None)
         self.assertEqual(result, [])
 
     @override_settings(
         ROOT_URLCONF="check_framework.urls.good_class_based_error_handlers",
     )
     def test_good_class_based_handlers(self):
-        result = check_url_config(None)
+        result = check_custom_error_handlers(None)
         self.assertEqual(result, [])
 
 
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 3ea5cf2323..87b97b2a85 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -12,15 +12,15 @@ class Product(models.Model):
         }
         constraints = [
             models.CheckConstraint(
-                check=models.Q(price__gt=models.F("discounted_price")),
+                condition=models.Q(price__gt=models.F("discounted_price")),
                 name="price_gt_discounted_price",
             ),
             models.CheckConstraint(
-                check=models.Q(price__gt=0),
+                condition=models.Q(price__gt=0),
                 name="%(app_label)s_%(class)s_price_gt_0",
             ),
             models.CheckConstraint(
-                check=models.Q(
+                condition=models.Q(
                     models.Q(unit__isnull=True) | models.Q(unit__in=["μg/mL", "ng/mL"])
                 ),
                 name="unicode_unit_list",
@@ -113,7 +113,7 @@ class AbstractModel(models.Model):
         }
         constraints = [
             models.CheckConstraint(
-                check=models.Q(age__gte=18),
+                condition=models.Q(age__gte=18),
                 name="%(app_label)s_%(class)s_adult",
             ),
         ]
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 55df5975de..86efaa79e7 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -123,104 +123,108 @@ class CheckConstraintTests(TestCase):
         check1 = models.Q(price__gt=models.F("discounted_price"))
         check2 = models.Q(price__lt=models.F("discounted_price"))
         self.assertEqual(
-            models.CheckConstraint(check=check1, name="price"),
-            models.CheckConstraint(check=check1, name="price"),
+            models.CheckConstraint(condition=check1, name="price"),
+            models.CheckConstraint(condition=check1, name="price"),
         )
-        self.assertEqual(models.CheckConstraint(check=check1, name="price"), mock.ANY)
-        self.assertNotEqual(
-            models.CheckConstraint(check=check1, name="price"),
-            models.CheckConstraint(check=check1, name="price2"),
+        self.assertEqual(
+            models.CheckConstraint(condition=check1, name="price"), mock.ANY
         )
         self.assertNotEqual(
-            models.CheckConstraint(check=check1, name="price"),
-            models.CheckConstraint(check=check2, name="price"),
+            models.CheckConstraint(condition=check1, name="price"),
+            models.CheckConstraint(condition=check1, name="price2"),
         )
-        self.assertNotEqual(models.CheckConstraint(check=check1, name="price"), 1)
         self.assertNotEqual(
-            models.CheckConstraint(check=check1, name="price"),
+            models.CheckConstraint(condition=check1, name="price"),
+            models.CheckConstraint(condition=check2, name="price"),
+        )
+        self.assertNotEqual(models.CheckConstraint(condition=check1, name="price"), 1)
+        self.assertNotEqual(
+            models.CheckConstraint(condition=check1, name="price"),
             models.CheckConstraint(
-                check=check1, name="price", violation_error_message="custom error"
+                condition=check1, name="price", violation_error_message="custom error"
             ),
         )
         self.assertNotEqual(
             models.CheckConstraint(
-                check=check1, name="price", violation_error_message="custom error"
+                condition=check1, name="price", violation_error_message="custom error"
             ),
             models.CheckConstraint(
-                check=check1, name="price", violation_error_message="other custom error"
+                condition=check1,
+                name="price",
+                violation_error_message="other custom error",
             ),
         )
         self.assertEqual(
             models.CheckConstraint(
-                check=check1, name="price", violation_error_message="custom error"
+                condition=check1, name="price", violation_error_message="custom error"
             ),
             models.CheckConstraint(
-                check=check1, name="price", violation_error_message="custom error"
+                condition=check1, name="price", violation_error_message="custom error"
             ),
         )
         self.assertNotEqual(
-            models.CheckConstraint(check=check1, name="price"),
+            models.CheckConstraint(condition=check1, name="price"),
             models.CheckConstraint(
-                check=check1, name="price", violation_error_code="custom_code"
+                condition=check1, name="price", violation_error_code="custom_code"
             ),
         )
         self.assertEqual(
             models.CheckConstraint(
-                check=check1, name="price", violation_error_code="custom_code"
+                condition=check1, name="price", violation_error_code="custom_code"
             ),
             models.CheckConstraint(
-                check=check1, name="price", violation_error_code="custom_code"
+                condition=check1, name="price", violation_error_code="custom_code"
             ),
         )
 
     def test_repr(self):
         constraint = models.CheckConstraint(
-            check=models.Q(price__gt=models.F("discounted_price")),
+            condition=models.Q(price__gt=models.F("discounted_price")),
             name="price_gt_discounted_price",
         )
         self.assertEqual(
             repr(constraint),
-            "<CheckConstraint: check=(AND: ('price__gt', F(discounted_price))) "
+            "<CheckConstraint: condition=(AND: ('price__gt', F(discounted_price))) "
             "name='price_gt_discounted_price'>",
         )
 
     def test_repr_with_violation_error_message(self):
         constraint = models.CheckConstraint(
-            check=models.Q(price__lt=1),
+            condition=models.Q(price__lt=1),
             name="price_lt_one",
             violation_error_message="More than 1",
         )
         self.assertEqual(
             repr(constraint),
-            "<CheckConstraint: check=(AND: ('price__lt', 1)) name='price_lt_one' "
+            "<CheckConstraint: condition=(AND: ('price__lt', 1)) name='price_lt_one' "
             "violation_error_message='More than 1'>",
         )
 
     def test_repr_with_violation_error_code(self):
         constraint = models.CheckConstraint(
-            check=models.Q(price__lt=1),
+            condition=models.Q(price__lt=1),
             name="price_lt_one",
             violation_error_code="more_than_one",
         )
         self.assertEqual(
             repr(constraint),
-            "<CheckConstraint: check=(AND: ('price__lt', 1)) name='price_lt_one' "
+            "<CheckConstraint: condition=(AND: ('price__lt', 1)) name='price_lt_one' "
             "violation_error_code='more_than_one'>",
         )
 
     def test_invalid_check_types(self):
-        msg = "CheckConstraint.check must be a Q instance or boolean expression."
+        msg = "CheckConstraint.condition must be a Q instance or boolean expression."
         with self.assertRaisesMessage(TypeError, msg):
-            models.CheckConstraint(check=models.F("discounted_price"), name="check")
+            models.CheckConstraint(condition=models.F("discounted_price"), name="check")
 
     def test_deconstruction(self):
         check = models.Q(price__gt=models.F("discounted_price"))
         name = "price_gt_discounted_price"
-        constraint = models.CheckConstraint(check=check, name=name)
+        constraint = models.CheckConstraint(condition=check, name=name)
         path, args, kwargs = constraint.deconstruct()
         self.assertEqual(path, "django.db.models.CheckConstraint")
         self.assertEqual(args, ())
-        self.assertEqual(kwargs, {"check": check, "name": name})
+        self.assertEqual(kwargs, {"condition": check, "name": name})
 
     @skipUnlessDBFeature("supports_table_check_constraints")
     def test_database_constraint(self):
@@ -255,7 +259,7 @@ class CheckConstraintTests(TestCase):
 
     def test_validate(self):
         check = models.Q(price__gt=models.F("discounted_price"))
-        constraint = models.CheckConstraint(check=check, name="price")
+        constraint = models.CheckConstraint(condition=check, name="price")
         # Invalid product.
         invalid_product = Product(price=10, discounted_price=42)
         with self.assertRaises(ValidationError):
@@ -276,7 +280,7 @@ class CheckConstraintTests(TestCase):
     def test_validate_custom_error(self):
         check = models.Q(price__gt=models.F("discounted_price"))
         constraint = models.CheckConstraint(
-            check=check,
+            condition=check,
             name="price",
             violation_error_message="discount is fake",
             violation_error_code="fake_discount",
@@ -290,7 +294,7 @@ class CheckConstraintTests(TestCase):
 
     def test_validate_boolean_expressions(self):
         constraint = models.CheckConstraint(
-            check=models.expressions.ExpressionWrapper(
+            condition=models.expressions.ExpressionWrapper(
                 models.Q(price__gt=500) | models.Q(price__lt=500),
                 output_field=models.BooleanField(),
             ),
@@ -304,7 +308,7 @@ class CheckConstraintTests(TestCase):
 
     def test_validate_rawsql_expressions_noop(self):
         constraint = models.CheckConstraint(
-            check=models.expressions.RawSQL(
+            condition=models.expressions.RawSQL(
                 "price < %s OR price > %s",
                 (500, 500),
                 output_field=models.BooleanField(),
@@ -320,7 +324,7 @@ class CheckConstraintTests(TestCase):
     def test_validate_nullable_field_with_none(self):
         # Nullable fields should be considered valid on None values.
         constraint = models.CheckConstraint(
-            check=models.Q(price__gte=0),
+            condition=models.Q(price__gte=0),
             name="positive_price",
         )
         constraint.validate(Product, Product())
@@ -328,7 +332,7 @@ class CheckConstraintTests(TestCase):
     @skipIfDBFeature("supports_comparing_boolean_expr")
     def test_validate_nullable_field_with_isnull(self):
         constraint = models.CheckConstraint(
-            check=models.Q(price__gte=0) | models.Q(price__isnull=True),
+            condition=models.Q(price__gte=0) | models.Q(price__isnull=True),
             name="positive_price",
         )
         constraint.validate(Product, Product())
@@ -336,11 +340,11 @@ class CheckConstraintTests(TestCase):
     @skipUnlessDBFeature("supports_json_field")
     def test_validate_nullable_jsonfield(self):
         is_null_constraint = models.CheckConstraint(
-            check=models.Q(data__isnull=True),
+            condition=models.Q(data__isnull=True),
             name="nullable_data",
         )
         is_not_null_constraint = models.CheckConstraint(
-            check=models.Q(data__isnull=False),
+            condition=models.Q(data__isnull=False),
             name="nullable_data",
         )
         is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None))
@@ -354,7 +358,7 @@ class CheckConstraintTests(TestCase):
 
     def test_validate_pk_field(self):
         constraint_with_pk = models.CheckConstraint(
-            check=~models.Q(pk=models.F("age")),
+            condition=~models.Q(pk=models.F("age")),
             name="pk_not_age_check",
         )
         constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=2))
@@ -365,6 +369,33 @@ class CheckConstraintTests(TestCase):
             constraint_with_pk.validate(ChildModel, ChildModel(id=1, age=1))
         constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1), exclude={"pk"})
 
+    @skipUnlessDBFeature("supports_json_field")
+    def test_validate_jsonfield_exact(self):
+        data = {"release": "5.0.2", "version": "stable"}
+        json_exact_constraint = models.CheckConstraint(
+            condition=models.Q(data__version="stable"),
+            name="only_stable_version",
+        )
+        json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
+
+        data = {"release": "5.0.2", "version": "not stable"}
+        msg = f"Constraint “{json_exact_constraint.name}” is violated."
+        with self.assertRaisesMessage(ValidationError, msg):
+            json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
+
+    def test_check_deprecation(self):
+        msg = "CheckConstraint.check is deprecated in favor of `.condition`."
+        condition = models.Q(foo="bar")
+        with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
+            constraint = models.CheckConstraint(name="constraint", check=condition)
+        with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
+            self.assertIs(constraint.check, condition)
+        other_condition = models.Q(something="else")
+        with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
+            constraint.check = other_condition
+        with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
+            self.assertIs(constraint.check, other_condition)
+
 
 class UniqueConstraintTests(TestCase):
     @classmethod
diff --git a/tests/contenttypes_tests/test_fields.py b/tests/contenttypes_tests/test_fields.py
index 5510f34cd0..15f1dafd63 100644
--- a/tests/contenttypes_tests/test_fields.py
+++ b/tests/contenttypes_tests/test_fields.py
@@ -45,6 +45,18 @@ class GenericForeignKeyTests(TestCase):
         new_entity = answer.question
         self.assertIsNot(old_entity, new_entity)
 
+    def test_clear_cached_generic_relation_explicit_fields(self):
+        question = Question.objects.create(text="question")
+        answer = Answer.objects.create(text="answer", question=question)
+        old_question_obj = answer.question
+        # The reverse relation is not refreshed if not passed explicitly in
+        # `fields`.
+        answer.refresh_from_db(fields=["text"])
+        self.assertIs(answer.question, old_question_obj)
+        answer.refresh_from_db(fields=["question"])
+        self.assertIsNot(answer.question, old_question_obj)
+        self.assertEqual(answer.question, old_question_obj)
+
 
 class GenericRelationTests(TestCase):
     def test_value_to_string(self):
@@ -55,6 +67,29 @@ class GenericRelationTests(TestCase):
         self.assertCountEqual(result, [answer1.pk, answer2.pk])
 
 
+class DeferredGenericRelationTests(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.question = Question.objects.create(text="question")
+        cls.answer = Answer.objects.create(text="answer", question=cls.question)
+
+    def test_defer_not_clear_cached_private_relations(self):
+        obj = Answer.objects.defer("text").get(pk=self.answer.pk)
+        with self.assertNumQueries(1):
+            obj.question
+        obj.text  # Accessing a deferred field.
+        with self.assertNumQueries(0):
+            obj.question
+
+    def test_only_not_clear_cached_private_relations(self):
+        obj = Answer.objects.only("content_type", "object_id").get(pk=self.answer.pk)
+        with self.assertNumQueries(1):
+            obj.question
+        obj.text  # Accessing a deferred field.
+        with self.assertNumQueries(0):
+            obj.question
+
+
 class GetPrefetchQuerySetDeprecation(TestCase):
     def test_generic_relation_warning(self):
         Question.objects.create(text="test")
diff --git a/tests/contenttypes_tests/test_models.py b/tests/contenttypes_tests/test_models.py
index 88f715ceff..799f1cc58c 100644
--- a/tests/contenttypes_tests/test_models.py
+++ b/tests/contenttypes_tests/test_models.py
@@ -331,6 +331,14 @@ class ContentTypesMultidbTests(TestCase):
 
 
 class GenericPrefetchTests(TestCase):
+    def test_querysets_required(self):
+        msg = (
+            "GenericPrefetch.__init__() missing 1 required "
+            "positional argument: 'querysets'"
+        )
+        with self.assertRaisesMessage(TypeError, msg):
+            GenericPrefetch("question")
+
     def test_values_queryset(self):
         msg = "Prefetch querysets cannot use raw(), values(), and values_list()."
         with self.assertRaisesMessage(ValueError, msg):
diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py
index 9407221cd1..b736276534 100644
--- a/tests/csrf_tests/tests.py
+++ b/tests/csrf_tests/tests.py
@@ -709,25 +709,21 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
         response = mw.process_view(req, post_form_view, (), {})
         self.assertContains(response, malformed_referer_msg, status_code=403)
         # missing scheme
-        # >>> urlparse('//example.com/')
-        # ParseResult(
-        #   scheme='', netloc='example.com', path='/', params='', query='', fragment='',
-        # )
+        # >>> urlsplit('//example.com/')
+        # SplitResult(scheme='', netloc='example.com', path='/', query='', fragment='')
         req.META["HTTP_REFERER"] = "//example.com/"
         self._check_referer_rejects(mw, req)
         response = mw.process_view(req, post_form_view, (), {})
         self.assertContains(response, malformed_referer_msg, status_code=403)
         # missing netloc
-        # >>> urlparse('https://')
-        # ParseResult(
-        #   scheme='https', netloc='', path='', params='', query='', fragment='',
-        # )
+        # >>> urlsplit('https://')
+        # SplitResult(scheme='https', netloc='', path='', query='', fragment='')
         req.META["HTTP_REFERER"] = "https://"
         self._check_referer_rejects(mw, req)
         response = mw.process_view(req, post_form_view, (), {})
         self.assertContains(response, malformed_referer_msg, status_code=403)
         # Invalid URL
-        # >>> urlparse('https://[')
+        # >>> urlsplit('https://[')
         # ValueError: Invalid IPv6 URL
         req.META["HTTP_REFERER"] = "https://["
         self._check_referer_rejects(mw, req)
@@ -979,7 +975,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
     @override_settings(ALLOWED_HOSTS=["www.example.com"])
     def test_bad_origin_cannot_be_parsed(self):
         """
-        A POST request with an origin that can't be parsed by urlparse() is
+        A POST request with an origin that can't be parsed by urlsplit() is
         rejected.
         """
         req = self._get_POST_request_with_token()
diff --git a/tests/custom_migration_operations/operations.py b/tests/custom_migration_operations/operations.py
index f63f0b2a3a..6bed8559d1 100644
--- a/tests/custom_migration_operations/operations.py
+++ b/tests/custom_migration_operations/operations.py
@@ -68,6 +68,11 @@ class ArgsKwargsOperation(TestOperation):
         )
 
 
+class ArgsAndKeywordOnlyArgsOperation(ArgsKwargsOperation):
+    def __init__(self, arg1, arg2, *, kwarg1, kwarg2):
+        super().__init__(arg1, arg2, kwarg1=kwarg1, kwarg2=kwarg2)
+
+
 class ExpandArgsOperation(TestOperation):
     serialization_expand_args = ["arg"]
 
diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py
index e576e6e464..3f13ca7989 100644
--- a/tests/db_functions/datetime/test_extract_trunc.py
+++ b/tests/db_functions/datetime/test_extract_trunc.py
@@ -939,73 +939,74 @@ class DateFunctionTests(TestCase):
         self.create_model(start_datetime, end_datetime)
         self.create_model(end_datetime, start_datetime)
 
-        def test_datetime_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc(
-                        "start_datetime", kind, output_field=DateTimeField()
-                    )
-                ).order_by("start_datetime"),
+        def assertDatetimeKind(kind):
+            truncated_start = truncate_to(start_datetime, kind)
+            truncated_end = truncate_to(end_datetime, kind)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc("start_datetime", kind, output_field=DateTimeField())
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (start_datetime, truncate_to(start_datetime, kind)),
-                    (end_datetime, truncate_to(end_datetime, kind)),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        def test_date_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc("start_date", kind, output_field=DateField())
-                ).order_by("start_datetime"),
+        def assertDateKind(kind):
+            truncated_start = truncate_to(start_datetime.date(), kind)
+            truncated_end = truncate_to(end_datetime.date(), kind)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc("start_date", kind, output_field=DateField())
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (start_datetime, truncate_to(start_datetime.date(), kind)),
-                    (end_datetime, truncate_to(end_datetime.date(), kind)),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        def test_time_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc("start_time", kind, output_field=TimeField())
-                ).order_by("start_datetime"),
+        def assertTimeKind(kind):
+            truncated_start = truncate_to(start_datetime.time(), kind)
+            truncated_end = truncate_to(end_datetime.time(), kind)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc("start_time", kind, output_field=TimeField())
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (start_datetime, truncate_to(start_datetime.time(), kind)),
-                    (end_datetime, truncate_to(end_datetime.time(), kind)),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        def test_datetime_to_time_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc("start_datetime", kind, output_field=TimeField()),
-                ).order_by("start_datetime"),
+        def assertDatetimeToTimeKind(kind):
+            truncated_start = truncate_to(start_datetime.time(), kind)
+            truncated_end = truncate_to(end_datetime.time(), kind)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc("start_datetime", kind, output_field=TimeField()),
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (start_datetime, truncate_to(start_datetime.time(), kind)),
-                    (end_datetime, truncate_to(end_datetime.time(), kind)),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        test_date_kind("year")
-        test_date_kind("quarter")
-        test_date_kind("month")
-        test_date_kind("day")
-        test_time_kind("hour")
-        test_time_kind("minute")
-        test_time_kind("second")
-        test_datetime_kind("year")
-        test_datetime_kind("quarter")
-        test_datetime_kind("month")
-        test_datetime_kind("day")
-        test_datetime_kind("hour")
-        test_datetime_kind("minute")
-        test_datetime_kind("second")
-        test_datetime_to_time_kind("hour")
-        test_datetime_to_time_kind("minute")
-        test_datetime_to_time_kind("second")
+        date_truncations = ["year", "quarter", "month", "day"]
+        time_truncations = ["hour", "minute", "second"]
+        tests = [
+            (assertDateKind, date_truncations),
+            (assertTimeKind, time_truncations),
+            (assertDatetimeKind, [*date_truncations, *time_truncations]),
+            (assertDatetimeToTimeKind, time_truncations),
+        ]
+        for assertion, truncations in tests:
+            for truncation in truncations:
+                with self.subTest(assertion=assertion.__name__, truncation=truncation):
+                    assertion(truncation)
 
         qs = DTModel.objects.filter(
             start_datetime__date=Trunc(
@@ -1831,93 +1832,90 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         end_datetime = timezone.make_aware(end_datetime)
         self.create_model(start_datetime, end_datetime)
         self.create_model(end_datetime, start_datetime)
-        melb = zoneinfo.ZoneInfo("Australia/Melbourne")
 
-        def test_datetime_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc(
-                        "start_datetime",
-                        kind,
-                        output_field=DateTimeField(),
-                        tzinfo=melb,
-                    )
-                ).order_by("start_datetime"),
+        def assertDatetimeKind(kind, tzinfo):
+            truncated_start = truncate_to(
+                start_datetime.astimezone(tzinfo), kind, tzinfo
+            )
+            truncated_end = truncate_to(end_datetime.astimezone(tzinfo), kind, tzinfo)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc(
+                    "start_datetime",
+                    kind,
+                    output_field=DateTimeField(),
+                    tzinfo=tzinfo,
+                )
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (
-                        start_datetime,
-                        truncate_to(start_datetime.astimezone(melb), kind, melb),
-                    ),
-                    (
-                        end_datetime,
-                        truncate_to(end_datetime.astimezone(melb), kind, melb),
-                    ),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        def test_datetime_to_date_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc(
-                        "start_datetime",
-                        kind,
-                        output_field=DateField(),
-                        tzinfo=melb,
-                    ),
-                ).order_by("start_datetime"),
+        def assertDatetimeToDateKind(kind, tzinfo):
+            truncated_start = truncate_to(
+                start_datetime.astimezone(tzinfo).date(), kind
+            )
+            truncated_end = truncate_to(end_datetime.astimezone(tzinfo).date(), kind)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc(
+                    "start_datetime",
+                    kind,
+                    output_field=DateField(),
+                    tzinfo=tzinfo,
+                ),
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (
-                        start_datetime,
-                        truncate_to(start_datetime.astimezone(melb).date(), kind),
-                    ),
-                    (
-                        end_datetime,
-                        truncate_to(end_datetime.astimezone(melb).date(), kind),
-                    ),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        def test_datetime_to_time_kind(kind):
-            self.assertQuerySetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc(
-                        "start_datetime",
-                        kind,
-                        output_field=TimeField(),
-                        tzinfo=melb,
-                    )
-                ).order_by("start_datetime"),
+        def assertDatetimeToTimeKind(kind, tzinfo):
+            truncated_start = truncate_to(
+                start_datetime.astimezone(tzinfo).time(), kind
+            )
+            truncated_end = truncate_to(end_datetime.astimezone(tzinfo).time(), kind)
+            queryset = DTModel.objects.annotate(
+                truncated=Trunc(
+                    "start_datetime",
+                    kind,
+                    output_field=TimeField(),
+                    tzinfo=tzinfo,
+                )
+            ).order_by("start_datetime")
+            self.assertSequenceEqual(
+                queryset.values_list("start_datetime", "truncated"),
                 [
-                    (
-                        start_datetime,
-                        truncate_to(start_datetime.astimezone(melb).time(), kind),
-                    ),
-                    (
-                        end_datetime,
-                        truncate_to(end_datetime.astimezone(melb).time(), kind),
-                    ),
+                    (start_datetime, truncated_start),
+                    (end_datetime, truncated_end),
                 ],
-                lambda m: (m.start_datetime, m.truncated),
             )
 
-        test_datetime_to_date_kind("year")
-        test_datetime_to_date_kind("quarter")
-        test_datetime_to_date_kind("month")
-        test_datetime_to_date_kind("week")
-        test_datetime_to_date_kind("day")
-        test_datetime_to_time_kind("hour")
-        test_datetime_to_time_kind("minute")
-        test_datetime_to_time_kind("second")
-        test_datetime_kind("year")
-        test_datetime_kind("quarter")
-        test_datetime_kind("month")
-        test_datetime_kind("week")
-        test_datetime_kind("day")
-        test_datetime_kind("hour")
-        test_datetime_kind("minute")
-        test_datetime_kind("second")
+        timezones = [
+            zoneinfo.ZoneInfo("Australia/Melbourne"),
+            zoneinfo.ZoneInfo("Etc/GMT+10"),
+        ]
+        date_truncations = ["year", "quarter", "month", "week", "day"]
+        time_truncations = ["hour", "minute", "second"]
+        tests = [
+            (assertDatetimeToDateKind, date_truncations),
+            (assertDatetimeToTimeKind, time_truncations),
+            (assertDatetimeKind, [*date_truncations, *time_truncations]),
+        ]
+        for assertion, truncations in tests:
+            for truncation in truncations:
+                for tzinfo in timezones:
+                    with self.subTest(
+                        assertion=assertion.__name__,
+                        truncation=truncation,
+                        tzinfo=tzinfo.key,
+                    ):
+                        assertion(truncation, tzinfo)
 
         qs = DTModel.objects.filter(
             start_datetime__date=Trunc(
diff --git a/tests/dbshell/test_postgresql.py b/tests/dbshell/test_postgresql.py
index 53dedaca01..79e2780d56 100644
--- a/tests/dbshell/test_postgresql.py
+++ b/tests/dbshell/test_postgresql.py
@@ -14,6 +14,7 @@ class PostgreSqlDbshellCommandTestCase(SimpleTestCase):
     def settings_to_cmd_args_env(self, settings_dict, parameters=None):
         if parameters is None:
             parameters = []
+        settings_dict.setdefault("OPTIONS", {})
         return DatabaseClient.settings_to_cmd_args_env(settings_dict, parameters)
 
     def test_basic(self):
diff --git a/tests/defer/models.py b/tests/defer/models.py
index 6de2e2af97..560e54c8c0 100644
--- a/tests/defer/models.py
+++ b/tests/defer/models.py
@@ -19,6 +19,14 @@ class Primary(models.Model):
         return self.name
 
 
+class PrimaryOneToOne(models.Model):
+    name = models.CharField(max_length=50)
+    value = models.CharField(max_length=50)
+    related = models.OneToOneField(
+        Secondary, models.CASCADE, related_name="primary_o2o"
+    )
+
+
 class Child(Primary):
     pass
 
diff --git a/tests/defer/tests.py b/tests/defer/tests.py
index c7eb03dc8a..3945b667ba 100644
--- a/tests/defer/tests.py
+++ b/tests/defer/tests.py
@@ -6,6 +6,7 @@ from .models import (
     Child,
     ChildProxy,
     Primary,
+    PrimaryOneToOne,
     RefreshPrimaryProxy,
     Secondary,
     ShadowChild,
@@ -326,3 +327,28 @@ class InvalidDeferTests(SimpleTestCase):
         )
         with self.assertRaisesMessage(FieldError, msg):
             Primary.objects.only("name").select_related("related")[0]
+
+
+class DeferredRelationTests(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.secondary = Secondary.objects.create(first="a", second="b")
+        cls.primary = PrimaryOneToOne.objects.create(
+            name="Bella", value="Baxter", related=cls.secondary
+        )
+
+    def test_defer_not_clear_cached_relations(self):
+        obj = Secondary.objects.defer("first").get(pk=self.secondary.pk)
+        with self.assertNumQueries(1):
+            obj.primary_o2o
+        obj.first  # Accessing a deferred field.
+        with self.assertNumQueries(0):
+            obj.primary_o2o
+
+    def test_only_not_clear_cached_relations(self):
+        obj = Secondary.objects.only("first").get(pk=self.secondary.pk)
+        with self.assertNumQueries(1):
+            obj.primary_o2o
+        obj.second  # Accessing a deferred field.
+        with self.assertNumQueries(0):
+            obj.primary_o2o
diff --git a/tests/defer_regress/models.py b/tests/defer_regress/models.py
index dd492993b7..38ba4a622f 100644
--- a/tests/defer_regress/models.py
+++ b/tests/defer_regress/models.py
@@ -10,6 +10,12 @@ class Item(models.Model):
     text = models.TextField(default="xyzzy")
     value = models.IntegerField()
     other_value = models.IntegerField(default=0)
+    source = models.OneToOneField(
+        "self",
+        related_name="destination",
+        on_delete=models.CASCADE,
+        null=True,
+    )
 
 
 class RelatedItem(models.Model):
diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py
index 10100e348d..1209325f21 100644
--- a/tests/defer_regress/tests.py
+++ b/tests/defer_regress/tests.py
@@ -309,6 +309,27 @@ class DeferRegressionTest(TestCase):
         with self.assertNumQueries(1):
             self.assertEqual(Item.objects.only("request").get(), item)
 
+    def test_self_referential_one_to_one(self):
+        first = Item.objects.create(name="first", value=1)
+        second = Item.objects.create(name="second", value=2, source=first)
+        with self.assertNumQueries(1):
+            deferred_first, deferred_second = (
+                Item.objects.select_related("source", "destination")
+                .only("name", "source__name", "destination__value")
+                .order_by("pk")
+            )
+        with self.assertNumQueries(0):
+            self.assertEqual(deferred_first.name, first.name)
+            self.assertEqual(deferred_second.name, second.name)
+            self.assertEqual(deferred_second.source.name, first.name)
+            self.assertEqual(deferred_first.destination.value, second.value)
+        with self.assertNumQueries(1):
+            self.assertEqual(deferred_first.value, first.value)
+        with self.assertNumQueries(1):
+            self.assertEqual(deferred_second.source.value, first.value)
+        with self.assertNumQueries(1):
+            self.assertEqual(deferred_first.destination.name, second.name)
+
 
 class DeferDeletionSignalsTests(TestCase):
     senders = [Item, Proxy]
diff --git a/tests/delete_regress/models.py b/tests/delete_regress/models.py
index cbe6fef334..4bc035e1c7 100644
--- a/tests/delete_regress/models.py
+++ b/tests/delete_regress/models.py
@@ -93,9 +93,6 @@ class Item(models.Model):
     location_value = models.ForeignKey(
         Location, models.SET(42), default=1, db_constraint=False, related_name="+"
     )
-    location_default = models.ForeignKey(
-        Location, models.SET_DEFAULT, default=1, db_constraint=False, related_name="+"
-    )
 
 
 # Models for #16128
@@ -151,3 +148,22 @@ class OrderedPerson(models.Model):
 
     class Meta:
         ordering = ["name"]
+
+
+def get_best_toy():
+    toy, _ = Toy.objects.get_or_create(name="best")
+    return toy
+
+
+def get_worst_toy():
+    toy, _ = Toy.objects.get_or_create(name="worst")
+    return toy
+
+
+class Collector(models.Model):
+    best_toy = models.ForeignKey(
+        Toy, default=get_best_toy, on_delete=models.SET_DEFAULT, related_name="toys"
+    )
+    worst_toy = models.ForeignKey(
+        Toy, models.SET(get_worst_toy), related_name="bad_toys"
+    )
diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py
index 89f4d5ddd8..ce5a0db8ab 100644
--- a/tests/delete_regress/tests.py
+++ b/tests/delete_regress/tests.py
@@ -408,9 +408,17 @@ class SetQueryCountTests(TestCase):
         Item.objects.create(
             version=version,
             location=location,
-            location_default=location,
             location_value=location,
         )
-        # 3 UPDATEs for SET of item values and one for DELETE locations.
-        with self.assertNumQueries(4):
+        # 2 UPDATEs for SET of item values and one for DELETE locations.
+        with self.assertNumQueries(3):
             location.delete()
+
+
+class SetCallableCollectorDefaultTests(TestCase):
+    def test_set(self):
+        # Collector doesn't call callables used by models.SET and
+        # models.SET_DEFAULT if not necessary.
+        Toy.objects.create(name="test")
+        Toy.objects.all().delete()
+        self.assertSequenceEqual(Toy.objects.all(), [])
diff --git a/tests/deprecation/test_middleware_mixin.py b/tests/deprecation/test_middleware_mixin.py
index 3b6ad6d8ee..f4eafc14e3 100644
--- a/tests/deprecation/test_middleware_mixin.py
+++ b/tests/deprecation/test_middleware_mixin.py
@@ -5,6 +5,7 @@ from asgiref.sync import async_to_sync, iscoroutinefunction
 from django.contrib.admindocs.middleware import XViewMiddleware
 from django.contrib.auth.middleware import (
     AuthenticationMiddleware,
+    LoginRequiredMiddleware,
     RemoteUserMiddleware,
 )
 from django.contrib.flatpages.middleware import FlatpageFallbackMiddleware
@@ -34,6 +35,7 @@ from django.utils.deprecation import MiddlewareMixin
 class MiddlewareMixinTests(SimpleTestCase):
     middlewares = [
         AuthenticationMiddleware,
+        LoginRequiredMiddleware,
         BrokenLinkEmailsMiddleware,
         CacheMiddleware,
         CommonMiddleware,
diff --git a/tests/expressions/models.py b/tests/expressions/models.py
index 43dd235fb5..31891a13d7 100644
--- a/tests/expressions/models.py
+++ b/tests/expressions/models.py
@@ -56,7 +56,11 @@ class Number(models.Model):
     decimal_value = models.DecimalField(max_digits=20, decimal_places=17, null=True)
 
     def __str__(self):
-        return "%i, %.3f, %.17f" % (self.integer, self.float, self.decimal_value)
+        return "%i, %s, %s" % (
+            self.integer,
+            "%.3f" % self.float if self.float is not None else None,
+            "%.17f" % self.decimal_value if self.decimal_value is not None else None,
+        )
 
 
 class Experiment(models.Model):
diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py
index 909e317dca..3538900092 100644
--- a/tests/expressions/tests.py
+++ b/tests/expressions/tests.py
@@ -1425,6 +1425,16 @@ class SimpleExpressionTests(SimpleTestCase):
             hash(Expression(TestModel._meta.get_field("other_field"))),
         )
 
+    def test_get_expression_for_validation_only_one_source_expression(self):
+        expression = Expression()
+        expression.constraint_validation_compatible = False
+        msg = (
+            "Expressions with constraint_validation_compatible set to False must have "
+            "only one source expression."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            expression.get_expression_for_validation()
+
 
 class ExpressionsNumericTests(TestCase):
     @classmethod
@@ -2654,6 +2664,29 @@ class CombinedExpressionTests(SimpleTestCase):
                 with self.assertRaisesMessage(FieldError, msg):
                     expr.output_field
 
+    def test_resolve_output_field_numbers_with_null(self):
+        test_values = [
+            (3.14159, None, FloatField),
+            (None, 3.14159, FloatField),
+            (None, 42, IntegerField),
+            (42, None, IntegerField),
+            (None, Decimal("3.14"), DecimalField),
+            (Decimal("3.14"), None, DecimalField),
+        ]
+        connectors = [
+            Combinable.ADD,
+            Combinable.SUB,
+            Combinable.MUL,
+            Combinable.DIV,
+            Combinable.MOD,
+            Combinable.POW,
+        ]
+        for lhs, rhs, expected_output_field in test_values:
+            for connector in connectors:
+                with self.subTest(lhs=lhs, connector=connector, rhs=rhs):
+                    expr = CombinedExpression(Value(lhs), connector, Value(rhs))
+                    self.assertIsInstance(expr.output_field, expected_output_field)
+
     def test_resolve_output_field_dates(self):
         tests = [
             # Add - same type.
diff --git a/tests/field_defaults/tests.py b/tests/field_defaults/tests.py
index c05d966bdb..6a5c75c36a 100644
--- a/tests/field_defaults/tests.py
+++ b/tests/field_defaults/tests.py
@@ -2,6 +2,7 @@ from datetime import datetime
 from decimal import Decimal
 from math import pi
 
+from django.core.exceptions import ValidationError
 from django.db import connection
 from django.db.models import Case, F, FloatField, Value, When
 from django.db.models.expressions import (
@@ -169,6 +170,23 @@ class DefaultTests(TestCase):
         years = DBDefaultsFunction.objects.values_list("year", flat=True)
         self.assertCountEqual(years, [2000, datetime.now().year])
 
+    def test_full_clean(self):
+        obj = DBArticle()
+        obj.full_clean()
+        obj.save()
+        obj.refresh_from_db()
+        self.assertEqual(obj.headline, "Default headline")
+
+        obj = DBArticle(headline="Other title")
+        obj.full_clean()
+        obj.save()
+        obj.refresh_from_db()
+        self.assertEqual(obj.headline, "Other title")
+
+        obj = DBArticle(headline="")
+        with self.assertRaises(ValidationError):
+            obj.full_clean()
+
 
 class AllowedDefaultTests(SimpleTestCase):
     def test_allowed(self):
diff --git a/tests/file_storage/test_generate_filename.py b/tests/file_storage/test_generate_filename.py
index 30d76c1a78..9631705fc8 100644
--- a/tests/file_storage/test_generate_filename.py
+++ b/tests/file_storage/test_generate_filename.py
@@ -37,11 +37,46 @@ class AWSS3Storage(Storage):
         return self.prefix + self.get_valid_name(filename)
 
 
+class StorageGenerateFilenameTests(SimpleTestCase):
+    """Tests for base Storage's generate_filename method."""
+
+    storage_class = Storage
+
+    def test_valid_names(self):
+        storage = self.storage_class()
+        name = "UnTRIVíAL @fil$ena#me!"
+        valid_name = storage.get_valid_name(name)
+        candidates = [
+            (name, valid_name),
+            (f"././././././{name}", valid_name),
+            (f"some/path/{name}", f"some/path/{valid_name}"),
+            (f"some/./path/./{name}", f"some/path/{valid_name}"),
+            (f"././some/././path/./{name}", f"some/path/{valid_name}"),
+            (f".\\.\\.\\.\\.\\.\\{name}", valid_name),
+            (f"some\\path\\{name}", f"some/path/{valid_name}"),
+            (f"some\\.\\path\\.\\{name}", f"some/path/{valid_name}"),
+            (f".\\.\\some\\.\\.\\path\\.\\{name}", f"some/path/{valid_name}"),
+        ]
+        for name, expected in candidates:
+            with self.subTest(name=name):
+                result = storage.generate_filename(name)
+                self.assertEqual(result, os.path.normpath(expected))
+
+
+class FileSystemStorageGenerateFilenameTests(StorageGenerateFilenameTests):
+
+    storage_class = FileSystemStorage
+
+
 class GenerateFilenameStorageTests(SimpleTestCase):
     def test_storage_dangerous_paths(self):
         candidates = [
             ("/tmp/..", ".."),
+            ("\\tmp\\..", ".."),
             ("/tmp/.", "."),
+            ("\\tmp\\.", "."),
+            ("..", ".."),
+            (".", "."),
             ("", ""),
         ]
         s = FileSystemStorage()
@@ -55,6 +90,8 @@ class GenerateFilenameStorageTests(SimpleTestCase):
 
     def test_storage_dangerous_paths_dir_name(self):
         candidates = [
+            ("../path", ".."),
+            ("..\\path", ".."),
             ("tmp/../path", "tmp/.."),
             ("tmp\\..\\path", "tmp/.."),
             ("/tmp/../path", "/tmp/.."),
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
index 637de0a3c9..fc3533ab7d 100644
--- a/tests/file_storage/tests.py
+++ b/tests/file_storage/tests.py
@@ -25,11 +25,18 @@ from django.core.files.uploadedfile import (
 )
 from django.db.models import FileField
 from django.db.models.fields.files import FileDescriptor
-from django.test import LiveServerTestCase, SimpleTestCase, TestCase, override_settings
+from django.test import (
+    LiveServerTestCase,
+    SimpleTestCase,
+    TestCase,
+    ignore_warnings,
+    override_settings,
+)
 from django.test.utils import requires_tz_support
 from django.urls import NoReverseMatch, reverse_lazy
 from django.utils import timezone
 from django.utils._os import symlinks_supported
+from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import (
     Storage,
@@ -88,18 +95,18 @@ class FileStorageTests(SimpleTestCase):
         """
         Standard file access options are available, and work as expected.
         """
-        self.assertFalse(self.storage.exists("storage_test"))
+        self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "storage_test")))
         f = self.storage.open("storage_test", "w")
         f.write("storage contents")
         f.close()
-        self.assertTrue(self.storage.exists("storage_test"))
+        self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "storage_test")))
 
         f = self.storage.open("storage_test", "r")
         self.assertEqual(f.read(), "storage contents")
         f.close()
 
         self.storage.delete("storage_test")
-        self.assertFalse(self.storage.exists("storage_test"))
+        self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "storage_test")))
 
     def _test_file_time_getter(self, getter):
         # Check for correct behavior under both USE_TZ=True and USE_TZ=False.
@@ -268,10 +275,10 @@ class FileStorageTests(SimpleTestCase):
         """
         Saving a pathname should create intermediate directories as necessary.
         """
-        self.assertFalse(self.storage.exists("path/to"))
+        self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "path/to")))
         self.storage.save("path/to/test.file", ContentFile("file saved with path"))
 
-        self.assertTrue(self.storage.exists("path/to"))
+        self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "path/to")))
         with self.storage.open("path/to/test.file") as f:
             self.assertEqual(f.read(), b"file saved with path")
 
@@ -607,6 +614,7 @@ class CustomStorageTests(FileStorageTests):
         self.storage.delete(second)
 
 
+# RemovedInDjango60Warning: Remove this class.
 class OverwritingStorage(FileSystemStorage):
     """
     Overwrite existing files instead of appending a suffix to generate an
@@ -621,7 +629,26 @@ class OverwritingStorage(FileSystemStorage):
         return name
 
 
-class OverwritingStorageTests(FileStorageTests):
+# RemovedInDjango60Warning: Remove this test class.
+class OverwritingStorageOSOpenFlagsWarningTests(SimpleTestCase):
+    storage_class = OverwritingStorage
+
+    def setUp(self):
+        self.temp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.temp_dir)
+
+    def test_os_open_flags_deprecation_warning(self):
+        msg = "Overriding OS_OPEN_FLAGS is deprecated. Use the allow_overwrite "
+        msg += "parameter instead."
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            self.storage = self.storage_class(
+                location=self.temp_dir, base_url="/test_media_url/"
+            )
+
+
+# RemovedInDjango60Warning: Remove this test class.
+@ignore_warnings(category=RemovedInDjango60Warning)
+class OverwritingStorageOSOpenFlagsTests(FileStorageTests):
     storage_class = OverwritingStorage
 
     def test_save_overwrite_behavior(self):
@@ -649,6 +676,65 @@ class OverwritingStorageTests(FileStorageTests):
             self.storage.delete(name)
 
 
+class OverwritingStorageTests(FileStorageTests):
+    storage_class = FileSystemStorage
+
+    def setUp(self):
+        self.temp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.temp_dir)
+        self.storage = self.storage_class(
+            location=self.temp_dir, base_url="/test_media_url/", allow_overwrite=True
+        )
+
+    def test_save_overwrite_behavior(self):
+        """Saving to same file name twice overwrites the first file."""
+        name = "test.file"
+        self.assertFalse(self.storage.exists(name))
+        content_1 = b"content one"
+        content_2 = b"second content"
+        f_1 = ContentFile(content_1)
+        f_2 = ContentFile(content_2)
+        stored_name_1 = self.storage.save(name, f_1)
+        try:
+            self.assertEqual(stored_name_1, name)
+            self.assertTrue(os.path.exists(os.path.join(self.temp_dir, name)))
+            with self.storage.open(name) as fp:
+                self.assertEqual(fp.read(), content_1)
+            stored_name_2 = self.storage.save(name, f_2)
+            self.assertEqual(stored_name_2, name)
+            self.assertTrue(os.path.exists(os.path.join(self.temp_dir, name)))
+            with self.storage.open(name) as fp:
+                self.assertEqual(fp.read(), content_2)
+        finally:
+            self.storage.delete(name)
+
+    def test_save_overwrite_behavior_temp_file(self):
+        """Saving to same file name twice overwrites the first file."""
+        name = "test.file"
+        self.assertFalse(self.storage.exists(name))
+        content_1 = b"content one"
+        content_2 = b"second content"
+        f_1 = TemporaryUploadedFile("tmp1", "text/plain", 11, "utf8")
+        f_1.write(content_1)
+        f_1.seek(0)
+        f_2 = TemporaryUploadedFile("tmp2", "text/plain", 14, "utf8")
+        f_2.write(content_2)
+        f_2.seek(0)
+        stored_name_1 = self.storage.save(name, f_1)
+        try:
+            self.assertEqual(stored_name_1, name)
+            self.assertTrue(os.path.exists(os.path.join(self.temp_dir, name)))
+            with self.storage.open(name) as fp:
+                self.assertEqual(fp.read(), content_1)
+            stored_name_2 = self.storage.save(name, f_2)
+            self.assertEqual(stored_name_2, name)
+            self.assertTrue(os.path.exists(os.path.join(self.temp_dir, name)))
+            with self.storage.open(name) as fp:
+                self.assertEqual(fp.read(), content_2)
+        finally:
+            self.storage.delete(name)
+
+
 class DiscardingFalseContentStorage(FileSystemStorage):
     def _save(self, name, content):
         if content:
@@ -769,18 +855,24 @@ class FileFieldStorageTests(TestCase):
 
     def test_duplicate_filename(self):
         # Multiple files with the same name get _(7 random chars) appended to them.
-        objs = [Storage() for i in range(2)]
-        for o in objs:
-            o.normal.save("multiple_files.txt", ContentFile("Same Content"))
-        try:
-            names = [o.normal.name for o in objs]
-            self.assertEqual(names[0], "tests/multiple_files.txt")
-            self.assertRegex(
-                names[1], "tests/multiple_files_%s.txt" % FILE_SUFFIX_REGEX
-            )
-        finally:
-            for o in objs:
-                o.delete()
+        tests = [
+            ("multiple_files", "txt"),
+            ("multiple_files_many_extensions", "tar.gz"),
+        ]
+        for filename, extension in tests:
+            with self.subTest(filename=filename):
+                objs = [Storage() for i in range(2)]
+                for o in objs:
+                    o.normal.save(f"{filename}.{extension}", ContentFile("Content"))
+                try:
+                    names = [o.normal.name for o in objs]
+                    self.assertEqual(names[0], f"tests/{filename}.{extension}")
+                    self.assertRegex(
+                        names[1], f"tests/{filename}_{FILE_SUFFIX_REGEX}.{extension}"
+                    )
+                finally:
+                    for o in objs:
+                        o.delete()
 
     def test_file_truncation(self):
         # Given the max_length is limited, when multiple files get uploaded
diff --git a/tests/files/tests.py b/tests/files/tests.py
index 9d3a471cb3..4f6d1fa74b 100644
--- a/tests/files/tests.py
+++ b/tests/files/tests.py
@@ -426,9 +426,10 @@ class FileMoveSafeTests(unittest.TestCase):
         handle_a, self.file_a = tempfile.mkstemp()
         handle_b, self.file_b = tempfile.mkstemp()
 
-        # file_move_safe() raises OSError if the destination file exists and
-        # allow_overwrite is False.
-        with self.assertRaises(FileExistsError):
+        # file_move_safe() raises FileExistsError if the destination file
+        # exists and allow_overwrite is False.
+        msg = r"Destination file .* exists and allow_overwrite is False\."
+        with self.assertRaisesRegex(FileExistsError, msg):
             file_move_safe(self.file_a, self.file_b, allow_overwrite=False)
 
         # should allow it and continue on if allow_overwrite is True
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index a86d443e33..3982cc93fe 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -2136,6 +2136,15 @@ class FormsTestCase(SimpleTestCase):
             p.as_p(), '<input type="hidden" name="foo"><input type="hidden" name="bar">'
         )
 
+    def test_hidden_widget_does_not_have_aria_describedby(self):
+        class TestForm(Form):
+            hidden_text = CharField(widget=HiddenInput, help_text="Help Text")
+
+        f = TestForm()
+        self.assertEqual(
+            str(f), '<input type="hidden" name="hidden_text" id="id_hidden_text">'
+        )
+
     def test_field_order(self):
         # A Form's fields are displayed in the same order in which they were defined.
         class TestForm(Form):
diff --git a/tests/forms_tests/widget_tests/test_clearablefileinput.py b/tests/forms_tests/widget_tests/test_clearablefileinput.py
index 53b84cc0ec..ae54cc4b5d 100644
--- a/tests/forms_tests/widget_tests/test_clearablefileinput.py
+++ b/tests/forms_tests/widget_tests/test_clearablefileinput.py
@@ -111,6 +111,21 @@ class ClearableFileInputTest(WidgetTest):
             ),
         )
 
+    def test_render_checked(self):
+        self.widget.checked = True
+        self.check_html(
+            self.widget,
+            "myfile",
+            FakeFieldFile(),
+            html=(
+                'Currently: <a href="something">something</a>'
+                '<input type="checkbox" name="myfile-clear" id="myfile-clear_id" '
+                "checked>"
+                '<label for="myfile-clear_id">Clear</label><br>Change: '
+                '<input type="file" name="myfile" checked>'
+            ),
+        )
+
     def test_render_no_disabled(self):
         class TestForm(Form):
             clearable_file = FileField(
diff --git a/tests/generic_views/urls.py b/tests/generic_views/urls.py
index 2d5301d15e..277b2c4c1b 100644
--- a/tests/generic_views/urls.py
+++ b/tests/generic_views/urls.py
@@ -27,7 +27,7 @@ urlpatterns = [
     ),
     path(
         "template/cached/<foo>/",
-        cache_page(2.0)(TemplateView.as_view(template_name="generic_views/about.html")),
+        cache_page(2)(TemplateView.as_view(template_name="generic_views/about.html")),
     ),
     path(
         "template/extra_context/",
diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py
index 0b56d6b1a2..5128335f56 100644
--- a/tests/get_or_create/tests.py
+++ b/tests/get_or_create/tests.py
@@ -227,19 +227,6 @@ class GetOrCreateTestsWithManualPKs(TestCase):
             ManualPrimaryKeyTest.objects.get_or_create(id=1, data="Different")
         self.assertEqual(ManualPrimaryKeyTest.objects.get(id=1).data, "Original")
 
-    def test_get_or_create_raises_IntegrityError_plus_traceback(self):
-        """
-        get_or_create should raise IntegrityErrors with the full traceback.
-        This is tested by checking that a known method call is in the traceback.
-        We cannot use assertRaises here because we need to inspect
-        the actual traceback. Refs #16340.
-        """
-        try:
-            ManualPrimaryKeyTest.objects.get_or_create(id=1, data="Different")
-        except IntegrityError:
-            formatted_traceback = traceback.format_exc()
-            self.assertIn("obj.save", formatted_traceback)
-
     def test_savepoint_rollback(self):
         """
         The database connection is still usable after a DatabaseError in
diff --git a/tests/gis_tests/gdal_tests/test_geom.py b/tests/gis_tests/gdal_tests/test_geom.py
index 35b11b753a..3967b945a4 100644
--- a/tests/gis_tests/gdal_tests/test_geom.py
+++ b/tests/gis_tests/gdal_tests/test_geom.py
@@ -675,10 +675,10 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin):
             ("Point M", 2001, True),
             ("LineString M", 2002, True),
             ("Polygon M", 2003, True),
-            ("MultiPoint M", 2004, False),
-            ("MultiLineString M", 2005, False),
-            ("MultiPolygon M", 2006, False),
-            ("GeometryCollection M", 2007, False),
+            ("MultiPoint M", 2004, True),
+            ("MultiLineString M", 2005, True),
+            ("MultiPolygon M", 2006, True),
+            ("GeometryCollection M", 2007, True),
             ("CircularString M", 2008, False),
             ("CompoundCurve M", 2009, False),
             ("CurvePolygon M", 2010, False),
@@ -690,10 +690,10 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin):
             ("Point ZM", 3001, True),
             ("LineString ZM", 3002, True),
             ("Polygon ZM", 3003, True),
-            ("MultiPoint ZM", 3004, False),
-            ("MultiLineString ZM", 3005, False),
-            ("MultiPolygon ZM", 3006, False),
-            ("GeometryCollection ZM", 3007, False),
+            ("MultiPoint ZM", 3004, True),
+            ("MultiLineString ZM", 3005, True),
+            ("MultiPolygon ZM", 3006, True),
+            ("GeometryCollection ZM", 3007, True),
             ("CircularString ZM", 3008, False),
             ("CompoundCurve ZM", 3009, False),
             ("CurvePolygon ZM", 3010, False),
@@ -943,6 +943,30 @@ class OGRGeomTest(SimpleTestCase, TestDataMixin):
             geom.shell.wkt, "LINEARRING (0 0 0,10 0 0,10 10 0,0 10 0,0 0 0)"
         )
 
+    def test_multi_geometries_m_dimension(self):
+        tests = [
+            "MULTIPOINT M ((10 40 10), (40 30 10), (20 20 10))",
+            "MULTIPOINT ZM ((10 40 0 10), (40 30 1 10), (20 20 1 10))",
+            "MULTILINESTRING M ((10 10 1, 20 20 2),(40 40 1, 30 30 2))",
+            "MULTILINESTRING ZM ((10 10 0 1, 20 20 0 2),(40 40 1, 30 30 0 2))",
+            (
+                "MULTIPOLYGON ZM (((30 20 1 0, 45 40 1 0, 30 20 1 0)),"
+                "((15 5 0 0, 40 10 0 0, 15 5 0 0)))"
+            ),
+            (
+                "GEOMETRYCOLLECTION M (POINT M (40 10 0),"
+                "LINESTRING M (10 10 0, 20 20 0, 10 40 0))"
+            ),
+            (
+                "GEOMETRYCOLLECTION ZM (POINT ZM (40 10 0 1),"
+                "LINESTRING ZM (10 10 1 0, 20 20 1 0, 10 40 1 0))"
+            ),
+        ]
+        for geom_input in tests:
+            with self.subTest(geom_input=geom_input):
+                geom = OGRGeometry(geom_input)
+                self.assertIs(geom.is_measured, True)
+
 
 class DeprecationTests(SimpleTestCase):
     def test_coord_setter_deprecation(self):
diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py
index 9edadc48c2..80b08f8d39 100644
--- a/tests/gis_tests/geoapp/test_functions.py
+++ b/tests/gis_tests/geoapp/test_functions.py
@@ -259,36 +259,31 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
             return (4 * num_seg) + 1
 
         if connection.ops.postgis:
-            expected_areas = (169, 136)
+            expected_area = 169
         elif connection.ops.spatialite:
-            expected_areas = (168, 135)
+            expected_area = 168
         else:  # Oracle.
-            expected_areas = (171, 126)
-        qs = Country.objects.annotate(
+            expected_area = 171
+        country = Country.objects.annotate(
             circle=functions.BoundingCircle("mpoly")
-        ).order_by("name")
-        self.assertAlmostEqual(qs[0].circle.area, expected_areas[0], 0)
-        self.assertAlmostEqual(qs[1].circle.area, expected_areas[1], 0)
+        ).order_by("name")[0]
+        self.assertAlmostEqual(country.circle.area, expected_area, 0)
         if connection.ops.postgis:
             # By default num_seg=48.
-            self.assertEqual(qs[0].circle.num_points, circle_num_points(48))
-            self.assertEqual(qs[1].circle.num_points, circle_num_points(48))
+            self.assertEqual(country.circle.num_points, circle_num_points(48))
 
         tests = [12, Value(12, output_field=IntegerField())]
         for num_seq in tests:
             with self.subTest(num_seq=num_seq):
-                qs = Country.objects.annotate(
+                country = Country.objects.annotate(
                     circle=functions.BoundingCircle("mpoly", num_seg=num_seq),
-                ).order_by("name")
+                ).order_by("name")[0]
                 if connection.ops.postgis:
-                    self.assertGreater(qs[0].circle.area, 168.4, 0)
-                    self.assertLess(qs[0].circle.area, 169.5, 0)
-                    self.assertAlmostEqual(qs[1].circle.area, 136, 0)
-                    self.assertEqual(qs[0].circle.num_points, circle_num_points(12))
-                    self.assertEqual(qs[1].circle.num_points, circle_num_points(12))
+                    self.assertGreater(country.circle.area, 168.4, 0)
+                    self.assertLess(country.circle.area, 169.5, 0)
+                    self.assertEqual(country.circle.num_points, circle_num_points(12))
                 else:
-                    self.assertAlmostEqual(qs[0].circle.area, expected_areas[0], 0)
-                    self.assertAlmostEqual(qs[1].circle.area, expected_areas[1], 0)
+                    self.assertAlmostEqual(country.circle.area, expected_area, 0)
 
     @skipUnlessDBFeature("has_Centroid_function")
     def test_centroid(self):
@@ -353,20 +348,24 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
     @skipUnlessDBFeature("has_FromWKB_function")
     def test_fromwkb(self):
         g = Point(56.811078, 60.608647)
-        g2 = City.objects.values_list(
+        pt1, pt2 = City.objects.values_list(
             functions.FromWKB(Value(g.wkb.tobytes())),
-            flat=True,
+            functions.FromWKB(Value(g.wkb.tobytes()), srid=4326),
         )[0]
-        self.assertIs(g.equals_exact(g2, 0.00001), True)
+        self.assertIs(g.equals_exact(pt1, 0.00001), True)
+        self.assertIsNone(pt1.srid)
+        self.assertEqual(pt2.srid, 4326)
 
     @skipUnlessDBFeature("has_FromWKT_function")
     def test_fromwkt(self):
         g = Point(56.811078, 60.608647)
-        g2 = City.objects.values_list(
+        pt1, pt2 = City.objects.values_list(
             functions.FromWKT(Value(g.wkt)),
-            flat=True,
+            functions.FromWKT(Value(g.wkt), srid=4326),
         )[0]
-        self.assertIs(g.equals_exact(g2, 0.00001), True)
+        self.assertIs(g.equals_exact(pt1, 0.00001), True)
+        self.assertIsNone(pt1.srid)
+        self.assertEqual(pt2.srid, 4326)
 
     @skipUnlessDBFeature("has_GeoHash_function")
     def test_geohash(self):
diff --git a/tests/gis_tests/gis_migrations/test_operations.py b/tests/gis_tests/gis_migrations/test_operations.py
index d2ad67945b..3ecde2025e 100644
--- a/tests/gis_tests/gis_migrations/test_operations.py
+++ b/tests/gis_tests/gis_migrations/test_operations.py
@@ -97,13 +97,12 @@ class OperationTestCase(TransactionTestCase):
         migration_class,
         model_name,
         field_name,
-        blank=False,
         field_class=None,
         field_class_kwargs=None,
     ):
         args = [model_name, field_name]
         if field_class:
-            field_class_kwargs = field_class_kwargs or {"srid": 4326, "blank": blank}
+            field_class_kwargs = field_class_kwargs or {}
             args.append(field_class(**field_class_kwargs))
         operation = migration_class(*args)
         old_state = self.current_state.clone()
@@ -122,7 +121,7 @@ class OperationTests(OperationTestCase):
         Test the AddField operation with a geometry-enabled column.
         """
         self.alter_gis_model(
-            migrations.AddField, "Neighborhood", "path", False, fields.LineStringField
+            migrations.AddField, "Neighborhood", "path", fields.LineStringField
         )
         self.assertColumnExists("gis_neighborhood", "path")
 
@@ -134,6 +133,24 @@ class OperationTests(OperationTestCase):
         if self.has_spatial_indexes:
             self.assertSpatialIndexExists("gis_neighborhood", "path")
 
+    @skipUnless(connection.vendor == "mysql", "MySQL specific test")
+    def test_remove_geom_field_nullable_with_index(self):
+        # MySQL doesn't support spatial indexes on NULL columns.
+        with self.assertNumQueries(1) as ctx:
+            self.alter_gis_model(
+                migrations.AddField,
+                "Neighborhood",
+                "path",
+                fields.LineStringField,
+                field_class_kwargs={"null": True},
+            )
+        self.assertColumnExists("gis_neighborhood", "path")
+        self.assertNotIn("CREATE SPATIAL INDEX", ctx.captured_queries[0]["sql"])
+
+        with self.assertNumQueries(1), self.assertNoLogs("django.contrib.gis", "ERROR"):
+            self.alter_gis_model(migrations.RemoveField, "Neighborhood", "path")
+        self.assertColumnNotExists("gis_neighborhood", "path")
+
     @skipUnless(HAS_GEOMETRY_COLUMNS, "Backend doesn't support GeometryColumns.")
     def test_geom_col_name(self):
         self.assertEqual(
@@ -147,7 +164,7 @@ class OperationTests(OperationTestCase):
         Test the AddField operation with a raster-enabled column.
         """
         self.alter_gis_model(
-            migrations.AddField, "Neighborhood", "heatmap", False, fields.RasterField
+            migrations.AddField, "Neighborhood", "heatmap", fields.RasterField
         )
         self.assertColumnExists("gis_neighborhood", "heatmap")
 
@@ -160,7 +177,11 @@ class OperationTests(OperationTestCase):
         Should be able to add a GeometryField with blank=True.
         """
         self.alter_gis_model(
-            migrations.AddField, "Neighborhood", "path", True, fields.LineStringField
+            migrations.AddField,
+            "Neighborhood",
+            "path",
+            fields.LineStringField,
+            field_class_kwargs={"blank": True},
         )
         self.assertColumnExists("gis_neighborhood", "path")
 
@@ -178,7 +199,11 @@ class OperationTests(OperationTestCase):
         Should be able to add a RasterField with blank=True.
         """
         self.alter_gis_model(
-            migrations.AddField, "Neighborhood", "heatmap", True, fields.RasterField
+            migrations.AddField,
+            "Neighborhood",
+            "heatmap",
+            fields.RasterField,
+            field_class_kwargs={"blank": True},
         )
         self.assertColumnExists("gis_neighborhood", "heatmap")
 
@@ -247,9 +272,8 @@ class OperationTests(OperationTestCase):
             migrations.AlterField,
             "Neighborhood",
             "geom",
-            False,
             fields.MultiPolygonField,
-            field_class_kwargs={"srid": 4326, "dim": 3},
+            field_class_kwargs={"dim": 3},
         )
         self.assertTrue(Neighborhood.objects.first().geom.hasz)
         # Rewind to 2 dimensions.
@@ -257,9 +281,8 @@ class OperationTests(OperationTestCase):
             migrations.AlterField,
             "Neighborhood",
             "geom",
-            False,
             fields.MultiPolygonField,
-            field_class_kwargs={"srid": 4326, "dim": 2},
+            field_class_kwargs={"dim": 2},
         )
         self.assertFalse(Neighborhood.objects.first().geom.hasz)
 
@@ -270,7 +293,7 @@ class OperationTests(OperationTestCase):
         Neighborhood = self.current_state.apps.get_model("gis", "Neighborhood")
         poly = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)))
         constraint = models.CheckConstraint(
-            check=models.Q(geom=poly),
+            condition=models.Q(geom=poly),
             name="geom_within_constraint",
         )
         Neighborhood._meta.constraints = [constraint]
@@ -296,9 +319,5 @@ class NoRasterSupportTests(OperationTestCase):
         with self.assertRaisesMessage(ImproperlyConfigured, msg):
             self.set_up_test_model()
             self.alter_gis_model(
-                migrations.AddField,
-                "Neighborhood",
-                "heatmap",
-                False,
-                fields.RasterField,
+                migrations.AddField, "Neighborhood", "heatmap", fields.RasterField
             )
diff --git a/tests/gis_tests/layermap/tests.py b/tests/gis_tests/layermap/tests.py
index 283149eafc..6cc903f3ad 100644
--- a/tests/gis_tests/layermap/tests.py
+++ b/tests/gis_tests/layermap/tests.py
@@ -218,7 +218,7 @@ class LayerMapTest(TestCase):
         #  `unique='name'`: Creates models on the condition that they have
         #    unique county names; geometries from each feature however will be
         #    appended to the geometry collection of the unique model.  Thus,
-        #    all of the various islands in Honolulu county will be in in one
+        #    all of the various islands in Honolulu county will be in one
         #    database record with a MULTIPOLYGON type.
         lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique="name")
         lm.save(silent=True, strict=True)
diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py
index 7a5a287da7..ffa362abdd 100644
--- a/tests/handlers/tests.py
+++ b/tests/handlers/tests.py
@@ -326,11 +326,22 @@ class AsyncHandlerRequestTests(SimpleTestCase):
         with self.assertRaisesMessage(ValueError, msg):
             await self.async_client.get("/unawaited/")
 
-    @override_settings(FORCE_SCRIPT_NAME="/FORCED_PREFIX/")
+    def test_root_path(self):
+        async_request_factory = AsyncRequestFactory()
+        request = async_request_factory.request(
+            **{"path": "/root/somepath/", "root_path": "/root"}
+        )
+        self.assertEqual(request.path, "/root/somepath/")
+        self.assertEqual(request.script_name, "/root")
+        self.assertEqual(request.path_info, "/somepath/")
+
+    @override_settings(FORCE_SCRIPT_NAME="/FORCED_PREFIX")
     def test_force_script_name(self):
         async_request_factory = AsyncRequestFactory()
-        request = async_request_factory.request(**{"path": "/somepath/"})
+        request = async_request_factory.request(**{"path": "/FORCED_PREFIX/somepath/"})
         self.assertEqual(request.path, "/FORCED_PREFIX/somepath/")
+        self.assertEqual(request.script_name, "/FORCED_PREFIX")
+        self.assertEqual(request.path_info, "/somepath/")
 
     async def test_sync_streaming(self):
         response = await self.async_client.get("/streaming/")
diff --git a/tests/humanize_tests/tests.py b/tests/humanize_tests/tests.py
index cf29f58232..ab967e2874 100644
--- a/tests/humanize_tests/tests.py
+++ b/tests/humanize_tests/tests.py
@@ -55,6 +55,9 @@ class HumanizeTests(SimpleTestCase):
             "102",
             "103",
             "111",
+            "-0",
+            "-1",
+            "-105",
             "something else",
             None,
         )
@@ -70,6 +73,9 @@ class HumanizeTests(SimpleTestCase):
             "102nd",
             "103rd",
             "111th",
+            "0th",
+            "-1",
+            "-105",
             "something else",
             None,
         )
@@ -116,39 +122,83 @@ class HumanizeTests(SimpleTestCase):
     def test_intcomma(self):
         test_list = (
             100,
+            -100,
             1000,
+            -1000,
             10123,
+            -10123,
             10311,
+            -10311,
             1000000,
+            -1000000,
             1234567.25,
+            -1234567.25,
             "100",
+            "-100",
+            "100.1",
+            "-100.1",
+            "100.13",
+            "-100.13",
             "1000",
+            "-1000",
             "10123",
+            "-10123",
             "10311",
+            "-10311",
+            "100000.13",
+            "-100000.13",
             "1000000",
+            "-1000000",
             "1234567.1234567",
+            "-1234567.1234567",
             Decimal("1234567.1234567"),
+            Decimal("-1234567.1234567"),
             None,
             "1234567",
+            "-1234567",
             "1234567.12",
+            "-1234567.12",
+            "the quick brown fox jumped over the lazy dog",
         )
         result_list = (
             "100",
+            "-100",
             "1,000",
+            "-1,000",
             "10,123",
+            "-10,123",
             "10,311",
+            "-10,311",
             "1,000,000",
+            "-1,000,000",
             "1,234,567.25",
+            "-1,234,567.25",
             "100",
+            "-100",
+            "100.1",
+            "-100.1",
+            "100.13",
+            "-100.13",
             "1,000",
+            "-1,000",
             "10,123",
+            "-10,123",
             "10,311",
+            "-10,311",
+            "100,000.13",
+            "-100,000.13",
             "1,000,000",
+            "-1,000,000",
             "1,234,567.1234567",
+            "-1,234,567.1234567",
             "1,234,567.1234567",
+            "-1,234,567.1234567",
             None,
             "1,234,567",
+            "-1,234,567",
             "1,234,567.12",
+            "-1,234,567.12",
+            "the quick brown fox jumped over the lazy dog",
         )
         with translation.override("en"):
             self.humanize_tester(test_list, result_list, "intcomma")
@@ -156,39 +206,71 @@ class HumanizeTests(SimpleTestCase):
     def test_l10n_intcomma(self):
         test_list = (
             100,
+            -100,
             1000,
+            -1000,
             10123,
+            -10123,
             10311,
+            -10311,
             1000000,
+            -1000000,
             1234567.25,
+            -1234567.25,
             "100",
+            "-100",
             "1000",
+            "-1000",
             "10123",
+            "-10123",
             "10311",
+            "-10311",
             "1000000",
+            "-1000000",
             "1234567.1234567",
+            "-1234567.1234567",
             Decimal("1234567.1234567"),
+            -Decimal("1234567.1234567"),
             None,
             "1234567",
+            "-1234567",
             "1234567.12",
+            "-1234567.12",
+            "the quick brown fox jumped over the lazy dog",
         )
         result_list = (
             "100",
+            "-100",
             "1,000",
+            "-1,000",
             "10,123",
+            "-10,123",
             "10,311",
+            "-10,311",
             "1,000,000",
+            "-1,000,000",
             "1,234,567.25",
+            "-1,234,567.25",
             "100",
+            "-100",
             "1,000",
+            "-1,000",
             "10,123",
+            "-10,123",
             "10,311",
+            "-10,311",
             "1,000,000",
+            "-1,000,000",
             "1,234,567.1234567",
+            "-1,234,567.1234567",
             "1,234,567.1234567",
+            "-1,234,567.1234567",
             None,
             "1,234,567",
+            "-1,234,567",
             "1,234,567.12",
+            "-1,234,567.12",
+            "the quick brown fox jumped over the lazy dog",
         )
         with self.settings(USE_THOUSAND_SEPARATOR=False):
             with translation.override("en"):
diff --git a/tests/i18n/patterns/locale/en/LC_MESSAGES/django.mo b/tests/i18n/patterns/locale/en/LC_MESSAGES/django.mo
index ec7644b504..0cd9391528 100644
Binary files a/tests/i18n/patterns/locale/en/LC_MESSAGES/django.mo and b/tests/i18n/patterns/locale/en/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po b/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po
index 9a14a80ceb..670a0ab1ed 100644
--- a/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po
+++ b/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po
@@ -7,31 +7,47 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2011-06-15 11:33+0200\n"
+"POT-Creation-Date: 2024-03-01 21:18+0000\n"
 "PO-Revision-Date: 2011-06-14 16:16+0100\n"
 "Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Language: \n"
 
 #: urls/default.py:11
-msgid "^translated/$"
-msgstr "^translated/$"
+msgid "translated/"
+msgstr "translated/"
 
 #: urls/default.py:12
+msgid "^translated-regex/$"
+msgstr "^translated-regex/$"
+
+#: urls/default.py:14
 msgid "^translated/(?P<slug>[\\w-]+)/$"
 msgstr "^translated/(?P<slug>[\\w-]+)/$"
 
-#: urls/default.py:17
+#: urls/default.py:25
+msgid "^with-arguments/(?P<argument>[\\w-]+)/(?:(?P<optional>[\\w-]+).html)?$"
+msgstr ""
+
+#: urls/default.py:29
 msgid "^users/$"
 msgstr "^users/$"
 
-#: urls/default.py:18 urls/wrong.py:7
+#: urls/default.py:31 urls/wrong.py:7
 msgid "^account/"
 msgstr "^account/"
 
 #: urls/namespace.py:9 urls/wrong_namespace.py:10
 msgid "^register/$"
 msgstr "^register/$"
+
+#: urls/namespace.py:10
+msgid "^register-without-slash$"
+msgstr ""
+
+#: urls/namespace.py:11
+msgid "register-as-path/"
+msgstr "register-as-path/"
diff --git a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
index 5eac50466c..544bfdbfc6 100644
Binary files a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo and b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
index a938e3371d..aa58c506d5 100644
--- a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
+++ b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
@@ -7,29 +7,37 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2011-06-15 11:33+0200\n"
+"POT-Creation-Date: 2024-03-01 21:18+0000\n"
 "PO-Revision-Date: 2011-06-14 16:16+0100\n"
 "Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Language: \n"
 "Plural-Forms: nplurals=2; plural=(n != 1)\n"
 
 #: urls/default.py:11
-msgid "^translated/$"
-msgstr "^vertaald/$"
+msgid "translated/"
+msgstr "vertaald/"
 
 #: urls/default.py:12
+msgid "^translated-regex/$"
+msgstr "^vertaald-regex/$"
+
+#: urls/default.py:14
 msgid "^translated/(?P<slug>[\\w-]+)/$"
 msgstr "^vertaald/(?P<slug>[\\w-]+)/$"
 
-#: urls/default.py:17
+#: urls/default.py:25
+msgid "^with-arguments/(?P<argument>[\\w-]+)/(?:(?P<optional>[\\w-]+).html)?$"
+msgstr ""
+
+#: urls/default.py:29
 msgid "^users/$"
 msgstr "^gebruikers/$"
 
-#: urls/default.py:18 urls/wrong.py:7
+#: urls/default.py:31 urls/wrong.py:7
 msgid "^account/"
 msgstr "^profiel/"
 
@@ -37,6 +45,10 @@ msgstr "^profiel/"
 msgid "^register/$"
 msgstr "^registreren/$"
 
-#: urls/namespace.py:12
+#: urls/namespace.py:10
+msgid "^register-without-slash$"
+msgstr ""
+
+#: urls/namespace.py:11
 msgid "register-as-path/"
 msgstr "registreren-als-pad/"
diff --git a/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo b/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo
index 1d7b346c27..bd28900eb1 100644
Binary files a/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo and b/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po b/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po
index fd3388e4b0..25300cf6f9 100644
--- a/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po
+++ b/tests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po
@@ -7,32 +7,48 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2011-06-15 11:34+0200\n"
+"POT-Creation-Date: 2024-03-01 21:18+0000\n"
 "PO-Revision-Date: 2011-06-14 16:17+0100\n"
 "Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Language: \n"
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
 
 #: urls/default.py:11
-msgid "^translated/$"
-msgstr "^traduzidos/$"
+msgid "translated/"
+msgstr "traduzidos/"
 
 #: urls/default.py:12
+msgid "^translated-regex/$"
+msgstr "^traduzidos-regex/$"
+
+#: urls/default.py:14
 msgid "^translated/(?P<slug>[\\w-]+)/$"
 msgstr "^traduzidos/(?P<slug>[\\w-]+)/$"
 
-#: urls/default.py:17
+#: urls/default.py:25
+msgid "^with-arguments/(?P<argument>[\\w-]+)/(?:(?P<optional>[\\w-]+).html)?$"
+msgstr ""
+
+#: urls/default.py:29
 msgid "^users/$"
 msgstr "^usuarios/$"
 
-#: urls/default.py:18 urls/wrong.py:7
+#: urls/default.py:31 urls/wrong.py:7
 msgid "^account/"
 msgstr "^conta/"
 
 #: urls/namespace.py:9 urls/wrong_namespace.py:10
 msgid "^register/$"
 msgstr "^registre-se/$"
+
+#: urls/namespace.py:10
+msgid "^register-without-slash$"
+msgstr ""
+
+#: urls/namespace.py:11
+msgid "register-as-path/"
+msgstr "registre-se-caminho/"
diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py
index e2fee904b1..bd329e69f8 100644
--- a/tests/i18n/patterns/tests.py
+++ b/tests/i18n/patterns/tests.py
@@ -134,6 +134,9 @@ class URLTranslationTests(URLTestCaseBase):
     def test_no_prefix_translated(self):
         with translation.override("en"):
             self.assertEqual(reverse("no-prefix-translated"), "/translated/")
+            self.assertEqual(
+                reverse("no-prefix-translated-regex"), "/translated-regex/"
+            )
             self.assertEqual(
                 reverse("no-prefix-translated-slug", kwargs={"slug": "yeah"}),
                 "/translated/yeah/",
@@ -141,6 +144,7 @@ class URLTranslationTests(URLTestCaseBase):
 
         with translation.override("nl"):
             self.assertEqual(reverse("no-prefix-translated"), "/vertaald/")
+            self.assertEqual(reverse("no-prefix-translated-regex"), "/vertaald-regex/")
             self.assertEqual(
                 reverse("no-prefix-translated-slug", kwargs={"slug": "yeah"}),
                 "/vertaald/yeah/",
@@ -148,6 +152,9 @@ class URLTranslationTests(URLTestCaseBase):
 
         with translation.override("pt-br"):
             self.assertEqual(reverse("no-prefix-translated"), "/traduzidos/")
+            self.assertEqual(
+                reverse("no-prefix-translated-regex"), "/traduzidos-regex/"
+            )
             self.assertEqual(
                 reverse("no-prefix-translated-slug", kwargs={"slug": "yeah"}),
                 "/traduzidos/yeah/",
@@ -180,7 +187,7 @@ class URLTranslationTests(URLTestCaseBase):
                 "/nl/profiel/registreren-als-pad/",
             )
             self.assertEqual(translation.get_language(), "en")
-            # URL with parameters.
+            # re_path() URL with parameters.
             self.assertEqual(
                 translate_url("/en/with-arguments/regular-argument/", "nl"),
                 "/nl/with-arguments/regular-argument/",
@@ -191,6 +198,11 @@ class URLTranslationTests(URLTestCaseBase):
                 ),
                 "/nl/with-arguments/regular-argument/optional.html",
             )
+            # path() URL with parameter.
+            self.assertEqual(
+                translate_url("/en/path-with-arguments/regular-argument/", "nl"),
+                "/nl/path-with-arguments/regular-argument/",
+            )
 
         with translation.override("nl"):
             self.assertEqual(translate_url("/nl/gebruikers/", "en"), "/en/users/")
diff --git a/tests/i18n/patterns/urls/default.py b/tests/i18n/patterns/urls/default.py
index 418e9f5685..090b92eeca 100644
--- a/tests/i18n/patterns/urls/default.py
+++ b/tests/i18n/patterns/urls/default.py
@@ -8,7 +8,8 @@ view = TemplateView.as_view(template_name="dummy.html")
 urlpatterns = [
     path("not-prefixed/", view, name="not-prefixed"),
     path("not-prefixed-include/", include("i18n.patterns.urls.included")),
-    re_path(_(r"^translated/$"), view, name="no-prefix-translated"),
+    path(_("translated/"), view, name="no-prefix-translated"),
+    re_path(_(r"^translated-regex/$"), view, name="no-prefix-translated-regex"),
     re_path(
         _(r"^translated/(?P<slug>[\w-]+)/$"),
         view,
@@ -25,6 +26,11 @@ urlpatterns += i18n_patterns(
         view,
         name="with-arguments",
     ),
+    path(
+        _("path-with-arguments/<str:argument>/"),
+        view,
+        name="path-with-arguments",
+    ),
     re_path(_(r"^users/$"), view, name="users"),
     re_path(
         _(r"^account/"), include("i18n.patterns.urls.namespace", namespace="account")
diff --git a/tests/indexes/tests.py b/tests/indexes/tests.py
index 107703c39a..0c4158a886 100644
--- a/tests/indexes/tests.py
+++ b/tests/indexes/tests.py
@@ -3,7 +3,7 @@ from unittest import skipUnless
 
 from django.conf import settings
 from django.db import connection
-from django.db.models import CASCADE, ForeignKey, Index, Q
+from django.db.models import CASCADE, CharField, ForeignKey, Index, Q
 from django.db.models.functions import Lower
 from django.test import (
     TestCase,
@@ -87,6 +87,24 @@ class SchemaIndexesTests(TestCase):
             str(index.create_sql(Article, editor)),
         )
 
+    @skipUnlessDBFeature("can_create_inline_fk", "can_rollback_ddl")
+    def test_alter_field_unique_false_removes_deferred_sql(self):
+        field_added = CharField(max_length=127, unique=True)
+        field_added.set_attributes_from_name("charfield_added")
+
+        field_to_alter = CharField(max_length=127, unique=True)
+        field_to_alter.set_attributes_from_name("charfield_altered")
+        altered_field = CharField(max_length=127, unique=False)
+        altered_field.set_attributes_from_name("charfield_altered")
+
+        with connection.schema_editor() as editor:
+            editor.add_field(ArticleTranslation, field_added)
+            editor.add_field(ArticleTranslation, field_to_alter)
+            self.assertEqual(len(editor.deferred_sql), 2)
+            editor.alter_field(ArticleTranslation, field_to_alter, altered_field)
+            self.assertEqual(len(editor.deferred_sql), 1)
+            self.assertIn("charfield_added", str(editor.deferred_sql[0].parts["name"]))
+
 
 class SchemaIndexesNotPostgreSQLTests(TransactionTestCase):
     available_apps = ["indexes"]
diff --git a/tests/introspection/models.py b/tests/introspection/models.py
index d31eb0cbfa..c4a60ab182 100644
--- a/tests/introspection/models.py
+++ b/tests/introspection/models.py
@@ -84,7 +84,7 @@ class CheckConstraintModel(models.Model):
         }
         constraints = [
             models.CheckConstraint(
-                name="up_votes_gte_0_check", check=models.Q(up_votes__gte=0)
+                name="up_votes_gte_0_check", condition=models.Q(up_votes__gte=0)
             ),
         ]
 
diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py
index 9d9a09d34e..8b6d705acb 100644
--- a/tests/invalid_models_tests/test_models.py
+++ b/tests/invalid_models_tests/test_models.py
@@ -436,7 +436,7 @@ class IndexesTests(TestCase):
             fk_2 = models.ForeignKey(Target, models.CASCADE, related_name="target_2")
 
             class Meta:
-                constraints = [
+                indexes = [
                     models.Index(
                         fields=["id"],
                         include=["fk_1_id", "fk_2"],
@@ -1343,6 +1343,17 @@ class OtherModelTests(SimpleTestCase):
             ],
         )
 
+    def test_inherited_overriden_property_no_clash(self):
+        class Cheese:
+            @property
+            def filling_id(self):
+                pass
+
+        class Sandwich(Cheese, models.Model):
+            filling = models.ForeignKey("self", models.CASCADE)
+
+        self.assertEqual(Sandwich.check(), [])
+
     def test_single_primary_key(self):
         class Model(models.Model):
             foo = models.IntegerField(primary_key=True)
@@ -1855,7 +1866,9 @@ class ConstraintsTests(TestCase):
 
             class Meta:
                 constraints = [
-                    models.CheckConstraint(check=models.Q(age__gte=18), name="is_adult")
+                    models.CheckConstraint(
+                        condition=models.Q(age__gte=18), name="is_adult"
+                    )
                 ]
 
         errors = Model.check(databases=self.databases)
@@ -1880,7 +1893,9 @@ class ConstraintsTests(TestCase):
             class Meta:
                 required_db_features = {"supports_table_check_constraints"}
                 constraints = [
-                    models.CheckConstraint(check=models.Q(age__gte=18), name="is_adult")
+                    models.CheckConstraint(
+                        condition=models.Q(age__gte=18), name="is_adult"
+                    )
                 ]
 
         self.assertEqual(Model.check(databases=self.databases), [])
@@ -1892,7 +1907,7 @@ class ConstraintsTests(TestCase):
                 constraints = [
                     models.CheckConstraint(
                         name="name",
-                        check=models.Q(missing_field=2),
+                        condition=models.Q(missing_field=2),
                     ),
                 ]
 
@@ -1919,7 +1934,7 @@ class ConstraintsTests(TestCase):
 
             class Meta:
                 constraints = [
-                    models.CheckConstraint(name="name", check=models.Q(parents=3)),
+                    models.CheckConstraint(name="name", condition=models.Q(parents=3)),
                 ]
 
         self.assertEqual(
@@ -1942,7 +1957,7 @@ class ConstraintsTests(TestCase):
                 constraints = [
                     models.CheckConstraint(
                         name="name",
-                        check=models.Q(model__isnull=True),
+                        condition=models.Q(model__isnull=True),
                     ),
                 ]
 
@@ -1964,7 +1979,7 @@ class ConstraintsTests(TestCase):
 
             class Meta:
                 constraints = [
-                    models.CheckConstraint(name="name", check=models.Q(m2m=2)),
+                    models.CheckConstraint(name="name", condition=models.Q(m2m=2)),
                 ]
 
         self.assertEqual(
@@ -1992,7 +2007,7 @@ class ConstraintsTests(TestCase):
                 constraints = [
                     models.CheckConstraint(
                         name="name",
-                        check=models.Q(fk_1_id=2) | models.Q(fk_2=2),
+                        condition=models.Q(fk_1_id=2) | models.Q(fk_2=2),
                     ),
                 ]
 
@@ -2007,7 +2022,7 @@ class ConstraintsTests(TestCase):
                 constraints = [
                     models.CheckConstraint(
                         name="name",
-                        check=models.Q(pk__gt=5) & models.Q(age__gt=models.F("pk")),
+                        condition=models.Q(pk__gt=5) & models.Q(age__gt=models.F("pk")),
                     ),
                 ]
 
@@ -2023,7 +2038,7 @@ class ConstraintsTests(TestCase):
 
             class Meta:
                 constraints = [
-                    models.CheckConstraint(name="name", check=models.Q(field1=1)),
+                    models.CheckConstraint(name="name", condition=models.Q(field1=1)),
                 ]
 
         self.assertEqual(
@@ -2053,20 +2068,21 @@ class ConstraintsTests(TestCase):
                 constraints = [
                     models.CheckConstraint(
                         name="name1",
-                        check=models.Q(
+                        condition=models.Q(
                             field1__lt=models.F("parent__field1")
                             + models.F("parent__field2")
                         ),
                     ),
                     models.CheckConstraint(
-                        name="name2", check=models.Q(name=Lower("parent__name"))
+                        name="name2", condition=models.Q(name=Lower("parent__name"))
                     ),
                     models.CheckConstraint(
-                        name="name3", check=models.Q(parent__field3=models.F("field1"))
+                        name="name3",
+                        condition=models.Q(parent__field3=models.F("field1")),
                     ),
                     models.CheckConstraint(
                         name="name4",
-                        check=models.Q(name=Lower("previous__name")),
+                        condition=models.Q(name=Lower("previous__name")),
                     ),
                 ]
 
@@ -2100,7 +2116,7 @@ class ConstraintsTests(TestCase):
                 constraints = [
                     models.CheckConstraint(
                         name="name",
-                        check=models.Q(
+                        condition=models.Q(
                             (
                                 models.Q(name="test")
                                 & models.Q(field1__lt=models.F("parent__field1"))
@@ -2136,16 +2152,18 @@ class ConstraintsTests(TestCase):
             class Meta:
                 required_db_features = {"supports_table_check_constraints"}
                 constraints = [
-                    models.CheckConstraint(check=models.Q(id__gt=0), name="q_check"),
                     models.CheckConstraint(
-                        check=models.ExpressionWrapper(
+                        condition=models.Q(id__gt=0), name="q_check"
+                    ),
+                    models.CheckConstraint(
+                        condition=models.ExpressionWrapper(
                             models.Q(price__gt=20),
                             output_field=models.BooleanField(),
                         ),
                         name="expression_wrapper_check",
                     ),
                     models.CheckConstraint(
-                        check=models.expressions.RawSQL(
+                        condition=models.expressions.RawSQL(
                             "id = 0",
                             params=(),
                             output_field=models.BooleanField(),
@@ -2153,7 +2171,7 @@ class ConstraintsTests(TestCase):
                         name="raw_sql_check",
                     ),
                     models.CheckConstraint(
-                        check=models.Q(
+                        condition=models.Q(
                             models.ExpressionWrapper(
                                 models.Q(
                                     models.expressions.RawSQL(
diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py
index 20d2852fde..e58109fb78 100644
--- a/tests/logging_tests/tests.py
+++ b/tests/logging_tests/tests.py
@@ -1,6 +1,7 @@
 import logging
 from contextlib import contextmanager
 from io import StringIO
+from unittest import mock
 
 from admin_scripts.tests import AdminScriptTestCase
 
@@ -466,10 +467,30 @@ class AdminEmailHandlerTest(SimpleTestCase):
         msg = mail.outbox[0]
         self.assertEqual(msg.subject, "[Django] ERROR: message")
         self.assertEqual(len(msg.alternatives), 1)
-        body_html = str(msg.alternatives[0][0])
+        body_html = str(msg.alternatives[0].content)
         self.assertIn('<div id="traceback">', body_html)
         self.assertNotIn("<form", body_html)
 
+    @override_settings(ADMINS=[])
+    def test_emit_no_admins(self):
+        handler = AdminEmailHandler()
+        record = self.logger.makeRecord(
+            "name",
+            logging.ERROR,
+            "function",
+            "lno",
+            "message",
+            None,
+            None,
+        )
+        with mock.patch.object(
+            handler,
+            "format_subject",
+            side_effect=AssertionError("Should not be called"),
+        ):
+            handler.emit(record)
+        self.assertEqual(len(mail.outbox), 0)
+
 
 class SettingsConfigTest(AdminScriptTestCase):
     """
diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py
index a198a13b62..ebdaa21e3d 100644
--- a/tests/lookup/tests.py
+++ b/tests/lookup/tests.py
@@ -1366,6 +1366,12 @@ class LookupTests(TestCase):
             [stock_1, stock_2],
         )
 
+    def test_lookup_direct_value_rhs_unwrapped(self):
+        with self.assertNumQueries(1) as ctx:
+            self.assertIs(Author.objects.filter(GreaterThan(2, 1)).exists(), True)
+        # Direct values on RHS are not wrapped.
+        self.assertIn("2 > 1", ctx.captured_queries[0]["sql"])
+
 
 class LookupQueryingTests(TestCase):
     @classmethod
diff --git a/tests/mail/tests.py b/tests/mail/tests.py
index dd6b72ab49..1f7cbbadca 100644
--- a/tests/mail/tests.py
+++ b/tests/mail/tests.py
@@ -29,7 +29,6 @@ from django.core.mail.message import BadHeaderError, sanitize_address
 from django.test import SimpleTestCase, override_settings
 from django.test.utils import requires_tz_support
 from django.utils.translation import gettext_lazy
-from django.utils.version import PY311
 
 try:
     from aiosmtpd.controller import Controller
@@ -92,6 +91,37 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
         self.assertEqual(message["From"], "from@example.com")
         self.assertEqual(message["To"], "to@example.com")
 
+    @mock.patch("django.core.mail.message.MIMEText.set_payload")
+    def test_nonascii_as_string_with_ascii_charset(self, mock_set_payload):
+        """Line length check should encode the payload supporting `surrogateescape`.
+
+        Following https://github.com/python/cpython/issues/76511, newer
+        versions of Python (3.11.9, 3.12.3 and 3.13) ensure that a message's
+        payload is encoded with the provided charset and `surrogateescape` is
+        used as the error handling strategy.
+
+        This test is heavily based on the test from the fix for the bug above.
+        Line length checks in SafeMIMEText's set_payload should also use the
+        same error handling strategy to avoid errors such as:
+
+        UnicodeEncodeError: 'utf-8' codec can't encode <...>: surrogates not allowed
+
+        """
+
+        def simplified_set_payload(instance, payload, charset):
+            instance._payload = payload
+
+        mock_set_payload.side_effect = simplified_set_payload
+
+        text = (
+            "Text heavily based in Python's text for non-ascii messages: Föö bär"
+        ).encode("iso-8859-1")
+        body = text.decode("ascii", errors="surrogateescape")
+        email = EmailMessage("Subject", body, "from@example.com", ["to@example.com"])
+        message = email.message()
+        mock_set_payload.assert_called_once()
+        self.assertEqual(message.get_payload(decode=True), text)
+
     def test_multiple_recipients(self):
         email = EmailMessage(
             "Subject",
@@ -520,6 +550,18 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
         msg.attach("example.txt", "Text file content", "text/plain")
         self.assertIn(html_content, msg.message().as_string())
 
+    def test_alternatives(self):
+        msg = EmailMultiAlternatives()
+        html_content = "<p>This is <strong>html</strong></p>"
+        mime_type = "text/html"
+        msg.attach_alternative(html_content, mime_type)
+
+        self.assertEqual(msg.alternatives[0][0], html_content)
+        self.assertEqual(msg.alternatives[0].content, html_content)
+
+        self.assertEqual(msg.alternatives[0][1], mime_type)
+        self.assertEqual(msg.alternatives[0].mimetype, mime_type)
+
     def test_none_body(self):
         msg = EmailMessage("subject", None, "from@example.com", ["to@example.com"])
         self.assertEqual(msg.body, "")
@@ -596,6 +638,22 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
         )
 
     def test_attachments(self):
+        msg = EmailMessage()
+        file_name = "example.txt"
+        file_content = "Text file content"
+        mime_type = "text/plain"
+        msg.attach(file_name, file_content, mime_type)
+
+        self.assertEqual(msg.attachments[0][0], file_name)
+        self.assertEqual(msg.attachments[0].filename, file_name)
+
+        self.assertEqual(msg.attachments[0][1], file_content)
+        self.assertEqual(msg.attachments[0].content, file_content)
+
+        self.assertEqual(msg.attachments[0][2], mime_type)
+        self.assertEqual(msg.attachments[0].mimetype, mime_type)
+
+    def test_decoded_attachments(self):
         """Regression test for #9367"""
         headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"}
         subject, from_email, to = "hello", "from@example.com", "to@example.com"
@@ -615,14 +673,14 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
         self.assertEqual(payload[0].get_content_type(), "multipart/alternative")
         self.assertEqual(payload[1].get_content_type(), "application/pdf")
 
-    def test_attachments_two_tuple(self):
+    def test_decoded_attachments_two_tuple(self):
         msg = EmailMessage(attachments=[("filename1", "content1")])
         filename, content, mimetype = self.get_decoded_attachments(msg)[0]
         self.assertEqual(filename, "filename1")
         self.assertEqual(content, b"content1")
         self.assertEqual(mimetype, "application/octet-stream")
 
-    def test_attachments_MIMEText(self):
+    def test_decoded_attachments_MIMEText(self):
         txt = MIMEText("content1")
         msg = EmailMessage(attachments=[txt])
         payload = msg.message().get_payload()
@@ -791,13 +849,7 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
                 filebased.EmailBackend,
             )
 
-        if sys.platform == "win32" and not PY311:
-            msg = (
-                "_getfullpathname: path should be string, bytes or os.PathLike, not "
-                "object"
-            )
-        else:
-            msg = "expected str, bytes or os.PathLike object, not object"
+        msg = " not object"
         with self.assertRaisesMessage(TypeError, msg):
             mail.get_connection(
                 "django.core.mail.backends.filebased.EmailBackend", file_path=object()
@@ -1111,6 +1163,24 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
         with self.assertRaisesMessage(ValueError, msg):
             email_msg.attach_alternative("<p>content</p>", None)
 
+    def test_body_contains(self):
+        email_msg = EmailMultiAlternatives()
+        email_msg.body = "I am content."
+        self.assertIs(email_msg.body_contains("I am"), True)
+        self.assertIs(email_msg.body_contains("I am content."), True)
+
+        email_msg.attach_alternative("<p>I am different content.</p>", "text/html")
+        self.assertIs(email_msg.body_contains("I am"), True)
+        self.assertIs(email_msg.body_contains("I am content."), False)
+        self.assertIs(email_msg.body_contains("<p>I am different content.</p>"), False)
+
+    def test_body_contains_alternative_non_text(self):
+        email_msg = EmailMultiAlternatives()
+        email_msg.body = "I am content."
+        email_msg.attach_alternative("I am content.", "text/html")
+        email_msg.attach_alternative(b"I am a song.", "audio/mpeg")
+        self.assertIs(email_msg.body_contains("I am content"), True)
+
 
 @requires_tz_support
 class MailTimeZoneTests(SimpleTestCase):
diff --git a/tests/many_to_many/models.py b/tests/many_to_many/models.py
index 42fc426990..df7222e08d 100644
--- a/tests/many_to_many/models.py
+++ b/tests/many_to_many/models.py
@@ -78,3 +78,15 @@ class InheritedArticleA(AbstractArticle):
 
 class InheritedArticleB(AbstractArticle):
     pass
+
+
+class NullableTargetArticle(models.Model):
+    headline = models.CharField(max_length=100)
+    publications = models.ManyToManyField(
+        Publication, through="NullablePublicationThrough"
+    )
+
+
+class NullablePublicationThrough(models.Model):
+    article = models.ForeignKey(NullableTargetArticle, models.CASCADE)
+    publication = models.ForeignKey(Publication, models.CASCADE, null=True)
diff --git a/tests/many_to_many/tests.py b/tests/many_to_many/tests.py
index 7ed3b80abc..351e4eb8cc 100644
--- a/tests/many_to_many/tests.py
+++ b/tests/many_to_many/tests.py
@@ -1,10 +1,18 @@
 from unittest import mock
 
-from django.db import transaction
+from django.db import connection, transaction
 from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
 from django.utils.deprecation import RemovedInDjango60Warning
 
-from .models import Article, InheritedArticleA, InheritedArticleB, Publication, User
+from .models import (
+    Article,
+    InheritedArticleA,
+    InheritedArticleB,
+    NullablePublicationThrough,
+    NullableTargetArticle,
+    Publication,
+    User,
+)
 
 
 class ManyToManyTests(TestCase):
@@ -558,10 +566,16 @@ class ManyToManyTests(TestCase):
     def test_custom_default_manager_exists_count(self):
         a5 = Article.objects.create(headline="deleted")
         a5.publications.add(self.p2)
-        self.assertEqual(self.p2.article_set.count(), self.p2.article_set.all().count())
-        self.assertEqual(
-            self.p3.article_set.exists(), self.p3.article_set.all().exists()
-        )
+        with self.assertNumQueries(2) as ctx:
+            self.assertEqual(
+                self.p2.article_set.count(), self.p2.article_set.all().count()
+            )
+        self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
+        with self.assertNumQueries(2) as ctx:
+            self.assertEqual(
+                self.p3.article_set.exists(), self.p3.article_set.all().exists()
+            )
+        self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
 
     def test_get_prefetch_queryset_warning(self):
         articles = Article.objects.all()
@@ -582,3 +596,73 @@ class ManyToManyTests(TestCase):
                 instances=articles,
                 querysets=[Publication.objects.all(), Publication.objects.all()],
             )
+
+
+class ManyToManyQueryTests(TestCase):
+    """
+    SQL is optimized to reference the through table without joining against the
+    related table when using count() and exists() functions on a queryset for
+    many to many relations. The optimization applies to the case where there
+    are no filters.
+    """
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.article = Article.objects.create(
+            headline="Django lets you build Web apps easily"
+        )
+        cls.nullable_target_article = NullableTargetArticle.objects.create(
+            headline="The python is good"
+        )
+        NullablePublicationThrough.objects.create(
+            article=cls.nullable_target_article, publication=None
+        )
+
+    @skipUnlessDBFeature("supports_foreign_keys")
+    def test_count_join_optimization(self):
+        with self.assertNumQueries(1) as ctx:
+            self.article.publications.count()
+        self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
+
+        with self.assertNumQueries(1) as ctx:
+            self.article.publications.count()
+        self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
+        self.assertEqual(self.nullable_target_article.publications.count(), 0)
+
+    def test_count_join_optimization_disabled(self):
+        with (
+            mock.patch.object(connection.features, "supports_foreign_keys", False),
+            self.assertNumQueries(1) as ctx,
+        ):
+            self.article.publications.count()
+
+        self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
+
+    @skipUnlessDBFeature("supports_foreign_keys")
+    def test_exists_join_optimization(self):
+        with self.assertNumQueries(1) as ctx:
+            self.article.publications.exists()
+        self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
+
+        self.article.publications.prefetch_related()
+        with self.assertNumQueries(1) as ctx:
+            self.article.publications.exists()
+        self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
+        self.assertIs(self.nullable_target_article.publications.exists(), False)
+
+    def test_exists_join_optimization_disabled(self):
+        with (
+            mock.patch.object(connection.features, "supports_foreign_keys", False),
+            self.assertNumQueries(1) as ctx,
+        ):
+            self.article.publications.exists()
+
+        self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
+
+    def test_prefetch_related_no_queries_optimization_disabled(self):
+        qs = Article.objects.prefetch_related("publications")
+        article = qs.get()
+        with self.assertNumQueries(0):
+            article.publications.count()
+        with self.assertNumQueries(0):
+            article.publications.exists()
diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py
index c54349313e..d4345208ca 100644
--- a/tests/migrations/test_autodetector.py
+++ b/tests/migrations/test_autodetector.py
@@ -13,6 +13,7 @@ from django.db.migrations.graph import MigrationGraph
 from django.db.migrations.loader import MigrationLoader
 from django.db.migrations.questioner import MigrationQuestioner
 from django.db.migrations.state import ModelState, ProjectState
+from django.db.models.functions import Concat, Lower
 from django.test import SimpleTestCase, TestCase, override_settings
 from django.test.utils import isolate_lru_cache
 
@@ -287,7 +288,7 @@ class AutodetectorTests(BaseAutodetectorTests):
         {
             "constraints": [
                 models.CheckConstraint(
-                    check=models.Q(name__contains="Bob"), name="name_contains_bob"
+                    condition=models.Q(name__contains="Bob"), name="name_contains_bob"
                 )
             ]
         },
@@ -1309,7 +1310,7 @@ class AutodetectorTests(BaseAutodetectorTests):
             changes, "testapp", 0, 0, name="name", preserve_default=True
         )
         self.assertOperationFieldAttributes(
-            changes, "testapp", 0, 0, db_default=models.Value("Ada Lovelace")
+            changes, "testapp", 0, 0, db_default="Ada Lovelace"
         )
 
     @mock.patch(
@@ -1369,6 +1370,82 @@ class AutodetectorTests(BaseAutodetectorTests):
         self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True)
         self.assertEqual(mocked_ask_method.call_count, 3)
 
+    def test_add_field_before_generated_field(self):
+        initial_state = ModelState(
+            "testapp",
+            "Author",
+            [
+                ("name", models.CharField(max_length=20)),
+            ],
+        )
+        updated_state = ModelState(
+            "testapp",
+            "Author",
+            [
+                ("name", models.CharField(max_length=20)),
+                ("surname", models.CharField(max_length=20)),
+                (
+                    "lower_full_name",
+                    models.GeneratedField(
+                        expression=Concat(Lower("name"), Lower("surname")),
+                        output_field=models.CharField(max_length=30),
+                        db_persist=True,
+                    ),
+                ),
+            ],
+        )
+        changes = self.get_changes([initial_state], [updated_state])
+        self.assertNumberMigrations(changes, "testapp", 1)
+        self.assertOperationTypes(changes, "testapp", 0, ["AddField", "AddField"])
+        self.assertOperationFieldAttributes(
+            changes, "testapp", 0, 1, expression=Concat(Lower("name"), Lower("surname"))
+        )
+
+    def test_add_fk_before_generated_field(self):
+        initial_state = ModelState(
+            "testapp",
+            "Author",
+            [
+                ("name", models.CharField(max_length=20)),
+            ],
+        )
+        updated_state = [
+            ModelState(
+                "testapp",
+                "Publisher",
+                [
+                    ("name", models.CharField(max_length=20)),
+                ],
+            ),
+            ModelState(
+                "testapp",
+                "Author",
+                [
+                    ("name", models.CharField(max_length=20)),
+                    (
+                        "publisher",
+                        models.ForeignKey("testapp.Publisher", models.CASCADE),
+                    ),
+                    (
+                        "lower_full_name",
+                        models.GeneratedField(
+                            expression=Concat("name", "publisher_id"),
+                            output_field=models.CharField(max_length=20),
+                            db_persist=True,
+                        ),
+                    ),
+                ],
+            ),
+        ]
+        changes = self.get_changes([initial_state], updated_state)
+        self.assertNumberMigrations(changes, "testapp", 1)
+        self.assertOperationTypes(
+            changes, "testapp", 0, ["CreateModel", "AddField", "AddField"]
+        )
+        self.assertOperationFieldAttributes(
+            changes, "testapp", 0, 2, expression=Concat("name", "publisher_id")
+        )
+
     def test_remove_field(self):
         """Tests autodetection of removed fields."""
         changes = self.get_changes([self.author_name], [self.author_empty])
@@ -1515,7 +1592,7 @@ class AutodetectorTests(BaseAutodetectorTests):
             changes, "testapp", 0, 0, name="name", preserve_default=True
         )
         self.assertOperationFieldAttributes(
-            changes, "testapp", 0, 0, db_default=models.Value("Ada Lovelace")
+            changes, "testapp", 0, 0, db_default="Ada Lovelace"
         )
 
     @mock.patch(
@@ -2756,27 +2833,30 @@ class AutodetectorTests(BaseAutodetectorTests):
             {
                 "constraints": [
                     models.CheckConstraint(
-                        check=models.Q(name__contains="Bob"), name="name_contains_bob"
+                        condition=models.Q(name__contains="Bob"),
+                        name="name_contains_bob",
                     )
                 ]
             },
         )
         changes = self.get_changes([], [author])
-        added_constraint = models.CheckConstraint(
-            check=models.Q(name__contains="Bob"), name="name_contains_bob"
+        constraint = models.CheckConstraint(
+            condition=models.Q(name__contains="Bob"), name="name_contains_bob"
         )
         # Right number of migrations?
         self.assertEqual(len(changes["otherapp"]), 1)
         # Right number of actions?
         migration = changes["otherapp"][0]
-        self.assertEqual(len(migration.operations), 2)
+        self.assertEqual(len(migration.operations), 1)
         # Right actions order?
-        self.assertOperationTypes(
-            changes, "otherapp", 0, ["CreateModel", "AddConstraint"]
-        )
-        self.assertOperationAttributes(changes, "otherapp", 0, 0, name="Author")
+        self.assertOperationTypes(changes, "otherapp", 0, ["CreateModel"])
         self.assertOperationAttributes(
-            changes, "otherapp", 0, 1, model_name="author", constraint=added_constraint
+            changes,
+            "otherapp",
+            0,
+            0,
+            name="Author",
+            options={"constraints": [constraint]},
         )
 
     def test_add_constraints(self):
@@ -2787,7 +2867,7 @@ class AutodetectorTests(BaseAutodetectorTests):
         self.assertNumberMigrations(changes, "testapp", 1)
         self.assertOperationTypes(changes, "testapp", 0, ["AddConstraint"])
         added_constraint = models.CheckConstraint(
-            check=models.Q(name__contains="Bob"), name="name_contains_bob"
+            condition=models.Q(name__contains="Bob"), name="name_contains_bob"
         )
         self.assertOperationAttributes(
             changes, "testapp", 0, 0, model_name="author", constraint=added_constraint
@@ -2836,7 +2916,7 @@ class AutodetectorTests(BaseAutodetectorTests):
             {
                 "constraints": [
                     models.CheckConstraint(
-                        check=models.Q(type__in=book_types.keys()),
+                        condition=models.Q(type__in=book_types.keys()),
                         name="book_type_check",
                     ),
                 ],
@@ -2852,7 +2932,7 @@ class AutodetectorTests(BaseAutodetectorTests):
             {
                 "constraints": [
                     models.CheckConstraint(
-                        check=models.Q(("type__in", tuple(book_types))),
+                        condition=models.Q(("type__in", tuple(book_types))),
                         name="book_type_check",
                     ),
                 ],
@@ -4166,7 +4246,7 @@ class AutodetectorTests(BaseAutodetectorTests):
                 "order_with_respect_to": "book",
                 "constraints": [
                     models.CheckConstraint(
-                        check=models.Q(_order__gt=1), name="book_order_gt_1"
+                        condition=models.Q(_order__gt=1), name="book_order_gt_1"
                     ),
                 ],
             },
@@ -4177,7 +4257,7 @@ class AutodetectorTests(BaseAutodetectorTests):
             changes,
             "testapp",
             0,
-            ["CreateModel", "AddConstraint"],
+            ["CreateModel"],
         )
         self.assertOperationAttributes(
             changes,
@@ -4185,7 +4265,14 @@ class AutodetectorTests(BaseAutodetectorTests):
             0,
             0,
             name="Author",
-            options={"order_with_respect_to": "book"},
+            options={
+                "order_with_respect_to": "book",
+                "constraints": [
+                    models.CheckConstraint(
+                        condition=models.Q(_order__gt=1), name="book_order_gt_1"
+                    )
+                ],
+            },
         )
 
     def test_add_model_order_with_respect_to_index(self):
@@ -4232,7 +4319,7 @@ class AutodetectorTests(BaseAutodetectorTests):
                 {
                     "constraints": [
                         models.CheckConstraint(
-                            check=models.Q(_order__gt=1),
+                            condition=models.Q(_order__gt=1),
                             name="book_order_gt_1",
                         ),
                     ]
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index 1f8b3fb011..6ef172ee6f 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -1427,6 +1427,53 @@ class MigrateTests(MigrationTestBase):
         with self.assertRaisesMessage(CommandError, msg):
             call_command("migrate", prune=True)
 
+    @override_settings(
+        MIGRATION_MODULES={
+            "migrations": "migrations.test_migrations_squashed_no_replaces",
+            "migrations2": "migrations2.test_migrations_2_squashed_with_replaces",
+        },
+        INSTALLED_APPS=["migrations", "migrations2"],
+    )
+    def test_prune_respect_app_label(self):
+        recorder = MigrationRecorder(connection)
+        recorder.record_applied("migrations", "0001_initial")
+        recorder.record_applied("migrations", "0002_second")
+        recorder.record_applied("migrations", "0001_squashed_0002")
+        # Second app has squashed migrations with replaces.
+        recorder.record_applied("migrations2", "0001_initial")
+        recorder.record_applied("migrations2", "0002_second")
+        recorder.record_applied("migrations2", "0001_squashed_0002")
+        out = io.StringIO()
+        try:
+            call_command("migrate", "migrations", prune=True, stdout=out, no_color=True)
+            self.assertEqual(
+                out.getvalue(),
+                "Pruning migrations:\n"
+                "  Pruning migrations.0001_initial OK\n"
+                "  Pruning migrations.0002_second OK\n",
+            )
+            applied_migrations = [
+                migration
+                for migration in recorder.applied_migrations()
+                if migration[0] in ["migrations", "migrations2"]
+            ]
+            self.assertEqual(
+                applied_migrations,
+                [
+                    ("migrations", "0001_squashed_0002"),
+                    ("migrations2", "0001_initial"),
+                    ("migrations2", "0002_second"),
+                    ("migrations2", "0001_squashed_0002"),
+                ],
+            )
+        finally:
+            recorder.record_unapplied("migrations", "0001_initial")
+            recorder.record_unapplied("migrations", "0001_second")
+            recorder.record_unapplied("migrations", "0001_squashed_0002")
+            recorder.record_unapplied("migrations2", "0001_initial")
+            recorder.record_unapplied("migrations2", "0002_second")
+            recorder.record_unapplied("migrations2", "0001_squashed_0002")
+
 
 class MakeMigrationsTests(MigrationTestBase):
     """
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 5733ba7618..f865500829 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -9,7 +9,7 @@ from django.db.migrations.operations.fields import FieldOperation
 from django.db.migrations.state import ModelState, ProjectState
 from django.db.models import F
 from django.db.models.expressions import Value
-from django.db.models.functions import Abs, Pi
+from django.db.models.functions import Abs, Concat, Pi
 from django.db.transaction import atomic
 from django.test import (
     SimpleTestCase,
@@ -441,7 +441,7 @@ class OperationTests(OperationTestBase):
     def test_create_model_with_constraint(self):
         where = models.Q(pink__gt=2)
         check_constraint = models.CheckConstraint(
-            check=where, name="test_constraint_pony_pink_gt_2"
+            condition=where, name="test_constraint_pony_pink_gt_2"
         )
         operation = migrations.CreateModel(
             "Pony",
@@ -484,13 +484,13 @@ class OperationTests(OperationTestBase):
     def test_create_model_with_boolean_expression_in_check_constraint(self):
         app_label = "test_crmobechc"
         rawsql_constraint = models.CheckConstraint(
-            check=models.expressions.RawSQL(
+            condition=models.expressions.RawSQL(
                 "price < %s", (1000,), output_field=models.BooleanField()
             ),
             name=f"{app_label}_price_lt_1000_raw",
         )
         wrapper_constraint = models.CheckConstraint(
-            check=models.expressions.ExpressionWrapper(
+            condition=models.expressions.ExpressionWrapper(
                 models.Q(price__gt=500) | models.Q(price__lt=500),
                 output_field=models.BooleanField(),
             ),
@@ -1345,6 +1345,89 @@ class OperationTests(OperationTestBase):
         ponyrider = PonyRider.objects.create()
         ponyrider.riders.add(jockey)
 
+    def test_rename_m2m_field_with_2_references(self):
+        app_label = "test_rename_many_refs"
+        project_state = self.apply_operations(
+            app_label,
+            ProjectState(),
+            operations=[
+                migrations.CreateModel(
+                    name="Person",
+                    fields=[
+                        (
+                            "id",
+                            models.BigAutoField(
+                                auto_created=True,
+                                primary_key=True,
+                                serialize=False,
+                                verbose_name="ID",
+                            ),
+                        ),
+                        ("name", models.CharField(max_length=255)),
+                    ],
+                ),
+                migrations.CreateModel(
+                    name="Relation",
+                    fields=[
+                        (
+                            "id",
+                            models.BigAutoField(
+                                auto_created=True,
+                                primary_key=True,
+                                serialize=False,
+                                verbose_name="ID",
+                            ),
+                        ),
+                        (
+                            "child",
+                            models.ForeignKey(
+                                on_delete=models.CASCADE,
+                                related_name="relations_as_child",
+                                to=f"{app_label}.person",
+                            ),
+                        ),
+                        (
+                            "parent",
+                            models.ForeignKey(
+                                on_delete=models.CASCADE,
+                                related_name="relations_as_parent",
+                                to=f"{app_label}.person",
+                            ),
+                        ),
+                    ],
+                ),
+                migrations.AddField(
+                    model_name="person",
+                    name="parents_or_children",
+                    field=models.ManyToManyField(
+                        blank=True,
+                        through=f"{app_label}.Relation",
+                        to=f"{app_label}.person",
+                    ),
+                ),
+            ],
+        )
+        Person = project_state.apps.get_model(app_label, "Person")
+        Relation = project_state.apps.get_model(app_label, "Relation")
+
+        person1 = Person.objects.create(name="John Doe")
+        person2 = Person.objects.create(name="Jane Smith")
+        Relation.objects.create(child=person2, parent=person1)
+
+        self.assertTableExists(app_label + "_person")
+        self.assertTableNotExists(app_label + "_other")
+
+        self.apply_operations(
+            app_label,
+            project_state,
+            operations=[
+                migrations.RenameModel(old_name="Person", new_name="Other"),
+            ],
+        )
+
+        self.assertTableNotExists(app_label + "_person")
+        self.assertTableExists(app_label + "_other")
+
     def test_add_field(self):
         """
         Tests the AddField operation.
@@ -1379,6 +1462,54 @@ class OperationTests(OperationTestBase):
         self.assertEqual(definition[1], [])
         self.assertEqual(sorted(definition[2]), ["field", "model_name", "name"])
 
+    @skipUnlessDBFeature("supports_stored_generated_columns")
+    def test_add_generated_field(self):
+        app_label = "test_add_generated_field"
+        project_state = self.apply_operations(
+            app_label,
+            ProjectState(),
+            operations=[
+                migrations.CreateModel(
+                    "Rider",
+                    fields=[
+                        ("id", models.AutoField(primary_key=True)),
+                    ],
+                ),
+                migrations.CreateModel(
+                    "Pony",
+                    fields=[
+                        ("id", models.AutoField(primary_key=True)),
+                        ("name", models.CharField(max_length=20)),
+                        (
+                            "rider",
+                            models.ForeignKey(
+                                f"{app_label}.Rider", on_delete=models.CASCADE
+                            ),
+                        ),
+                        (
+                            "name_and_id",
+                            models.GeneratedField(
+                                expression=Concat(("name"), ("rider_id")),
+                                output_field=models.TextField(),
+                                db_persist=True,
+                            ),
+                        ),
+                    ],
+                ),
+            ],
+        )
+        Pony = project_state.apps.get_model(app_label, "Pony")
+        Rider = project_state.apps.get_model(app_label, "Rider")
+        rider = Rider.objects.create()
+        pony = Pony.objects.create(name="pony", rider=rider)
+        self.assertEqual(pony.name_and_id, str(pony.name) + str(rider.id))
+
+        new_rider = Rider.objects.create()
+        pony.rider = new_rider
+        pony.save()
+        pony.refresh_from_db()
+        self.assertEqual(pony.name_and_id, str(pony.name) + str(new_rider.id))
+
     def test_add_charfield(self):
         """
         Tests the AddField operation on TextField.
@@ -1581,7 +1712,7 @@ class OperationTests(OperationTestBase):
         self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
         field = new_state.models[app_label, "pony"].fields["height"]
         self.assertEqual(field.default, models.NOT_PROVIDED)
-        self.assertEqual(field.db_default, Value(4))
+        self.assertEqual(field.db_default, 4)
         project_state.apps.get_model(app_label, "pony").objects.create(weight=4)
         self.assertColumnNotExists(table_name, "height")
         # Add field.
@@ -1632,7 +1763,7 @@ class OperationTests(OperationTestBase):
                 self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
                 field = new_state.models[app_label, "pony"].fields["special_char"]
                 self.assertEqual(field.default, models.NOT_PROVIDED)
-                self.assertEqual(field.db_default, Value(db_default))
+                self.assertEqual(field.db_default, db_default)
                 self.assertColumnNotExists(table_name, "special_char")
                 with connection.schema_editor() as editor:
                     operation.database_forwards(
@@ -1700,7 +1831,7 @@ class OperationTests(OperationTestBase):
         self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
         field = new_state.models[app_label, "pony"].fields["height"]
         self.assertEqual(field.default, 3)
-        self.assertEqual(field.db_default, Value(4))
+        self.assertEqual(field.db_default, 4)
         pre_pony_pk = (
             project_state.apps.get_model(app_label, "pony").objects.create(weight=4).pk
         )
@@ -2145,7 +2276,7 @@ class OperationTests(OperationTestBase):
         old_weight = project_state.models[app_label, "pony"].fields["weight"]
         self.assertIs(old_weight.db_default, models.NOT_PROVIDED)
         new_weight = new_state.models[app_label, "pony"].fields["weight"]
-        self.assertEqual(new_weight.db_default, Value(4.5))
+        self.assertEqual(new_weight.db_default, 4.5)
         with self.assertRaises(IntegrityError), transaction.atomic():
             project_state.apps.get_model(app_label, "pony").objects.create()
         # Alter field.
@@ -2187,7 +2318,7 @@ class OperationTests(OperationTestBase):
         self.assertIs(old_pink.db_default, models.NOT_PROVIDED)
         new_pink = new_state.models[app_label, "pony"].fields["pink"]
         self.assertIs(new_pink.default, models.NOT_PROVIDED)
-        self.assertEqual(new_pink.db_default, Value(4))
+        self.assertEqual(new_pink.db_default, 4)
         pony = project_state.apps.get_model(app_label, "pony").objects.create(weight=1)
         self.assertEqual(pony.pink, 3)
         # Alter field.
@@ -2217,7 +2348,7 @@ class OperationTests(OperationTestBase):
         old_green = project_state.models[app_label, "pony"].fields["green"]
         self.assertIs(old_green.db_default, models.NOT_PROVIDED)
         new_green = new_state.models[app_label, "pony"].fields["green"]
-        self.assertEqual(new_green.db_default, Value(4))
+        self.assertEqual(new_green.db_default, 4)
         old_pony = project_state.apps.get_model(app_label, "pony").objects.create(
             weight=1
         )
@@ -2802,6 +2933,42 @@ class OperationTests(OperationTestBase):
                 (f"{app_label}_pony", "id"),
             )
 
+    def test_alter_id_pk_to_uuid_pk(self):
+        app_label = "test_alidpktuuidpk"
+        project_state = self.set_up_test_model(app_label)
+        new_state = project_state.clone()
+        # Add UUID field.
+        operation = migrations.AddField("Pony", "uuid", models.UUIDField())
+        operation.state_forwards(app_label, new_state)
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, project_state, new_state)
+        # Remove ID.
+        project_state = new_state
+        new_state = new_state.clone()
+        operation = migrations.RemoveField("Pony", "id")
+        operation.state_forwards(app_label, new_state)
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, project_state, new_state)
+        self.assertColumnNotExists(f"{app_label}_pony", "id")
+        # Rename to ID.
+        project_state = new_state
+        new_state = new_state.clone()
+        operation = migrations.RenameField("Pony", "uuid", "id")
+        operation.state_forwards(app_label, new_state)
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, project_state, new_state)
+        self.assertColumnNotExists(f"{app_label}_pony", "uuid")
+        self.assertColumnExists(f"{app_label}_pony", "id")
+        # Change to a primary key.
+        project_state = new_state
+        new_state = new_state.clone()
+        operation = migrations.AlterField(
+            "Pony", "id", models.UUIDField(primary_key=True)
+        )
+        operation.state_forwards(app_label, new_state)
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, project_state, new_state)
+
     @skipUnlessDBFeature("supports_foreign_keys")
     def test_alter_field_reloads_state_on_fk_with_to_field_target_type_change(self):
         app_label = "test_alflrsfkwtflttc"
@@ -3858,7 +4025,7 @@ class OperationTests(OperationTestBase):
         project_state = self.set_up_test_model("test_addconstraint")
         gt_check = models.Q(pink__gt=2)
         gt_constraint = models.CheckConstraint(
-            check=gt_check, name="test_add_constraint_pony_pink_gt_2"
+            condition=gt_check, name="test_add_constraint_pony_pink_gt_2"
         )
         gt_operation = migrations.AddConstraint("Pony", gt_constraint)
         self.assertEqual(
@@ -3901,7 +4068,7 @@ class OperationTests(OperationTestBase):
         # Add another one.
         lt_check = models.Q(pink__lt=100)
         lt_constraint = models.CheckConstraint(
-            check=lt_check, name="test_add_constraint_pony_pink_lt_100"
+            condition=lt_check, name="test_add_constraint_pony_pink_lt_100"
         )
         lt_operation = migrations.AddConstraint("Pony", lt_constraint)
         lt_operation.state_forwards("test_addconstraint", new_state)
@@ -3981,8 +4148,8 @@ class OperationTests(OperationTestBase):
             ),
         ]
         for check, valid, invalid in checks:
-            with self.subTest(check=check, valid=valid, invalid=invalid):
-                constraint = models.CheckConstraint(check=check, name="constraint")
+            with self.subTest(condition=check, valid=valid, invalid=invalid):
+                constraint = models.CheckConstraint(condition=check, name="constraint")
                 operation = migrations.AddConstraint("Author", constraint)
                 to_state = from_state.clone()
                 operation.state_forwards(app_label, to_state)
@@ -4006,7 +4173,7 @@ class OperationTests(OperationTestBase):
         constraint_name = "add_constraint_or"
         from_state = self.set_up_test_model(app_label)
         check = models.Q(pink__gt=2, weight__gt=2) | models.Q(weight__lt=0)
-        constraint = models.CheckConstraint(check=check, name=constraint_name)
+        constraint = models.CheckConstraint(condition=check, name=constraint_name)
         operation = migrations.AddConstraint("Pony", constraint)
         to_state = from_state.clone()
         operation.state_forwards(app_label, to_state)
@@ -4040,7 +4207,7 @@ class OperationTests(OperationTestBase):
         ]
         from_state = self.apply_operations(app_label, ProjectState(), operations)
         constraint = models.CheckConstraint(
-            check=models.Q(read=(100 - models.F("unread"))),
+            condition=models.Q(read=(100 - models.F("unread"))),
             name="test_addconstraint_combinable_sum_100",
         )
         operation = migrations.AddConstraint("Book", constraint)
@@ -4058,11 +4225,11 @@ class OperationTests(OperationTestBase):
             "test_removeconstraint",
             constraints=[
                 models.CheckConstraint(
-                    check=models.Q(pink__gt=2),
+                    condition=models.Q(pink__gt=2),
                     name="test_remove_constraint_pony_pink_gt_2",
                 ),
                 models.CheckConstraint(
-                    check=models.Q(pink__lt=100),
+                    condition=models.Q(pink__lt=100),
                     name="test_remove_constraint_pony_pink_lt_100",
                 ),
             ],
@@ -5079,6 +5246,15 @@ class OperationTests(OperationTestBase):
                     collected_sql = "\n".join(editor.collected_sql)
                     self.assertEqual(collected_sql.count(";"), 1)
 
+    def test_run_sql_backward_reverse_sql_required(self):
+        operation = migrations.RunSQL(sql=migrations.RunSQL.noop)
+        msg = "You cannot reverse this operation"
+        with (
+            connection.schema_editor() as editor,
+            self.assertRaisesMessage(NotImplementedError, msg),
+        ):
+            operation.database_backwards("test_runsql", editor, None, None)
+
     def test_run_python(self):
         """
         Tests the RunPython operation
@@ -5202,6 +5378,11 @@ class OperationTests(OperationTestBase):
         elidable_operation = migrations.RunPython(inner_method, elidable=True)
         self.assertEqual(elidable_operation.reduce(operation, []), [operation])
 
+    def test_run_python_invalid_reverse_code(self):
+        msg = "RunPython must be supplied with callable arguments"
+        with self.assertRaisesMessage(ValueError, msg):
+            migrations.RunPython(code=migrations.RunPython.noop, reverse_code="invalid")
+
     def test_run_python_atomic(self):
         """
         Tests the RunPython operation correctly handles the "atomic" keyword
@@ -5792,6 +5973,50 @@ class OperationTests(OperationTestBase):
     def test_invalid_generated_field_changes_virtual(self):
         self._test_invalid_generated_field_changes(db_persist=False)
 
+    def _test_invalid_generated_field_changes_on_rename(self, db_persist):
+        app_label = "test_igfcor"
+        operation = migrations.AddField(
+            "Pony",
+            "modified_pink",
+            models.GeneratedField(
+                expression=F("pink") + F("pink"),
+                output_field=models.IntegerField(),
+                db_persist=db_persist,
+            ),
+        )
+        project_state, new_state = self.make_test_state(app_label, operation)
+        # Add generated column.
+        with connection.schema_editor() as editor:
+            operation.database_forwards(app_label, editor, project_state, new_state)
+        # Rename field used in the generated field.
+        operations = [
+            migrations.RenameField("Pony", "pink", "renamed_pink"),
+            migrations.AlterField(
+                "Pony",
+                "modified_pink",
+                models.GeneratedField(
+                    expression=F("renamed_pink"),
+                    output_field=models.IntegerField(),
+                    db_persist=db_persist,
+                ),
+            ),
+        ]
+        msg = (
+            "Modifying GeneratedFields is not supported - the field "
+            f"{app_label}.Pony.modified_pink must be removed and re-added with the "
+            "new definition."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            self.apply_operations(app_label, new_state, operations)
+
+    @skipUnlessDBFeature("supports_stored_generated_columns")
+    def test_invalid_generated_field_changes_on_rename_stored(self):
+        self._test_invalid_generated_field_changes_on_rename(db_persist=True)
+
+    @skipUnlessDBFeature("supports_virtual_generated_columns")
+    def test_invalid_generated_field_changes_on_rename_virtual(self):
+        self._test_invalid_generated_field_changes_on_rename(db_persist=False)
+
     @skipUnlessDBFeature(
         "supports_stored_generated_columns",
         "supports_virtual_generated_columns",
diff --git a/tests/migrations/test_optimizer.py b/tests/migrations/test_optimizer.py
index ece6580ad8..2acbc7f09f 100644
--- a/tests/migrations/test_optimizer.py
+++ b/tests/migrations/test_optimizer.py
@@ -1208,7 +1208,7 @@ class OptimizerTests(SimpleTestCase):
 
     def test_add_remove_constraint(self):
         gt_constraint = models.CheckConstraint(
-            check=models.Q(pink__gt=2), name="constraint_pony_pink_gt_2"
+            condition=models.Q(pink__gt=2), name="constraint_pony_pink_gt_2"
         )
         self.assertOptimizesTo(
             [
@@ -1326,3 +1326,67 @@ class OptimizerTests(SimpleTestCase):
                 ),
             ],
         )
+
+    def test_create_model_add_constraint(self):
+        gt_constraint = models.CheckConstraint(
+            condition=models.Q(weight__gt=0), name="pony_weight_gt_0"
+        )
+        self.assertOptimizesTo(
+            [
+                migrations.CreateModel(
+                    name="Pony",
+                    fields=[
+                        ("weight", models.IntegerField()),
+                    ],
+                ),
+                migrations.AddConstraint("Pony", gt_constraint),
+            ],
+            [
+                migrations.CreateModel(
+                    name="Pony",
+                    fields=[
+                        ("weight", models.IntegerField()),
+                    ],
+                    options={"constraints": [gt_constraint]},
+                ),
+            ],
+        )
+
+    def test_create_model_remove_constraint(self):
+        self.assertOptimizesTo(
+            [
+                migrations.CreateModel(
+                    name="Pony",
+                    fields=[
+                        ("weight", models.IntegerField()),
+                    ],
+                    options={
+                        "constraints": [
+                            models.CheckConstraint(
+                                condition=models.Q(weight__gt=0),
+                                name="pony_weight_gt_0",
+                            ),
+                            models.UniqueConstraint(
+                                "weight", name="pony_weight_unique"
+                            ),
+                        ],
+                    },
+                ),
+                migrations.RemoveConstraint("Pony", "pony_weight_gt_0"),
+            ],
+            [
+                migrations.CreateModel(
+                    name="Pony",
+                    fields=[
+                        ("weight", models.IntegerField()),
+                    ],
+                    options={
+                        "constraints": [
+                            models.UniqueConstraint(
+                                "weight", name="pony_weight_unique"
+                            ),
+                        ]
+                    },
+                ),
+            ],
+        )
diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py
index 686eba4500..c64e4ebb4d 100644
--- a/tests/migrations/test_state.py
+++ b/tests/migrations/test_state.py
@@ -1856,8 +1856,11 @@ class ModelStateTests(SimpleTestCase):
         class Child2(Abstract):
             pass
 
+        abstract_state = ModelState.from_model(Abstract)
         child1_state = ModelState.from_model(Child1)
         child2_state = ModelState.from_model(Child2)
+        index_names = [index.name for index in abstract_state.options["indexes"]]
+        self.assertEqual(index_names, ["migrations__name_ae16a4_idx"])
         index_names = [index.name for index in child1_state.options["indexes"]]
         self.assertEqual(index_names, ["migrations__name_b0afd7_idx"])
         index_names = [index.name for index in child2_state.options["indexes"]]
@@ -1887,7 +1890,9 @@ class ModelStateTests(SimpleTestCase):
 
             class Meta:
                 constraints = [
-                    models.CheckConstraint(check=models.Q(size__gt=1), name="size_gt_1")
+                    models.CheckConstraint(
+                        condition=models.Q(size__gt=1), name="size_gt_1"
+                    )
                 ]
 
         state = ModelState.from_model(ModelWithConstraints)
diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py
index a2ac673804..891efd8ac7 100644
--- a/tests/migrations/test_writer.py
+++ b/tests/migrations/test_writer.py
@@ -152,6 +152,24 @@ class OperationWriterTests(SimpleTestCase):
             "),",
         )
 
+    def test_keyword_only_args_signature(self):
+        operation = (
+            custom_migration_operations.operations.ArgsAndKeywordOnlyArgsOperation(
+                1, 2, kwarg1=3, kwarg2=4
+            )
+        )
+        buff, imports = OperationWriter(operation, indentation=0).serialize()
+        self.assertEqual(imports, {"import custom_migration_operations.operations"})
+        self.assertEqual(
+            buff,
+            "custom_migration_operations.operations.ArgsAndKeywordOnlyArgsOperation(\n"
+            "    arg1=1,\n"
+            "    arg2=2,\n"
+            "    kwarg1=3,\n"
+            "    kwarg2=4,\n"
+            "),",
+        )
+
     def test_nested_args_signature(self):
         operation = custom_migration_operations.operations.ArgsOperation(
             custom_migration_operations.operations.ArgsOperation(1, 2),
diff --git a/tests/migrations2/test_migrations_2_squashed_with_replaces/0001_squashed_0002.py b/tests/migrations2/test_migrations_2_squashed_with_replaces/0001_squashed_0002.py
new file mode 100644
index 0000000000..30700e5eb6
--- /dev/null
+++ b/tests/migrations2/test_migrations_2_squashed_with_replaces/0001_squashed_0002.py
@@ -0,0 +1,30 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    replaces = [
+        ("migrations2", "0001_initial"),
+        ("migrations2", "0002_second"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            "OtherAuthor",
+            [
+                ("id", models.AutoField(primary_key=True)),
+                ("name", models.CharField(max_length=255)),
+            ],
+        ),
+        migrations.CreateModel(
+            "OtherBook",
+            [
+                ("id", models.AutoField(primary_key=True)),
+                (
+                    "author",
+                    models.ForeignKey(
+                        "migrations2.OtherAuthor", models.SET_NULL, null=True
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/tests/migrations2/test_migrations_2_squashed_with_replaces/__init__.py b/tests/migrations2/test_migrations_2_squashed_with_replaces/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index e34f3c8947..652c808b40 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -13,6 +13,8 @@ from django.db.models.functions import Lower
 from django.utils.functional import SimpleLazyObject
 from django.utils.translation import gettext_lazy as _
 
+from .storage import NoReadFileSystemStorage
+
 try:
     from PIL import Image
 except ImportError:
@@ -373,6 +375,21 @@ if Image:
             width_field="headshot_width",
         )
 
+    class PersonNoReadImage(models.Model):
+        """
+        Model that defines an ImageField with a storage backend that does not
+        support reading.
+        """
+
+        mugshot = models.ImageField(
+            upload_to="tests",
+            storage=NoReadFileSystemStorage(temp_storage_dir),
+            width_field="mugshot_width",
+            height_field="mugshot_height",
+        )
+        mugshot_width = models.IntegerField()
+        mugshot_height = models.IntegerField()
+
 
 class CustomJSONDecoder(json.JSONDecoder):
     def __init__(self, object_hook=None, *args, **kwargs):
diff --git a/tests/model_fields/storage.py b/tests/model_fields/storage.py
new file mode 100644
index 0000000000..9002c12683
--- /dev/null
+++ b/tests/model_fields/storage.py
@@ -0,0 +1,6 @@
+from django.core.files.storage.filesystem import FileSystemStorage
+
+
+class NoReadFileSystemStorage(FileSystemStorage):
+    def open(self, *args, **kwargs):
+        raise AssertionError("This storage class does not support reading.")
diff --git a/tests/model_fields/test_filefield.py b/tests/model_fields/test_filefield.py
index 2259c1e480..478e9edd36 100644
--- a/tests/model_fields/test_filefield.py
+++ b/tests/model_fields/test_filefield.py
@@ -5,13 +5,14 @@ import tempfile
 import unittest
 from pathlib import Path
 
-from django.core.exceptions import SuspiciousFileOperation
+from django.core.exceptions import FieldError, SuspiciousFileOperation
 from django.core.files import File, temp
 from django.core.files.base import ContentFile
 from django.core.files.uploadedfile import TemporaryUploadedFile
 from django.db import IntegrityError, models
 from django.test import TestCase, override_settings
 from django.test.utils import isolate_apps
+from django.utils.version import PY311
 
 from .models import Document
 
@@ -72,6 +73,27 @@ class FileFieldTests(TestCase):
             with self.assertRaisesMessage(SuspiciousFileOperation, msg):
                 document.save()
 
+    def test_save_content_file_without_name(self):
+        d = Document()
+        d.myfile = ContentFile(b"")
+        msg = "File for myfile must have the name attribute specified to be saved."
+        with self.assertRaisesMessage(FieldError, msg) as cm:
+            d.save()
+
+        if PY311:
+            self.assertEqual(
+                cm.exception.__notes__, ["Pass a 'name' argument to ContentFile."]
+            )
+
+    def test_delete_content_file(self):
+        file = ContentFile(b"", name="foo")
+        d = Document.objects.create(myfile=file)
+        d.myfile.delete()
+        self.assertIsNone(d.myfile.name)
+        msg = "The 'myfile' attribute has no file associated with it."
+        with self.assertRaisesMessage(ValueError, msg):
+            getattr(d.myfile, "file")
+
     def test_defer(self):
         Document.objects.create(myfile="something.txt")
         self.assertEqual(Document.objects.defer("myfile")[0].myfile, "something.txt")
diff --git a/tests/model_fields/test_generatedfield.py b/tests/model_fields/test_generatedfield.py
index a636e984fd..2fbfe3c82a 100644
--- a/tests/model_fields/test_generatedfield.py
+++ b/tests/model_fields/test_generatedfield.py
@@ -123,7 +123,12 @@ class BaseGeneratedFieldTests(SimpleTestCase):
                 db_persist=True,
             )
 
-        col = Square._meta.get_field("area").get_col("alias")
+        field = Square._meta.get_field("area")
+
+        col = field.get_col("alias")
+        self.assertIsInstance(col.output_field, IntegerField)
+
+        col = field.get_col("alias", field)
         self.assertIsInstance(col.output_field, IntegerField)
 
         class FloatSquare(Model):
@@ -134,7 +139,12 @@ class BaseGeneratedFieldTests(SimpleTestCase):
                 output_field=FloatField(),
             )
 
-        col = FloatSquare._meta.get_field("area").get_col("alias")
+        field = FloatSquare._meta.get_field("area")
+
+        col = field.get_col("alias")
+        self.assertIsInstance(col.output_field, FloatField)
+
+        col = field.get_col("alias", field)
         self.assertIsInstance(col.output_field, FloatField)
 
     @isolate_apps("model_fields")
@@ -197,6 +207,12 @@ class GeneratedFieldTestMixin:
         m.refresh_from_db()
         self.assertEqual(m.field, 8)
 
+    def test_save_model_with_pk(self):
+        m = self.base_model(pk=1, a=1, b=2)
+        m.save()
+        m = self._refresh_if_needed(m)
+        self.assertEqual(m.field, 3)
+
     def test_save_model_with_foreign_key(self):
         fk_object = Foo.objects.create(a="abc", d=Decimal("12.34"))
         m = self.base_model(a=1, b=2, fk=fk_object)
diff --git a/tests/model_fields/test_imagefield.py b/tests/model_fields/test_imagefield.py
index 8c93ed1bdb..7265da598b 100644
--- a/tests/model_fields/test_imagefield.py
+++ b/tests/model_fields/test_imagefield.py
@@ -18,6 +18,7 @@ if Image:
     from .models import (
         Person,
         PersonDimensionsFirst,
+        PersonNoReadImage,
         PersonTwoImages,
         PersonWithHeight,
         PersonWithHeightAndWidth,
@@ -30,7 +31,7 @@ else:
         pass
 
     PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person
-    PersonTwoImages = Person
+    PersonTwoImages = PersonNoReadImage = Person
 
 
 class ImageFieldTestMixin(SerializeMixin):
@@ -469,3 +470,28 @@ class TwoImageFieldTests(ImageFieldTestMixin, TestCase):
         # Dimensions were recalculated, and hence file should have opened.
         self.assertIs(p.mugshot.was_opened, True)
         self.assertIs(p.headshot.was_opened, True)
+
+
+@skipIf(Image is None, "Pillow is required to test ImageField")
+class NoReadTests(ImageFieldTestMixin, TestCase):
+    def test_width_height_correct_name_mangling_correct(self):
+        instance1 = PersonNoReadImage()
+
+        instance1.mugshot.save("mug", self.file1)
+
+        self.assertEqual(instance1.mugshot_width, 4)
+        self.assertEqual(instance1.mugshot_height, 8)
+
+        instance1.save()
+
+        self.assertEqual(instance1.mugshot_width, 4)
+        self.assertEqual(instance1.mugshot_height, 8)
+
+        instance2 = PersonNoReadImage()
+        instance2.mugshot.save("mug", self.file1)
+        instance2.save()
+
+        self.assertNotEqual(instance1.mugshot.name, instance2.mugshot.name)
+
+        self.assertEqual(instance1.mugshot_width, instance2.mugshot_width)
+        self.assertEqual(instance1.mugshot_height, instance2.mugshot_height)
diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py
index 1be59e17b3..ff42b1a14c 100644
--- a/tests/model_fields/test_jsonfield.py
+++ b/tests/model_fields/test_jsonfield.py
@@ -1120,3 +1120,10 @@ class TestQuerying(TestCase):
             KT("value")
         with self.assertRaisesMessage(ValueError, msg):
             KT("")
+
+    def test_literal_annotation_filtering(self):
+        all_objects = NullableJSONModel.objects.order_by("id")
+        qs = all_objects.annotate(data=Value({"foo": "bar"}, JSONField())).filter(
+            data__foo="bar"
+        )
+        self.assertQuerySetEqual(qs, all_objects)
diff --git a/tests/model_fields/test_mixins.py b/tests/model_fields/test_mixins.py
new file mode 100644
index 0000000000..5ccfac4d78
--- /dev/null
+++ b/tests/model_fields/test_mixins.py
@@ -0,0 +1,75 @@
+from django.db.models.fields.mixins import FieldCacheMixin
+from django.test import SimpleTestCase
+from django.utils.deprecation import RemovedInDjango60Warning
+from django.utils.functional import cached_property
+
+from .models import Foo
+
+
+# RemovedInDjango60Warning.
+class ExampleOld(FieldCacheMixin):
+    def get_cache_name(self):
+        return "example"
+
+
+class Example(FieldCacheMixin):
+    @cached_property
+    def cache_name(self):
+        return "example"
+
+
+class FieldCacheMixinTests(SimpleTestCase):
+    def setUp(self):
+        self.instance = Foo()
+        self.field = Example()
+
+    # RemovedInDjango60Warning: when the deprecation ends, replace with:
+    # def test_cache_name_not_implemented(self):
+    #   with self.assertRaises(NotImplementedError):
+    #       FieldCacheMixin().cache_name
+    def test_get_cache_name_not_implemented(self):
+        with self.assertRaises(NotImplementedError):
+            FieldCacheMixin().get_cache_name()
+
+    # RemovedInDjango60Warning.
+    def test_get_cache_name_deprecated(self):
+        msg = "Override ExampleOld.cache_name instead of get_cache_name()."
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            result = ExampleOld().cache_name
+        self.assertEqual(result, "example")
+
+    def test_cache_name(self):
+        result = Example().cache_name
+        self.assertEqual(result, "example")
+
+    def test_get_cached_value_missing(self):
+        with self.assertRaises(KeyError):
+            self.field.get_cached_value(self.instance)
+
+    def test_get_cached_value_default(self):
+        default = object()
+        result = self.field.get_cached_value(self.instance, default=default)
+        self.assertIs(result, default)
+
+    def test_get_cached_value_after_set(self):
+        value = object()
+
+        self.field.set_cached_value(self.instance, value)
+        result = self.field.get_cached_value(self.instance)
+
+        self.assertIs(result, value)
+
+    def test_is_cached_false(self):
+        result = self.field.is_cached(self.instance)
+        self.assertFalse(result)
+
+    def test_is_cached_true(self):
+        self.field.set_cached_value(self.instance, 1)
+        result = self.field.is_cached(self.instance)
+        self.assertTrue(result)
+
+    def test_delete_cached_value(self):
+        self.field.set_cached_value(self.instance, 1)
+        self.field.delete_cached_value(self.instance)
+        result = self.field.is_cached(self.instance)
+        self.assertFalse(result)
diff --git a/tests/model_forms/test_modelchoicefield.py b/tests/model_forms/test_modelchoicefield.py
index 19e9db69a0..83d801768a 100644
--- a/tests/model_forms/test_modelchoicefield.py
+++ b/tests/model_forms/test_modelchoicefield.py
@@ -7,7 +7,7 @@ from django.forms.widgets import CheckboxSelectMultiple
 from django.template import Context, Template
 from django.test import TestCase
 
-from .models import Article, Author, Book, Category, Writer
+from .models import Article, Author, Book, Category, ExplicitPK, Writer
 
 
 class ModelChoiceFieldTests(TestCase):
@@ -79,6 +79,12 @@ class ModelChoiceFieldTests(TestCase):
         self.assertEqual(f.clean(self.c1.slug), self.c1)
         self.assertEqual(f.clean(self.c1), self.c1)
 
+    def test_model_choice_null_characters(self):
+        f = forms.ModelChoiceField(queryset=ExplicitPK.objects.all())
+        msg = "Null characters are not allowed."
+        with self.assertRaisesMessage(ValidationError, msg):
+            f.clean("\x00something")
+
     def test_choices(self):
         f = forms.ModelChoiceField(
             Category.objects.filter(pk=self.c1.id), required=False
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index 3f927cb053..c6e12e1aab 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -2227,6 +2227,15 @@ class ModelMultipleChoiceFieldTests(TestCase):
         f = forms.ModelMultipleChoiceField(queryset=Writer.objects.all())
         self.assertNumQueries(1, f.clean, [p.pk for p in persons[1:11:2]])
 
+    def test_model_multiple_choice_null_characters(self):
+        f = forms.ModelMultipleChoiceField(queryset=ExplicitPK.objects.all())
+        msg = "Null characters are not allowed."
+        with self.assertRaisesMessage(ValidationError, msg):
+            f.clean(["\x00something"])
+
+        with self.assertRaisesMessage(ValidationError, msg):
+            f.clean(["valid", "\x00something"])
+
     def test_model_multiple_choice_run_validators(self):
         """
         ModelMultipleChoiceField run given validators (#14144).
diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py
index f78772da56..e5c026cee6 100644
--- a/tests/model_formsets/tests.py
+++ b/tests/model_formsets/tests.py
@@ -1703,6 +1703,30 @@ class ModelFormsetTest(TestCase):
             [{}, {"__all__": ["Please correct the duplicate values below."]}, {}],
         )
 
+    def test_inlineformset_with_jsonfield(self):
+        class BookForm(forms.ModelForm):
+            title = forms.JSONField()
+
+            class Meta:
+                model = Book
+                fields = ("title",)
+
+        BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
+        data = {
+            "book_set-TOTAL_FORMS": "3",
+            "book_set-INITIAL_FORMS": "0",
+            "book_set-MAX_NUM_FORMS": "",
+            "book_set-0-title": {"test1": "test2"},
+            "book_set-1-title": {"test1": "test2"},
+            "book_set-2-title": {"test3": "test4"},
+        }
+        author = Author.objects.create(name="test")
+        formset = BookFormSet(data, instance=author)
+        self.assertEqual(
+            formset.errors,
+            [{}, {"__all__": ["Please correct the duplicate values below."]}, {}],
+        )
+
     def test_model_formset_with_custom_pk(self):
         # a formset for a Model that has a custom primary key that still needs to be
         # added to the formset automatically
diff --git a/tests/model_meta/models.py b/tests/model_meta/models.py
index 6da62be2ac..20a75baf4f 100644
--- a/tests/model_meta/models.py
+++ b/tests/model_meta/models.py
@@ -1,6 +1,7 @@
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
+from django.utils.translation import gettext_lazy as _
 
 
 class Relation(models.Model):
@@ -124,6 +125,9 @@ class Person(BasePerson):
     # GR fields
     generic_relation_concrete = GenericRelation(Relation)
 
+    class Meta:
+        verbose_name = _("Person")
+
 
 class ProxyPerson(Person):
     class Meta:
@@ -162,6 +166,11 @@ class Relating(models.Model):
     people_hidden = models.ManyToManyField(Person, related_name="+")
 
 
+class Swappable(models.Model):
+    class Meta:
+        swappable = "MODEL_META_TESTS_SWAPPED"
+
+
 # ParentListTests models
 class CommonAncestor(models.Model):
     pass
diff --git a/tests/model_meta/tests.py b/tests/model_meta/tests.py
index fe2f6e63da..93883b5cf1 100644
--- a/tests/model_meta/tests.py
+++ b/tests/model_meta/tests.py
@@ -3,7 +3,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
 from django.core.exceptions import FieldDoesNotExist
 from django.db.models import CharField, Field, ForeignObjectRel, ManyToManyField
 from django.db.models.options import EMPTY_RELATION_TREE, IMMUTABLE_WARNING
-from django.test import SimpleTestCase
+from django.test import SimpleTestCase, override_settings
 
 from .models import (
     AbstractPerson,
@@ -16,6 +16,7 @@ from .models import (
     Relating,
     Relation,
     SecondParent,
+    Swappable,
 )
 from .results import TEST_RESULTS
 
@@ -222,6 +223,42 @@ class GetFieldByNameTests(OptionsBaseTests):
             opts.apps.models_ready = True
 
 
+class VerboseNameRawTests(SimpleTestCase):
+    def test_string(self):
+        # Clear cached property.
+        Relation._meta.__dict__.pop("verbose_name_raw", None)
+        self.assertEqual(Relation._meta.verbose_name_raw, "relation")
+
+    def test_gettext(self):
+        Person._meta.__dict__.pop("verbose_name_raw", None)
+        self.assertEqual(Person._meta.verbose_name_raw, "Person")
+
+
+class SwappedTests(SimpleTestCase):
+    def test_plain_model_none(self):
+        self.assertIsNone(Relation._meta.swapped)
+
+    def test_unset(self):
+        self.assertIsNone(Swappable._meta.swapped)
+
+    def test_set_and_unset(self):
+        with override_settings(MODEL_META_TESTS_SWAPPED="model_meta.Relation"):
+            self.assertEqual(Swappable._meta.swapped, "model_meta.Relation")
+        self.assertIsNone(Swappable._meta.swapped)
+
+    def test_setting_none(self):
+        with override_settings(MODEL_META_TESTS_SWAPPED=None):
+            self.assertIsNone(Swappable._meta.swapped)
+
+    def test_setting_non_label(self):
+        with override_settings(MODEL_META_TESTS_SWAPPED="not-a-label"):
+            self.assertEqual(Swappable._meta.swapped, "not-a-label")
+
+    def test_setting_self(self):
+        with override_settings(MODEL_META_TESTS_SWAPPED="model_meta.swappable"):
+            self.assertIsNone(Swappable._meta.swapped)
+
+
 class RelationTreeTests(SimpleTestCase):
     all_models = (Relation, AbstractPerson, BasePerson, Person, ProxyPerson, Relating)
 
@@ -258,7 +295,7 @@ class RelationTreeTests(SimpleTestCase):
             sorted(
                 field.related_query_name()
                 for field in Relation._meta._relation_tree
-                if not field.remote_field.field.remote_field.is_hidden()
+                if not field.remote_field.field.remote_field.hidden
             ),
             sorted(
                 [
@@ -314,15 +351,19 @@ class RelationTreeTests(SimpleTestCase):
         )
 
 
-class ParentListTests(SimpleTestCase):
-    def test_get_parent_list(self):
-        self.assertEqual(CommonAncestor._meta.get_parent_list(), [])
-        self.assertEqual(FirstParent._meta.get_parent_list(), [CommonAncestor])
-        self.assertEqual(SecondParent._meta.get_parent_list(), [CommonAncestor])
+class AllParentsTests(SimpleTestCase):
+    def test_all_parents(self):
+        self.assertEqual(CommonAncestor._meta.all_parents, ())
+        self.assertEqual(FirstParent._meta.all_parents, (CommonAncestor,))
+        self.assertEqual(SecondParent._meta.all_parents, (CommonAncestor,))
         self.assertEqual(
-            Child._meta.get_parent_list(), [FirstParent, SecondParent, CommonAncestor]
+            Child._meta.all_parents,
+            (FirstParent, SecondParent, CommonAncestor),
         )
 
+    def test_get_parent_list(self):
+        self.assertEqual(Child._meta.get_parent_list(), list(Child._meta.all_parents))
+
 
 class PropertyNamesTests(SimpleTestCase):
     def test_person(self):
diff --git a/tests/modeladmin/test_checks.py b/tests/modeladmin/test_checks.py
index 73777f05ab..f767a6c92b 100644
--- a/tests/modeladmin/test_checks.py
+++ b/tests/modeladmin/test_checks.py
@@ -69,7 +69,7 @@ class RawIdCheckTests(CheckTestCase):
 
     def test_missing_field(self):
         class TestModelAdmin(ModelAdmin):
-            raw_id_fields = ("non_existent_field",)
+            raw_id_fields = ["non_existent_field"]
 
         self.assertIsInvalid(
             TestModelAdmin,
@@ -602,8 +602,21 @@ class ListDisplayTests(CheckTestCase):
             TestModelAdmin,
             ValidationTestModel,
             "The value of 'list_display[0]' refers to 'non_existent_field', "
-            "which is not a callable, an attribute of 'TestModelAdmin', "
-            "or an attribute or method on 'modeladmin.ValidationTestModel'.",
+            "which is not a callable or attribute of 'TestModelAdmin', "
+            "or an attribute, method, or field on 'modeladmin.ValidationTestModel'.",
+            "admin.E108",
+        )
+
+    def test_missing_related_field(self):
+        class TestModelAdmin(ModelAdmin):
+            list_display = ("band__non_existent_field",)
+
+        self.assertIsInvalid(
+            TestModelAdmin,
+            ValidationTestModel,
+            "The value of 'list_display[0]' refers to 'band__non_existent_field', "
+            "which is not a callable or attribute of 'TestModelAdmin', "
+            "or an attribute, method, or field on 'modeladmin.ValidationTestModel'.",
             "admin.E108",
         )
 
diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py
index fad2dfaa1c..e8b59ed0bf 100644
--- a/tests/modeladmin/tests.py
+++ b/tests/modeladmin/tests.py
@@ -174,7 +174,19 @@ class ModelAdminTests(TestCase):
             pass
 
         ma = PlaceAdmin(Place, self.site)
-        self.assertIs(ma.lookup_allowed("country", "1", request), True)
+
+        cases = [
+            ("country", "1"),
+            ("country__exact", "1"),
+            ("country__id", "1"),
+            ("country__id__exact", "1"),
+            ("country__isnull", True),
+            ("country__isnull", False),
+            ("country__id__isnull", False),
+        ]
+        for lookup, lookup_value in cases:
+            with self.subTest(lookup=lookup):
+                self.assertIs(ma.lookup_allowed(lookup, lookup_value, request), True)
 
     @isolate_apps("modeladmin")
     def test_lookup_allowed_non_autofield_primary_key(self):
@@ -650,7 +662,8 @@ class ModelAdminTests(TestCase):
         self.assertHTMLEqual(
             str(form["main_band"]),
             '<div class="related-widget-wrapper" data-model-ref="band">'
-            '<select name="main_band" id="id_main_band" required>'
+            '<select data-context="available-source" '
+            'name="main_band" id="id_main_band" required>'
             '<option value="" selected>---------</option>'
             '<option value="%d">The Beatles</option>'
             '<option value="%d">The Doors</option>'
@@ -673,7 +686,8 @@ class ModelAdminTests(TestCase):
         self.assertHTMLEqual(
             str(form["main_band"]),
             '<div class="related-widget-wrapper" data-model-ref="band">'
-            '<select name="main_band" id="id_main_band" required>'
+            '<select data-context="available-source" '
+            'name="main_band" id="id_main_band" required>'
             '<option value="" selected>---------</option>'
             '<option value="%d">The Doors</option>'
             "</select></div>" % self.band.id,
@@ -767,7 +781,8 @@ class ModelAdminTests(TestCase):
             type(cmafa.base_fields["main_band"].widget.widget), AdminRadioSelect
         )
         self.assertEqual(
-            cmafa.base_fields["main_band"].widget.attrs, {"class": "radiolist inline"}
+            cmafa.base_fields["main_band"].widget.attrs,
+            {"class": "radiolist inline", "data-context": "available-source"},
         )
         self.assertEqual(
             list(cmafa.base_fields["main_band"].widget.choices),
@@ -778,7 +793,8 @@ class ModelAdminTests(TestCase):
             type(cmafa.base_fields["opening_band"].widget.widget), AdminRadioSelect
         )
         self.assertEqual(
-            cmafa.base_fields["opening_band"].widget.attrs, {"class": "radiolist"}
+            cmafa.base_fields["opening_band"].widget.attrs,
+            {"class": "radiolist", "data-context": "available-source"},
         )
         self.assertEqual(
             list(cmafa.base_fields["opening_band"].widget.choices),
diff --git a/tests/multiple_database/tests.py b/tests/multiple_database/tests.py
index 337ebae75e..9587030a46 100644
--- a/tests/multiple_database/tests.py
+++ b/tests/multiple_database/tests.py
@@ -1302,6 +1302,34 @@ class QueryTestCase(TestCase):
             title="Dive into Water", published=datetime.date(2009, 5, 4), extra_arg=True
         )
 
+    @override_settings(DATABASE_ROUTERS=["multiple_database.tests.TestRouter"])
+    def test_contenttype_in_separate_db(self):
+        ContentType.objects.using("other").all().delete()
+        book_other = Book.objects.using("other").create(
+            title="Test title other", published=datetime.date(2009, 5, 4)
+        )
+        book_default = Book.objects.using("default").create(
+            title="Test title default", published=datetime.date(2009, 5, 4)
+        )
+        book_type = ContentType.objects.using("default").get(
+            app_label="multiple_database", model="book"
+        )
+
+        book = book_type.get_object_for_this_type(title=book_other.title)
+        self.assertEqual(book, book_other)
+        book = book_type.get_object_for_this_type(using="other", title=book_other.title)
+        self.assertEqual(book, book_other)
+
+        with self.assertRaises(Book.DoesNotExist):
+            book_type.get_object_for_this_type(title=book_default.title)
+        book = book_type.get_object_for_this_type(
+            using="default", title=book_default.title
+        )
+        self.assertEqual(book, book_default)
+
+        all_books = book_type.get_all_objects_for_this_type()
+        self.assertCountEqual(all_books, [book_other])
+
 
 class ConnectionRouterTestCase(SimpleTestCase):
     @override_settings(
diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py
index 83644871fe..280a8273fb 100644
--- a/tests/one_to_one/tests.py
+++ b/tests/one_to_one/tests.py
@@ -473,9 +473,7 @@ class OneToOneTests(TestCase):
         self.assertFalse(
             hasattr(
                 Target,
-                HiddenPointer._meta.get_field(
-                    "target"
-                ).remote_field.get_accessor_name(),
+                HiddenPointer._meta.get_field("target").remote_field.accessor_name,
             )
         )
 
diff --git a/tests/postgres_tests/test_aggregates.py b/tests/postgres_tests/test_aggregates.py
index 386c55da25..b72310bdf1 100644
--- a/tests/postgres_tests/test_aggregates.py
+++ b/tests/postgres_tests/test_aggregates.py
@@ -1,4 +1,4 @@
-from django.db import connection, transaction
+from django.db import transaction
 from django.db.models import (
     CharField,
     F,
@@ -12,8 +12,7 @@ from django.db.models import (
     Window,
 )
 from django.db.models.fields.json import KeyTextTransform, KeyTransform
-from django.db.models.functions import Cast, Concat, Substr
-from django.test import skipUnlessDBFeature
+from django.db.models.functions import Cast, Concat, LPad, Substr
 from django.test.utils import Approximate
 from django.utils import timezone
 
@@ -95,9 +94,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
             BoolOr("boolean_field"),
             JSONBAgg("integer_field"),
             StringAgg("char_field", delimiter=";"),
+            BitXor("integer_field"),
         ]
-        if connection.features.has_bit_xor:
-            tests.append(BitXor("integer_field"))
         for aggregation in tests:
             with self.subTest(aggregation=aggregation):
                 # Empty result with non-execution optimization.
@@ -133,9 +131,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
                 StringAgg("char_field", delimiter=";", default=Value("<empty>")),
                 "<empty>",
             ),
+            (BitXor("integer_field", default=0), 0),
         ]
-        if connection.features.has_bit_xor:
-            tests.append((BitXor("integer_field", default=0), 0))
         for aggregation, expected_result in tests:
             with self.subTest(aggregation=aggregation):
                 # Empty result with non-execution optimization.
@@ -238,6 +235,16 @@ class TestGeneralAggregate(PostgreSQLTestCase):
         )
         self.assertEqual(values, {"arrayagg": ["en", "pl"]})
 
+    def test_array_agg_filter_and_ordering_params(self):
+        values = AggregateTestModel.objects.aggregate(
+            arrayagg=ArrayAgg(
+                "char_field",
+                filter=Q(json_field__has_key="lang"),
+                ordering=LPad(Cast("integer_field", CharField()), 2, Value("0")),
+            )
+        )
+        self.assertEqual(values, {"arrayagg": ["Foo2", "Foo4"]})
+
     def test_array_agg_filter(self):
         values = AggregateTestModel.objects.aggregate(
             arrayagg=ArrayAgg("integer_field", filter=Q(integer_field__gt=0)),
@@ -338,7 +345,6 @@ class TestGeneralAggregate(PostgreSQLTestCase):
         )
         self.assertEqual(values, {"bitor": 0})
 
-    @skipUnlessDBFeature("has_bit_xor")
     def test_bit_xor_general(self):
         AggregateTestModel.objects.create(integer_field=3)
         values = AggregateTestModel.objects.filter(
@@ -346,14 +352,12 @@ class TestGeneralAggregate(PostgreSQLTestCase):
         ).aggregate(bitxor=BitXor("integer_field"))
         self.assertEqual(values, {"bitxor": 2})
 
-    @skipUnlessDBFeature("has_bit_xor")
     def test_bit_xor_on_only_true_values(self):
         values = AggregateTestModel.objects.filter(
             integer_field=1,
         ).aggregate(bitxor=BitXor("integer_field"))
         self.assertEqual(values, {"bitxor": 1})
 
-    @skipUnlessDBFeature("has_bit_xor")
     def test_bit_xor_on_only_false_values(self):
         values = AggregateTestModel.objects.filter(
             integer_field=0,
diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py
index e5a8e9dbe9..770d4b1702 100644
--- a/tests/postgres_tests/test_constraints.py
+++ b/tests/postgres_tests/test_constraints.py
@@ -2,12 +2,17 @@ import datetime
 from unittest import mock
 
 from django.contrib.postgres.indexes import OpClass
+from django.core.checks import Error
 from django.core.exceptions import ValidationError
-from django.db import IntegrityError, NotSupportedError, connection, transaction
+from django.db import IntegrityError, connection, transaction
 from django.db.models import (
+    CASCADE,
+    CharField,
     CheckConstraint,
+    DateField,
     Deferrable,
     F,
+    ForeignKey,
     Func,
     IntegerField,
     Model,
@@ -54,7 +59,7 @@ class SchemaTests(PostgreSQLTestCase):
             constraint_name, self.get_constraints(RangesModel._meta.db_table)
         )
         constraint = CheckConstraint(
-            check=Q(ints__contained_by=NumericRange(10, 30)),
+            condition=Q(ints__contained_by=NumericRange(10, 30)),
             name=constraint_name,
         )
         with connection.schema_editor() as editor:
@@ -66,7 +71,7 @@ class SchemaTests(PostgreSQLTestCase):
 
     def test_check_constraint_array_contains(self):
         constraint = CheckConstraint(
-            check=Q(field__contains=[1]),
+            condition=Q(field__contains=[1]),
             name="array_contains",
         )
         msg = f"Constraint “{constraint.name}” is violated."
@@ -76,7 +81,7 @@ class SchemaTests(PostgreSQLTestCase):
 
     def test_check_constraint_array_length(self):
         constraint = CheckConstraint(
-            check=Q(field__len=1),
+            condition=Q(field__len=1),
             name="array_length",
         )
         msg = f"Constraint “{constraint.name}” is violated."
@@ -90,7 +95,7 @@ class SchemaTests(PostgreSQLTestCase):
             constraint_name, self.get_constraints(RangesModel._meta.db_table)
         )
         constraint = CheckConstraint(
-            check=Q(dates__contains=F("dates_inner")),
+            condition=Q(dates__contains=F("dates_inner")),
             name=constraint_name,
         )
         with connection.schema_editor() as editor:
@@ -114,7 +119,7 @@ class SchemaTests(PostgreSQLTestCase):
             constraint_name, self.get_constraints(RangesModel._meta.db_table)
         )
         constraint = CheckConstraint(
-            check=Q(timestamps__contains=F("timestamps_inner")),
+            condition=Q(timestamps__contains=F("timestamps_inner")),
             name=constraint_name,
         )
         with connection.schema_editor() as editor:
@@ -134,7 +139,7 @@ class SchemaTests(PostgreSQLTestCase):
 
     def test_check_constraint_range_contains(self):
         constraint = CheckConstraint(
-            check=Q(ints__contains=(1, 5)),
+            condition=Q(ints__contains=(1, 5)),
             name="ints_contains",
         )
         msg = f"Constraint “{constraint.name}” is violated."
@@ -143,7 +148,7 @@ class SchemaTests(PostgreSQLTestCase):
 
     def test_check_constraint_range_lower_upper(self):
         constraint = CheckConstraint(
-            check=Q(ints__startswith__gte=0) & Q(ints__endswith__lte=99),
+            condition=Q(ints__startswith__gte=0) & Q(ints__endswith__lte=99),
             name="ints_range_lower_upper",
         )
         msg = f"Constraint “{constraint.name}” is violated."
@@ -155,12 +160,12 @@ class SchemaTests(PostgreSQLTestCase):
 
     def test_check_constraint_range_lower_with_nulls(self):
         constraint = CheckConstraint(
-            check=Q(ints__isnull=True) | Q(ints__startswith__gte=0),
+            condition=Q(ints__isnull=True) | Q(ints__startswith__gte=0),
             name="ints_optional_positive_range",
         )
         constraint.validate(RangesModel, RangesModel())
         constraint = CheckConstraint(
-            check=Q(ints__startswith__gte=0),
+            condition=Q(ints__startswith__gte=0),
             name="ints_positive_range",
         )
         constraint.validate(RangesModel, RangesModel())
@@ -261,6 +266,19 @@ class SchemaTests(PostgreSQLTestCase):
         self.assertNotIn(constraint.name, self.get_constraints(Scene._meta.db_table))
         Scene.objects.create(scene="ScEnE 10", setting="Sir Bedemir's Castle")
 
+    def test_opclass_func_validate_constraints(self):
+        constraint_name = "test_opclass_func_validate_constraints"
+        constraint = UniqueConstraint(
+            OpClass(Lower("scene"), name="text_pattern_ops"),
+            name="test_opclass_func_validate_constraints",
+        )
+        Scene.objects.create(scene="First scene")
+        # Non-unique scene.
+        msg = f"Constraint “{constraint_name}” is violated."
+        with self.assertRaisesMessage(ValidationError, msg):
+            constraint.validate(Scene, Scene(scene="first Scene"))
+        constraint.validate(Scene, Scene(scene="second Scene"))
+
 
 class ExclusionConstraintTests(PostgreSQLTestCase):
     def get_constraints(self, table):
@@ -328,6 +346,57 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
                 include="invalid",
             )
 
+    @isolate_apps("postgres_tests")
+    def test_check(self):
+        class Author(Model):
+            name = CharField(max_length=255)
+            alias = CharField(max_length=255)
+
+            class Meta:
+                app_label = "postgres_tests"
+
+        class Book(Model):
+            title = CharField(max_length=255)
+            published_date = DateField()
+            author = ForeignKey(Author, CASCADE)
+
+            class Meta:
+                app_label = "postgres_tests"
+                constraints = [
+                    ExclusionConstraint(
+                        name="exclude_check",
+                        expressions=[
+                            (F("title"), RangeOperators.EQUAL),
+                            (F("published_date__year"), RangeOperators.EQUAL),
+                            ("published_date__month", RangeOperators.EQUAL),
+                            (F("author__name"), RangeOperators.EQUAL),
+                            ("author__alias", RangeOperators.EQUAL),
+                            ("nonexistent", RangeOperators.EQUAL),
+                        ],
+                    )
+                ]
+
+        self.assertCountEqual(
+            Book.check(databases=self.databases),
+            [
+                Error(
+                    "'constraints' refers to the nonexistent field 'nonexistent'.",
+                    obj=Book,
+                    id="models.E012",
+                ),
+                Error(
+                    "'constraints' refers to the joined field 'author__alias'.",
+                    obj=Book,
+                    id="models.E041",
+                ),
+                Error(
+                    "'constraints' refers to the joined field 'author__name'.",
+                    obj=Book,
+                    id="models.E041",
+                ),
+            ],
+        )
+
     def test_repr(self):
         constraint = ExclusionConstraint(
             name="exclude_overlapping",
@@ -928,7 +997,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
         RangesModel.objects.create(ints=(10, 19))
         RangesModel.objects.create(ints=(51, 60))
 
-    @skipUnlessDBFeature("supports_covering_spgist_indexes")
     def test_range_adjacent_spgist_include(self):
         constraint_name = "ints_adjacent_spgist_include"
         self.assertNotIn(
@@ -965,7 +1033,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
             editor.add_constraint(RangesModel, constraint)
         self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
 
-    @skipUnlessDBFeature("supports_covering_spgist_indexes")
     def test_range_adjacent_spgist_include_condition(self):
         constraint_name = "ints_adjacent_spgist_include_condition"
         self.assertNotIn(
@@ -998,7 +1065,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
             editor.add_constraint(RangesModel, constraint)
         self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
 
-    @skipUnlessDBFeature("supports_covering_spgist_indexes")
     def test_range_adjacent_spgist_include_deferrable(self):
         constraint_name = "ints_adjacent_spgist_include_deferrable"
         self.assertNotIn(
@@ -1015,27 +1081,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
             editor.add_constraint(RangesModel, constraint)
         self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
 
-    def test_spgist_include_not_supported(self):
-        constraint_name = "ints_adjacent_spgist_include_not_supported"
-        constraint = ExclusionConstraint(
-            name=constraint_name,
-            expressions=[("ints", RangeOperators.ADJACENT_TO)],
-            index_type="spgist",
-            include=["id"],
-        )
-        msg = (
-            "Covering exclusion constraints using an SP-GiST index require "
-            "PostgreSQL 14+."
-        )
-        with connection.schema_editor() as editor:
-            with mock.patch(
-                "django.db.backends.postgresql.features.DatabaseFeatures."
-                "supports_covering_spgist_indexes",
-                False,
-            ):
-                with self.assertRaisesMessage(NotSupportedError, msg):
-                    editor.add_constraint(RangesModel, constraint)
-
     def test_range_adjacent_opclass(self):
         constraint_name = "ints_adjacent_opclass"
         self.assertNotIn(
@@ -1118,7 +1163,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
             editor.add_constraint(RangesModel, constraint)
         self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
 
-    @skipUnlessDBFeature("supports_covering_spgist_indexes")
     def test_range_adjacent_spgist_opclass_include(self):
         constraint_name = "ints_adjacent_spgist_opclass_include"
         self.assertNotIn(
diff --git a/tests/postgres_tests/test_hstore.py b/tests/postgres_tests/test_hstore.py
index 2aaad637c6..cac3eb742a 100644
--- a/tests/postgres_tests/test_hstore.py
+++ b/tests/postgres_tests/test_hstore.py
@@ -410,6 +410,13 @@ class TestFormField(PostgreSQLSimpleTestCase):
         form_w_hstore = HStoreFormTest({"f1": '{"a": 2}'}, initial={"f1": {"a": 1}})
         self.assertTrue(form_w_hstore.has_changed())
 
+    def test_prepare_value(self):
+        field = forms.HStoreField()
+        self.assertEqual(
+            field.prepare_value({"aira_maplayer": "Αρδευτικό δίκτυο"}),
+            '{"aira_maplayer": "Αρδευτικό δίκτυο"}',
+        )
+
 
 class TestValidator(PostgreSQLSimpleTestCase):
     def test_simple_valid(self):
diff --git a/tests/postgres_tests/test_indexes.py b/tests/postgres_tests/test_indexes.py
index 8a7ee39a76..f98d03c6c1 100644
--- a/tests/postgres_tests/test_indexes.py
+++ b/tests/postgres_tests/test_indexes.py
@@ -1,5 +1,3 @@
-from unittest import mock
-
 from django.contrib.postgres.indexes import (
     BloomIndex,
     BrinIndex,
@@ -11,10 +9,9 @@ from django.contrib.postgres.indexes import (
     PostgresIndex,
     SpGistIndex,
 )
-from django.db import NotSupportedError, connection
+from django.db import connection
 from django.db.models import CharField, F, Index, Q
 from django.db.models.functions import Cast, Collate, Length, Lower
-from django.test import skipUnlessDBFeature
 from django.test.utils import register_lookup
 
 from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase
@@ -640,7 +637,6 @@ class SchemaTests(PostgreSQLTestCase):
             index_name, self.get_constraints(TextFieldModel._meta.db_table)
         )
 
-    @skipUnlessDBFeature("supports_covering_spgist_indexes")
     def test_spgist_include(self):
         index_name = "scene_spgist_include_setting"
         index = SpGistIndex(name=index_name, fields=["scene"], include=["setting"])
@@ -654,20 +650,6 @@ class SchemaTests(PostgreSQLTestCase):
             editor.remove_index(Scene, index)
         self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
 
-    def test_spgist_include_not_supported(self):
-        index_name = "spgist_include_exception"
-        index = SpGistIndex(fields=["scene"], name=index_name, include=["setting"])
-        msg = "Covering SP-GiST indexes require PostgreSQL 14+."
-        with self.assertRaisesMessage(NotSupportedError, msg):
-            with mock.patch(
-                "django.db.backends.postgresql.features.DatabaseFeatures."
-                "supports_covering_spgist_indexes",
-                False,
-            ):
-                with connection.schema_editor() as editor:
-                    editor.add_index(Scene, index)
-        self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
-
     def test_custom_suffix(self):
         class CustomSuffixIndex(PostgresIndex):
             suffix = "sfx"
diff --git a/tests/postgres_tests/test_operations.py b/tests/postgres_tests/test_operations.py
index ff344e3cb0..5780348251 100644
--- a/tests/postgres_tests/test_operations.py
+++ b/tests/postgres_tests/test_operations.py
@@ -4,6 +4,7 @@ from migrations.test_base import OperationTestBase
 
 from django.db import IntegrityError, NotSupportedError, connection, transaction
 from django.db.migrations.state import ProjectState
+from django.db.migrations.writer import OperationWriter
 from django.db.models import CheckConstraint, Index, Q, UniqueConstraint
 from django.db.utils import ProgrammingError
 from django.test import modify_settings, override_settings
@@ -393,6 +394,25 @@ class CreateCollationTests(PostgreSQLTestCase):
         self.assertEqual(len(captured_queries), 1)
         self.assertIn("DROP COLLATION", captured_queries[0]["sql"])
 
+    def test_writer(self):
+        operation = CreateCollation(
+            "sample_collation",
+            "und-u-ks-level2",
+            provider="icu",
+            deterministic=False,
+        )
+        buff, imports = OperationWriter(operation, indentation=0).serialize()
+        self.assertEqual(imports, {"import django.contrib.postgres.operations"})
+        self.assertEqual(
+            buff,
+            "django.contrib.postgres.operations.CreateCollation(\n"
+            "    name='sample_collation',\n"
+            "    locale='und-u-ks-level2',\n"
+            "    provider='icu',\n"
+            "    deterministic=False,\n"
+            "),",
+        )
+
 
 @unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific tests.")
 class RemoveCollationTests(PostgreSQLTestCase):
@@ -476,7 +496,7 @@ class AddConstraintNotValidTests(OperationTestBase):
     def test_add(self):
         table_name = f"{self.app_label}_pony"
         constraint_name = "pony_pink_gte_check"
-        constraint = CheckConstraint(check=Q(pink__gte=4), name=constraint_name)
+        constraint = CheckConstraint(condition=Q(pink__gte=4), name=constraint_name)
         operation = AddConstraintNotValid("Pony", constraint=constraint)
         project_state, new_state = self.make_test_state(self.app_label, operation)
         self.assertEqual(
@@ -529,7 +549,7 @@ class ValidateConstraintTests(OperationTestBase):
 
     def test_validate(self):
         constraint_name = "pony_pink_gte_check"
-        constraint = CheckConstraint(check=Q(pink__gte=4), name=constraint_name)
+        constraint = CheckConstraint(condition=Q(pink__gte=4), name=constraint_name)
         operation = AddConstraintNotValid("Pony", constraint=constraint)
         project_state, new_state = self.make_test_state(self.app_label, operation)
         Pony = new_state.apps.get_model(self.app_label, "Pony")
diff --git a/tests/prefetch_related/test_prefetch_related_objects.py b/tests/prefetch_related/test_prefetch_related_objects.py
index ca1f904c52..eea9a7fff7 100644
--- a/tests/prefetch_related/test_prefetch_related_objects.py
+++ b/tests/prefetch_related/test_prefetch_related_objects.py
@@ -1,7 +1,7 @@
 from django.db.models import Prefetch, prefetch_related_objects
 from django.test import TestCase
 
-from .models import Author, Book, Reader
+from .models import Author, Book, House, Reader, Room
 
 
 class PrefetchRelatedObjectsTests(TestCase):
@@ -33,6 +33,17 @@ class PrefetchRelatedObjectsTests(TestCase):
         cls.reader1.books_read.add(cls.book1, cls.book4)
         cls.reader2.books_read.add(cls.book2, cls.book4)
 
+        cls.house1 = House.objects.create(name="b1", address="1")
+        cls.house2 = House.objects.create(name="b2", address="2")
+
+        cls.room1 = Room.objects.create(name="a1", house=cls.house1)
+        cls.room2 = Room.objects.create(name="a2", house=cls.house2)
+
+        cls.house1.main_room = cls.room1
+        cls.house1.save()
+        cls.house2.main_room = cls.room2
+        cls.house2.save()
+
     def test_unknown(self):
         book1 = Book.objects.get(id=self.book1.id)
         with self.assertRaises(AttributeError):
@@ -58,20 +69,75 @@ class PrefetchRelatedObjectsTests(TestCase):
 
     def test_foreignkey_forward(self):
         authors = list(Author.objects.all())
-        with self.assertNumQueries(1):
+        with self.assertNumQueries(1) as ctx:
             prefetch_related_objects(authors, "first_book")
+        self.assertNotIn("ORDER BY", ctx.captured_queries[0]["sql"])
 
         with self.assertNumQueries(0):
             [author.first_book for author in authors]
 
+        authors = list(Author.objects.all())
+        with self.assertNumQueries(1) as ctx:
+            prefetch_related_objects(
+                authors,
+                Prefetch("first_book", queryset=Book.objects.order_by("-title")),
+            )
+        self.assertNotIn("ORDER BY", ctx.captured_queries[0]["sql"])
+
     def test_foreignkey_reverse(self):
         books = list(Book.objects.all())
-        with self.assertNumQueries(1):
+        with self.assertNumQueries(1) as ctx:
             prefetch_related_objects(books, "first_time_authors")
+        self.assertIn("ORDER BY", ctx.captured_queries[0]["sql"])
 
         with self.assertNumQueries(0):
             [list(book.first_time_authors.all()) for book in books]
 
+        books = list(Book.objects.all())
+        with self.assertNumQueries(1) as ctx:
+            prefetch_related_objects(
+                books,
+                Prefetch(
+                    "first_time_authors",
+                    queryset=Author.objects.order_by("-name"),
+                ),
+            )
+        self.assertIn("ORDER BY", ctx.captured_queries[0]["sql"])
+
+    def test_one_to_one_forward(self):
+        houses = list(House.objects.all())
+        with self.assertNumQueries(1) as ctx:
+            prefetch_related_objects(houses, "main_room")
+        self.assertNotIn("ORDER BY", ctx.captured_queries[0]["sql"])
+
+        with self.assertNumQueries(0):
+            [house.main_room for house in houses]
+
+        houses = list(House.objects.all())
+        with self.assertNumQueries(1) as ctx:
+            prefetch_related_objects(
+                houses,
+                Prefetch("main_room", queryset=Room.objects.order_by("-name")),
+            )
+        self.assertNotIn("ORDER BY", ctx.captured_queries[0]["sql"])
+
+    def test_one_to_one_reverse(self):
+        rooms = list(Room.objects.all())
+        with self.assertNumQueries(1) as ctx:
+            prefetch_related_objects(rooms, "main_room_of")
+        self.assertNotIn("ORDER BY", ctx.captured_queries[0]["sql"])
+
+        with self.assertNumQueries(0):
+            [room.main_room_of for room in rooms]
+
+        rooms = list(Room.objects.all())
+        with self.assertNumQueries(1) as ctx:
+            prefetch_related_objects(
+                rooms,
+                Prefetch("main_room_of", queryset=House.objects.order_by("-name")),
+            )
+        self.assertNotIn("ORDER BY", ctx.captured_queries[0]["sql"])
+
     def test_m2m_then_m2m(self):
         """A m2m can be followed through another m2m."""
         authors = list(Author.objects.all())
diff --git a/tests/queries/test_explain.py b/tests/queries/test_explain.py
index 980e8e9621..44689aedf8 100644
--- a/tests/queries/test_explain.py
+++ b/tests/queries/test_explain.py
@@ -96,6 +96,15 @@ class ExplainTests(TestCase):
                     option = "{} {}".format(name.upper(), "true" if value else "false")
                     self.assertIn(option, captured_queries[0]["sql"])
 
+    def test_multi_page_text_explain(self):
+        if "TEXT" not in connection.features.supported_explain_formats:
+            self.skipTest("This backend does not support TEXT format.")
+
+        base_qs = Tag.objects.order_by()
+        qs = base_qs.filter(name="test").union(*[base_qs for _ in range(100)])
+        result = qs.explain(format="text")
+        self.assertGreaterEqual(result.count("\n"), 100)
+
     def test_option_sql_injection(self):
         qs = Tag.objects.filter(name="test")
         options = {"SUMMARY true) SELECT 1; --": True}
diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py
index d3bab1f2a0..f7192a430a 100644
--- a/tests/queries/test_q.py
+++ b/tests/queries/test_q.py
@@ -10,6 +10,7 @@ from django.db.models import (
 )
 from django.db.models.expressions import NegatedExpression, RawSQL
 from django.db.models.functions import Lower
+from django.db.models.lookups import Exact, IsNull
 from django.db.models.sql.where import NothingNode
 from django.test import SimpleTestCase, TestCase
 
@@ -263,6 +264,33 @@ class QTests(SimpleTestCase):
                     Q(*items, _connector=connector),
                 )
 
+    def test_referenced_base_fields(self):
+        # Make sure Q.referenced_base_fields retrieves all base fields from
+        # both filters and F expressions.
+        tests = [
+            (Q(field_1=1) & Q(field_2=1), {"field_1", "field_2"}),
+            (
+                Q(Exact(F("field_3"), IsNull(F("field_4"), True))),
+                {"field_3", "field_4"},
+            ),
+            (Q(Exact(Q(field_5=F("field_6")), True)), {"field_5", "field_6"}),
+            (Q(field_2=1), {"field_2"}),
+            (Q(field_7__lookup=True), {"field_7"}),
+            (Q(field_7__joined_field__lookup=True), {"field_7"}),
+        ]
+        combined_q = Q(1)
+        combined_q_base_fields = set()
+        for q, expected_base_fields in tests:
+            combined_q &= q
+            combined_q_base_fields |= expected_base_fields
+        tests.append((combined_q, combined_q_base_fields))
+        for q, expected_base_fields in tests:
+            with self.subTest(q=q):
+                self.assertEqual(
+                    q.referenced_base_fields,
+                    expected_base_fields,
+                )
+
 
 class QCheckTests(TestCase):
     def test_basic(self):
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
index 48d610bb2b..7ac8a65d42 100644
--- a/tests/queries/tests.py
+++ b/tests/queries/tests.py
@@ -1357,6 +1357,24 @@ class Queries1Tests(TestCase):
         )
         self.assertSequenceEqual(Note.objects.exclude(negate=True), [self.n3])
 
+    def test_combining_does_not_mutate(self):
+        all_authors = Author.objects.all()
+        authors_with_report = Author.objects.filter(
+            Exists(Report.objects.filter(creator__pk=OuterRef("id")))
+        )
+        authors_without_report = all_authors.exclude(pk__in=authors_with_report)
+        items_before = Item.objects.filter(creator__in=authors_without_report)
+        self.assertCountEqual(items_before, [self.i2, self.i3, self.i4])
+        # Combining querysets doesn't mutate them.
+        all_authors | authors_with_report
+        all_authors & authors_with_report
+
+        authors_without_report = all_authors.exclude(pk__in=authors_with_report)
+        items_after = Item.objects.filter(creator__in=authors_without_report)
+
+        self.assertCountEqual(items_after, [self.i2, self.i3, self.i4])
+        self.assertCountEqual(items_before, items_after)
+
 
 class Queries2Tests(TestCase):
     @classmethod
diff --git a/tests/raw_query/tests.py b/tests/raw_query/tests.py
index 1dcc7ce740..853b7ee20e 100644
--- a/tests/raw_query/tests.py
+++ b/tests/raw_query/tests.py
@@ -176,10 +176,7 @@ class RawQueryTests(TestCase):
         self.assertSuccessfulRawQuery(MixedCaseIDColumn, query, queryset)
 
     def test_order_handler(self):
-        """
-        Test of raw raw query's tolerance for columns being returned in any
-        order
-        """
+        """Raw query tolerates columns being returned in any order."""
         selects = (
             ("dob, last_name, first_name, id"),
             ("last_name, dob, first_name, id"),
diff --git a/tests/requirements/postgres.txt b/tests/requirements/postgres.txt
index ab215b1ebc..91f911080c 100644
--- a/tests/requirements/postgres.txt
+++ b/tests/requirements/postgres.txt
@@ -1,2 +1,3 @@
 psycopg>=3.1.14; implementation_name == 'pypy'
 psycopg[binary]>=3.1.8; implementation_name != 'pypy'
+psycopg-pool>=3.2.0
diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt
index 26800e0506..d1f3708720 100644
--- a/tests/requirements/py3.txt
+++ b/tests/requirements/py3.txt
@@ -1,21 +1,21 @@
 aiosmtpd
 asgiref >= 3.7.0
-argon2-cffi >= 19.2.0; sys.platform != 'win32' or python_version < '3.13'
+argon2-cffi >= 19.2.0; sys_platform != 'win32' or python_version < '3.13'
 bcrypt
 black
 docutils >= 0.19
 geoip2; python_version < '3.13'
 jinja2 >= 2.11.0
-numpy; sys.platform != 'win32' or python_version < '3.13'
-Pillow >= 6.2.1; sys.platform != 'win32' or python_version < '3.13'
+numpy; sys_platform != 'win32' or python_version < '3.13'
+Pillow >= 6.2.1; sys_platform != 'win32' or python_version < '3.13'
 # pylibmc/libmemcached can't be built on Windows.
-pylibmc; sys.platform != 'win32'
+pylibmc; sys_platform != 'win32'
 pymemcache >= 3.4.0
-pywatchman; sys.platform != 'win32'
+pywatchman; sys_platform != 'win32'
 PyYAML
 redis >= 3.4.0
-selenium >= 4.8.0; sys.platform != 'win32' or python_version < '3.13'
+selenium >= 4.8.0; sys_platform != 'win32' or python_version < '3.13'
 sqlparse >= 0.3.1
 tblib >= 1.5.0
 tzdata
-colorama >= 0.4.6; sys.platform == 'win32'
+colorama >= 0.4.6; sys_platform == 'win32'
diff --git a/tests/runtests.py b/tests/runtests.py
index 1e3d15591f..c5bb637d33 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -28,7 +28,10 @@ else:
     from django.test.runner import get_max_test_processes, parallel_type
     from django.test.selenium import SeleniumTestCase, SeleniumTestCaseBase
     from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner
-    from django.utils.deprecation import RemovedInDjango60Warning
+    from django.utils.deprecation import (
+        RemovedInDjango60Warning,
+        RemovedInDjango61Warning,
+    )
     from django.utils.log import DEFAULT_LOGGING
     from django.utils.version import PY312, PYPY
 
@@ -42,6 +45,7 @@ else:
 
 # Make deprecation warnings errors to ensure no usage of deprecated features.
 warnings.simplefilter("error", RemovedInDjango60Warning)
+warnings.simplefilter("error", RemovedInDjango61Warning)
 # Make resource and runtime warning errors to ensure no usage of error prone
 # patterns.
 warnings.simplefilter("error", ResourceWarning)
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index 3a026281bd..3a2947cf43 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -7,6 +7,7 @@ from unittest import mock
 
 from django.core.exceptions import FieldError
 from django.core.management.color import no_style
+from django.core.serializers.json import DjangoJSONEncoder
 from django.db import (
     DatabaseError,
     DataError,
@@ -53,7 +54,16 @@ from django.db.models import (
     Value,
 )
 from django.db.models.fields.json import KT, KeyTextTransform
-from django.db.models.functions import Abs, Cast, Collate, Lower, Random, Round, Upper
+from django.db.models.functions import (
+    Abs,
+    Cast,
+    Collate,
+    Concat,
+    Lower,
+    Random,
+    Round,
+    Upper,
+)
 from django.db.models.indexes import IndexExpression
 from django.db.transaction import TransactionManagementError, atomic
 from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
@@ -885,6 +895,72 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.create_model(GeneratedFieldOutputFieldModel)
 
+    @isolate_apps("schema")
+    @skipUnlessDBFeature("supports_stored_generated_columns")
+    def test_add_generated_field_contains(self):
+        class GeneratedFieldContainsModel(Model):
+            text = TextField(default="foo")
+            generated = GeneratedField(
+                expression=Concat("text", Value("%")),
+                db_persist=True,
+                output_field=TextField(),
+            )
+
+            class Meta:
+                app_label = "schema"
+
+        with connection.schema_editor() as editor:
+            editor.create_model(GeneratedFieldContainsModel)
+
+        field = GeneratedField(
+            expression=Q(text__contains="foo"),
+            db_persist=True,
+            output_field=BooleanField(),
+        )
+        field.contribute_to_class(GeneratedFieldContainsModel, "contains_foo")
+
+        with connection.schema_editor() as editor:
+            editor.add_field(GeneratedFieldContainsModel, field)
+
+        obj = GeneratedFieldContainsModel.objects.create()
+        obj.refresh_from_db()
+        self.assertEqual(obj.text, "foo")
+        self.assertEqual(obj.generated, "foo%")
+        self.assertIs(obj.contains_foo, True)
+
+    @isolate_apps("schema")
+    @skipUnlessDBFeature("supports_stored_generated_columns")
+    def test_alter_generated_field(self):
+        class GeneratedFieldIndexedModel(Model):
+            number = IntegerField(default=1)
+            generated = GeneratedField(
+                expression=F("number"),
+                db_persist=True,
+                output_field=IntegerField(),
+            )
+
+            class Meta:
+                app_label = "schema"
+
+        with connection.schema_editor() as editor:
+            editor.create_model(GeneratedFieldIndexedModel)
+
+        old_field = GeneratedFieldIndexedModel._meta.get_field("generated")
+        new_field = GeneratedField(
+            expression=F("number"),
+            db_persist=True,
+            db_index=True,
+            output_field=IntegerField(),
+        )
+        new_field.contribute_to_class(GeneratedFieldIndexedModel, "generated")
+
+        with connection.schema_editor() as editor:
+            editor.alter_field(GeneratedFieldIndexedModel, old_field, new_field)
+
+        self.assertIn(
+            "generated", self.get_indexes(GeneratedFieldIndexedModel._meta.db_table)
+        )
+
     @isolate_apps("schema")
     def test_add_auto_field(self):
         class AddAutoFieldModel(Model):
@@ -2303,6 +2379,56 @@ class SchemaTests(TransactionTestCase):
         columns = self.column_classes(Author)
         self.assertEqual(columns["birth_year"][1].default, "1988")
 
+    @isolate_apps("schema")
+    def test_add_text_field_with_db_default(self):
+        class Author(Model):
+            description = TextField(db_default="(missing)")
+
+            class Meta:
+                app_label = "schema"
+
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+        columns = self.column_classes(Author)
+        self.assertIn("(missing)", columns["description"][1].default)
+
+    @isolate_apps("schema")
+    def test_db_default_equivalent_sql_noop(self):
+        class Author(Model):
+            name = TextField(db_default=Value("foo"))
+
+            class Meta:
+                app_label = "schema"
+
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+
+        new_field = TextField(db_default="foo")
+        new_field.set_attributes_from_name("name")
+        new_field.model = Author
+        with connection.schema_editor() as editor, self.assertNumQueries(0):
+            editor.alter_field(Author, Author._meta.get_field("name"), new_field)
+
+    @isolate_apps("schema")
+    def test_db_default_output_field_resolving(self):
+        class Author(Model):
+            data = JSONField(
+                encoder=DjangoJSONEncoder,
+                db_default={
+                    "epoch": datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
+                },
+            )
+
+            class Meta:
+                app_label = "schema"
+
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+
+        author = Author.objects.create()
+        author.refresh_from_db()
+        self.assertEqual(author.data, {"epoch": "1970-01-01T00:00:00Z"})
+
     @skipUnlessDBFeature(
         "supports_column_check_constraints", "can_introspect_check_constraints"
     )
@@ -2740,7 +2866,7 @@ class SchemaTests(TransactionTestCase):
         self.isolated_local_models = [DurationModel]
         constraint_name = "duration_gte_5_minutes"
         constraint = CheckConstraint(
-            check=Q(duration__gt=datetime.timedelta(minutes=5)),
+            condition=Q(duration__gt=datetime.timedelta(minutes=5)),
             name=constraint_name,
         )
         DurationModel._meta.constraints = [constraint]
@@ -2752,6 +2878,40 @@ class SchemaTests(TransactionTestCase):
             DurationModel.objects.create(duration=datetime.timedelta(minutes=4))
         DurationModel.objects.create(duration=datetime.timedelta(minutes=10))
 
+    @skipUnlessDBFeature(
+        "supports_column_check_constraints",
+        "can_introspect_check_constraints",
+        "supports_json_field",
+    )
+    @isolate_apps("schema")
+    def test_check_constraint_exact_jsonfield(self):
+        class JSONConstraintModel(Model):
+            data = JSONField()
+
+            class Meta:
+                app_label = "schema"
+
+        with connection.schema_editor() as editor:
+            editor.create_model(JSONConstraintModel)
+        self.isolated_local_models = [JSONConstraintModel]
+        constraint_name = "check_only_stable_version"
+        constraint = CheckConstraint(
+            condition=Q(data__version="stable"),
+            name=constraint_name,
+        )
+        JSONConstraintModel._meta.constraints = [constraint]
+        with connection.schema_editor() as editor:
+            editor.add_constraint(JSONConstraintModel, constraint)
+        constraints = self.get_constraints(JSONConstraintModel._meta.db_table)
+        self.assertIn(constraint_name, constraints)
+        with self.assertRaises(IntegrityError), atomic():
+            JSONConstraintModel.objects.create(
+                data={"release": "5.0.2dev", "version": "dev"}
+            )
+        JSONConstraintModel.objects.create(
+            data={"release": "5.0.3", "version": "stable"}
+        )
+
     @skipUnlessDBFeature(
         "supports_column_check_constraints", "can_introspect_check_constraints"
     )
@@ -2760,7 +2920,7 @@ class SchemaTests(TransactionTestCase):
             editor.create_model(Author)
         # Add the custom check constraint
         constraint = CheckConstraint(
-            check=Q(height__gte=0), name="author_height_gte_0_check"
+            condition=Q(height__gte=0), name="author_height_gte_0_check"
         )
         custom_constraint_name = constraint.name
         Author._meta.constraints = [constraint]
@@ -3171,7 +3331,9 @@ class SchemaTests(TransactionTestCase):
         "supports_column_check_constraints", "can_introspect_check_constraints"
     )
     def test_composed_check_constraint_with_fk(self):
-        constraint = CheckConstraint(check=Q(author__gt=0), name="book_author_check")
+        constraint = CheckConstraint(
+            condition=Q(author__gt=0), name="book_author_check"
+        )
         self._test_composed_constraint_with_fk(constraint)
 
     @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
@@ -3542,6 +3704,38 @@ class SchemaTests(TransactionTestCase):
         constraints = self.get_constraints(Author._meta.db_table)
         self.assertNotIn(constraint.name, constraints)
 
+    @skipUnlessDBFeature(
+        "supports_nulls_distinct_unique_constraints",
+        "supports_partial_indexes",
+    )
+    def test_unique_constraint_nulls_distinct_condition(self):
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+        constraint = UniqueConstraint(
+            fields=["height", "weight"],
+            name="un_height_weight_start_A",
+            condition=Q(name__startswith="A"),
+            nulls_distinct=False,
+        )
+        with connection.schema_editor() as editor:
+            editor.add_constraint(Author, constraint)
+        Author.objects.create(name="Adam", height=None, weight=None)
+        Author.objects.create(name="Avocado", height=1, weight=None)
+        Author.objects.create(name="Adrian", height=None, weight=1)
+        with self.assertRaises(IntegrityError):
+            Author.objects.create(name="Alex", height=None, weight=None)
+        Author.objects.create(name="Bob", height=None, weight=None)
+        with self.assertRaises(IntegrityError):
+            Author.objects.create(name="Alex", height=1, weight=None)
+        Author.objects.create(name="Bill", height=None, weight=None)
+        with self.assertRaises(IntegrityError):
+            Author.objects.create(name="Alex", height=None, weight=1)
+        Author.objects.create(name="Celine", height=None, weight=1)
+        with connection.schema_editor() as editor:
+            editor.remove_constraint(Author, constraint)
+        constraints = self.get_constraints(Author._meta.db_table)
+        self.assertNotIn(constraint.name, constraints)
+
     @skipIfDBFeature("supports_nulls_distinct_unique_constraints")
     def test_unique_constraint_nulls_distinct_unsupported(self):
         # UniqueConstraint is ignored on databases that don't support
diff --git a/tests/serializers/models/data.py b/tests/serializers/models/data.py
index a0e8751461..212ea0e06f 100644
--- a/tests/serializers/models/data.py
+++ b/tests/serializers/models/data.py
@@ -210,10 +210,6 @@ class EmailPKData(models.Model):
     data = models.EmailField(primary_key=True)
 
 
-# class FilePKData(models.Model):
-#    data = models.FileField(primary_key=True)
-
-
 class FilePathPKData(models.Model):
     data = models.FilePathField(primary_key=True)
 
@@ -226,10 +222,6 @@ class IntegerPKData(models.Model):
     data = models.IntegerField(primary_key=True)
 
 
-# class ImagePKData(models.Model):
-#    data = models.ImageField(primary_key=True)
-
-
 class GenericIPAddressPKData(models.Model):
     data = models.GenericIPAddressField(primary_key=True)
 
diff --git a/tests/serializers/test_data.py b/tests/serializers/test_data.py
index 6361dc0c05..33ea3458de 100644
--- a/tests/serializers/test_data.py
+++ b/tests/serializers/test_data.py
@@ -369,7 +369,6 @@ The end.""",
     (pk_obj, 620, DatePKData, datetime.date(2006, 6, 16)),
     (pk_obj, 630, DateTimePKData, datetime.datetime(2006, 6, 16, 10, 42, 37)),
     (pk_obj, 640, EmailPKData, "hovercraft@example.com"),
-    # (pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'),
     (pk_obj, 660, FilePathPKData, "/foo/bar/whiz.txt"),
     (pk_obj, 670, DecimalPKData, decimal.Decimal("12.345")),
     (pk_obj, 671, DecimalPKData, decimal.Decimal("-12.345")),
@@ -380,7 +379,6 @@ The end.""",
     (pk_obj, 680, IntegerPKData, 123456789),
     (pk_obj, 681, IntegerPKData, -123456789),
     (pk_obj, 682, IntegerPKData, 0),
-    # (XX, ImagePKData
     (pk_obj, 695, GenericIPAddressPKData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
     (pk_obj, 720, PositiveIntegerPKData, 123456789),
     (pk_obj, 730, PositiveSmallIntegerPKData, 12),
@@ -393,7 +391,6 @@ The end.""",
     # Several of them.
     # The end."""),
     # (pk_obj, 770, TimePKData, datetime.time(10, 42, 37)),
-    # (pk_obj, 790, XMLPKData, "<foo></foo>"),
     (pk_obj, 791, UUIDData, uuid_obj),
     (fk_obj, 792, FKToUUID, uuid_obj),
     (pk_obj, 793, UUIDDefaultData, uuid_obj),
diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py
index 7e0677d08d..9eabb933a8 100644
--- a/tests/sessions_tests/tests.py
+++ b/tests/sessions_tests/tests.py
@@ -61,11 +61,19 @@ class SessionTestsMixin:
     def test_get_empty(self):
         self.assertIsNone(self.session.get("cat"))
 
+    async def test_get_empty_async(self):
+        self.assertIsNone(await self.session.aget("cat"))
+
     def test_store(self):
         self.session["cat"] = "dog"
         self.assertIs(self.session.modified, True)
         self.assertEqual(self.session.pop("cat"), "dog")
 
+    async def test_store_async(self):
+        await self.session.aset("cat", "dog")
+        self.assertIs(self.session.modified, True)
+        self.assertEqual(await self.session.apop("cat"), "dog")
+
     def test_pop(self):
         self.session["some key"] = "exists"
         # Need to reset these to pretend we haven't accessed it:
@@ -77,6 +85,17 @@ class SessionTestsMixin:
         self.assertIs(self.session.modified, True)
         self.assertIsNone(self.session.get("some key"))
 
+    async def test_pop_async(self):
+        await self.session.aset("some key", "exists")
+        # Need to reset these to pretend we haven't accessed it:
+        self.accessed = False
+        self.modified = False
+
+        self.assertEqual(await self.session.apop("some key"), "exists")
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, True)
+        self.assertIsNone(await self.session.aget("some key"))
+
     def test_pop_default(self):
         self.assertEqual(
             self.session.pop("some key", "does not exist"), "does not exist"
@@ -84,6 +103,13 @@ class SessionTestsMixin:
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, False)
 
+    async def test_pop_default_async(self):
+        self.assertEqual(
+            await self.session.apop("some key", "does not exist"), "does not exist"
+        )
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, False)
+
     def test_pop_default_named_argument(self):
         self.assertEqual(
             self.session.pop("some key", default="does not exist"), "does not exist"
@@ -91,22 +117,46 @@ class SessionTestsMixin:
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, False)
 
+    async def test_pop_default_named_argument_async(self):
+        self.assertEqual(
+            await self.session.apop("some key", default="does not exist"),
+            "does not exist",
+        )
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, False)
+
     def test_pop_no_default_keyerror_raised(self):
         with self.assertRaises(KeyError):
             self.session.pop("some key")
 
+    async def test_pop_no_default_keyerror_raised_async(self):
+        with self.assertRaises(KeyError):
+            await self.session.apop("some key")
+
     def test_setdefault(self):
         self.assertEqual(self.session.setdefault("foo", "bar"), "bar")
         self.assertEqual(self.session.setdefault("foo", "baz"), "bar")
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, True)
 
+    async def test_setdefault_async(self):
+        self.assertEqual(await self.session.asetdefault("foo", "bar"), "bar")
+        self.assertEqual(await self.session.asetdefault("foo", "baz"), "bar")
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, True)
+
     def test_update(self):
         self.session.update({"update key": 1})
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, True)
         self.assertEqual(self.session.get("update key", None), 1)
 
+    async def test_update_async(self):
+        await self.session.aupdate({"update key": 1})
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, True)
+        self.assertEqual(await self.session.aget("update key", None), 1)
+
     def test_has_key(self):
         self.session["some key"] = 1
         self.session.modified = False
@@ -115,6 +165,14 @@ class SessionTestsMixin:
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, False)
 
+    async def test_has_key_async(self):
+        await self.session.aset("some key", 1)
+        self.session.modified = False
+        self.session.accessed = False
+        self.assertIs(await self.session.ahas_key("some key"), True)
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, False)
+
     def test_values(self):
         self.assertEqual(list(self.session.values()), [])
         self.assertIs(self.session.accessed, True)
@@ -125,6 +183,16 @@ class SessionTestsMixin:
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, False)
 
+    async def test_values_async(self):
+        self.assertEqual(list(await self.session.avalues()), [])
+        self.assertIs(self.session.accessed, True)
+        await self.session.aset("some key", 1)
+        self.session.modified = False
+        self.session.accessed = False
+        self.assertEqual(list(await self.session.avalues()), [1])
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, False)
+
     def test_keys(self):
         self.session["x"] = 1
         self.session.modified = False
@@ -133,6 +201,14 @@ class SessionTestsMixin:
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, False)
 
+    async def test_keys_async(self):
+        await self.session.aset("x", 1)
+        self.session.modified = False
+        self.session.accessed = False
+        self.assertEqual(list(await self.session.akeys()), ["x"])
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, False)
+
     def test_items(self):
         self.session["x"] = 1
         self.session.modified = False
@@ -141,6 +217,14 @@ class SessionTestsMixin:
         self.assertIs(self.session.accessed, True)
         self.assertIs(self.session.modified, False)
 
+    async def test_items_async(self):
+        await self.session.aset("x", 1)
+        self.session.modified = False
+        self.session.accessed = False
+        self.assertEqual(list(await self.session.aitems()), [("x", 1)])
+        self.assertIs(self.session.accessed, True)
+        self.assertIs(self.session.modified, False)
+
     def test_clear(self):
         self.session["x"] = 1
         self.session.modified = False
@@ -155,11 +239,20 @@ class SessionTestsMixin:
         self.session.save()
         self.assertIs(self.session.exists(self.session.session_key), True)
 
+    async def test_save_async(self):
+        await self.session.asave()
+        self.assertIs(await self.session.aexists(self.session.session_key), True)
+
     def test_delete(self):
         self.session.save()
         self.session.delete(self.session.session_key)
         self.assertIs(self.session.exists(self.session.session_key), False)
 
+    async def test_delete_async(self):
+        await self.session.asave()
+        await self.session.adelete(self.session.session_key)
+        self.assertIs(await self.session.aexists(self.session.session_key), False)
+
     def test_flush(self):
         self.session["foo"] = "bar"
         self.session.save()
@@ -171,6 +264,17 @@ class SessionTestsMixin:
         self.assertIs(self.session.modified, True)
         self.assertIs(self.session.accessed, True)
 
+    async def test_flush_async(self):
+        await self.session.aset("foo", "bar")
+        await self.session.asave()
+        prev_key = self.session.session_key
+        await self.session.aflush()
+        self.assertIs(await self.session.aexists(prev_key), False)
+        self.assertNotEqual(self.session.session_key, prev_key)
+        self.assertIsNone(self.session.session_key)
+        self.assertIs(self.session.modified, True)
+        self.assertIs(self.session.accessed, True)
+
     def test_cycle(self):
         self.session["a"], self.session["b"] = "c", "d"
         self.session.save()
@@ -181,6 +285,17 @@ class SessionTestsMixin:
         self.assertNotEqual(self.session.session_key, prev_key)
         self.assertEqual(list(self.session.items()), prev_data)
 
+    async def test_cycle_async(self):
+        await self.session.aset("a", "c")
+        await self.session.aset("b", "d")
+        await self.session.asave()
+        prev_key = self.session.session_key
+        prev_data = list(await self.session.aitems())
+        await self.session.acycle_key()
+        self.assertIs(await self.session.aexists(prev_key), False)
+        self.assertNotEqual(self.session.session_key, prev_key)
+        self.assertEqual(list(await self.session.aitems()), prev_data)
+
     def test_cycle_with_no_session_cache(self):
         self.session["a"], self.session["b"] = "c", "d"
         self.session.save()
@@ -190,11 +305,26 @@ class SessionTestsMixin:
         self.session.cycle_key()
         self.assertCountEqual(self.session.items(), prev_data)
 
+    async def test_cycle_with_no_session_cache_async(self):
+        await self.session.aset("a", "c")
+        await self.session.aset("b", "d")
+        await self.session.asave()
+        prev_data = await self.session.aitems()
+        self.session = self.backend(self.session.session_key)
+        self.assertIs(hasattr(self.session, "_session_cache"), False)
+        await self.session.acycle_key()
+        self.assertCountEqual(await self.session.aitems(), prev_data)
+
     def test_save_doesnt_clear_data(self):
         self.session["a"] = "b"
         self.session.save()
         self.assertEqual(self.session["a"], "b")
 
+    async def test_save_doesnt_clear_data_async(self):
+        await self.session.aset("a", "b")
+        await self.session.asave()
+        self.assertEqual(await self.session.aget("a"), "b")
+
     def test_invalid_key(self):
         # Submitting an invalid session key (either by guessing, or if the db has
         # removed the key) results in a new key being generated.
@@ -209,6 +339,20 @@ class SessionTestsMixin:
             # session key; make sure that entry is manually deleted
             session.delete("1")
 
+    async def test_invalid_key_async(self):
+        # Submitting an invalid session key (either by guessing, or if the db has
+        # removed the key) results in a new key being generated.
+        try:
+            session = self.backend("1")
+            await session.asave()
+            self.assertNotEqual(session.session_key, "1")
+            self.assertIsNone(await session.aget("cat"))
+            await session.adelete()
+        finally:
+            # Some backends leave a stale cache entry for the invalid
+            # session key; make sure that entry is manually deleted
+            await session.adelete("1")
+
     def test_session_key_empty_string_invalid(self):
         """Falsey values (Such as an empty string) are rejected."""
         self.session._session_key = ""
@@ -241,6 +385,18 @@ class SessionTestsMixin:
         self.session.set_expiry(0)
         self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE)
 
+    async def test_default_expiry_async(self):
+        # A normal session has a max age equal to settings.
+        self.assertEqual(
+            await self.session.aget_expiry_age(), settings.SESSION_COOKIE_AGE
+        )
+        # So does a custom session with an idle expiration time of 0 (but it'll
+        # expire at browser close).
+        await self.session.aset_expiry(0)
+        self.assertEqual(
+            await self.session.aget_expiry_age(), settings.SESSION_COOKIE_AGE
+        )
+
     def test_custom_expiry_seconds(self):
         modification = timezone.now()
 
@@ -252,6 +408,17 @@ class SessionTestsMixin:
         age = self.session.get_expiry_age(modification=modification)
         self.assertEqual(age, 10)
 
+    async def test_custom_expiry_seconds_async(self):
+        modification = timezone.now()
+
+        await self.session.aset_expiry(10)
+
+        date = await self.session.aget_expiry_date(modification=modification)
+        self.assertEqual(date, modification + timedelta(seconds=10))
+
+        age = await self.session.aget_expiry_age(modification=modification)
+        self.assertEqual(age, 10)
+
     def test_custom_expiry_timedelta(self):
         modification = timezone.now()
 
@@ -269,6 +436,23 @@ class SessionTestsMixin:
         age = self.session.get_expiry_age(modification=modification)
         self.assertEqual(age, 10)
 
+    async def test_custom_expiry_timedelta_async(self):
+        modification = timezone.now()
+
+        # Mock timezone.now, because set_expiry calls it on this code path.
+        original_now = timezone.now
+        try:
+            timezone.now = lambda: modification
+            await self.session.aset_expiry(timedelta(seconds=10))
+        finally:
+            timezone.now = original_now
+
+        date = await self.session.aget_expiry_date(modification=modification)
+        self.assertEqual(date, modification + timedelta(seconds=10))
+
+        age = await self.session.aget_expiry_age(modification=modification)
+        self.assertEqual(age, 10)
+
     def test_custom_expiry_datetime(self):
         modification = timezone.now()
 
@@ -280,12 +464,31 @@ class SessionTestsMixin:
         age = self.session.get_expiry_age(modification=modification)
         self.assertEqual(age, 10)
 
+    async def test_custom_expiry_datetime_async(self):
+        modification = timezone.now()
+
+        await self.session.aset_expiry(modification + timedelta(seconds=10))
+
+        date = await self.session.aget_expiry_date(modification=modification)
+        self.assertEqual(date, modification + timedelta(seconds=10))
+
+        age = await self.session.aget_expiry_age(modification=modification)
+        self.assertEqual(age, 10)
+
     def test_custom_expiry_reset(self):
         self.session.set_expiry(None)
         self.session.set_expiry(10)
         self.session.set_expiry(None)
         self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE)
 
+    async def test_custom_expiry_reset_async(self):
+        await self.session.aset_expiry(None)
+        await self.session.aset_expiry(10)
+        await self.session.aset_expiry(None)
+        self.assertEqual(
+            await self.session.aget_expiry_age(), settings.SESSION_COOKIE_AGE
+        )
+
     def test_get_expire_at_browser_close(self):
         # Tests get_expire_at_browser_close with different settings and different
         # set_expiry calls
@@ -309,6 +512,29 @@ class SessionTestsMixin:
             self.session.set_expiry(None)
             self.assertIs(self.session.get_expire_at_browser_close(), True)
 
+    async def test_get_expire_at_browser_close_async(self):
+        # Tests get_expire_at_browser_close with different settings and different
+        # set_expiry calls
+        with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
+            await self.session.aset_expiry(10)
+            self.assertIs(await self.session.aget_expire_at_browser_close(), False)
+
+            await self.session.aset_expiry(0)
+            self.assertIs(await self.session.aget_expire_at_browser_close(), True)
+
+            await self.session.aset_expiry(None)
+            self.assertIs(await self.session.aget_expire_at_browser_close(), False)
+
+        with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True):
+            await self.session.aset_expiry(10)
+            self.assertIs(await self.session.aget_expire_at_browser_close(), False)
+
+            await self.session.aset_expiry(0)
+            self.assertIs(await self.session.aget_expire_at_browser_close(), True)
+
+            await self.session.aset_expiry(None)
+            self.assertIs(await self.session.aget_expire_at_browser_close(), True)
+
     def test_decode(self):
         # Ensure we can decode what we encode
         data = {"a test key": "a test value"}
@@ -350,6 +576,22 @@ class SessionTestsMixin:
             self.session.delete(old_session_key)
             self.session.delete(new_session_key)
 
+    async def test_actual_expiry_async(self):
+        old_session_key = None
+        new_session_key = None
+        try:
+            await self.session.aset("foo", "bar")
+            await self.session.aset_expiry(-timedelta(seconds=10))
+            await self.session.asave()
+            old_session_key = self.session.session_key
+            # With an expiry date in the past, the session expires instantly.
+            new_session = self.backend(self.session.session_key)
+            new_session_key = new_session.session_key
+            self.assertIs(await new_session.ahas_key("foo"), False)
+        finally:
+            await self.session.adelete(old_session_key)
+            await self.session.adelete(new_session_key)
+
     def test_session_load_does_not_create_record(self):
         """
         Loading an unknown session key does not create a session record.
@@ -364,6 +606,15 @@ class SessionTestsMixin:
         # provided unknown key was cycled, not reused
         self.assertNotEqual(session.session_key, "someunknownkey")
 
+    async def test_session_load_does_not_create_record_async(self):
+        session = self.backend("someunknownkey")
+        await session.aload()
+
+        self.assertIsNone(session.session_key)
+        self.assertIs(await session.aexists(session.session_key), False)
+        # Provided unknown key was cycled, not reused.
+        self.assertNotEqual(session.session_key, "someunknownkey")
+
     def test_session_save_does_not_resurrect_session_logged_out_in_other_context(self):
         """
         Sessions shouldn't be resurrected by a concurrent request.
@@ -386,6 +637,28 @@ class SessionTestsMixin:
 
         self.assertEqual(s1.load(), {})
 
+    async def test_session_asave_does_not_resurrect_session_logged_out_in_other_context(
+        self,
+    ):
+        """Sessions shouldn't be resurrected by a concurrent request."""
+        # Create new session.
+        s1 = self.backend()
+        await s1.aset("test_data", "value1")
+        await s1.asave(must_create=True)
+
+        # Logout in another context.
+        s2 = self.backend(s1.session_key)
+        await s2.adelete()
+
+        # Modify session in first context.
+        await s1.aset("test_data", "value2")
+        with self.assertRaises(UpdateError):
+            # This should throw an exception as the session is deleted, not
+            # resurrect the session.
+            await s1.asave()
+
+        self.assertEqual(await s1.aload(), {})
+
 
 class DatabaseSessionTests(SessionTestsMixin, TestCase):
     backend = DatabaseSession
@@ -456,6 +729,25 @@ class DatabaseSessionTests(SessionTestsMixin, TestCase):
         # ... and one is deleted.
         self.assertEqual(1, self.model.objects.count())
 
+    async def test_aclear_expired(self):
+        self.assertEqual(await self.model.objects.acount(), 0)
+
+        # Object in the future.
+        await self.session.aset("key", "value")
+        await self.session.aset_expiry(3600)
+        await self.session.asave()
+        # Object in the past.
+        other_session = self.backend()
+        await other_session.aset("key", "value")
+        await other_session.aset_expiry(-3600)
+        await other_session.asave()
+
+        # Two sessions are in the database before clearing expired.
+        self.assertEqual(await self.model.objects.acount(), 2)
+        await self.session.aclear_expired()
+        await other_session.aclear_expired()
+        self.assertEqual(await self.model.objects.acount(), 1)
+
 
 @override_settings(USE_TZ=True)
 class DatabaseSessionWithTimeZoneTests(DatabaseSessionTests):
@@ -491,11 +783,28 @@ class CustomDatabaseSessionTests(DatabaseSessionTests):
         self.session.set_expiry(None)
         self.assertEqual(self.session.get_expiry_age(), self.custom_session_cookie_age)
 
+    async def test_custom_expiry_reset_async(self):
+        await self.session.aset_expiry(None)
+        await self.session.aset_expiry(10)
+        await self.session.aset_expiry(None)
+        self.assertEqual(
+            await self.session.aget_expiry_age(), self.custom_session_cookie_age
+        )
+
     def test_default_expiry(self):
         self.assertEqual(self.session.get_expiry_age(), self.custom_session_cookie_age)
         self.session.set_expiry(0)
         self.assertEqual(self.session.get_expiry_age(), self.custom_session_cookie_age)
 
+    async def test_default_expiry_async(self):
+        self.assertEqual(
+            await self.session.aget_expiry_age(), self.custom_session_cookie_age
+        )
+        await self.session.aset_expiry(0)
+        self.assertEqual(
+            await self.session.aget_expiry_age(), self.custom_session_cookie_age
+        )
+
 
 class CacheDBSessionTests(SessionTestsMixin, TestCase):
     backend = CacheDBSession
@@ -517,6 +826,38 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase):
         with self.assertRaises(InvalidCacheBackendError):
             self.backend()
 
+    @override_settings(
+        CACHES={"default": {"BACKEND": "cache.failing_cache.CacheClass"}}
+    )
+    def test_cache_set_failure_non_fatal(self):
+        """Failing to write to the cache does not raise errors."""
+        session = self.backend()
+        session["key"] = "val"
+
+        with self.assertLogs("django.contrib.sessions", "ERROR") as cm:
+            session.save()
+
+        # A proper ERROR log message was recorded.
+        log = cm.records[-1]
+        self.assertEqual(log.message, f"Error saving to cache ({session._cache})")
+        self.assertEqual(str(log.exc_info[1]), "Faked exception saving to cache")
+
+    @override_settings(
+        CACHES={"default": {"BACKEND": "cache.failing_cache.CacheClass"}}
+    )
+    async def test_cache_async_set_failure_non_fatal(self):
+        """Failing to write to the cache does not raise errors."""
+        session = self.backend()
+        await session.aset("key", "val")
+
+        with self.assertLogs("django.contrib.sessions", "ERROR") as cm:
+            await session.asave()
+
+        # A proper ERROR log message was recorded.
+        log = cm.records[-1]
+        self.assertEqual(log.message, f"Error saving to cache ({session._cache})")
+        self.assertEqual(str(log.exc_info[1]), "Faked exception saving to cache")
+
 
 @override_settings(USE_TZ=True)
 class CacheDBSessionWithTimeZoneTests(CacheDBSessionTests):
@@ -657,6 +998,12 @@ class CacheSessionTests(SessionTestsMixin, SimpleTestCase):
         self.session.save()
         self.assertIsNotNone(caches["default"].get(self.session.cache_key))
 
+    async def test_create_and_save_async(self):
+        self.session = self.backend()
+        await self.session.acreate()
+        await self.session.asave()
+        self.assertIsNotNone(caches["default"].get(await self.session.acache_key()))
+
 
 class SessionMiddlewareTests(TestCase):
     request_factory = RequestFactory()
@@ -883,6 +1230,9 @@ class CookieSessionTests(SessionTestsMixin, SimpleTestCase):
         """
         pass
 
+    async def test_save_async(self):
+        pass
+
     def test_cycle(self):
         """
         This test tested cycle_key() which would create a new session
@@ -892,11 +1242,17 @@ class CookieSessionTests(SessionTestsMixin, SimpleTestCase):
         """
         pass
 
+    async def test_cycle_async(self):
+        pass
+
     @unittest.expectedFailure
     def test_actual_expiry(self):
         # The cookie backend doesn't handle non-default expiry dates, see #19201
         super().test_actual_expiry()
 
+    async def test_actual_expiry_async(self):
+        pass
+
     def test_unpickling_exception(self):
         # signed_cookies backend should handle unpickle exceptions gracefully
         # by creating a new session
@@ -911,12 +1267,26 @@ class CookieSessionTests(SessionTestsMixin, SimpleTestCase):
     def test_session_load_does_not_create_record(self):
         pass
 
+    @unittest.skip(
+        "Cookie backend doesn't have an external store to create records in."
+    )
+    async def test_session_load_does_not_create_record_async(self):
+        pass
+
     @unittest.skip(
         "CookieSession is stored in the client and there is no way to query it."
     )
     def test_session_save_does_not_resurrect_session_logged_out_in_other_context(self):
         pass
 
+    @unittest.skip(
+        "CookieSession is stored in the client and there is no way to query it."
+    )
+    async def test_session_asave_does_not_resurrect_session_logged_out_in_other_context(
+        self,
+    ):
+        pass
+
 
 class ClearSessionsCommandTests(SimpleTestCase):
     def test_clearsessions_unsupported(self):
@@ -940,26 +1310,51 @@ class SessionBaseTests(SimpleTestCase):
         with self.assertRaisesMessage(NotImplementedError, msg):
             self.session.create()
 
+    async def test_acreate(self):
+        msg = self.not_implemented_msg % "a create"
+        with self.assertRaisesMessage(NotImplementedError, msg):
+            await self.session.acreate()
+
     def test_delete(self):
         msg = self.not_implemented_msg % "a delete"
         with self.assertRaisesMessage(NotImplementedError, msg):
             self.session.delete()
 
+    async def test_adelete(self):
+        msg = self.not_implemented_msg % "a delete"
+        with self.assertRaisesMessage(NotImplementedError, msg):
+            await self.session.adelete()
+
     def test_exists(self):
         msg = self.not_implemented_msg % "an exists"
         with self.assertRaisesMessage(NotImplementedError, msg):
             self.session.exists(None)
 
+    async def test_aexists(self):
+        msg = self.not_implemented_msg % "an exists"
+        with self.assertRaisesMessage(NotImplementedError, msg):
+            await self.session.aexists(None)
+
     def test_load(self):
         msg = self.not_implemented_msg % "a load"
         with self.assertRaisesMessage(NotImplementedError, msg):
             self.session.load()
 
+    async def test_aload(self):
+        msg = self.not_implemented_msg % "a load"
+        with self.assertRaisesMessage(NotImplementedError, msg):
+            await self.session.aload()
+
     def test_save(self):
         msg = self.not_implemented_msg % "a save"
         with self.assertRaisesMessage(NotImplementedError, msg):
             self.session.save()
 
+    async def test_asave(self):
+        msg = self.not_implemented_msg % "a save"
+        with self.assertRaisesMessage(NotImplementedError, msg):
+            await self.session.asave()
+
     def test_test_cookie(self):
         self.assertIs(self.session.has_key(self.session.TEST_COOKIE_NAME), False)
         self.session.set_test_cookie()
@@ -967,5 +1362,12 @@ class SessionBaseTests(SimpleTestCase):
         self.session.delete_test_cookie()
         self.assertIs(self.session.has_key(self.session.TEST_COOKIE_NAME), False)
 
+    async def test_atest_cookie(self):
+        self.assertIs(await self.session.ahas_key(self.session.TEST_COOKIE_NAME), False)
+        await self.session.aset_test_cookie()
+        self.assertIs(await self.session.atest_cookie_worked(), True)
+        await self.session.adelete_test_cookie()
+        self.assertIs(await self.session.ahas_key(self.session.TEST_COOKIE_NAME), False)
+
     def test_is_empty(self):
         self.assertIs(self.session.is_empty(), True)
diff --git a/tests/signals/tests.py b/tests/signals/tests.py
index 5558778bbe..6c90c6aa52 100644
--- a/tests/signals/tests.py
+++ b/tests/signals/tests.py
@@ -626,3 +626,19 @@ class AsyncReceiversTests(SimpleTestCase):
                 (async_handler, 1),
             ],
         )
+
+    async def test_asend_only_async_receivers(self):
+        async_handler = AsyncHandler()
+        signal = dispatch.Signal()
+        signal.connect(async_handler)
+
+        result = await signal.asend(self.__class__)
+        self.assertEqual(result, [(async_handler, 1)])
+
+    async def test_asend_robust_only_async_receivers(self):
+        async_handler = AsyncHandler()
+        signal = dispatch.Signal()
+        signal.connect(async_handler)
+
+        result = await signal.asend_robust(self.__class__)
+        self.assertEqual(result, [(async_handler, 1)])
diff --git a/tests/sphinx/__init__.py b/tests/sphinx/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/sphinx/test_github_links.py b/tests/sphinx/test_github_links.py
new file mode 100644
index 0000000000..c0ca43b092
--- /dev/null
+++ b/tests/sphinx/test_github_links.py
@@ -0,0 +1,211 @@
+import pathlib
+import sys
+
+from django.test import SimpleTestCase
+
+
+def last_n_parts(path, n):
+    return "/".join(path.parts[-n:])
+
+
+# The import must happen at the end of setUpClass, so it can't be imported at
+# the top of the file.
+github_links = None
+
+
+class GitHubLinkTests(SimpleTestCase):
+    @classmethod
+    def setUpClass(cls):
+        # The file implementing the code under test is in the docs folder and
+        # is not part of the Django package. This means it cannot be imported
+        # through standard means. Include its parent in the pythonpath for the
+        # duration of the tests to allow the code to be imported.
+        cls.ext_path = str((pathlib.Path(__file__).parents[2] / "docs/_ext").resolve())
+        sys.path.insert(0, cls.ext_path)
+        cls.addClassCleanup(sys.path.remove, cls.ext_path)
+        cls.addClassCleanup(sys.modules.pop, "github_links", None)
+        # Linters/IDEs may not be able to detect this as a valid import.
+        import github_links as _github_links
+
+        global github_links
+        github_links = _github_links
+
+    def test_code_locator(self):
+        locator = github_links.CodeLocator.from_code(
+            """
+from a import b, c
+from .d import e, f as g
+
+def h():
+    pass
+
+class I:
+    def j(self):
+        pass"""
+        )
+
+        self.assertEqual(locator.node_line_numbers, {"h": 5, "I": 8, "I.j": 9})
+        self.assertEqual(locator.import_locations, {"b": "a", "c": "a", "e": ".d"})
+
+    def test_module_name_to_file_path_package(self):
+        path = github_links.module_name_to_file_path("django")
+
+        self.assertEqual(last_n_parts(path, 2), "django/__init__.py")
+
+    def test_module_name_to_file_path_module(self):
+        path = github_links.module_name_to_file_path("django.shortcuts")
+
+        self.assertEqual(last_n_parts(path, 2), "django/shortcuts.py")
+
+    def test_get_path_and_line_class(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module", fullname="MyClass"
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py"
+        )
+        self.assertEqual(line, 12)
+
+    def test_get_path_and_line_func(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module", fullname="my_function"
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py"
+        )
+        self.assertEqual(line, 24)
+
+    def test_get_path_and_line_method(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module", fullname="MyClass.my_method"
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py"
+        )
+        self.assertEqual(line, 16)
+
+    def test_get_path_and_line_cached_property(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module",
+            fullname="MyClass.my_cached_property",
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py"
+        )
+        self.assertEqual(line, 20)
+
+    def test_get_path_and_line_forwarded_import(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module", fullname="MyOtherClass"
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5), "tests/sphinx/testdata/package/other_module.py"
+        )
+        self.assertEqual(line, 1)
+
+    def test_get_path_and_line_wildcard_import(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module", fullname="WildcardClass"
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5),
+            "tests/sphinx/testdata/package/wildcard_module.py",
+        )
+        self.assertEqual(line, 4)
+
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module",
+            fullname="WildcardMixin",
+        )
+        self.assertEqual(
+            last_n_parts(path, 5),
+            "tests/sphinx/testdata/package/wildcard_base.py",
+        )
+        self.assertEqual(line, 1)
+
+    def test_get_path_and_line_forwarded_import_module(self):
+        path, line = github_links.get_path_and_line(
+            module="tests.sphinx.testdata.package.module",
+            fullname="other_module.MyOtherClass",
+        )
+
+        self.assertEqual(
+            last_n_parts(path, 5), "tests/sphinx/testdata/package/other_module.py"
+        )
+        self.assertEqual(line, 1)
+
+    def test_get_branch_stable(self):
+        branch = github_links.get_branch(version="2.2", next_version="3.2")
+        self.assertEqual(branch, "stable/2.2.x")
+
+    def test_get_branch_latest(self):
+        branch = github_links.get_branch(version="3.2", next_version="3.2")
+        self.assertEqual(branch, "main")
+
+    def test_github_linkcode_resolve_unspecified_domain(self):
+        domain = "unspecified"
+        info = {}
+        self.assertIsNone(
+            github_links.github_linkcode_resolve(
+                domain, info, version="3.2", next_version="3.2"
+            )
+        )
+
+    def test_github_linkcode_resolve_unspecified_info(self):
+        domain = "py"
+        info = {"module": None, "fullname": None}
+        self.assertIsNone(
+            github_links.github_linkcode_resolve(
+                domain, info, version="3.2", next_version="3.2"
+            )
+        )
+
+    def test_github_linkcode_resolve_not_found(self):
+        info = {
+            "module": "foo.bar.baz.hopefully_non_existant_module",
+            "fullname": "MyClass",
+        }
+        self.assertIsNone(
+            github_links.github_linkcode_resolve(
+                "py", info, version="3.2", next_version="3.2"
+            )
+        )
+
+    def test_github_linkcode_resolve_link_to_object(self):
+        info = {
+            "module": "tests.sphinx.testdata.package.module",
+            "fullname": "MyClass",
+        }
+        self.assertEqual(
+            github_links.github_linkcode_resolve(
+                "py", info, version="3.2", next_version="3.2"
+            ),
+            "https://github.com/django/django/blob/main/tests/sphinx/"
+            "testdata/package/module.py#L12",
+        )
+
+    def test_github_linkcode_resolve_link_to_class_older_version(self):
+        info = {
+            "module": "tests.sphinx.testdata.package.module",
+            "fullname": "MyClass",
+        }
+        self.assertEqual(
+            github_links.github_linkcode_resolve(
+                "py", info, version="2.2", next_version="3.2"
+            ),
+            "https://github.com/django/django/blob/stable/2.2.x/tests/sphinx/"
+            "testdata/package/module.py#L12",
+        )
+
+    def test_import_error(self):
+        msg = "Could not import '.....test' in 'tests.sphinx.testdata.package'."
+        with self.assertRaisesMessage(ImportError, msg):
+            github_links.get_path_and_line(
+                module="tests.sphinx.testdata.package.import_error", fullname="Test"
+            )
diff --git a/tests/sphinx/testdata/package/__init__.py b/tests/sphinx/testdata/package/__init__.py
new file mode 100644
index 0000000000..3a41636b7b
--- /dev/null
+++ b/tests/sphinx/testdata/package/__init__.py
@@ -0,0 +1,2 @@
+# This file should never get imported. If it is, then something failed already.
+raise Exception
diff --git a/tests/sphinx/testdata/package/import_error.py b/tests/sphinx/testdata/package/import_error.py
new file mode 100644
index 0000000000..3eb0a2288f
--- /dev/null
+++ b/tests/sphinx/testdata/package/import_error.py
@@ -0,0 +1,5 @@
+from .....test import Test  # noqa
+
+
+class MyClass:
+    pass
diff --git a/tests/sphinx/testdata/package/module.py b/tests/sphinx/testdata/package/module.py
new file mode 100644
index 0000000000..987923b55e
--- /dev/null
+++ b/tests/sphinx/testdata/package/module.py
@@ -0,0 +1,25 @@
+"""
+Example docstring
+"""
+
+from django.utils.functional import cached_property
+from tests.sphinx.testdata.package.wildcard_module import *  # noqa
+
+from . import other_module  # noqa
+from .other_module import MyOtherClass  # noqa
+
+
+class MyClass(object):
+    def __init__(self):
+        pass
+
+    def my_method(self):
+        pass
+
+    @cached_property
+    def my_cached_property(self):
+        pass
+
+
+def my_function(self):
+    pass
diff --git a/tests/sphinx/testdata/package/other_module.py b/tests/sphinx/testdata/package/other_module.py
new file mode 100644
index 0000000000..f210b8e158
--- /dev/null
+++ b/tests/sphinx/testdata/package/other_module.py
@@ -0,0 +1,2 @@
+class MyOtherClass:
+    pass
diff --git a/tests/sphinx/testdata/package/wildcard_base.py b/tests/sphinx/testdata/package/wildcard_base.py
new file mode 100644
index 0000000000..e8c5b97cb9
--- /dev/null
+++ b/tests/sphinx/testdata/package/wildcard_base.py
@@ -0,0 +1,2 @@
+class WildcardMixin:
+    pass
diff --git a/tests/sphinx/testdata/package/wildcard_module.py b/tests/sphinx/testdata/package/wildcard_module.py
new file mode 100644
index 0000000000..187e367b72
--- /dev/null
+++ b/tests/sphinx/testdata/package/wildcard_module.py
@@ -0,0 +1,5 @@
+from .wildcard_base import WildcardMixin  # noqa
+
+
+class WildcardClass:
+    pass
diff --git a/tests/staticfiles_tests/project/static_url_slash/ignored.css b/tests/staticfiles_tests/project/static_url_slash/ignored.css
new file mode 100644
index 0000000000..369ff04632
--- /dev/null
+++ b/tests/staticfiles_tests/project/static_url_slash/ignored.css
@@ -0,0 +1,3 @@
+body {
+    background: url("//foobar");
+}
diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py
index 1e537dfe54..469d5ec690 100644
--- a/tests/staticfiles_tests/test_storage.py
+++ b/tests/staticfiles_tests/test_storage.py
@@ -22,7 +22,7 @@ from .settings import TEST_ROOT
 
 def hashed_file_path(test, path):
     fullpath = test.render_template(test.static_template_snippet(path))
-    return fullpath.replace(settings.STATIC_URL, "")
+    return fullpath.removeprefix(settings.STATIC_URL)
 
 
 class TestHashedFiles:
@@ -560,6 +560,32 @@ class TestCollectionManifestStorage(TestHashedFiles, CollectionTestCase):
         self.assertEqual(manifest_content, {"dummy.txt": "dummy.txt"})
 
 
+@override_settings(
+    STATIC_URL="/",
+    STORAGES={
+        **settings.STORAGES,
+        STATICFILES_STORAGE_ALIAS: {
+            "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
+        },
+    },
+)
+class TestCollectionManifestStorageStaticUrlSlash(CollectionTestCase):
+    run_collectstatic_in_setUp = False
+    hashed_file_path = hashed_file_path
+
+    def test_protocol_relative_url_ignored(self):
+        with override_settings(
+            STATICFILES_DIRS=[os.path.join(TEST_ROOT, "project", "static_url_slash")],
+            STATICFILES_FINDERS=["django.contrib.staticfiles.finders.FileSystemFinder"],
+        ):
+            self.run_collectstatic()
+        relpath = self.hashed_file_path("ignored.css")
+        self.assertEqual(relpath, "ignored.61707f5f4942.css")
+        with storage.staticfiles_storage.open(relpath) as relfile:
+            content = relfile.read()
+            self.assertIn(b"//foobar", content)
+
+
 @override_settings(
     STORAGES={
         **settings.STORAGES,
diff --git a/tests/syndication_tests/feeds.py b/tests/syndication_tests/feeds.py
index a35dc29e20..56e540c633 100644
--- a/tests/syndication_tests/feeds.py
+++ b/tests/syndication_tests/feeds.py
@@ -236,6 +236,13 @@ class TestGetObjectFeed(TestRss2Feed):
         return "Title: %s" % item.title
 
 
+class TestFeedWithStylesheets(TestRss2Feed):
+    stylesheets = [
+        "/stylesheet1.xsl",
+        feedgenerator.Stylesheet("/stylesheet2.xsl"),
+    ]
+
+
 class NaiveDatesFeed(TestAtomFeed):
     """
     A feed with naive (non-timezone-aware) dates.
diff --git a/tests/syndication_tests/tests.py b/tests/syndication_tests/tests.py
index a68ed879db..6403f7461a 100644
--- a/tests/syndication_tests/tests.py
+++ b/tests/syndication_tests/tests.py
@@ -4,12 +4,16 @@ from xml.dom import minidom
 from django.contrib.sites.models import Site
 from django.contrib.syndication import views
 from django.core.exceptions import ImproperlyConfigured
+from django.templatetags.static import static
 from django.test import TestCase, override_settings
 from django.test.utils import requires_tz_support
+from django.urls import reverse, reverse_lazy
 from django.utils import timezone
 from django.utils.feedgenerator import (
     Atom1Feed,
     Rss201rev2Feed,
+    Stylesheet,
+    SyndicationFeed,
     rfc2822_date,
     rfc3339_date,
 )
@@ -561,6 +565,125 @@ class SyndicationFeedTest(FeedTestCase):
                 doc = feed.writeString("utf-8")
                 self.assertIn(f'<{tag} href="https://feed.url.com" rel="self"/>', doc)
 
+    def test_stylesheets_none(self):
+        feed = Rss201rev2Feed(
+            title="test",
+            link="https://example.com",
+            description="test",
+            stylesheets=None,
+        )
+        self.assertNotIn("xml-stylesheet", feed.writeString("utf-8"))
+
+    def test_stylesheets(self):
+        testdata = [
+            # Plain strings.
+            ("/test.xsl", 'href="/test.xsl" type="text/xsl" media="screen"'),
+            ("/test.xslt", 'href="/test.xslt" type="text/xsl" media="screen"'),
+            ("/test.css", 'href="/test.css" type="text/css" media="screen"'),
+            ("/test", 'href="/test" media="screen"'),
+            (
+                "https://example.com/test.xsl",
+                'href="https://example.com/test.xsl" type="text/xsl" media="screen"',
+            ),
+            (
+                "https://example.com/test.css",
+                'href="https://example.com/test.css" type="text/css" media="screen"',
+            ),
+            (
+                "https://example.com/test",
+                'href="https://example.com/test" media="screen"',
+            ),
+            ("/♥.xsl", 'href="/%E2%99%A5.xsl" type="text/xsl" media="screen"'),
+            (
+                static("stylesheet.xsl"),
+                'href="/static/stylesheet.xsl" type="text/xsl" media="screen"',
+            ),
+            (
+                static("stylesheet.css"),
+                'href="/static/stylesheet.css" type="text/css" media="screen"',
+            ),
+            (static("stylesheet"), 'href="/static/stylesheet" media="screen"'),
+            (
+                reverse("syndication-xsl-stylesheet"),
+                'href="/syndication/stylesheet.xsl" type="text/xsl" media="screen"',
+            ),
+            (
+                reverse_lazy("syndication-xsl-stylesheet"),
+                'href="/syndication/stylesheet.xsl" type="text/xsl" media="screen"',
+            ),
+            # Stylesheet objects.
+            (
+                Stylesheet("/test.xsl"),
+                'href="/test.xsl" type="text/xsl" media="screen"',
+            ),
+            (Stylesheet("/test.xsl", mimetype=None), 'href="/test.xsl" media="screen"'),
+            (Stylesheet("/test.xsl", media=None), 'href="/test.xsl" type="text/xsl"'),
+            (Stylesheet("/test.xsl", mimetype=None, media=None), 'href="/test.xsl"'),
+            (
+                Stylesheet("/test.xsl", mimetype="text/xml"),
+                'href="/test.xsl" type="text/xml" media="screen"',
+            ),
+        ]
+        for stylesheet, expected in testdata:
+            feed = Rss201rev2Feed(
+                title="test",
+                link="https://example.com",
+                description="test",
+                stylesheets=[stylesheet],
+            )
+            doc = feed.writeString("utf-8")
+            with self.subTest(expected=expected):
+                self.assertIn(f"<?xml-stylesheet {expected}?>", doc)
+
+    def test_stylesheets_instructions_are_at_the_top(self):
+        response = self.client.get("/syndication/stylesheet/")
+        doc = minidom.parseString(response.content)
+        self.assertEqual(doc.childNodes[0].nodeName, "xml-stylesheet")
+        self.assertEqual(
+            doc.childNodes[0].data,
+            'href="/stylesheet1.xsl" type="text/xsl" media="screen"',
+        )
+        self.assertEqual(doc.childNodes[1].nodeName, "xml-stylesheet")
+        self.assertEqual(
+            doc.childNodes[1].data,
+            'href="/stylesheet2.xsl" type="text/xsl" media="screen"',
+        )
+
+    def test_stylesheets_typeerror_if_str_or_stylesheet(self):
+        for stylesheet, error_message in [
+            ("/stylesheet.xsl", "stylesheets should be a list, not <class 'str'>"),
+            (
+                Stylesheet("/stylesheet.xsl"),
+                "stylesheets should be a list, "
+                "not <class 'django.utils.feedgenerator.Stylesheet'>",
+            ),
+        ]:
+            args = ("title", "/link", "description")
+            with self.subTest(stylesheets=stylesheet):
+                self.assertRaisesMessage(
+                    TypeError,
+                    error_message,
+                    SyndicationFeed,
+                    *args,
+                    stylesheets=stylesheet,
+                )
+
+    def test_stylesheets_repr(self):
+        testdata = [
+            (Stylesheet("/test.xsl", mimetype=None), "('/test.xsl', None, 'screen')"),
+            (Stylesheet("/test.xsl", media=None), "('/test.xsl', 'text/xsl', None)"),
+            (
+                Stylesheet("/test.xsl", mimetype=None, media=None),
+                "('/test.xsl', None, None)",
+            ),
+            (
+                Stylesheet("/test.xsl", mimetype="text/xml"),
+                "('/test.xsl', 'text/xml', 'screen')",
+            ),
+        ]
+        for stylesheet, expected in testdata:
+            self.assertEqual(repr(stylesheet), expected)
+
     @requires_tz_support
     def test_feed_last_modified_time_naive_date(self):
         """
diff --git a/tests/syndication_tests/urls.py b/tests/syndication_tests/urls.py
index 50f673373e..bb1d3d990d 100644
--- a/tests/syndication_tests/urls.py
+++ b/tests/syndication_tests/urls.py
@@ -36,8 +36,14 @@ urlpatterns = [
     path("syndication/articles/", feeds.ArticlesFeed()),
     path("syndication/template/", feeds.TemplateFeed()),
     path("syndication/template_context/", feeds.TemplateContextFeed()),
+    path("syndication/stylesheet/", feeds.TestFeedWithStylesheets()),
     path("syndication/rss2/single-enclosure/", feeds.TestSingleEnclosureRSSFeed()),
     path("syndication/rss2/multiple-enclosure/", feeds.TestMultipleEnclosureRSSFeed()),
     path("syndication/atom/single-enclosure/", feeds.TestSingleEnclosureAtomFeed()),
     path("syndication/atom/multiple-enclosure/", feeds.TestMultipleEnclosureAtomFeed()),
+    path(
+        "syndication/stylesheet.xsl",
+        lambda request: None,
+        name="syndication-xsl-stylesheet",
+    ),
 ]
diff --git a/tests/template_tests/filter_tests/test_slice.py b/tests/template_tests/filter_tests/test_slice.py
index 5a5dd6b155..23257b1282 100644
--- a/tests/template_tests/filter_tests/test_slice.py
+++ b/tests/template_tests/filter_tests/test_slice.py
@@ -53,3 +53,6 @@ class FunctionTests(SimpleTestCase):
     def test_fail_silently(self):
         obj = object()
         self.assertEqual(slice_filter(obj, "0::2"), obj)
+
+    def test_empty_dict(self):
+        self.assertEqual(slice_filter({}, "1"), {})
diff --git a/tests/template_tests/filter_tests/test_truncatechars.py b/tests/template_tests/filter_tests/test_truncatechars.py
index a444125cf8..351b32f9de 100644
--- a/tests/template_tests/filter_tests/test_truncatechars.py
+++ b/tests/template_tests/filter_tests/test_truncatechars.py
@@ -22,3 +22,8 @@ class TruncatecharsTests(SimpleTestCase):
             "truncatechars03", {"a": "Testing, testing"}
         )
         self.assertEqual(output, "Testing, testing")
+
+    @setup({"truncatechars04": "{{ a|truncatechars:3 }}"})
+    def test_truncatechars04(self):
+        output = self.engine.render_to_string("truncatechars04", {"a": "abc"})
+        self.assertEqual(output, "abc")
diff --git a/tests/template_tests/filter_tests/test_truncatechars_html.py b/tests/template_tests/filter_tests/test_truncatechars_html.py
index 6c5fc3c883..881290d47d 100644
--- a/tests/template_tests/filter_tests/test_truncatechars_html.py
+++ b/tests/template_tests/filter_tests/test_truncatechars_html.py
@@ -8,7 +8,7 @@ class FunctionTests(SimpleTestCase):
             truncatechars_html(
                 '<p>one <a href="#">two - three <br>four</a> five</p>', 0
             ),
-            "…",
+            "",
         )
 
     def test_truncate(self):
diff --git a/tests/template_tests/filter_tests/test_truncatewords_html.py b/tests/template_tests/filter_tests/test_truncatewords_html.py
index 32b7c81a76..0cf41d83ae 100644
--- a/tests/template_tests/filter_tests/test_truncatewords_html.py
+++ b/tests/template_tests/filter_tests/test_truncatewords_html.py
@@ -24,7 +24,7 @@ class FunctionTests(SimpleTestCase):
             truncatewords_html(
                 '<p>one <a href="#">two - three <br>four</a> five</p>', 4
             ),
-            '<p>one <a href="#">two - three …</a></p>',
+            '<p>one <a href="#">two - three <br> …</a></p>',
         )
 
     def test_truncate3(self):
@@ -32,7 +32,7 @@ class FunctionTests(SimpleTestCase):
             truncatewords_html(
                 '<p>one <a href="#">two - three <br>four</a> five</p>', 5
             ),
-            '<p>one <a href="#">two - three <br>four …</a></p>',
+            '<p>one <a href="#">two - three <br>four</a> …</p>',
         )
 
     def test_truncate4(self):
@@ -53,7 +53,7 @@ class FunctionTests(SimpleTestCase):
             truncatewords_html(
                 "<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo est&aacute;?</i>", 3
             ),
-            "<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo …</i>",
+            "<i>Buenos días! ¿Cómo …</i>",
         )
 
     def test_invalid_arg(self):
diff --git a/tests/template_tests/test_context.py b/tests/template_tests/test_context.py
index 7420bb4c36..6d8ee7a6e6 100644
--- a/tests/template_tests/test_context.py
+++ b/tests/template_tests/test_context.py
@@ -158,6 +158,17 @@ class ContextTests(SimpleTestCase):
             },
         )
 
+    def test_flatten_context_with_context_copy(self):
+        ctx1 = Context({"a": 2})
+        ctx2 = ctx1.new(Context({"b": 4}))
+        self.assertEqual(
+            ctx2.dicts, [{"True": True, "False": False, "None": None}, {"b": 4}]
+        )
+        self.assertEqual(
+            ctx2.flatten(),
+            {"False": False, "None": None, "True": True, "b": 4},
+        )
+
     def test_context_comparable(self):
         """
         #21765 -- equality comparison should work
diff --git a/tests/test_runner/test_parallel.py b/tests/test_runner/test_parallel.py
index e83f53bf4e..73ef480cc1 100644
--- a/tests/test_runner/test_parallel.py
+++ b/tests/test_runner/test_parallel.py
@@ -51,6 +51,13 @@ class SampleFailingSubtest(SimpleTestCase):
             with self.subTest(index=i):
                 self.assertEqual(i, 1)
 
+    # This method name doesn't begin with "test" to prevent test discovery
+    # from seeing it.
+    def pickle_error_test(self):
+        with self.subTest("TypeError: cannot pickle memoryview object"):
+            self.x = memoryview(b"")
+            self.fail("expected failure")
+
 
 class RemoteTestResultTest(SimpleTestCase):
     def _test_error_exc_info(self):
@@ -106,6 +113,17 @@ class RemoteTestResultTest(SimpleTestCase):
         with self.assertRaisesMessage(TypeError, msg):
             result._confirm_picklable(not_unpicklable_error)
 
+    @unittest.skipUnless(tblib is not None, "requires tblib to be installed")
+    def test_unpicklable_subtest(self):
+        result = RemoteTestResult()
+        subtest_test = SampleFailingSubtest(methodName="pickle_error_test")
+        subtest_test.run(result=result)
+
+        events = result.events
+        subtest_event = events[1]
+        assertion_error = subtest_event[3]
+        self.assertEqual(str(assertion_error[1]), "expected failure")
+
     @unittest.skipUnless(tblib is not None, "requires tblib to be installed")
     def test_add_failing_subtests(self):
         """
diff --git a/tests/test_utils/test_testcase.py b/tests/test_utils/test_testcase.py
index eb6ca80036..efca01e29e 100644
--- a/tests/test_utils/test_testcase.py
+++ b/tests/test_utils/test_testcase.py
@@ -1,12 +1,34 @@
+import pickle
 from functools import wraps
 
 from django.db import IntegrityError, connections, transaction
 from django.test import TestCase, skipUnlessDBFeature
-from django.test.testcases import DatabaseOperationForbidden, TestData
+from django.test.testcases import (
+    DatabaseOperationForbidden,
+    SimpleTestCase,
+    TestData,
+    is_pickable,
+)
 
 from .models import Car, Person, PossessedCar
 
 
+class UnpicklableObject:
+    def __getstate__(self):
+        raise pickle.PickleError("cannot be pickled for testing reasons")
+
+
+class TestSimpleTestCase(SimpleTestCase):
+    def test_is_picklable_with_non_picklable_properties(self):
+        """ParallelTestSuite requires that all TestCases are picklable."""
+        self.non_picklable = lambda: 0
+        self.assertEqual(self, pickle.loads(pickle.dumps(self)))
+
+    def test_is_picklable_with_non_picklable_object(self):
+        unpicklable_obj = UnpicklableObject()
+        self.assertEqual(is_pickable(unpicklable_obj), False)
+
+
 class TestTestCase(TestCase):
     @skipUnlessDBFeature("can_defer_constraint_checks")
     @skipUnlessDBFeature("supports_foreign_keys")
diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py
index e001e119ee..cd64c087c4 100644
--- a/tests/test_utils/tests.py
+++ b/tests/test_utils/tests.py
@@ -184,9 +184,11 @@ class SkippingClassTestCase(TransactionTestCase):
         except unittest.SkipTest:
             self.fail("SkipTest should not be raised here.")
         result = unittest.TextTestRunner(stream=StringIO()).run(test_suite)
-        # PY312: Python 3.12.1+ no longer includes skipped tests in the number
-        # of running tests.
-        self.assertEqual(result.testsRun, 1 if sys.version_info >= (3, 12, 1) else 3)
+        # PY312: Python 3.12.1 does not include skipped tests in the number of
+        # running tests.
+        self.assertEqual(
+            result.testsRun, 1 if sys.version_info[:3] == (3, 12, 1) else 3
+        )
         self.assertEqual(len(result.skipped), 2)
         self.assertEqual(result.skipped[0][1], "Database has feature(s) __class__")
         self.assertEqual(result.skipped[1][1], "Database has feature(s) __class__")
@@ -2130,12 +2132,46 @@ class AllowedDatabaseQueriesTests(SimpleTestCase):
         next(Car.objects.iterator(), None)
 
     def test_allowed_threaded_database_queries(self):
+        connections_dict = {}
+
         def thread_func():
+            # Passing django.db.connection between threads doesn't work while
+            # connections[DEFAULT_DB_ALIAS] does.
+            from django.db import connections
+
+            connection = connections["default"]
+
             next(Car.objects.iterator(), None)
 
-        t = threading.Thread(target=thread_func)
-        t.start()
-        t.join()
+            # Allow thread sharing so the connection can be closed by the main
+            # thread.
+            connection.inc_thread_sharing()
+            connections_dict[id(connection)] = connection
+
+        try:
+            t = threading.Thread(target=thread_func)
+            t.start()
+            t.join()
+        finally:
+            # Finish by closing the connections opened by the other threads
+            # (the connection opened in the main thread will automatically be
+            # closed on teardown).
+            for conn in connections_dict.values():
+                if conn is not connection and conn.allow_thread_sharing:
+                    conn.validate_thread_sharing()
+                    conn._close()
+                    conn.dec_thread_sharing()
+
+    def test_allowed_database_copy_queries(self):
+        new_connection = connection.copy("dynamic_connection")
+        try:
+            with new_connection.cursor() as cursor:
+                sql = f"SELECT 1{new_connection.features.bare_select_suffix}"
+                cursor.execute(sql)
+                self.assertEqual(cursor.fetchone()[0], 1)
+        finally:
+            new_connection.validate_thread_sharing()
+            new_connection._close()
 
 
 class DatabaseAliasTests(SimpleTestCase):
diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py
index f8d73fdb4a..78b71fe325 100644
--- a/tests/urlpatterns/tests.py
+++ b/tests/urlpatterns/tests.py
@@ -4,10 +4,20 @@ import uuid
 from django.core.exceptions import ImproperlyConfigured
 from django.test import SimpleTestCase
 from django.test.utils import override_settings
-from django.urls import NoReverseMatch, Resolver404, path, re_path, resolve, reverse
+from django.urls import (
+    NoReverseMatch,
+    Resolver404,
+    path,
+    re_path,
+    register_converter,
+    resolve,
+    reverse,
+)
+from django.urls.converters import REGISTERED_CONVERTERS, IntConverter
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.views import View
 
-from .converters import DynamicConverter
+from .converters import Base64Converter, DynamicConverter
 from .views import empty_view
 
 included_kwargs = {"base": b"hello", "value": b"world"}
@@ -193,6 +203,35 @@ class SimplifiedURLTests(SimpleTestCase):
         with self.assertRaisesMessage(ImproperlyConfigured, msg):
             path("foo/<nonexistent:var>/", empty_view)
 
+    def test_warning_override_default_converter(self):
+        # RemovedInDjango60Warning: when the deprecation ends, replace with
+        # msg = "Converter 'int' is already registered."
+        # with self.assertRaisesMessage(ValueError, msg):
+        msg = (
+            "Converter 'int' is already registered. Support for overriding registered "
+            "converters is deprecated and will be removed in Django 6.0."
+        )
+        try:
+            with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+                register_converter(IntConverter, "int")
+        finally:
+            REGISTERED_CONVERTERS.pop("int", None)
+
+    def test_warning_override_converter(self):
+        # RemovedInDjango60Warning: when the deprecation ends, replace with
+        # msg = "Converter 'base64' is already registered."
+        # with self.assertRaisesMessage(ValueError, msg):
+        msg = (
+            "Converter 'base64' is already registered. Support for overriding "
+            "registered converters is deprecated and will be removed in Django 6.0."
+        )
+        try:
+            with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+                register_converter(Base64Converter, "base64")
+                register_converter(Base64Converter, "base64")
+        finally:
+            REGISTERED_CONVERTERS.pop("base64", None)
+
     def test_invalid_view(self):
         msg = "view must be a callable or a list/tuple in the case of include()."
         with self.assertRaisesMessage(TypeError, msg):
@@ -207,14 +246,12 @@ class SimplifiedURLTests(SimpleTestCase):
             path("foo", EmptyCBV())
 
     def test_whitespace_in_route(self):
-        msg = (
-            "URL route 'space/<int:num>/extra/<str:%stest>' cannot contain "
-            "whitespace in angle brackets <…>"
-        )
+        msg = "URL route %r cannot contain whitespace in angle brackets <…>"
         for whitespace in string.whitespace:
             with self.subTest(repr(whitespace)):
-                with self.assertRaisesMessage(ImproperlyConfigured, msg % whitespace):
-                    path("space/<int:num>/extra/<str:%stest>" % whitespace, empty_view)
+                route = "space/<int:num>/extra/<str:%stest>" % whitespace
+                with self.assertRaisesMessage(ImproperlyConfigured, msg % route):
+                    path(route, empty_view)
         # Whitespaces are valid in paths.
         p = path("space%s/<int:num>/" % string.whitespace, empty_view)
         match = p.resolve("space%s/1/" % string.whitespace)
diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py
index 8384f55b3c..5843382a8c 100644
--- a/tests/urlpatterns_reverse/tests.py
+++ b/tests/urlpatterns_reverse/tests.py
@@ -1456,7 +1456,7 @@ class RequestURLconfTests(SimpleTestCase):
 
 
 class ErrorHandlerResolutionTests(SimpleTestCase):
-    """Tests for handler400, handler404 and handler500"""
+    """Tests for handler400, handler403, handler404 and handler500"""
 
     def setUp(self):
         urlconf = "urlpatterns_reverse.urls_error_handlers"
@@ -1465,12 +1465,12 @@ class ErrorHandlerResolutionTests(SimpleTestCase):
         self.callable_resolver = URLResolver(RegexPattern(r"^$"), urlconf_callables)
 
     def test_named_handlers(self):
-        for code in [400, 404, 500]:
+        for code in [400, 403, 404, 500]:
             with self.subTest(code=code):
                 self.assertEqual(self.resolver.resolve_error_handler(code), empty_view)
 
     def test_callable_handlers(self):
-        for code in [400, 404, 500]:
+        for code in [400, 403, 404, 500]:
             with self.subTest(code=code):
                 self.assertEqual(
                     self.callable_resolver.resolve_error_handler(code), empty_view
diff --git a/tests/urlpatterns_reverse/urls_error_handlers.py b/tests/urlpatterns_reverse/urls_error_handlers.py
index 7261a97e07..d483864f4b 100644
--- a/tests/urlpatterns_reverse/urls_error_handlers.py
+++ b/tests/urlpatterns_reverse/urls_error_handlers.py
@@ -3,5 +3,6 @@
 urlpatterns = []
 
 handler400 = "urlpatterns_reverse.views.empty_view"
+handler403 = "urlpatterns_reverse.views.empty_view"
 handler404 = "urlpatterns_reverse.views.empty_view"
 handler500 = "urlpatterns_reverse.views.empty_view"
diff --git a/tests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/urlpatterns_reverse/urls_error_handlers_callables.py
index 4a8d35116e..614fc460fc 100644
--- a/tests/urlpatterns_reverse/urls_error_handlers_callables.py
+++ b/tests/urlpatterns_reverse/urls_error_handlers_callables.py
@@ -5,5 +5,6 @@ from .views import empty_view
 urlpatterns = []
 
 handler400 = empty_view
+handler403 = empty_view
 handler404 = empty_view
 handler500 = empty_view
diff --git a/tests/utils_tests/test_feedgenerator.py b/tests/utils_tests/test_feedgenerator.py
index ee15b6e928..e5ceafb8fa 100644
--- a/tests/utils_tests/test_feedgenerator.py
+++ b/tests/utils_tests/test_feedgenerator.py
@@ -1,7 +1,9 @@
 import datetime
+from unittest import mock
 
 from django.test import SimpleTestCase
 from django.utils import feedgenerator
+from django.utils.functional import SimpleLazyObject
 from django.utils.timezone import get_fixed_timezone
 
 
@@ -148,3 +150,12 @@ class FeedgeneratorTests(SimpleTestCase):
                     rss_feed.latest_post_date().tzinfo,
                     datetime.timezone.utc,
                 )
+
+    def test_stylesheet_keeps_lazy_urls(self):
+        m = mock.Mock(return_value="test.css")
+        stylesheet = feedgenerator.Stylesheet(SimpleLazyObject(m))
+        m.assert_not_called()
+        self.assertEqual(
+            str(stylesheet), 'href="test.css" type="text/css" media="screen"'
+        )
+        m.assert_called_once()
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
index 77e637ae6c..ab2cfb3f7c 100644
--- a/tests/utils_tests/test_text.py
+++ b/tests/utils_tests/test_text.py
@@ -89,30 +89,118 @@ class TestUtilsText(SimpleTestCase):
 
         # Make a best effort to shorten to the desired length, but requesting
         # a length shorter than the ellipsis shouldn't break
-        self.assertEqual("…", text.Truncator("asdf").chars(0))
+        self.assertEqual("...", text.Truncator("asdf").chars(1, truncate="..."))
         # lazy strings are handled correctly
         self.assertEqual(
             text.Truncator(lazystr("The quick brown fox")).chars(10), "The quick…"
         )
 
+    def test_truncate_chars_html(self):
+        truncator = text.Truncator(
+            '<p id="par"><strong><em>The quick brown fox jumped over the lazy dog.</em>'
+            "</strong></p>"
+        )
+        self.assertEqual(
+            '<p id="par"><strong><em>The quick brown fox jumped over the lazy dog.</em>'
+            "</strong></p>",
+            truncator.chars(80, html=True),
+        )
+        self.assertEqual(
+            '<p id="par"><strong><em>The quick brown fox jumped over the lazy dog.</em>'
+            "</strong></p>",
+            truncator.chars(46, html=True),
+        )
+        self.assertEqual(
+            '<p id="par"><strong><em>The quick brown fox jumped over the lazy dog…</em>'
+            "</strong></p>",
+            truncator.chars(45, html=True),
+        )
+        self.assertEqual(
+            '<p id="par"><strong><em>The quick…</em></strong></p>',
+            truncator.chars(10, html=True),
+        )
+        self.assertEqual(
+            '<p id="par"><strong><em>…</em></strong></p>',
+            truncator.chars(1, html=True),
+        )
+        self.assertEqual("", truncator.chars(0, html=True))
+        self.assertEqual("", truncator.chars(-1, html=True))
+        self.assertEqual(
+            '<p id="par"><strong><em>The qu....</em></strong></p>',
+            truncator.chars(10, "....", html=True),
+        )
+        self.assertEqual(
+            '<p id="par"><strong><em>The quick </em></strong></p>',
+            truncator.chars(10, "", html=True),
+        )
+        truncator = text.Truncator("foo</p>")
+        self.assertEqual("foo</p>", truncator.chars(5, html=True))
+
     @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
     def test_truncate_chars_html_size_limit(self):
         max_len = text.Truncator.MAX_LENGTH_HTML
         bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
         valid_html = "<p>Joel is a slug</p>"  # 14 chars
         perf_test_values = [
-            ("</a" + "\t" * (max_len - 6) + "//>", None),
-            ("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * 6 + "…"),
-            ("&" * bigger_len, "&" * 9 + "…"),
-            ("_X<<<<<<<<<<<>", None),
+            ("</a" + "\t" * (max_len - 6) + "//>", "</a>"),
+            ("</p" + "\t" * bigger_len + "//>", "</p>"),
+            ("&" * bigger_len, ""),
+            ("_X<<<<<<<<<<<>", "_X&lt;&lt;&lt;&lt;&lt;&lt;&lt;…"),
             (valid_html * bigger_len, "<p>Joel is a…</p>"),  # 10 chars
         ]
         for value, expected in perf_test_values:
             with self.subTest(value=value):
                 truncator = text.Truncator(value)
-                self.assertEqual(
-                    expected if expected else value, truncator.chars(10, html=True)
-                )
+                self.assertEqual(expected, truncator.chars(10, html=True))
+
+    def test_truncate_chars_html_with_newline_inside_tag(self):
+        truncator = text.Truncator(
+            '<p>The quick <a href="xyz.html"\n id="mylink">brown fox</a> jumped over '
+            "the lazy dog.</p>"
+        )
+        self.assertEqual(
+            '<p>The quick <a href="xyz.html"\n id="mylink">brow…</a></p>',
+            truncator.chars(15, html=True),
+        )
+        self.assertEqual(
+            "<p>Th…</p>",
+            truncator.chars(3, html=True),
+        )
+
+    def test_truncate_chars_html_with_void_elements(self):
+        truncator = text.Truncator(
+            "<br/>The <hr />quick brown fox jumped over the lazy dog."
+        )
+        self.assertEqual("<br/>The <hr />quick brown…", truncator.chars(16, html=True))
+        truncator = text.Truncator(
+            "<br>The <hr/>quick <em>brown fox</em> jumped over the lazy dog."
+        )
+        self.assertEqual(
+            "<br>The <hr/>quick <em>brown…</em>", truncator.chars(16, html=True)
+        )
+        self.assertEqual("<br>The <hr/>q…", truncator.chars(6, html=True))
+        self.assertEqual("<br>The <hr/>…", truncator.chars(5, html=True))
+        self.assertEqual("<br>The…", truncator.chars(4, html=True))
+        self.assertEqual("<br>Th…", truncator.chars(3, html=True))
+
+    def test_truncate_chars_html_with_html_entities(self):
+        truncator = text.Truncator(
+            "<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo est&aacute;?</i>"
+        )
+        self.assertEqual(
+            "<i>Buenos días! ¿Cómo está?</i>",
+            truncator.chars(40, html=True),
+        )
+        self.assertEqual(
+            "<i>Buenos días…</i>",
+            truncator.chars(12, html=True),
+        )
+        self.assertEqual(
+            "<i>Buenos días! ¿Cómo está…</i>",
+            truncator.chars(24, html=True),
+        )
+        truncator = text.Truncator("<p>I &lt;3 python, what about you?</p>")
+        self.assertEqual("<p>I &lt;3 python, wh…</p>", truncator.chars(16, html=True))
 
     def test_truncate_words(self):
         truncator = text.Truncator("The quick brown fox jumped over the lazy dog.")
@@ -126,6 +214,8 @@ class TestUtilsText(SimpleTestCase):
             lazystr("The quick brown fox jumped over the lazy dog.")
         )
         self.assertEqual("The quick brown fox…", truncator.words(4))
+        self.assertEqual("", truncator.words(0))
+        self.assertEqual("", truncator.words(-1))
 
     def test_truncate_html_words(self):
         truncator = text.Truncator(
@@ -141,6 +231,10 @@ class TestUtilsText(SimpleTestCase):
             '<p id="par"><strong><em>The quick brown fox…</em></strong></p>',
             truncator.words(4, html=True),
         )
+        self.assertEqual(
+            "",
+            truncator.words(0, html=True),
+        )
         self.assertEqual(
             '<p id="par"><strong><em>The quick brown fox....</em></strong></p>',
             truncator.words(4, "....", html=True),
@@ -150,6 +244,14 @@ class TestUtilsText(SimpleTestCase):
             truncator.words(4, "", html=True),
         )
 
+        truncator = text.Truncator(
+            "<p>The  quick \t brown fox jumped over the lazy dog.</p>"
+        )
+        self.assertEqual(
+            "<p>The quick brown fox…</p>",
+            truncator.words(4, html=True),
+        )
+
         # Test with new line inside tag
         truncator = text.Truncator(
             '<p>The quick <a href="xyz.html"\n id="mylink">brown fox</a> jumped over '
@@ -159,6 +261,10 @@ class TestUtilsText(SimpleTestCase):
             '<p>The quick <a href="xyz.html"\n id="mylink">brown…</a></p>',
             truncator.words(3, html=True),
         )
+        self.assertEqual(
+            "<p>The…</p>",
+            truncator.words(1, html=True),
+        )
 
         # Test self-closing tags
         truncator = text.Truncator(
@@ -177,31 +283,59 @@ class TestUtilsText(SimpleTestCase):
             "<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo est&aacute;?</i>"
         )
         self.assertEqual(
-            "<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo…</i>",
+            "<i>Buenos días! ¿Cómo…</i>",
             truncator.words(3, html=True),
         )
         truncator = text.Truncator("<p>I &lt;3 python, what about you?</p>")
         self.assertEqual("<p>I &lt;3 python,…</p>", truncator.words(3, html=True))
 
+        truncator = text.Truncator("foo</p>")
+        self.assertEqual("foo</p>", truncator.words(3, html=True))
+
+        # Only open brackets.
+        truncator = text.Truncator("<" * 60_000)
+        self.assertEqual(truncator.words(1, html=True), "&lt;…")
+
+        # Tags with special chars in attrs.
+        truncator = text.Truncator(
+            """<i style="margin: 5%; font: *;">Hello, my dear lady!</i>"""
+        )
+        self.assertEqual(
+            """<i style="margin: 5%; font: *;">Hello, my dear…</i>""",
+            truncator.words(3, html=True),
+        )
+
+        # Tags with special non-latin chars in attrs.
+        truncator = text.Truncator("""<p data-x="א">Hello, my dear lady!</p>""")
+        self.assertEqual(
+            """<p data-x="א">Hello, my dear…</p>""",
+            truncator.words(3, html=True),
+        )
+
+        # Misplaced brackets.
+        truncator = text.Truncator("hello >< world")
+        self.assertEqual(truncator.words(1, html=True), "hello…")
+        self.assertEqual(truncator.words(2, html=True), "hello &gt;…")
+        self.assertEqual(truncator.words(3, html=True), "hello &gt;&lt;…")
+        self.assertEqual(truncator.words(4, html=True), "hello &gt;&lt; world")
+
     @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
     def test_truncate_words_html_size_limit(self):
         max_len = text.Truncator.MAX_LENGTH_HTML
         bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
         valid_html = "<p>Joel is a slug</p>"  # 4 words
         perf_test_values = [
-            ("</a" + "\t" * (max_len - 6) + "//>", None),
-            ("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * (max_len - 3) + "…"),
-            ("&" * max_len, None),  # no change
-            ("&" * bigger_len, "&" * max_len + "…"),
-            ("_X<<<<<<<<<<<>", None),
+            ("</a" + "\t" * (max_len - 6) + "//>", "</a>"),
+            ("</p" + "\t" * bigger_len + "//>", "</p>"),
+            ("&" * max_len, ""),
+            ("&" * bigger_len, ""),
+            ("_X<<<<<<<<<<<>", "_X&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&gt;"),
             (valid_html * bigger_len, valid_html * 12 + "<p>Joel is…</p>"),  # 50 words
         ]
         for value, expected in perf_test_values:
             with self.subTest(value=value):
                 truncator = text.Truncator(value)
-                self.assertEqual(
-                    expected if expected else value, truncator.words(50, html=True)
-                )
+                self.assertEqual(expected, truncator.words(50, html=True))
 
     def test_wrap(self):
         digits = "1234 67 9"
diff --git a/tests/validation/models.py b/tests/validation/models.py
index 612a8dd63a..f6b1e0cd62 100644
--- a/tests/validation/models.py
+++ b/tests/validation/models.py
@@ -173,7 +173,7 @@ class Product(models.Model):
         }
         constraints = [
             models.CheckConstraint(
-                check=models.Q(price__gt=models.F("discounted_price")),
+                condition=models.Q(price__gt=models.F("discounted_price")),
                 name="price_gt_discounted_price_validation",
             ),
         ]
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index 5376517a4a..ba1db5ea46 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -9,6 +9,7 @@ from django.core.files.base import ContentFile
 from django.core.validators import (
     BaseValidator,
     DecimalValidator,
+    DomainNameValidator,
     EmailValidator,
     FileExtensionValidator,
     MaxLengthValidator,
@@ -21,6 +22,7 @@ from django.core.validators import (
     URLValidator,
     int_list_validator,
     validate_comma_separated_integer_list,
+    validate_domain_name,
     validate_email,
     validate_image_file_extension,
     validate_integer,
@@ -618,6 +620,38 @@ TEST_DATA = [
     (ProhibitNullCharactersValidator(), "\x00something", ValidationError),
     (ProhibitNullCharactersValidator(), "something", None),
     (ProhibitNullCharactersValidator(), None, None),
+    (validate_domain_name, "000000.org", None),
+    (validate_domain_name, "python.org", None),
+    (validate_domain_name, "python.co.uk", None),
+    (validate_domain_name, "python.tk", None),
+    (validate_domain_name, "domain.with.idn.tld.उदाहरण.परीक्ष", None),
+    (validate_domain_name, "ıçğü.com", None),
+    (validate_domain_name, "xn--7ca6byfyc.com", None),
+    (validate_domain_name, "hg.python.org", None),
+    (validate_domain_name, "python.xyz", None),
+    (validate_domain_name, "djangoproject.com", None),
+    (validate_domain_name, "DJANGOPROJECT.COM", None),
+    (validate_domain_name, "spam.eggs", None),
+    (validate_domain_name, "python-python.com", None),
+    (validate_domain_name, "python.name.uk", None),
+    (validate_domain_name, "python.tips", None),
+    (validate_domain_name, "http://例子.测试", None),
+    (validate_domain_name, "http://dashinpunytld.xn---c", None),
+    (validate_domain_name, "python..org", ValidationError),
+    (validate_domain_name, "python-.org", ValidationError),
+    (validate_domain_name, "too-long-name." * 20 + "com", ValidationError),
+    (validate_domain_name, "stupid-name试", ValidationError),
+    (validate_domain_name, "255.0.0.0", ValidationError),
+    (validate_domain_name, "fe80::1", ValidationError),
+    (validate_domain_name, "1:2:3:4:5:6:7:8", ValidationError),
+    (DomainNameValidator(accept_idna=False), "non-idna-domain-name-passes.com", None),
+    (
+        DomainNameValidator(accept_idna=False),
+        "domain.with.idn.tld.उदाहरण.परीक्ष",
+        ValidationError,
+    ),
+    (DomainNameValidator(accept_idna=False), "ıçğü.com", ValidationError),
+    (DomainNameValidator(accept_idna=False), "not-domain-name", ValidationError),
 ]
 
 # Add valid and invalid URL tests.
@@ -847,3 +881,25 @@ class TestValidatorEquality(TestCase):
             ProhibitNullCharactersValidator(message="message", code="code1"),
             ProhibitNullCharactersValidator(message="message", code="code2"),
         )
+
+    def test_domain_name_equality(self):
+        self.assertEqual(
+            DomainNameValidator(),
+            DomainNameValidator(),
+        )
+        self.assertNotEqual(
+            DomainNameValidator(),
+            EmailValidator(),
+        )
+        self.assertNotEqual(
+            DomainNameValidator(),
+            DomainNameValidator(code="custom_code"),
+        )
+        self.assertEqual(
+            DomainNameValidator(message="custom error message"),
+            DomainNameValidator(message="custom error message"),
+        )
+        self.assertNotEqual(
+            DomainNameValidator(message="custom error message"),
+            DomainNameValidator(message="custom error message", code="custom_code"),
+        )
diff --git a/tests/view_tests/tests/test_csrf.py b/tests/view_tests/tests/test_csrf.py
index af16ffd740..2d530cc586 100644
--- a/tests/view_tests/tests/test_csrf.py
+++ b/tests/view_tests/tests/test_csrf.py
@@ -132,3 +132,15 @@ class CsrfViewTests(SimpleTestCase):
         with mock.patch.object(Path, "open") as m:
             csrf_failure(mock.MagicMock(), mock.Mock())
             m.assert_called_once_with(encoding="utf-8")
+
+    @override_settings(DEBUG=True)
+    @mock.patch("django.views.csrf.get_docs_version", return_value="4.2")
+    def test_doc_links(self, mocked_get_complete_version):
+        response = self.client.post("/")
+        self.assertContains(response, "Forbidden", status_code=403)
+        self.assertNotContains(
+            response, "https://docs.djangoproject.com/en/dev/", status_code=403
+        )
+        self.assertContains(
+            response, "https://docs.djangoproject.com/en/4.2/", status_code=403
+        )
diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py
index af7f2f9f8a..7a798dc8e6 100644
--- a/tests/view_tests/tests/test_debug.py
+++ b/tests/view_tests/tests/test_debug.py
@@ -1542,7 +1542,7 @@ class ExceptionReportTestMixin:
             self.assertNotIn("worcestershire", body_plain)
 
             # Frames vars are shown in html email reports.
-            body_html = str(email.alternatives[0][0])
+            body_html = str(email.alternatives[0].content)
             self.assertIn("cooked_eggs", body_html)
             self.assertIn("scrambled", body_html)
             self.assertIn("sauce", body_html)
@@ -1578,7 +1578,7 @@ class ExceptionReportTestMixin:
             self.assertNotIn("worcestershire", body_plain)
 
             # Frames vars are shown in html email reports.
-            body_html = str(email.alternatives[0][0])
+            body_html = str(email.alternatives[0].content)
             self.assertIn("cooked_eggs", body_html)
             self.assertIn("scrambled", body_html)
             self.assertIn("sauce", body_html)
diff --git a/tests/view_tests/tests/test_defaults.py b/tests/view_tests/tests/test_defaults.py
index 415a9a8c67..66bc1da168 100644
--- a/tests/view_tests/tests/test_defaults.py
+++ b/tests/view_tests/tests/test_defaults.py
@@ -123,7 +123,7 @@ class DefaultsTests(TestCase):
     )
     def test_custom_bad_request_template(self):
         response = self.client.get("/raises400/")
-        self.assertIs(response.wsgi_request, response.context[-1].request)
+        self.assertIs(response.wsgi_request, response.context.request)
 
     @override_settings(
         TEMPLATES=[