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 | |