From d02ba7f4ee7c448de28724369fc5dd9cf3e2538a Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sun, 5 Feb 2012 07:11:53 +0000 Subject: [PATCH] =?UTF-8?q?Fixed=20#11670=20--=20Prevented=20genuine=20mod?= =?UTF-8?q?el=20fields=20named=20'year',=20'month',=20'gt',=20'lt'=20etc.?= =?UTF-8?q?=20from=20being=20mistaken=20for=20lookup=20types=20in=20lookup?= =?UTF-8?q?s=20across=20relations.=20Thanks=20to=20andy=20for=20the=20repo?= =?UTF-8?q?rt,=20to=20jpwatts=20for=20the=20initial=20patch=20and=20to=20A?= =?UTF-8?q?nssi=20Ka=CC=88a=CC=88ria=CC=88inen=20and=20Alex=20Gaynor=20for?= =?UTF-8?q?=20the=20reviews.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://code.djangoproject.com/svn/django/trunk@17450 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/sql/query.py | 30 +++++++++-- tests/modeltests/lookup/models.py | 22 +++++++++ tests/modeltests/lookup/tests.py | 82 +++++++++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 8 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index ed2bc06d4fb..a78df343a7b 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1061,11 +1061,31 @@ class Query(object): if not parts: raise FieldError("Cannot parse keyword query %r" % arg) - # Work out the lookup type and remove it from 'parts', if necessary. - if len(parts) == 1 or parts[-1] not in self.query_terms: - lookup_type = 'exact' - else: - lookup_type = parts.pop() + # Work out the lookup type and remove it from the end of 'parts', + # if necessary. + lookup_type = 'exact' # Default lookup type + num_parts = len(parts) + if (len(parts) > 1 and parts[-1] in self.query_terms + and arg not in self.aggregates): + # Traverse the lookup query to distinguish related fields from + # lookup types. + lookup_model = self.model + for counter, field_name in enumerate(parts): + try: + lookup_field = lookup_model._meta.get_field(field_name) + except FieldDoesNotExist: + # Not a field. Bail out. + lookup_type = parts.pop() + break + # Unless we're at the end of the list of lookups, let's attempt + # to continue traversing relations. + if (counter + 1) < num_parts: + try: + lookup_model = lookup_field.rel.to + except AttributeError: + # Not a related field. Bail out. + lookup_type = parts.pop() + break # By default, this is a WHERE clause. If an aggregate is referenced # in the value, the filter will be promoted to a HAVING diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index 7c264a40e4a..bcdd3d7c68d 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -27,3 +27,25 @@ class Tag(models.Model): name = models.CharField(max_length=100) class Meta: ordering = ('name', ) + +class Season(models.Model): + year = models.PositiveSmallIntegerField() + gt = models.IntegerField(null=True, blank=True) + + def __unicode__(self): + return unicode(self.year) + +class Game(models.Model): + season = models.ForeignKey(Season, related_name='games') + home = models.CharField(max_length=100) + away = models.CharField(max_length=100) + + def __unicode__(self): + return u"%s at %s" % (self.away, self.home) + +class Player(models.Model): + name = models.CharField(max_length=100) + games = models.ManyToManyField(Game, related_name='players') + + def __unicode__(self): + return self.name \ No newline at end of file diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py index 9c2b0c6cea2..3571e216be1 100644 --- a/tests/modeltests/lookup/tests.py +++ b/tests/modeltests/lookup/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import absolute_import, with_statement from datetime import datetime from operator import attrgetter @@ -6,12 +6,11 @@ from operator import attrgetter from django.core.exceptions import FieldError from django.test import TestCase, skipUnlessDBFeature -from .models import Author, Article, Tag +from .models import Author, Article, Tag, Game, Season, Player class LookupTests(TestCase): - #def setUp(self): def setUp(self): # Create a few Authors. self.au1 = Author(name='Author 1') @@ -610,3 +609,80 @@ class LookupTests(TestCase): a16.save() self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'), ['', '', '']) + + def test_nonfield_lookups(self): + """ + Ensure that a lookup query containing non-fields raises the proper + exception. + """ + with self.assertRaises(FieldError): + Article.objects.filter(headline__blahblah=99) + with self.assertRaises(FieldError): + Article.objects.filter(headline__blahblah__exact=99) + with self.assertRaises(FieldError): + Article.objects.filter(blahblah=99) + + def test_lookup_collision(self): + """ + Ensure that genuine field names don't collide with built-in lookup + types ('year', 'gt', 'range', 'in' etc.). + Refs #11670. + """ + + # Here we're using 'gt' as a code number for the year, e.g. 111=>2009. + season_2009 = Season.objects.create(year=2009, gt=111) + season_2009.games.create(home="Houston Astros", away="St. Louis Cardinals") + season_2010 = Season.objects.create(year=2010, gt=222) + season_2010.games.create(home="Houston Astros", away="Chicago Cubs") + season_2010.games.create(home="Houston Astros", away="Milwaukee Brewers") + season_2010.games.create(home="Houston Astros", away="St. Louis Cardinals") + season_2011 = Season.objects.create(year=2011, gt=333) + season_2011.games.create(home="Houston Astros", away="St. Louis Cardinals") + season_2011.games.create(home="Houston Astros", away="Milwaukee Brewers") + hunter_pence = Player.objects.create(name="Hunter Pence") + hunter_pence.games = Game.objects.filter(season__year__in=[2009, 2010]) + pudge = Player.objects.create(name="Ivan Rodriquez") + pudge.games = Game.objects.filter(season__year=2009) + pedro_feliz = Player.objects.create(name="Pedro Feliz") + pedro_feliz.games = Game.objects.filter(season__year__in=[2011]) + johnson = Player.objects.create(name="Johnson") + johnson.games = Game.objects.filter(season__year__in=[2011]) + + # Games in 2010 + self.assertEqual(Game.objects.filter(season__year=2010).count(), 3) + self.assertEqual(Game.objects.filter(season__year__exact=2010).count(), 3) + self.assertEqual(Game.objects.filter(season__gt=222).count(), 3) + self.assertEqual(Game.objects.filter(season__gt__exact=222).count(), 3) + + # Games in 2011 + self.assertEqual(Game.objects.filter(season__year=2011).count(), 2) + self.assertEqual(Game.objects.filter(season__year__exact=2011).count(), 2) + self.assertEqual(Game.objects.filter(season__gt=333).count(), 2) + self.assertEqual(Game.objects.filter(season__gt__exact=333).count(), 2) + self.assertEqual(Game.objects.filter(season__year__gt=2010).count(), 2) + self.assertEqual(Game.objects.filter(season__gt__gt=222).count(), 2) + + # Games played in 2010 and 2011 + self.assertEqual(Game.objects.filter(season__year__in=[2010, 2011]).count(), 5) + self.assertEqual(Game.objects.filter(season__year__gt=2009).count(), 5) + self.assertEqual(Game.objects.filter(season__gt__in=[222, 333]).count(), 5) + self.assertEqual(Game.objects.filter(season__gt__gt=111).count(), 5) + + # Players who played in 2009 + self.assertEqual(Player.objects.filter(games__season__year=2009).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__year__exact=2009).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__gt=111).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__gt__exact=111).distinct().count(), 2) + + # Players who played in 2010 + self.assertEqual(Player.objects.filter(games__season__year=2010).distinct().count(), 1) + self.assertEqual(Player.objects.filter(games__season__year__exact=2010).distinct().count(), 1) + self.assertEqual(Player.objects.filter(games__season__gt=222).distinct().count(), 1) + self.assertEqual(Player.objects.filter(games__season__gt__exact=222).distinct().count(), 1) + + # Players who played in 2011 + self.assertEqual(Player.objects.filter(games__season__year=2011).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__year__exact=2011).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__gt=333).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__year__gt=2010).distinct().count(), 2) + self.assertEqual(Player.objects.filter(games__season__gt__gt=222).distinct().count(), 2)