Ticket #5833: 5833.custom-filterspecs.diff

File 5833.custom-filterspecs.diff, 15.5 KB (added by Julien Phalip, 8 years ago)

Works with Django 1.3 (still no tests or doc)

  • django/contrib/admin/__init__.py

    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..77c1143 100644
    a b  
    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/filterspecs.py

    diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
    index 965b32b..8580255 100644
    a b import datetime 
    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
    30 
    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)
    4122
    4223    def has_output(self):
    4324        return True
    class FilterSpec(object): 
    4627        raise NotImplementedError()
    4728
    4829    def title(self):
    49         return self.field.verbose_name
     30        raise NotImplementedError()
     31
     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 []
    5044
    5145    def output(self, cl):
    5246        t = []
    class FilterSpec(object): 
    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__(request, params, model, model_admin)
     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        else:
     72            self.field_path = field_path
     73
     74    def title(self):
     75        return self.field.verbose_name
     76
     77    @classmethod
     78    def register(cls, test, factory, high_priority=True):
     79        if high_priority:
     80            cls.field_filter_specs.insert(cls._high_priority_index, (test, factory))
     81            cls._high_priority_index += 1
     82        else:
     83            cls.field_filter_specs.append((test, factory))
     84
     85    @classmethod
     86    def create(cls, f, request, params, model, model_admin, field_path=None):
     87        for test, factory in cls.field_filter_specs:
     88            if test(f):
     89                return factory(f, request, params, model, model_admin,
     90                               field_path=field_path)
     91
     92       
     93class RelatedFilterSpec(FieldFilterSpec):
    6594    def __init__(self, f, request, params, model, model_admin,
    6695                 field_path=None):
    6796        super(RelatedFilterSpec, self).__init__(
    class RelatedFilterSpec(FilterSpec): 
    117146                                   [self.lookup_kwarg]),
    118147                   'display': EMPTY_CHANGELIST_VALUE}
    119148
    120 FilterSpec.register(lambda f: (
     149FieldFilterSpec.register(lambda f: (
    121150        hasattr(f, 'rel') and bool(f.rel) or
    122         isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
     151        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec, False)
    123152
    124 class BooleanFieldFilterSpec(FilterSpec):
     153class BooleanFieldFilterSpec(FieldFilterSpec):
    125154    def __init__(self, f, request, params, model, model_admin,
    126155                 field_path=None):
    127156        super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
    class BooleanFieldFilterSpec(FilterSpec): 
    132161        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    133162        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    134163
    135     def title(self):
    136         return self.field.verbose_name
    137 
    138164    def choices(self, cl):
    139165        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
    140166            yield {'selected': self.lookup_val == v and not self.lookup_val2,
    class BooleanFieldFilterSpec(FilterSpec): 
    149175                                   [self.lookup_kwarg]),
    150176                   'display': _('Unknown')}
    151177
    152 FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
     178FieldFilterSpec.register(lambda f: isinstance(f, models.BooleanField)
    153179                              or isinstance(f, models.NullBooleanField),
    154                                  BooleanFieldFilterSpec)
     180                                 BooleanFieldFilterSpec, False)
    155181
    156 class ChoicesFilterSpec(FilterSpec):
     182class ChoicesFilterSpec(FieldFilterSpec):
    157183    def __init__(self, f, request, params, model, model_admin,
    158184                 field_path=None):
    159185        super(ChoicesFilterSpec, self).__init__(f, request, params, model,
    class ChoicesFilterSpec(FilterSpec): 
    172198                                    {self.lookup_kwarg: k}),
    173199                    'display': v}
    174200
    175 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
     201FieldFilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec, False)
    176202
    177 class DateFieldFilterSpec(FilterSpec):
     203class DateFieldFilterSpec(FieldFilterSpec):
    178204    def __init__(self, f, request, params, model, model_admin,
    179205                 field_path=None):
    180206        super(DateFieldFilterSpec, self).__init__(f, request, params, model,
    class DateFieldFilterSpec(FilterSpec): 
    205231            (_('This year'), {'%s__year' % self.field_path: str(today.year)})
    206232        )
    207233
    208     def title(self):
    209         return self.field.verbose_name
    210 
    211234    def choices(self, cl):
    212235        for title, param_dict in self.links:
    213236            yield {'selected': self.date_params == param_dict,
    class DateFieldFilterSpec(FilterSpec): 
    216239                                   [self.field_generic]),
    217240                   'display': title}
    218241
    219 FilterSpec.register(lambda f: isinstance(f, models.DateField),
    220                               DateFieldFilterSpec)
     242FieldFilterSpec.register(lambda f: isinstance(f, models.DateField),
     243                              DateFieldFilterSpec, False)
    221244
    222245
    223246# This should be registered last, because it's a last resort. For example,
    224247# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
    225248# more appropriate, and the AllValuesFilterSpec won't get used for it.
    226 class AllValuesFilterSpec(FilterSpec):
     249class AllValuesFilterSpec(FieldFilterSpec):
    227250    def __init__(self, f, request, params, model, model_admin,
    228251                 field_path=None):
    229252        super(AllValuesFilterSpec, self).__init__(f, request, params, model,
    class AllValuesFilterSpec(FilterSpec): 
    245268        self.lookup_choices = \
    246269            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
    247270
    248     def title(self):
    249         return self.field.verbose_name
    250 
    251271    def choices(self, cl):
    252272        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    253273        yield {'selected': self.lookup_val is None
    class AllValuesFilterSpec(FilterSpec): 
    276296                                    [self.lookup_kwarg]),
    277297                    'display': EMPTY_CHANGELIST_VALUE}
    278298
    279 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
     299FieldFilterSpec.register(lambda f: True, AllValuesFilterSpec, False)
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index 159afa4..4fdb8a1 100644
    a b from django.db import models 
    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,
    89    HORIZONTAL, VERTICAL)
    def validate(cls, model): 
    5455    # list_filter
    5556    if hasattr(cls, 'list_filter'):
    5657        check_isseq(cls, 'list_filter', cls.list_filter)
    57         for idx, fpath in enumerate(cls.list_filter):
    58             try:
    59                 get_fields_from_path(model, fpath)
    60             except (NotRelationField, FieldDoesNotExist), e:
    61                 raise ImproperlyConfigured(
    62                     "'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
    63                         cls.__name__, idx, fpath
    64                     )
    65                 )
     58        for idx, item in enumerate(cls.list_filter):
     59            # There are three methods of specifying a filter:
     60            #   1: 'field' - a simple field filter, poss. w/ relationships (eg, 'field__rel')
     61            #   2: ('field', SomeFieldFilterSpec) - a field-based filter spec
     62            #   3: SomeFilterSpec - a non-field filter spec
     63            if callable(item) and not isinstance(item, models.Field):
     64                # If item is option 3, it should be a FilterSpec, but not a FieldFilterSpec
     65                if not issubclass(item, FilterSpec) or issubclass(item, FieldFilterSpec):
     66                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     67                            " which is not of type FilterSpec."
     68                            % (cls.__name__, idx, item))
     69            else:
     70                try:
     71                    # Check for option #2 (tuple)
     72                    field, factory = item
     73                except (TypeError, ValueError):
     74                    # item is option #1
     75                    field = item
     76                else:
     77                    # item is option #2
     78                    if not issubclass(factory, FieldFilterSpec):
     79                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
     80                            " refers to '%s' which is not of type FieldFilterSpec."
     81                            % (cls.__name__, idx, factory.__name__))
     82                # Validate the field string
     83                try:
     84                    get_fields_from_path(model, field)
     85                except (NotRelationField, FieldDoesNotExist), e:
     86                    raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
     87                            " which does not refer to a Field."
     88                            % (cls.__name__, idx, field))
    6689
    6790    # list_per_page = 100
    6891    if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
  • django/contrib/admin/views/main.py

    diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
    index 170d168..5ce6fc3 100644
    a b  
    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.exceptions import SuspiciousOperation
    class ChangeList(object): 
    5959            self.list_editable = ()
    6060        else:
    6161            self.list_editable = list_editable
     62        self.filter_specs, self.has_filters = self.get_filters(request)
    6263        self.order_field, self.order_type = self.get_ordering()
    6364        self.query = request.GET.get(SEARCH_VAR, '')
    6465        self.query_set = self.get_query_set()
    6566        self.get_results(request)
    6667        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))
    67         self.filter_specs, self.has_filters = self.get_filters(request)
    6868        self.pk_attname = self.lookup_opts.pk.attname
    6969
    7070    def get_filters(self, request):
    7171        filter_specs = []
    7272        if self.list_filter:
    73             for filter_name in self.list_filter:
    74                 field = get_fields_from_path(self.model, filter_name)[-1]
    75                 spec = FilterSpec.create(field, request, self.params,
    76                                          self.model, self.model_admin,
    77                                          field_path=filter_name)
     73            for item in self.list_filter:
     74                if callable(item):
     75                    spec = item(request, self.params, self.model, self.model_admin)
     76                else:
     77                    field_path = None
     78                    try:
     79                        field, factory = item
     80                    except (TypeError, ValueError):
     81                        field, factory = item, FieldFilterSpec.create
     82                    if not isinstance(field, models.Field):
     83                        field_path = field
     84                        field = get_fields_from_path(self.model, field_path)[-1]
     85                    spec = factory(field, request, self.params, self.model,
     86                            self.model_admin, field_path=field_path)
    7887                if spec and spec.has_output():
    7988                    filter_specs.append(spec)
    8089        return filter_specs, bool(filter_specs)
    class ChangeList(object): 
    166175            order_type = params[ORDER_TYPE_VAR]
    167176        return order_field, order_type
    168177
     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   
    169191    def get_query_set(self):
    170192        use_distinct = False
    171193
    class ChangeList(object): 
    174196        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR):
    175197            if i in lookup_params:
    176198                del lookup_params[i]
     199        key = ''
    177200        for key, value in lookup_params.items():
    178201            if not isinstance(key, str):
    179202                # 'key' will be used as a keyword argument later, so Python
    class ChangeList(object): 
    210233                    "Filtering by %s not allowed" % key
    211234                )
    212235
     236        # Let every filter spec modify the qs and params to its liking
     237        qs = self.apply_filter_specs(qs, lookup_params)
     238       
    213239        # Apply lookup parameters from the query string.
    214240        try:
    215241            qs = qs.filter(**lookup_params)
Back to Top