Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions django/contrib/admin/static/admin/css/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,6 @@ button {
flex: 0 1 auto;
}

.stacked select {
margin-bottom: 0;
}

.stacked .selector-available,
.stacked .selector-chosen {
width: auto;
Expand Down
7 changes: 1 addition & 6 deletions django/contrib/admin/static/admin/css/widgets.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion django/utils/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
50 changes: 50 additions & 0 deletions docs/howto/mailers-migration.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
<migrating-to-mailers-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"

25 changes: 19 additions & 6 deletions docs/ref/databases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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:

Expand Down
57 changes: 14 additions & 43 deletions tests/admin_widgets/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1285,7 +1285,6 @@ def setUp(self):

def assertButtonsDisabled(
self,
mode,
field_name,
choose_btn_disabled=False,
remove_btn_disabled=False,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
[
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 20 additions & 12 deletions tests/utils_tests/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading