From b2ec6473c09bcad88194cfb52252c44ed7e960aa Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Thu, 14 Aug 2008 20:12:19 +0000 Subject: [PATCH] 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 --- .../contrib/admin/templatetags/admin_list.py | 45 ++++++++--- django/contrib/admin/validation.py | 19 +++-- docs/admin.txt | 75 +++++++++++++------ 3 files changed, 100 insertions(+), 39 deletions(-) diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 87fad70ec3..c837cbe72a 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -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: - attr = getattr(result, field_name) + 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: diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index a42f2eb985..48e35c68b3 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -35,11 +35,20 @@ def validate(cls, model): if hasattr(cls, 'list_display'): _check_istuplew('list_display', cls.list_display) for idx, field in enumerate(cls.list_display): - f = _check_attr_existsw("list_display[%d]" % idx, field) - if isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a " - "ManyToManyField which is not supported." - % (cls.__name__, idx, field)) + 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 " + "ManyToManyField which is not supported." + % (cls.__name__, idx, field)) # list_display_links if hasattr(cls, 'list_display_links'): diff --git a/docs/admin.txt b/docs/admin.txt index dba28ecbdf..03c0154331 100644 --- a/docs/admin.txt +++ b/docs/admin.txt @@ -201,6 +201,48 @@ Example:: If you don't set ``list_display``, the admin site will display a single column that displays the ``__unicode__()`` representation of each object. +You have four possible values that can be used in ``list_display``: + + * A field of the model. For example:: + + class PersonAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name') + + * A callable that accepts one parameter for the model instance. For + example:: + + def upper_case_name(obj): + return "%s %s" % (obj.first_name, obj.last_name).upper() + upper_case_name.short_description = 'Name' + + 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) + birthday = models.DateField() + + def decade_born_in(self): + return self.birthday.strftime('%Y')[:3] + "0's" + decade_born_in.short_description = 'Birth decade' + + class PersonAdmin(admin.ModelAdmin): + list_display = ('name', 'decade_born_in') + A few special cases to note about ``list_display``: * If the field is a ``ForeignKey``, Django will display the @@ -215,27 +257,11 @@ A few special cases to note about ``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, 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. - - Here's a full example model:: - - class Person(models.Model): - name = models.CharField(max_length=50) - birthday = models.DateField() - - def decade_born_in(self): - return self.birthday.strftime('%Y')[:3] + "0's" - decade_born_in.short_description = 'Birth decade' - - 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``. - + * 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:: class Person(models.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::