1 | From 0a07e0d820f31f47c0052cf62a6b2b7110d5348a Mon Sep 17 00:00:00 2001
|
---|
2 | From: Chris Adams <chris@improbable.org>
|
---|
3 | Date: Mon, 6 Dec 2010 14:37:05 -0500
|
---|
4 | Subject: [PATCH] ModelAdmin validation: allow non-model fields in list_display_links (#11058)
|
---|
5 |
|
---|
6 | In addition to model field names, list_display accepts callables, ModelAdmin
|
---|
7 | attributes or Model attributes.
|
---|
8 |
|
---|
9 | This patch reuses the list_display resolution for these values for
|
---|
10 | list_display_links validation as well and adds a test for each of the three
|
---|
11 | extra types.
|
---|
12 |
|
---|
13 | See http://code.djangoproject.com/ticket/11058
|
---|
14 | ---
|
---|
15 | django/contrib/admin/validation.py | 18 +++++++++++--
|
---|
16 | docs/ref/contrib/admin/index.txt | 10 ++++----
|
---|
17 | tests/regressiontests/admin_validation/tests.py | 30 ++++++++++++++++++++++-
|
---|
18 | tests/regressiontests/modeladmin/models.py | 2 +-
|
---|
19 | 4 files changed, 50 insertions(+), 10 deletions(-)
|
---|
20 |
|
---|
21 | diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
|
---|
22 | index bee2891..9a653ea 100644
|
---|
23 | --- a/django/contrib/admin/validation.py
|
---|
24 | +++ b/django/contrib/admin/validation.py
|
---|
25 | @@ -22,35 +22,47 @@ def validate(cls, model):
|
---|
26 | opts = model._meta
|
---|
27 | validate_base(cls, model)
|
---|
28 |
|
---|
29 | + # To handle values other than pure strings, we'll maintain a list of
|
---|
30 | + # possible values allowing the possibility of using ModelAdmin fields
|
---|
31 | + # or arbitrary callables in list_display_links.
|
---|
32 | + list_display_fields = set()
|
---|
33 | +
|
---|
34 | # list_display
|
---|
35 | if hasattr(cls, 'list_display'):
|
---|
36 | check_isseq(cls, 'list_display', cls.list_display)
|
---|
37 | for idx, field in enumerate(cls.list_display):
|
---|
38 | + list_display_fields.add(field)
|
---|
39 | +
|
---|
40 | if not callable(field):
|
---|
41 | if not hasattr(cls, field):
|
---|
42 | if not hasattr(model, field):
|
---|
43 | try:
|
---|
44 | - opts.get_field(field)
|
---|
45 | + f = opts.get_field(field)
|
---|
46 | except models.FieldDoesNotExist:
|
---|
47 | raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r."
|
---|
48 | % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
|
---|
49 | else:
|
---|
50 | # getattr(model, field) could be an X_RelatedObjectsDescriptor
|
---|
51 | f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field)
|
---|
52 | +
|
---|
53 | if isinstance(f, models.ManyToManyField):
|
---|
54 | raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported."
|
---|
55 | % (cls.__name__, idx, field))
|
---|
56 |
|
---|
57 | + list_display_fields.add(f)
|
---|
58 | +
|
---|
59 | # list_display_links
|
---|
60 | if hasattr(cls, 'list_display_links'):
|
---|
61 | check_isseq(cls, 'list_display_links', cls.list_display_links)
|
---|
62 | +
|
---|
63 | for idx, field in enumerate(cls.list_display_links):
|
---|
64 | - fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field)
|
---|
65 | - if field not in cls.list_display:
|
---|
66 | + if field not in list_display_fields:
|
---|
67 | raise ImproperlyConfigured("'%s.list_display_links[%d]'"
|
---|
68 | "refers to '%s' which is not defined in 'list_display'."
|
---|
69 | % (cls.__name__, idx, field))
|
---|
70 |
|
---|
71 | + del list_display_fields
|
---|
72 | +
|
---|
73 | # list_filter
|
---|
74 | if hasattr(cls, 'list_filter'):
|
---|
75 | check_isseq(cls, 'list_filter', cls.list_filter)
|
---|
76 | diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
|
---|
77 | index 0550576..0cc6c3c 100644
|
---|
78 | --- a/docs/ref/contrib/admin/index.txt
|
---|
79 | +++ b/docs/ref/contrib/admin/index.txt
|
---|
80 | @@ -404,12 +404,12 @@ be linked to the "change" page for an object.
|
---|
81 | By default, the change list page will link the first column -- the first field
|
---|
82 | specified in ``list_display`` -- to the change page for each item. But
|
---|
83 | ``list_display_links`` lets you change which columns are linked. Set
|
---|
84 | -``list_display_links`` to a list or tuple of field names (in the same format as
|
---|
85 | -``list_display``) to link.
|
---|
86 | +``list_display_links`` to a list or tuple of fields in the same format as
|
---|
87 | +``list_display`` to link.
|
---|
88 |
|
---|
89 | -``list_display_links`` can specify one or many field names. As long as the
|
---|
90 | -field names appear in ``list_display``, Django doesn't care how many (or how
|
---|
91 | -few) fields are linked. The only requirement is: If you want to use
|
---|
92 | +``list_display_links`` can specify one or many fields. As long as the fields
|
---|
93 | +appear in ``list_display``, Django doesn't care how many (or how few) fields
|
---|
94 | +are linked. The only requirement is: If you want to use
|
---|
95 | ``list_display_links``, you must define ``list_display``.
|
---|
96 |
|
---|
97 | In this example, the ``first_name`` and ``last_name`` fields will be linked on
|
---|
98 | diff --git a/tests/regressiontests/admin_validation/tests.py b/tests/regressiontests/admin_validation/tests.py
|
---|
99 | index 9166360..aa44e92 100644
|
---|
100 | --- a/tests/regressiontests/admin_validation/tests.py
|
---|
101 | +++ b/tests/regressiontests/admin_validation/tests.py
|
---|
102 | @@ -6,7 +6,9 @@ from models import Song
|
---|
103 |
|
---|
104 |
|
---|
105 | class ValidationTestCase(TestCase):
|
---|
106 | +
|
---|
107 | def test_readonly_and_editable(self):
|
---|
108 | +
|
---|
109 | class SongAdmin(admin.ModelAdmin):
|
---|
110 | readonly_fields = ["original_release"]
|
---|
111 | fieldsets = [
|
---|
112 | @@ -14,5 +16,31 @@ class ValidationTestCase(TestCase):
|
---|
113 | "fields": ["title", "original_release"],
|
---|
114 | }),
|
---|
115 | ]
|
---|
116 | -
|
---|
117 | +
|
---|
118 | + validate(SongAdmin, Song)
|
---|
119 | +
|
---|
120 | + def test_list_display_links(self):
|
---|
121 | + """
|
---|
122 | + Confirm that list_display_links supports model fields, model admin
|
---|
123 | + methods and arbitrary callables
|
---|
124 | + """
|
---|
125 | +
|
---|
126 | + def fancy_formatter(obj):
|
---|
127 | + return "Custom display of %s" % obj
|
---|
128 | +
|
---|
129 | + class SongAdmin(admin.ModelAdmin):
|
---|
130 | + foo_date = fancy_formatter
|
---|
131 | +
|
---|
132 | + list_display = ("title", "album", "original_release",
|
---|
133 | + "readonly_method_on_model", "short_title",
|
---|
134 | + foo_date)
|
---|
135 | + list_display_links = ("title", "album", "original_release",
|
---|
136 | + "readonly_method_on_model", "short_title",
|
---|
137 | + foo_date)
|
---|
138 | +
|
---|
139 | + def short_title(self, obj):
|
---|
140 | + # In real code this would use truncatewords/chars:
|
---|
141 | + return obj.title[:20]
|
---|
142 | + short_title.short_description = "Short Title"
|
---|
143 | +
|
---|
144 | validate(SongAdmin, Song)
|
---|
145 | diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
|
---|
146 | index 36ea416..8ca2031 100644
|
---|
147 | --- a/tests/regressiontests/modeladmin/models.py
|
---|
148 | +++ b/tests/regressiontests/modeladmin/models.py
|
---|
149 | @@ -653,7 +653,7 @@ ImproperlyConfigured: 'ValidationTestModelAdmin.list_display_links' must be a li
|
---|
150 | >>> validate(ValidationTestModelAdmin, ValidationTestModel)
|
---|
151 | Traceback (most recent call last):
|
---|
152 | ...
|
---|
153 | -ImproperlyConfigured: 'ValidationTestModelAdmin.list_display_links[0]' refers to 'non_existent_field' that is neither a field, method or property of model 'ValidationTestModel'.
|
---|
154 | +ImproperlyConfigured: 'ValidationTestModelAdmin.list_display_links[0]'refers to 'non_existent_field' which is not defined in 'list_display'.
|
---|
155 |
|
---|
156 | >>> class ValidationTestModelAdmin(ModelAdmin):
|
---|
157 | ... list_display_links = ('name',)
|
---|
158 | --
|
---|
159 | 1.7.0.2
|
---|
160 |
|
---|