Ticket #5833: 5833.custom-filterspecs.8.diff
File 5833.custom-filterspecs.8.diff, 75.9 KB (added by , 14 years ago) |
---|
-
django/contrib/admin/__init__.py
From 270cd4e0938db0023e0cadfc48ca4b4a0b068d41 Mon Sep 17 00:00:00 2001 From: Jannis Leidel <jannis@leidel.info> Date: Mon, 2 May 2011 18:58:29 +0200 Subject: [PATCH] Fixed #5833 -- Extended admin's FilterSpecs to be easier to override. --- django/contrib/admin/__init__.py | 4 + django/contrib/admin/filterspecs.py | 497 +++++++++++++-------- django/contrib/admin/templatetags/admin_list.py | 2 +- django/contrib/admin/validation.py | 47 ++- django/contrib/admin/views/main.py | 106 +++-- django/db/models/related.py | 2 +- docs/ref/contrib/admin/index.txt | 123 +++++- tests/regressiontests/admin_filterspecs/models.py | 16 +- tests/regressiontests/admin_filterspecs/tests.py | 348 ++++++++++++--- tests/regressiontests/modeladmin/tests.py | 69 +++- 10 files changed, 886 insertions(+), 328 deletions(-) diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index f8e634e..3e69c37 100644
a b from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME 4 4 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL 5 5 from django.contrib.admin.options import StackedInline, TabularInline 6 6 from django.contrib.admin.sites import AdminSite, site 7 from django.contrib.admin.filterspecs import (ListFilterBase, 8 SimpleListFilter, FieldListFilter, BooleanFieldListFilter, 9 RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter, 10 AllValuesFieldListFilter) 7 11 8 12 9 13 def autodiscover(): -
django/contrib/admin/filterspecs.py
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py index 965b32b..a871c70 100644
a b Filters are specified in models with the "list_filter" option. 5 5 Each filter subclass knows how to display a filter for a field that passes a 6 6 certain test -- e.g. being a DateField or ForeignKey. 7 7 """ 8 import datetime 8 9 9 10 from django.db import models 10 from django.utils.encoding import smart_unicode, iri_to_uri 11 from django.utils.translation import ugettext as _ 12 from django.utils.html import escape 13 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 16 import datetime 11 from django.core.exceptions import ImproperlyConfigured 12 from django.utils.encoding import smart_unicode 13 from django.utils.translation import ugettext_lazy as _ 14 15 from django.contrib.admin.util import (get_model_from_relation, 16 reverse_field_path, get_limit_choices_to_from_path) 17 18 class ListFilterBase(object): 19 title = None # Human-readable title to appear in the right sidebar. 17 20 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 21 def __init__(self, request, params, model, model_admin): 23 22 self.params = params 23 if self.title is None: 24 raise ImproperlyConfigured( 25 "The list filter '%s' does not specify " 26 "a 'title'." % self.__class__.__name__) 27 28 def has_output(self): 29 """ 30 Returns True if some choices would be output for the filter. 31 """ 32 raise NotImplementedError 33 34 def choices(self, cl): 35 """ 36 Returns choices ready to be output in the template. 37 """ 38 raise NotImplementedError 39 40 def queryset(self, request, queryset): 41 """ 42 Returns the filtered queryset. 43 """ 44 raise NotImplementedError 45 46 def used_params(self): 47 """ 48 Return a list of parameters to consume from the change list 49 querystring. 50 """ 51 raise NotImplementedError 52 53 54 55 class SimpleListFilter(ListFilterBase): 56 # The parameter that should be used in the query string for that filter. 57 # Defaults to the title, slugified. 58 parameter_name = None 59 60 def __init__(self, request, params, model, model_admin): 61 super(SimpleListFilter, self).__init__( 62 request, params, model, model_admin) 63 if self.parameter_name is None: 64 raise ImproperlyConfigured( 65 "The list filter '%s' does not specify " 66 "a 'parameter_name'." % self.__class__.__name__) 67 self.lookup_choices = self.lookups(request) 68 69 def has_output(self): 70 return len(self.lookup_choices) > 0 71 72 def value(self): 73 """ 74 Returns the value given in the query string for this filter, 75 if any. Returns None otherwise. 76 """ 77 return self.params.get(self.parameter_name, None) 78 79 def lookups(self, request): 80 """ 81 Must be overriden to return a list of tuples (value, verbose value) 82 """ 83 raise NotImplementedError 84 85 def used_params(self): 86 return [self.parameter_name] 87 88 def choices(self, cl): 89 yield { 90 'selected': self.value() is None, 91 'query_string': cl.get_query_string({}, [self.parameter_name]), 92 'display': _('All'), 93 } 94 for lookup, title in self.lookup_choices: 95 yield { 96 'selected': self.value() == lookup, 97 'query_string': cl.get_query_string({ 98 self.parameter_name: lookup, 99 }, []), 100 'display': title, 101 } 102 103 104 class FieldListFilter(ListFilterBase): 105 _field_list_filters = [] 106 _take_priority_index = 0 107 108 def __init__(self, field, request, params, model, model_admin, field_path): 109 self.field = field 24 110 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) 111 self.title = field_path 112 super(FieldListFilter, self).__init__(request, params, model, model_admin) 41 113 42 114 def has_output(self): 43 115 return True 44 116 45 def choices(self, cl): 46 raise NotImplementedError() 47 48 def title(self): 49 return self.field.verbose_name 50 51 def output(self, cl): 52 t = [] 53 if self.has_output(): 54 t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title())) 55 56 for choice in self.choices(cl): 57 t.append(u'<li%s><a href="%s">%s</a></li>\n' % \ 58 ((choice['selected'] and ' class="selected"' or ''), 59 iri_to_uri(choice['query_string']), 60 choice['display'])) 61 t.append('</ul>\n\n') 62 return mark_safe("".join(t)) 63 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 70 other_model = get_model_from_relation(f) 71 if isinstance(f, (models.ManyToManyField, 117 def queryset(self, request, queryset): 118 for p in self.used_params(): 119 if p in self.params: 120 return queryset.filter(**{p: self.params[p]}) 121 122 @classmethod 123 def register(cls, test, list_filter_class, take_priority=False): 124 if take_priority: 125 # This is to allow overriding the default filters for certain types 126 # of fields with some custom filters. The first found in the list 127 # is used in priority. 128 cls._field_list_filters.insert( 129 cls._take_priority_index, (test, list_filter_class)) 130 cls._take_priority_index += 1 131 else: 132 cls._field_list_filters.append((test, list_filter_class)) 133 134 @classmethod 135 def create(cls, field, request, params, model, model_admin, field_path): 136 for test, list_filter_class in cls._field_list_filters: 137 if not test(field): 138 continue 139 return list_filter_class(field, request, params, 140 model, model_admin, field_path=field_path) 141 142 143 class RelatedFieldListFilter(FieldListFilter): 144 def __init__(self, field, request, params, model, model_admin, field_path): 145 super(RelatedFieldListFilter, self).__init__( 146 field, request, params, model, model_admin, field_path) 147 148 other_model = get_model_from_relation(field) 149 if isinstance(field, (models.ManyToManyField, 72 150 models.related.RelatedObject)): 73 151 # no direct field on this model, get name from other model 74 152 self.lookup_title = other_model._meta.verbose_name 75 153 else: 76 self.lookup_title = f .verbose_name # use field name154 self.lookup_title = field.verbose_name # use field name 77 155 rel_name = other_model._meta.pk.name 78 156 self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name) 79 157 self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path) 80 158 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 81 159 self.lookup_val_isnull = request.GET.get( 82 160 self.lookup_kwarg_isnull, None) 83 self.lookup_choices = f.get_choices(include_blank=False) 161 self.lookup_choices = field.get_choices(include_blank=False) 162 self.title = self.lookup_title 84 163 85 164 def has_output(self): 86 if isinstance(self.field, models.related.RelatedObject) \87 and self.field.field.null or hasattr(self.field, 'rel') \88 and self.field.null:165 if (isinstance(self.field, models.related.RelatedObject) 166 and self.field.field.null or hasattr(self.field, 'rel') 167 and self.field.null): 89 168 extra = 1 90 169 else: 91 170 extra = 0 92 171 return len(self.lookup_choices) + extra > 1 93 172 94 def title(self):95 return self.lookup_title173 def used_params(self): 174 return [self.lookup_kwarg, self.lookup_kwarg_isnull] 96 175 97 176 def choices(self, cl): 98 177 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 99 yield { 'selected': self.lookup_val is None100 101 'query_string': cl.get_query_string(102 {},103 [self.lookup_kwarg, self.lookup_kwarg_isnull]),104 'display': _('All')}178 yield { 179 'selected': self.lookup_val is None and not self.lookup_val_isnull, 180 'query_string': cl.get_query_string({}, 181 [self.lookup_kwarg, self.lookup_kwarg_isnull]), 182 'display': _('All'), 183 } 105 184 for pk_val, val in self.lookup_choices: 106 yield {'selected': self.lookup_val == smart_unicode(pk_val), 107 'query_string': cl.get_query_string( 108 {self.lookup_kwarg: pk_val}, 109 [self.lookup_kwarg_isnull]), 110 'display': val} 111 if isinstance(self.field, models.related.RelatedObject) \ 112 and self.field.field.null or hasattr(self.field, 'rel') \ 113 and self.field.null: 114 yield {'selected': bool(self.lookup_val_isnull), 115 'query_string': cl.get_query_string( 116 {self.lookup_kwarg_isnull: 'True'}, 117 [self.lookup_kwarg]), 118 'display': EMPTY_CHANGELIST_VALUE} 119 120 FilterSpec.register(lambda f: ( 185 yield { 186 'selected': self.lookup_val == smart_unicode(pk_val), 187 'query_string': cl.get_query_string({ 188 self.lookup_kwarg: pk_val, 189 }, [self.lookup_kwarg_isnull]), 190 'display': val, 191 } 192 if (isinstance(self.field, models.related.RelatedObject) 193 and self.field.field.null or hasattr(self.field, 'rel') 194 and self.field.null): 195 yield { 196 'selected': bool(self.lookup_val_isnull), 197 'query_string': cl.get_query_string({ 198 self.lookup_kwarg_isnull: 'True', 199 }, [self.lookup_kwarg]), 200 'display': EMPTY_CHANGELIST_VALUE, 201 } 202 203 FieldListFilter.register(lambda f: ( 121 204 hasattr(f, 'rel') and bool(f.rel) or 122 isinstance(f, models.related.RelatedObject)), RelatedFilterSpec) 123 124 class BooleanFieldFilterSpec(FilterSpec): 125 def __init__(self, f, request, params, model, model_admin, 126 field_path=None): 127 super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, 128 model_admin, 129 field_path=field_path) 205 isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) 206 207 class BooleanFieldListFilter(FieldListFilter): 208 def __init__(self, field, request, params, model, model_admin, field_path): 209 super(BooleanFieldListFilter, self).__init__(field, 210 request, params, model, model_admin, field_path) 130 211 self.lookup_kwarg = '%s__exact' % self.field_path 131 212 self.lookup_kwarg2 = '%s__isnull' % self.field_path 132 213 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 133 214 self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None) 134 215 135 def title(self):136 return self.field.verbose_name216 def used_params(self): 217 return [self.lookup_kwarg, self.lookup_kwarg2] 137 218 138 219 def choices(self, cl): 139 for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')): 140 yield {'selected': self.lookup_val == v and not self.lookup_val2, 141 'query_string': cl.get_query_string( 142 {self.lookup_kwarg: v}, 143 [self.lookup_kwarg2]), 144 'display': k} 220 for lookup, title in ( 221 (None, _('All')), 222 ('1', _('Yes')), 223 ('0', _('No'))): 224 yield { 225 'selected': self.lookup_val == lookup and not self.lookup_val2, 226 'query_string': cl.get_query_string({ 227 self.lookup_kwarg: lookup, 228 }, [self.lookup_kwarg2]), 229 'display': title, 230 } 145 231 if isinstance(self.field, models.NullBooleanField): 146 yield { 'selected': self.lookup_val2 == 'True',147 'query_string': cl.get_query_string(148 {self.lookup_kwarg2: 'True'},149 [self.lookup_kwarg]),150 'display': _('Unknown')}151 152 FilterSpec.register(lambda f: isinstance(f, models.BooleanField) 153 or isinstance(f, models.NullBooleanField), 154 BooleanFieldFilterSpec) 155 156 class ChoicesFilterSpec(FilterSpec): 157 def __init__(self, f, request, params, model, model_admin, 158 field_path=None):159 super(ChoicesFilterSpec, self).__init__(f, request, params, model,160 model_admin,161 field_path=field_path)232 yield { 233 'selected': self.lookup_val2 == 'True', 234 'query_string': cl.get_query_string({ 235 self.lookup_kwarg2: 'True', 236 }, [self.lookup_kwarg]), 237 'display': _('Unknown'), 238 } 239 240 FieldListFilter.register(lambda f: isinstance(f, 241 (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter) 242 243 244 class ChoicesFieldListFilter(FieldListFilter): 245 def __init__(self, field, request, params, model, model_admin, field_path): 246 super(ChoicesFieldListFilter, self).__init__( 247 field, request, params, model, model_admin, field_path) 162 248 self.lookup_kwarg = '%s__exact' % self.field_path 163 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 249 self.lookup_val = request.GET.get(self.lookup_kwarg) 250 251 def used_params(self): 252 return [self.lookup_kwarg] 164 253 165 254 def choices(self, cl): 166 yield {'selected': self.lookup_val is None, 167 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 168 'display': _('All')} 169 for k, v in self.field.flatchoices: 170 yield {'selected': smart_unicode(k) == self.lookup_val, 171 'query_string': cl.get_query_string( 172 {self.lookup_kwarg: k}), 173 'display': v} 174 175 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec) 176 177 class DateFieldFilterSpec(FilterSpec): 178 def __init__(self, f, request, params, model, model_admin, 179 field_path=None): 180 super(DateFieldFilterSpec, self).__init__(f, request, params, model, 181 model_admin, 182 field_path=field_path) 255 yield { 256 'selected': self.lookup_val is None, 257 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 258 'display': _('All') 259 } 260 for lookup, title in self.field.flatchoices: 261 yield { 262 'selected': smart_unicode(lookup) == self.lookup_val, 263 'query_string': cl.get_query_string({self.lookup_kwarg: lookup}), 264 'display': title, 265 } 266 267 FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter) 268 269 270 class DateFieldListFilter(FieldListFilter): 271 def __init__(self, field, request, params, model, model_admin, field_path): 272 super(DateFieldListFilter, self).__init__( 273 field, request, params, model, model_admin, field_path) 183 274 184 275 self.field_generic = '%s__' % self.field_path 185 186 276 self.date_params = dict([(k, v) for k, v in params.items() 187 277 if k.startswith(self.field_generic)]) 188 278 189 279 today = datetime.date.today() 190 280 one_week_ago = today - datetime.timedelta(days=7) 191 today_str = isinstance(self.field, models.DateTimeField) \ 192 and today.strftime('%Y-%m-%d 23:59:59') \ 193 or today.strftime('%Y-%m-%d') 281 today_str = (isinstance(self.field, models.DateTimeField) 282 and today.strftime('%Y-%m-%d 23:59:59') 283 or today.strftime('%Y-%m-%d')) 284 285 self.lookup_kwarg_year = '%s__year' % self.field_path 286 self.lookup_kwarg_month = '%s__month' % self.field_path 287 self.lookup_kwarg_day = '%s__day' % self.field_path 288 self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path 289 self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path 194 290 195 291 self.links = ( 196 292 (_('Any date'), {}), 197 (_('Today'), {'%s__year' % self.field_path: str(today.year), 198 '%s__month' % self.field_path: str(today.month), 199 '%s__day' % self.field_path: str(today.day)}), 200 (_('Past 7 days'), {'%s__gte' % self.field_path: 201 one_week_ago.strftime('%Y-%m-%d'), 202 '%s__lte' % self.field_path: today_str}), 203 (_('This month'), {'%s__year' % self.field_path: str(today.year), 204 '%s__month' % self.field_path: str(today.month)}), 205 (_('This year'), {'%s__year' % self.field_path: str(today.year)}) 293 (_('Today'), { 294 self.lookup_kwarg_year: str(today.year), 295 self.lookup_kwarg_month: str(today.month), 296 self.lookup_kwarg_day: str(today.day), 297 }), 298 (_('Past 7 days'), { 299 self.lookup_kwarg_past_7_days_gte: one_week_ago.strftime('%Y-%m-%d'), 300 self.lookup_kwarg_past_7_days_lte: today_str, 301 }), 302 (_('This month'), { 303 self.lookup_kwarg_year: str(today.year), 304 self.lookup_kwarg_month: str(today.month), 305 }), 306 (_('This year'), { 307 self.lookup_kwarg_year: str(today.year), 308 }), 206 309 ) 207 310 208 def title(self): 209 return self.field.verbose_name 311 def used_params(self): 312 return [ 313 self.lookup_kwarg_year, self.lookup_kwarg_month, self.lookup_kwarg_day, 314 self.lookup_kwarg_past_7_days_gte, self.lookup_kwarg_past_7_days_lte 315 ] 316 317 def queryset(self, request, queryset): 318 """ 319 Override the default behaviour since there can be multiple query 320 string parameters used for the same date filter (e.g. year + month). 321 """ 322 query_dict = {} 323 for p in self.used_params(): 324 if p in self.params: 325 query_dict[p] = self.params[p] 326 if len(query_dict): 327 return queryset.filter(**query_dict) 210 328 211 329 def choices(self, cl): 212 330 for title, param_dict in self.links: 213 yield {'selected': self.date_params == param_dict, 214 'query_string': cl.get_query_string( 215 param_dict, 216 [self.field_generic]), 217 'display': title} 331 yield { 332 'selected': self.date_params == param_dict, 333 'query_string': cl.get_query_string( 334 param_dict, [self.field_generic]), 335 'display': title, 336 } 218 337 219 Fi lterSpec.register(lambda f: isinstance(f, models.DateField),220 DateFieldFilterSpec)338 FieldListFilter.register( 339 lambda f: isinstance(f, models.DateField), DateFieldListFilter) 221 340 222 341 223 342 # This should be registered last, because it's a last resort. For example, 224 # if a field is eligible to use the BooleanFieldFilterSpec, that'd be much 225 # more appropriate, and the AllValuesFilterSpec won't get used for it. 226 class AllValuesFilterSpec(FilterSpec): 227 def __init__(self, f, request, params, model, model_admin, 228 field_path=None): 229 super(AllValuesFilterSpec, self).__init__(f, request, params, model, 230 model_admin, 231 field_path=field_path) 343 # if a field is eligible to use the BooleanFieldListFilter, that'd be much 344 # more appropriate, and the AllValuesFieldListFilter won't get used for it. 345 class AllValuesFieldListFilter(FieldListFilter): 346 def __init__(self, field, request, params, model, model_admin, field_path): 347 super(AllValuesFieldListFilter, self).__init__( 348 field, request, params, model, model_admin, field_path) 232 349 self.lookup_kwarg = self.field_path 233 350 self.lookup_kwarg_isnull = '%s__isnull' % self.field_path 234 351 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 235 self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, 236 None) 352 self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, None) 237 353 parent_model, reverse_path = reverse_field_path(model, self.field_path) 238 354 queryset = parent_model._default_manager.all() 239 355 # optional feature: limit choices base on existing relationships … … class AllValuesFilterSpec(FilterSpec): 242 358 limit_choices_to = get_limit_choices_to_from_path(model, field_path) 243 359 queryset = queryset.filter(limit_choices_to) 244 360 245 self.lookup_choices = \246 queryset.distinct().order_by(f.name).values_list(f.name, flat=True)361 self.lookup_choices = queryset.distinct( 362 ).order_by(field.name).values_list(field.name, flat=True) 247 363 248 def title(self):249 return self.field.verbose_name364 def used_params(self): 365 return [self.lookup_kwarg, self.lookup_kwarg_isnull] 250 366 251 367 def choices(self, cl): 252 368 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 253 yield {'selected': self.lookup_val is None 254 and self.lookup_val_isnull is None, 255 'query_string': cl.get_query_string( 256 {}, 257 [self.lookup_kwarg, self.lookup_kwarg_isnull]), 258 'display': _('All')} 369 yield { 370 'selected': (self.lookup_val is None 371 and self.lookup_val_isnull is None), 372 'query_string': cl.get_query_string({}, 373 [self.lookup_kwarg, self.lookup_kwarg_isnull]), 374 'display': _('All'), 375 } 259 376 include_none = False 260 261 377 for val in self.lookup_choices: 262 378 if val is None: 263 379 include_none = True 264 380 continue 265 381 val = smart_unicode(val) 266 267 yield {'selected': self.lookup_val == val, 268 'query_string': cl.get_query_string( 269 {self.lookup_kwarg: val}, 270 [self.lookup_kwarg_isnull]), 271 'display': val} 382 yield { 383 'selected': self.lookup_val == val, 384 'query_string': cl.get_query_string({ 385 self.lookup_kwarg: val, 386 }, [self.lookup_kwarg_isnull]), 387 'display': val, 388 } 272 389 if include_none: 273 yield {'selected': bool(self.lookup_val_isnull), 274 'query_string': cl.get_query_string( 275 {self.lookup_kwarg_isnull: 'True'}, 276 [self.lookup_kwarg]), 277 'display': EMPTY_CHANGELIST_VALUE} 278 279 FilterSpec.register(lambda f: True, AllValuesFilterSpec) 390 yield { 391 'selected': bool(self.lookup_val_isnull), 392 'query_string': cl.get_query_string({ 393 self.lookup_kwarg_isnull: 'True', 394 }, [self.lookup_kwarg]), 395 'display': EMPTY_CHANGELIST_VALUE, 396 } 397 398 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 5e199ce..b72c0be 100644
a b def search_form(cl): 319 319 320 320 @register.inclusion_tag('admin/filter.html') 321 321 def admin_list_filter(cl, spec): 322 return {'title': spec.title (), 'choices' : list(spec.choices(cl))}322 return {'title': spec.title, 'choices' : list(spec.choices(cl))} 323 323 324 324 @register.inclusion_tag('admin/actions.html', takes_context=True) 325 325 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..f989cf3 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 ListFilterBase, 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 options for 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 ListFilterBase... 65 if not issubclass(item, ListFilterBase): 66 raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" 67 " which is not a descendant of ListFilterBase." 68 % (cls.__name__, idx, item.__name__)) 69 # ... but not a FieldListFilter. 70 if issubclass(item, FieldListFilter): 71 raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" 72 " which is of type FieldListFilter but is not" 73 " associated with a field name." 74 % (cls.__name__, idx, item.__name__)) 75 else: 76 try: 77 # Check for option #2 (tuple) 78 field, list_filter_class = item 79 except (TypeError, ValueError): 80 # item is option #1 81 field = item 82 else: 83 # item is option #2 84 if not issubclass(list_filter_class, FieldListFilter): 85 raise ImproperlyConfigured("'%s.list_filter[%d][1]'" 86 " is '%s' which is not of type FieldListFilter." 87 % (cls.__name__, idx, list_filter_class.__name__)) 88 # Validate the field string 89 try: 90 get_fields_from_path(model, field) 91 except (NotRelationField, FieldDoesNotExist): 92 raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" 93 " which does not refer to a Field." 94 % (cls.__name__, idx, field)) 66 95 67 96 # list_per_page = 100 68 97 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 0cfc43d..4413466 100644
a b 1 from django.contrib.admin.filterspecs import FilterSpec 2 from django.contrib.admin.options import IncorrectLookupParameters 3 from django.contrib.admin.util import quote, get_fields_from_path 1 import operator 2 4 3 from django.core.exceptions import SuspiciousOperation 5 4 from django.core.paginator import InvalidPage 6 5 from django.db import models 7 6 from django.utils.encoding import force_unicode, smart_str 8 7 from django.utils.translation import ugettext, ugettext_lazy 9 8 from django.utils.http import urlencode 10 import operator 9 10 from django.contrib.admin.filterspecs import FieldListFilter 11 from django.contrib.admin.options import IncorrectLookupParameters 12 from django.contrib.admin.util import quote, get_fields_from_path 11 13 12 14 # The system will display a "Show all" link on the change list only if the 13 15 # total result count is less than or equal to this setting. … … TO_FIELD_VAR = 't' 23 25 IS_POPUP_VAR = 'pop' 24 26 ERROR_FLAG = 'e' 25 27 28 IGNORED_PARAMS = ( 29 ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR) 30 26 31 # Text to display within change-list table cells if the value is blank. 27 32 EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)') 28 33 … … def field_needs_distinct(field): 36 41 37 42 38 43 class ChangeList(object): 39 def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin): 44 def __init__(self, request, model, list_display, list_display_links, 45 list_filter, date_hierarchy, search_fields, list_select_related, 46 list_per_page, list_editable, model_admin): 47 self.request = request 40 48 self.model = model 41 49 self.opts = model._meta 42 50 self.lookup_opts = self.opts … … class ChangeList(object): 72 80 self.query = request.GET.get(SEARCH_VAR, '') 73 81 self.query_set = self.get_query_set() 74 82 self.get_results(request) 75 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)) 76 self.filter_specs, self.has_filters = self.get_filters(request) 83 if self.is_popup: 84 title = ugettext('Select %s') 85 else: 86 title = ugettext('Select %s to change') 87 self.title = title % force_unicode(self.opts.verbose_name) 77 88 self.pk_attname = self.lookup_opts.pk.attname 78 89 79 def get_filters(self, request ):90 def get_filters(self, request, use_distinct=False): 80 91 filter_specs = [] 92 cleaned_params, use_distinct = self.get_lookup_params(use_distinct) 81 93 if self.list_filter: 82 for filter_name in self.list_filter: 83 field = get_fields_from_path(self.model, filter_name)[-1] 84 spec = FilterSpec.create(field, request, self.params, 85 self.model, self.model_admin, 86 field_path=filter_name) 94 for list_filer in self.list_filter: 95 if callable(list_filer): 96 # This is simply a custom list filter class. 97 spec = list_filer(request, cleaned_params, 98 self.model, self.model_admin) 99 else: 100 field_path = None 101 try: 102 # This is custom FieldListFilter class for a given field. 103 field, field_list_filter_class = list_filer 104 except (TypeError, ValueError): 105 # This is simply a field name, so use the default 106 # FieldListFilter class that has been registered for 107 # the type of the given field. 108 field, field_list_filter_class = list_filer, FieldListFilter.create 109 if not isinstance(field, models.Field): 110 field_path = field 111 field = get_fields_from_path(self.model, field_path)[-1] 112 spec = field_list_filter_class(field, request, cleaned_params, 113 self.model, self.model_admin, field_path=field_path) 87 114 if spec and spec.has_output(): 88 115 filter_specs.append(spec) 89 116 return filter_specs, bool(filter_specs) … … class ChangeList(object): 175 202 order_type = params[ORDER_TYPE_VAR] 176 203 return order_field, order_type 177 204 178 def get_query_set(self): 179 use_distinct = False 180 181 qs = self.root_query_set 205 def get_lookup_params(self, use_distinct=False): 182 206 lookup_params = self.params.copy() # a dictionary of the query string 183 for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR): 184 if i in lookup_params: 185 del lookup_params[i] 207 208 for ignored in IGNORED_PARAMS: 209 if ignored in lookup_params: 210 del lookup_params[ignored] 211 186 212 for key, value in lookup_params.items(): 187 213 if not isinstance(key, str): 188 214 # 'key' will be used as a keyword argument later, so Python … … class ChangeList(object): 195 221 # instance 196 222 field_name = key.split('__', 1)[0] 197 223 try: 198 f = self.lookup_opts.get_field_by_name(field_name)[0] 224 field = self.lookup_opts.get_field_by_name(field_name)[0] 225 use_distinct = field_needs_distinct(field) 199 226 except models.FieldDoesNotExist: 200 raise IncorrectLookupParameters201 use_distinct = field_needs_distinct(f)227 # It might be a custom NonFieldFilter 228 pass 202 229 203 230 # if key ends with __in, split parameter into separate values 204 231 if key.endswith('__in'): … … class ChangeList(object): 214 241 lookup_params[key] = value 215 242 216 243 if not self.model_admin.lookup_allowed(key, value): 217 raise SuspiciousOperation( 218 "Filtering by %s not allowed" % key 219 ) 244 raise SuspiciousOperation("Filtering by %s not allowed" % key) 245 246 return lookup_params, use_distinct 247 248 def get_query_set(self): 249 lookup_params, use_distinct = self.get_lookup_params(use_distinct=False) 250 self.filter_specs, self.has_filters = self.get_filters(self.request, use_distinct) 251 252 # Let every list filter modify the qs and params to its liking 253 qs = self.root_query_set 254 for filter_spec in self.filter_specs: 255 new_qs = filter_spec.queryset(self.request, qs) 256 if new_qs is not None: 257 qs = new_qs 258 for param in filter_spec.used_params(): 259 try: 260 del lookup_params[param] 261 except KeyError: 262 pass 220 263 221 # Apply lookup parameters from the query string. 264 # Apply the remaining lookup parameters from the query string (i.e. 265 # those that haven't already been processed by the filters). 222 266 try: 223 267 qs = qs.filter(**lookup_params) 224 268 # Naked except! Because we don't have any other way of validating "params". … … class ChangeList(object): 226 270 # values are not in the correct type, so we might get FieldError, ValueError, 227 271 # ValicationError, or ? from a custom field that raises yet something else 228 272 # when handed impossible data. 229 except :230 raise IncorrectLookupParameters 273 except Exception, e: 274 raise IncorrectLookupParameters(e) 231 275 232 276 # Use select_related() if one of the list_display options is a field 233 277 # with a relationship and the provided queryset doesn't already have … … class ChangeList(object): 238 282 else: 239 283 for field_name in self.list_display: 240 284 try: 241 f = self.lookup_opts.get_field(field_name)285 field = self.lookup_opts.get_field(field_name) 242 286 except models.FieldDoesNotExist: 243 287 pass 244 288 else: 245 if isinstance(f .rel, models.ManyToOneRel):289 if isinstance(field.rel, models.ManyToOneRel): 246 290 qs = qs.select_related() 247 291 break 248 292 -
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 633c53f..31adf46 100644
a b 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``. 532 533 This example, taken from the ``django.contrib.auth.models.User`` model, 534 shows how both ``list_display`` and ``list_filter`` work:: 528 .. versionchanged:: 1.4 535 529 536 class UserAdmin(admin.ModelAdmin): 537 list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') 538 list_filter = ('is_staff', 'is_superuser') 539 540 The above code results in an admin change list page that looks like this: 530 Set ``list_filter`` to activate filters in the right sidebar of the change 531 list page of the admin, as illustrated in the following screenshot: 541 532 542 533 .. image:: _images/users_changelist.png 543 534 544 (This example also has ``search_fields`` defined. See below.) 545 546 .. versionadded:: 1.3 547 548 Fields in ``list_filter`` can also span relations using the ``__`` lookup:: 549 550 class UserAdminWithLookup(UserAdmin): 551 list_filter = ('groups__name') 535 ``list_filter`` should be a list of elements, where each element should be 536 of one of the following types: 537 538 * a field name, where the specified field should be either a 539 ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``, 540 ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``, for example:: 541 542 class PersonAdmin(ModelAdmin): 543 list_filter = ('is_staff', 'company') 544 545 .. versionadded:: 1.3 546 547 Field names in ``list_filter`` can also span relations 548 using the ``__`` lookup, for example:: 549 550 class PersonAdmin(UserAdmin): 551 list_filter = ('company__name',) 552 553 * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`, 554 where you need to provide a few attributes and override a few 555 methods:: 556 557 from django.db.models import Q 558 from django.utils.translation import ugettext_lazy as _ 559 560 from django.contrib.admin import SimpleListFilter 561 562 class DecadeBornListFilter(SimpleListFilter): 563 # Human-readable title which will be displayed in the 564 # right sidebar just above the filter options. 565 verbose_name = _('decade born') 566 567 # Parameter for the filter that will be used in the url query. 568 # Providing this attribute is optional. If it is not provided then a 569 # slugified version of the title will automatically be used instead 570 # (that is, 'decade-born' in this example). 571 parameter_name = 'decade' 572 573 def lookups(self, *args, **kwargs): 574 """ 575 Returns a list of tuples. The first element in each tuple 576 is the coded value for the option that will appear in the 577 url query. The second element is the human-readable name 578 for the option that will appear in the right sidebar. 579 """ 580 return ( 581 ('80s', 'in the eighties'), 582 ('other', 'other'), 583 ) 584 585 def queryset(self, queryset, *args, **kwargs): 586 """ 587 Returns the filtered queryset based on the value provided 588 in the query string and retrievable via `value()`. 589 """ 590 # Compare the requested value (either '80s' or 'other') 591 # to decide how to filter the queryset. 592 if self.value() == '80s': 593 return queryset.filter(birthday__year__gte=1980, 594 birthday__year__lte=1989) 595 if self.value() == 'other': 596 return queryset.filter(Q(year__lte=1979) | 597 Q(year__gte=1990)) 598 599 class PersonAdmin(ModelAdmin): 600 list_filter = (DecadeBornListFilter,) 601 602 .. note:: 603 604 As a convenience, the ``HttpRequest`` object is passed to the 605 filter's methods, for example:: 606 607 class AuthDecadeBornListFilter(DecadeBornListFilter): 608 609 def lookups(self, request, *args, **kwargs): 610 if request.user.is_authenticated(): 611 return ( 612 ('80s', 'in the eighties'), 613 ('other', 'other'), 614 ) 615 else: 616 return ( 617 ('90s', 'in the nineties'), 618 ) 619 620 * a tuple, where the first element is a field name and the second 621 element is a class inheriting from 622 :mod:`django.contrib.admin.FieldListFilter`, for example:: 623 624 from django.contrib.admin import BooleanFieldListFilter 625 626 class PersonAdmin(ModelAdmin): 627 list_filter = ( 628 ('is_staff', BooleanFieldListFilter), 629 ) 630 631 .. note:: 632 633 The ``FieldListFilter`` API is currently considered internal 634 and prone to refactoring. 552 635 553 636 .. attribute:: ModelAdmin.list_per_page 554 637 -
tests/regressiontests/admin_filterspecs/models.py
diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py index 5b284c7..80d54c7 100644
a b from django.db import models 2 2 from django.contrib.auth.models import User 3 3 4 4 class Book(models.Model): 5 title = models.CharField(max_length= 25)5 title = models.CharField(max_length=50) 6 6 year = models.PositiveIntegerField(null=True, blank=True) 7 7 author = models.ForeignKey(User, related_name='books_authored', blank=True, null=True) 8 8 contributors = models.ManyToManyField(User, related_name='books_contributed', blank=True, null=True) 9 is_best_seller = models.NullBooleanField(default=0) 10 date_registered = models.DateField(null=True) 9 11 10 12 def __unicode__(self): 11 13 return self.title 12 13 class BoolTest(models.Model):14 NO = False15 YES = True16 YES_NO_CHOICES = (17 (NO, 'no'),18 (YES, 'yes')19 )20 completed = models.BooleanField(21 default=NO,22 choices=YES_NO_CHOICES23 ) -
tests/regressiontests/admin_filterspecs/tests.py
diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py index 8b9e734..b3c5562 100644
a b 1 import datetime 2 import calendar 3 1 4 from django.contrib.auth.admin import UserAdmin 2 5 from django.test import TestCase 3 6 from django.test.client import RequestFactory … … from django.contrib.auth.models import User 5 8 from django.contrib import admin 6 9 from django.contrib.admin.views.main import ChangeList 7 10 from django.utils.encoding import force_unicode 11 from django.contrib.admin.filterspecs import (SimpleListFilter, 12 BooleanFieldListFilter, FieldListFilter) 8 13 9 from models import Book , BoolTest14 from models import Book 10 15 11 16 def select_by(dictlist, key, value): 12 17 return [x for x in dictlist if x[key] == value][0] 13 18 14 class FilterSpecsTests(TestCase):15 19 16 def setUp(self): 17 # Users 18 self.alfred = User.objects.create_user('alfred', 'alfred@example.com') 19 self.bob = User.objects.create_user('bob', 'bob@example.com') 20 lisa = User.objects.create_user('lisa', 'lisa@example.com') 20 class DecadeListFilterBase(SimpleListFilter): 21 22 def lookups(self, request): 23 return ( 24 (u'the 90s', u'the 1990\'s'), 25 (u'the 00s', u'the 2000\'s'), 26 (u'other', u'other decades'), 27 ) 28 29 def queryset(self, request, queryset): 30 decade = self.value() 31 if decade == u'the 90s': 32 return queryset.filter(year__gte=1990, year__lte=1999) 33 if decade == u'the 00s': 34 return queryset.filter(year__gte=2000, year__lte=2009) 35 return queryset 36 37 class DecadeListFilterWithTitle(DecadeListFilterBase): 38 title = 'publication decade' 39 parameter_name = 'publication-decade' 21 40 22 #Books 23 self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred) 24 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() 41 class DecadeListFilterWithParamName(DecadeListFilterBase): 42 title = 'another publication decade' 43 parameter_name = 'blah' 28 44 29 # BoolTests 30 self.trueTest = BoolTest.objects.create(completed=True) 31 self.falseTest = BoolTest.objects.create(completed=False) 45 class ListFiltersTests(TestCase): 46 47 def setUp(self): 48 self.today = datetime.date.today() 49 self.one_week_ago = self.today - datetime.timedelta(days=7) 32 50 33 51 self.request_factory = RequestFactory() 34 52 53 # Users 54 self.alfred = User.objects.create_user('alfred', 'alfred@example.com') 55 self.bob = User.objects.create_user('bob', 'bob@example.com') 56 self.lisa = User.objects.create_user('lisa', 'lisa@example.com') 57 58 # Books 59 self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred, is_best_seller=True, date_registered=self.today) 60 self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred, is_best_seller=False) 61 self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob, is_best_seller=None, date_registered=self.today) 62 self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002, is_best_seller=True, date_registered=self.one_week_ago) 63 self.gipsy_book.contributors = [self.bob, self.lisa] 64 self.gipsy_book.save() 35 65 36 66 def get_changelist(self, request, model, modeladmin): 37 67 return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links, 38 68 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, 39 69 modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin) 40 70 41 def test_AllValuesFilterSpec(self): 71 def test_DateFieldListFilter(self): 72 modeladmin = BookAdmin(Book, admin.site) 73 74 request = self.request_factory.get('/') 75 changelist = self.get_changelist(request, Book, modeladmin) 76 77 request = self.request_factory.get('/', {'date_registered__year': self.today.year, 78 'date_registered__month': self.today.month, 79 'date_registered__day': self.today.day}) 80 changelist = self.get_changelist(request, Book, modeladmin) 81 82 # Make sure the correct queryset is returned 83 queryset = changelist.get_query_set() 84 self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book]) 85 86 # Make sure the correct choice is selected 87 filterspec = changelist.get_filters(request)[0][4] 88 self.assertEqual(force_unicode(filterspec.title), u'date_registered') 89 choice = select_by(filterspec.choices(changelist), "display", "Today") 90 self.assertEqual(choice['selected'], True) 91 self.assertEqual(choice['query_string'], '?date_registered__day=%s' 92 '&date_registered__month=%s' 93 '&date_registered__year=%s' 94 % (self.today.day, self.today.month, self.today.year)) 95 96 request = self.request_factory.get('/', {'date_registered__year': self.today.year, 97 'date_registered__month': self.today.month}) 98 changelist = self.get_changelist(request, Book, modeladmin) 99 100 # Make sure the correct queryset is returned 101 queryset = changelist.get_query_set() 102 if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month): 103 # In case one week ago is in the same month. 104 self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book]) 105 else: 106 self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book]) 107 108 # Make sure the correct choice is selected 109 filterspec = changelist.get_filters(request)[0][4] 110 self.assertEqual(force_unicode(filterspec.title), u'date_registered') 111 choice = select_by(filterspec.choices(changelist), "display", "This month") 112 self.assertEqual(choice['selected'], True) 113 self.assertEqual(choice['query_string'], '?date_registered__month=%s' 114 '&date_registered__year=%s' 115 % (self.today.month, self.today.year)) 116 117 request = self.request_factory.get('/', {'date_registered__year': self.today.year}) 118 changelist = self.get_changelist(request, Book, modeladmin) 119 120 # Make sure the correct queryset is returned 121 queryset = changelist.get_query_set() 122 if self.today.year == self.one_week_ago.year: 123 # In case one week ago is in the same year. 124 self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book]) 125 else: 126 self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book]) 127 128 # Make sure the correct choice is selected 129 filterspec = changelist.get_filters(request)[0][4] 130 self.assertEqual(force_unicode(filterspec.title), u'date_registered') 131 choice = select_by(filterspec.choices(changelist), "display", "This year") 132 self.assertEqual(choice['selected'], True) 133 self.assertEqual(choice['query_string'], '?date_registered__year=%s' 134 % (self.today.year)) 135 136 request = self.request_factory.get('/', {'date_registered__gte': self.one_week_ago.strftime('%Y-%m-%d'), 137 'date_registered__lte': self.today.strftime('%Y-%m-%d')}) 138 changelist = self.get_changelist(request, Book, modeladmin) 139 140 # Make sure the correct queryset is returned 141 queryset = changelist.get_query_set() 142 self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book]) 143 144 # Make sure the correct choice is selected 145 filterspec = changelist.get_filters(request)[0][4] 146 self.assertEqual(force_unicode(filterspec.title), u'date_registered') 147 choice = select_by(filterspec.choices(changelist), "display", "Past 7 days") 148 self.assertEqual(choice['selected'], True) 149 self.assertEqual(choice['query_string'], '?date_registered__gte=%s' 150 '&date_registered__lte=%s' 151 % (self.one_week_ago.strftime('%Y-%m-%d'), self.today.strftime('%Y-%m-%d'))) 152 153 def test_AllValuesFieldListFilter(self): 42 154 modeladmin = BookAdmin(Book, admin.site) 43 155 44 156 request = self.request_factory.get('/', {'year__isnull': 'True'}) 45 157 changelist = self.get_changelist(request, Book, modeladmin) 46 158 47 # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters159 # Make sure the correct queryset is returned 48 160 queryset = changelist.get_query_set() 161 self.assertEqual(list(queryset), [self.django_book]) 49 162 50 163 # Make sure the last choice is None and is selected 51 164 filterspec = changelist.get_filters(request)[0][0] 52 self.assertEqual(force_unicode(filterspec.title ()), u'year')165 self.assertEqual(force_unicode(filterspec.title), u'year') 53 166 choices = list(filterspec.choices(changelist)) 54 167 self.assertEqual(choices[-1]['selected'], True) 55 168 self.assertEqual(choices[-1]['query_string'], '?year__isnull=True') … … class FilterSpecsTests(TestCase): 59 172 60 173 # Make sure the correct choice is selected 61 174 filterspec = changelist.get_filters(request)[0][0] 62 self.assertEqual(force_unicode(filterspec.title ()), u'year')175 self.assertEqual(force_unicode(filterspec.title), u'year') 63 176 choices = list(filterspec.choices(changelist)) 64 177 self.assertEqual(choices[2]['selected'], True) 65 178 self.assertEqual(choices[2]['query_string'], '?year=2002') 66 179 67 def test_RelatedFi lterSpec_ForeignKey(self):180 def test_RelatedFieldListFilter_ForeignKey(self): 68 181 modeladmin = BookAdmin(Book, admin.site) 69 182 70 183 request = self.request_factory.get('/', {'author__isnull': 'True'}) 71 changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links, 72 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, 73 modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin) 184 changelist = self.get_changelist(request, Book, modeladmin) 74 185 75 # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters186 # Make sure the correct queryset is returned 76 187 queryset = changelist.get_query_set() 188 self.assertEqual(list(queryset), [self.gipsy_book]) 77 189 78 190 # Make sure the last choice is None and is selected 79 191 filterspec = changelist.get_filters(request)[0][1] 80 self.assertEqual(force_unicode(filterspec.title ()), u'author')192 self.assertEqual(force_unicode(filterspec.title), u'author') 81 193 choices = list(filterspec.choices(changelist)) 82 194 self.assertEqual(choices[-1]['selected'], True) 83 195 self.assertEqual(choices[-1]['query_string'], '?author__isnull=True') … … class FilterSpecsTests(TestCase): 87 199 88 200 # Make sure the correct choice is selected 89 201 filterspec = changelist.get_filters(request)[0][1] 90 self.assertEqual(force_unicode(filterspec.title ()), u'author')202 self.assertEqual(force_unicode(filterspec.title), u'author') 91 203 # order of choices depends on User model, which has no order 92 204 choice = select_by(filterspec.choices(changelist), "display", "alfred") 93 205 self.assertEqual(choice['selected'], True) 94 206 self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk) 95 207 96 def test_RelatedFi lterSpec_ManyToMany(self):208 def test_RelatedFieldListFilter_ManyToMany(self): 97 209 modeladmin = BookAdmin(Book, admin.site) 98 210 99 211 request = self.request_factory.get('/', {'contributors__isnull': 'True'}) 100 212 changelist = self.get_changelist(request, Book, modeladmin) 101 213 102 # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters214 # Make sure the correct queryset is returned 103 215 queryset = changelist.get_query_set() 216 self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book]) 104 217 105 218 # Make sure the last choice is None and is selected 106 219 filterspec = changelist.get_filters(request)[0][2] 107 self.assertEqual(force_unicode(filterspec.title ()), u'user')220 self.assertEqual(force_unicode(filterspec.title), u'user') 108 221 choices = list(filterspec.choices(changelist)) 109 222 self.assertEqual(choices[-1]['selected'], True) 110 223 self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True') … … class FilterSpecsTests(TestCase): 114 227 115 228 # Make sure the correct choice is selected 116 229 filterspec = changelist.get_filters(request)[0][2] 117 self.assertEqual(force_unicode(filterspec.title ()), u'user')230 self.assertEqual(force_unicode(filterspec.title), u'user') 118 231 choice = select_by(filterspec.choices(changelist), "display", "bob") 119 232 self.assertEqual(choice['selected'], True) 120 233 self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk) 121 234 122 123 def test_RelatedFilterSpec_reverse_relationships(self): 235 def test_RelatedFieldListFilter_reverse_relationships(self): 124 236 modeladmin = CustomUserAdmin(User, admin.site) 125 237 126 238 # FK relationship ----- 127 239 request = self.request_factory.get('/', {'books_authored__isnull': 'True'}) 128 240 changelist = self.get_changelist(request, User, modeladmin) 129 241 130 # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters242 # Make sure the correct queryset is returned 131 243 queryset = changelist.get_query_set() 244 self.assertEqual(list(queryset), [self.lisa]) 132 245 133 246 # Make sure the last choice is None and is selected 134 247 filterspec = changelist.get_filters(request)[0][0] 135 self.assertEqual(force_unicode(filterspec.title ()), u'book')248 self.assertEqual(force_unicode(filterspec.title), u'book') 136 249 choices = list(filterspec.choices(changelist)) 137 250 self.assertEqual(choices[-1]['selected'], True) 138 251 self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True') … … class FilterSpecsTests(TestCase): 142 255 143 256 # Make sure the correct choice is selected 144 257 filterspec = changelist.get_filters(request)[0][0] 145 self.assertEqual(force_unicode(filterspec.title ()), u'book')258 self.assertEqual(force_unicode(filterspec.title), u'book') 146 259 choice = select_by(filterspec.choices(changelist), "display", self.bio_book.title) 147 260 self.assertEqual(choice['selected'], True) 148 261 self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk) … … class FilterSpecsTests(TestCase): 151 264 request = self.request_factory.get('/', {'books_contributed__isnull': 'True'}) 152 265 changelist = self.get_changelist(request, User, modeladmin) 153 266 154 # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters267 # Make sure the correct queryset is returned 155 268 queryset = changelist.get_query_set() 269 self.assertEqual(list(queryset), [self.alfred]) 156 270 157 271 # Make sure the last choice is None and is selected 158 272 filterspec = changelist.get_filters(request)[0][1] 159 self.assertEqual(force_unicode(filterspec.title ()), u'book')273 self.assertEqual(force_unicode(filterspec.title), u'book') 160 274 choices = list(filterspec.choices(changelist)) 161 275 self.assertEqual(choices[-1]['selected'], True) 162 276 self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True') … … class FilterSpecsTests(TestCase): 166 280 167 281 # Make sure the correct choice is selected 168 282 filterspec = changelist.get_filters(request)[0][1] 169 self.assertEqual(force_unicode(filterspec.title ()), u'book')283 self.assertEqual(force_unicode(filterspec.title), u'book') 170 284 choice = select_by(filterspec.choices(changelist), "display", self.django_book.title) 171 285 self.assertEqual(choice['selected'], True) 172 286 self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk) 173 287 174 def test_BooleanFilterSpec(self): 175 modeladmin = BoolTestAdmin(BoolTest, admin.site) 288 def test_BooleanFieldListFilter(self): 289 modeladmin = BookAdmin(Book, admin.site) 290 self.verify_BooleanFieldListFilter(modeladmin) 291 292 def test_BooleanFieldListFilter_Tuple(self): 293 modeladmin = BookAdmin(Book, admin.site) 294 self.verify_BooleanFieldListFilter(modeladmin) 176 295 296 def verify_BooleanFieldListFilter(self, modeladmin): 177 297 request = self.request_factory.get('/') 178 changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links, 179 modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, 180 modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin) 298 changelist = self.get_changelist(request, Book, modeladmin) 299 300 request = self.request_factory.get('/', {'is_best_seller__exact': 0}) 301 changelist = self.get_changelist(request, Book, modeladmin) 181 302 182 # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters303 # Make sure the correct queryset is returned 183 304 queryset = changelist.get_query_set() 305 self.assertEqual(list(queryset), [self.bio_book]) 184 306 185 # Make sure the last choice is None and is selected 186 filterspec = changelist.get_filters(request)[0][0] 187 self.assertEqual(force_unicode(filterspec.title()), u'completed') 307 # Make sure the correct choice is selected 308 filterspec = changelist.get_filters(request)[0][3] 309 self.assertEqual(force_unicode(filterspec.title), u'is_best_seller') 310 choice = select_by(filterspec.choices(changelist), "display", "No") 311 self.assertEqual(choice['selected'], True) 312 self.assertEqual(choice['query_string'], '?is_best_seller__exact=0') 313 314 request = self.request_factory.get('/', {'is_best_seller__exact': 1}) 315 changelist = self.get_changelist(request, Book, modeladmin) 316 317 # Make sure the correct queryset is returned 318 queryset = changelist.get_query_set() 319 self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book]) 320 321 # Make sure the correct choice is selected 322 filterspec = changelist.get_filters(request)[0][3] 323 self.assertEqual(force_unicode(filterspec.title), u'is_best_seller') 324 choice = select_by(filterspec.choices(changelist), "display", "Yes") 325 self.assertEqual(choice['selected'], True) 326 self.assertEqual(choice['query_string'], '?is_best_seller__exact=1') 327 328 request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'}) 329 changelist = self.get_changelist(request, Book, modeladmin) 330 331 # Make sure the correct queryset is returned 332 queryset = changelist.get_query_set() 333 self.assertEqual(list(queryset), [self.django_book]) 334 335 # Make sure the correct choice is selected 336 filterspec = changelist.get_filters(request)[0][3] 337 self.assertEqual(force_unicode(filterspec.title), u'is_best_seller') 338 choice = select_by(filterspec.choices(changelist), "display", "Unknown") 339 self.assertEqual(choice['selected'], True) 340 self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True') 341 342 def test_SimpleListFilter(self): 343 modeladmin = DecadeFilterBookAdmin(Book, admin.site) 344 345 # Make sure that the first option is 'All' --------------------------- 346 347 request = self.request_factory.get('/', {}) 348 changelist = self.get_changelist(request, Book, modeladmin) 349 350 # Make sure the correct queryset is returned 351 queryset = changelist.get_query_set() 352 self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id'))) 353 354 # Make sure the correct choice is selected 355 filterspec = changelist.get_filters(request)[0][1] 356 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 357 choices = list(filterspec.choices(changelist)) 358 self.assertEqual(choices[0]['display'], u'All') 359 self.assertEqual(choices[0]['selected'], True) 360 self.assertEqual(choices[0]['query_string'], '?') 361 362 # Make sure that one can override the query parameter name ----------- 363 364 request = self.request_factory.get('/', {'blah': 'the 90s'}) 365 changelist = self.get_changelist(request, Book, modeladmin) 366 367 # Make sure the correct choice is selected 368 filterspec = changelist.get_filters(request)[0][2] 369 self.assertEqual(force_unicode(filterspec.title), u'another publication decade') 370 choices = list(filterspec.choices(changelist)) 371 self.assertEqual(choices[1]['display'], u'the 1990\'s') 372 self.assertEqual(choices[1]['selected'], True) 373 self.assertEqual(choices[1]['query_string'], '?blah=the+90s') 374 375 # Look for books in the 1990s ---------------------------------------- 376 377 request = self.request_factory.get('/', {'publication-decade': 'the 90s'}) 378 changelist = self.get_changelist(request, Book, modeladmin) 379 380 # Make sure the correct queryset is returned 381 queryset = changelist.get_query_set() 382 self.assertEqual(list(queryset), [self.bio_book]) 383 384 # Make sure the correct choice is selected 385 filterspec = changelist.get_filters(request)[0][1] 386 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 188 387 choices = list(filterspec.choices(changelist)) 189 self.assertEqual(choices[-1]['selected'], False) 190 self.assertEqual(choices[-1]['query_string'], '?completed__exact=0') 388 self.assertEqual(choices[1]['display'], u'the 1990\'s') 389 self.assertEqual(choices[1]['selected'], True) 390 self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s') 391 392 # Look for books in the 2000s ---------------------------------------- 393 394 request = self.request_factory.get('/', {'publication-decade': 'the 00s'}) 395 changelist = self.get_changelist(request, Book, modeladmin) 191 396 192 request = self.request_factory.get('/', {'completed__exact': 1}) 193 changelist = self.get_changelist(request, BoolTest, modeladmin) 397 # Make sure the correct queryset is returned 398 queryset = changelist.get_query_set() 399 self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book]) 194 400 195 401 # Make sure the correct choice is selected 402 filterspec = changelist.get_filters(request)[0][1] 403 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 404 choices = list(filterspec.choices(changelist)) 405 self.assertEqual(choices[2]['display'], u'the 2000\'s') 406 self.assertEqual(choices[2]['selected'], True) 407 self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s') 408 409 # Combine multiple filters ------------------------------------------- 410 411 request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk}) 412 changelist = self.get_changelist(request, Book, modeladmin) 413 414 # Make sure the correct queryset is returned 415 queryset = changelist.get_query_set() 416 self.assertEqual(list(queryset), [self.djangonaut_book]) 417 418 # Make sure the correct choices are selected 419 filterspec = changelist.get_filters(request)[0][1] 420 self.assertEqual(force_unicode(filterspec.title), u'publication decade') 421 choices = list(filterspec.choices(changelist)) 422 self.assertEqual(choices[2]['display'], u'the 2000\'s') 423 self.assertEqual(choices[2]['selected'], True) 424 self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) 425 196 426 filterspec = changelist.get_filters(request)[0][0] 197 self.assertEqual(force_unicode(filterspec.title()), u'completed') 198 # order of choices depends on User model, which has no order 199 choice = select_by(filterspec.choices(changelist), "display", "Yes") 427 self.assertEqual(force_unicode(filterspec.title), u'author') 428 choice = select_by(filterspec.choices(changelist), "display", "alfred") 200 429 self.assertEqual(choice['selected'], True) 201 self.assertEqual(choice['query_string'], '? completed__exact=1')430 self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) 202 431 203 432 class CustomUserAdmin(UserAdmin): 204 433 list_filter = ('books_authored', 'books_contributed') 205 434 206 435 class BookAdmin(admin.ModelAdmin): 207 list_filter = ('year', 'author', 'contributors' )436 list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered') 208 437 order_by = '-id' 209 438 210 class BoolTestAdmin(admin.ModelAdmin): 211 list_filter = ('completed',) 439 class DecadeFilterBookAdmin(admin.ModelAdmin): 440 list_filter = ('author', DecadeListFilterWithTitle, DecadeListFilterWithParamName) 441 order_by = '-id' -
tests/regressiontests/modeladmin/tests.py
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py index a20e579..b6c62d9 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): 851 853 ValidationTestModel, 852 854 ) 853 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 a descendant of ListFilterBase.", 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),) 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 class ValidationTestModelAdmin(ModelAdmin): 901 list_filter = (BooleanFieldListFilter,) 902 903 self.assertRaisesRegexp( 904 ImproperlyConfigured, 905 "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.", 906 validate, 907 ValidationTestModelAdmin, 908 ValidationTestModel, 909 ) 910 911 # Valid declarations below ----------- 912 854 913 class ValidationTestModelAdmin(ModelAdmin): 855 list_filter = ('is_active', )914 list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter)) 856 915 857 916 validate(ValidationTestModelAdmin, ValidationTestModel) 858 917