diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py
index 830a190..6c4beda 100644
|
a
|
b
|
class ModelAdminChecks(BaseModelAdminChecks):
|
| 587 | 587 | elif hasattr(obj, item): |
| 588 | 588 | return [] |
| 589 | 589 | elif hasattr(model, item): |
| 590 | | # getattr(model, item) could be an X_RelatedObjectsDescriptor |
| | 590 | # Because hasattr(model, item) succeeded, we know that |
| | 591 | # getattr(model, item) will succeed, and can thus be displayed in |
| | 592 | # the listview, since the former is implemented by calling the |
| | 593 | # latter and testing for errors. In this branch, we therefore only |
| | 594 | # need to test for ManyToManyFields. |
| 591 | 595 | try: |
| 592 | 596 | field = model._meta.get_field(item) |
| 593 | 597 | except FieldDoesNotExist: |
| 594 | | try: |
| 595 | | field = getattr(model, item) |
| 596 | | except AttributeError: |
| 597 | | field = None |
| 598 | | |
| 599 | | if field is None: |
| 600 | | return [ |
| 601 | | checks.Error( |
| 602 | | "The value of '%s' refers to '%s', which is not a " |
| 603 | | "callable, an attribute of '%s', or an attribute or method on '%s.%s'." % ( |
| 604 | | label, item, obj.__class__.__name__, model._meta.app_label, model._meta.object_name |
| 605 | | ), |
| 606 | | obj=obj.__class__, |
| 607 | | id='admin.E108', |
| 608 | | ) |
| 609 | | ] |
| 610 | | elif isinstance(field, models.ManyToManyField): |
| 611 | | return [ |
| 612 | | checks.Error( |
| 613 | | "The value of '%s' must not be a ManyToManyField." % label, |
| 614 | | obj=obj.__class__, |
| 615 | | id='admin.E109', |
| 616 | | ) |
| 617 | | ] |
| 618 | | else: |
| 619 | 598 | return [] |
| 620 | | else: |
| 621 | | try: |
| 622 | | model._meta.get_field(item) |
| 623 | | except FieldDoesNotExist: |
| 624 | | return [ |
| 625 | | # This is a deliberate repeat of E108; there's more than one path |
| 626 | | # required to test this condition. |
| 627 | | checks.Error( |
| 628 | | "The value of '%s' refers to '%s', which is not a callable, " |
| 629 | | "an attribute of '%s', or an attribute or method on '%s.%s'." % ( |
| 630 | | label, item, obj.__class__.__name__, model._meta.app_label, model._meta.object_name |
| 631 | | ), |
| 632 | | obj=obj.__class__, |
| 633 | | id='admin.E108', |
| 634 | | ) |
| 635 | | ] |
| 636 | 599 | else: |
| 637 | | return [] |
| | 600 | if isinstance(field, models.ManyToManyField): |
| | 601 | return [ |
| | 602 | checks.Error( |
| | 603 | "The value of '%s' must not be a ManyToManyField." % label, |
| | 604 | obj=obj.__class__, |
| | 605 | id='admin.E109', |
| | 606 | ) |
| | 607 | ] |
| | 608 | else: |
| | 609 | return [] |
| | 610 | else: |
| | 611 | return [ |
| | 612 | checks.Error( |
| | 613 | "The value of '%s' refers to '%s', which is not a callable, " |
| | 614 | "an attribute of '%s', or an attribute or method on '%s.%s'." % ( |
| | 615 | label, item, obj.__class__.__name__, model._meta.app_label, model._meta.object_name |
| | 616 | ), |
| | 617 | obj=obj.__class__, |
| | 618 | id='admin.E108', |
| | 619 | ) |
| | 620 | ] |
| 638 | 621 | |
| 639 | 622 | def _check_list_display_links(self, obj): |
| 640 | 623 | """ Check that list_display_links is a unique subset of list_display. |
diff --git a/tests/modeladmin/models.py b/tests/modeladmin/models.py
index 861a2db..845b119 100644
|
a
|
b
|
class Concert(models.Model):
|
| 25 | 25 | ), blank=True) |
| 26 | 26 | |
| 27 | 27 | |
| | 28 | class DescriptorThatReturnsNone(object): |
| | 29 | def __get__(self, obj, objtype): |
| | 30 | # In general, Descriptors should return `self` when `obj is None`, but |
| | 31 | # let's test the case where they don't. |
| | 32 | return None |
| | 33 | |
| | 34 | def __set__(self, obj, value): |
| | 35 | pass |
| | 36 | |
| | 37 | |
| 28 | 38 | class ValidationTestModel(models.Model): |
| 29 | 39 | name = models.CharField(max_length=100) |
| 30 | 40 | slug = models.SlugField() |
| … |
… |
class ValidationTestModel(models.Model):
|
| 36 | 46 | best_friend = models.OneToOneField(User, models.CASCADE, related_name='best_friend') |
| 37 | 47 | # This field is intentionally 2 characters long (#16080). |
| 38 | 48 | no = models.IntegerField(verbose_name="Number", blank=True, null=True) |
| | 49 | none_descriptor = DescriptorThatReturnsNone() |
| 39 | 50 | |
| 40 | 51 | def decade_published_in(self): |
| 41 | 52 | return self.pub_date.strftime('%Y')[:3] + "0's" |
diff --git a/tests/modeladmin/test_checks.py b/tests/modeladmin/test_checks.py
index acca6b1..5d3528a 100644
|
a
|
b
|
class ListDisplayTests(CheckTestCase):
|
| 479 | 479 | class TestModelAdmin(ModelAdmin): |
| 480 | 480 | def a_method(self, obj): |
| 481 | 481 | pass |
| 482 | | list_display = ('name', 'decade_published_in', 'a_method', a_callable) |
| | 482 | list_display = ('name', 'decade_published_in', 'a_method', a_callable, 'none_descriptor') |
| 483 | 483 | |
| 484 | 484 | self.assertIsValid(TestModelAdmin, ValidationTestModel) |
| 485 | 485 | |