Ticket #11670: 11670.field-lookup-collisions.3.diff
File 11670.field-lookup-collisions.3.diff, 21.4 KB (added by , 13 years ago) |
---|
-
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 6be2a64..5f2c253 100644
a b class BaseModelAdmin(object): 233 233 if k == lookup and v == value: 234 234 return True 235 235 236 parts = lookup.split(LOOKUP_SEP) 236 parts, fields, _ = model._meta.resolve_lookup_path(lookup) 237 num_parts = len(parts) 238 num_fields = len(fields) 237 239 238 # Last term in lookup is a query term (__exact, __startswith etc) 239 # This term can be ignored. 240 if len(parts) > 1 and parts[-1] in QUERY_TERMS: 240 if not num_fields: 241 # Lookups on non-existants fields are ok, since they're ignored 242 # later. 243 return True 244 245 elif num_parts > 1 and parts[-1] in QUERY_TERMS: 241 246 parts.pop() 247 num_parts += -1 242 248 243 249 # Special case -- foo__id__exact and foo__id queries are implied 244 250 # if foo has been specificially included in the lookup list; so 245 251 # drop __id if it is the last part. However, first we need to find 246 252 # the pk attribute name. 247 pk_attr_name = None 248 for part in parts[:-1]: 249 field, _, _, _ = model._meta.get_field_by_name(part) 250 if hasattr(field, 'rel'): 251 model = field.rel.to 252 pk_attr_name = model._meta.pk.name 253 elif isinstance(field, RelatedObject): 254 model = field.model 255 pk_attr_name = model._meta.pk.name 256 else: 257 pk_attr_name = None 258 if pk_attr_name and len(parts) > 1 and parts[-1] == pk_attr_name: 253 if (num_fields == num_parts and 254 parts[-1] == fields[-1].model._meta.pk.name): 259 255 parts.pop() 256 num_parts += -1 260 257 261 try: 262 self.model._meta.get_field_by_name(parts[0]) 263 except FieldDoesNotExist: 264 # Lookups on non-existants fields are ok, since they're ignored 265 # later. 258 if num_parts == 1: 266 259 return True 267 else: 268 if len(parts) == 1: 269 return True 270 clean_lookup = LOOKUP_SEP.join(parts) 271 return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy 260 261 clean_lookup = LOOKUP_SEP.join(parts) 262 return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy 272 263 273 264 def has_add_permission(self, request): 274 265 """ -
django/contrib/admin/util.py
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 7204a12..5427e0b 100644
a b 1 1 from django.db import models 2 2 from django.db.models.sql.constants import LOOKUP_SEP 3 3 from django.db.models.deletion import Collector 4 from django.db.models.related import RelatedObject 4 from django.db.models.related import RelatedObject, get_model_from_relation, NotRelationField 5 5 from django.forms.forms import pretty_name 6 6 from django.utils import formats 7 7 from django.utils.html import escape … … def display_for_field(value, field): 302 302 else: 303 303 return smart_unicode(value) 304 304 305 306 class NotRelationField(Exception):307 pass308 309 310 def get_model_from_relation(field):311 if isinstance(field, models.related.RelatedObject):312 return field.model313 elif getattr(field, 'rel'): # or isinstance?314 return field.rel.to315 else:316 raise NotRelationField317 318 319 305 def reverse_field_path(model, path): 320 306 """ Create a reversed field path. 321 307 … … def reverse_field_path(model, path): 346 332 return (parent, LOOKUP_SEP.join(reversed_path)) 347 333 348 334 349 def get_fields_from_path(model, path):350 """ Return list of Fields given path relative to model.351 352 e.g. (ModelX, "user__groups__name") -> [353 <django.db.models.fields.related.ForeignKey object at 0x...>,354 <django.db.models.fields.related.ManyToManyField object at 0x...>,355 <django.db.models.fields.CharField object at 0x...>,356 ]357 """358 pieces = path.split(LOOKUP_SEP)359 fields = []360 for piece in pieces:361 if fields:362 parent = get_model_from_relation(fields[-1])363 else:364 parent = model365 fields.append(parent._meta.get_field_by_name(piece)[0])366 return fields367 368 369 335 def remove_trailing_data_field(fields): 370 """ Discard trailing non-relation field if ex tant. """336 """ Discard trailing non-relation field if existant. """ 371 337 try: 372 338 get_model_from_relation(fields[-1]) 373 339 except NotRelationField: … … def get_limit_choices_to_from_path(model, path): 381 347 If final model in path is linked via a ForeignKey or ManyToManyField which 382 348 has a `limit_choices_to` attribute, return it as a Q object. 383 349 """ 384 385 fields = get_fields_from_path(model, path) 350 _, fields, _ = model._meta.resolve_lookup_path(path) 386 351 fields = remove_trailing_data_field(fields) 387 352 limit_choices_to = ( 388 353 fields and hasattr(fields[-1], 'rel') and -
django/contrib/admin/validation.py
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 733f89d..511a644 100644
a b 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 6 from django.contrib.admin import ListFilter, FieldListFilter 7 from django.contrib.admin.util import get_fields_from_path, NotRelationField8 7 from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin, 9 8 HORIZONTAL, VERTICAL) 10 9 … … def validate(cls, model): 84 83 # item is option #1 85 84 field = item 86 85 # Validate the field string 87 try: 88 get_fields_from_path(model, field) 89 except (NotRelationField, FieldDoesNotExist): 86 _, _, last_field = model._meta.resolve_lookup_path(field) 87 if not last_field: 90 88 raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" 91 89 " which does not refer to a Field." 92 90 % (cls.__name__, idx, field)) -
django/contrib/admin/views/main.py
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 616b249..70341f5 100644
a b from django.utils.http import urlencode 10 10 11 11 from django.contrib.admin import FieldListFilter 12 12 from django.contrib.admin.options import IncorrectLookupParameters 13 from django.contrib.admin.util import quote , get_fields_from_path13 from django.contrib.admin.util import quote 14 14 15 15 # Changelist settings 16 16 ALL_VAR = 'all' … … class ChangeList(object): 105 105 field, field_list_filter_class = list_filter, FieldListFilter.create 106 106 if not isinstance(field, models.Field): 107 107 field_path = field 108 field = get_fields_from_path(self.model, field_path)[-1]108 _, _, field = self.model._meta.resolve_lookup_path(field_path) 109 109 spec = field_list_filter_class(field, request, cleaned_params, 110 110 self.model, self.model_admin, field_path=field_path) 111 111 if spec and spec.has_output(): -
django/contrib/gis/db/models/sql/where.py
diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index 0e15222..b4c3920 100644
a b class GeoWhereNode(WhereNode): 58 58 'address__point'. 59 59 60 60 If a GeometryField exists according to the given lookup on the model 61 options, it will be returned. Otherwise returns None.61 options, it will be returned. Otherwise returns False. 62 62 """ 63 # This takes into account the situation where the lookup is a 64 # lookup to a related geographic field, e.g., 'address__point'. 65 field_list = lookup.split(LOOKUP_SEP) 66 67 # Reversing so list operates like a queue of related lookups, 68 # and popping the top lookup. 69 field_list.reverse() 70 fld_name = field_list.pop() 71 72 try: 73 geo_fld = opts.get_field(fld_name) 74 # If the field list is still around, then it means that the 75 # lookup was for a geometry field across a relationship -- 76 # thus we keep on getting the related model options and the 77 # model field associated with the next field in the list 78 # until there's no more left. 79 while len(field_list): 80 opts = geo_fld.rel.to._meta 81 geo_fld = opts.get_field(field_list.pop()) 82 except (FieldDoesNotExist, AttributeError): 83 return False 84 85 # Finally, make sure we got a Geographic field and return. 86 if isinstance(geo_fld, GeometryField): 87 return geo_fld 63 _, _, field = opts.resolve_lookup_path(lookup) 64 if field and isinstance(field, GeometryField): 65 return field 88 66 else: 89 67 return False -
django/db/models/options.py
diff --git a/django/db/models/options.py b/django/db/models/options.py index 0cd52a3..09a0e8d 100644
a b 1 from copy import copy 1 2 import re 2 3 from bisect import bisect 3 4 5 from django.core.exceptions import FieldError 6 from django.db.models.sql.constants import LOOKUP_SEP 4 7 from django.conf import settings 5 from django.db.models.related import RelatedObject 8 from django.db.models.related import RelatedObject, get_model_from_relation, NotRelationField 6 9 from django.db.models.fields.related import ManyToManyRel 7 10 from django.db.models.fields import AutoField, FieldDoesNotExist 8 11 from django.db.models.fields.proxy import OrderWrt … … class Options(object): 54 57 # from *other* models. Needed for some admin checks. Internal use only. 55 58 self.related_fkey_lookups = [] 56 59 60 self._resolved_lookup_path_cache = {} 61 57 62 def contribute_to_class(self, cls, name): 58 63 from django.db import connection 59 64 from django.db.backends.util import truncate_name … … class Options(object): 495 500 Returns the index of the primary key field in the self.fields list. 496 501 """ 497 502 return self.fields.index(self.pk) 503 504 def resolve_lookup_path(self, path): 505 """ 506 Resolves the given lookup path and returns a tuple (parts, fields, 507 last_field) where parts is the lookup path split by LOOKUP_SEP, 508 fields is the list of fields that have been resolved by traversing the 509 relations, and last_field is the last field if the relation traversal 510 reached the end of the lookup or None if it didn't. 511 """ 512 # Look in the cache first. 513 if path in self._resolved_lookup_path_cache: 514 # Return a copy of parts and fields, as they may be modified later 515 # by the calling code. 516 cached = self._resolved_lookup_path_cache[path] 517 return copy(cached[0]), copy(cached[1]), cached[2] 518 519 parts = path.split(LOOKUP_SEP) 520 if not parts: 521 raise FieldError("Cannot parse lookup path %r" % path) 522 523 num_parts = len(parts) 524 525 fields = [] 526 last_field = None 527 if num_parts == 1: 528 try: 529 # Let's see if the only one lookup provided is a field 530 last_field, _, _, _ = self.get_field_by_name(path) 531 fields.append(last_field) 532 except FieldDoesNotExist: 533 # Not a field, let's move on. 534 pass 535 else: 536 # Traverse the lookup query to distinguish related fields from 537 # lookup types. 538 try: 539 opts = self 540 for counter, field_name in enumerate(parts): 541 lookup_field, _, _, _ = opts.get_field_by_name(field_name) 542 fields.append(lookup_field) 543 if (counter + 1) < num_parts: 544 # Unless we haven't reached the end of the list of 545 # lookups yet, then let's attempt to continue 546 # traversing relations. 547 related_model = get_model_from_relation(lookup_field) 548 opts = related_model._meta 549 # We have reached the end of the query and the last lookup 550 # is a field. 551 last_field = fields[-1] 552 except (FieldDoesNotExist, NotRelationField): 553 # The traversing didn't reach the end because at least one of 554 # the lookups wasn't a field. 555 pass 556 self._resolved_lookup_path_cache[path] = ( 557 parts, fields, last_field) 558 # Return a copy of parts and fields, as they may be modified later by 559 # the calling code. 560 return copy(parts), copy(fields), last_field -
django/db/models/related.py
diff --git a/django/db/models/related.py b/django/db/models/related.py index 90995d7..ad1fe47 100644
a b class RelatedObject(object): 36 36 {'%s__isnull' % self.parent_model._meta.module_name: False}) 37 37 lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset] 38 38 return first_choice + lst 39 39 40 40 def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): 41 41 # Defer to the actual field definition for db prep 42 42 return self.field.get_db_prep_lookup(lookup_type, value, … … class RelatedObject(object): 67 67 68 68 def get_cache_name(self): 69 69 return "_%s_cache" % self.get_accessor_name() 70 71 class NotRelationField(Exception): 72 pass 73 74 def get_model_from_relation(field): 75 if isinstance(field, RelatedObject): 76 return field.model 77 elif getattr(field, 'rel'): # or isinstance? 78 return field.rel.to 79 else: 80 raise NotRelationField 81 No newline at end of file -
django/db/models/sql/query.py
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 61fd2be..e6c25d2 100644
a b class Query(object): 1041 1041 during the processing of extra filters to avoid infinite recursion. 1042 1042 """ 1043 1043 arg, value = filter_expr 1044 parts = arg.split(LOOKUP_SEP)1045 if not parts:1046 raise FieldError("Cannot parse keyword query %r" % arg)1047 1044 1048 # Work out the lookup type and remove it from 'parts', if necessary. 1049 if len(parts) == 1 or parts[-1] not in self.query_terms: 1050 lookup_type = 'exact' 1051 else: 1045 parts, _, last_field = self.model._meta.resolve_lookup_path(arg) 1046 1047 # Work out the lookup type and remove it from the end of 'parts', 1048 # if necessary. 1049 if not last_field and len(parts) and parts[-1] in self.query_terms: 1052 1050 lookup_type = parts.pop() 1051 else: 1052 lookup_type = 'exact' 1053 1053 1054 1054 # By default, this is a WHERE clause. If an aggregate is referenced 1055 1055 # in the value, the filter will be promoted to a HAVING -
tests/modeltests/lookup/models.py
diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index 82434bb..dbd52e3 100644
a b class Tag(models.Model): 26 26 name = models.CharField(max_length=100) 27 27 class Meta: 28 28 ordering = ('name', ) 29 30 class Season(models.Model): 31 year = models.PositiveSmallIntegerField() 32 gt = models.IntegerField(null=True, blank=True) 33 34 def __unicode__(self): 35 return unicode(self.year) 36 37 class Game(models.Model): 38 season = models.ForeignKey(Season, related_name='games') 39 home = models.CharField(max_length=100) 40 away = models.CharField(max_length=100) 41 42 def __unicode__(self): 43 return u"%s at %s" % (self.away, self.home) 44 45 class Player(models.Model): 46 name = models.CharField(max_length=100) 47 games = models.ManyToManyField(Game, related_name='players') 48 49 def __unicode__(self): 50 return self.name 51 No newline at end of file -
tests/modeltests/lookup/tests.py
diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py index 33eeae7..6e4d98e 100644
a b 1 1 from datetime import datetime 2 2 from operator import attrgetter 3 3 4 from django.core.exceptions import FieldError 4 5 from django.test import TestCase, skipUnlessDBFeature 5 from models import Author, Article, Tag 6 7 from models import Author, Article, Tag, Game, Season, Player 6 8 7 9 8 10 class LookupTests(TestCase): … … class LookupTests(TestCase): 243 245 self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(), 244 246 [{ 245 247 'id': self.a5.id, 246 'author_id': self.au2.id, 248 'author_id': self.au2.id, 247 249 'headline': 'Article 5', 248 250 'pub_date': datetime(2005, 8, 1, 9, 0) 249 251 }], transform=identity) … … class LookupTests(TestCase): 606 608 a16.save() 607 609 self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'), 608 610 ['<Article: barfoobaz>', '<Article: bazbaRFOO>', '<Article: foobarbaz>']) 611 612 class LookupCollisionTests(TestCase): 613 614 def setUp(self): 615 # Here we're using 'gt' as a code number for the year, e.g. 111=>2009. 616 season_2009 = Season.objects.create(year=2009, gt=111) 617 season_2009.games.create(home="Houston Astros", away="St. Louis Cardinals") 618 season_2010 = Season.objects.create(year=2010, gt=222) 619 season_2010.games.create(home="Houston Astros", away="Chicago Cubs") 620 season_2010.games.create(home="Houston Astros", away="Milwaukee Brewers") 621 season_2010.games.create(home="Houston Astros", away="St. Louis Cardinals") 622 season_2011 = Season.objects.create(year=2011, gt=333) 623 season_2011.games.create(home="Houston Astros", away="St. Louis Cardinals") 624 season_2011.games.create(home="Houston Astros", away="Milwaukee Brewers") 625 hunter_pence = Player.objects.create(name="Hunter Pence") 626 hunter_pence.games = Game.objects.filter(season__year__in=[2009, 2010]) 627 pudge = Player.objects.create(name="Ivan Rodriquez") 628 pudge.games = Game.objects.filter(season__year=2009) 629 pedro_feliz = Player.objects.create(name="Pedro Feliz") 630 pedro_feliz.games = Game.objects.filter(season__year__in=[2011]) 631 johnson = Player.objects.create(name="Johnson") 632 johnson.games = Game.objects.filter(season__year__in=[2011]) 633 634 def test_lookup_collision(self): 635 """ 636 Ensure that genuine field names don't collide with built-in lookup 637 types ('year', 'gt', 'range', 'in' etc.). 638 Refs #11670. 639 """ 640 # Games in 2010 641 self.assertEqual(Game.objects.filter(season__year=2010).count(), 3) 642 self.assertEqual(Game.objects.filter(season__year__exact=2010).count(), 3) 643 self.assertEqual(Game.objects.filter(season__gt=222).count(), 3) 644 self.assertEqual(Game.objects.filter(season__gt__exact=222).count(), 3) 645 646 # Games in 2011 647 self.assertEqual(Game.objects.filter(season__year=2011).count(), 2) 648 self.assertEqual(Game.objects.filter(season__year__exact=2011).count(), 2) 649 self.assertEqual(Game.objects.filter(season__gt=333).count(), 2) 650 self.assertEqual(Game.objects.filter(season__gt__exact=333).count(), 2) 651 self.assertEqual(Game.objects.filter(season__year__gt=2010).count(), 2) 652 self.assertEqual(Game.objects.filter(season__gt__gt=222).count(), 2) 653 654 # Games played in 2010 and 2011 655 self.assertEqual(Game.objects.filter(season__year__in=[2010, 2011]).count(), 5) 656 self.assertEqual(Game.objects.filter(season__year__gt=2009).count(), 5) 657 self.assertEqual(Game.objects.filter(season__gt__in=[222, 333]).count(), 5) 658 self.assertEqual(Game.objects.filter(season__gt__gt=111).count(), 5) 659 660 # Players who played in 2009 661 self.assertEqual(Player.objects.filter(games__season__year=2009).distinct().count(), 2) 662 self.assertEqual(Player.objects.filter(games__season__year__exact=2009).distinct().count(), 2) 663 self.assertEqual(Player.objects.filter(games__season__gt=111).distinct().count(), 2) 664 self.assertEqual(Player.objects.filter(games__season__gt__exact=111).distinct().count(), 2) 665 666 # Players who played in 2010 667 self.assertEqual(Player.objects.filter(games__season__year=2010).distinct().count(), 1) 668 self.assertEqual(Player.objects.filter(games__season__year__exact=2010).distinct().count(), 1) 669 self.assertEqual(Player.objects.filter(games__season__gt=222).distinct().count(), 1) 670 self.assertEqual(Player.objects.filter(games__season__gt__exact=222).distinct().count(), 1) 671 672 # Players who played in 2011 673 self.assertEqual(Player.objects.filter(games__season__year=2011).distinct().count(), 2) 674 self.assertEqual(Player.objects.filter(games__season__year__exact=2011).distinct().count(), 2) 675 self.assertEqual(Player.objects.filter(games__season__gt=333).distinct().count(), 2) 676 self.assertEqual(Player.objects.filter(games__season__year__gt=2010).distinct().count(), 2) 677 self.assertEqual(Player.objects.filter(games__season__gt__gt=222).distinct().count(), 2)