Ticket #5833: 5833.custom-filterspecs.4.diff
File 5833.custom-filterspecs.4.diff, 50.0 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..44845d7 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 3 4 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME 4 5 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL 5 6 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..e8853f7 100644
a b certain test -- e.g. being a DateField or ForeignKey. 7 7 """ 8 8 9 9 from django.db import models 10 from django.core.exceptions import ImproperlyConfigured 10 11 from django.utils.encoding import smart_unicode, iri_to_uri 11 12 from django.utils.translation import ugettext as _ 12 13 from django.utils.html import escape 13 14 from django.utils.safestring import mark_safe 14 from django.contrib.admin.util import get_model_from_relation, \ 15 reverse_field_path, get_limit_choices_to_from_path 15 from django.contrib.admin.util import (get_model_from_relation, 16 reverse_field_path, get_limit_choices_to_from_path) 17 from django.template.defaultfilters import slugify 16 18 import datetime 17 19 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 20 class ListFilterBase(object): 21 title = None # Human-readable title to appear in the right sidebar. 22 23 def __init__(self, request, params, model, model_admin): 24 if self.title is None: 25 raise ImproperlyConfigured("The list filter '%s' does specify " 26 "a 'title'." % self.__class__.__name__) 23 27 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 28 42 29 def has_output(self): 43 30 return True 44 31 45 def choices(self, cl): 46 raise NotImplementedError() 47 48 def title(self): 49 return self.field.verbose_name 32 def _choices(self, cl): 33 raise NotImplementedError 34 35 def get_query_set(self, queryset): 36 return queryset 37 38 def _consumed_params(self): 39 """ 40 Return a list of parameters to consume from the change list 41 querystring. 42 43 Override this for non-field based ListFilter subclasses in order 44 to consume custom GET parameters, as any GET parameters that are not 45 consumed and are not a field name raises an exception. 46 """ 47 return [] 50 48 51 49 def output(self, cl): 52 50 t = [] 53 51 if self.has_output(): 54 t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title ()))52 t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title)) 55 53 56 for choice in self. choices(cl):54 for choice in self._choices(cl): 57 55 t.append(u'<li%s><a href="%s">%s</a></li>\n' % \ 58 56 ((choice['selected'] and ' class="selected"' or ''), 59 57 iri_to_uri(choice['query_string']), 60 58 choice['display'])) 61 59 t.append('</ul>\n\n') 62 60 return mark_safe("".join(t)) 61 62 63 64 65 66 class ListFilter(ListFilterBase): 67 """ 68 API to make the creation of a custom non-field list filter as simple 69 and easy as possible. 70 """ 71 72 # Parameter that should be used in the query string for that filter. 73 # Defaults to the title, slugified 74 query_parameter_name = None 75 76 def __init__(self, request, params, model, model_admin): 77 super(ListFilter, self).__init__(request, params, model, model_admin) 78 if self.query_parameter_name is None: 79 self.query_parameter_name = slugify(self.title) 80 self.lookup_choices = self.get_choices(request) 81 82 def has_output(self): 83 return len(self.lookup_choices) > 0 84 85 def get_value(self): 86 """ 87 Returns the value given in the query string for this filter, 88 if any. Returns None otherwise. 89 """ 90 return self.params.get(self.query_parameter_name, None) 91 92 def get_choices(self, request): 93 """ 94 Must be overriden to return a list of tuples (value, verbose value) 95 """ 96 raise NotImplementedError 97 98 def _consumed_params(self): 99 return [self.query_parameter_name] 100 101 def _choices(self, cl): 102 yield {'selected': self.get_value() is None, 103 'query_string': cl.get_query_string({}, [self.query_parameter_name]), 104 'display': _('All')} 105 for k, v in self.lookup_choices: 106 yield {'selected': self.get_value() == k, 107 'query_string': cl.get_query_string( 108 {self.query_parameter_name: k}, 109 []), 110 'display': v} 63 111 64 class RelatedFilterSpec(FilterSpec):65 def __init__(self, f, request, params, model, model_admin,66 field_path=None):67 super(RelatedFilterSpec, self).__init__(68 f, request, params, model, model_admin, field_path=field_path)69 112 70 other_model = get_model_from_relation(f) 71 if isinstance(f, (models.ManyToManyField, 113 114 class FieldListFilter(ListFilterBase): 115 _field_list_filters = [] 116 _take_priority_index = 0 117 118 def __init__(self, field, request, params, model, model_admin, \ 119 field_path): 120 self.field = field 121 self.field_path = field_path 122 self.title = field_path 123 super(FieldListFilter, self).__init__(request, params, model, \ 124 model_admin) 125 126 127 @classmethod 128 def register(cls, test, list_filter_class, take_priority=False): 129 if take_priority: 130 # This is to allow overriding the default filters for certain types 131 # of fields with some custom filters. The first found in the list 132 # is used in priority. 133 cls._field_list_filters.insert(cls._take_priority_index, (test, list_filter_class)) 134 _take_priority_index += 1 135 else: 136 cls._field_list_filters.append((test, list_filter_class)) 137 138 @classmethod 139 def create(cls, field, request, params, model, model_admin, field_path): 140 for test, list_filter_class in cls._field_list_filters: 141 if test(field): 142 return list_filter_class(field, request, params, model, model_admin, 143 field_path=field_path) 144 145 146 class RelatedFieldListFilter(FieldListFilter): 147 def __init__(self, field, request, params, model, model_admin, 148 field_path): 149 super(RelatedFieldListFilter, self).__init__( 150 field, request, params, model, model_admin, field_path) 151 152 other_model = get_model_from_relation(field) 153 if isinstance(field, (models.ManyToManyField, 72 154 models.related.RelatedObject)): 73 155 # no direct field on this model, get name from other model 74 156 self.lookup_title = other_model._meta.verbose_name 75 157 else: 76 self.lookup_title = f .verbose_name # use field name158 self.lookup_title = field.verbose_name # use field name 77 159 rel_name = other_model._meta.pk.name 78 160 self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name) 79 161 self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path) 80 162 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 81 163 self.lookup_val_isnull = request.GET.get( 82 164 self.lookup_kwarg_isnull, None) 83 self.lookup_choices = f.get_choices(include_blank=False) 165 self.lookup_choices = field.get_choices(include_blank=False) 166 self.title = self.lookup_title 84 167 85 168 def has_output(self): 86 169 if isinstance(self.field, models.related.RelatedObject) \ … … class RelatedFilterSpec(FilterSpec): 91 174 extra = 0 92 175 return len(self.lookup_choices) + extra > 1 93 176 94 def title(self): 95 return self.lookup_title 96 97 def choices(self, cl): 177 def _choices(self, cl): 98 178 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 99 179 yield {'selected': self.lookup_val is None 100 180 and not self.lookup_val_isnull, … … class RelatedFilterSpec(FilterSpec): 117 197 [self.lookup_kwarg]), 118 198 'display': EMPTY_CHANGELIST_VALUE} 119 199 120 Fi lterSpec.register(lambda f: (200 FieldListFilter.register(lambda f: ( 121 201 hasattr(f, 'rel') and bool(f.rel) or 122 isinstance(f, models.related.RelatedObject)), RelatedFi lterSpec)202 isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) 123 203 124 class BooleanField FilterSpec(FilterSpec):204 class BooleanFieldListFilter(FieldListFilter): 125 205 def __init__(self, f, request, params, model, model_admin, 126 field_path =None):127 super(BooleanField FilterSpec, self).__init__(f, request, params, model,206 field_path): 207 super(BooleanFieldListFilter, self).__init__(f, request, params, model, 128 208 model_admin, 129 field_path =field_path)209 field_path) 130 210 self.lookup_kwarg = '%s__exact' % self.field_path 131 211 self.lookup_kwarg2 = '%s__isnull' % self.field_path 132 212 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 133 213 self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None) 134 214 135 def title(self): 136 return self.field.verbose_name 137 138 def choices(self, cl): 215 def _choices(self, cl): 139 216 for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')): 140 217 yield {'selected': self.lookup_val == v and not self.lookup_val2, 141 218 'query_string': cl.get_query_string( … … class BooleanFieldFilterSpec(FilterSpec): 149 226 [self.lookup_kwarg]), 150 227 'display': _('Unknown')} 151 228 152 Fi lterSpec.register(lambda f: isinstance(f, models.BooleanField)229 FieldListFilter.register(lambda f: isinstance(f, models.BooleanField) 153 230 or isinstance(f, models.NullBooleanField), 154 BooleanField FilterSpec)231 BooleanFieldListFilter) 155 232 156 class ChoicesFi lterSpec(FilterSpec):233 class ChoicesFieldListFilter(FieldListFilter): 157 234 def __init__(self, f, request, params, model, model_admin, 158 field_path =None):159 super(ChoicesFi lterSpec, self).__init__(f, request, params, model,235 field_path): 236 super(ChoicesFieldListFilter, self).__init__(f, request, params, model, 160 237 model_admin, 161 field_path =field_path)238 field_path) 162 239 self.lookup_kwarg = '%s__exact' % self.field_path 163 self.lookup_val = request.GET.get(self.lookup_kwarg , None)240 self.lookup_val = request.GET.get(self.lookup_kwarg) 164 241 165 def choices(self, cl):242 def _choices(self, cl): 166 243 yield {'selected': self.lookup_val is None, 167 244 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 168 245 'display': _('All')} … … class ChoicesFilterSpec(FilterSpec): 172 249 {self.lookup_kwarg: k}), 173 250 'display': v} 174 251 175 Fi lterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)252 FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter) 176 253 177 class DateField FilterSpec(FilterSpec):254 class DateFieldListFilter(FieldListFilter): 178 255 def __init__(self, f, request, params, model, model_admin, 179 field_path =None):180 super(DateField FilterSpec, self).__init__(f, request, params, model,256 field_path): 257 super(DateFieldListFilter, self).__init__(f, request, params, model, 181 258 model_admin, 182 field_path =field_path)259 field_path) 183 260 184 261 self.field_generic = '%s__' % self.field_path 185 262 … … class DateFieldFilterSpec(FilterSpec): 205 282 (_('This year'), {'%s__year' % self.field_path: str(today.year)}) 206 283 ) 207 284 208 def title(self): 209 return self.field.verbose_name 210 211 def choices(self, cl): 285 def _choices(self, cl): 212 286 for title, param_dict in self.links: 213 287 yield {'selected': self.date_params == param_dict, 214 288 'query_string': cl.get_query_string( … … class DateFieldFilterSpec(FilterSpec): 216 290 [self.field_generic]), 217 291 'display': title} 218 292 219 Fi lterSpec.register(lambda f: isinstance(f, models.DateField),220 DateField FilterSpec)293 FieldListFilter.register(lambda f: isinstance(f, models.DateField), 294 DateFieldListFilter) 221 295 222 296 223 297 # 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):298 # if a field is eligible to use the BooleanFieldListFilter, that'd be much 299 # more appropriate, and the AllValuesFieldListFilter won't get used for it. 300 class AllValuesFieldListFilter(FieldListFilter): 227 301 def __init__(self, f, request, params, model, model_admin, 228 field_path =None):229 super(AllValuesFi lterSpec, self).__init__(f, request, params, model,302 field_path): 303 super(AllValuesFieldListFilter, self).__init__(f, request, params, model, 230 304 model_admin, 231 field_path =field_path)305 field_path) 232 306 self.lookup_kwarg = self.field_path 233 307 self.lookup_kwarg_isnull = '%s__isnull' % self.field_path 234 308 self.lookup_val = request.GET.get(self.lookup_kwarg, None) … … class AllValuesFilterSpec(FilterSpec): 245 319 self.lookup_choices = \ 246 320 queryset.distinct().order_by(f.name).values_list(f.name, flat=True) 247 321 248 def title(self): 249 return self.field.verbose_name 250 251 def choices(self, cl): 322 def _choices(self, cl): 252 323 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 253 324 yield {'selected': self.lookup_val is None 254 325 and self.lookup_val_isnull is None, … … class AllValuesFilterSpec(FilterSpec): 276 347 [self.lookup_kwarg]), 277 348 'display': EMPTY_CHANGELIST_VALUE} 278 349 279 Fi lterSpec.register(lambda f: True, AllValuesFilterSpec)350 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..4919eda 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.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..cdc86d1 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. 65 if not issubclass(item, ListFilter): 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..1cfce43 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): 165 179 if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'): 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(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..cd4bfe3 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.ListFilter`, 547 where you need to provide a few attributes and override a few 548 methods:: 549 550 from django.contrib.admin import ListFilter 551 from django.db.models import Q 552 553 class DecadeBornListFilter(ListFilter): 554 # Human-readable title which will be displayed in the 555 # right sidebar just above the filter options. 556 title = u'decade born' 557 558 # This is the code name for the filter that will be used in 559 # the url query. Providing this attribute is optional. If it is 560 # not provided then a slugified version of the title will 561 # automatically be used instead (that is, 'decade-born' in this example). 562 query_parameter_name = u'decade' 563 564 def get_choices(self, request): 565 # Return a list of tuples. The first element in each tuple 566 # is the coded value for the option that will appear in the 567 # url query. The second element is the human-readable name 568 # for the option that will appear in the right sidebar. You 569 # may specify as many choices as you like, and you may even 570 # vary the list of choices depending on the HttpRequest 571 # object provided as argument to this method. 572 return ( 573 (u'80s', u'in the eighties'), 574 (u'other', u'other'), 575 ) 576 577 def get_query_set(self, queryset): 578 # First, retrieve the requested value (either '80s' or 'other'). 579 decade = self.get_value() 580 # Then decide how to filter the queryset based on that value. 581 if decade == u'80s': 582 return queryset.filter(birthday__year__gte=1980, 583 birthday__year__lte=1989) 584 if decade == u'other': 585 return queryset.filter(Q(year__lte=1979) | 586 Q(year__gte=1990) 587 # Always return the unchanged queryset by default 588 return queryset 589 590 class PersonAdmin(ModelAdmin): 591 list_filter = (DecadeBornListFilter,) 592 593 * a tuple, where the first element is a field name and the second 594 element is a class inheriting from 595 :mod:`django.contrib.admin.FieldListFilter`. Note that the 596 `FieldListFilter` API is currently considered internal and prone to 597 refactoring. 598 599 Finally, the following example, taken from the ``django.contrib.auth.models.User`` 600 model, shows how both ``list_display`` and ``list_filter`` work:: 535 601 536 602 class UserAdmin(admin.ModelAdmin): 537 603 list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') … … subclass:: 543 609 544 610 (This example also has ``search_fields`` defined. See below.) 545 611 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 612 .. attribute:: ModelAdmin.list_per_page 554 613 555 614 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..1d5e9a0 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 (ListFilter, 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 DecadeListFilterBase(ListFilter): 19 20 def get_choices(self, request): 21 return ( 22 (u'the 90s', u'the 1990\'s'), 23 (u'the 00s', u'the 2000\'s'), 24 (u'other', u'other decades'), 25 ) 26 27 def get_query_set(self, qs): 28 decade = self.get_value() 29 if decade == u'the 90s': 30 return qs.filter(year__gte=1990, year__lte=1999) 31 if decade == u'the 00s': 32 return qs.filter(year__gte=2000, year__lte=2009) 33 return qs 34 35 class DecadeListFilterWithTitle(DecadeListFilterBase): 36 title = u'publication decade' 37 38 class DecadeListFilterWithParamName(DecadeListFilterBase): 39 title = u'another publication decade' 40 query_parameter_name = u'blah' 41 42 class ListFiltersTests(TestCase): 15 43 16 44 def setUp(self): 17 45 # Users 18 46 self.alfred = User.objects.create_user('alfred', 'alfred@example.com') 19 47 self.bob = User.objects.create_user('bob', 'bob@example.com') 20 lisa = User.objects.create_user('lisa', 'lisa@example.com')48 self.lisa = User.objects.create_user('lisa', 'lisa@example.com') 21 49 22 50 #Books 51 self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred) 23 52 self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred) 24 53 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()54 self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002) 55 self.gipsy_book.contributors = [self.bob, self.lisa] 56 self.gipsy_book.save() 28 57 29 58 # BoolTests 30 59 self.trueTest = BoolTest.objects.create(completed=True) … … class FilterSpecsTests(TestCase): 38 67 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, 39 68 modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin) 40 69 41 def test_AllValuesFi lterSpec(self):70 def test_AllValuesFieldListFilter(self): 42 71 modeladmin = BookAdmin(Book, admin.site) 43 72 44 73 request = self.request_factory.get('/', {'year__isnull': 'True'}) … … class FilterSpecsTests(TestCase): 49 78 50 79 # Make sure the last choice is None and is selected 51 80 filterspec = changelist.get_filters(request)[0][0] 52 self.assertEqual(force_unicode(filterspec.title ()), u'year')53 choices = list(filterspec. choices(changelist))81 self.assertEqual(force_unicode(filterspec.title), u'year') 82 choices = list(filterspec._choices(changelist)) 54 83 self.assertEqual(choices[-1]['selected'], True) 55 84 self.assertEqual(choices[-1]['query_string'], '?year__isnull=True') 56 85 … … class FilterSpecsTests(TestCase): 59 88 60 89 # Make sure the correct choice is selected 61 90 filterspec = changelist.get_filters(request)[0][0] 62 self.assertEqual(force_unicode(filterspec.title ()), u'year')63 choices = list(filterspec. choices(changelist))91 self.assertEqual(force_unicode(filterspec.title), u'year') 92 choices = list(filterspec._choices(changelist)) 64 93 self.assertEqual(choices[2]['selected'], True) 65 94 self.assertEqual(choices[2]['query_string'], '?year=2002') 66 95 67 def test_RelatedFi lterSpec_ForeignKey(self):96 def test_RelatedFieldListFilter_ForeignKey(self): 68 97 modeladmin = BookAdmin(Book, admin.site) 69 98 70 99 request = self.request_factory.get('/', {'author__isnull': 'True'}) … … class FilterSpecsTests(TestCase): 77 106 78 107 # Make sure the last choice is None and is selected 79 108 filterspec = changelist.get_filters(request)[0][1] 80 self.assertEqual(force_unicode(filterspec.title ()), u'author')81 choices = list(filterspec. choices(changelist))109 self.assertEqual(force_unicode(filterspec.title), u'author') 110 choices = list(filterspec._choices(changelist)) 82 111 self.assertEqual(choices[-1]['selected'], True) 83 112 self.assertEqual(choices[-1]['query_string'], '?author__isnull=True') 84 113 … … class FilterSpecsTests(TestCase): 87 116 88 117 # Make sure the correct choice is selected 89 118 filterspec = changelist.get_filters(request)[0][1] 90 self.assertEqual(force_unicode(filterspec.title ()), u'author')119 self.assertEqual(force_unicode(filterspec.title), u'author') 91 120 # order of choices depends on User model, which has no order 92 choice = select_by(filterspec. choices(changelist), "display", "alfred")121 choice = select_by(filterspec._choices(changelist), "display", "alfred") 93 122 self.assertEqual(choice['selected'], True) 94 123 self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk) 95 124 96 def test_RelatedFi lterSpec_ManyToMany(self):125 def test_RelatedFieldListFilter_ManyToMany(self): 97 126 modeladmin = BookAdmin(Book, admin.site) 98 127 99 128 request = self.request_factory.get('/', {'contributors__isnull': 'True'}) … … class FilterSpecsTests(TestCase): 104 133 105 134 # Make sure the last choice is None and is selected 106 135 filterspec = changelist.get_filters(request)[0][2] 107 self.assertEqual(force_unicode(filterspec.title ()), u'user')108 choices = list(filterspec. choices(changelist))136 self.assertEqual(force_unicode(filterspec.title), u'user') 137 choices = list(filterspec._choices(changelist)) 109 138 self.assertEqual(choices[-1]['selected'], True) 110 139 self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True') 111 140 … … class FilterSpecsTests(TestCase): 114 143 115 144 # Make sure the correct choice is selected 116 145 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")146 self.assertEqual(force_unicode(filterspec.title), u'user') 147 choice = select_by(filterspec._choices(changelist), "display", "bob") 119 148 self.assertEqual(choice['selected'], True) 120 149 self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk) 121 150 122 151 123 def test_RelatedFi lterSpec_reverse_relationships(self):152 def test_RelatedFieldListFilter_reverse_relationships(self): 124 153 modeladmin = CustomUserAdmin(User, admin.site) 125 154 126 155 # FK relationship ----- … … class FilterSpecsTests(TestCase): 132 161 133 162 # Make sure the last choice is None and is selected 134 163 filterspec = changelist.get_filters(request)[0][0] 135 self.assertEqual(force_unicode(filterspec.title ()), u'book')136 choices = list(filterspec. choices(changelist))164 self.assertEqual(force_unicode(filterspec.title), u'book') 165 choices = list(filterspec._choices(changelist)) 137 166 self.assertEqual(choices[-1]['selected'], True) 138 167 self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True') 139 168 … … class FilterSpecsTests(TestCase): 142 171 143 172 # Make sure the correct choice is selected 144 173 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)174 self.assertEqual(force_unicode(filterspec.title), u'book') 175 choice = select_by(filterspec._choices(changelist), "display", self.bio_book.title) 147 176 self.assertEqual(choice['selected'], True) 148 177 self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk) 149 178 … … class FilterSpecsTests(TestCase): 156 185 157 186 # Make sure the last choice is None and is selected 158 187 filterspec = changelist.get_filters(request)[0][1] 159 self.assertEqual(force_unicode(filterspec.title ()), u'book')160 choices = list(filterspec. choices(changelist))188 self.assertEqual(force_unicode(filterspec.title), u'book') 189 choices = list(filterspec._choices(changelist)) 161 190 self.assertEqual(choices[-1]['selected'], True) 162 191 self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True') 163 192 … … class FilterSpecsTests(TestCase): 166 195 167 196 # Make sure the correct choice is selected 168 197 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)198 self.assertEqual(force_unicode(filterspec.title), u'book') 199 choice = select_by(filterspec._choices(changelist), "display", self.django_book.title) 171 200 self.assertEqual(choice['selected'], True) 172 201 self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk) 173 202 174 def test_BooleanFi lterSpec(self):203 def test_BooleanFieldListFilter(self): 175 204 modeladmin = BoolTestAdmin(BoolTest, admin.site) 176 205 self.verify_BooleanFieldListFilter(modeladmin) 206 207 def test_BooleanFieldListFilter_Tuple(self): 208 modeladmin = BoolTupleTestAdmin(BoolTest, admin.site) 209 self.verify_BooleanFieldListFilter(modeladmin) 210 211 def verify_BooleanFieldListFilter(self, modeladmin): 177 212 request = self.request_factory.get('/') 178 213 changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links, 179 214 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, … … class FilterSpecsTests(TestCase): 184 219 185 220 # Make sure the last choice is None and is selected 186 221 filterspec = changelist.get_filters(request)[0][0] 187 self.assertEqual(force_unicode(filterspec.title ()), u'completed')188 choices = list(filterspec. choices(changelist))222 self.assertEqual(force_unicode(filterspec.title), u'completed') 223 choices = list(filterspec._choices(changelist)) 189 224 self.assertEqual(choices[-1]['selected'], False) 190 225 self.assertEqual(choices[-1]['query_string'], '?completed__exact=0') 191 226 … … class FilterSpecsTests(TestCase): 194 229 195 230 # Make sure the correct choice is selected 196 231 filterspec = changelist.get_filters(request)[0][0] 197 self.assertEqual(force_unicode(filterspec.title ()), u'completed')232 self.assertEqual(force_unicode(filterspec.title), u'completed') 198 233 # order of choices depends on User model, which has no order 199 choice = select_by(filterspec. choices(changelist), "display", "Yes")234 choice = select_by(filterspec._choices(changelist), "display", "Yes") 200 235 self.assertEqual(choice['selected'], True) 201 236 self.assertEqual(choice['query_string'], '?completed__exact=1') 202 237 238 def test_ListFilter(self): 239 modeladmin = DecadeFilterBookAdmin(Book, admin.site) 240 241 # Make sure that the first option is 'All' --------------------------- 242 243 request = self.request_factory.get('/', {}) 244 changelist = self.get_changelist(request, Book, modeladmin) 245 246 # Make sure the correct queryset is returned 247 queryset = changelist.get_query_set() 248 self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id'))) 249 250 # Make sure the correct choice is selected 251 filterspec = changelist.get_filters(request)[0][1] 252 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 253 choices = list(filterspec._choices(changelist)) 254 self.assertEqual(choices[0]['display'], u'All') 255 self.assertEqual(choices[0]['selected'], True) 256 self.assertEqual(choices[0]['query_string'], '?') 257 258 # Make sure that one can override the query parameter name ----------- 259 260 request = self.request_factory.get('/', {'blah': 'the 90s'}) 261 changelist = self.get_changelist(request, Book, modeladmin) 262 263 # Make sure the correct choice is selected 264 filterspec = changelist.get_filters(request)[0][2] 265 self.assertEqual(force_unicode(filterspec.title), u'another publication decade') 266 267 # Look for books in the 1990s ---------------------------------------- 268 269 request = self.request_factory.get('/', {'publication-decade': 'the 90s'}) 270 changelist = self.get_changelist(request, Book, modeladmin) 271 272 # Make sure the correct queryset is returned 273 queryset = changelist.get_query_set() 274 self.assertEqual(list(queryset), [self.bio_book]) 275 276 # Make sure the correct choice is selected 277 filterspec = changelist.get_filters(request)[0][1] 278 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 279 choices = list(filterspec._choices(changelist)) 280 self.assertEqual(choices[1]['display'], u'the 1990\'s') 281 self.assertEqual(choices[1]['selected'], True) 282 self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s') 283 284 # Look for books in the 2000s ---------------------------------------- 285 286 request = self.request_factory.get('/', {'publication-decade': 'the 00s'}) 287 changelist = self.get_changelist(request, Book, modeladmin) 288 289 # Make sure the correct queryset is returned 290 queryset = changelist.get_query_set() 291 self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book]) 292 293 # Make sure the correct choice is selected 294 filterspec = changelist.get_filters(request)[0][1] 295 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 296 choices = list(filterspec._choices(changelist)) 297 self.assertEqual(choices[2]['display'], u'the 2000\'s') 298 self.assertEqual(choices[2]['selected'], True) 299 self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s') 300 301 # Combine multiple filters ------------------------------------------- 302 303 request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk}) 304 changelist = self.get_changelist(request, Book, modeladmin) 305 306 # Make sure the correct queryset is returned 307 queryset = changelist.get_query_set() 308 self.assertEqual(list(queryset), [self.djangonaut_book]) 309 310 # Make sure the correct choices are selected 311 filterspec = changelist.get_filters(request)[0][1] 312 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 313 choices = list(filterspec._choices(changelist)) 314 self.assertEqual(choices[2]['display'], u'the 2000\'s') 315 self.assertEqual(choices[2]['selected'], True) 316 self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) 317 318 filterspec = changelist.get_filters(request)[0][0] 319 self.assertEqual(force_unicode(filterspec.title), u'author') 320 choice = select_by(filterspec._choices(changelist), "display", "alfred") 321 self.assertEqual(choice['selected'], True) 322 self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) 323 203 324 class CustomUserAdmin(UserAdmin): 204 325 list_filter = ('books_authored', 'books_contributed') 205 326 … … class BookAdmin(admin.ModelAdmin): 209 330 210 331 class BoolTestAdmin(admin.ModelAdmin): 211 332 list_filter = ('completed',) 333 334 class BoolTupleTestAdmin(admin.ModelAdmin): 335 list_filter = (('completed', BooleanFieldListFilter),) 336 337 class DecadeFilterBookAdmin(admin.ModelAdmin): 338 list_filter = ('author', DecadeListFilterWithTitle, DecadeListFilterWithParamName) 339 order_by = '-id' -
tests/regressiontests/modeladmin/tests.py
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py index a20e579..c14f743 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 (ListFilter, 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(ListFilter): 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