Ticket #11058: 1.2.3...ticket-11058-list_display_links.txt

File 1.2.3...ticket-11058-list_display_links.txt, 7.1 KB (added by acdha, 5 years ago)

New patch against 1.2.3

Line 
1From 0a07e0d820f31f47c0052cf62a6b2b7110d5348a Mon Sep 17 00:00:00 2001
2From: Chris Adams <chris@improbable.org>
3Date: Mon, 6 Dec 2010 14:37:05 -0500
4Subject: [PATCH] ModelAdmin validation: allow non-model fields in list_display_links (#11058)
5
6In addition to model field names, list_display accepts callables, ModelAdmin
7attributes or Model attributes.
8
9This patch reuses the list_display resolution for these values for
10list_display_links validation as well and adds a test for each of the three
11extra types.
12
13See 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
21diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
22index 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)
76diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
77index 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
98diff --git a/tests/regressiontests/admin_validation/tests.py b/tests/regressiontests/admin_validation/tests.py
99index 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)
145diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
146index 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--
1591.7.0.2
160
Back to Top