diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index f8e634e..77c1143 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -1,5 +1,6 @@
 # ACTION_CHECKBOX_NAME is unused, but should stay since its import from here
 # has been referenced in documentation.
+from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
 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
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
index 965b32b..12322e0 100644
--- a/django/contrib/admin/filterspecs.py
+++ b/django/contrib/admin/filterspecs.py
@@ -13,74 +13,155 @@ 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.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
+    _filter_specs = []
+    _high_priority_index = 0
+    
+    def __init__(self, request, params, model, model_admin):
         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 has_output(self):
         return True
 
-    def choices(self, cl):
-        raise NotImplementedError()
-
-    def title(self):
-        return self.field.verbose_name
+    def _choices(self, cl):
+        raise NotImplementedError
+
+    def get_title(self):
+        raise NotImplementedError
+
+    def get_query_set(self, cl, qs):
+        return False
+    
+    def _consumed_params(self):
+        """
+        Return a list of parameters to consume from the change list
+        querystring.
+        
+        Override this for non-field based FilterSpecs subclasses in order
+        to consume custom GET parameters, as any GET parameters that are not
+        consumed and are not a field name raises an exception.
+        """
+        return []
 
     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.get_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))
+    
+    @classmethod
+    def register(cls, test, factory, high_priority=True):
+        if high_priority:
+            cls._filter_specs.insert(cls._high_priority_index, (test, factory))
+            cls._high_priority_index += 1
+        else:
+            cls._filter_specs.append((test, factory))
+
+            
+            
+class SimpleFilterSpec(FilterSpec):
+    
+    def __init__(self, request, params, model, model_admin):
+        super(SimpleFilterSpec, self).__init__(request, params, model, model_admin)
+        self.lookup_kwarg = self.get_lookup_parameter()
+        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
+        self.lookup_choices = self.get_choices(request)
+    
+    def has_output(self):
+        return len(self.lookup_choices) > 0
+        
+    def get_lookup_parameter(self):
+        """
+            Returns the parameter that should be used in the query string
+            for that filter. Defaults to the title, slugified.
+        """
+        return slugify(self.get_title())
+    
+    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.lookup_kwarg, None)
+
+    def get_choices(self, request):
+        """
+            Must be overriden to return a list of tuples (value, verbose value)
+        """
+        raise NotImplementedError
+
+    def _consumed_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')}
+        for k, v in self.lookup_choices:
+            yield {'selected': self.lookup_val == k,
+                   'query_string': cl.get_query_string(
+                                   {self.lookup_kwarg: k},
+                                   []),
+                   'display': v}
 
-class RelatedFilterSpec(FilterSpec):
-    def __init__(self, f, request, params, model, model_admin,
+
+            
+class FieldFilterSpec(FilterSpec):
+
+    def __init__(self, field, request, params, model, model_admin, \
+                 field_path=None):
+        super(FieldFilterSpec, self).__init__(request, params, model, \
+                                              model_admin)
+        self.field = field
+        if field_path is None:
+            if isinstance(field, models.related.RelatedObject):
+                self.field_path = field.var_name
+            else:
+                self.field_path = field.name
+        else:
+            self.field_path = field_path
+
+    def get_title(self):
+        return self.field.verbose_name
+
+    @classmethod
+    def create(cls, field, request, params, model, model_admin, field_path=None):
+        for test, factory in cls._filter_specs:
+            if test(field):
+                return factory(field, request, params, model, model_admin,
+                               field_path=field_path)
+
+        
+class RelatedFilterSpec(FieldFilterSpec):
+    def __init__(self, field, request, params, model, model_admin,
                  field_path=None):
         super(RelatedFilterSpec, self).__init__(
-            f, request, params, model, model_admin, field_path=field_path)
+            field, 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)
 
     def has_output(self):
         if isinstance(self.field, models.related.RelatedObject) \
@@ -91,10 +172,10 @@ class RelatedFilterSpec(FilterSpec):
             extra = 0
         return len(self.lookup_choices) + extra > 1
 
-    def title(self):
+    def get_title(self):
         return self.lookup_title
 
-    def choices(self, cl):
+    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,11 +198,11 @@ class RelatedFilterSpec(FilterSpec):
                                    [self.lookup_kwarg]),
                    'display': EMPTY_CHANGELIST_VALUE}
 
-FilterSpec.register(lambda f: (
+FieldFilterSpec.register(lambda f: (
         hasattr(f, 'rel') and bool(f.rel) or
-        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
+        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec, False)
 
-class BooleanFieldFilterSpec(FilterSpec):
+class BooleanFieldFilterSpec(FieldFilterSpec):
     def __init__(self, f, request, params, model, model_admin,
                  field_path=None):
         super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
@@ -132,10 +213,7 @@ class BooleanFieldFilterSpec(FilterSpec):
         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 _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,11 +227,11 @@ class BooleanFieldFilterSpec(FilterSpec):
                                    [self.lookup_kwarg]),
                    'display': _('Unknown')}
 
-FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
+FieldFilterSpec.register(lambda f: isinstance(f, models.BooleanField)
                               or isinstance(f, models.NullBooleanField),
-                                 BooleanFieldFilterSpec)
+                                 BooleanFieldFilterSpec, False)
 
-class ChoicesFilterSpec(FilterSpec):
+class ChoicesFilterSpec(FieldFilterSpec):
     def __init__(self, f, request, params, model, model_admin,
                  field_path=None):
         super(ChoicesFilterSpec, self).__init__(f, request, params, model,
@@ -162,7 +240,7 @@ class ChoicesFilterSpec(FilterSpec):
         self.lookup_kwarg = '%s__exact' % self.field_path
         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
 
-    def choices(self, cl):
+    def _choices(self, cl):
         yield {'selected': self.lookup_val is None,
                'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
                'display': _('All')}
@@ -172,9 +250,9 @@ class ChoicesFilterSpec(FilterSpec):
                                     {self.lookup_kwarg: k}),
                     'display': v}
 
-FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
+FieldFilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec, False)
 
-class DateFieldFilterSpec(FilterSpec):
+class DateFieldFilterSpec(FieldFilterSpec):
     def __init__(self, f, request, params, model, model_admin,
                  field_path=None): 
         super(DateFieldFilterSpec, self).__init__(f, request, params, model,
@@ -205,10 +283,7 @@ class DateFieldFilterSpec(FilterSpec):
             (_('This year'), {'%s__year' % self.field_path: str(today.year)})
         )
 
-    def title(self):
-        return self.field.verbose_name
-
-    def choices(self, cl):
+    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,14 +291,14 @@ class DateFieldFilterSpec(FilterSpec):
                                    [self.field_generic]),
                    'display': title}
 
-FilterSpec.register(lambda f: isinstance(f, models.DateField),
-                              DateFieldFilterSpec)
+FieldFilterSpec.register(lambda f: isinstance(f, models.DateField),
+                              DateFieldFilterSpec, False)
 
 
 # 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):
+class AllValuesFilterSpec(FieldFilterSpec):
     def __init__(self, f, request, params, model, model_admin,
                  field_path=None):
         super(AllValuesFilterSpec, self).__init__(f, request, params, model,
@@ -245,10 +320,7 @@ 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 _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 +348,4 @@ class AllValuesFilterSpec(FilterSpec):
                                     [self.lookup_kwarg]),
                     'display': EMPTY_CHANGELIST_VALUE}
 
-FilterSpec.register(lambda f: True, AllValuesFilterSpec)
+FieldFilterSpec.register(lambda f: True, AllValuesFilterSpec, False)
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index fdf082b..6bac03b 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.get_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..4fdb8a1 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 FilterSpec, FieldFilterSpec
 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 of specifying a filter:
+            #   1: 'field' - a simple field filter, poss. w/ relationships (eg, 'field__rel')
+            #   2: ('field', SomeFieldFilterSpec) - a field-based filter spec
+            #   3: SomeFilterSpec - a non-field filter spec
+            if callable(item) and not isinstance(item, models.Field):
+                # If item is option 3, it should be a FilterSpec, but not a FieldFilterSpec
+                if not issubclass(item, FilterSpec) or issubclass(item, FieldFilterSpec):
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
+                            " which is not of type FilterSpec." 
+                            % (cls.__name__, idx, item))
+            else:
+                try:
+                    # Check for option #2 (tuple)
+                    field, factory = item
+                except (TypeError, ValueError):
+                    # item is option #1
+                    field = item
+                else:
+                    # item is option #2
+                    if not issubclass(factory, FieldFilterSpec):
+                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
+                            " refers to '%s' which is not of type FieldFilterSpec."
+                            % (cls.__name__, idx, factory.__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..77a52d8 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 FilterSpec, FieldFilterSpec
 from django.contrib.admin.options import IncorrectLookupParameters
 from django.contrib.admin.util import quote, get_fields_from_path
 from django.core.exceptions import SuspiciousOperation
@@ -59,22 +59,35 @@ class ChangeList(object):
             self.list_editable = ()
         else:
             self.list_editable = list_editable
+        self.filter_specs, self.has_filters = self.get_filters(request)
         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.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 FilterSpec class.
+                    spec = item(request, self.params, self.model, self.model_admin)
+                else:
+                    field_path = None
+                    try:
+                        # This is custom FilterSpec class for a given field. 
+                        field, factory = item
+                    except (TypeError, ValueError):
+                        # This simply a field name, so use the default
+                        # 'create' factory.
+                        field, factory = item, FieldFilterSpec.create
+                    if not isinstance(field, models.Field):
+                        field_path = field
+                        field = get_fields_from_path(self.model, field_path)[-1]
+                    spec = factory(field, request, self.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)
@@ -166,6 +179,19 @@ class ChangeList(object):
             order_type = params[ORDER_TYPE_VAR]
         return order_field, order_type
 
+    def apply_filter_specs(self, qs, lookup_params): 
+        for filter_spec in self.filter_specs: 
+            new_qs = filter_spec.get_query_set(self, qs) 
+            if new_qs is not None and new_qs is not False: 
+                qs = new_qs 
+                # Only consume params if we got a new queryset 
+                for param in filter_spec._consumed_params(): 
+                    try: 
+                        del lookup_params[param] 
+                    except KeyError: 
+                        pass 
+        return qs
+    
     def get_query_set(self):
         use_distinct = False
 
@@ -187,10 +213,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,7 +236,9 @@ class ChangeList(object):
                 raise SuspiciousOperation(
                     "Filtering by %s not allowed" % key
                 )
-
+        # Let every filter spec modify the qs and params to its liking
+        qs = self.apply_filter_specs(qs, lookup_params)
+        
         # Apply lookup parameters from the query string.
         try:
             qs = qs.filter(**lookup_params)
diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
index 8b9e734..a39db9b 100644
--- a/tests/regressiontests/admin_filterspecs/tests.py
+++ b/tests/regressiontests/admin_filterspecs/tests.py
@@ -5,26 +5,51 @@ 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 SimpleFilterSpec
 
 from models import Book, BoolTest
 
 def select_by(dictlist, key, value):
     return [x for x in dictlist if x[key] == value][0]
 
+
+
+class DecadeFilterSpec(SimpleFilterSpec):
+    
+    def get_title(self):
+        return u'publication decade'
+    
+    def get_choices(self, request):
+        return (
+            (u'90s', u'1990\'s'),
+            (u'00s', u'2000\'s'),
+            (u'other', u'other'),
+        )
+    
+    def get_query_set(self, cl, qs):
+        decade = self.get_value()
+        if decade == u'90s':
+            return qs.filter(year__gte=1990, year__lte=1999)
+        if decade == u'00s':
+            return qs.filter(year__gte=2000, year__lte=2009)
+        return qs
+
+
 class FilterSpecsTests(TestCase):
 
     def setUp(self):
         # 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')
+        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)
         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()
+        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
+        self.gipsy_book.contributors = [self.bob, self.lisa]
+        self.gipsy_book.save()
 
         # BoolTests
         self.trueTest = BoolTest.objects.create(completed=True)
@@ -49,8 +74,8 @@ class FilterSpecsTests(TestCase):
 
         # 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.get_title()), u'year')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
 
@@ -59,8 +84,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'year')
-        choices = list(filterspec.choices(changelist))
+        self.assertEqual(force_unicode(filterspec.get_title()), u'year')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[2]['selected'], True)
         self.assertEqual(choices[2]['query_string'], '?year=2002')
 
@@ -77,8 +102,8 @@ class FilterSpecsTests(TestCase):
 
         # 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.get_title()), u'author')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
 
@@ -87,9 +112,9 @@ 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.get_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)
 
@@ -104,8 +129,8 @@ class FilterSpecsTests(TestCase):
 
         # 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.get_title()), u'user')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], True)
         self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
 
@@ -114,8 +139,8 @@ 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.get_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)
 
@@ -132,8 +157,8 @@ class FilterSpecsTests(TestCase):
 
         # 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.get_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 +167,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.get_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)
 
@@ -156,8 +181,8 @@ class FilterSpecsTests(TestCase):
 
         # 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.get_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,8 +191,8 @@ 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.get_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)
 
@@ -184,8 +209,8 @@ class FilterSpecsTests(TestCase):
 
         # 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(force_unicode(filterspec.get_title()), u'completed')
+        choices = list(filterspec._choices(changelist))
         self.assertEqual(choices[-1]['selected'], False)
         self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
 
@@ -194,12 +219,70 @@ class FilterSpecsTests(TestCase):
 
         # Make sure the correct choice is selected
         filterspec = changelist.get_filters(request)[0][0]
-        self.assertEqual(force_unicode(filterspec.title()), u'completed')
+        self.assertEqual(force_unicode(filterspec.get_title()), u'completed')
         # order of choices depends on User model, which has no order
-        choice = select_by(filterspec.choices(changelist), "display", "Yes")
+        choice = select_by(filterspec._choices(changelist), "display", "Yes")
         self.assertEqual(choice['selected'], True)
         self.assertEqual(choice['query_string'], '?completed__exact=1')
 
+    def test_simple_filterspecs(self):
+        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
+        
+        # Look for books in the 1990s --------
+        
+        request = self.request_factory.get('/', {'publication-decade': '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(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
+        choices = list(filterspec._choices(changelist))
+        self.assertEqual(choices[1]['selected'], True)
+        self.assertEqual(choices[1]['query_string'], '?publication-decade=90s')
+        
+        # Look for books in the 2000s --------
+        
+        request = self.request_factory.get('/', {'publication-decade': '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(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
+        choices = list(filterspec._choices(changelist))
+        self.assertEqual(choices[2]['selected'], True)
+        self.assertEqual(choices[2]['query_string'], '?publication-decade=00s')
+        
+        # Combine multiple filters --------
+        
+        request = self.request_factory.get('/', {'publication-decade': '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(request)[0][1]
+        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
+        choices = list(filterspec._choices(changelist))
+        self.assertEqual(choices[2]['selected'], True)
+        self.assertEqual(choices[2]['query_string'], '?publication-decade=00s&author__id__exact=%s' % self.alfred.pk)
+        
+        filterspec = changelist.get_filters(request)[0][0]
+        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
+        choice = select_by(filterspec._choices(changelist), "display", "alfred")
+        self.assertEqual(choice['selected'], True)
+        self.assertEqual(choice['query_string'], '?publication-decade=00s&author__id__exact=%s' % self.alfred.pk)
+        
+        
 class CustomUserAdmin(UserAdmin):
     list_filter = ('books_authored', 'books_contributed')
 
@@ -209,3 +292,7 @@ class BookAdmin(admin.ModelAdmin):
 
 class BoolTestAdmin(admin.ModelAdmin):
     list_filter = ('completed',)
+
+class DecadeFilterBookAdmin(admin.ModelAdmin):
+    list_filter = ('author', DecadeFilterSpec)
+    order_by = '-id'
