Index: django/contrib/admin/validation.py
===================================================================
--- django/contrib/admin/validation.py	(revision 14124)
+++ django/contrib/admin/validation.py	(working copy)
@@ -5,6 +5,7 @@
 from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
 from django.contrib.admin.options import HORIZONTAL, VERTICAL
 from django.contrib.admin.util import lookup_field
+from django.contrib.admin.options import ListColumn 
 
 
 __all__ = ['validate']
@@ -27,8 +28,8 @@
         check_isseq(cls, '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):
+                if isinstance(field, ListColumn) or not hasattr(cls, field):
+                    if not isinstance(field, ListColumn) and not hasattr(model, field):
                         try:
                             opts.get_field(field)
                         except models.FieldDoesNotExist:
@@ -36,7 +37,10 @@
                                 % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
                     else:
                         # getattr(model, field) could be an X_RelatedObjectsDescriptor
-                        f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field)
+                        if isinstance(field, ListColumn):
+                            f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field.field_name)
+                        else:
+                            f = fetch_attr(cls, model, opts, "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))
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 14124)
+++ django/contrib/admin/options.py	(working copy)
@@ -9,6 +9,9 @@
 from django.contrib import messages
 from django.views.decorators.csrf import csrf_protect
 from django.core.exceptions import PermissionDenied, ValidationError
+from django.contrib.admin.util import label_for_field, lookup_field
+from django.contrib.admin import views
+from django.core.exceptions import ObjectDoesNotExist
 from django.db import models, transaction
 from django.db.models.fields import BLANK_CHOICE_DASH
 from django.http import Http404, HttpResponse, HttpResponseRedirect
@@ -16,6 +19,7 @@
 from django.utils.decorators import method_decorator
 from django.utils.datastructures import SortedDict
 from django.utils.functional import update_wrapper
+from django.utils.encoding import smart_unicode, smart_str
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.functional import curry
@@ -24,6 +28,140 @@
 from django.utils.translation import ungettext, ugettext_lazy
 from django.utils.encoding import force_unicode
 
+
+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, cl=None):
+        """Create a new ListColumn instance where unset properties in this
+        ListColumn have been filled in by inspecting the Model provided."""
+        header, attr = label_for_field(self.field_name, model,
+                model_admin = cl.model_admin,
+                return_attr = True
+            )
+        try:
+            f = model._meta.get_field(self.field_name)
+        except models.FieldDoesNotExist:
+            return AttributeListColumn(model, field_name=self.field_name,
+                header=self.header if self.header else header,
+                filter=self.filter,
+                load_filters=self.load_filters,
+                order_field=self.order_field,
+                attr=attr
+            )
+        else:
+            return FieldListColumn(f, self.field_name,
+                header=self.header if self.header else header,
+                filter=self.filter,
+                load_filters=self.load_filters,
+                order_field=self.order_field
+                #attr=attr
+            )
+
+    def get_value_display(self, result, cl=None):
+        from django.template import Context, Parser, get_library
+        #field_value = self.get_value(result)
+        # TODO: i don't know how 2 lines below is working
+        #if self.value_map:
+        #    field_value = self.value_map.get(field_value, None)
+        try:   
+            f, attr, field_value = lookup_field(self.field_name, result, cl.model_admin)
+        except (AttributeError, ObjectDoesNotExist):
+            field_value = views.main.EMPTY_CHANGELIST_VALUE
+        else:
+            #if field_value is None:
+            #    return EMPTY_CHANGELIST_VALUE
+        
+            # apply filters by faking a template token
+            if self.filter:
+                p = Parser([])
+                for lib in ['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}))
+
+        if force_unicode(field_value) == '':
+            return mark_safe('&nbsp;')
+        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
+        if 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, attr=None, *args, **kwargs):
+        super(AttributeListColumn, self).__init__(*args, **kwargs)
+        
+        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
+    
+
 HORIZONTAL, VERTICAL = 1, 2
 # returns the <ul> class for a given radio_admin field
 get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
@@ -233,7 +371,10 @@
         if not self.list_display_links:
             for name in self.list_display:
                 if name != 'action_checkbox':
-                    self.list_display_links = [name]
+                    if isinstance(name, ListColumn):
+                        self.list_display_links = [name.field_name]
+                    else:
+                        self.list_display_links = [name]
                     break
         super(ModelAdmin, self).__init__()
 
Index: django/contrib/admin/util.py
===================================================================
--- django/contrib/admin/util.py	(revision 14124)
+++ django/contrib/admin/util.py	(working copy)
@@ -314,7 +314,7 @@
 
 
 def display_for_field(value, field):
-    from django.contrib.admin.templatetags.admin_list import _boolean_icon
+    from django.contrib.admin.templatetags.admin_list import boolean_icon
     from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
 
     if field.flatchoices:
@@ -322,7 +322,7 @@
     # NullBooleanField needs special-case null-handling, so it comes
     # before the general null test.
     elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
-        return _boolean_icon(value)
+        return boolean_icon(value)
     elif value is None:
         return EMPTY_CHANGELIST_VALUE
     elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
Index: django/contrib/admin/__init__.py
===================================================================
--- django/contrib/admin/__init__.py	(revision 14124)
+++ django/contrib/admin/__init__.py	(working copy)
@@ -2,6 +2,7 @@
 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
 from django.contrib.admin.options import StackedInline, TabularInline
 from django.contrib.admin.sites import AdminSite, site
+from django.contrib.admin.options import ListColumn
 
 
 def autodiscover():
Index: django/contrib/admin/helpers.py
===================================================================
--- django/contrib/admin/helpers.py	(revision 14124)
+++ django/contrib/admin/helpers.py	(working copy)
@@ -161,7 +161,7 @@
         })
 
     def contents(self):
-        from django.contrib.admin.templatetags.admin_list import _boolean_icon
+        from django.contrib.admin.templatetags.admin_list import boolean_icon
         from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
         field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
         try:
@@ -172,7 +172,7 @@
             if f is None:
                 boolean = getattr(attr, "boolean", False)
                 if boolean:
-                    result_repr = _boolean_icon(value)
+                    result_repr = boolean_icon(value)
                 else:
                     result_repr = smart_unicode(value)
                     if getattr(attr, "allow_tags", False):
Index: django/contrib/admin/templatetags/admin_list.py
===================================================================
--- django/contrib/admin/templatetags/admin_list.py	(revision 14124)
+++ django/contrib/admin/templatetags/admin_list.py	(working copy)
@@ -1,19 +1,19 @@
-import datetime
-
 from django.conf import settings
 from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
 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.contrib.admin.options import ListColumn
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
 from django.forms.forms import pretty_name
-from django.utils import formats
+from django.utils import formats, dateformat
 from django.utils.html import escape, conditional_escape
 from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
-from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext as _, get_date_formats
 from django.utils.encoding import smart_unicode, force_unicode
 from django.template import Library
+import datetime
 
 
 register = Library()
@@ -32,6 +32,7 @@
         return mark_safe(u'<a href="%s"%s>%d</a> ' % (escape(cl.get_query_string({PAGE_VAR: i})), (i == cl.paginator.num_pages-1 and ' class="end"' or ''), i+1))
 paginator_number = register.simple_tag(paginator_number)
 
+
 def pagination(cl):
     """
     Generates the series of links to the pages in a paginated list.
@@ -78,96 +79,79 @@
     }
 pagination = register.inclusion_tag('admin/pagination.html')(pagination)
 
-def result_headers(cl):
+def result_headers(cl, columns):
     """
     Generates the list column headers.
     """
-    lookup_opts = cl.lookup_opts
-
-    for i, field_name in enumerate(cl.list_display):
-        header, attr = label_for_field(field_name, cl.model,
-            model_admin = cl.model_admin,
-            return_attr = True
-        )
-        if attr:
-            # if the field is the action checkbox: no sorting and special class
-            if field_name == 'action_checkbox':
-                yield {
+    for i, col in enumerate(columns):
+        # if the field is the action checkbox: no sorting and special class
+        if col.field_name == 'action_checkbox':
+            header, attr = label_for_field(col.field_name, cl.model,
+                model_admin = cl.model_admin,
+                return_attr = True
+            )
+            yield {
                     "text": header,
                     "class_attrib": mark_safe(' class="action-checkbox-column"')
                 }
-                continue
+            continue
 
-            # It is a non-field, but perhaps one that is sortable
-            admin_order_field = getattr(attr, "admin_order_field", None)
-            if not admin_order_field:
-                yield {"text": header}
-                continue
+        if col.order_field is None:
+            yield {'text': col.header}
+            continue
 
-            # So this _is_ a sortable non-field.  Go to the yield
-            # after the else clause.
-        else:
-            admin_order_field = None
-
         th_classes = []
         new_order_type = 'asc'
-        if field_name == cl.order_field or admin_order_field == cl.order_field:
+        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": header,
+            "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 _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 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)
 
-def items_for_result(cl, result, form):
+
+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)
+
+
+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 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)
+
+
+def items_for_result(cl, result, form, columns):
     """
     Generates the actual list of data.
     """
-    first = True
     pk = cl.lookup_opts.pk.attname
-    for field_name in cl.list_display:
-        row_class = ''
-        try:
-            f, attr, value = lookup_field(field_name, result, cl.model_admin)
-        except (AttributeError, ObjectDoesNotExist):
-            result_repr = EMPTY_CHANGELIST_VALUE
-        else:
-            if f is None:
-                allow_tags = getattr(attr, 'allow_tags', False)
-                boolean = getattr(attr, 'boolean', False)
-                if boolean:
-                    allow_tags = True
-                    result_repr = _boolean_icon(value)
-                else:
-                    result_repr = smart_unicode(value)
-                # 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:
-                if value is None:
-                    result_repr = EMPTY_CHANGELIST_VALUE
-                if isinstance(f.rel, models.ManyToOneRel):
-                    result_repr = escape(getattr(result, f.name))
-                else:
-                    result_repr = display_for_field(value, f)
-                if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
-                    row_class = ' class="nowrap"'
-        if force_unicode(result_repr) == '':
-            result_repr = mark_safe('&nbsp;')
+    for i, col in enumerate(columns):
+        result_repr = col.get_value_display(result, cl)
+        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 field_name in cl.list_display_links:
+        if (first and not cl.list_display_links) or col.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.
@@ -183,8 +167,8 @@
             # By default the fields come from ModelAdmin.list_editable, but if we pull
             # the fields out of the form instead of list_editable custom admins
             # can provide fields on a per request basis
-            if form and field_name in form.fields:
-                bf = form[field_name]
+            if form and col.field_name in form.fields:
+                bf = form[col.field_name]
                 result_repr = mark_safe(force_unicode(bf.errors) + force_unicode(bf))
             else:
                 result_repr = conditional_escape(result_repr)
@@ -192,13 +176,13 @@
     if form and not form[cl.model._meta.pk.name].is_hidden:
         yield mark_safe(u'<td>%s</td>' % force_unicode(form[cl.model._meta.pk.name]))
 
-def results(cl):
+def results(cl, columns):
     if cl.formset:
         for res, form in zip(cl.result_list, cl.formset.forms):
-            yield list(items_for_result(cl, res, form))
+            yield list(items_for_result(cl, res, form, columns))
     else:
         for res in cl.result_list:
-            yield list(items_for_result(cl, res, None))
+            yield list(items_for_result(cl, res, None, columns))
 
 def result_hidden_fields(cl):
     if cl.formset:
@@ -210,10 +194,16 @@
     """
     Displays the headers and data list together
     """
+    cols = []
+    for col in cl.list_display:
+        if isinstance(col, ListColumn):
+            cols += [col.for_model(cl.model, cl)]
+        else:
+            cols += [ListColumn(col).for_model(cl.model, cl)]
     return {'cl': cl,
             'result_hidden_fields': list(result_hidden_fields(cl)),
-            'result_headers': list(result_headers(cl)),
-            'results': list(results(cl))}
+            'result_headers': list(result_headers(cl, cols)),
+            'results': list(results(cl, cols))}
 result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)
 
 def date_hierarchy(cl):
Index: django/contrib/admin/views/main.py
===================================================================
--- django/contrib/admin/views/main.py	(revision 14124)
+++ django/contrib/admin/views/main.py	(working copy)
@@ -1,5 +1,5 @@
 from django.contrib.admin.filterspecs import FilterSpec
-from django.contrib.admin.options import IncorrectLookupParameters
+from django.contrib.admin.options import IncorrectLookupParameters, ListColumn
 from django.contrib.admin.util import quote
 from django.core.paginator import Paginator, InvalidPage
 from django.db import models
@@ -140,6 +140,11 @@
         if ORDER_VAR in params:
             try:
                 field_name = self.list_display[int(params[ORDER_VAR])]
+                field = self.list_display[int(params[ORDER_VAR])] 
+                if isinstance(field, ListColumn): 
+                    field_name = field.field_name
+                else:
+                    field_name = field
                 try:
                     f = lookup_opts.get_field(field_name)
                 except models.FieldDoesNotExist:
