From abb9c04bd6f4a1be893d40b2e1afe098e1beecf0 Mon Sep 17 00:00:00 2001 From: jgoneit Date: Mon, 17 Aug 2026 14:55:28 +0900 Subject: [PATCH 1/4] Fixed #36629 -- Added missing "Choose all" and "Remove all" buttons in admin m2m filter_vertical widget. Co-authored-by: Md. Miftahul Kabir --- .../admin/static/admin/css/responsive.css | 4 -- .../admin/static/admin/css/widgets.css | 7 +-- tests/admin_widgets/tests.py | 57 +++++-------------- 3 files changed, 15 insertions(+), 53 deletions(-) diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index 862fdd445828..d97601e648d7 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -289,10 +289,6 @@ button { flex: 0 1 auto; } - .stacked select { - margin-bottom: 0; - } - .stacked .selector-available, .stacked .selector-chosen { width: auto; diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 128b2498dd72..cd134673d044 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -241,7 +241,7 @@ display: flex; height: 30px; width: 64px; - margin: 0 auto 10px auto; + margin: 10px auto; background-color: #eee; border-radius: 10px; transform: none; @@ -252,11 +252,6 @@ padding: 3px 3px 3px 5px; } -.stacked .selector-chooseall, -.stacked .selector-clearall { - display: none; -} - .stacked .selector-add { background: url(../img/selector-icons.svg) 0 -48px no-repeat; background-size: 24px auto; diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index d0a22774bc04..23e1d49f33f2 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1285,7 +1285,6 @@ def setUp(self): def assertButtonsDisabled( self, - mode, field_name, choose_btn_disabled=False, remove_btn_disabled=False, @@ -1304,17 +1303,16 @@ def assertButtonsDisabled( self.expect(remove_button).to_be_disabled() else: self.expect(remove_button).to_be_enabled() - if mode == "horizontal": - if choose_all_btn_disabled: - self.expect(choose_all_button).to_be_disabled() - else: - self.expect(choose_all_button).to_be_enabled() - if remove_all_btn_disabled: - self.expect(remove_all_button).to_be_disabled() - else: - self.expect(remove_all_button).to_be_enabled() - - def execute_basic_operations(self, mode, field_name): + if choose_all_btn_disabled: + self.expect(choose_all_button).to_be_disabled() + else: + self.expect(choose_all_button).to_be_enabled() + if remove_all_btn_disabled: + self.expect(remove_all_button).to_be_disabled() + else: + self.expect(remove_all_button).to_be_enabled() + + def execute_basic_operations(self, field_name): original_url = self.page.url from_box = "#id_%s_from" % field_name @@ -1338,7 +1336,6 @@ def execute_basic_operations(self, mode, field_name): ) self.assertSelectOptions(to_box, [str(self.lisa.id), str(self.peter.id)]) self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=True, remove_btn_disabled=True, @@ -1347,17 +1344,7 @@ def execute_basic_operations(self, mode, field_name): ) # Click 'Choose all' -------------------------------------------------- - if mode == "horizontal": - self.page.locator(choose_all_button).click() - elif mode == "vertical": - # There's no 'Choose all' button in vertical mode, so individually - # select all options and click 'Choose'. - all_values = [ - el.get_attribute("value") - for el in self.page.locator(f"{from_box} > option").all() - ] - self.page.locator(from_box).select_option(value=all_values) - self.page.locator(choose_button).click() + self.page.locator(choose_all_button).click() self.assertSelectOptions(from_box, []) self.assertSelectOptions( to_box, @@ -1373,7 +1360,6 @@ def execute_basic_operations(self, mode, field_name): ], ) self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=True, remove_btn_disabled=True, @@ -1382,17 +1368,7 @@ def execute_basic_operations(self, mode, field_name): ) # Click 'Remove all' -------------------------------------------------- - if mode == "horizontal": - self.page.locator(remove_all_button).click() - elif mode == "vertical": - # There's no 'Remove all' button in vertical mode, so individually - # select all options and click 'Remove'. - all_values = [ - el.get_attribute("value") - for el in self.page.locator(f"{to_box} > option").all() - ] - self.page.locator(to_box).select_option(value=all_values) - self.page.locator(remove_button).click() + self.page.locator(remove_all_button).click() self.assertSelectOptions( from_box, [ @@ -1408,7 +1384,6 @@ def execute_basic_operations(self, mode, field_name): ) self.assertSelectOptions(to_box, []) self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=True, remove_btn_disabled=True, @@ -1435,7 +1410,6 @@ def execute_basic_operations(self, mode, field_name): ] ) self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=False, remove_btn_disabled=True, @@ -1444,7 +1418,6 @@ def execute_basic_operations(self, mode, field_name): ) self.page.locator(choose_button).click() self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=True, remove_btn_disabled=True, @@ -1484,7 +1457,6 @@ def execute_basic_operations(self, mode, field_name): value=[str(self.lisa.id), str(self.bob.id)] ) self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=True, remove_btn_disabled=False, @@ -1493,7 +1465,6 @@ def execute_basic_operations(self, mode, field_name): ) self.page.locator(remove_button).click() self.assertButtonsDisabled( - mode, field_name, choose_btn_disabled=True, remove_btn_disabled=True, @@ -1581,8 +1552,8 @@ def test_basic(self): + reverse("admin:admin_widgets_school_change", args=(self.school.id,)) ) - self.execute_basic_operations("vertical", "students") - self.execute_basic_operations("horizontal", "alumni") + self.execute_basic_operations("students") + self.execute_basic_operations("alumni") # Save, everything should be stored properly stored in the # database. From 97c38590a2396c8bb123d0782dbe96ddfd760a9a Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Wed, 2 Sep 2026 13:47:05 -0700 Subject: [PATCH 2/4] Fixed false positive in func_supports_parameter() for non-keyword args. Clarified that the internal utility `func_supports_parameter()` is meant to check whether `func` supports a particular named argument, and changed it to return False for "args" and "kwargs" on functions that accept variable `*args` and `**kwargs`, as well as for positional-only parameters, which likewise cannot be passed by name. --- django/utils/inspect.py | 13 ++++++++++++- tests/utils_tests/test_inspect.py | 32 +++++++++++++++++++------------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/django/utils/inspect.py b/django/utils/inspect.py index 1b4d5733fd87..27d66ae2ba9a 100644 --- a/django/utils/inspect.py +++ b/django/utils/inspect.py @@ -38,6 +38,13 @@ def _get_callable_parameters(meth_or_func): } ) +KEYWORD_ARG_KINDS = frozenset( + { + inspect.Parameter.KEYWORD_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + } +) + def get_func_args(func): params = _get_callable_parameters(func) @@ -87,7 +94,11 @@ def method_has_no_args(meth): def func_supports_parameter(func, name): - return any(param.name == name for param in _get_callable_parameters(func)) + """Return True if function 'func' accepts a (keyword) argument 'name'.""" + return any( + param.name == name and param.kind in KEYWORD_ARG_KINDS + for param in _get_callable_parameters(func) + ) def is_module_level_function(func): diff --git a/tests/utils_tests/test_inspect.py b/tests/utils_tests/test_inspect.py index f6e82e58083d..dbc44c37fa0b 100644 --- a/tests/utils_tests/test_inspect.py +++ b/tests/utils_tests/test_inspect.py @@ -23,6 +23,9 @@ def just_args(self, *args): def all_kinds(self, name, address="home", age=25, *args, **kwargs): return kwargs + def pos_and_kw_only(self, pos_only, /, normal, *, kw_only): + return pos_only, normal, kw_only + @classmethod def cls_all_kinds(cls, name, address="home", age=25, *args, **kwargs): return kwargs @@ -89,18 +92,23 @@ def test_method_has_no_args(self): self.assertIs(inspect.method_has_no_args(Person().one_argument), False) def test_func_supports_parameter(self): - self.assertIs( - inspect.func_supports_parameter(Person.all_kinds, "address"), True - ) - self.assertIs( - inspect.func_supports_parameter(Person().all_kinds, "address"), - True, - ) - self.assertIs(inspect.func_supports_parameter(Person.all_kinds, "zone"), False) - self.assertIs( - inspect.func_supports_parameter(Person().all_kinds, "zone"), - False, - ) + cases = [ + (Person.all_kinds, "address", True), + (Person().all_kinds, "address", True), + (Person.all_kinds, "zone", False), + (Person().all_kinds, "zone", False), + # Variable *args and **kwargs cannot be passed by name. + (Person.all_kinds, "args", False), + (Person.all_kinds, "kwargs", False), + # Positional-only arguments cannot be passed by name. + (Person.pos_and_kw_only, "pos_only", False), + # Regular and keyword-only arguments can be passed by name. + (Person.pos_and_kw_only, "normal", True), + (Person.pos_and_kw_only, "kw_only", True), + ] + for func, name, expected in cases: + with self.subTest(func=func.__name__, name=name): + self.assertIs(inspect.func_supports_parameter(func, name), expected) def test_func_accepts_kwargs(self): self.assertIs(inspect.func_accepts_kwargs(Person.just_args), False) From 516d06e48fb5a4e7deb055ef578fa1272e86d326 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:02:30 +0200 Subject: [PATCH 3/4] Fixed #37283 -- Clarified PostgreSQL connection pooling configuration. Documented its interaction with CONN_MAX_AGE and CONN_HEALTH_CHECKS. --- docs/ref/databases.txt | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 3e0fa2760002..62c2617ded3e 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -254,10 +254,18 @@ database configuration in :setting:`DATABASES`:: Connection pool --------------- -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:: +Connection pooling allows connections to be reused across threads, reducing +connection setup overhead. When enabled, Django maintains a separate pool for +each database alias in each process. Closing a Django database connection +returns it to the pool for reuse. + +Connection pooling is supported only with `psycopg`_ and requires +``psycopg[pool]`` (which installs :pypi:`psycopg-pool`). + +To enable connection pooling, set ``"pool"`` in the :setting:`OPTIONS` part of +your database configuration in :setting:`DATABASES` to a dictionary of options +to pass to :class:`~psycopg:psycopg_pool.ConnectionPool`, or to ``True`` to use +the :class:`~psycopg:psycopg_pool.ConnectionPool` defaults:: DATABASES = { "default": { @@ -269,8 +277,13 @@ to ``True`` to use the ``ConnectionPool`` defaults:: }, } -This option requires ``psycopg[pool]`` or :pypi:`psycopg-pool` to be installed -and is ignored with ``psycopg2``. +The :setting:`CONN_MAX_AGE` setting must be ``0`` when connection pooling is +enabled. Configure connection lifetime with the ``max_lifetime`` pool option +instead. + +Unless a ``check`` callback is provided in the pool options, +:setting:`CONN_HEALTH_CHECKS` determines whether connections are checked when +retrieved from the pool. .. _database-server-side-parameters-binding: From bbf5a998c02b52f1b72474e4b310cff94f24ff36 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Tue, 8 Sep 2026 11:46:28 -0700 Subject: [PATCH 4/4] Fixed #37261 -- Documented error reporting migration to MAILERS. Documented specifics for migrating AdminEmailHandler and BrokenLinkEmailsMiddleware to MAILERS when relying on fail_silently with an unreliable email backend connection. --- docs/howto/mailers-migration.txt | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/howto/mailers-migration.txt b/docs/howto/mailers-migration.txt index 30788f1592a8..d4eddeeaa9e9 100644 --- a/docs/howto/mailers-migration.txt +++ b/docs/howto/mailers-migration.txt @@ -47,6 +47,7 @@ use these specific features: * :ref:`migrating-to-mailers-email-backends` * :ref:`migrating-to-mailers-auth` * :ref:`migrating-to-mailers-adminemailhandler` +* :ref:`migrating-to-mailers-error-reporting` .. deprecated:: 6.1 @@ -312,6 +313,10 @@ depending on the caller's intent: Calls with ``fail_silently=False`` should be updated to remove the ``fail_silently`` arg, as that is the default. +If you use :class:`.AdminEmailHandler` or :class:`.BrokenLinkEmailsMiddleware` +and rely on their previous fail-silent behavior, see +:ref:`migrating-to-mailers-error-reporting`. + .. _migrating-to-mailers-email-backends: Migrating custom email backends @@ -444,3 +449,48 @@ Replace that with:: "BACKEND": "third.party.EmailBackend", }, } + +.. _migrating-to-mailers-error-reporting: + +Ignoring transient failures in error reporting +---------------------------------------------- + +Django includes two features that report errors via email: +:class:`.AdminEmailHandler` and :class:`.BrokenLinkEmailsMiddleware`. In +earlier releases, those features used the deprecated :ref:`fail_silently +` argument. Starting in Django 2028 -- or +immediately once the :setting:`MAILERS` setting is defined -- those features no +longer use ``fail_silently`` so no longer ignore certain backend-specific +errors. + +This may affect projects that use those features with an unreliable SMTP +connection and rely on silently ignoring temporary network failures (and +certain SMTP configuration errors). If you want to continue dropping error +notification emails that cannot be sent, define a :setting:`MAILERS` +configuration with the ``"fail_silently"`` option enabled for use by those +error handlers:: + + MAILERS = { + "default": {...}, + "admin-logging": { + "BACKEND": "django.core.mail.backends.smtp.EmailBackend", + "OPTIONS": { + # ... other SMTP options ... + "fail_silently": True, + }, + }, + } + +To use this for :class:`.AdminEmailHandler`, set the ``"using"`` option in the +:setting:`LOGGING` config as shown in the previous section. + +To use this for :class:`.BrokenLinkEmailsMiddleware`, create a subclass that +defines ``using``, and then specify the path to that subclass in your +:setting:`MIDDLEWARE` setting:: + + from django.middleware.common import BrokenLinkEmailsMiddleware + + + class CustomBrokenLinkEmailsMiddleware(BrokenLinkEmailsMiddleware): + using = "admin-logging" +