Ticket #5833: 5833-custom-filter-spec-1.3.diff

File 5833-custom-filter-spec-1.3.diff, 15.6 KB (added by Ben Davis, 13 years ago)

Patch for 1.3, updated w/ apply_filter_specs() method

  • django/contrib/admin/validation.py

     
    33from django.db.models.fields import FieldDoesNotExist
    44from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
    55    _get_foreign_key)
     6from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
    67from django.contrib.admin.util import get_fields_from_path, NotRelationField
    78from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
    89from django.contrib.admin.options import HORIZONTAL, VERTICAL
     
    4950            fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field)
    5051            if field not in cls.list_display:
    5152                raise ImproperlyConfigured("'%s.list_display_links[%d]'"
    52                         "refers to '%s' which is not defined in 'list_display'."
     53                        " refers to '%s' which is not defined in 'list_display'."
    5354                        % (cls.__name__, idx, field))
    5455
    5556    # list_filter
    5657    if hasattr(cls, 'list_filter'):
    5758        check_isseq(cls, 'list_filter', cls.list_filter)
    58         for idx, fpath in enumerate(cls.list_filter):
    59             try:
    60                 get_fields_from_path(model, fpath)
    61             except (NotRelationField, FieldDoesNotExist), e:
    62                 raise ImproperlyConfigured(
    63                     "'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
    64                         cls.__name__, idx, fpath
    65                     )
    66                 )
     59        for idx, item in enumerate(cls.list_filter):
     60            # There are three methods of specifying a filter:
     61            #   1: 'field' - a simple field filter, poss. w/ relationships (eg, 'field__rel')
     62            #   2: ('field', SomeFieldFilterSpec) - a field-based filter spec
     63            #   3: SomeFilterSpec - a non-field filter spec
     64            if callable(item) and not isinstance(item, models.Field):
     65                # If item is option 3, it should be a FilterSpec, but not a FieldFilterSpec
     66                if not issubclass(item, FilterSpec) or issubclass(item, FieldFilterSpec):
     67                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     68                            " which is not of type FilterSpec."
     69                            % (cls.__name__, idx, item))
     70            else:
     71                try:
     72                    # Check for option #2 (tuple)
     73                    field, factory = item
     74                except (TypeError, ValueError):
     75                    # item is option #1
     76                    field = item
     77                else:
     78                    # item is option #2
     79                    if not issubclass(factory, FieldFilterSpec):
     80                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
     81                            " refers to '%s' which is not of type FieldFilterSpec."
     82                            % (cls.__name__, idx, factory.__name__))
     83                # Validate the field string
     84                try:
     85                    get_fields_from_path(model, field)
     86                except (NotRelationField, FieldDoesNotExist), e:
     87                    raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
     88                            " which does not refer to a Field."
     89                            % (cls.__name__, idx, field))
    6790
    6891    # list_per_page = 100
    6992    if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
  • django/contrib/admin/__init__.py

     
    11# ACTION_CHECKBOX_NAME is unused, but should stay since its import from here
    22# has been referenced in documentation.
     3from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
    34from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
    45from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    56from django.contrib.admin.options import StackedInline, TabularInline
  • django/contrib/admin/views/main.py

     
    1 from django.contrib.admin.filterspecs import FilterSpec
     1from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
    22from django.contrib.admin.options import IncorrectLookupParameters
    33from django.contrib.admin.util import quote, get_fields_from_path
    44from django.core.paginator import Paginator, InvalidPage
     
    5757        if ERROR_FLAG in self.params:
    5858            del self.params[ERROR_FLAG]
    5959
     60        self.filter_specs, self.has_filters = self.get_filters(request)
    6061        self.order_field, self.order_type = self.get_ordering()
    6162        self.query = request.GET.get(SEARCH_VAR, '')
    6263        self.query_set = self.get_query_set()
    6364        self.get_results(request)
    6465        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))
    65         self.filter_specs, self.has_filters = self.get_filters(request)
    6666        self.pk_attname = self.lookup_opts.pk.attname
    6767
    6868    def get_filters(self, request):
    6969        filter_specs = []
    7070        if self.list_filter:
    71             for filter_name in self.list_filter:
    72                 field = get_fields_from_path(self.model, filter_name)[-1]
    73                 spec = FilterSpec.create(field, request, self.params,
    74                                          self.model, self.model_admin,
    75                                          field_path=filter_name)
     71            for item in self.list_filter:
     72                if callable(item):
     73                    spec = item(request, self.params, self.model, self.model_admin)
     74                else:
     75                    field_path = None
     76                    try:
     77                        field, factory = item
     78                    except (TypeError, ValueError):
     79                        field, factory = item, FieldFilterSpec.create
     80                    if not isinstance(field, models.Field):
     81                        field_path = field
     82                        field = get_fields_from_path(self.mode, field_path)[-1]
     83                    spec = factory(field, request, self.params, self.model,
     84                            self.model_admin, field_path=field_path)
     85                       
     86
    7687                if spec and spec.has_output():
    7788                    filter_specs.append(spec)
    7889        return filter_specs, bool(filter_specs)
     
    164175            order_type = params[ORDER_TYPE_VAR]
    165176        return order_field, order_type
    166177
     178    def apply_filter_specs(self, qs, lookup_params):
     179        for filter_spec in self.filter_specs:
     180            new_qs = filter_spec.get_query_set(self, qs)
     181            if new_qs is not None and new_qs is not False:
     182                qs = new_qs
     183                # Only consume params if we got a new queryset
     184                for param in filter_spec.consumed_params():
     185                    try:
     186                        del lookup_params[param]
     187                    except KeyError:
     188                        pass
     189        return qs
     190
    167191    def get_query_set(self):
    168192        qs = self.root_query_set
    169193        lookup_params = self.params.copy() # a dictionary of the query string
    170194        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
    171195            if i in lookup_params:
    172196                del lookup_params[i]
     197        key = ''
    173198        for key, value in lookup_params.items():
    174199            if not isinstance(key, str):
    175200                # 'key' will be used as a keyword argument later, so Python
     
    187212                    lookup_params[key] = False
    188213                else:
    189214                    lookup_params[key] = True
     215               
     216        # Let every filter spec modify the qs and params to its liking
     217        qs = self.apply_filter_specs(self, qs, lookup_params)
    190218
    191219        # Apply lookup parameters from the query string.
    192220        try:
  • django/contrib/admin/filterspecs.py

     
    1717
    1818class FilterSpec(object):
    1919    filter_specs = []
    20     def __init__(self, f, request, params, model, model_admin,
    21                  field_path=None):
    22         self.field = f
     20    def __init__(self, request, params, model, model_admin):
    2321        self.params = params
    24         self.field_path = field_path
    25         if field_path is None:
    26             if isinstance(f, models.related.RelatedObject):
    27                 self.field_path = f.var_name
    28             else:
    29                 self.field_path = f.name
    3022
    31     def register(cls, test, factory):
    32         cls.filter_specs.append((test, factory))
    33     register = classmethod(register)
    34 
    35     def create(cls, f, request, params, model, model_admin, field_path=None):
    36         for test, factory in cls.filter_specs:
    37             if test(f):
    38                 return factory(f, request, params, model, model_admin,
    39                                field_path=field_path)
    40     create = classmethod(create)
    41 
    4223    def has_output(self):
    4324        return True
    4425
     
    4627        raise NotImplementedError()
    4728
    4829    def title(self):
    49         return self.field.verbose_name
     30        raise NotImplementedError()
    5031
     32    def get_query_set(self, cl, qs):
     33        return False
     34   
     35    def consumed_params(self):
     36        """
     37        Return a list of parameters to consume from the change list querystring.
     38       
     39        Override this for non-field based FilterSpecs subclasses in order
     40        to consume custom GET parameters, as any GET parameters that are not
     41        consumed and are not a field name raises an exception.
     42        """
     43        return []
     44
    5145    def output(self, cl):
    5246        t = []
    5347        if self.has_output():
     
    6155            t.append('</ul>\n\n')
    6256        return mark_safe("".join(t))
    6357
    64 class RelatedFilterSpec(FilterSpec):
     58
     59class FieldFilterSpec(FilterSpec):
     60    field_filter_specs = []
     61    _high_priority_index = 0
     62
     63    def __init__(self, f, request, params, model, model_admin, field_path=None):
     64        super(FieldFilterSpec, self).__init__(f, request, params, model, model_admin, field_path=None)
     65        self.field = f
     66        if field_path is None:
     67            if isinstance(f, models.related.RelatedObject):
     68                self.field_path = f.var_name
     69            else:
     70                self.field_path = f.name
     71
     72    def title(self):
     73        return self.field.verbose_name
     74
     75    def register(cls, test, factory, high_priority=True):
     76        if high_priority:
     77            cls.field_filter_Specs.insert(cls._high_priority_index, (test, factory))
     78            cls._high_priority_index += 1
     79        else:
     80            cls.field_filter_specs.append((test, factory))
     81    register = classmethod(register)
     82
     83    def create(cls, f, request, params, model, model_admin, field_path=None):
     84        for test, factory in cls.filter_specs:
     85            if test(f):
     86                return factory(f, request, params, model, model_admin,
     87                               field_path=field_path)
     88    create = classmethod(create)
     89
     90       
     91class RelatedFilterSpec(FieldFilterSpec):
    6592    def __init__(self, f, request, params, model, model_admin,
    6693                 field_path=None):
    6794        super(RelatedFilterSpec, self).__init__(
     
    94121                   'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}),
    95122                   'display': val}
    96123
    97 FilterSpec.register(lambda f: (
     124FieldFilterSpec.register(lambda f: (
    98125        hasattr(f, 'rel') and bool(f.rel) or
    99         isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
     126        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec, False)
    100127
    101 class ChoicesFilterSpec(FilterSpec):
     128class ChoicesFilterSpec(FieldFilterSpec):
    102129    def __init__(self, f, request, params, model, model_admin,
    103130                 field_path=None):
    104131        super(ChoicesFilterSpec, self).__init__(f, request, params, model,
     
    116143                    'query_string': cl.get_query_string({self.lookup_kwarg: k}),
    117144                    'display': v}
    118145
    119 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
     146FieldFilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec, False)
    120147
    121 class DateFieldFilterSpec(FilterSpec):
     148class DateFieldFilterSpec(FieldFilterSpec):
    122149    def __init__(self, f, request, params, model, model_admin,
    123150                 field_path=None):
    124151        super(DateFieldFilterSpec, self).__init__(f, request, params, model,
     
    146173            (_('This year'), {'%s__year' % self.field_path: str(today.year)})
    147174        )
    148175
    149     def title(self):
    150         return self.field.verbose_name
    151 
    152176    def choices(self, cl):
    153177        for title, param_dict in self.links:
    154178            yield {'selected': self.date_params == param_dict,
    155179                   'query_string': cl.get_query_string(param_dict, [self.field_generic]),
    156180                   'display': title}
    157181
    158 FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec)
     182FieldFilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec, False)
    159183
    160 class BooleanFieldFilterSpec(FilterSpec):
     184class BooleanFieldFilterSpec(FieldFilterSpec):
    161185    def __init__(self, f, request, params, model, model_admin,
    162186                 field_path=None):
    163187        super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
     
    168192        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    169193        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    170194
    171     def title(self):
    172         return self.field.verbose_name
    173 
    174195    def choices(self, cl):
    175196        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
    176197            yield {'selected': self.lookup_val == v and not self.lookup_val2,
     
    181202                   'query_string': cl.get_query_string({self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]),
    182203                   'display': _('Unknown')}
    183204
    184 FilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField), BooleanFieldFilterSpec)
     205FieldFilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField), BooleanFieldFilterSpec, False)
    185206
    186207# This should be registered last, because it's a last resort. For example,
    187208# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
    188209# more appropriate, and the AllValuesFilterSpec won't get used for it.
    189 class AllValuesFilterSpec(FilterSpec):
     210class AllValuesFilterSpec(FieldFilterSpec):
    190211    def __init__(self, f, request, params, model, model_admin,
    191212                 field_path=None):
    192213        super(AllValuesFilterSpec, self).__init__(f, request, params, model,
     
    204225        self.lookup_choices = \
    205226            queryset.distinct().order_by(f.name).values(f.name)
    206227
    207     def title(self):
    208         return self.field.verbose_name
    209 
    210228    def choices(self, cl):
    211229        yield {'selected': self.lookup_val is None,
    212230               'query_string': cl.get_query_string({}, [self.field_path]),
     
    216234            yield {'selected': self.lookup_val == val,
    217235                   'query_string': cl.get_query_string({self.field_path: val}),
    218236                   'display': val}
    219 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
     237FieldFilterSpec.register(lambda f: True, AllValuesFilterSpec, False)
Back to Top