Index: django/contrib/admin/validation.py
===================================================================
--- django/contrib/admin/validation.py	(revision 8308)
+++ django/contrib/admin/validation.py	(working copy)
@@ -7,6 +7,7 @@
 from django.db import models
 from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model
 from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
+from django.contrib.admin.options import ListColumn
 from django.contrib.admin.options import HORIZONTAL, VERTICAL
 
 def validate(cls, model):
@@ -35,7 +36,10 @@
     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(field, ListColumn):
+                f = _check_attr_existsw("list_display[%d]" % idx, field.field_name)
+            else:
+                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."
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 8308)
+++ django/contrib/admin/options.py	(working copy)
@@ -9,8 +9,9 @@
 from django.db import models, transaction
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, render_to_response
-from django.utils.html import escape
+from django.utils.html import escape, conditional_escape
 from django.utils.safestring import mark_safe
+from django.utils.encoding import smart_unicode, smart_str, force_unicode
 from django.utils.text import capfirst, get_text_list
 from django.utils.translation import ugettext as _
 from django.utils.encoding import force_unicode
@@ -905,3 +906,127 @@
                 self.extend(inline_formset.non_form_errors())
                 for errors_in_inline_form in inline_formset.errors:
                     self.extend(errors_in_inline_form.values())
+
+
+class ListColumn(object):
+	def __init__(self, field_name, header=None, filter='', load_filters=[], order_field=None, value_map=None):
+		self.field_name = field_name
+		self.header = header
+		self.filter = filter
+		self.load_filters = load_filters
+		self.order_field = order_field
+		self.value_map = value_map
+		self._nowrap = False
+
+	def for_model(self, model):
+		"""Create a new ListColumn instance where unset properties in this
+		ListColumn have been filled in by inspecting the Model provided."""
+		
+		try:
+			f = model._meta.get_field(self.field_name)
+		except models.FieldDoesNotExist:
+			return AttributeListColumn(model, self.field_name,
+				header=self.header,
+				filter=self.filter,
+				load_filters=self.load_filters,
+				order_field=self.order_field
+			)
+		else:
+			return FieldListColumn(f, self.field_name,
+				header=self.header,
+				filter=self.filter,
+				load_filters=self.load_filters,
+				order_field=self.order_field
+			)
+
+	def get_value_display(self, result):
+		from django.template import Context, Parser, get_library
+		field_value = self.get_value(result)
+		if self.value_map:
+			field_value = self.value_map.get(field_value, None)
+
+		if field_value is None:
+			return EMPTY_CHANGELIST_VALUE
+		if field_value == '':
+			return mark_safe('&nbsp;')
+
+		# apply filters by faking a template token
+		if self.filter:
+			p = Parser([])
+			for lib in ['django.contrib.admin.templatetags.admin_list'] + self.load_filters:
+				p.add_library(get_library(lib))
+			fe = p.compile_filter('val|' + self.filter)
+			return fe.resolve(Context({'val': field_value}))
+
+		return smart_unicode(field_value)
+
+
+class FieldListColumn(ListColumn):
+	def __init__(self, field, *args, **kwargs):
+		super(FieldListColumn, self).__init__(*args, **kwargs)
+		self.field = field 
+
+		if not self.header:
+			self.header = field.verbose_name
+
+		if isinstance(field.rel, models.ManyToOneRel):
+			if field.null:
+				self.order_field = None
+		elif not self.order_field:
+			self.order_field = self.field_name
+
+		if not self.filter:
+			if isinstance(field, models.DateTimeField):
+				self.filter = 'admin_datetime_format'
+			elif isinstance(field, models.DateField):
+				self.filter = 'admin_date_format'
+			elif isinstance(field, models.TimeField):
+				self.filter = 'admin_time_format'
+			elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
+				self.filter = 'boolean_icon'
+			elif isinstance(field, models.DecimalField):
+				self.filter = 'floatformat:%d' % field.decimal_places
+
+		if isinstance(field, models.DateField) or isinstance(field, models.TimeField):
+			self._nowrap = True	
+
+		if not self.value_map and field.choices:
+			self.value_map = dict(field.choices)
+
+	def get_value(self, result):
+		return getattr(result, self.field_name, None)
+
+
+class AttributeListColumn(ListColumn):
+	def __init__(self, model, *args, **kwargs):
+		super(AttributeListColumn, self).__init__(*args, **kwargs)
+		attr = getattr(model, self.field_name, None)
+		
+		if attr is not None and not self.filter:
+			if getattr(attr, 'boolean', False):
+				self.filter = 'boolean_icon'
+			elif getattr(attr, 'allow_tags', False):
+				self.filter = 'safe'
+
+		if self.header is None:
+			if self.field_name == '__unicode__':
+				self.header = force_unicode(model._meta.verbose_name)
+			elif self.field_name == '__str__':
+				self.header = smart_str(model._meta.verbose_name)
+			elif attr is not None:
+				try:
+					self.header = attr.short_description
+				except AttributeError:
+					pass
+				
+			if not self.header:	
+				self.header = self.field_name.replace('_', ' ')
+
+		if attr is not None and not self.order_field:
+			self.order_field = getattr(attr, "admin_order_field", None)
+
+	def get_value(self, result):
+		attr = getattr(result, self.field_name, None)
+		if callable(attr):
+			return attr()
+		return attr
Index: django/contrib/admin/__init__.py
===================================================================
--- django/contrib/admin/__init__.py	(revision 8308)
+++ django/contrib/admin/__init__.py	(working copy)
@@ -1,5 +1,6 @@
 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
 from django.contrib.admin.options import StackedInline, TabularInline
+from django.contrib.admin.options import ListColumn
 from django.contrib.admin.sites import AdminSite, site
 
 def autodiscover():
Index: django/contrib/admin/templatetags/admin_list.py
===================================================================
--- django/contrib/admin/templatetags/admin_list.py	(revision 8308)
+++ django/contrib/admin/templatetags/admin_list.py	(working copy)
@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.contrib.admin.options import ListColumn
 from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
 from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
 from django.core.exceptions import ObjectDoesNotExist
@@ -68,149 +69,84 @@
     }
 pagination = register.inclusion_tag('admin/pagination.html')(pagination)
 
-def result_headers(cl):
-    lookup_opts = cl.lookup_opts
 
-    for i, field_name in enumerate(cl.list_display):
-        try:
-            f = lookup_opts.get_field(field_name)
-            admin_order_field = None
-        except models.FieldDoesNotExist:
-            # For non-field list_display values, check for the function
-            # attribute "short_description". If that doesn't exist, fall back
-            # to the method name. And __str__ and __unicode__ are special-cases.
-            if field_name == '__unicode__':
-                header = force_unicode(lookup_opts.verbose_name)
-            elif field_name == '__str__':
-                header = smart_str(lookup_opts.verbose_name)
-            else:
-                attr = getattr(cl.model, field_name) # Let AttributeErrors propagate.
-                try:
-                    header = attr.short_description
-                except AttributeError:
-                    header = field_name.replace('_', ' ')
+def boolean_icon(field_val):
+    if field_val is None:
+	v = 'unknown'
+    else:
+	v = field_val and 'yes' or 'no'
+    return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, v, field_val))
+boolean_icon = register.filter(boolean_icon)
 
-            # It is a non-field, but perhaps one that is sortable
-            admin_order_field = getattr(getattr(cl.model, field_name), "admin_order_field", None)
-            if not admin_order_field:
-                yield {"text": header}
-                continue
 
-            # So this _is_ a sortable non-field.  Go to the yield
-            # after the else clause.
-        else:
-            if isinstance(f.rel, models.ManyToOneRel) and f.null:
-                yield {"text": f.verbose_name}
-                continue
-            else:
-                header = f.verbose_name
+def admin_datetime_format(field_val):
+    (date_format, datetime_format, time_format) = get_date_formats()
+    return capfirst(dateformat.format(field_val, datetime_format))
+admin_datetime_format = register.filter(admin_datetime_format)
 
-        th_classes = []
-        new_order_type = 'asc'
-        if field_name == cl.order_field or admin_order_field == cl.order_field:
-            th_classes.append('sorted %sending' % cl.order_type.lower())
-            new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
 
-        yield {"text": header,
-               "sortable": True,
-               "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
-               "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')}
+def admin_date_format(field_val):
+    (date_format, datetime_format, time_format) = get_date_formats()
+    return capfirst(dateformat.format(field_val, date_format))
+admin_date_format = register.filter(admin_date_format)
 
-def _boolean_icon(field_val):
-    BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
-    return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val))
 
-def items_for_result(cl, result):
-    first = True
-    pk = cl.lookup_opts.pk.attname
-    for field_name in cl.list_display:
-        row_class = ''
-        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.
-            try:
-                attr = getattr(result, field_name)
-                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)
-                else:
-                    result_repr = smart_unicode(attr)
-            except (AttributeError, ObjectDoesNotExist):
-                result_repr = EMPTY_CHANGELIST_VALUE
-            else:
-                # Strip HTML tags in the resulting text, except if the
-                # function has an "allow_tags" attribute set to True.
-                if not allow_tags:
-                    result_repr = escape(result_repr)
-                else:
-                    result_repr = mark_safe(result_repr)
-        else:
-            field_val = getattr(result, f.attname)
+def admin_time_format(field_val):
+    (date_format, datetime_format, time_format) = get_date_formats()
+    return capfirst(dateformat.format(field_val, time_format))
+admin_time_format = register.filter(admin_time_format)
 
-            if isinstance(f.rel, models.ManyToOneRel):
-                if field_val is not None:
-                    result_repr = escape(getattr(result, f.name))
-                else:
-                    result_repr = EMPTY_CHANGELIST_VALUE
-            # Dates and times are special: They're formatted in a certain way.
-            elif isinstance(f, models.DateField) or isinstance(f, models.TimeField):
-                if field_val:
-                    (date_format, datetime_format, time_format) = get_date_formats()
-                    if isinstance(f, models.DateTimeField):
-                        result_repr = capfirst(dateformat.format(field_val, datetime_format))
-                    elif isinstance(f, models.TimeField):
-                        result_repr = capfirst(dateformat.time_format(field_val, time_format))
-                    else:
-                        result_repr = capfirst(dateformat.format(field_val, date_format))
-                else:
-                    result_repr = EMPTY_CHANGELIST_VALUE
-                row_class = ' class="nowrap"'
-            # Booleans are special: We use images.
-            elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
-                result_repr = _boolean_icon(field_val)
-            # DecimalFields are special: Zero-pad the decimals.
-            elif isinstance(f, models.DecimalField):
-                if field_val is not None:
-                    result_repr = ('%%.%sf' % f.decimal_places) % field_val
-                else:
-                    result_repr = EMPTY_CHANGELIST_VALUE
-            # Fields with choices are special: Use the representation
-            # of the choice.
-            elif f.choices:
-                result_repr = dict(f.choices).get(field_val, EMPTY_CHANGELIST_VALUE)
-            else:
-                result_repr = escape(field_val)
-        if force_unicode(result_repr) == '':
-            result_repr = mark_safe('&nbsp;')
-        # If list_display_links not defined, add the link tag to the first field
-        if (first and not cl.list_display_links) or field_name in cl.list_display_links:
-            table_tag = {True:'th', False:'td'}[first]
-            first = False
-            url = cl.url_for_result(result)
-            # Convert the pk to something that can be used in Javascript.
-            # Problem cases are long ints (23L) and non-ASCII strings.
-            result_id = repr(force_unicode(getattr(result, pk)))[1:]
-            yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
-                (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
-        else:
-            yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
 
-def results(cl):
-    for res in cl.result_list:
-        yield list(items_for_result(cl,res))
+def result_headers(cl, columns):
+	for i, col in enumerate(columns):
+		if col.order_field is None:
+			yield {'text': col.header}
+			continue
 
+		th_classes = []
+		new_order_type = 'asc'
+		if col.order_field == cl.order_field:
+			th_classes.append('sorted %sending' % cl.order_type.lower())
+			new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
+
+		yield {"text": col.header,
+			   "sortable": True,
+			   "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
+			   "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')}
+
+
+def items_for_result(cl, columns, result):
+	pk = cl.lookup_opts.pk.attname
+	for i, col in enumerate(columns): 
+		result_repr = col.get_value_display(result)
+		row_class = col._nowrap and ' class="nowrap"' or ''
+		first = i == 0
+		# If list_display_links not defined, add the link tag to the first field
+		if (first and not cl.list_display_links) or col.field_name in cl.list_display_links:
+			table_tag = {True:'th', False:'td'}[first]
+			url = cl.url_for_result(result)
+			# Convert the pk to something that can be used in Javascript.
+			# Problem cases are long ints (23L) and non-ASCII strings.
+			result_id = repr(force_unicode(getattr(result, pk)))[1:]
+			yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
+				(table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
+		else:
+			yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
+
+
 def result_list(cl):
+    cols = []
+    for col in cl.list_display:
+        if isinstance(col, ListColumn):
+            cols += [col.for_model(cl.model)]
+        else:
+            cols += [ListColumn(col).for_model(cl.model)]
     return {'cl': cl,
-            'result_headers': list(result_headers(cl)),
-            'results': list(results(cl))}
+            'result_headers': list(result_headers(cl, cols)),
+            'results': [items_for_result(cl, cols, r) for r in cl.result_list]}
 result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)
 
+
 def date_hierarchy(cl):
     if cl.date_hierarchy:
         field_name = cl.date_hierarchy
