Ticket #5833: 5833.custom-filterspecs.3.diff
File 5833.custom-filterspecs.3.diff, 49.1 KB (added by , 14 years ago) |
---|
-
django/contrib/admin/__init__.py
diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index f8e634e..86695b1 100644
a b 1 1 # ACTION_CHECKBOX_NAME is unused, but should stay since its import from here 2 2 # has been referenced in documentation. 3 from django.contrib.admin.filterspecs import ListFilter, FieldListFilter,\ 4 SimpleListFilter 3 5 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME 4 6 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL 5 7 from 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..ac2ca72 100644
a b from django.utils.html import escape 13 13 from django.utils.safestring import mark_safe 14 14 from django.contrib.admin.util import get_model_from_relation, \ 15 15 reverse_field_path, get_limit_choices_to_from_path 16 from django.template.defaultfilters import slugify 16 17 import datetime 17 18 18 class FilterSpec(object): 19 filter_specs = [] 20 def __init__(self, f, request, params, model, model_admin, 21 field_path=None): 22 self.field = f 19 class ListFilter(object): 20 21 def __init__(self, request, params, model, model_admin): 23 22 self.params = params 24 self.field_path = field_path25 if field_path is None:26 if isinstance(f, models.related.RelatedObject):27 self.field_path = f.var_name28 else:29 self.field_path = f.name30 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 23 42 24 def has_output(self): 43 25 return True 44 26 45 def choices(self, cl): 46 raise NotImplementedError() 47 48 def title(self): 49 return self.field.verbose_name 27 def _choices(self, cl): 28 raise NotImplementedError 29 30 def get_title(self): 31 raise NotImplementedError 32 33 def get_query_set(self, changelist, queryset): 34 return queryset 35 36 def _consumed_params(self): 37 """ 38 Return a list of parameters to consume from the change list 39 querystring. 40 41 Override this for non-field based ListFilter subclasses in order 42 to consume custom GET parameters, as any GET parameters that are not 43 consumed and are not a field name raises an exception. 44 """ 45 return [] 50 46 51 47 def output(self, cl): 52 48 t = [] 53 49 if self.has_output(): 54 t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self. title()))50 t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.get_title())) 55 51 56 for choice in self. choices(cl):52 for choice in self._choices(cl): 57 53 t.append(u'<li%s><a href="%s">%s</a></li>\n' % \ 58 54 ((choice['selected'] and ' class="selected"' or ''), 59 55 iri_to_uri(choice['query_string']), 60 56 choice['display'])) 61 57 t.append('</ul>\n\n') 62 58 return mark_safe("".join(t)) 59 63 60 64 class RelatedFilterSpec(FilterSpec): 65 def __init__(self, f, request, params, model, model_admin, 61 62 63 64 class SimpleListFilter(ListFilter): 65 66 def __init__(self, request, params, model, model_admin): 67 super(SimpleListFilter, self).__init__(request, params, model, model_admin) 68 self.lookup_kwarg = self.get_query_parameter_name() 69 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 70 self.lookup_choices = self.get_choices(request) 71 72 def has_output(self): 73 return len(self.lookup_choices) > 0 74 75 def get_query_parameter_name(self): 76 """ 77 Returns the parameter that should be used in the query string 78 for that filter. Defaults to the title, slugified. 79 """ 80 return slugify(self.get_title()) 81 82 def get_value(self): 83 """ 84 Returns the value given in the query string for this filter, 85 if any. Returns None otherwise. 86 """ 87 return self.params.get(self.lookup_kwarg, None) 88 89 def get_choices(self, request): 90 """ 91 Must be overriden to return a list of tuples (value, verbose value) 92 """ 93 raise NotImplementedError 94 95 def _consumed_params(self): 96 return [self.lookup_kwarg] 97 98 def _choices(self, cl): 99 yield {'selected': self.lookup_val is None, 100 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 101 'display': _('All')} 102 for k, v in self.lookup_choices: 103 yield {'selected': self.lookup_val == k, 104 'query_string': cl.get_query_string( 105 {self.lookup_kwarg: k}, 106 []), 107 'display': v} 108 109 110 111 class FieldListFilter(ListFilter): 112 _field_list_filters = [] 113 114 def __init__(self, field, request, params, model, model_admin, \ 115 field_path=None): 116 super(FieldListFilter, self).__init__(request, params, model, \ 117 model_admin) 118 self.field = field 119 if field_path is None: 120 if isinstance(field, models.related.RelatedObject): 121 self.field_path = field.var_name 122 else: 123 self.field_path = field.name 124 else: 125 self.field_path = field_path 126 127 def get_title(self): 128 return self.field.verbose_name 129 130 @classmethod 131 def register(cls, test, list_filter_class, insert_first=False): 132 if insert_first: 133 # This is to allow overriding default filters for certain types 134 # of fields with custom ones. The first found in the list is used 135 # in priority. 136 cls._field_list_filters.insert(0, (test, list_filter_class)) 137 else: 138 cls._field_list_filters.append((test, list_filter_class)) 139 140 @classmethod 141 def create(cls, field, request, params, model, model_admin, field_path=None): 142 for test, list_filter_class in cls._field_list_filters: 143 if test(field): 144 return list_filter_class(field, request, params, model, model_admin, 145 field_path=field_path) 146 147 148 class RelatedFieldListFilter(FieldListFilter): 149 def __init__(self, field, request, params, model, model_admin, 66 150 field_path=None): 67 super(RelatedFi lterSpec, self).__init__(68 f , request, params, model, model_admin, field_path=field_path)151 super(RelatedFieldListFilter, self).__init__( 152 field, request, params, model, model_admin, field_path=field_path) 69 153 70 other_model = get_model_from_relation(f )71 if isinstance(f , (models.ManyToManyField,154 other_model = get_model_from_relation(field) 155 if isinstance(field, (models.ManyToManyField, 72 156 models.related.RelatedObject)): 73 157 # no direct field on this model, get name from other model 74 158 self.lookup_title = other_model._meta.verbose_name 75 159 else: 76 self.lookup_title = f .verbose_name # use field name160 self.lookup_title = field.verbose_name # use field name 77 161 rel_name = other_model._meta.pk.name 78 162 self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name) 79 163 self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path) 80 164 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 81 165 self.lookup_val_isnull = request.GET.get( 82 166 self.lookup_kwarg_isnull, None) 83 self.lookup_choices = f .get_choices(include_blank=False)167 self.lookup_choices = field.get_choices(include_blank=False) 84 168 85 169 def has_output(self): 86 170 if isinstance(self.field, models.related.RelatedObject) \ … … class RelatedFilterSpec(FilterSpec): 91 175 extra = 0 92 176 return len(self.lookup_choices) + extra > 1 93 177 94 def title(self):178 def get_title(self): 95 179 return self.lookup_title 96 180 97 def choices(self, cl):181 def _choices(self, cl): 98 182 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 99 183 yield {'selected': self.lookup_val is None 100 184 and not self.lookup_val_isnull, … … class RelatedFilterSpec(FilterSpec): 117 201 [self.lookup_kwarg]), 118 202 'display': EMPTY_CHANGELIST_VALUE} 119 203 120 Fi lterSpec.register(lambda f: (204 FieldListFilter.register(lambda f: ( 121 205 hasattr(f, 'rel') and bool(f.rel) or 122 isinstance(f, models.related.RelatedObject)), RelatedFi lterSpec)206 isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) 123 207 124 class BooleanField FilterSpec(FilterSpec):208 class BooleanFieldListFilter(FieldListFilter): 125 209 def __init__(self, f, request, params, model, model_admin, 126 210 field_path=None): 127 super(BooleanField FilterSpec, self).__init__(f, request, params, model,211 super(BooleanFieldListFilter, self).__init__(f, request, params, model, 128 212 model_admin, 129 213 field_path=field_path) 130 214 self.lookup_kwarg = '%s__exact' % self.field_path … … class BooleanFieldFilterSpec(FilterSpec): 132 216 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 133 217 self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None) 134 218 135 def title(self): 136 return self.field.verbose_name 137 138 def choices(self, cl): 219 def _choices(self, cl): 139 220 for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')): 140 221 yield {'selected': self.lookup_val == v and not self.lookup_val2, 141 222 'query_string': cl.get_query_string( … … class BooleanFieldFilterSpec(FilterSpec): 149 230 [self.lookup_kwarg]), 150 231 'display': _('Unknown')} 151 232 152 Fi lterSpec.register(lambda f: isinstance(f, models.BooleanField)233 FieldListFilter.register(lambda f: isinstance(f, models.BooleanField) 153 234 or isinstance(f, models.NullBooleanField), 154 BooleanField FilterSpec)235 BooleanFieldListFilter) 155 236 156 class ChoicesFi lterSpec(FilterSpec):237 class ChoicesFieldListFilter(FieldListFilter): 157 238 def __init__(self, f, request, params, model, model_admin, 158 239 field_path=None): 159 super(ChoicesFi lterSpec, self).__init__(f, request, params, model,240 super(ChoicesFieldListFilter, self).__init__(f, request, params, model, 160 241 model_admin, 161 242 field_path=field_path) 162 243 self.lookup_kwarg = '%s__exact' % self.field_path 163 self.lookup_val = request.GET.get(self.lookup_kwarg , None)244 self.lookup_val = request.GET.get(self.lookup_kwarg) 164 245 165 def choices(self, cl):246 def _choices(self, cl): 166 247 yield {'selected': self.lookup_val is None, 167 248 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 168 249 'display': _('All')} … … class ChoicesFilterSpec(FilterSpec): 172 253 {self.lookup_kwarg: k}), 173 254 'display': v} 174 255 175 Fi lterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)256 FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter) 176 257 177 class DateField FilterSpec(FilterSpec):258 class DateFieldListFilter(FieldListFilter): 178 259 def __init__(self, f, request, params, model, model_admin, 179 260 field_path=None): 180 super(DateField FilterSpec, self).__init__(f, request, params, model,261 super(DateFieldListFilter, self).__init__(f, request, params, model, 181 262 model_admin, 182 263 field_path=field_path) 183 264 … … class DateFieldFilterSpec(FilterSpec): 205 286 (_('This year'), {'%s__year' % self.field_path: str(today.year)}) 206 287 ) 207 288 208 def title(self): 209 return self.field.verbose_name 210 211 def choices(self, cl): 289 def _choices(self, cl): 212 290 for title, param_dict in self.links: 213 291 yield {'selected': self.date_params == param_dict, 214 292 'query_string': cl.get_query_string( … … class DateFieldFilterSpec(FilterSpec): 216 294 [self.field_generic]), 217 295 'display': title} 218 296 219 Fi lterSpec.register(lambda f: isinstance(f, models.DateField),220 DateField FilterSpec)297 FieldListFilter.register(lambda f: isinstance(f, models.DateField), 298 DateFieldListFilter) 221 299 222 300 223 301 # This should be registered last, because it's a last resort. For example, 224 # if a field is eligible to use the BooleanField FilterSpec, that'd be much225 # more appropriate, and the AllValuesFi lterSpecwon't get used for it.226 class AllValuesFi lterSpec(FilterSpec):302 # if a field is eligible to use the BooleanFieldListFilter, that'd be much 303 # more appropriate, and the AllValuesFieldListFilter won't get used for it. 304 class AllValuesFieldListFilter(FieldListFilter): 227 305 def __init__(self, f, request, params, model, model_admin, 228 306 field_path=None): 229 super(AllValuesFi lterSpec, self).__init__(f, request, params, model,307 super(AllValuesFieldListFilter, self).__init__(f, request, params, model, 230 308 model_admin, 231 309 field_path=field_path) 232 310 self.lookup_kwarg = self.field_path … … class AllValuesFilterSpec(FilterSpec): 245 323 self.lookup_choices = \ 246 324 queryset.distinct().order_by(f.name).values_list(f.name, flat=True) 247 325 248 def title(self): 249 return self.field.verbose_name 250 251 def choices(self, cl): 326 def _choices(self, cl): 252 327 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 253 328 yield {'selected': self.lookup_val is None 254 329 and self.lookup_val_isnull is None, … … class AllValuesFilterSpec(FilterSpec): 276 351 [self.lookup_kwarg]), 277 352 'display': EMPTY_CHANGELIST_VALUE} 278 353 279 Fi lterSpec.register(lambda f: True, AllValuesFilterSpec)354 FieldListFilter.register(lambda f: True, AllValuesFieldListFilter) -
django/contrib/admin/templatetags/admin_list.py
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index fdf082b..6bac03b 100644
a b def search_form(cl): 317 317 search_form = register.inclusion_tag('admin/search_form.html')(search_form) 318 318 319 319 def admin_list_filter(cl, spec): 320 return {'title': spec. title(), 'choices' : list(spec.choices(cl))}320 return {'title': spec.get_title(), 'choices' : list(spec._choices(cl))} 321 321 admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter) 322 322 323 323 def admin_actions(context): -
django/contrib/admin/validation.py
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 159afa4..278b45d 100644
a b from django.db import models 3 3 from django.db.models.fields import FieldDoesNotExist 4 4 from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model, 5 5 _get_foreign_key) 6 from django.contrib.admin.filterspecs import ListFilter, FieldListFilter 6 7 from django.contrib.admin.util import get_fields_from_path, NotRelationField 7 8 from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin, 8 9 HORIZONTAL, VERTICAL) … … def validate(cls, model): 54 55 # list_filter 55 56 if hasattr(cls, 'list_filter'): 56 57 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 basic field filter, possibly w/ relationships (eg, 'field__rel') 61 # 2: ('field', SomeFieldListFilter) - a field-based list filter class 62 # 3: SomeListFilter - a non-field list filter class 63 if callable(item) and not isinstance(item, models.Field): 64 # If item is option 3, it should be a ListFilter, but not a FieldListFilter 65 if not issubclass(item, ListFilter) or issubclass(item, FieldListFilter): 66 raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" 67 " which is not of type ListFilter." 68 % (cls.__name__, idx, item.__name__)) 69 else: 70 try: 71 # Check for option #2 (tuple) 72 field, list_filter_class = item 73 except (TypeError, ValueError): 74 # item is option #1 75 field = item 76 else: 77 # item is option #2 78 if not issubclass(list_filter_class, FieldListFilter): 79 raise ImproperlyConfigured("'%s.list_filter[%d][1]'" 80 " is '%s' which is not of type FieldListFilter." 81 % (cls.__name__, idx, list_filter_class.__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)) 66 89 67 90 # list_per_page = 100 68 91 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..de037e6 100644
a b 1 from django.contrib.admin.filterspecs import FilterSpec1 from django.contrib.admin.filterspecs import ListFilter, FieldListFilter 2 2 from django.contrib.admin.options import IncorrectLookupParameters 3 3 from django.contrib.admin.util import quote, get_fields_from_path 4 4 from django.core.exceptions import SuspiciousOperation … … class ChangeList(object): 59 59 self.list_editable = () 60 60 else: 61 61 self.list_editable = list_editable 62 self.filter_specs, self.has_filters = self.get_filters(request) 62 63 self.order_field, self.order_type = self.get_ordering() 63 64 self.query = request.GET.get(SEARCH_VAR, '') 64 65 self.query_set = self.get_query_set() 65 66 self.get_results(request) 66 67 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)68 68 self.pk_attname = self.lookup_opts.pk.attname 69 69 70 70 def get_filters(self, request): 71 71 filter_specs = [] 72 72 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 # This is simply a custom ListFilter class. 76 spec = item(request, self.params, self.model, self.model_admin) 77 else: 78 field_path = None 79 try: 80 # This is custom FieldListFilter class for a given field. 81 field, field_list_filter_class = item 82 except (TypeError, ValueError): 83 # This is simply a field name, so use the default 84 # FieldListFilter class that has been registered for 85 # the type of the given field. 86 field, field_list_filter_class = item, FieldListFilter.create 87 if not isinstance(field, models.Field): 88 field_path = field 89 field = get_fields_from_path(self.model, field_path)[-1] 90 spec = field_list_filter_class(field, request, self.params, self.model, 91 self.model_admin, field_path=field_path) 78 92 if spec and spec.has_output(): 79 93 filter_specs.append(spec) 80 94 return filter_specs, bool(filter_specs) … … class ChangeList(object): 166 180 order_type = params[ORDER_TYPE_VAR] 167 181 return order_field, order_type 168 182 183 def apply_list_filters(self, qs, lookup_params): 184 for filter_spec in self.filter_specs: 185 new_qs = filter_spec.get_query_set(self, qs) 186 if new_qs is not None and new_qs is not False: 187 qs = new_qs 188 # Only consume params if we got a new queryset 189 for param in filter_spec._consumed_params(): 190 try: 191 del lookup_params[param] 192 except KeyError: 193 pass 194 return qs 195 169 196 def get_query_set(self): 170 197 use_distinct = False 171 198 … … class ChangeList(object): 187 214 field_name = key.split('__', 1)[0] 188 215 try: 189 216 f = self.lookup_opts.get_field_by_name(field_name)[0] 217 if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel): 218 use_distinct = True 190 219 except models.FieldDoesNotExist: 191 raise IncorrectLookupParameters 192 if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel): 193 use_distinct = True 220 # It might be for a non-field custom filter specs. 221 pass 194 222 195 223 # if key ends with __in, split parameter into separate values 196 224 if key.endswith('__in'): … … class ChangeList(object): 209 237 raise SuspiciousOperation( 210 238 "Filtering by %s not allowed" % key 211 239 ) 212 240 # Let every list filter modify the qs and params to its liking 241 qs = self.apply_list_filters(qs, lookup_params) 242 213 243 # Apply lookup parameters from the query string. 214 244 try: 215 245 qs = qs.filter(**lookup_params) -
django/db/models/related.py
diff --git a/django/db/models/related.py b/django/db/models/related.py index 7734230..90995d7 100644
a b class RelatedObject(object): 27 27 as SelectField choices for this field. 28 28 29 29 Analogue of django.db.models.fields.Field.get_choices, provided 30 initially for utilisation by RelatedFi lterSpec.30 initially for utilisation by RelatedFieldListFilter. 31 31 """ 32 32 first_choice = include_blank and blank_choice or [] 33 33 queryset = self.model._default_manager.all() -
docs/ref/contrib/admin/index.txt
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 415e1fe..a8c6b8b 100644
a b subclass:: 512 512 .. note:: 513 513 514 514 ``list_editable`` interacts with a couple of other options in 515 particular ways; you should note the following rules: 515 particular ways; you should note the following rules:: 516 516 517 517 * Any field in ``list_editable`` must also be in ``list_display``. 518 518 You can't edit a field that's not displayed! … … subclass:: 525 525 526 526 .. attribute:: ModelAdmin.list_filter 527 527 528 Set ``list_filter`` to activate filters in the right sidebar of the change 529 list page of the admin. This should be a list of field names, and each 530 specified field should be either a ``BooleanField``, ``CharField``, 531 ``DateField``, ``DateTimeField``, ``IntegerField`` or ``ForeignKey``. 528 .. versionchanged:: Development version 532 529 533 This example, taken from the ``django.contrib.auth.models.User`` model, 534 shows how both ``list_display`` and ``list_filter`` work:: 530 Set ``list_filter`` to activate filters in the right sidebar of the change 531 list page of the admin. This should be a list of elements, where each 532 element should be of one of the following types:: 533 534 * a field name, where the specified field should be either a 535 ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``, 536 ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``. 537 538 .. versionadded:: 1.3 539 540 Field names in ``list_filter`` can also span relations 541 using the ``__`` lookup, for example:: 542 543 class UserAdminWithLookup(UserAdmin): 544 list_filter = ('groups__name') 545 546 * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`, 547 where you need to override a few methods:: 548 549 from django.contrib.admin import SimpleListFilter 550 from django.db.models import Q 551 552 class DecadeBornListFilter(SimpleListFilter): 553 554 def get_title(self): 555 # Human-readable title which will be displayed in the 556 # right sidebar just above the filter options. 557 return u'decade born' 558 559 def get_query_parameter_name(self): 560 # This is the code name for the filter that will be used in 561 # the url query. Overriding this method is optional (by default, 562 # a slugified version of the title will automatically be used, 563 # that is, 'decade-born' in this example). 564 return u'decade' 565 566 def get_choices(self, request): 567 # Return a list of tuples. The first element in each tuple 568 # is the coded value for the option that will appear in the 569 # url query. The second element is the human-readable name 570 # for the option that will appear in the right sidebar. You 571 # may specify as many choices as you like, and you may even 572 # vary the list of choices depending on the HttpRequest 573 # object provided as argument to this method. 574 return ( 575 (u'80s', u'in the eighties'), 576 (u'other', u'other'), 577 ) 578 579 def get_query_set(self, changelist, queryset): 580 # First, retrieve the requested value (either '80s' or 'other'). 581 decade = self.get_value() 582 # Then decide how to filter the queryset based on that value. 583 if decade == u'80s': 584 return queryset.filter(birthday__year__gte=1980, 585 birthday__year__lte=1989) 586 if decade == u'other': 587 return queryset.filter(Q(year__lte=1979) | 588 Q(year__gte=1990) 589 # Always return the unchanged queryset by default 590 return queryset 591 592 class PersonAdmin(ModelAdmin): 593 list_filter = (DecadeBornListFilter,) 594 595 * a class inheriting from :mod:`django.contrib.admin.FieldListFilter`, 596 in case you would like to change the default behaviour of a filter 597 for a given field. For example, you may define a custom boolean 598 filter called ``CustomBooleanFieldListFiler``. You may then register 599 it using a tuple to bind it to a given field:: 600 601 class PersonAdmin(ModelAdmin): 602 list_filter = (('is_staff', CustomBooleanFieldListFiler),) 603 604 Note that this method is far more complex than simply using a field 605 name or a ``SimpleListFilter`` class, as there currently is no simple 606 way available to manipulate a ``FieldListFilter``. You may, however, 607 find some useful examples with the built-in filters defined in 608 :mod:`django.contrib.admin.filterspecs`. 609 610 Finally, the following example, taken from the ``django.contrib.auth.models.User`` 611 model, shows how both ``list_display`` and ``list_filter`` work:: 535 612 536 613 class UserAdmin(admin.ModelAdmin): 537 614 list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') … … subclass:: 543 620 544 621 (This example also has ``search_fields`` defined. See below.) 545 622 546 .. versionadded:: 1.3547 548 Fields in ``list_filter`` can also span relations using the ``__`` lookup::549 550 class UserAdminWithLookup(UserAdmin):551 list_filter = ('groups__name')552 553 623 .. attribute:: ModelAdmin.list_per_page 554 624 555 625 Set ``list_per_page`` to control how many items appear on each paginated -
tests/regressiontests/admin_filterspecs/tests.py
diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py index 8b9e734..7bbc9dd 100644
a b from django.contrib.auth.models import User 5 5 from django.contrib import admin 6 6 from django.contrib.admin.views.main import ChangeList 7 7 from django.utils.encoding import force_unicode 8 from django.contrib.admin.filterspecs import (SimpleListFilter, 9 BooleanFieldListFilter, FieldListFilter) 8 10 9 11 from models import Book, BoolTest 10 12 11 13 def select_by(dictlist, key, value): 12 14 return [x for x in dictlist if x[key] == value][0] 13 15 14 class FilterSpecsTests(TestCase): 16 17 18 class DecadeListFilter(SimpleListFilter): 19 20 def get_title(self): 21 return u'publication decade' 22 23 def get_choices(self, request): 24 return ( 25 (u'the 90s', u'the 1990\'s'), 26 (u'the 00s', u'the 2000\'s'), 27 (u'other', u'other decades'), 28 ) 29 30 def get_query_set(self, cl, qs): 31 decade = self.get_value() 32 if decade == u'the 90s': 33 return qs.filter(year__gte=1990, year__lte=1999) 34 if decade == u'the 00s': 35 return qs.filter(year__gte=2000, year__lte=2009) 36 return qs 37 38 39 class ListFiltersTests(TestCase): 15 40 16 41 def setUp(self): 17 42 # Users 18 43 self.alfred = User.objects.create_user('alfred', 'alfred@example.com') 19 44 self.bob = User.objects.create_user('bob', 'bob@example.com') 20 lisa = User.objects.create_user('lisa', 'lisa@example.com')45 self.lisa = User.objects.create_user('lisa', 'lisa@example.com') 21 46 22 47 #Books 48 self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred) 23 49 self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred) 24 50 self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob) 25 gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)26 gipsy_book.contributors = [self.bob,lisa]27 gipsy_book.save()51 self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002) 52 self.gipsy_book.contributors = [self.bob, self.lisa] 53 self.gipsy_book.save() 28 54 29 55 # BoolTests 30 56 self.trueTest = BoolTest.objects.create(completed=True) … … class FilterSpecsTests(TestCase): 38 64 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, 39 65 modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin) 40 66 41 def test_AllValuesFi lterSpec(self):67 def test_AllValuesFieldListFilter(self): 42 68 modeladmin = BookAdmin(Book, admin.site) 43 69 44 70 request = self.request_factory.get('/', {'year__isnull': 'True'}) … … class FilterSpecsTests(TestCase): 49 75 50 76 # Make sure the last choice is None and is selected 51 77 filterspec = changelist.get_filters(request)[0][0] 52 self.assertEqual(force_unicode(filterspec. title()), u'year')53 choices = list(filterspec. choices(changelist))78 self.assertEqual(force_unicode(filterspec.get_title()), u'year') 79 choices = list(filterspec._choices(changelist)) 54 80 self.assertEqual(choices[-1]['selected'], True) 55 81 self.assertEqual(choices[-1]['query_string'], '?year__isnull=True') 56 82 … … class FilterSpecsTests(TestCase): 59 85 60 86 # Make sure the correct choice is selected 61 87 filterspec = changelist.get_filters(request)[0][0] 62 self.assertEqual(force_unicode(filterspec. title()), u'year')63 choices = list(filterspec. choices(changelist))88 self.assertEqual(force_unicode(filterspec.get_title()), u'year') 89 choices = list(filterspec._choices(changelist)) 64 90 self.assertEqual(choices[2]['selected'], True) 65 91 self.assertEqual(choices[2]['query_string'], '?year=2002') 66 92 67 def test_RelatedFi lterSpec_ForeignKey(self):93 def test_RelatedFieldListFilter_ForeignKey(self): 68 94 modeladmin = BookAdmin(Book, admin.site) 69 95 70 96 request = self.request_factory.get('/', {'author__isnull': 'True'}) … … class FilterSpecsTests(TestCase): 77 103 78 104 # Make sure the last choice is None and is selected 79 105 filterspec = changelist.get_filters(request)[0][1] 80 self.assertEqual(force_unicode(filterspec. title()), u'author')81 choices = list(filterspec. choices(changelist))106 self.assertEqual(force_unicode(filterspec.get_title()), u'author') 107 choices = list(filterspec._choices(changelist)) 82 108 self.assertEqual(choices[-1]['selected'], True) 83 109 self.assertEqual(choices[-1]['query_string'], '?author__isnull=True') 84 110 … … class FilterSpecsTests(TestCase): 87 113 88 114 # Make sure the correct choice is selected 89 115 filterspec = changelist.get_filters(request)[0][1] 90 self.assertEqual(force_unicode(filterspec. title()), u'author')116 self.assertEqual(force_unicode(filterspec.get_title()), u'author') 91 117 # order of choices depends on User model, which has no order 92 choice = select_by(filterspec. choices(changelist), "display", "alfred")118 choice = select_by(filterspec._choices(changelist), "display", "alfred") 93 119 self.assertEqual(choice['selected'], True) 94 120 self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk) 95 121 96 def test_RelatedFi lterSpec_ManyToMany(self):122 def test_RelatedFieldListFilter_ManyToMany(self): 97 123 modeladmin = BookAdmin(Book, admin.site) 98 124 99 125 request = self.request_factory.get('/', {'contributors__isnull': 'True'}) … … class FilterSpecsTests(TestCase): 104 130 105 131 # Make sure the last choice is None and is selected 106 132 filterspec = changelist.get_filters(request)[0][2] 107 self.assertEqual(force_unicode(filterspec. title()), u'user')108 choices = list(filterspec. choices(changelist))133 self.assertEqual(force_unicode(filterspec.get_title()), u'user') 134 choices = list(filterspec._choices(changelist)) 109 135 self.assertEqual(choices[-1]['selected'], True) 110 136 self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True') 111 137 … … class FilterSpecsTests(TestCase): 114 140 115 141 # Make sure the correct choice is selected 116 142 filterspec = changelist.get_filters(request)[0][2] 117 self.assertEqual(force_unicode(filterspec. title()), u'user')118 choice = select_by(filterspec. choices(changelist), "display", "bob")143 self.assertEqual(force_unicode(filterspec.get_title()), u'user') 144 choice = select_by(filterspec._choices(changelist), "display", "bob") 119 145 self.assertEqual(choice['selected'], True) 120 146 self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk) 121 147 122 148 123 def test_RelatedFi lterSpec_reverse_relationships(self):149 def test_RelatedFieldListFilter_reverse_relationships(self): 124 150 modeladmin = CustomUserAdmin(User, admin.site) 125 151 126 152 # FK relationship ----- … … class FilterSpecsTests(TestCase): 132 158 133 159 # Make sure the last choice is None and is selected 134 160 filterspec = changelist.get_filters(request)[0][0] 135 self.assertEqual(force_unicode(filterspec. title()), u'book')136 choices = list(filterspec. choices(changelist))161 self.assertEqual(force_unicode(filterspec.get_title()), u'book') 162 choices = list(filterspec._choices(changelist)) 137 163 self.assertEqual(choices[-1]['selected'], True) 138 164 self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True') 139 165 … … class FilterSpecsTests(TestCase): 142 168 143 169 # Make sure the correct choice is selected 144 170 filterspec = changelist.get_filters(request)[0][0] 145 self.assertEqual(force_unicode(filterspec. title()), u'book')146 choice = select_by(filterspec. choices(changelist), "display", self.bio_book.title)171 self.assertEqual(force_unicode(filterspec.get_title()), u'book') 172 choice = select_by(filterspec._choices(changelist), "display", self.bio_book.title) 147 173 self.assertEqual(choice['selected'], True) 148 174 self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk) 149 175 … … class FilterSpecsTests(TestCase): 156 182 157 183 # Make sure the last choice is None and is selected 158 184 filterspec = changelist.get_filters(request)[0][1] 159 self.assertEqual(force_unicode(filterspec. title()), u'book')160 choices = list(filterspec. choices(changelist))185 self.assertEqual(force_unicode(filterspec.get_title()), u'book') 186 choices = list(filterspec._choices(changelist)) 161 187 self.assertEqual(choices[-1]['selected'], True) 162 188 self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True') 163 189 … … class FilterSpecsTests(TestCase): 166 192 167 193 # Make sure the correct choice is selected 168 194 filterspec = changelist.get_filters(request)[0][1] 169 self.assertEqual(force_unicode(filterspec. title()), u'book')170 choice = select_by(filterspec. choices(changelist), "display", self.django_book.title)195 self.assertEqual(force_unicode(filterspec.get_title()), u'book') 196 choice = select_by(filterspec._choices(changelist), "display", self.django_book.title) 171 197 self.assertEqual(choice['selected'], True) 172 198 self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk) 173 199 174 def test_BooleanFi lterSpec(self):200 def test_BooleanFieldListFilter(self): 175 201 modeladmin = BoolTestAdmin(BoolTest, admin.site) 176 202 self.verify_BooleanFieldListFilter(modeladmin) 203 204 def test_BooleanFieldListFilter_Tuple(self): 205 modeladmin = BoolTupleTestAdmin(BoolTest, admin.site) 206 self.verify_BooleanFieldListFilter(modeladmin) 207 208 def verify_BooleanFieldListFilter(self, modeladmin): 177 209 request = self.request_factory.get('/') 178 210 changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links, 179 211 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, … … class FilterSpecsTests(TestCase): 184 216 185 217 # Make sure the last choice is None and is selected 186 218 filterspec = changelist.get_filters(request)[0][0] 187 self.assertEqual(force_unicode(filterspec. title()), u'completed')188 choices = list(filterspec. choices(changelist))219 self.assertEqual(force_unicode(filterspec.get_title()), u'completed') 220 choices = list(filterspec._choices(changelist)) 189 221 self.assertEqual(choices[-1]['selected'], False) 190 222 self.assertEqual(choices[-1]['query_string'], '?completed__exact=0') 191 223 … … class FilterSpecsTests(TestCase): 194 226 195 227 # Make sure the correct choice is selected 196 228 filterspec = changelist.get_filters(request)[0][0] 197 self.assertEqual(force_unicode(filterspec. title()), u'completed')229 self.assertEqual(force_unicode(filterspec.get_title()), u'completed') 198 230 # order of choices depends on User model, which has no order 199 choice = select_by(filterspec. choices(changelist), "display", "Yes")231 choice = select_by(filterspec._choices(changelist), "display", "Yes") 200 232 self.assertEqual(choice['selected'], True) 201 233 self.assertEqual(choice['query_string'], '?completed__exact=1') 202 234 235 def test_SimpleListFilter(self): 236 modeladmin = DecadeFilterBookAdmin(Book, admin.site) 237 238 # Make sure that the first option is 'All' ------- 239 240 request = self.request_factory.get('/', {}) 241 changelist = self.get_changelist(request, Book, modeladmin) 242 243 # Make sure the correct queryset is returned 244 queryset = changelist.get_query_set() 245 self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id'))) 246 247 # Make sure the correct choice is selected 248 filterspec = changelist.get_filters(request)[0][1] 249 self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade') 250 choices = list(filterspec._choices(changelist)) 251 self.assertEqual(choices[0]['display'], u'All') 252 self.assertEqual(choices[0]['selected'], True) 253 self.assertEqual(choices[0]['query_string'], '?') 254 255 # Look for books in the 1990s -------- 256 257 request = self.request_factory.get('/', {'publication-decade': 'the 90s'}) 258 changelist = self.get_changelist(request, Book, modeladmin) 259 260 # Make sure the correct queryset is returned 261 queryset = changelist.get_query_set() 262 self.assertEqual(list(queryset), [self.bio_book]) 263 264 # Make sure the correct choice is selected 265 filterspec = changelist.get_filters(request)[0][1] 266 self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade') 267 choices = list(filterspec._choices(changelist)) 268 self.assertEqual(choices[1]['display'], u'the 1990\'s') 269 self.assertEqual(choices[1]['selected'], True) 270 self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s') 271 272 # Look for books in the 2000s -------- 273 274 request = self.request_factory.get('/', {'publication-decade': 'the 00s'}) 275 changelist = self.get_changelist(request, Book, modeladmin) 276 277 # Make sure the correct queryset is returned 278 queryset = changelist.get_query_set() 279 self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book]) 280 281 # Make sure the correct choice is selected 282 filterspec = changelist.get_filters(request)[0][1] 283 self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade') 284 choices = list(filterspec._choices(changelist)) 285 self.assertEqual(choices[2]['display'], u'the 2000\'s') 286 self.assertEqual(choices[2]['selected'], True) 287 self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s') 288 289 # Combine multiple filters -------- 290 291 request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk}) 292 changelist = self.get_changelist(request, Book, modeladmin) 293 294 # Make sure the correct queryset is returned 295 queryset = changelist.get_query_set() 296 self.assertEqual(list(queryset), [self.djangonaut_book]) 297 298 # Make sure the correct choices are selected 299 filterspec = changelist.get_filters(request)[0][1] 300 self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade') 301 choices = list(filterspec._choices(changelist)) 302 self.assertEqual(choices[2]['display'], u'the 2000\'s') 303 self.assertEqual(choices[2]['selected'], True) 304 self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) 305 306 filterspec = changelist.get_filters(request)[0][0] 307 self.assertEqual(force_unicode(filterspec.get_title()), u'author') 308 choice = select_by(filterspec._choices(changelist), "display", "alfred") 309 self.assertEqual(choice['selected'], True) 310 self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) 311 203 312 class CustomUserAdmin(UserAdmin): 204 313 list_filter = ('books_authored', 'books_contributed') 205 314 … … class BookAdmin(admin.ModelAdmin): 209 318 210 319 class BoolTestAdmin(admin.ModelAdmin): 211 320 list_filter = ('completed',) 321 322 class BoolTupleTestAdmin(admin.ModelAdmin): 323 list_filter = (('completed', BooleanFieldListFilter),) 324 325 class DecadeFilterBookAdmin(admin.ModelAdmin): 326 list_filter = ('author', DecadeListFilter) 327 order_by = '-id' -
tests/regressiontests/modeladmin/tests.py
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py index a20e579..6f31e7f 100644
a b from datetime import date 2 2 3 3 from django import forms 4 4 from django.conf import settings 5 from django.contrib.admin.options import ModelAdmin, TabularInline, \6 HORIZONTAL, VERTICAL 5 from django.contrib.admin.options import (ModelAdmin, TabularInline, 6 HORIZONTAL, VERTICAL) 7 7 from django.contrib.admin.sites import AdminSite 8 8 from django.contrib.admin.validation import validate 9 9 from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect 10 from django.contrib.admin.filterspecs import (SimpleListFilter, 11 BooleanFieldListFilter) 10 12 from django.core.exceptions import ImproperlyConfigured 11 13 from django.forms.models import BaseModelFormSet 12 14 from django.forms.widgets import Select 13 15 from django.test import TestCase 14 16 from django.utils import unittest 15 17 16 from models import Band, Concert, ValidationTestModel, \17 ValidationTestInlineModel 18 from models import (Band, Concert, ValidationTestModel, 19 ValidationTestInlineModel) 18 20 19 21 20 22 # None of the following tests really depend on the content of the request, … … class ValidationTests(unittest.TestCase): 850 852 ValidationTestModelAdmin, 851 853 ValidationTestModel, 852 854 ) 855 856 class RandomClass(object): 857 pass 858 859 class ValidationTestModelAdmin(ModelAdmin): 860 list_filter = (RandomClass,) 861 862 self.assertRaisesRegexp( 863 ImproperlyConfigured, 864 "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not of type ListFilter.", 865 validate, 866 ValidationTestModelAdmin, 867 ValidationTestModel, 868 ) 869 870 class ValidationTestModelAdmin(ModelAdmin): 871 list_filter = (('is_active', RandomClass),) 872 873 self.assertRaisesRegexp( 874 ImproperlyConfigured, 875 "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.", 876 validate, 877 ValidationTestModelAdmin, 878 ValidationTestModel, 879 ) 880 881 class AwesomeFilter(SimpleListFilter): 882 def get_title(self): 883 return 'awesomeness' 884 def get_choices(self, request): 885 return (('bit', 'A bit awesome'), ('very', 'Very awesome'), ) 886 def get_query_set(self, cl, qs): 887 return qs 888 889 class ValidationTestModelAdmin(ModelAdmin): 890 list_filter = (('is_active', AwesomeFilter),) 853 891 892 self.assertRaisesRegexp( 893 ImproperlyConfigured, 894 "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.", 895 validate, 896 ValidationTestModelAdmin, 897 ValidationTestModel, 898 ) 899 900 # Valid declarations below ----------- 901 854 902 class ValidationTestModelAdmin(ModelAdmin): 855 list_filter = ('is_active', )903 list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter)) 856 904 857 905 validate(ValidationTestModelAdmin, ValidationTestModel) 858 906