Skip to content
3 changes: 3 additions & 0 deletions django/contrib/admin/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,9 @@ def _check_list_select_related(self, obj):

if not isinstance(obj.list_select_related, (bool, list, tuple)):
return must_be(
# RemovedInDjango70Warning: when the deprecation ends, replace:
# "a tuple, list, or False",
# and also update docs/ref/checks.txt.
"a boolean, tuple or list",
option="list_select_related",
obj=obj,
Expand Down
29 changes: 27 additions & 2 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import enum
import json
import re
import warnings
from functools import partial, update_wrapper
from urllib.parse import parse_qsl
from urllib.parse import quote as urlquote
Expand Down Expand Up @@ -58,6 +59,7 @@
from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes
from django.utils.html import format_html
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
Expand Down Expand Up @@ -676,6 +678,18 @@ class ModelAdmin(BaseModelAdmin):
actions_selection_counter = True
checks_class = ModelAdminChecks

def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
if cls.__dict__.get("list_select_related") is True:
# RemovedInDjango70Warning: when the deprecation ends, raise a
# ValueError.
warnings.warn(
"Setting ModelAdmin.list_select_related to True is deprecated. "
"Use False or a list or tuple of fields to fetch instead.",
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)

def __init__(self, model, admin_site):
self.model = model
self.opts = model._meta
Expand Down Expand Up @@ -860,6 +874,17 @@ def get_changelist_instance(self, request):
list_display = ["action_checkbox", *list_display]
sortable_by = self.get_sortable_by(request)
ChangeList = self.get_changelist(request)
list_select_related = self.get_list_select_related(request)
if list_select_related is True:
# RemovedInDjango70Warning: when the deprecation ends, remove the
# below 'if' clause and raise a ValueError here.
if self.list_select_related is not True:
warnings.warn(
"Returning True from ModelAdmin.get_list_select_related() is "
"deprecated. Return False or a list or tuple of fields to "
"fetch instead.",
RemovedInDjango70Warning,
)
return ChangeList(
request,
self.model,
Expand All @@ -868,7 +893,7 @@ def get_changelist_instance(self, request):
self.get_list_filter(request),
self.date_hierarchy,
self.get_search_fields(request),
self.get_list_select_related(request),
list_select_related,
self.list_per_page,
self.list_max_show_all,
self.list_editable,
Expand Down Expand Up @@ -2332,7 +2357,7 @@ def history_view(self, request, object_id, extra_context=None):
object_id=unquote(object_id),
content_type=get_content_type_for_model(model),
)
.select_related()
.select_related("user", "content_type")
.order_by("action_time")
)

Expand Down
17 changes: 10 additions & 7 deletions django/contrib/admin/views/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,25 +529,28 @@ def apply_select_related(self, qs):
return qs.select_related()

if self.list_select_related is False:
if self.has_related_field_in_list_display():
return qs.select_related()
if fields := self.get_select_related_fields():
return qs.select_related(*fields)

if self.list_select_related:
return qs.select_related(*self.list_select_related)
return qs

def has_related_field_in_list_display(self):
def get_select_related_fields(self):
fields = []
for field_name in self.list_display:
try:
field = self.lookup_opts.get_field(field_name)
except FieldDoesNotExist:
pass
else:
if isinstance(field.remote_field, ManyToOneRel):
if (
isinstance(field.remote_field, ManyToOneRel)
# <FK>_id field names don't require a join.
if field_name != field.attname:
return True
return False
and field_name != field.attname
):
fields.append(field_name)
return fields

def url_for_result(self, result):
pk = getattr(result, self.pk_attname)
Expand Down
10 changes: 9 additions & 1 deletion django/db/models/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
resolve_callables,
)
from django.utils import timezone
from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes
from django.utils.functional import cached_property

# The maximum number of results to fetch in a get() query.
Expand Down Expand Up @@ -1774,6 +1774,14 @@ def select_related(self, *fields):
elif fields:
obj.query.add_select_related(fields)
else:
# RemovedInDjango70Warning: when the deprecation ends, raise a
# TypeError instead.
warnings.warn(
"Calling select_related() with no arguments is deprecated. "
"Specify the fields to fetch instead.",
category=RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
obj.query.select_related = True
return obj

Expand Down
9 changes: 9 additions & 0 deletions docs/internals/deprecation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ details on these changes.
``django.core.signing.base64_hmac()`` will change from ``"sha1"`` to
``"sha256"``.

* :meth:`.QuerySet.select_related` will raise :exc:`TypeError` if passed
``True`` rather than :exc:`AttributeError`.

* :class:`.ModelAdmin` will raise :exc:`ValueError` if subclassed with its
:attr:`.list_select_related` attribute set to ``True``.

* :class:`.ModelAdmin` will raise :exc:`ValueError` if its
:meth:`.get_list_select_related` method returns ``True``.

.. _deprecation-removed-in-6.1:

6.1
Expand Down
6 changes: 3 additions & 3 deletions docs/misc/design-philosophies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ optimize statements internally.
This is why developers need to call ``save()`` explicitly, rather than the
framework saving things behind the scenes silently.

This is also why the ``select_related()`` ``QuerySet`` method exists. It's an
optional performance booster for the common case of selecting "every related
object."
This is also why the ``FETCH_PEERS`` :doc:`fetch mode </topics/db/fetch-modes>`
exists. It's an optional performance booster for the common case of selecting
related objects for every peer in a ``QuerySet``.

Terse, powerful syntax
----------------------
Expand Down
10 changes: 10 additions & 0 deletions docs/ref/contrib/admin/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,11 @@ subclass::
If you need to specify a dynamic value based on the request, you can
implement a :meth:`~ModelAdmin.get_list_select_related` method.

.. deprecated:: 6.1

Using ``True`` for ``list_select_related`` is deprecated. Use a list or
tuple of field names instead.

.. note::

``ModelAdmin`` ignores this attribute when
Expand Down Expand Up @@ -1630,6 +1635,11 @@ default templates used by the :class:`ModelAdmin` views:
should return a boolean or list as :attr:`ModelAdmin.list_select_related`
does.

.. deprecated:: 6.1

Returning ``True`` is deprecated. Return a list or tuple of field names
instead.

.. method:: ModelAdmin.get_search_fields(request)

The ``get_search_fields`` method is given the ``HttpRequest`` and is
Expand Down
19 changes: 12 additions & 7 deletions docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1136,13 +1136,6 @@ is defined. Instead of specifying the field name, use the :attr:`related_name
<django.db.models.ForeignKey.related_name>` for the field on the related
object.

There may be some situations where you wish to call ``select_related()`` with a
lot of related objects, or where you don't know all of the relations. In these
cases it is possible to call ``select_related()`` with no arguments. This will
follow all non-null foreign keys it can find - nullable foreign keys must be
specified. This is not recommended in most cases as it is likely to make the
underlying query more complex, and return more data, than is actually needed.

If you need to clear the list of related fields added by past calls of
``select_related`` on a ``QuerySet``, you can pass ``None`` as a parameter:

Expand All @@ -1154,6 +1147,18 @@ Chaining ``select_related`` calls works in a similar way to other methods -
that is that ``select_related('foo', 'bar')`` is equivalent to
``select_related('foo').select_related('bar')``.

There may be some situations where you wish to call ``select_related()`` with a
lot of related objects, or where you don't know all of the relations. In these
cases it is possible to call ``select_related()`` with no arguments. This will
follow all non-null foreign keys it can find - nullable foreign keys must be
specified. This is not recommended in most cases as it is likely to make the
underlying query more complex, and return more data, than is actually needed.

.. deprecated:: 6.1

Calling ``select_related()`` with no arguments is deprecated, and support
for it will be removed in Django 7.0. Specify the fields to fetch instead.

``prefetch_related()``
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
16 changes: 16 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ Minor features
preserve :ref:`named groups <field-choices-named-groups>` (e.g.
``choices=[("Group", [("1", "Item")]), ...]``).

* When :attr:`.ModelAdmin.list_select_related` is ``False`` (the default),
the change list now selects only the foreign key fields specified in
:attr:`.ModelAdmin.list_display`, rather than all foreign key fields. This
should improve performance for models with many foreign key fields.

* The :attr:`~django.contrib.admin.ModelAdmin.delete_confirmation_max_display`
option allows customizing how many objects are displayed on admin delete
confirmation pages before the remainder is truncated. The default is
Expand Down Expand Up @@ -480,6 +485,9 @@ backends.
* The undocumented ``InclusionAdminNode.__init__()`` now takes the template tag
``name`` as the first positional argument.

* The undocumented ``ChangeList.has_related_field_in_list_display()`` method
has been replaced with ``ChangeList.get_select_related_fields()``.

:mod:`django.contrib.auth`
--------------------------

Expand Down Expand Up @@ -592,6 +600,14 @@ Features deprecated in 6.1
Miscellaneous
-------------

* Calling :meth:`~django.db.models.query.QuerySet.select_related` with no
arguments to select all related fields, is deprecated. Specify the related
fields to fetch instead.

* Setting :attr:`.ModelAdmin.list_select_related` to ``True`` and returning
``True`` from :attr:`.ModelAdmin.get_list_select_related()` are deprecated.
Specify the related fields to fetch instead.

* Calling :meth:`.QuerySet.values_list` with ``flat=True`` and no field name
is deprecated. Pass an explicit field name, like
``values_list("pk", flat=True)``.
Expand Down
2 changes: 1 addition & 1 deletion docs/topics/db/search.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Standard textual queries
------------------------

Text-based fields have a selection of matching operations. For example, you may
wish to allow lookup up an author like so:
wish to allow the lookup of an author like so:

.. code-block:: pycon

Expand Down
12 changes: 8 additions & 4 deletions tests/admin_views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,17 @@ def has_module_permission(self, request):


class RowLevelChangePermissionModelAdmin(admin.ModelAdmin):
# These fields aren't intended to be modified by the change form. By
# making them read-only, they don't need to be included in post data.
readonly_fields = ("can_change", "can_view")

def has_change_permission(self, request, obj=None):
"""Only allow changing objects with even id number"""
return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0)
"""Only allow changing objects with can_change=True."""
return request.user.is_staff and obj is not None and obj.can_change

def has_view_permission(self, request, obj=None):
"""Only allow viewing objects if id is a multiple of 3."""
return request.user.is_staff and obj is not None and obj.id % 3 == 0
"""Only allow viewing objects with can_view=True."""
return request.user.is_staff and obj is not None and obj.can_view


class CustomArticleAdmin(admin.ModelAdmin):
Expand Down
2 changes: 2 additions & 0 deletions tests/admin_views/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ def __str__(self):

class RowLevelChangePermissionModel(models.Model):
name = models.CharField(max_length=100, blank=True)
can_change = models.BooleanField(default=False)
can_view = models.BooleanField(default=False)


class CustomArticle(models.Model):
Expand Down
Loading
Loading