Ticket #3400: 3400.2.diff
File 3400.2.diff, 26.3 KB (added by , 14 years ago) |
---|
-
django/db/models/related.py
1 from django.utils.encoding import smart_unicode 2 from django.db.models.fields import BLANK_CHOICE_DASH 3 1 4 class BoundRelatedObject(object): 2 5 def __init__(self, related_object, field_mapping, original): 3 6 self.relation = related_object … … 18 21 self.name = '%s:%s' % (self.opts.app_label, self.opts.module_name) 19 22 self.var_name = self.opts.object_name.lower() 20 23 24 def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, 25 limit_to_currently_related=False): 26 """Returns choices with a default blank choices included, for use 27 as SelectField choices for this field. 28 29 Analogue of django.db.models.fields.Field.get_choices, provided 30 initially for utilisation by RelatedFilterSpec. 31 """ 32 first_choice = include_blank and blank_choice or [] 33 queryset = self.model._default_manager.all() 34 if limit_to_currently_related: 35 queryset = queryset.complex_filter( 36 {'%s__isnull' % self.parent_model._meta.module_name: False}) 37 lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset] 38 return first_choice + lst 39 21 40 def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): 22 41 # Defer to the actual field definition for db prep 23 42 return self.field.get_db_prep_lookup(lookup_type, value, -
django/contrib/admin/validation.py
54 54 # list_filter 55 55 if hasattr(cls, 'list_filter'): 56 56 check_isseq(cls, 'list_filter', cls.list_filter) 57 for idx, field in enumerate(cls.list_filter): 58 get_field(cls, model, opts, 'list_filter[%d]' % idx, field) 57 # strict validation removed; same as search_fields now 59 58 60 59 # list_per_page = 100 61 60 if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): -
django/contrib/admin/util.py
1 1 from django.core.exceptions import ObjectDoesNotExist 2 2 from django.db import models 3 from django.db.models.sql.constants import LOOKUP_SEP 3 4 from django.forms.forms import pretty_name 4 5 from django.utils import formats 5 6 from django.utils.html import escape … … 333 334 return formats.number_format(value) 334 335 else: 335 336 return smart_unicode(value) 337 338 339 class NotRelationField(Exception): 340 pass 341 342 343 def get_model_from_relation(field): 344 if isinstance(field, models.related.RelatedObject): 345 return field.model 346 elif getattr(field, 'rel'): # or isinstance? 347 return field.rel.to 348 else: 349 raise NotRelationField 350 351 352 def reverse_field_path(model, path): 353 """ Create a reversed field path. 354 355 E.g. Given (Order, "user__groups"), 356 return (Group, "user__order"). 357 358 Final field must be a related model, not a data field. 359 360 """ 361 reversed_path = [] 362 parent = model 363 pieces = path.split(LOOKUP_SEP) 364 for piece in pieces: 365 field, model, direct, m2m = parent._meta.get_field_by_name(piece) 366 # skip trailing data field if extant: 367 if len(reversed_path) == len(pieces)-1: # final iteration 368 try: 369 get_model_from_relation(field) 370 except NotRelationField: 371 break 372 if direct: 373 related_name = field.related_query_name() 374 parent = field.rel.to 375 else: 376 related_name = field.field.name 377 parent = field.model 378 reversed_path.insert(0, related_name) 379 return (parent, LOOKUP_SEP.join(reversed_path)) 380 381 382 def get_fields_from_path(model, path): 383 """ Return list of Fields given path relative to model. 384 385 e.g. (ModelX, "user__groups__name") -> [ 386 <django.db.models.fields.related.ForeignKey object at 0x...>, 387 <django.db.models.fields.related.ManyToManyField object at 0x...>, 388 <django.db.models.fields.CharField object at 0x...>, 389 ] 390 """ 391 pieces = path.split(LOOKUP_SEP) 392 fields = [] 393 for piece in pieces: 394 if fields: 395 parent = get_model_from_relation(fields[-1]) 396 else: 397 parent = model 398 fields.append(parent._meta.get_field_by_name(piece)[0]) 399 return fields 400 401 402 def remove_trailing_data_field(fields): 403 """ Discard trailing non-relation field if extant. """ 404 try: 405 get_model_from_relation(fields[-1]) 406 except NotRelationField: 407 fields = fields[:-1] 408 return fields 409 410 411 def get_limit_choices_to_from_path(model, path): 412 """ Return Q object for limiting choices if applicable. 413 414 If final model in path is linked via a ForeignKey or ManyToManyField which 415 has a `limit_choices_to` attribute, return it as a Q object. 416 """ 417 418 fields = get_fields_from_path(model, path) 419 fields = remove_trailing_data_field(fields) 420 limit_choices_to = ( 421 fields and hasattr(fields[-1], 'rel') and 422 getattr(fields[-1].rel, 'limit_choices_to', None)) 423 if not limit_choices_to: 424 return models.Q() # empty Q 425 elif isinstance(limit_choices_to, models.Q): 426 return limit_choices_to # already a Q 427 else: 428 return models.Q(**limit_choices_to) # convert dict to Q -
django/contrib/admin/filterspecs.py
11 11 from django.utils.translation import ugettext as _ 12 12 from django.utils.html import escape 13 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 14 16 import datetime 15 17 16 18 class FilterSpec(object): 17 19 filter_specs = [] 18 def __init__(self, f, request, params, model, model_admin): 20 def __init__(self, f, request, params, model, model_admin, 21 field_path=None): 19 22 self.field = f 20 23 self.params = params 21 24 self.field_path = field_path 25 if field_path is None: 26 if isinstance(f, models.related.RelatedObject): 27 self.field_path = f.var_name 28 else: 29 self.field_path = f.name 30 22 31 def register(cls, test, factory): 23 32 cls.filter_specs.append((test, factory)) 24 33 register = classmethod(register) 25 34 26 def create(cls, f, request, params, model, model_admin ):35 def create(cls, f, request, params, model, model_admin, field_path=None): 27 36 for test, factory in cls.filter_specs: 28 37 if test(f): 29 return factory(f, request, params, model, model_admin) 38 return factory(f, request, params, model, model_admin, 39 field_path=field_path) 30 40 create = classmethod(create) 31 41 32 42 def has_output(self): … … 52 62 return mark_safe("".join(t)) 53 63 54 64 class RelatedFilterSpec(FilterSpec): 55 def __init__(self, f, request, params, model, model_admin): 56 super(RelatedFilterSpec, self).__init__(f, request, params, model, model_admin) 57 if isinstance(f, models.ManyToManyField): 58 self.lookup_title = f.rel.to._meta.verbose_name 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, 72 models.related.RelatedObject)): 73 # no direct field on this model, get name from other model 74 self.lookup_title = other_model._meta.verbose_name 59 75 else: 60 self.lookup_title = f.verbose_name 61 rel_name = f.rel.get_related_field().name62 self.lookup_kwarg = '%s__%s__exact' % ( f.name, rel_name)76 self.lookup_title = f.verbose_name # use field name 77 rel_name = other_model._meta.pk.name 78 self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name) 63 79 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 64 80 self.lookup_choices = f.get_choices(include_blank=False) 65 81 66 82 def has_output(self): 67 83 return len(self.lookup_choices) > 1 68 84 … … 78 94 'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}), 79 95 'display': val} 80 96 81 FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) 97 FilterSpec.register(lambda f: ( 98 hasattr(f, 'rel') and bool(f.rel) or 99 isinstance(f, models.related.RelatedObject)), RelatedFilterSpec) 82 100 83 101 class ChoicesFilterSpec(FilterSpec): 84 def __init__(self, f, request, params, model, model_admin): 85 super(ChoicesFilterSpec, self).__init__(f, request, params, model, model_admin) 86 self.lookup_kwarg = '%s__exact' % f.name 102 def __init__(self, f, request, params, model, model_admin, 103 field_path=None): 104 super(ChoicesFilterSpec, self).__init__(f, request, params, model, 105 model_admin, 106 field_path=field_path) 107 self.lookup_kwarg = '%s__exact' % self.field_path 87 108 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 88 109 89 110 def choices(self, cl): … … 98 119 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec) 99 120 100 121 class DateFieldFilterSpec(FilterSpec): 101 def __init__(self, f, request, params, model, model_admin): 102 super(DateFieldFilterSpec, self).__init__(f, request, params, model, model_admin) 122 def __init__(self, f, request, params, model, model_admin, 123 field_path=None): 124 super(DateFieldFilterSpec, self).__init__(f, request, params, model, 125 model_admin, 126 field_path=field_path) 103 127 104 self.field_generic = '%s__' % self.field .name128 self.field_generic = '%s__' % self.field_path 105 129 106 130 self.date_params = dict([(k, v) for k, v in params.items() if k.startswith(self.field_generic)]) 107 131 … … 111 135 112 136 self.links = ( 113 137 (_('Any date'), {}), 114 (_('Today'), {'%s__year' % self.field.name: str(today.year), 115 '%s__month' % self.field.name: str(today.month), 116 '%s__day' % self.field.name: str(today.day)}), 117 (_('Past 7 days'), {'%s__gte' % self.field.name: one_week_ago.strftime('%Y-%m-%d'), 118 '%s__lte' % f.name: today_str}), 119 (_('This month'), {'%s__year' % self.field.name: str(today.year), 120 '%s__month' % f.name: str(today.month)}), 121 (_('This year'), {'%s__year' % self.field.name: str(today.year)}) 138 (_('Today'), {'%s__year' % self.field_path: str(today.year), 139 '%s__month' % self.field_path: str(today.month), 140 '%s__day' % self.field_path: str(today.day)}), 141 (_('Past 7 days'), {'%s__gte' % self.field_path: 142 one_week_ago.strftime('%Y-%m-%d'), 143 '%s__lte' % self.field_path: today_str}), 144 (_('This month'), {'%s__year' % self.field_path: str(today.year), 145 '%s__month' % self.field_path: str(today.month)}), 146 (_('This year'), {'%s__year' % self.field_path: str(today.year)}) 122 147 ) 123 148 124 149 def title(self): … … 133 158 FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec) 134 159 135 160 class BooleanFieldFilterSpec(FilterSpec): 136 def __init__(self, f, request, params, model, model_admin): 137 super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, model_admin) 138 self.lookup_kwarg = '%s__exact' % f.name 139 self.lookup_kwarg2 = '%s__isnull' % f.name 161 def __init__(self, f, request, params, model, model_admin, 162 field_path=None): 163 super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, 164 model_admin, 165 field_path=field_path) 166 self.lookup_kwarg = '%s__exact' % self.field_path 167 self.lookup_kwarg2 = '%s__isnull' % self.field_path 140 168 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 141 169 self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None) 142 170 … … 159 187 # if a field is eligible to use the BooleanFieldFilterSpec, that'd be much 160 188 # more appropriate, and the AllValuesFilterSpec won't get used for it. 161 189 class AllValuesFilterSpec(FilterSpec): 162 def __init__(self, f, request, params, model, model_admin): 163 super(AllValuesFilterSpec, self).__init__(f, request, params, model, model_admin) 164 self.lookup_val = request.GET.get(f.name, None) 165 self.lookup_choices = model_admin.queryset(request).distinct().order_by(f.name).values(f.name) 190 def __init__(self, f, request, params, model, model_admin, 191 field_path=None): 192 super(AllValuesFilterSpec, self).__init__(f, request, params, model, 193 model_admin, 194 field_path=field_path) 195 self.lookup_val = request.GET.get(self.field_path, None) 196 parent_model, reverse_path = reverse_field_path(model, field_path) 197 queryset = parent_model._default_manager.all() 198 # optional feature: limit choices base on existing relationships 199 # queryset = queryset.complex_filter( 200 # {'%s__isnull' % reverse_path: False}) 201 limit_choices_to = get_limit_choices_to_from_path(model, field_path) 202 queryset = queryset.filter(limit_choices_to) 203 204 self.lookup_choices = \ 205 queryset.distinct().order_by(f.name).values(f.name) 166 206 167 207 def title(self): 168 208 return self.field.verbose_name 169 209 170 210 def choices(self, cl): 171 211 yield {'selected': self.lookup_val is None, 172 'query_string': cl.get_query_string({}, [self.field .name]),212 'query_string': cl.get_query_string({}, [self.field_path]), 173 213 'display': _('All')} 174 214 for val in self.lookup_choices: 175 215 val = smart_unicode(val[self.field.name]) 176 216 yield {'selected': self.lookup_val == val, 177 'query_string': cl.get_query_string({self.field .name: val}),217 'query_string': cl.get_query_string({self.field_path: val}), 178 218 'display': val} 179 219 FilterSpec.register(lambda f: True, AllValuesFilterSpec) -
django/contrib/admin/views/main.py
1 1 from django.contrib.admin.filterspecs import FilterSpec 2 2 from django.contrib.admin.options import IncorrectLookupParameters 3 from django.contrib.admin.util import quote 3 from django.contrib.admin.util import quote, get_fields_from_path 4 4 from django.core.paginator import Paginator, InvalidPage 5 5 from django.db import models 6 6 from django.db.models.query import QuerySet … … 69 69 def get_filters(self, request): 70 70 filter_specs = [] 71 71 if self.list_filter: 72 filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter] 73 for f in filter_fields: 74 spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin) 72 for filter_name in self.list_filter: 73 field = get_fields_from_path(self.model, filter_name)[-1] 74 spec = FilterSpec.create(field, request, self.params, 75 self.model, self.model_admin, 76 field_path=filter_name) 75 77 if spec and spec.has_output(): 76 78 filter_specs.append(spec) 77 79 return filter_specs, bool(filter_specs) -
tests/regressiontests/admin_views/fixtures/admin-views-books.xml
1 <?xml version="1.0" encoding="utf-8"?> 2 <django-objects version="1.0"> 3 <object pk="1" model="admin_views.book"> 4 <field type="CharField" name="name">Book 1</field> 5 </object> 6 <object pk="2" model="admin_views.book"> 7 <field type="CharField" name="name">Book 2</field> 8 </object> 9 <object pk="1" model="admin_views.promo"> 10 <field type="CharField" name="name">Promo 1</field> 11 <field type="ForiegnKey" name="book">1</field> 12 </object> 13 <object pk="2" model="admin_views.promo"> 14 <field type="CharField" name="name">Promo 2</field> 15 <field type="ForiegnKey" name="book">2</field> 16 </object> 17 <object pk="1" model="admin_views.chapter"> 18 <field type="CharField" name="title">Chapter 1</field> 19 <field type="TextField" name="content">[ insert contents here ]</field> 20 <field type="ForiegnKey" name="book">1</field> 21 </object> 22 <object pk="2" model="admin_views.chapter"> 23 <field type="CharField" name="title">Chapter 2</field> 24 <field type="TextField" name="content">[ insert contents here ]</field> 25 <field type="ForiegnKey" name="book">1</field> 26 </object> 27 <object pk="3" model="admin_views.chapter"> 28 <field type="CharField" name="title">Chapter 1</field> 29 <field type="TextField" name="content">[ insert contents here ]</field> 30 <field type="ForiegnKey" name="book">2</field> 31 </object> 32 <object pk="4" model="admin_views.chapter"> 33 <field type="CharField" name="title">Chapter 2</field> 34 <field type="TextField" name="content">[ insert contents here ]</field> 35 <field type="ForiegnKey" name="book">2</field> 36 </object> 37 <object pk="1" model="admin_views.chapterxtra1"> 38 <field type="CharField" name="xtra">ChapterXtra1 1</field> 39 <field type="ForiegnKey" name="chap">1</field> 40 </object> 41 <object pk="2" model="admin_views.chapterxtra1"> 42 <field type="CharField" name="xtra">ChapterXtra1 2</field> 43 <field type="ForiegnKey" name="chap">3</field> 44 </object> 45 </django-objects> -
tests/regressiontests/admin_views/tests.py
17 17 from django.utils.cache import get_max_age 18 18 from django.utils.encoding import iri_to_uri 19 19 from django.utils.html import escape 20 from django.utils.http import urlencode 20 21 from django.utils.translation import get_date_formats, activate, deactivate 22 from django.db.models.sql.constants import LOOKUP_SEP 21 23 22 24 # local test models 23 25 from models import Article, BarAccount, CustomArticle, EmptyModel, \ 24 26 FooAccount, Gallery, ModelWithStringPrimaryKey, \ 25 27 Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ 26 28 Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ 27 Category, Post, Plot, FunkyTag 29 Category, Post, Plot, FunkyTag, Chapter, Book, Promo 28 30 29 31 30 32 class AdminViewBasicTest(TestCase): 31 fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml'] 33 fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 34 'admin-views-fabrics.xml', 'admin-views-books.xml'] 32 35 33 36 # Store the bit of the URL where the admin is registered as a class 34 37 # variable. That way we can test a second AdminSite just by subclassing … … 201 204 ) 202 205 203 206 def testLimitedFilter(self): 204 """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.""" 207 """Ensure admin changelist filters do not contain objects excluded via limit_choices_to. 208 This also tests relation-spanning filters (e.g. 'color__value'). 209 """ 205 210 response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit) 206 211 self.failUnlessEqual(response.status_code, 200) 207 212 self.failUnless( … … 213 218 "Changelist filter not correctly limited by limit_choices_to." 214 219 ) 215 220 221 def testRelationSpanningFilters(self): 222 response = self.client.get('/test_admin/%s/admin_views/chapterxtra1/' % 223 self.urlbit) 224 self.failUnlessEqual(response.status_code, 200) 225 self.assertContains(response, '<div id="changelist-filter">') 226 filters = { 227 'chap__id__exact': dict( 228 values=[c.id for c in Chapter.objects.all()], 229 test=lambda obj, value: obj.chap.id == value), 230 'chap__title': dict( 231 values=[c.title for c in Chapter.objects.all()], 232 test=lambda obj, value: obj.chap.title == value), 233 'chap__book__id__exact': dict( 234 values=[b.id for b in Book.objects.all()], 235 test=lambda obj, value: obj.chap.book.id == value), 236 'chap__book__name': dict( 237 values=[b.name for b in Book.objects.all()], 238 test=lambda obj, value: obj.chap.book.name == value), 239 'chap__book__promo__id__exact': dict( 240 values=[p.id for p in Promo.objects.all()], 241 test=lambda obj, value: 242 obj.chap.book.promo_set.filter(id=value).exists()), 243 'chap__book__promo__name': dict( 244 values=[p.name for p in Promo.objects.all()], 245 test=lambda obj, value: 246 obj.chap.book.promo_set.filter(name=value).exists()), 247 } 248 for filter_path, params in filters.items(): 249 for value in params['values']: 250 query_string = urlencode({filter_path: value}) 251 # ensure filter link exists 252 self.assertContains(response, '<a href="?%s">' % query_string) 253 # ensure link works 254 filtered_response = self.client.get( 255 '/test_admin/%s/admin_views/chapterxtra1/?%s' % ( 256 self.urlbit, query_string)) 257 self.failUnlessEqual(filtered_response.status_code, 200) 258 # ensure changelist contains only valid objects 259 for obj in filtered_response.context['cl'].query_set.all(): 260 self.assertTrue(params['test'](obj, value)) 261 216 262 def testIncorrectLookupParameters(self): 217 263 """Ensure incorrect lookup parameters are handled gracefully.""" 218 264 response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'}) -
tests/regressiontests/admin_views/customadmin.py
32 32 site.register(models.Section, inlines=[models.ArticleInline]) 33 33 site.register(models.Thing, models.ThingAdmin) 34 34 site.register(models.Fabric, models.FabricAdmin) 35 site.register(models.ChapterXtra1, models.ChapterXtra1Admin) -
tests/regressiontests/admin_views/models.py
89 89 class ChapterInline(admin.TabularInline): 90 90 model = Chapter 91 91 92 class ChapterXtra1Admin(admin.ModelAdmin): 93 list_filter = ('chap', 94 'chap__title', 95 'chap__book', 96 'chap__book__name', 97 'chap__book__promo', 98 'chap__book__promo__name',) 99 92 100 class ArticleAdmin(admin.ModelAdmin): 93 101 list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year') 94 102 list_filter = ('date',) … … 148 156 return self.title 149 157 150 158 class ThingAdmin(admin.ModelAdmin): 151 list_filter = ('color', )159 list_filter = ('color', 'color__warm', 'color__value') 152 160 153 161 class Fabric(models.Model): 154 162 NG_CHOICES = ( … … 622 630 # contrib.admin.util's get_deleted_objects function. 623 631 admin.site.register(Book, inlines=[ChapterInline]) 624 632 admin.site.register(Promo) 625 admin.site.register(ChapterXtra1 )633 admin.site.register(ChapterXtra1, ChapterXtra1Admin) 626 634 admin.site.register(Pizza, PizzaAdmin) 627 635 admin.site.register(Topping) -
tests/regressiontests/modeladmin/models.py
677 677 ImproperlyConfigured: 'ValidationTestModelAdmin.list_filter' must be a list or tuple. 678 678 679 679 >>> class ValidationTestModelAdmin(ModelAdmin): 680 ... list_filter = ('non_existent_field',)681 >>> validate(ValidationTestModelAdmin, ValidationTestModel)682 Traceback (most recent call last):683 ...684 ImproperlyConfigured: 'ValidationTestModelAdmin.list_filter[0]' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.685 686 >>> class ValidationTestModelAdmin(ModelAdmin):687 680 ... list_filter = ('is_active',) 688 681 >>> validate(ValidationTestModelAdmin, ValidationTestModel) 689 682 -
docs/ref/contrib/admin/index.txt
461 461 list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') 462 462 list_filter = ('is_staff', 'is_superuser') 463 463 464 In ``list_filter`` can be defined lookup separator as well:: 465 466 class UserAdminWithLookup(UserAdmin): 467 list_filter = ('groups__name') 468 464 469 The above code results in an admin change list page that looks like this: 465 470 466 471 .. image:: _images/users_changelist.png