Commit b2ec6473 authored by Brian Rosner's avatar Brian Rosner
Browse files

Fixed #7503 -- Allow callables in list_display. This also does a lookup on the...

Fixed #7503 -- Allow callables in list_display. This also does a lookup 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
parent 9e423b51
Loading
Loading
Loading
Loading
+35 −10
Original line number Diff line number Diff line
@@ -84,14 +84,30 @@ def result_headers(cl):
            elif field_name == '__str__':
                header = smart_str(lookup_opts.verbose_name)
            else:
                attr = getattr(cl.model, field_name) # Let AttributeErrors propagate.
                if callable(field_name):
                    attr = field_name # field_name can be a callable
                else:
                    try:
                        attr = getattr(cl.model_admin, field_name)
                    except AttributeError:
                        try:
                            attr = getattr(cl.model, field_name)
                        except AttributeError:
                            raise AttributeError, \
                                "'%s' model or '%s' objects have no attribute '%s'" % \
                                    (lookup_opts.object_name, cl.model_admin.__class__, field_name)
                
                try:
                    header = attr.short_description
                except AttributeError:
                    header = field_name.replace('_', ' ')
                    if callable(field_name):
                        header = field_name.__name__
                    else:
                        header = field_name
                    header = header.replace('_', ' ')

            # It is a non-field, but perhaps one that is sortable
            admin_order_field = getattr(getattr(cl.model, field_name), "admin_order_field", None)
            admin_order_field = getattr(attr, "admin_order_field", None)
            if not admin_order_field:
                yield {"text": header}
                continue
@@ -128,19 +144,28 @@ def items_for_result(cl, result):
        try:
            f = cl.lookup_opts.get_field(field_name)
        except models.FieldDoesNotExist:
            # For non-field list_display values, the value is either a method
            # or a property.
            # For non-field list_display values, the value is either a method,
            # property or returned via a callable.
            try:
                if callable(field_name):
                    attr = field_name
                    value = attr(result)
                elif hasattr(cl.model_admin, field_name):
                    attr = getattr(cl.model_admin, field_name)
                    value = attr(result)
                else:
                    attr = getattr(result, field_name)
                    if callable(attr):
                        value = attr()
                    else:
                        value = attr
                allow_tags = getattr(attr, 'allow_tags', False)
                boolean = getattr(attr, 'boolean', False)
                if callable(attr):
                    attr = attr()
                if boolean:
                    allow_tags = True
                    result_repr = _boolean_icon(attr)
                    result_repr = _boolean_icon(value)
                else:
                    result_repr = smart_unicode(attr)
                    result_repr = smart_unicode(value)
            except (AttributeError, ObjectDoesNotExist):
                result_repr = EMPTY_CHANGELIST_VALUE
            else:
+14 −5
Original line number Diff line number Diff line
@@ -35,6 +35,15 @@ def validate(cls, model):
    if hasattr(cls, 'list_display'):
        _check_istuplew('list_display', cls.list_display)
        for idx, field in enumerate(cls.list_display):
            if not callable(field):
                if not hasattr(cls, field):
                    if not hasattr(model, field):
                        try:
                            return opts.get_field(field)
                        except models.FieldDoesNotExist:
                            raise ImproperlyConfigured("%s.list_display[%d], %r is "
                                "not a callable or an attribute of %r or found in the model %r."
                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
                        f = _check_attr_existsw("list_display[%d]" % idx, field)
                        if isinstance(f, models.ManyToManyField):
                            raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a "
+51 −24
Original line number Diff line number Diff line
@@ -201,25 +201,36 @@ Example::
If you don't set ``list_display``, the admin site will display a single column
that displays the ``__unicode__()`` representation of each object.

A few special cases to note about ``list_display``:
You have four possible values that can be used in ``list_display``:

    * If the field is a ``ForeignKey``, Django will display the
      ``__unicode__()`` of the related object.
    * A field of the model. For example::
    
    * ``ManyToManyField`` fields aren't supported, because that would entail
      executing a separate SQL statement for each row in the table. If you
      want to do this nonetheless, give your model a custom method, and add
      that method's name to ``list_display``. (See below for more on custom
      methods in ``list_display``.)
          class PersonAdmin(admin.ModelAdmin):
              list_display = ('first_name', 'last_name')
    
    * If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
      display a pretty "on" or "off" icon instead of ``True`` or ``False``.
    * A callable that accepts one parameter for the model instance. For
      example::
    
    * If the string given is a method of the model, Django will call it and
      display the output. This method should have a ``short_description``
      function attribute, for use as the header for the field.
          def upper_case_name(obj):
              return "%s %s" % (obj.first_name, obj.last_name).upper()
          upper_case_name.short_description = 'Name'
        
      Here's a full example model::
          class PersonAdmin(admin.ModelAdmin):
              list_display = (upper_case_name,)
    
    * A string representating an attribute on the ``ModelAdmin``. This behaves
      the same as the callable. For example::
      
          class PersonAdmin(admin.ModelAdmin):
              list_display = ('upper_case_name',)
              
              def upper_case_name(self, obj):
                return "%s %s" % (obj.first_name, obj.last_name).upper()
              upper_case_name.short_description = 'Name'
    
    * A string representating an attribute on the model. This behaves almost
      the same as the callable, but ``self`` in this context is the model
      instance. Here's a full model example::

          class Person(models.Model):
              name = models.CharField(max_length=50)
@@ -232,9 +243,24 @@ A few special cases to note about ``list_display``:
          class PersonAdmin(admin.ModelAdmin):
              list_display = ('name', 'decade_born_in')

    * If the string given is a method of the model, Django will HTML-escape the
      output by default. If you'd rather not escape the output of the method,
      give the method an ``allow_tags`` attribute whose value is ``True``.
A few special cases to note about ``list_display``:

    * If the field is a ``ForeignKey``, Django will display the
      ``__unicode__()`` of the related object.

    * ``ManyToManyField`` fields aren't supported, because that would entail
      executing a separate SQL statement for each row in the table. If you
      want to do this nonetheless, give your model a custom method, and add
      that method's name to ``list_display``. (See below for more on custom
      methods in ``list_display``.)

    * If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
      display a pretty "on" or "off" icon instead of ``True`` or ``False``.

    * If the string given is a method of the model, ``ModelAdmin`` or a
      callable, Django will HTML-escape the output by default. If you'd rather
      not escape the output of the method, give the method an ``allow_tags``
      attribute whose value is ``True``.
      
      Here's a full example model::

@@ -250,9 +276,10 @@ A few special cases to note about ``list_display``:
          class PersonAdmin(admin.ModelAdmin):
              list_display = ('first_name', 'last_name', 'colored_name')

    * If the string given is a method of the model that returns True or False
      Django will display a pretty "on" or "off" icon if you give the method a
      ``boolean`` attribute whose value is ``True``.
    * If the string given is a method of the model, ``ModelAdmin`` or a
      callable that returns True or False Django will display a pretty "on" or
      "off" icon if you give the method a ``boolean`` attribute whose value is
      ``True``.

      Here's a full example model::