From bb052c2234aa9f59ffde62d39b8ea04449d119d0 Mon Sep 17 00:00:00 2001
From: Chris Adams <chris@improbable.org>
Date: Wed, 2 Feb 2011 10:22:55 -0500
Subject: [PATCH] ModelAdmin: allow non-model fields in list_display_links (See #11058)
In addition to model field names, list_display accepts callables, ModelAdmin
attributes or Model attributes.
This patch reuses the list_display resolution for these values for
list_display_links validation as well and adds a test for each of the three
extra types.
See http://code.djangoproject.com/ticket/11058
---
django/contrib/admin/validation.py | 20 ++++++++++++++++----
docs/ref/contrib/admin/index.txt | 6 +++---
tests/regressiontests/admin_validation/tests.py | 22 ++++++++++++++++++++++
tests/regressiontests/modeladmin/tests.py | 4 ++--
4 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 027db63..deca4b8 100644
a
|
b
|
def validate(cls, model):
|
23 | 23 | opts = model._meta |
24 | 24 | validate_base(cls, model) |
25 | 25 | |
| 26 | # To handle values other than pure strings, we'll maintain a list of |
| 27 | # possible values allowing the possibility of using ModelAdmin fields |
| 28 | # or arbitrary callables in list_display_links. |
| 29 | list_display_fields = set() |
| 30 | |
26 | 31 | # list_display |
27 | 32 | if hasattr(cls, 'list_display'): |
28 | 33 | check_isseq(cls, 'list_display', cls.list_display) |
29 | 34 | for idx, field in enumerate(cls.list_display): |
| 35 | list_display_fields.add(field) |
| 36 | |
30 | 37 | if not callable(field): |
31 | 38 | if not hasattr(cls, field): |
32 | 39 | if not hasattr(model, field): |
33 | 40 | try: |
34 | | opts.get_field(field) |
| 41 | f = opts.get_field(field) |
35 | 42 | except models.FieldDoesNotExist: |
36 | 43 | raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." |
37 | 44 | % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) |
38 | 45 | else: |
39 | 46 | # getattr(model, field) could be an X_RelatedObjectsDescriptor |
40 | 47 | f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) |
| 48 | |
41 | 49 | if isinstance(f, models.ManyToManyField): |
42 | 50 | raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." |
43 | 51 | % (cls.__name__, idx, field)) |
44 | 52 | |
| 53 | list_display_fields.add(f) |
| 54 | |
45 | 55 | # list_display_links |
46 | 56 | if hasattr(cls, 'list_display_links'): |
47 | 57 | check_isseq(cls, 'list_display_links', cls.list_display_links) |
| 58 | |
48 | 59 | for idx, field in enumerate(cls.list_display_links): |
49 | | fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field) |
50 | | if field not in cls.list_display: |
| 60 | if field not in list_display_fields: |
51 | 61 | raise ImproperlyConfigured("'%s.list_display_links[%d]'" |
52 | | "refers to '%s' which is not defined in 'list_display'." |
| 62 | " refers to '%s' which is not defined in 'list_display'." |
53 | 63 | % (cls.__name__, idx, field)) |
54 | 64 | |
| 65 | del list_display_fields |
| 66 | |
55 | 67 | # list_filter |
56 | 68 | if hasattr(cls, 'list_filter'): |
57 | 69 | check_isseq(cls, 'list_filter', cls.list_filter) |
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 2815b5d..bd1485f 100644
a
|
b
|
subclass::
|
479 | 479 | By default, the change list page will link the first column -- the first |
480 | 480 | field specified in ``list_display`` -- to the change page for each item. |
481 | 481 | But ``list_display_links`` lets you change which columns are linked. Set |
482 | | ``list_display_links`` to a list or tuple of field names (in the same |
| 482 | ``list_display_links`` to a list or tuple of field (in the same |
483 | 483 | format as ``list_display``) to link. |
484 | 484 | |
485 | | ``list_display_links`` can specify one or many field names. As long as the |
486 | | field names appear in ``list_display``, Django doesn't care how many (or |
| 485 | ``list_display_links`` can specify one or many field. As long as the |
| 486 | fields appear in ``list_display``, Django doesn't care how many (or |
487 | 487 | how few) fields are linked. The only requirement is: If you want to use |
488 | 488 | ``list_display_links``, you must define ``list_display``. |
489 | 489 | |
diff --git a/tests/regressiontests/admin_validation/tests.py b/tests/regressiontests/admin_validation/tests.py
index 1872ca5..207933b 100644
a
|
b
|
class ValidationTestCase(TestCase):
|
242 | 242 | |
243 | 243 | validate(FieldsOnFormOnlyAdmin, Song) |
244 | 244 | |
| 245 | def test_list_display_links(self): |
| 246 | """ |
| 247 | Confirm that list_display_links supports model fields, model admin |
| 248 | methods and arbitrary callables |
| 249 | """ |
| 250 | |
| 251 | def fancy_formatter(obj): |
| 252 | return "Custom display of %s" % obj |
245 | 253 | |
| 254 | class SongAdmin(admin.ModelAdmin): |
| 255 | foo_date = fancy_formatter |
| 256 | |
| 257 | list_display = ("title", "album", "original_release", |
| 258 | "readonly_method_on_model", "short_title", |
| 259 | foo_date) |
| 260 | list_display_links = ("title", "album", "original_release", |
| 261 | "readonly_method_on_model", "short_title", |
| 262 | foo_date) |
246 | 263 | |
| 264 | def short_title(self, obj): |
| 265 | # In real code this would use truncatewords/chars: |
| 266 | return obj.title[:20] |
| 267 | short_title.short_description = "Short Title" |
247 | 268 | |
| 269 | validate(SongAdmin, Song) |
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
index 042c1b7..3eeb845 100644
a
|
b
|
class ValidationTests(unittest.TestCase):
|
794 | 794 | |
795 | 795 | self.assertRaisesRegexp( |
796 | 796 | ImproperlyConfigured, |
797 | | "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'non_existent_field' that is neither a field, method or property of model 'ValidationTestModel'.", |
| 797 | "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'non_existent_field' which is not defined in 'list_display'.", |
798 | 798 | validate, |
799 | 799 | ValidationTestModelAdmin, |
800 | 800 | ValidationTestModel, |
… |
… |
class ValidationTests(unittest.TestCase):
|
805 | 805 | |
806 | 806 | self.assertRaisesRegexp( |
807 | 807 | ImproperlyConfigured, |
808 | | "'ValidationTestModelAdmin.list_display_links\[0\]'refers to 'name' which is not defined in 'list_display'.", |
| 808 | "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'name' which is not defined in 'list_display'.", |
809 | 809 | validate, |
810 | 810 | ValidationTestModelAdmin, |
811 | 811 | ValidationTestModel, |