mirror of https://github.com/django/django.git
Fixed #10743 -- Allowed lookups for related fields in ModelAdmin.list_display.
Co-authored-by: Alex Garcia <me@alexoteiza.com> Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Nina Menezes <https://github.com/nmenezes0>
This commit is contained in:
parent
3580b47ed3
commit
4ade8386eb
2
AUTHORS
2
AUTHORS
|
@ -44,6 +44,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Albert Wang <https://github.com/albertyw/>
|
Albert Wang <https://github.com/albertyw/>
|
||||||
Alcides Fonseca
|
Alcides Fonseca
|
||||||
Aldian Fazrihady <mobile@aldian.net>
|
Aldian Fazrihady <mobile@aldian.net>
|
||||||
|
Alejandro García Ruiz de Oteiza <https://github.com/AlexOteiza>
|
||||||
Aleksandra Sendecka <asendecka@hauru.eu>
|
Aleksandra Sendecka <asendecka@hauru.eu>
|
||||||
Aleksi Häkli <aleksi.hakli@iki.fi>
|
Aleksi Häkli <aleksi.hakli@iki.fi>
|
||||||
Alex Dutton <django@alexdutton.co.uk>
|
Alex Dutton <django@alexdutton.co.uk>
|
||||||
|
@ -760,6 +761,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Nicolas Noé <nicolas@niconoe.eu>
|
Nicolas Noé <nicolas@niconoe.eu>
|
||||||
Nikita Marchant <nikita.marchant@gmail.com>
|
Nikita Marchant <nikita.marchant@gmail.com>
|
||||||
Nikita Sobolev <mail@sobolevn.me>
|
Nikita Sobolev <mail@sobolevn.me>
|
||||||
|
Nina Menezes <https://github.com/nmenezes0>
|
||||||
Niran Babalola <niran@niran.org>
|
Niran Babalola <niran@niran.org>
|
||||||
Nis Jørgensen <nis@superlativ.dk>
|
Nis Jørgensen <nis@superlativ.dk>
|
||||||
Nowell Strite <https://nowell.strite.org/>
|
Nowell Strite <https://nowell.strite.org/>
|
||||||
|
|
|
@ -915,21 +915,19 @@ class ModelAdminChecks(BaseModelAdminChecks):
|
||||||
try:
|
try:
|
||||||
field = getattr(obj.model, item)
|
field = getattr(obj.model, item)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return [
|
try:
|
||||||
checks.Error(
|
field = get_fields_from_path(obj.model, item)[-1]
|
||||||
"The value of '%s' refers to '%s', which is not a "
|
except (FieldDoesNotExist, NotRelationField):
|
||||||
"callable, an attribute of '%s', or an attribute or "
|
return [
|
||||||
"method on '%s'."
|
checks.Error(
|
||||||
% (
|
f"The value of '{label}' refers to '{item}', which is not "
|
||||||
label,
|
f"a callable or attribute of '{obj.__class__.__name__}', "
|
||||||
item,
|
"or an attribute, method, or field on "
|
||||||
obj.__class__.__name__,
|
f"'{obj.model._meta.label}'.",
|
||||||
obj.model._meta.label,
|
obj=obj.__class__,
|
||||||
),
|
id="admin.E108",
|
||||||
obj=obj.__class__,
|
)
|
||||||
id="admin.E108",
|
]
|
||||||
)
|
|
||||||
]
|
|
||||||
if (
|
if (
|
||||||
getattr(field, "is_relation", False)
|
getattr(field, "is_relation", False)
|
||||||
and (field.many_to_many or field.one_to_many)
|
and (field.many_to_many or field.one_to_many)
|
||||||
|
|
|
@ -289,8 +289,8 @@ def lookup_field(name, obj, model_admin=None):
|
||||||
try:
|
try:
|
||||||
f = _get_non_gfk_field(opts, name)
|
f = _get_non_gfk_field(opts, name)
|
||||||
except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
|
except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
|
||||||
# For non-field values, the value is either a method, property or
|
# For non-regular field values, the value is either a method,
|
||||||
# returned via a callable.
|
# property, related field, or returned via a callable.
|
||||||
if callable(name):
|
if callable(name):
|
||||||
attr = name
|
attr = name
|
||||||
value = attr(obj)
|
value = attr(obj)
|
||||||
|
@ -298,10 +298,17 @@ def lookup_field(name, obj, model_admin=None):
|
||||||
attr = getattr(model_admin, name)
|
attr = getattr(model_admin, name)
|
||||||
value = attr(obj)
|
value = attr(obj)
|
||||||
else:
|
else:
|
||||||
attr = getattr(obj, name)
|
sentinel = object()
|
||||||
|
attr = getattr(obj, name, sentinel)
|
||||||
if callable(attr):
|
if callable(attr):
|
||||||
value = attr()
|
value = attr()
|
||||||
else:
|
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
|
value = attr
|
||||||
if hasattr(model_admin, "model") and hasattr(model_admin.model, name):
|
if hasattr(model_admin, "model") and hasattr(model_admin.model, name):
|
||||||
attr = getattr(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,
|
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
|
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
|
object's attribute, as well as a model field, including across related
|
||||||
return the resolved attribute (which could be a callable). This will be
|
objects. If return_attr is True, also return the resolved attribute
|
||||||
None if (and only if) the name refers to a field.
|
(which could be a callable). This will be None if (and only if) the name
|
||||||
|
refers to a field.
|
||||||
"""
|
"""
|
||||||
attr = None
|
attr = None
|
||||||
try:
|
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:
|
elif form and name in form.fields:
|
||||||
attr = form.fields[name]
|
attr = form.fields[name]
|
||||||
else:
|
else:
|
||||||
message = "Unable to lookup '%s' on %s" % (
|
try:
|
||||||
name,
|
attr = get_fields_from_path(model, name)[-1]
|
||||||
model._meta.object_name,
|
except (FieldDoesNotExist, NotRelationField):
|
||||||
)
|
message = f"Unable to lookup '{name}' on {model._meta.object_name}"
|
||||||
if model_admin:
|
if model_admin:
|
||||||
message += " or %s" % model_admin.__class__.__name__
|
message += f" or {model_admin.__class__.__name__}"
|
||||||
if form:
|
if form:
|
||||||
message += " or %s" % form.__class__.__name__
|
message += f" or {form.__class__.__name__}"
|
||||||
raise AttributeError(message)
|
raise AttributeError(message)
|
||||||
|
|
||||||
if hasattr(attr, "short_description"):
|
if hasattr(attr, "short_description"):
|
||||||
label = attr.short_description
|
label = attr.short_description
|
||||||
|
|
|
@ -30,6 +30,7 @@ from django.core.exceptions import (
|
||||||
)
|
)
|
||||||
from django.core.paginator import InvalidPage
|
from django.core.paginator import InvalidPage
|
||||||
from django.db.models import F, Field, ManyToOneRel, OrderBy
|
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.db.models.expressions import Combinable
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.deprecation import RemovedInDjango60Warning
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
|
@ -356,9 +357,9 @@ class ChangeList:
|
||||||
"""
|
"""
|
||||||
Return the proper model field name corresponding to the given
|
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
|
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
|
proper model field, possibly across relations, or the name of a method
|
||||||
callable with the 'admin_order_field' attribute. Return None if no
|
(on the admin or model) or a callable with the 'admin_order_field'
|
||||||
proper model field name can be matched.
|
attribute. Return None if no proper model field name can be matched.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
field = self.lookup_opts.get_field(field_name)
|
field = self.lookup_opts.get_field(field_name)
|
||||||
|
@ -371,7 +372,12 @@ class ChangeList:
|
||||||
elif hasattr(self.model_admin, field_name):
|
elif hasattr(self.model_admin, field_name):
|
||||||
attr = getattr(self.model_admin, field_name)
|
attr = getattr(self.model_admin, field_name)
|
||||||
else:
|
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"):
|
if isinstance(attr, property) and hasattr(attr, "fget"):
|
||||||
attr = attr.fget
|
attr = attr.fget
|
||||||
return getattr(attr, "admin_order_field", None)
|
return getattr(attr, "admin_order_field", None)
|
||||||
|
|
|
@ -726,9 +726,9 @@ with the admin site:
|
||||||
* **admin.E106**: The value of ``<InlineModelAdmin class>.model`` must be a
|
* **admin.E106**: The value of ``<InlineModelAdmin class>.model`` must be a
|
||||||
``Model``.
|
``Model``.
|
||||||
* **admin.E107**: The value of ``list_display`` must be a list or tuple.
|
* **admin.E107**: The value of ``list_display`` must be a list or tuple.
|
||||||
* **admin.E108**: The value of ``list_display[n]`` refers to ``<label>``,
|
* **admin.E108**: The value of ``list_display[n]`` refers to ``<label>``, which
|
||||||
which is not a callable, an attribute of ``<ModelAdmin class>``, or an
|
is not a callable or attribute of ``<ModelAdmin class>``, or an attribute,
|
||||||
attribute or method on ``<model>``.
|
method, or field on ``<model>``.
|
||||||
* **admin.E109**: The value of ``list_display[n]`` must not be a many-to-many
|
* **admin.E109**: The value of ``list_display[n]`` must not be a many-to-many
|
||||||
field or a reverse foreign key.
|
field or a reverse foreign key.
|
||||||
* **admin.E110**: The value of ``list_display_links`` must be a list, a tuple,
|
* **admin.E110**: The value of ``list_display_links`` must be a list, a tuple,
|
||||||
|
|
|
@ -315,9 +315,9 @@ subclass::
|
||||||
For more complex layout needs, see the :attr:`~ModelAdmin.fieldsets` option.
|
For more complex layout needs, see the :attr:`~ModelAdmin.fieldsets` option.
|
||||||
|
|
||||||
The ``fields`` option accepts the same types of values as
|
The ``fields`` option accepts the same types of values as
|
||||||
:attr:`~ModelAdmin.list_display`, except that callables aren't accepted.
|
:attr:`~ModelAdmin.list_display`, except that callables and ``__`` lookups
|
||||||
Names of model and model admin methods will only be used if they're listed
|
for related fields aren't accepted. Names of model and model admin methods
|
||||||
in :attr:`~ModelAdmin.readonly_fields`.
|
will only be used if they're listed in :attr:`~ModelAdmin.readonly_fields`.
|
||||||
|
|
||||||
To display multiple fields on the same line, wrap those fields in their own
|
To display multiple fields on the same line, wrap those fields in their own
|
||||||
tuple. In this example, the ``url`` and ``title`` fields will display on the
|
tuple. In this example, the ``url`` and ``title`` fields will display on the
|
||||||
|
@ -565,7 +565,7 @@ subclass::
|
||||||
If you don't set ``list_display``, the admin site will display a single
|
If you don't set ``list_display``, the admin site will display a single
|
||||||
column that displays the ``__str__()`` representation of each object.
|
column that displays the ``__str__()`` representation of each object.
|
||||||
|
|
||||||
There are four types of values that can be used in ``list_display``. All
|
There are five types of values that can be used in ``list_display``. All
|
||||||
but the simplest may use the :func:`~django.contrib.admin.display`
|
but the simplest may use the :func:`~django.contrib.admin.display`
|
||||||
decorator, which is used to customize how the field is presented:
|
decorator, which is used to customize how the field is presented:
|
||||||
|
|
||||||
|
@ -574,6 +574,11 @@ subclass::
|
||||||
class PersonAdmin(admin.ModelAdmin):
|
class PersonAdmin(admin.ModelAdmin):
|
||||||
list_display = ["first_name", "last_name"]
|
list_display = ["first_name", "last_name"]
|
||||||
|
|
||||||
|
* The name of a related field, using the ``__`` notation. For example::
|
||||||
|
|
||||||
|
class PersonAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["city__name"]
|
||||||
|
|
||||||
* A callable that accepts one argument, the model instance. For example::
|
* A callable that accepts one argument, the model instance. For example::
|
||||||
|
|
||||||
@admin.display(description="Name")
|
@admin.display(description="Name")
|
||||||
|
@ -614,6 +619,11 @@ subclass::
|
||||||
class PersonAdmin(admin.ModelAdmin):
|
class PersonAdmin(admin.ModelAdmin):
|
||||||
list_display = ["name", "decade_born_in"]
|
list_display = ["name", "decade_born_in"]
|
||||||
|
|
||||||
|
.. versionchanged:: 5.1
|
||||||
|
|
||||||
|
Support for using ``__`` lookups was added, when targeting related
|
||||||
|
fields.
|
||||||
|
|
||||||
A few special cases to note about ``list_display``:
|
A few special cases to note about ``list_display``:
|
||||||
|
|
||||||
* If the field is a ``ForeignKey``, Django will display the
|
* If the field is a ``ForeignKey``, Django will display the
|
||||||
|
@ -831,7 +841,7 @@ subclass::
|
||||||
* Django will try to interpret every element of ``list_display`` in this
|
* Django will try to interpret every element of ``list_display`` in this
|
||||||
order:
|
order:
|
||||||
|
|
||||||
* A field of the model.
|
* A field of the model or from a related field.
|
||||||
* A callable.
|
* A callable.
|
||||||
* A string representing a ``ModelAdmin`` attribute.
|
* A string representing a ``ModelAdmin`` attribute.
|
||||||
* A string representing a model attribute.
|
* A string representing a model attribute.
|
||||||
|
|
|
@ -32,7 +32,8 @@ Minor features
|
||||||
:mod:`django.contrib.admin`
|
:mod:`django.contrib.admin`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* :attr:`.ModelAdmin.list_display` now supports using ``__`` lookups to list
|
||||||
|
fields from related models.
|
||||||
|
|
||||||
:mod:`django.contrib.admindocs`
|
:mod:`django.contrib.admindocs`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -53,6 +53,10 @@ class ChildAdmin(admin.ModelAdmin):
|
||||||
return super().get_queryset(request).select_related("parent")
|
return super().get_queryset(request).select_related("parent")
|
||||||
|
|
||||||
|
|
||||||
|
class GrandChildAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "parent__name", "parent__parent__name"]
|
||||||
|
|
||||||
|
|
||||||
class CustomPaginationAdmin(ChildAdmin):
|
class CustomPaginationAdmin(ChildAdmin):
|
||||||
paginator = CustomPaginator
|
paginator = CustomPaginator
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,11 @@ class Child(models.Model):
|
||||||
age = models.IntegerField(null=True, blank=True)
|
age = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class GrandChild(models.Model):
|
||||||
|
parent = models.ForeignKey(Child, models.SET_NULL, editable=False, null=True)
|
||||||
|
name = models.CharField(max_length=30, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class Genre(models.Model):
|
class Genre(models.Model):
|
||||||
name = models.CharField(max_length=20)
|
name = models.CharField(max_length=20)
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ from .admin import (
|
||||||
EmptyValueChildAdmin,
|
EmptyValueChildAdmin,
|
||||||
EventAdmin,
|
EventAdmin,
|
||||||
FilteredChildAdmin,
|
FilteredChildAdmin,
|
||||||
|
GrandChildAdmin,
|
||||||
GroupAdmin,
|
GroupAdmin,
|
||||||
InvitationAdmin,
|
InvitationAdmin,
|
||||||
NoListDisplayLinksParentAdmin,
|
NoListDisplayLinksParentAdmin,
|
||||||
|
@ -61,6 +62,7 @@ from .models import (
|
||||||
CustomIdUser,
|
CustomIdUser,
|
||||||
Event,
|
Event,
|
||||||
Genre,
|
Genre,
|
||||||
|
GrandChild,
|
||||||
Group,
|
Group,
|
||||||
Invitation,
|
Invitation,
|
||||||
Membership,
|
Membership,
|
||||||
|
@ -1634,6 +1636,62 @@ class ChangeListTests(TestCase):
|
||||||
response, f'0 results (<a href="{href}">1 total</a>)'
|
response, f'0 results (<a href="{href}">1 total</a>)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_list_display_related_field(self):
|
||||||
|
parent = Parent.objects.create(name="I am your father")
|
||||||
|
child = Child.objects.create(name="I am your child", parent=parent)
|
||||||
|
GrandChild.objects.create(name="I am your grandchild", parent=child)
|
||||||
|
request = self._mocked_authenticated_request("/grandchild/", self.superuser)
|
||||||
|
|
||||||
|
m = GrandChildAdmin(GrandChild, custom_site)
|
||||||
|
response = m.changelist_view(request)
|
||||||
|
self.assertContains(response, parent.name)
|
||||||
|
self.assertContains(response, child.name)
|
||||||
|
|
||||||
|
def test_list_display_related_field_null(self):
|
||||||
|
GrandChild.objects.create(name="I am parentless", parent=None)
|
||||||
|
request = self._mocked_authenticated_request("/grandchild/", self.superuser)
|
||||||
|
|
||||||
|
m = GrandChildAdmin(GrandChild, custom_site)
|
||||||
|
response = m.changelist_view(request)
|
||||||
|
self.assertContains(response, '<td class="field-parent__name">-</td>')
|
||||||
|
self.assertContains(response, '<td class="field-parent__parent__name">-</td>')
|
||||||
|
|
||||||
|
def test_list_display_related_field_ordering(self):
|
||||||
|
parent_a = Parent.objects.create(name="Alice")
|
||||||
|
parent_z = Parent.objects.create(name="Zara")
|
||||||
|
Child.objects.create(name="Alice's child", parent=parent_a)
|
||||||
|
Child.objects.create(name="Zara's child", parent=parent_z)
|
||||||
|
|
||||||
|
class ChildAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "parent__name"]
|
||||||
|
list_per_page = 1
|
||||||
|
|
||||||
|
m = ChildAdmin(Child, custom_site)
|
||||||
|
|
||||||
|
# Order ascending.
|
||||||
|
request = self._mocked_authenticated_request("/grandchild/?o=1", self.superuser)
|
||||||
|
response = m.changelist_view(request)
|
||||||
|
self.assertContains(response, parent_a.name)
|
||||||
|
self.assertNotContains(response, parent_z.name)
|
||||||
|
|
||||||
|
# Order descending.
|
||||||
|
request = self._mocked_authenticated_request(
|
||||||
|
"/grandchild/?o=-1", self.superuser
|
||||||
|
)
|
||||||
|
response = m.changelist_view(request)
|
||||||
|
self.assertNotContains(response, parent_a.name)
|
||||||
|
self.assertContains(response, parent_z.name)
|
||||||
|
|
||||||
|
def test_list_display_related_field_ordering_fields(self):
|
||||||
|
class ChildAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "parent__name"]
|
||||||
|
ordering = ["parent__name"]
|
||||||
|
|
||||||
|
m = ChildAdmin(Child, custom_site)
|
||||||
|
request = self._mocked_authenticated_request("/", self.superuser)
|
||||||
|
cl = m.get_changelist_instance(request)
|
||||||
|
self.assertEqual(cl.get_ordering_field_columns(), {2: "asc"})
|
||||||
|
|
||||||
|
|
||||||
class GetAdminLogTests(TestCase):
|
class GetAdminLogTests(TestCase):
|
||||||
def test_custom_user_pk_not_named_id(self):
|
def test_custom_user_pk_not_named_id(self):
|
||||||
|
|
|
@ -1009,3 +1009,26 @@ class SystemChecksTestCase(SimpleTestCase):
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
finally:
|
finally:
|
||||||
Book._meta.apps.ready = True
|
Book._meta.apps.ready = True
|
||||||
|
|
||||||
|
def test_related_field_list_display(self):
|
||||||
|
class SongAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["pk", "original_release", "album__title"]
|
||||||
|
|
||||||
|
errors = SongAdmin(Song, AdminSite()).check()
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
|
||||||
|
def test_related_field_list_display_wrong_field(self):
|
||||||
|
class SongAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["pk", "original_release", "album__hello"]
|
||||||
|
|
||||||
|
errors = SongAdmin(Song, AdminSite()).check()
|
||||||
|
expected = [
|
||||||
|
checks.Error(
|
||||||
|
"The value of 'list_display[2]' refers to 'album__hello', which is not "
|
||||||
|
"a callable or attribute of 'SongAdmin', or an attribute, method, or "
|
||||||
|
"field on 'admin_checks.Song'.",
|
||||||
|
obj=SongAdmin,
|
||||||
|
id="admin.E108",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.assertEqual(errors, expected)
|
||||||
|
|
|
@ -137,6 +137,7 @@ class UtilsTests(SimpleTestCase):
|
||||||
(simple_function, SIMPLE_FUNCTION),
|
(simple_function, SIMPLE_FUNCTION),
|
||||||
("test_from_model", article.test_from_model()),
|
("test_from_model", article.test_from_model()),
|
||||||
("non_field", INSTANCE_ATTRIBUTE),
|
("non_field", INSTANCE_ATTRIBUTE),
|
||||||
|
("site__domain", SITE_NAME),
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_admin = MockModelAdmin()
|
mock_admin = MockModelAdmin()
|
||||||
|
@ -294,6 +295,17 @@ class UtilsTests(SimpleTestCase):
|
||||||
|
|
||||||
self.assertEqual(label_for_field(lambda x: "nothing", Article), "--")
|
self.assertEqual(label_for_field(lambda x: "nothing", Article), "--")
|
||||||
self.assertEqual(label_for_field("site_id", Article), "Site id")
|
self.assertEqual(label_for_field("site_id", Article), "Site id")
|
||||||
|
# The correct name and attr are returned when `__` is in the field name.
|
||||||
|
self.assertEqual(label_for_field("site__domain", Article), "Site domain")
|
||||||
|
self.assertEqual(
|
||||||
|
label_for_field("site__domain", Article, return_attr=True),
|
||||||
|
("Site domain", Site._meta.get_field("domain")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_label_for_field_failed_lookup(self):
|
||||||
|
msg = "Unable to lookup 'site__unknown' on Article"
|
||||||
|
with self.assertRaisesMessage(AttributeError, msg):
|
||||||
|
label_for_field("site__unknown", Article)
|
||||||
|
|
||||||
class MockModelAdmin:
|
class MockModelAdmin:
|
||||||
@admin.display(description="not Really the Model")
|
@admin.display(description="not Really the Model")
|
||||||
|
|
|
@ -69,7 +69,7 @@ class RawIdCheckTests(CheckTestCase):
|
||||||
|
|
||||||
def test_missing_field(self):
|
def test_missing_field(self):
|
||||||
class TestModelAdmin(ModelAdmin):
|
class TestModelAdmin(ModelAdmin):
|
||||||
raw_id_fields = ("non_existent_field",)
|
raw_id_fields = ["non_existent_field"]
|
||||||
|
|
||||||
self.assertIsInvalid(
|
self.assertIsInvalid(
|
||||||
TestModelAdmin,
|
TestModelAdmin,
|
||||||
|
@ -602,8 +602,21 @@ class ListDisplayTests(CheckTestCase):
|
||||||
TestModelAdmin,
|
TestModelAdmin,
|
||||||
ValidationTestModel,
|
ValidationTestModel,
|
||||||
"The value of 'list_display[0]' refers to 'non_existent_field', "
|
"The value of 'list_display[0]' refers to 'non_existent_field', "
|
||||||
"which is not a callable, an attribute of 'TestModelAdmin', "
|
"which is not a callable or attribute of 'TestModelAdmin', "
|
||||||
"or an attribute or method on 'modeladmin.ValidationTestModel'.",
|
"or an attribute, method, or field on 'modeladmin.ValidationTestModel'.",
|
||||||
|
"admin.E108",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_related_field(self):
|
||||||
|
class TestModelAdmin(ModelAdmin):
|
||||||
|
list_display = ("band__non_existent_field",)
|
||||||
|
|
||||||
|
self.assertIsInvalid(
|
||||||
|
TestModelAdmin,
|
||||||
|
ValidationTestModel,
|
||||||
|
"The value of 'list_display[0]' refers to 'band__non_existent_field', "
|
||||||
|
"which is not a callable or attribute of 'TestModelAdmin', "
|
||||||
|
"or an attribute, method, or field on 'modeladmin.ValidationTestModel'.",
|
||||||
"admin.E108",
|
"admin.E108",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue