From 10a04188c0d002509507a4e77756742f8729379d Mon Sep 17 00:00:00 2001
From: Chris Adams <chris@improbable.org>
Date: Thu, 23 Dec 2010 10:30:27 -0500
Subject: [PATCH] ModelAdmin validation: allow non-model fields in list_display_links (#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 | 18 +++++++++++++++---
docs/ref/contrib/admin/index.txt | 10 +++++-----
tests/regressiontests/admin_validation/tests.py | 22 ++++++++++++++++++++++
3 files changed, 42 insertions(+), 8 deletions(-)
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index bee2891..9a653ea 100644
a
|
b
|
def validate(cls, model):
|
22 | 22 | opts = model._meta |
23 | 23 | validate_base(cls, model) |
24 | 24 | |
| 25 | # To handle values other than pure strings, we'll maintain a list of |
| 26 | # possible values allowing the possibility of using ModelAdmin fields |
| 27 | # or arbitrary callables in list_display_links. |
| 28 | list_display_fields = set() |
| 29 | |
25 | 30 | # list_display |
26 | 31 | if hasattr(cls, 'list_display'): |
27 | 32 | check_isseq(cls, 'list_display', cls.list_display) |
28 | 33 | for idx, field in enumerate(cls.list_display): |
| 34 | list_display_fields.add(field) |
| 35 | |
29 | 36 | if not callable(field): |
30 | 37 | if not hasattr(cls, field): |
31 | 38 | if not hasattr(model, field): |
32 | 39 | try: |
33 | | opts.get_field(field) |
| 40 | f = opts.get_field(field) |
34 | 41 | except models.FieldDoesNotExist: |
35 | 42 | raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." |
36 | 43 | % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) |
37 | 44 | else: |
38 | 45 | # getattr(model, field) could be an X_RelatedObjectsDescriptor |
39 | 46 | f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) |
| 47 | |
40 | 48 | if isinstance(f, models.ManyToManyField): |
41 | 49 | raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." |
42 | 50 | % (cls.__name__, idx, field)) |
43 | 51 | |
| 52 | list_display_fields.add(f) |
| 53 | |
44 | 54 | # list_display_links |
45 | 55 | if hasattr(cls, 'list_display_links'): |
46 | 56 | check_isseq(cls, 'list_display_links', cls.list_display_links) |
| 57 | |
47 | 58 | for idx, field in enumerate(cls.list_display_links): |
48 | | fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field) |
49 | | if field not in cls.list_display: |
| 59 | if field not in list_display_fields: |
50 | 60 | raise ImproperlyConfigured("'%s.list_display_links[%d]'" |
51 | 61 | "refers to '%s' which is not defined in 'list_display'." |
52 | 62 | % (cls.__name__, idx, field)) |
53 | 63 | |
| 64 | del list_display_fields |
| 65 | |
54 | 66 | # list_filter |
55 | 67 | if hasattr(cls, 'list_filter'): |
56 | 68 | 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 da17ed0..03c5706 100644
a
|
b
|
be linked to the "change" page for an object.
|
405 | 405 | By default, the change list page will link the first column -- the first field |
406 | 406 | specified in ``list_display`` -- to the change page for each item. But |
407 | 407 | ``list_display_links`` lets you change which columns are linked. Set |
408 | | ``list_display_links`` to a list or tuple of field names (in the same format as |
409 | | ``list_display``) to link. |
| 408 | ``list_display_links`` to a list or tuple of fields in the same format as |
| 409 | ``list_display`` to link. |
410 | 410 | |
411 | | ``list_display_links`` can specify one or many field names. As long as the |
412 | | field names appear in ``list_display``, Django doesn't care how many (or how |
413 | | few) fields are linked. The only requirement is: If you want to use |
| 411 | ``list_display_links`` can specify one or many fields. As long as the fields |
| 412 | appear in ``list_display``, Django doesn't care how many (or how few) fields |
| 413 | are linked. The only requirement is: If you want to use |
414 | 414 | ``list_display_links``, you must define ``list_display``. |
415 | 415 | |
416 | 416 | In this example, the ``first_name`` and ``last_name`` fields will be linked on |
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) |