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 + + +ticket-XXXXX + +# Branch description +Provide a concise overview of the issue or rationale behind the proposed changes. + +# Checklist +- [ ] This PR targets the `main` branch. +- [ ] 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 + Aaron Linville Aaron Swartz Aaron T. Myers Abeer Upadhyay @@ -20,6 +21,7 @@ answer newbie questions, and generally made Django that much better: Adam Johnson Adam Malinowski Adam Vandenberg + Adam Zapletal Ade Lee Adiyat Mubarak Adnan Umer @@ -44,6 +46,8 @@ answer newbie questions, and generally made Django that much better: Albert Wang Alcides Fonseca Aldian Fazrihady + Alejandro García Ruiz de Oteiza + Aleksander Milinkevich Aleksandra Sendecka Aleksi Häkli Alex Dutton @@ -318,6 +322,7 @@ answer newbie questions, and generally made Django that much better: Erik Karulf Erik Romijn eriks@win.tue.nl + Erin Kelly Erwin Junge Esdras Beleza Espen Grindhaug @@ -325,6 +330,7 @@ answer newbie questions, and generally made Django that much better: Eugene Lazutkin Evan Grim Fabian Büchler + Fabian Braun Fabrice Aneche Faishal Manzar Farhaan Bukhsh @@ -369,6 +375,7 @@ answer newbie questions, and generally made Django that much better: George Karpenkov George Song George Vilches + George Y. Kussumoto Georg "Hugo" Bauer Georgi Stanojevski Gerardo Orozco @@ -415,7 +422,6 @@ answer newbie questions, and generally made Django that much better: Iacopo Spalletti Ian A Wilson Ian Clelland - Ian G. Kelly Ian Holsman Ian Lee Ibon @@ -504,6 +510,7 @@ answer newbie questions, and generally made Django that much better: Joe Topjian Johan C. Stöver Johann Queuniet + Johannes Westphal john@calixto.net John D'Agostino John D'Ambrosio @@ -560,6 +567,7 @@ answer newbie questions, and generally made Django that much better: Karderio Karen Tracey Karol Sikora + Kasun Herath Katherine “Kati” Michel Kathryn Killebrew Katie Miller @@ -761,6 +769,7 @@ answer newbie questions, and generally made Django that much better: Nicolas Noé Nikita Marchant Nikita Sobolev + Nina Menezes Niran Babalola Nis Jørgensen Nowell Strite @@ -918,6 +927,7 @@ answer newbie questions, and generally made Django that much better: Sergey Fedoseev Sergey Kolosov Seth Hill + Shafiya Adzhani Shai Berger Shannon -jj Behrens Shawn Milochik 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 \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__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__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 \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 %(username)s." msgstr "" +#: contrib/admin/templates/admin/auth/user/change_password.html:35 +msgid "" +"This action will enable 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 \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 '...//add/' or // '...///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 @@

{% translate "Enter a username and password." %}

{% endif %} {% endblock %} +{% block extrahead %} + {{ block.super }} + +{% endblock %} +{% block admin_change_form_document_ready %} + {{ block.super }} + +{% 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 }}{% endblock %} +{% block extrastyle %} + {{ block.super }} + + +{% endblock %} {% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.model_name }} change-form{% endblock %} {% if not is_popup %} {% block breadcrumbs %} @@ -11,7 +15,7 @@ › {{ opts.app_config.verbose_name }}{{ opts.verbose_name_plural|capfirst }}{{ original|truncatewords:"18" }} -› {% translate 'Change password' %} +› {% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %} {% endblock %} {% endif %} @@ -27,10 +31,23 @@ {% endif %}

{% blocktranslate with username=original %}Enter a new password for the user {{ username }}.{% endblocktranslate %}

+{% if not form.user.has_usable_password %} +

{% blocktranslate %}This action will enable password-based authentication for this user.{% endblocktranslate %}

+{% endif %}
+ {{ form.usable_password.errors }} +
{{ form.usable_password.label_tag }} {{ form.usable_password }}
+ {% if form.usable_password.help_text %} +
+

{{ form.usable_password.help_text|safe }}

+
+ {% endif %} +
+ +
{{ form.password1.errors }}
{{ form.password1.label_tag }} {{ form.password1 }}
{% if form.password1.help_text %} @@ -38,7 +55,7 @@ {% endif %}
-
+
{{ form.password2.errors }}
{{ form.password2.label_tag }} {{ form.password2 }}
{% if form.password2.help_text %} @@ -49,9 +66,15 @@
- + {% if form.user.has_usable_password %} + + + {% else %} + + {% endif %}
+ {% 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 @@ +{% block extrabody %}{% endblock extrabody %} 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 %}