Ticket #11670: 11670.field-lookup-collisions.3.diff

File 11670.field-lookup-collisions.3.diff, 21.4 KB (added by Julien Phalip, 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):  
    233233                if k == lookup and v == value:
    234234                    return True
    235235
    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)
    237239
    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:
    241246            parts.pop()
     247            num_parts += -1
    242248
    243249        # Special case -- foo__id__exact and foo__id queries are implied
    244250        # if foo has been specificially included in the lookup list; so
    245251        # drop __id if it is the last part. However, first we need to find
    246252        # 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):
    259255            parts.pop()
     256            num_parts += -1
    260257
    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:
    266259            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
    272263
    273264    def has_add_permission(self, request):
    274265        """
  • 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  
    11from django.db import models
    22from django.db.models.sql.constants import LOOKUP_SEP
    33from django.db.models.deletion import Collector
    4 from django.db.models.related import RelatedObject
     4from django.db.models.related import RelatedObject, get_model_from_relation, NotRelationField
    55from django.forms.forms import pretty_name
    66from django.utils import formats
    77from django.utils.html import escape
    def display_for_field(value, field):  
    302302    else:
    303303        return smart_unicode(value)
    304304
    305 
    306 class NotRelationField(Exception):
    307     pass
    308 
    309 
    310 def get_model_from_relation(field):
    311     if isinstance(field, models.related.RelatedObject):
    312         return field.model
    313     elif getattr(field, 'rel'): # or isinstance?
    314         return field.rel.to
    315     else:
    316         raise NotRelationField
    317 
    318 
    319305def reverse_field_path(model, path):
    320306    """ Create a reversed field path.
    321307
    def reverse_field_path(model, path):  
    346332    return (parent, LOOKUP_SEP.join(reversed_path))
    347333
    348334
    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 = model
    365         fields.append(parent._meta.get_field_by_name(piece)[0])
    366     return fields
    367 
    368 
    369335def remove_trailing_data_field(fields):
    370     """ Discard trailing non-relation field if extant. """
     336    """ Discard trailing non-relation field if existant. """
    371337    try:
    372338        get_model_from_relation(fields[-1])
    373339    except NotRelationField:
    def get_limit_choices_to_from_path(model, path):  
    381347    If final model in path is linked via a ForeignKey or ManyToManyField which
    382348    has a `limit_choices_to` attribute, return it as a Q object.
    383349    """
    384 
    385     fields = get_fields_from_path(model, path)
     350    _, fields, _ = model._meta.resolve_lookup_path(path)
    386351    fields = remove_trailing_data_field(fields)
    387352    limit_choices_to = (
    388353        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  
    44from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
    55    _get_foreign_key)
    66from django.contrib.admin import ListFilter, FieldListFilter
    7 from django.contrib.admin.util import get_fields_from_path, NotRelationField
    87from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
    98    HORIZONTAL, VERTICAL)
    109
    def validate(cls, model):  
    8483                    # item is option #1
    8584                    field = item
    8685                # 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:
    9088                    raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
    9189                            " which does not refer to a Field."
    9290                            % (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  
    1010
    1111from django.contrib.admin import FieldListFilter
    1212from django.contrib.admin.options import IncorrectLookupParameters
    13 from django.contrib.admin.util import quote, get_fields_from_path
     13from django.contrib.admin.util import quote
    1414
    1515# Changelist settings
    1616ALL_VAR = 'all'
    class ChangeList(object):  
    105105                        field, field_list_filter_class = list_filter, FieldListFilter.create
    106106                    if not isinstance(field, models.Field):
    107107                        field_path = field
    108                         field = get_fields_from_path(self.model, field_path)[-1]
     108                        _, _, field = self.model._meta.resolve_lookup_path(field_path)
    109109                    spec = field_list_filter_class(field, request, cleaned_params,
    110110                        self.model, self.model_admin, field_path=field_path)
    111111                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):  
    5858        'address__point'.
    5959
    6060        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.
    6262        """
    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
    8866        else:
    8967            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  
     1from copy import copy
    12import re
    23from bisect import bisect
    34
     5from django.core.exceptions import FieldError
     6from django.db.models.sql.constants import LOOKUP_SEP
    47from django.conf import settings
    5 from django.db.models.related import RelatedObject
     8from django.db.models.related import RelatedObject, get_model_from_relation, NotRelationField
    69from django.db.models.fields.related import ManyToManyRel
    710from django.db.models.fields import AutoField, FieldDoesNotExist
    811from django.db.models.fields.proxy import OrderWrt
    class Options(object):  
    5457        # from *other* models. Needed for some admin checks. Internal use only.
    5558        self.related_fkey_lookups = []
    5659
     60        self._resolved_lookup_path_cache = {}
     61
    5762    def contribute_to_class(self, cls, name):
    5863        from django.db import connection
    5964        from django.db.backends.util import truncate_name
    class Options(object):  
    495500        Returns the index of the primary key field in the self.fields list.
    496501        """
    497502        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):  
    3636                {'%s__isnull' % self.parent_model._meta.module_name: False})
    3737        lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset]
    3838        return first_choice + lst
    39        
     39
    4040    def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
    4141        # Defer to the actual field definition for db prep
    4242        return self.field.get_db_prep_lookup(lookup_type, value,
    class RelatedObject(object):  
    6767
    6868    def get_cache_name(self):
    6969        return "_%s_cache" % self.get_accessor_name()
     70
     71class NotRelationField(Exception):
     72    pass
     73
     74def 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):  
    10411041        during the processing of extra filters to avoid infinite recursion.
    10421042        """
    10431043        arg, value = filter_expr
    1044         parts = arg.split(LOOKUP_SEP)
    1045         if not parts:
    1046             raise FieldError("Cannot parse keyword query %r" % arg)
    10471044
    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:
    10521050            lookup_type = parts.pop()
     1051        else:
     1052            lookup_type = 'exact'
    10531053
    10541054        # By default, this is a WHERE clause. If an aggregate is referenced
    10551055        # 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):  
    2626    name = models.CharField(max_length=100)
    2727    class Meta:
    2828        ordering = ('name', )
     29
     30class 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
     37class 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
     45class 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  
    11from datetime import datetime
    22from operator import attrgetter
     3
    34from django.core.exceptions import FieldError
    45from django.test import TestCase, skipUnlessDBFeature
    5 from models import Author, Article, Tag
     6
     7from models import Author, Article, Tag, Game, Season, Player
    68
    79
    810class LookupTests(TestCase):
    class LookupTests(TestCase):  
    243245        self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(),
    244246            [{
    245247                'id': self.a5.id,
    246                 'author_id': self.au2.id, 
     248                'author_id': self.au2.id,
    247249                'headline': 'Article 5',
    248250                'pub_date': datetime(2005, 8, 1, 9, 0)
    249251            }], transform=identity)
    class LookupTests(TestCase):  
    606608        a16.save()
    607609        self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'),
    608610            ['<Article: barfoobaz>', '<Article: bazbaRFOO>', '<Article: foobarbaz>'])
     611
     612class 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)
Back to Top