diff --git a/AUTHORS b/AUTHORS
index 8c903ff6c5..26cfe71138 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -44,6 +44,7 @@ answer newbie questions, and generally made Django that much better:
Albert Wang
Alcides Fonseca
Aldian Fazrihady
+ Alejandro García Ruiz de Oteiza
Aleksandra Sendecka
Aleksi Häkli
Alex Dutton
@@ -760,6 +761,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
diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py
index aa43718cd6..c1a17af076 100644
--- a/django/contrib/admin/checks.py
+++ b/django/contrib/admin/checks.py
@@ -915,21 +915,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)
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..d8fff50d18 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)
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index f0eeaca268..cf0ab32efa 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -726,9 +726,9 @@ with the admin site:
* **admin.E106**: The value of ``.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 ``