
 
Index: django/db/models/manager.py
Index: django/contrib/admin/validation.py
===================================================================
--- django/contrib/admin/validation.py	(revision 9836)
+++ django/contrib/admin/validation.py	(working copy)
@@ -8,6 +8,7 @@
 from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model
 from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
 from django.contrib.admin.options import HORIZONTAL, VERTICAL
+from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
 
 __all__ = ['validate']
 
@@ -50,14 +51,32 @@
             fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field)
             if field not in cls.list_display:
                 raise ImproperlyConfigured("'%s.list_display_links[%d]'"
-                        "refers to '%s' which is not defined in 'list_display'."
+                        " refers to '%s' which is not defined in 'list_display'."
                         % (cls.__name__, idx, field))
 
     # list_filter
     if hasattr(cls, 'list_filter'):
         check_isseq(cls, 'list_filter', cls.list_filter)
-        for idx, field in enumerate(cls.list_filter):
-            get_field(cls, model, opts, 'list_filter[%d]' % idx, field)
+        for idx, item in enumerate(cls.list_filter):
+            if callable(item):
+                # Make sure the item is not FieldFilterSpec or a subclass thereof
+                # since it has a different __init__ signature, which leads to 
+                # strange exceptions if not caught here
+                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:
+                        field_name, factory = item
+                    except (TypeError, ValueError):
+                        field_name = item
+                    else:
+                        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 field
+                    get_field(cls, model, opts, 'list_filter[%d]' % idx, field)
 
     # list_per_page = 100
     if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
Index: django/contrib/admin/__init__.py
===================================================================
--- django/contrib/admin/__init__.py	(revision 9836)
+++ django/contrib/admin/__init__.py	(working copy)
@@ -1,6 +1,7 @@
 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
 from django.contrib.admin.options import StackedInline, TabularInline
 from django.contrib.admin.sites import AdminSite, site
+from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
 
 # A flag to tell us if autodiscover is running.  autodiscover will set this to
 # True while running, and False when it finishes.
Index: django/contrib/admin/filterspecs.py
===================================================================
--- django/contrib/admin/filterspecs.py	(revision 9836)
+++ django/contrib/admin/filterspecs.py	(working copy)
@@ -14,29 +14,30 @@
 import datetime
 
 class FilterSpec(object):
-    filter_specs = []
-    def __init__(self, f, request, params, model, model_admin):
-        self.field = f
+    def __init__(self, request, params, model, model_admin):
         self.params = params
 
-    def register(cls, test, factory):
-        cls.filter_specs.append((test, factory))
-    register = classmethod(register)
-
-    def create(cls, f, request, params, model, model_admin):
-        for test, factory in cls.filter_specs:
-            if test(f):
-                return factory(f, request, params, model, model_admin)
-    create = classmethod(create)
-
     def has_output(self):
         return True
 
     def choices(self, cl):
         raise NotImplementedError()
-
+        
     def title(self):
-        return self.field.verbose_name
+        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 = []
@@ -50,10 +51,35 @@
                      choice['display']))
             t.append('</ul>\n\n')
         return mark_safe("".join(t))
+        
+class FieldFilterSpec(FilterSpec):
+    field_filter_specs = []
+    _high_priority_index = 0
+    
+    def __init__(self, request, params, model, model_admin, f):
+        super(FieldFilterSpec, self).__init__(request, params, model, model_admin)
+        self.field = f
+        
+    def title(self):
+        return self.field.verbose_name
+    
+    def register(cls, test, factory, high_priority=True):
+        if high_priority:
+            cls.field_filter_specs.insert(cls._high_priority_index, (test, factory))
+            cls._high_priority_index += 1
+        else:
+            cls.field_filter_specs.append((test, factory))
+    register = classmethod(register)
+    
+    def create(cls, request, params, model, model_admin, f):
+        for test, factory in cls.field_filter_specs:
+            if test(f):
+                return factory(request, params, model, model_admin, f)
+    create = classmethod(create)
 
-class RelatedFilterSpec(FilterSpec):
-    def __init__(self, f, request, params, model, model_admin):
-        super(RelatedFilterSpec, self).__init__(f, request, params, model, model_admin)
+class RelatedFilterSpec(FieldFilterSpec):
+    def __init__(self, request, params, model, model_admin, f):
+        super(RelatedFilterSpec, self).__init__(request, params, model, model_admin, f)
         if isinstance(f, models.ManyToManyField):
             self.lookup_title = f.rel.to._meta.verbose_name
         else:
@@ -77,11 +103,11 @@
                    'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}),
                    'display': val}
 
-FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)
+FieldFilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec, False)
 
-class ChoicesFilterSpec(FilterSpec):
-    def __init__(self, f, request, params, model, model_admin):
-        super(ChoicesFilterSpec, self).__init__(f, request, params, model, model_admin)
+class ChoicesFilterSpec(FieldFilterSpec):
+    def __init__(self, request, params, model, model_admin, f):
+        super(ChoicesFilterSpec, self).__init__(request, params, model, model_admin, f)
         self.lookup_kwarg = '%s__exact' % f.name
         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
 
@@ -94,11 +120,11 @@
                     'query_string': cl.get_query_string({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):
-    def __init__(self, f, request, params, model, model_admin):
-        super(DateFieldFilterSpec, self).__init__(f, request, params, model, model_admin)
+class DateFieldFilterSpec(FieldFilterSpec):
+    def __init__(self, request, params, model, model_admin, f):
+        super(DateFieldFilterSpec, self).__init__(request, params, model, model_admin, f)
 
         self.field_generic = '%s__' % self.field.name
 
@@ -120,28 +146,22 @@
             (_('This year'), {'%s__year' % self.field.name: str(today.year)})
         )
 
-    def title(self):
-        return self.field.verbose_name
-
     def choices(self, cl):
         for title, param_dict in self.links:
             yield {'selected': self.date_params == param_dict,
                    'query_string': cl.get_query_string(param_dict, [self.field_generic]),
                    'display': title}
 
-FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec)
+FieldFilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec, False)
 
-class BooleanFieldFilterSpec(FilterSpec):
-    def __init__(self, f, request, params, model, model_admin):
-        super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, model_admin)
+class BooleanFieldFilterSpec(FieldFilterSpec):
+    def __init__(self, request, params, model, model_admin, f):
+        super(BooleanFieldFilterSpec, self).__init__(request, params, model, model_admin, f)
         self.lookup_kwarg = '%s__exact' % f.name
         self.lookup_kwarg2 = '%s__isnull' % f.name
         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):
         for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
             yield {'selected': self.lookup_val == v and not self.lookup_val2,
@@ -152,20 +172,17 @@
                    'query_string': cl.get_query_string({self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]),
                    'display': _('Unknown')}
 
-FilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField), BooleanFieldFilterSpec)
+FieldFilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField), BooleanFieldFilterSpec, 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):
-    def __init__(self, f, request, params, model, model_admin):
-        super(AllValuesFilterSpec, self).__init__(f, request, params, model, model_admin)
+class AllValuesFilterSpec(FieldFilterSpec):
+    def __init__(self, request, params, model, model_admin, f):
+        super(AllValuesFilterSpec, self).__init__(request, params, model, model_admin, f)
         self.lookup_val = request.GET.get(f.name, None)
         self.lookup_choices = model_admin.queryset(request).distinct().order_by(f.name).values(f.name)
 
-    def title(self):
-        return self.field.verbose_name
-
     def choices(self, cl):
         yield {'selected': self.lookup_val is None,
                'query_string': cl.get_query_string({}, [self.field.name]),
@@ -175,4 +192,4 @@
             yield {'selected': self.lookup_val == val,
                    'query_string': cl.get_query_string({self.field.name: val}),
                    'display': val}
-FilterSpec.register(lambda f: True, AllValuesFilterSpec)
+FieldFilterSpec.register(lambda f: True, AllValuesFilterSpec, False)
Index: django/contrib/admin/views/main.py
===================================================================
--- django/contrib/admin/views/main.py	(revision 9836)
+++ django/contrib/admin/views/main.py	(working copy)
@@ -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
 from django.core.paginator import Paginator, InvalidPage
@@ -62,20 +62,28 @@
         if ERROR_FLAG in self.params:
             del self.params[ERROR_FLAG]
 
+        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:
-            filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter]
-            for f in filter_fields:
-                spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin)
+            for item in self.list_filter:
+                if callable(item):
+                    spec = item(request, self.params, self.model, self.model_admin)
+                else:
+                    try:
+                        field_name, factory = item
+                    except (TypeError, ValueError):
+                        field_name, factory = item, FieldFilterSpec.create
+                    field = self.lookup_opts.get_field(field_name)
+                    spec = factory(request, self.params, self.model,
+                                   self.model_admin, field)
                 if spec and spec.has_output():
                     filter_specs.append(spec)
         return filter_specs, bool(filter_specs)
@@ -183,6 +191,18 @@
             # if key ends with __in, split parameter into separate values
             if key.endswith('__in'):
                 lookup_params[key] = value.split(',')
+                
+        # Let every filter spec modify the qs and params to its liking
+        for filter_spec in self.filter_specs:
+            new_qs = filter_spec.get_query_set(self, qs)
+            if new_qs:
+                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
 
         # Apply lookup parameters from the query string.
         try: