diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index f8e634e..d725568 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -4,7 +4,7 @@ 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 ListFilter, FieldListFilter
 
 def autodiscover():
     """
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
index 965b32b..39288f0 100644
--- a/django/contrib/admin/filterspecs.py
+++ b/django/contrib/admin/filterspecs.py
@@ -7,80 +7,176 @@ 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.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)
 
+    def should_be_used(self):
+        """
+            Returns True if the filter should be used, based on the parameters
+            given in the query string.
+        """
+        for p in self.used_params():
+            if p in self.params:
+                return True
+        return False
+        
     def has_output(self):
-        return True
+        raise NotImplementedError
 
-    def choices(self, cl):
-        raise NotImplementedError()
+    def _choices(self, cl):
+        raise NotImplementedError
 
-    def title(self):
-        return self.field.verbose_name
+    def get_query_set(self, request, queryset):
+        raise NotImplementedError
+    
+    def used_params(self):
+        """
+        Return a list of parameters to consume from the change list
+        querystring.
+        """
+        raise NotImplementedError
 
     def output(self, cl):
         t = []
         if self.has_output():
-            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
+            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title))
 
-            for choice in self.choices(cl):
+            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 ListFilter(ListFilterBase):
+    """
+        API to make the creation of a custom non-field list filter as simple
+        and easy as possible.
+    """
+    
+    # Parameter that should be used in the query string for that filter.
+    # Defaults to the title, slugified.
+    query_parameter_name = None
+    
+    def __init__(self, request, params, model, model_admin):
+        super(ListFilter, self).__init__(request, params, model, model_admin)
+        if self.query_parameter_name is None:
+            self.query_parameter_name = slugify(self.title)
+        self.lookup_choices = self.get_choices(request)
+    
+    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_name, None)
+
+    def get_choices(self, request):
+        """
+            Must be overriden to return a list of tuples (value, verbose value)
+        """
+        raise NotImplementedError
+
+    def used_params(self):
+        return [self.query_parameter_name]
+
+    def _choices(self, cl):
+        yield {'selected': self.get_value() is None,
+               'query_string': cl.get_query_string({}, [self.query_parameter_name]),
+               'display': _('All')}
+        for k, v in self.lookup_choices:
+            yield {'selected': self.get_value() == k,
+                   'query_string': cl.get_query_string(
+                                   {self.query_parameter_name: 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
+        self.title = field_path
+        super(FieldListFilter, self).__init__(request, params, model, \
+                                              model_admin)
 
-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)
+    def has_output(self):
+        return True
+        
+    def get_query_set(self, request, 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)
 
-        other_model = get_model_from_relation(f)
-        if isinstance(f, (models.ManyToManyField,
+        
+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)
+
+        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 +187,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 _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 +213,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 _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 +245,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 _choices(self, cl):
         yield {'selected': self.lookup_val is None,
                'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
                'display': _('All')}
@@ -172,14 +271,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 +291,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, request, 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 _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 +336,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 +365,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 _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 +396,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/options.py b/django/contrib/admin/options.py
index fbda8b7..cd91273 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1075,7 +1075,7 @@ class ModelAdmin(BaseModelAdmin):
         if (actions and request.method == 'POST' and
                 'index' in request.POST and '_save' not in request.POST):
             if selected:
-                response = self.response_action(request, queryset=cl.get_query_set())
+                response = self.response_action(request, queryset=cl.get_query_set(request))
                 if response:
                     return response
                 else:
@@ -1091,7 +1091,7 @@ class ModelAdmin(BaseModelAdmin):
                 helpers.ACTION_CHECKBOX_NAME in request.POST and
                 'index' not in request.POST and '_save' not in request.POST):
             if selected:
-                response = self.response_action(request, queryset=cl.get_query_set())
+                response = self.response_action(request, queryset=cl.get_query_set(request))
                 if response:
                     return response
                 else:
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index fdf082b..4919eda 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._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..40d14f6 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 ListFilter, 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,37 @@ 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 methods 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 ListFilter.
+                if not issubclass(item, ListFilter):
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
+                            " which is not of type ListFilter." 
+                            % (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 170d168..d854703 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 ListFilter, 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
@@ -61,20 +61,33 @@ class ChangeList(object):
             self.list_editable = list_editable
         self.order_field, self.order_type = self.get_ordering()
         self.query = request.GET.get(SEARCH_VAR, '')
-        self.query_set = self.get_query_set()
+        self.query_set = self.get_query_set(request)
         self.get_results(request)
         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):
         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 ListFilter class.
+                    spec = item(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, 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)
@@ -165,8 +178,20 @@ 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 get_query_set(self):
+    
+    def apply_list_filters(self, request, qs, lookup_params):
+        self.filter_specs, self.has_filters = self.get_filters(request)
+        for filter_spec in self.filter_specs:
+            if filter_spec.should_be_used():
+                qs = filter_spec.get_query_set(request, qs)
+                for param in filter_spec.used_params():
+                    try:
+                        del lookup_params[param]
+                    except KeyError:
+                        pass
+        return qs
+    
+    def get_query_set(self, request):
         use_distinct = False
 
         qs = self.root_query_set
@@ -187,10 +212,11 @@ class ChangeList(object):
                 field_name = key.split('__', 1)[0]
                 try:
                     f = self.lookup_opts.get_field_by_name(field_name)[0]
+                    if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
+                        use_distinct = True
                 except models.FieldDoesNotExist:
-                    raise IncorrectLookupParameters
-                if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
-                    use_distinct = True
+                    # It might be for a non-field custom filter specs.
+                    pass
 
             # if key ends with __in, split parameter into separate values
             if key.endswith('__in'):
@@ -209,8 +235,16 @@ class ChangeList(object):
                 raise SuspiciousOperation(
                     "Filtering by %s not allowed" % key
                 )
-
-        # Apply lookup parameters from the query string.
+        
+        # Keep a copy of cleaned querystring values so it can be passed to
+        # the list filters.
+        self.cleaned_params = lookup_params.copy()
+        
+        # Let every list filter modify the qs and params to its liking
+        qs = self.apply_list_filters(request, 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 415e1fe..47b987a 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -512,7 +512,7 @@ subclass::
     .. note::
 
         ``list_editable`` interacts with a couple of other options in
-        particular ways; you should note the following rules:
+        particular ways; you should note the following rules::
 
             * Any field in ``list_editable`` must also be in ``list_display``.
               You can't edit a field that's not displayed!
@@ -525,13 +525,87 @@ 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``.
+    .. versionchanged:: Development version
 
-    This example, taken from the ``django.contrib.auth.models.User`` model,
-    shows how both ``list_display`` and ``list_filter`` work::
+    Set ``list_filter`` to activate filters in the right sidebar of the change
+    list page of the admin. This 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``.
+
+            .. versionadded:: 1.3
+
+                Field names in ``list_filter`` can also span relations
+                using the ``__`` lookup, for example::
+                
+                    class UserAdminWithLookup(UserAdmin):
+                        list_filter = ('groups__name')
+
+        * a class inheriting from :mod:`django.contrib.admin.ListFilter`,
+          where you need to provide a few attributes and override a few
+          methods::
+        
+            from django.contrib.admin import ListFilter
+            from django.db.models import Q
+            
+            class DecadeBornListFilter(ListFilter):
+                # Human-readable title which will be displayed in the
+                # right sidebar just above the filter options.
+                title = u'decade born'
+                
+                # This is the code name 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_name = u'decade' 
+                
+                def get_choices(self, request):
+                    """
+                       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. You
+                       may specify as many choices as you like, and you may even
+                       vary the list of choices depending on the HttpRequest
+                       object provided as argument to this method.
+                    """
+                    return (
+                        (u'80s', u'in the eighties'),
+                        (u'other', u'other'),
+                    )
+                
+                def get_query_set(self, request, queryset):
+                    """
+                       Returns the filtered queryset based on the value provided
+                       in the query string and retrievable via `get_value()`.
+                       This method is only called when necessary, that is, if
+                       the corresponding parameter is present in the query string.
+                       The HttpRequest object is also provided for your convenience
+                       in case you need to modify the list of results for each request.
+                    """
+                    # 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,)
+        
+        * a tuple, where the first element is a field name and the second
+          element is a class inheriting from
+          :mod:`django.contrib.admin.FieldListFilter`. Note that the
+          `FieldListFilter` API is currently considered internal and prone to
+          refactoring.
+        
+    Finally, the following 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')
@@ -543,13 +617,6 @@ subclass::
 
     (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')
-
 .. attribute:: ModelAdmin.list_per_page
 
     Set ``list_per_page`` to control how many items appear on each paginated
diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
index 5b284c7..db9912f 100644
--- a/tests/regressiontests/admin_filterspecs/models.py
+++ b/tests/regressiontests/admin_filterspecs/models.py
@@ -6,18 +6,8 @@ class Book(models.Model):
     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..e9c68dd 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 (ListFilter,
+    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(ListFilter):
+    
+    def get_choices(self, request):
+        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, request, 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_name = 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(request)
+        self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters(request)[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec._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(request)
+        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(request)[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec._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(request)
+        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(request)[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec._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(request)
+        self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters(request)[0][4]
+        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+        choice = select_by(filterspec._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
-        queryset = changelist.get_query_set()
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set(request)
+        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))
+        self.assertEqual(force_unicode(filterspec.title), u'year')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
 
@@ -59,26 +171,25 @@ class FilterSpecsTests(TestCase):
 
         # 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))
+        self.assertEqual(force_unicode(filterspec.title), u'year')
+        choices = list(filterspec._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
-        queryset = changelist.get_query_set()
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set(request)
+        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))
+        self.assertEqual(force_unicode(filterspec.title), u'author')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
 
@@ -87,25 +198,26 @@ class FilterSpecsTests(TestCase):
 
         # Make sure the correct choice is selected
         filterspec = changelist.get_filters(request)[0][1]
-        self.assertEqual(force_unicode(filterspec.title()), u'author')
+        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._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
-        queryset = changelist.get_query_set()
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set(request)
+        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))
+        self.assertEqual(force_unicode(filterspec.title), u'user')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
 
@@ -114,26 +226,26 @@ class FilterSpecsTests(TestCase):
 
         # 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")
+        self.assertEqual(force_unicode(filterspec.title), u'user')
+        choice = select_by(filterspec._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
-        queryset = changelist.get_query_set()
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set(request)
+        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))
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
 
@@ -142,8 +254,8 @@ class FilterSpecsTests(TestCase):
 
         # 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)
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choice = select_by(filterspec._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
-        queryset = changelist.get_query_set()
+        # Make sure the correct queryset is returned
+        queryset = changelist.get_query_set(request)
+        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))
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
 
@@ -166,46 +279,162 @@ class FilterSpecsTests(TestCase):
 
         # 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)
+        self.assertEqual(force_unicode(filterspec.title), u'book')
+        choice = select_by(filterspec._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
-        queryset = changelist.get_query_set()
+        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(request)
+        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(request)[0][3]
+        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+        choice = select_by(filterspec._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(request)
+        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(request)[0][3]
+        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+        choice = select_by(filterspec._choices(changelist), "display", "Yes")
+        self.assertEqual(choice['selected'], True)
+        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(request)
+        self.assertEqual(list(queryset), [self.django_book])
+        
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters(request)[0][3]
+        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+        choice = select_by(filterspec._choices(changelist), "display", "Unknown")
         self.assertEqual(choice['selected'], True)
-        self.assertEqual(choice['query_string'], '?completed__exact=1')
+        self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
+        
+    def test_ListFilter(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(request)
+        self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
+
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec._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(request)[0][2]
+        self.assertEqual(force_unicode(filterspec.title), u'another publication decade')
+        choices = list(filterspec._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(request)
+        self.assertEqual(list(queryset), [self.bio_book])
+
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec._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(request)
+        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
+
+        # Make sure the correct choice is selected
+        filterspec = changelist.get_filters(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec._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(request)
+        self.assertEqual(list(queryset), [self.djangonaut_book])
+
+        # Make sure the correct choices are selected
+        filterspec = changelist.get_filters(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+        choices = list(filterspec._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(request)[0][0]
+        self.assertEqual(force_unicode(filterspec.title), u'author')
+        choice = select_by(filterspec._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/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index b65f8a4..cfa4123 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -566,7 +566,7 @@ class Gadget(models.Model):
         return self.name
 
 class CustomChangeList(ChangeList):
-    def get_query_set(self):
+    def get_query_set(self, request):
         return self.root_query_set.filter(pk=9999) # Does not exist
 
 class GadgetAdmin(admin.ModelAdmin):
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
index a20e579..c14f743 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 (ListFilter,
+     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,55 @@ 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 of type ListFilter.",
+            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(ListFilter):
+            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,
+        ) 
+        
+        # Valid declarations below -----------
+        
         class ValidationTestModelAdmin(ModelAdmin):
-            list_filter = ('is_active',)
+            list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter))
 
         validate(ValidationTestModelAdmin, ValidationTestModel)
 
