diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index f8e634e..b22a5a2 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -4,7 +4,11 @@ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
 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.filterspecs import (ListFilterBase,
+        SingleQueryParameterListFilter, FieldListFilter, BooleanFieldListFilter,
+        RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter,
+        AllValuesFieldListFilter)
+        
 
 def autodiscover():
     """
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
index 965b32b..3d228b9 100644
--- a/django/contrib/admin/filterspecs.py
+++ b/django/contrib/admin/filterspecs.py
@@ -7,80 +7,156 @@ certain test -- e.g. being a DateField or ForeignKey.
 """
 
 from django.db import models
+from django.core.exceptions import ImproperlyConfigured
 from django.utils.encoding import smart_unicode, iri_to_uri
 from django.utils.translation import ugettext as _
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
-from django.contrib.admin.util import get_model_from_relation, \
-    reverse_field_path, get_limit_choices_to_from_path
+from django.contrib.admin.util import (get_model_from_relation,
+    reverse_field_path, get_limit_choices_to_from_path)
+from django.template.defaultfilters import slugify
 import datetime
 
-class FilterSpec(object):
-    filter_specs = []
-    def __init__(self, f, request, params, model, model_admin,
-                 field_path=None):
-        self.field = f
+class ListFilterBase(object):
+    title = None # Human-readable title to appear in the right sidebar.
+    
+    def __init__(self, request, params, model, model_admin):
+        if self.title is None:
+            raise ImproperlyConfigured("The list filter '%s' does not specify "
+                                       "a 'title'." % self.__class__.__name__)
         self.params = params
+        self.request = request
+        
+    def has_output(self):
+        """
+        Returns True if some choices would be output for the filter.
+        """
+        raise NotImplementedError
+
+    def get_output_choices(self, cl):
+        """
+        Returns choices ready to be output in the template.
+        """
+        raise NotImplementedError
+
+    def get_query_set(self, queryset):
+        """
+        Returns the filtered queryset.
+        """
+        raise NotImplementedError
+    
+    def used_params(self):
+        """
+        Return a list of parameters to consume from the change list
+        querystring.
+        """
+        raise NotImplementedError   
+
+
+
+class SingleQueryParameterListFilter(ListFilterBase):    
+    # The parameter that should be used in the query string for that filter.
+    # Defaults to the title, slugified.
+    query_parameter = None
+    
+    def __init__(self, request, params, model, model_admin):
+        super(SingleQueryParameterListFilter, self).__init__(request, params, model, model_admin)
+        if self.query_parameter is None:
+            self.query_parameter = slugify(self.title)
+        self.lookup_choices = self.get_choices()
+    
+    def has_output(self):
+        return len(self.lookup_choices) > 0
+    
+    def get_value(self):
+        """
+        Returns the value given in the query string for this filter,
+        if any. Returns None otherwise.
+        """
+        return self.params.get(self.query_parameter, None)
+
+    def get_choices(self):
+        """
+        Must be overriden to return a list of tuples (value, verbose value)
+        """
+        raise NotImplementedError
+
+    def used_params(self):
+        return [self.query_parameter]
+
+    def get_output_choices(self, cl):
+        yield {'selected': self.get_value() is None,
+               'query_string': cl.get_query_string({}, [self.query_parameter]),
+               'display': _('All')}
+        for k, v in self.lookup_choices:
+            yield {'selected': self.get_value() == k,
+                   'query_string': cl.get_query_string(
+                                   {self.query_parameter: k},
+                                   []),
+                   'display': v}
+
+
+            
+class FieldListFilter(ListFilterBase):
+    _field_list_filters = []
+    _take_priority_index = 0
+    
+    def __init__(self, field, request, params, model, model_admin, \
+                 field_path):
+        self.field = field
         self.field_path = field_path
-        if field_path is None:
-            if isinstance(f, models.related.RelatedObject):
-                self.field_path = f.var_name
-            else:
-                self.field_path = f.name
-
-    def register(cls, test, factory):
-        cls.filter_specs.append((test, factory))
-    register = classmethod(register)
-
-    def create(cls, f, request, params, model, model_admin, field_path=None):
-        for test, factory in cls.filter_specs:
-            if test(f):
-                return factory(f, request, params, model, model_admin,
-                               field_path=field_path)
-    create = classmethod(create)
+        self.title = field_path
+        super(FieldListFilter, self).__init__(request, params, model, \
+                                              model_admin)
 
     def has_output(self):
         return True
+        
+    def get_query_set(self, queryset):
+        for p in self.used_params():
+            if p in self.params:
+                return queryset.filter(**{p: self.params[p]})
+        
+    @classmethod
+    def register(cls, test, list_filter_class, take_priority=False):
+        if take_priority:
+            # This is to allow overriding the default filters for certain types
+            # of fields with some custom filters. The first found in the list
+            # is used in priority.
+            cls._field_list_filters.insert(cls._take_priority_index, (test, list_filter_class))
+            _take_priority_index += 1
+        else:
+            cls._field_list_filters.append((test, list_filter_class))
+    
+    @classmethod
+    def create(cls, field, request, params, model, model_admin, field_path):
+        for test, list_filter_class in cls._field_list_filters:
+            if test(field):
+                return list_filter_class(field, request, params, model, model_admin,
+                               field_path=field_path)
 
-    def choices(self, cl):
-        raise NotImplementedError()
-
-    def title(self):
-        return self.field.verbose_name
-
-    def output(self, cl):
-        t = []
-        if self.has_output():
-            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
-
-            for choice in self.choices(cl):
-                t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
-                    ((choice['selected'] and ' class="selected"' or ''),
-                     iri_to_uri(choice['query_string']),
-                     choice['display']))
-            t.append('</ul>\n\n')
-        return mark_safe("".join(t))
+        
+class RelatedFieldListFilter(FieldListFilter):
+    def __init__(self, field, request, params, model, model_admin,
+                 field_path):
+        super(RelatedFieldListFilter, self).__init__(
+            field, request, params, model, model_admin, field_path)
 
-class RelatedFilterSpec(FilterSpec):
-    def __init__(self, f, request, params, model, model_admin,
-                 field_path=None):
-        super(RelatedFilterSpec, self).__init__(
-            f, request, params, model, model_admin, field_path=field_path)
-
-        other_model = get_model_from_relation(f)
-        if isinstance(f, (models.ManyToManyField,
+        other_model = get_model_from_relation(field)
+        if isinstance(field, (models.ManyToManyField,
                           models.related.RelatedObject)):
             # no direct field on this model, get name from other model
             self.lookup_title = other_model._meta.verbose_name
         else:
-            self.lookup_title = f.verbose_name # use field name
+            self.lookup_title = field.verbose_name # use field name
         rel_name = other_model._meta.pk.name
         self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
         self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
         self.lookup_val_isnull = request.GET.get(
                                       self.lookup_kwarg_isnull, None)
-        self.lookup_choices = f.get_choices(include_blank=False)
+        self.lookup_choices = field.get_choices(include_blank=False)
+        self.title = self.lookup_title
 
     def has_output(self):
         if isinstance(self.field, models.related.RelatedObject) \
@@ -91,10 +167,10 @@ class RelatedFilterSpec(FilterSpec):
             extra = 0
         return len(self.lookup_choices) + extra > 1
 
-    def title(self):
-        return self.lookup_title
-
-    def choices(self, cl):
+    def used_params(self):
+        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
+    
+    def get_output_choices(self, cl):
         from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
         yield {'selected': self.lookup_val is None
                            and not self.lookup_val_isnull,
@@ -117,25 +193,25 @@ class RelatedFilterSpec(FilterSpec):
                                    [self.lookup_kwarg]),
                    'display': EMPTY_CHANGELIST_VALUE}
 
-FilterSpec.register(lambda f: (
+FieldListFilter.register(lambda f: (
         hasattr(f, 'rel') and bool(f.rel) or
-        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
+        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
 
-class BooleanFieldFilterSpec(FilterSpec):
+class BooleanFieldListFilter(FieldListFilter):
     def __init__(self, f, request, params, model, model_admin,
-                 field_path=None):
-        super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
+                 field_path):
+        super(BooleanFieldListFilter, self).__init__(f, request, params, model,
                                                      model_admin,
-                                                     field_path=field_path)
+                                                     field_path)
         self.lookup_kwarg = '%s__exact' % self.field_path
         self.lookup_kwarg2 = '%s__isnull' % self.field_path
         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
         self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
 
-    def title(self):
-        return self.field.verbose_name
-
-    def choices(self, cl):
+    def used_params(self):
+        return [self.lookup_kwarg, self.lookup_kwarg2]
+    
+    def get_output_choices(self, cl):
         for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
             yield {'selected': self.lookup_val == v and not self.lookup_val2,
                    'query_string': cl.get_query_string(
@@ -149,20 +225,23 @@ class BooleanFieldFilterSpec(FilterSpec):
                                    [self.lookup_kwarg]),
                    'display': _('Unknown')}
 
-FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
+FieldListFilter.register(lambda f: isinstance(f, models.BooleanField)
                               or isinstance(f, models.NullBooleanField),
-                                 BooleanFieldFilterSpec)
+                                 BooleanFieldListFilter)
 
-class ChoicesFilterSpec(FilterSpec):
+class ChoicesFieldListFilter(FieldListFilter):
     def __init__(self, f, request, params, model, model_admin,
-                 field_path=None):
-        super(ChoicesFilterSpec, self).__init__(f, request, params, model,
+                 field_path):
+        super(ChoicesFieldListFilter, self).__init__(f, request, params, model,
                                                 model_admin,
-                                                field_path=field_path)
+                                                field_path)
         self.lookup_kwarg = '%s__exact' % self.field_path
-        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
+        self.lookup_val = request.GET.get(self.lookup_kwarg)
 
-    def choices(self, cl):
+    def used_params(self):
+        return [self.lookup_kwarg]
+        
+    def get_output_choices(self, cl):
         yield {'selected': self.lookup_val is None,
                'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
                'display': _('All')}
@@ -172,14 +251,14 @@ class ChoicesFilterSpec(FilterSpec):
                                     {self.lookup_kwarg: k}),
                     'display': v}
 
-FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
+FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
 
-class DateFieldFilterSpec(FilterSpec):
+class DateFieldListFilter(FieldListFilter):
     def __init__(self, f, request, params, model, model_admin,
-                 field_path=None): 
-        super(DateFieldFilterSpec, self).__init__(f, request, params, model,
+                 field_path): 
+        super(DateFieldListFilter, self).__init__(f, request, params, model,
                                                   model_admin,
-                                                  field_path=field_path)
+                                                  field_path)
 
         self.field_generic = '%s__' % self.field_path
 
@@ -192,23 +271,44 @@ class DateFieldFilterSpec(FilterSpec):
                     and today.strftime('%Y-%m-%d 23:59:59') \
                     or today.strftime('%Y-%m-%d')
 
+        self.lookup_kwarg_year = '%s__year' % self.field_path
+        self.lookup_kwarg_month = '%s__month' % self.field_path
+        self.lookup_kwarg_day = '%s__day' % self.field_path
+        self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path
+        self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path
+        
         self.links = (
             (_('Any date'), {}),
-            (_('Today'), {'%s__year' % self.field_path: str(today.year),
-                       '%s__month' % self.field_path: str(today.month),
-                       '%s__day' % self.field_path: str(today.day)}),
-            (_('Past 7 days'), {'%s__gte' % self.field_path:
+            (_('Today'), {self.lookup_kwarg_year: str(today.year),
+                          self.lookup_kwarg_month: str(today.month),
+                          self.lookup_kwarg_day: str(today.day)}),
+            (_('Past 7 days'), {self.lookup_kwarg_past_7_days_gte:
                                     one_week_ago.strftime('%Y-%m-%d'),
-                             '%s__lte' % self.field_path: today_str}),
-            (_('This month'), {'%s__year' % self.field_path: str(today.year),
-                             '%s__month' % self.field_path: str(today.month)}),
-            (_('This year'), {'%s__year' % self.field_path: str(today.year)})
+                                self.lookup_kwarg_past_7_days_lte:
+                                    today_str}),
+            (_('This month'), {self.lookup_kwarg_year: str(today.year),
+                               self.lookup_kwarg_month: str(today.month)}),
+            (_('This year'), {self.lookup_kwarg_year: str(today.year)})
         )
 
-    def title(self):
-        return self.field.verbose_name
-
-    def choices(self, cl):
+    def used_params(self):
+        return [self.lookup_kwarg_year, self.lookup_kwarg_month,
+                self.lookup_kwarg_day, self.lookup_kwarg_past_7_days_gte,
+                self.lookup_kwarg_past_7_days_lte]
+    
+    def get_query_set(self, queryset):
+        """
+        Override the default behaviour since there can be multiple query
+        string parameters used for the same date filter (e.g. year + month).
+        """
+        query_dict = {}
+        for p in self.used_params():
+            if p in self.params:
+                query_dict[p] = self.params[p]
+        if len(query_dict):
+            return queryset.filter(**query_dict)
+    
+    def get_output_choices(self, cl):
         for title, param_dict in self.links:
             yield {'selected': self.date_params == param_dict,
                    'query_string': cl.get_query_string(
@@ -216,19 +316,19 @@ class DateFieldFilterSpec(FilterSpec):
                                    [self.field_generic]),
                    'display': title}
 
-FilterSpec.register(lambda f: isinstance(f, models.DateField),
-                              DateFieldFilterSpec)
+FieldListFilter.register(lambda f: isinstance(f, models.DateField),
+                              DateFieldListFilter)
 
 
 # This should be registered last, because it's a last resort. For example,
-# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
-# more appropriate, and the AllValuesFilterSpec won't get used for it.
-class AllValuesFilterSpec(FilterSpec):
+# if a field is eligible to use the BooleanFieldListFilter, that'd be much
+# more appropriate, and the AllValuesFieldListFilter won't get used for it.
+class AllValuesFieldListFilter(FieldListFilter):
     def __init__(self, f, request, params, model, model_admin,
-                 field_path=None):
-        super(AllValuesFilterSpec, self).__init__(f, request, params, model,
+                 field_path):
+        super(AllValuesFieldListFilter, self).__init__(f, request, params, model,
                                                   model_admin,
-                                                  field_path=field_path)
+                                                  field_path)
         self.lookup_kwarg = self.field_path
         self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
@@ -245,10 +345,10 @@ class AllValuesFilterSpec(FilterSpec):
         self.lookup_choices = \
             queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
 
-    def title(self):
-        return self.field.verbose_name
-
-    def choices(self, cl):
+    def used_params(self):
+        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
+        
+    def get_output_choices(self, cl):
         from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
         yield {'selected': self.lookup_val is None
                            and self.lookup_val_isnull is None,
@@ -276,4 +376,4 @@ class AllValuesFilterSpec(FilterSpec):
                                     [self.lookup_kwarg]),
                     'display': EMPTY_CHANGELIST_VALUE}
 
-FilterSpec.register(lambda f: True, AllValuesFilterSpec)
+FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index fdf082b..3125116 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -317,7 +317,7 @@ def search_form(cl):
 search_form = register.inclusion_tag('admin/search_form.html')(search_form)
 
 def admin_list_filter(cl, spec):
-    return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
+    return {'title': spec.title, 'choices' : list(spec.get_output_choices(cl))}
 admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
 
 def admin_actions(context):
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 159afa4..b788a1a 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -3,6 +3,7 @@ from django.db import models
 from django.db.models.fields import FieldDoesNotExist
 from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
     _get_foreign_key)
+from django.contrib.admin.filterspecs import ListFilterBase, FieldListFilter
 from django.contrib.admin.util import get_fields_from_path, NotRelationField
 from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
     HORIZONTAL, VERTICAL)
@@ -54,15 +55,43 @@ def validate(cls, model):
     # list_filter
     if hasattr(cls, 'list_filter'):
         check_isseq(cls, 'list_filter', cls.list_filter)
-        for idx, fpath in enumerate(cls.list_filter):
-            try:
-                get_fields_from_path(model, fpath)
-            except (NotRelationField, FieldDoesNotExist), e:
-                raise ImproperlyConfigured(
-                    "'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
-                        cls.__name__, idx, fpath
-                    )
-                )
+        for idx, item in enumerate(cls.list_filter):
+            # There are three options for specifying a filter:
+            #   1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
+            #   2: ('field', SomeFieldListFilter) - a field-based list filter class
+            #   3: SomeListFilter - a non-field list filter class
+            if callable(item) and not isinstance(item, models.Field):
+                # If item is option 3, it should be a ListFilterBase...
+                if not issubclass(item, ListFilterBase):
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
+                            " which is not a descendant of ListFilterBase." 
+                            % (cls.__name__, idx, item.__name__))
+                # ...  but not a FieldListFilter.
+                if issubclass(item, FieldListFilter):
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
+                            " which is of type FieldListFilter but is not"
+                            " associated with a field name." 
+                            % (cls.__name__, idx, item.__name__))
+            else:
+                try:
+                    # Check for option #2 (tuple)
+                    field, list_filter_class = item
+                except (TypeError, ValueError):
+                    # item is option #1
+                    field = item
+                else:
+                    # item is option #2
+                    if not issubclass(list_filter_class, FieldListFilter):
+                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
+                            " is '%s' which is not of type FieldListFilter."
+                            % (cls.__name__, idx, list_filter_class.__name__))
+                # Validate the field string
+                try:
+                    get_fields_from_path(model, field)
+                except (NotRelationField, FieldDoesNotExist), e:
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
+                            " which does not refer to a Field."
+                            % (cls.__name__, idx, field))
 
     # list_per_page = 100
     if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 0cfc43d..7e2165f 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -1,4 +1,4 @@
-from django.contrib.admin.filterspecs import FilterSpec
+from django.contrib.admin.filterspecs import SingleQueryParameterListFilter, FieldListFilter
 from django.contrib.admin.options import IncorrectLookupParameters
 from django.contrib.admin.util import quote, get_fields_from_path
 from django.core.exceptions import SuspiciousOperation
@@ -37,6 +37,7 @@ def field_needs_distinct(field):
 
 class ChangeList(object):
     def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
+        self.request = request
         self.model = model
         self.opts = model._meta
         self.lookup_opts = self.opts
@@ -71,19 +72,32 @@ class ChangeList(object):
         self.order_field, self.order_type = self.get_ordering()
         self.query = request.GET.get(SEARCH_VAR, '')
         self.query_set = self.get_query_set()
-        self.get_results(request)
+        self.get_results()
         self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
-        self.filter_specs, self.has_filters = self.get_filters(request)
         self.pk_attname = self.lookup_opts.pk.attname
 
-    def get_filters(self, request):
+    def get_filters(self):
         filter_specs = []
         if self.list_filter:
-            for filter_name in self.list_filter:
-                field = get_fields_from_path(self.model, filter_name)[-1]
-                spec = FilterSpec.create(field, request, self.params,
-                                         self.model, self.model_admin,
-                                         field_path=filter_name)
+            for item in self.list_filter:
+                if callable(item):
+                    # This is simply a custom list filter class.
+                    spec = item(self.request, self.cleaned_params, self.model, self.model_admin)
+                else:
+                    field_path = None
+                    try:
+                        # This is custom FieldListFilter class for a given field. 
+                        field, field_list_filter_class = item
+                    except (TypeError, ValueError):
+                        # This is simply a field name, so use the default
+                        # FieldListFilter class that has been registered for
+                        # the type of the given field.
+                        field, field_list_filter_class = item, FieldListFilter.create
+                    if not isinstance(field, models.Field):
+                        field_path = field
+                        field = get_fields_from_path(self.model, field_path)[-1]
+                    spec = field_list_filter_class(field, self.request, self.cleaned_params, self.model,
+                            self.model_admin, field_path=field_path)
                 if spec and spec.has_output():
                     filter_specs.append(spec)
         return filter_specs, bool(filter_specs)
@@ -104,8 +118,8 @@ class ChangeList(object):
                 p[k] = v
         return '?%s' % urlencode(p)
 
-    def get_results(self, request):
-        paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
+    def get_results(self):
+        paginator = self.model_admin.get_paginator(self.request, self.query_set, self.list_per_page)
         # Get the number of objects, with admin filters applied.
         result_count = paginator.count
 
@@ -174,7 +188,19 @@ class ChangeList(object):
         if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
             order_type = params[ORDER_TYPE_VAR]
         return order_field, order_type
-
+    
+    def apply_list_filters(self, qs, lookup_params):
+        for filter_spec in self.filter_specs:
+            new_qs = filter_spec.get_query_set(qs)
+            if new_qs is not None:
+                qs = new_qs
+                for param in filter_spec.used_params():
+                    try:
+                        del lookup_params[param]
+                    except KeyError:
+                        pass
+        return qs
+    
     def get_query_set(self):
         use_distinct = False
 
@@ -196,10 +222,11 @@ class ChangeList(object):
                 field_name = key.split('__', 1)[0]
                 try:
                     f = self.lookup_opts.get_field_by_name(field_name)[0]
+                    use_distinct = field_needs_distinct(f)
                 except models.FieldDoesNotExist:
-                    raise IncorrectLookupParameters
-                use_distinct = field_needs_distinct(f)
-
+                    # It might be a custom NonFieldFilter
+                    pass
+                
             # if key ends with __in, split parameter into separate values
             if key.endswith('__in'):
                 value = value.split(',')
@@ -217,8 +244,18 @@ class ChangeList(object):
                 raise SuspiciousOperation(
                     "Filtering by %s not allowed" % key
                 )
-
-        # Apply lookup parameters from the query string.
+        
+        # Keep a copy of cleaned querystring parameters so they can be passed
+        # to the list filters.
+        self.cleaned_params = lookup_params.copy()
+        
+        self.filter_specs, self.has_filters = self.get_filters()
+        
+        # Let every list filter modify the qs and params to its liking
+        qs = self.apply_list_filters(qs, lookup_params)
+        
+        # Apply the remaining lookup parameters from the query string (i.e.
+        # those that haven't already been processed by the filters).
         try:
             qs = qs.filter(**lookup_params)
         # Naked except! Because we don't have any other way of validating "params".
diff --git a/django/db/models/related.py b/django/db/models/related.py
index 7734230..90995d7 100644
--- a/django/db/models/related.py
+++ b/django/db/models/related.py
@@ -27,7 +27,7 @@ class RelatedObject(object):
         as SelectField choices for this field.
 
         Analogue of django.db.models.fields.Field.get_choices, provided
-        initially for utilisation by RelatedFilterSpec.
+        initially for utilisation by RelatedFieldListFilter.
         """
         first_choice = include_blank and blank_choice or []
         queryset = self.model._default_manager.all()
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 633c53f..b1cffe1 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -525,30 +525,110 @@ subclass::
 
 .. attribute:: ModelAdmin.list_filter
 
-    Set ``list_filter`` to activate filters in the right sidebar of the change
-    list page of the admin. This should be a list of field names, and each
-    specified field should be either a ``BooleanField``, ``CharField``,
-    ``DateField``, ``DateTimeField``, ``IntegerField`` or ``ForeignKey``.
-
-    This example, taken from the ``django.contrib.auth.models.User`` model,
-    shows how both ``list_display`` and ``list_filter`` work::
-
-        class UserAdmin(admin.ModelAdmin):
-            list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
-            list_filter = ('is_staff', 'is_superuser')
-
-    The above code results in an admin change list page that looks like this:
+    .. versionchanged:: 1.4
 
+    Set ``list_filter`` to activate filters in the right sidebar of the change
+    list page of the admin, as illustrated in the following screenshot:
+    
         .. image:: _images/users_changelist.png
-
-    (This example also has ``search_fields`` defined. See below.)
-
-    .. versionadded:: 1.3
-
-    Fields in ``list_filter`` can also span relations using the ``__`` lookup::
-
-        class UserAdminWithLookup(UserAdmin):
-            list_filter = ('groups__name')
+    
+    ``list_filter`` should be a list of elements, where each element should be
+    of one of the following types:
+    
+        * a field name, where the specified field should be either a
+          ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
+          ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``, for example::
+          
+              class PersonAdmin(ModelAdmin):
+                  list_filter = ('is_staff', 'company',)
+
+          .. versionadded:: 1.3
+
+          Field names in ``list_filter`` can also span relations
+          using the ``__`` lookup, for example::
+            
+              class PersonAdmin(UserAdmin):
+                  list_filter = ('company__name')
+
+        * a class inheriting from :mod:`django.contrib.admin.SingleQueryParameterListFilter`,
+          where you need to provide a few attributes and override a few
+          methods::
+          
+              from django.contrib.admin import SingleQueryParameterListFilter
+              from django.db.models import Q
+            
+              class DecadeBornListFilter(SingleQueryParameterListFilter):
+                  # Human-readable title which will be displayed in the
+                  # right sidebar just above the filter options.
+                  title = u'decade born'
+                
+                  # Parameter for the filter that will be used in the url query.
+                  # Providing this attribute is optional. If it is not provided then a 
+                  # slugified version of the title will automatically be used instead
+                  # (that is, 'decade-born' in this example).
+                  query_parameter = u'decade' 
+                
+                  def get_choices(self):
+                      """
+                      Returns a list of tuples. The first element in each tuple
+                      is the coded value for the option that will appear in the
+                      url query. The second element is the human-readable name
+                      for the option that will appear in the right sidebar.
+                      """
+                      return (
+                          (u'80s', u'in the eighties'),
+                          (u'other', u'other'),
+                      )
+                
+                  def get_query_set(self, queryset):
+                      """
+                      Returns the filtered queryset based on the value provided
+                      in the query string and retrievable via `get_value()`.
+                      """
+                      # First, retrieve the requested value (either '80s' or 'other').
+                      decade = self.get_value()
+                      # Then decide how to filter the queryset based on that value.
+                      if decade == u'80s':
+                          return queryset.filter(birthday__year__gte=1980,
+                                                 birthday__year__lte=1989)
+                      if decade == u'other':
+                          return queryset.filter(Q(year__lte=1979) |
+                                                 Q(year__gte=1990))
+    
+              class PersonAdmin(ModelAdmin):
+                  list_filter = (DecadeBornListFilter,)
+        
+          .. note::
+          
+              As a convenience, the ``HttpRequest`` object is accessible using 
+              ``self.request`` from any of the filter's methods, for example::
+              
+                  class DecadeBornListFilter(SingleQueryParameterListFilter):
+                  
+                      def get_choices(self):
+                          if self.request.user.is_authenticated():
+                              return (
+                                  (u'80s', u'in the eighties'),
+                                  (u'other', u'other'),
+                              )
+                          else:
+                              return (
+                                  (u'90s', u'in the nineties'),
+                              )
+        
+        * a tuple, where the first element is a field name and the second
+          element is a class inheriting from
+          :mod:`django.contrib.admin.FieldListFilter`, for example::
+          
+              from django.contrib.admin import BooleanFieldListFilter
+          
+              class PersonAdmin(ModelAdmin):
+                  list_filter = (('is_staff', BooleanFieldListFilter),)
+          
+          .. note::
+          
+              The ``FieldListFilter`` API is currently considered internal and
+              prone to refactoring.
 
 .. attribute:: ModelAdmin.list_per_page
 
diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
index 5186508..ae0db75 100644
--- a/tests/regressiontests/admin_changelist/tests.py
+++ b/tests/regressiontests/admin_changelist/tests.py
@@ -133,7 +133,7 @@ class ChangeListTests(TransactionTestCase):
                 m.list_filter, m.date_hierarchy, m.search_fields,
                 m.list_select_related, m.list_per_page, m.list_editable, m)
 
-        cl.get_results(request)
+        cl.get_results()
         self.assertIsInstance(cl.paginator, CustomPaginator)
 
     def test_distinct_for_m2m_in_list_filter(self):
@@ -155,7 +155,7 @@ class ChangeListTests(TransactionTestCase):
                 m.search_fields, m.list_select_related, m.list_per_page,
                 m.list_editable, m)
 
-        cl.get_results(request)
+        cl.get_results()
 
         # There's only one Group instance
         self.assertEqual(cl.result_count, 1)
@@ -178,7 +178,7 @@ class ChangeListTests(TransactionTestCase):
                 m.search_fields, m.list_select_related, m.list_per_page,
                 m.list_editable, m)
 
-        cl.get_results(request)
+        cl.get_results()
 
         # There's only one Group instance
         self.assertEqual(cl.result_count, 1)
@@ -202,7 +202,7 @@ class ChangeListTests(TransactionTestCase):
                 m.search_fields, m.list_select_related, m.list_per_page,
                 m.list_editable, m)
 
-        cl.get_results(request)
+        cl.get_results()
 
         # There's only one Quartet instance
         self.assertEqual(cl.result_count, 1)
@@ -226,7 +226,7 @@ class ChangeListTests(TransactionTestCase):
                 m.search_fields, m.list_select_related, m.list_per_page,
                 m.list_editable, m)
 
-        cl.get_results(request)
+        cl.get_results()
 
         # There's only one ChordsBand instance
         self.assertEqual(cl.result_count, 1)
diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
index 5b284c7..5e81d1c 100644
--- a/tests/regressiontests/admin_filterspecs/models.py
+++ b/tests/regressiontests/admin_filterspecs/models.py
@@ -2,22 +2,12 @@ from django.db import models
 from django.contrib.auth.models import User
 
 class Book(models.Model):
-    title = models.CharField(max_length=25)
+    title = models.CharField(max_length=50)
     year = models.PositiveIntegerField(null=True, blank=True)
     author = models.ForeignKey(User, related_name='books_authored', blank=True, null=True)
     contributors = models.ManyToManyField(User, related_name='books_contributed', blank=True, null=True)
-
+    is_best_seller = models.NullBooleanField(default=0)
+    date_registered = models.DateField(null=True)
+    
     def __unicode__(self):
         return self.title
-
-class BoolTest(models.Model):
-    NO = False
-    YES = True
-    YES_NO_CHOICES = (
-        (NO, 'no'),
-        (YES, 'yes')
-    )
-    completed = models.BooleanField(
-        default=NO,
-        choices=YES_NO_CHOICES
-    )
diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
index 8b9e734..cef5b98 100644
--- a/tests/regressiontests/admin_filterspecs/tests.py
+++ b/tests/regressiontests/admin_filterspecs/tests.py
@@ -1,3 +1,6 @@
+import datetime
+import calendar
+
 from django.contrib.auth.admin import UserAdmin
 from django.test import TestCase
 from django.test.client import RequestFactory
@@ -5,52 +8,161 @@ from django.contrib.auth.models import User
 from django.contrib import admin
 from django.contrib.admin.views.main import ChangeList
 from django.utils.encoding import force_unicode
+from django.contrib.admin.filterspecs import (SingleQueryParameterListFilter,
+    BooleanFieldListFilter, FieldListFilter)
 
-from models import Book, BoolTest
+from models import Book
 
 def select_by(dictlist, key, value):
     return [x for x in dictlist if x[key] == value][0]
 
-class FilterSpecsTests(TestCase):
+
+
+class DecadeListFilterBase(SingleQueryParameterListFilter):
+    
+    def get_choices(self):
+        return (
+            (u'the 90s', u'the 1990\'s'),
+            (u'the 00s', u'the 2000\'s'),
+            (u'other', u'other decades'),
+        )
+    
+    def get_query_set(self, queryset):
+        decade = self.get_value()
+        if decade == u'the 90s':
+            return queryset.filter(year__gte=1990, year__lte=1999)
+        if decade == u'the 00s':
+            return queryset.filter(year__gte=2000, year__lte=2009)
+
+class DecadeListFilterWithTitle(DecadeListFilterBase):
+    title = u'publication decade'
+    
+class DecadeListFilterWithParamName(DecadeListFilterBase):
+    title = u'another publication decade'
+    query_parameter = u'blah'
+
+class ListFiltersTests(TestCase):
 
     def setUp(self):
+        self.today = datetime.date.today()
+        self.one_week_ago = self.today - datetime.timedelta(days=7)
+        
+        self.request_factory = RequestFactory()
+        
         # Users
         self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
         self.bob = User.objects.create_user('bob', 'bob@example.com')
-        lisa = User.objects.create_user('lisa', 'lisa@example.com')
-
-        #Books
-        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred)
-        self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob)
-        gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
-        gipsy_book.contributors = [self.bob, lisa]
-        gipsy_book.save()
-
-        # BoolTests
-        self.trueTest = BoolTest.objects.create(completed=True)
-        self.falseTest = BoolTest.objects.create(completed=False)
-
-        self.request_factory = RequestFactory()
+        self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
 
+        # Books
+        self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred, is_best_seller=True, date_registered=self.today)
+        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred, is_best_seller=False)
+        self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob, is_best_seller=None, date_registered=self.today)
+        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002, is_best_seller=True, date_registered=self.one_week_ago)
+        self.gipsy_book.contributors = [self.bob, self.lisa]
+        self.gipsy_book.save()        
 
     def get_changelist(self, request, model, modeladmin):
         return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links,
             modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
             modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
 
-    def test_AllValuesFilterSpec(self):
+    def test_DateFieldListFilter(self):
+        modeladmin = BookAdmin(Book, admin.site)
+        
+        request = self.request_factory.get('/')
+        changelist = self.get_changelist(request, Book, modeladmin)
+
+        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
+                                                 'date_registered__month': self.today.month,
+                                                 'date_registered__day': self.today.day})
+        changelist = self.get_changelist(request, Book, modeladmin)
+        
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "Today")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?date_registered__day=%s'
+                                                 '&date_registered__month=%s'
+                                                 '&date_registered__year=%s'
+                                                % (self.today.day, self.today.month, self.today.year))
+        
+        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
+                                                 'date_registered__month': self.today.month})
+        changelist = self.get_changelist(request, Book, modeladmin)
+        
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month):
+            # In case one week ago is in the same month.
+            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+        else:
+            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "This month")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?date_registered__month=%s'
+                                                 '&date_registered__year=%s'
+                                                % (self.today.month, self.today.year))
+    
+        request = self.request_factory.get('/', {'date_registered__year': self.today.year})
+        changelist = self.get_changelist(request, Book, modeladmin)
+        
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        if self.today.year == self.one_week_ago.year:
+            # In case one week ago is in the same year.
+            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+        else:
+            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "This year")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?date_registered__year=%s'
+                                                % (self.today.year))
+
+        request = self.request_factory.get('/', {'date_registered__gte': self.one_week_ago.strftime('%Y-%m-%d'),
+                                                 'date_registered__lte': self.today.strftime('%Y-%m-%d')})
+        changelist = self.get_changelist(request, Book, modeladmin)
+        
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "Past 7 days")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?date_registered__gte=%s'
+                                                 '&date_registered__lte=%s'
+                                                % (self.one_week_ago.strftime('%Y-%m-%d'), self.today.strftime('%Y-%m-%d')))
+        
+    def test_AllValuesFieldListFilter(self):
         modeladmin = BookAdmin(Book, admin.site)
 
         request = self.request_factory.get('/', {'year__isnull': 'True'})
         changelist = self.get_changelist(request, Book, modeladmin)
 
-        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
+        # Make sure the correct queryset is returned
         queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.django_book])
 
         # Make sure the last choice is None and is selected
-        filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'year')
-        choices = list(filterspec.choices(changelist))
+        filterspec = changelist.get_filters()[0][0]
+        self.assertEqual(force_unicode(filterspec.title), u'year')
+        choices = list(filterspec.get_output_choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
 
@@ -58,27 +170,26 @@ class FilterSpecsTests(TestCase):
         changelist = self.get_changelist(request, Book, modeladmin)
 
         # Make sure the correct choice is selected
-        filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'year')
-        choices = list(filterspec.choices(changelist))
+        filterspec = changelist.get_filters()[0][0]
+        self.assertEqual(force_unicode(filterspec.title), u'year')
+        choices = list(filterspec.get_output_choices(changelist))
         self.assertEqual(choices[2]['selected'], True)
         self.assertEqual(choices[2]['query_string'], '?year=2002')
 
-    def test_RelatedFilterSpec_ForeignKey(self):
+    def test_RelatedFieldListFilter_ForeignKey(self):
         modeladmin = BookAdmin(Book, admin.site)
 
         request = self.request_factory.get('/', {'author__isnull': 'True'})
-        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
-            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
-            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
+        changelist = self.get_changelist(request, Book, modeladmin)
 
-        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
+        # Make sure the correct queryset is returned
         queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.gipsy_book])
 
         # Make sure the last choice is None and is selected
-        filterspec = changelist.get_filters(request)[0][1]
-        self.assertEqual(force_unicode(filterspec.title()), u'author')
-        choices = list(filterspec.choices(changelist))
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'author')
+        choices = list(filterspec.get_output_choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
 
@@ -86,26 +197,27 @@ class FilterSpecsTests(TestCase):
         changelist = self.get_changelist(request, Book, modeladmin)
 
         # Make sure the correct choice is selected
-        filterspec = changelist.get_filters(request)[0][1]
-        self.assertEqual(force_unicode(filterspec.title()), u'author')
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'author')
         # order of choices depends on User model, which has no order
-        choice = select_by(filterspec.choices(changelist), "display", "alfred")
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "alfred")
         self.assertEqual(choice['selected'], True)
         self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
 
-    def test_RelatedFilterSpec_ManyToMany(self):
+    def test_RelatedFieldListFilter_ManyToMany(self):
         modeladmin = BookAdmin(Book, admin.site)
 
         request = self.request_factory.get('/', {'contributors__isnull': 'True'})
         changelist = self.get_changelist(request, Book, modeladmin)
 
-        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
+        # Make sure the correct queryset is returned
         queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book])
 
         # Make sure the last choice is None and is selected
-        filterspec = changelist.get_filters(request)[0][2]
-        self.assertEqual(force_unicode(filterspec.title()), u'user')
-        choices = list(filterspec.choices(changelist))
+        filterspec = changelist.get_filters()[0][2]
+        self.assertEqual(force_unicode(filterspec.title), u'user')
+        choices = list(filterspec.get_output_choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
 
@@ -113,27 +225,27 @@ class FilterSpecsTests(TestCase):
         changelist = self.get_changelist(request, Book, modeladmin)
 
         # Make sure the correct choice is selected
-        filterspec = changelist.get_filters(request)[0][2]
-        self.assertEqual(force_unicode(filterspec.title()), u'user')
-        choice = select_by(filterspec.choices(changelist), "display", "bob")
+        filterspec = changelist.get_filters()[0][2]
+        self.assertEqual(force_unicode(filterspec.title), u'user')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "bob")
         self.assertEqual(choice['selected'], True)
         self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
 
-
-    def test_RelatedFilterSpec_reverse_relationships(self):
+    def test_RelatedFieldListFilter_reverse_relationships(self):
         modeladmin = CustomUserAdmin(User, admin.site)
 
         # FK relationship -----
         request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
         changelist = self.get_changelist(request, User, modeladmin)
 
-        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
+        # Make sure the correct queryset is returned
         queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.lisa])
 
         # Make sure the last choice is None and is selected
-        filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'book')
-        choices = list(filterspec.choices(changelist))
+        filterspec = changelist.get_filters()[0][0]
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choices = list(filterspec.get_output_choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
 
@@ -141,9 +253,9 @@ class FilterSpecsTests(TestCase):
         changelist = self.get_changelist(request, User, modeladmin)
 
         # Make sure the correct choice is selected
-        filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'book')
-        choice = select_by(filterspec.choices(changelist), "display", self.bio_book.title)
+        filterspec = changelist.get_filters()[0][0]
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", self.bio_book.title)
         self.assertEqual(choice['selected'], True)
         self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
 
@@ -151,13 +263,14 @@ class FilterSpecsTests(TestCase):
         request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
         changelist = self.get_changelist(request, User, modeladmin)
 
-        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
+        # Make sure the correct queryset is returned
         queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.alfred])
 
         # Make sure the last choice is None and is selected
-        filterspec = changelist.get_filters(request)[0][1]
-        self.assertEqual(force_unicode(filterspec.title()), u'book')
-        choices = list(filterspec.choices(changelist))
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choices = list(filterspec.get_output_choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
 
@@ -165,47 +278,163 @@ class FilterSpecsTests(TestCase):
         changelist = self.get_changelist(request, User, modeladmin)
 
         # Make sure the correct choice is selected
-        filterspec = changelist.get_filters(request)[0][1]
-        self.assertEqual(force_unicode(filterspec.title()), u'book')
-        choice = select_by(filterspec.choices(changelist), "display", self.django_book.title)
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", self.django_book.title)
         self.assertEqual(choice['selected'], True)
         self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
 
-    def test_BooleanFilterSpec(self):
-        modeladmin = BoolTestAdmin(BoolTest, admin.site)
-
+    def test_BooleanFieldListFilter(self):
+        modeladmin = BookAdmin(Book, admin.site)
+        self.verify_BooleanFieldListFilter(modeladmin)
+        
+    def test_BooleanFieldListFilter_Tuple(self):
+        modeladmin = BookAdmin(Book, admin.site)
+        self.verify_BooleanFieldListFilter(modeladmin)
+        
+    def verify_BooleanFieldListFilter(self, modeladmin):
         request = self.request_factory.get('/')
-        changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links,
-            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
-            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
+        changelist = self.get_changelist(request, Book, modeladmin)
 
-        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
+        request = self.request_factory.get('/', {'is_best_seller__exact': 0})
+        changelist = self.get_changelist(request, Book, modeladmin)
+        
+        # Make sure the correct queryset is returned
         queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.bio_book])
 
-        # Make sure the last choice is None and is selected
-        filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'completed')
-        choices = list(filterspec.choices(changelist))
-        self.assertEqual(choices[-1]['selected'], False)
-        self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][3]
+        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "No")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?is_best_seller__exact=0')
 
-        request = self.request_factory.get('/', {'completed__exact': 1})
-        changelist = self.get_changelist(request, BoolTest, modeladmin)
+        request = self.request_factory.get('/', {'is_best_seller__exact': 1})
+        changelist = self.get_changelist(request, Book, modeladmin)
 
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
+        
         # Make sure the correct choice is selected
-        filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'completed')
-        # order of choices depends on User model, which has no order
-        choice = select_by(filterspec.choices(changelist), "display", "Yes")
+        filterspec = changelist.get_filters()[0][3]
+        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "Yes")
         self.assertEqual(choice['selected'], True)
-        self.assertEqual(choice['query_string'], '?completed__exact=1')
+        self.assertEqual(choice['query_string'], '?is_best_seller__exact=1')
+
+        request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'})
+        changelist = self.get_changelist(request, Book, modeladmin)
+
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.django_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][3]
+        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "Unknown")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
+        
+    def test_SingleQueryParameterListFilter(self):
+        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
+        
+        # Make sure that the first option is 'All' ---------------------------
+        
+        request = self.request_factory.get('/', {})
+        changelist = self.get_changelist(request, Book, modeladmin)
+
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
+
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec.get_output_choices(changelist))
+        self.assertEqual(choices[0]['display'], u'All')
+        self.assertEqual(choices[0]['selected'], True)
+        self.assertEqual(choices[0]['query_string'], '?')
+        
+        # Make sure that one can override the query parameter name -----------
+        
+        request = self.request_factory.get('/', {'blah': 'the 90s'})
+        changelist = self.get_changelist(request, Book, modeladmin)
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][2]
+        self.assertEqual(force_unicode(filterspec.title), u'another publication decade')
+        choices = list(filterspec.get_output_choices(changelist))
+        self.assertEqual(choices[1]['display'], u'the 1990\'s')
+        self.assertEqual(choices[1]['selected'], True)
+        self.assertEqual(choices[1]['query_string'], '?blah=the+90s')
+        
+        # Look for books in the 1990s ----------------------------------------
+        
+        request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
+        changelist = self.get_changelist(request, Book, modeladmin)
+
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.bio_book])
+
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec.get_output_choices(changelist))
+        self.assertEqual(choices[1]['display'], u'the 1990\'s')
+        self.assertEqual(choices[1]['selected'], True)
+        self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s')
+        
+        # Look for books in the 2000s ----------------------------------------
+        
+        request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
+        changelist = self.get_changelist(request, Book, modeladmin)
 
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
+
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec.get_output_choices(changelist))
+        self.assertEqual(choices[2]['display'], u'the 2000\'s')
+        self.assertEqual(choices[2]['selected'], True)
+        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s')
+        
+        # Combine multiple filters -------------------------------------------
+        
+        request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
+        changelist = self.get_changelist(request, Book, modeladmin)
+
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set()
+        self.assertEqual(list(queryset), [self.djangonaut_book])
+
+        # Make sure the correct choices are selected
+        filterspec = changelist.get_filters()[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec.get_output_choices(changelist))
+        self.assertEqual(choices[2]['display'], u'the 2000\'s')
+        self.assertEqual(choices[2]['selected'], True)
+        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
+        
+        filterspec = changelist.get_filters()[0][0]
+        self.assertEqual(force_unicode(filterspec.title), u'author')
+        choice = select_by(filterspec.get_output_choices(changelist), "display", "alfred")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
+        
 class CustomUserAdmin(UserAdmin):
     list_filter = ('books_authored', 'books_contributed')
 
 class BookAdmin(admin.ModelAdmin):
-    list_filter = ('year', 'author', 'contributors')
+    list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered')
+    order_by = '-id'
+    
+class DecadeFilterBookAdmin(admin.ModelAdmin):
+    list_filter = ('author', DecadeListFilterWithTitle, DecadeListFilterWithParamName)
     order_by = '-id'
-
-class BoolTestAdmin(admin.ModelAdmin):
-    list_filter = ('completed',)
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
index a20e579..44bb957 100644
--- a/tests/regressiontests/modeladmin/tests.py
+++ b/tests/regressiontests/modeladmin/tests.py
@@ -2,19 +2,21 @@ from datetime import date
 
 from django import forms
 from django.conf import settings
-from django.contrib.admin.options import ModelAdmin, TabularInline, \
-    HORIZONTAL, VERTICAL
+from django.contrib.admin.options import (ModelAdmin, TabularInline,
+    HORIZONTAL, VERTICAL)
 from django.contrib.admin.sites import AdminSite
 from django.contrib.admin.validation import validate
 from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
+from django.contrib.admin.filterspecs import (SingleQueryParameterListFilter,
+     BooleanFieldListFilter)
 from django.core.exceptions import ImproperlyConfigured
 from django.forms.models import BaseModelFormSet
 from django.forms.widgets import Select
 from django.test import TestCase
 from django.utils import unittest
 
-from models import Band, Concert, ValidationTestModel, \
-    ValidationTestInlineModel
+from models import (Band, Concert, ValidationTestModel,
+    ValidationTestInlineModel)
 
 
 # None of the following tests really depend on the content of the request,
@@ -850,9 +852,66 @@ class ValidationTests(unittest.TestCase):
             ValidationTestModelAdmin,
             ValidationTestModel,
         )
+        
+        class RandomClass(object):
+            pass
+        
+        class ValidationTestModelAdmin(ModelAdmin):
+            list_filter = (RandomClass,)
+
+        self.assertRaisesRegexp(
+            ImproperlyConfigured,
+            "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of ListFilterBase.",
+            validate,
+            ValidationTestModelAdmin,
+            ValidationTestModel,
+        )
+        
+        class ValidationTestModelAdmin(ModelAdmin):
+            list_filter = (('is_active', RandomClass),)
+
+        self.assertRaisesRegexp(
+            ImproperlyConfigured,
+            "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.",
+            validate,
+            ValidationTestModelAdmin,
+            ValidationTestModel,
+        )
+
+        class AwesomeFilter(SingleQueryParameterListFilter):
+            def get_title(self):
+                return 'awesomeness'
+            def get_choices(self, request):
+                return (('bit', 'A bit awesome'), ('very', 'Very awesome'), )
+            def get_query_set(self, cl, qs):
+                return qs
+        
+        class ValidationTestModelAdmin(ModelAdmin):
+            list_filter = (('is_active', AwesomeFilter),)
+
+        self.assertRaisesRegexp(
+            ImproperlyConfigured,
+            "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.",
+            validate,
+            ValidationTestModelAdmin,
+            ValidationTestModel,
+        ) 
+
+        class ValidationTestModelAdmin(ModelAdmin):
+            list_filter = (BooleanFieldListFilter,)
 
+        self.assertRaisesRegexp(
+            ImproperlyConfigured,
+            "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.",
+            validate,
+            ValidationTestModelAdmin,
+            ValidationTestModel,
+        ) 
+        
+        # Valid declarations below -----------
+        
         class ValidationTestModelAdmin(ModelAdmin):
-            list_filter = ('is_active',)
+            list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter))
 
         validate(ValidationTestModelAdmin, ValidationTestModel)
 
