Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #7503 -- Allow callables in list_display. This also does a look…

…up on the ModelAdmin for the method if the value is a string before looking on the model. Refs #8054. Thanks qmanic and Daniel Pope for tickets and patches.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8352 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit b2ec6473c09bcad88194cfb52252c44ed7e960aa 1 parent 9e423b5
Brian Rosner authored August 14, 2008
45  django/contrib/admin/templatetags/admin_list.py
@@ -84,14 +84,30 @@ def result_headers(cl):
84 84
             elif field_name == '__str__':
85 85
                 header = smart_str(lookup_opts.verbose_name)
86 86
             else:
87  
-                attr = getattr(cl.model, field_name) # Let AttributeErrors propagate.
  87
+                if callable(field_name):
  88
+                    attr = field_name # field_name can be a callable
  89
+                else:
  90
+                    try:
  91
+                        attr = getattr(cl.model_admin, field_name)
  92
+                    except AttributeError:
  93
+                        try:
  94
+                            attr = getattr(cl.model, field_name)
  95
+                        except AttributeError:
  96
+                            raise AttributeError, \
  97
+                                "'%s' model or '%s' objects have no attribute '%s'" % \
  98
+                                    (lookup_opts.object_name, cl.model_admin.__class__, field_name)
  99
+                
88 100
                 try:
89 101
                     header = attr.short_description
90 102
                 except AttributeError:
91  
-                    header = field_name.replace('_', ' ')
  103
+                    if callable(field_name):
  104
+                        header = field_name.__name__
  105
+                    else:
  106
+                        header = field_name
  107
+                    header = header.replace('_', ' ')
92 108
 
93 109
             # It is a non-field, but perhaps one that is sortable
94  
-            admin_order_field = getattr(getattr(cl.model, field_name), "admin_order_field", None)
  110
+            admin_order_field = getattr(attr, "admin_order_field", None)
95 111
             if not admin_order_field:
96 112
                 yield {"text": header}
97 113
                 continue
@@ -128,19 +144,28 @@ def items_for_result(cl, result):
128 144
         try:
129 145
             f = cl.lookup_opts.get_field(field_name)
130 146
         except models.FieldDoesNotExist:
131  
-            # For non-field list_display values, the value is either a method
132  
-            # or a property.
  147
+            # For non-field list_display values, the value is either a method,
  148
+            # property or returned via a callable.
133 149
             try:
134  
-                attr = getattr(result, field_name)
  150
+                if callable(field_name):
  151
+                    attr = field_name
  152
+                    value = attr(result)
  153
+                elif hasattr(cl.model_admin, field_name):
  154
+                    attr = getattr(cl.model_admin, field_name)
  155
+                    value = attr(result)
  156
+                else:
  157
+                    attr = getattr(result, field_name)
  158
+                    if callable(attr):
  159
+                        value = attr()
  160
+                    else:
  161
+                        value = attr
135 162
                 allow_tags = getattr(attr, 'allow_tags', False)
136 163
                 boolean = getattr(attr, 'boolean', False)
137  
-                if callable(attr):
138  
-                    attr = attr()
139 164
                 if boolean:
140 165
                     allow_tags = True
141  
-                    result_repr = _boolean_icon(attr)
  166
+                    result_repr = _boolean_icon(value)
142 167
                 else:
143  
-                    result_repr = smart_unicode(attr)
  168
+                    result_repr = smart_unicode(value)
144 169
             except (AttributeError, ObjectDoesNotExist):
145 170
                 result_repr = EMPTY_CHANGELIST_VALUE
146 171
             else:
19  django/contrib/admin/validation.py
@@ -35,11 +35,20 @@ def _check_attr_existsw(label, field):
35 35
     if hasattr(cls, 'list_display'):
36 36
         _check_istuplew('list_display', cls.list_display)
37 37
         for idx, field in enumerate(cls.list_display):
38  
-            f = _check_attr_existsw("list_display[%d]" % idx, field)
39  
-            if isinstance(f, models.ManyToManyField):
40  
-                raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a "
41  
-                        "ManyToManyField which is not supported."
42  
-                        % (cls.__name__, idx, field))
  38
+            if not callable(field):
  39
+                if not hasattr(cls, field):
  40
+                    if not hasattr(model, field):
  41
+                        try:
  42
+                            return opts.get_field(field)
  43
+                        except models.FieldDoesNotExist:
  44
+                            raise ImproperlyConfigured("%s.list_display[%d], %r is "
  45
+                                "not a callable or an attribute of %r or found in the model %r."
  46
+                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
  47
+                        f = _check_attr_existsw("list_display[%d]" % idx, field)
  48
+                        if isinstance(f, models.ManyToManyField):
  49
+                            raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a "
  50
+                                "ManyToManyField which is not supported."
  51
+                                % (cls.__name__, idx, field))
43 52
 
44 53
     # list_display_links
45 54
     if hasattr(cls, 'list_display_links'):
75  docs/admin.txt
@@ -201,25 +201,36 @@ Example::
201 201
 If you don't set ``list_display``, the admin site will display a single column
202 202
 that displays the ``__unicode__()`` representation of each object.
203 203
 
204  
-A few special cases to note about ``list_display``:
205  
-
206  
-    * If the field is a ``ForeignKey``, Django will display the
207  
-      ``__unicode__()`` of the related object.
208  
-
209  
-    * ``ManyToManyField`` fields aren't supported, because that would entail
210  
-      executing a separate SQL statement for each row in the table. If you
211  
-      want to do this nonetheless, give your model a custom method, and add
212  
-      that method's name to ``list_display``. (See below for more on custom
213  
-      methods in ``list_display``.)
214  
-
215  
-    * If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
216  
-      display a pretty "on" or "off" icon instead of ``True`` or ``False``.
217  
-
218  
-    * If the string given is a method of the model, Django will call it and
219  
-      display the output. This method should have a ``short_description``
220  
-      function attribute, for use as the header for the field.
  204
+You have four possible values that can be used in ``list_display``:
221 205
 
222  
-      Here's a full example model::
  206
+    * A field of the model. For example::
  207
+    
  208
+          class PersonAdmin(admin.ModelAdmin):
  209
+              list_display = ('first_name', 'last_name')
  210
+    
  211
+    * A callable that accepts one parameter for the model instance. For
  212
+      example::
  213
+    
  214
+          def upper_case_name(obj):
  215
+              return "%s %s" % (obj.first_name, obj.last_name).upper()
  216
+          upper_case_name.short_description = 'Name'
  217
+        
  218
+          class PersonAdmin(admin.ModelAdmin):
  219
+              list_display = (upper_case_name,)
  220
+    
  221
+    * A string representating an attribute on the ``ModelAdmin``. This behaves
  222
+      the same as the callable. For example::
  223
+      
  224
+          class PersonAdmin(admin.ModelAdmin):
  225
+              list_display = ('upper_case_name',)
  226
+              
  227
+              def upper_case_name(self, obj):
  228
+                return "%s %s" % (obj.first_name, obj.last_name).upper()
  229
+              upper_case_name.short_description = 'Name'
  230
+    
  231
+    * A string representating an attribute on the model. This behaves almost
  232
+      the same as the callable, but ``self`` in this context is the model
  233
+      instance. Here's a full model example::
223 234
 
224 235
           class Person(models.Model):
225 236
               name = models.CharField(max_length=50)
@@ -232,10 +243,25 @@ A few special cases to note about ``list_display``:
232 243
           class PersonAdmin(admin.ModelAdmin):
233 244
               list_display = ('name', 'decade_born_in')
234 245
 
235  
-    * If the string given is a method of the model, Django will HTML-escape the
236  
-      output by default. If you'd rather not escape the output of the method,
237  
-      give the method an ``allow_tags`` attribute whose value is ``True``.
  246
+A few special cases to note about ``list_display``:
  247
+
  248
+    * If the field is a ``ForeignKey``, Django will display the
  249
+      ``__unicode__()`` of the related object.
  250
+
  251
+    * ``ManyToManyField`` fields aren't supported, because that would entail
  252
+      executing a separate SQL statement for each row in the table. If you
  253
+      want to do this nonetheless, give your model a custom method, and add
  254
+      that method's name to ``list_display``. (See below for more on custom
  255
+      methods in ``list_display``.)
  256
+
  257
+    * If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
  258
+      display a pretty "on" or "off" icon instead of ``True`` or ``False``.
238 259
 
  260
+    * If the string given is a method of the model, ``ModelAdmin`` or a
  261
+      callable, Django will HTML-escape the output by default. If you'd rather
  262
+      not escape the output of the method, give the method an ``allow_tags``
  263
+      attribute whose value is ``True``.
  264
+      
239 265
       Here's a full example model::
240 266
 
241 267
           class Person(models.Model):
@@ -250,9 +276,10 @@ A few special cases to note about ``list_display``:
250 276
           class PersonAdmin(admin.ModelAdmin):
251 277
               list_display = ('first_name', 'last_name', 'colored_name')
252 278
 
253  
-    * If the string given is a method of the model that returns True or False
254  
-      Django will display a pretty "on" or "off" icon if you give the method a
255  
-      ``boolean`` attribute whose value is ``True``.
  279
+    * If the string given is a method of the model, ``ModelAdmin`` or a
  280
+      callable that returns True or False Django will display a pretty "on" or
  281
+      "off" icon if you give the method a ``boolean`` attribute whose value is
  282
+      ``True``.
256 283
 
257 284
       Here's a full example model::
258 285
 

0 notes on commit b2ec647

Please sign in to comment.
Something went wrong with that request. Please try again.