diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 07d593de025..0375f4a7cbd 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -10,6 +10,7 @@ from django.contrib.gis.db.models.sql import ( from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Area, Distance from django.db import connections +from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import RawSQL from django.db.models.fields import Field from django.db.models.query import QuerySet @@ -675,9 +676,10 @@ class GeoQuerySet(QuerySet): if geo_field not in opts.fields: # Is this operation going to be on a related geographic field? # If so, it'll have to be added to the select related information - # (e.g., if 'location__point' was given as the field name). + # (e.g., if 'location__point' was given as the field name, then + # chop the non-relational field and add select_related('location')). # Note: the operation really is defined as "must add select related!" - self.query.add_select_related([field_name]) + self.query.add_select_related([field_name.rsplit(LOOKUP_SEP, 1)[0]]) # Call pre_sql_setup() so that compiler.select gets populated. compiler.pre_sql_setup() for col, _, _ in compiler.select: diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 790f5f53f70..ea9b3e78b4d 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -673,7 +673,7 @@ class SQLCompiler(object): if not f.is_relation: # If a non-related field is used like a relation, # or if a single non-relational field is given. - if next or (cur_depth == 1 and f.name in requested): + if next or f.name in requested: raise FieldError( "Non-relational field given in select_related: '%s'. " "Choices are: %s" % ( diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index d72b0b627b0..c720c65b809 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -217,6 +217,23 @@ Database backend API * ... +``select_related()`` prohibits non-relational fields for nested relations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.8 added validation for non-relational fields in ``select_related()``:: + + >>> Book.objects.select_related('title') + Traceback (most recent call last): + ... + FieldError: Non-relational field given in select_related: 'title' + +But it didn't prohibit nested non-relation fields as it does now:: + + >>> Book.objects.select_related('author__name') + Traceback (most recent call last): + ... + FieldError: Non-relational field given in select_related: 'name' + Miscellaneous ~~~~~~~~~~~~~ diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index c32082f78aa..4ca5ae587a6 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -36,7 +36,7 @@ class ChildAdmin(admin.ModelAdmin): list_filter = ['parent', 'age'] def get_queryset(self, request): - return super(ChildAdmin, self).get_queryset(request).select_related("parent__name") + return super(ChildAdmin, self).get_queryset(request).select_related("parent") class CustomPaginationAdmin(ChildAdmin): diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index caf9beeea04..e5a0ae2c2bb 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -75,9 +75,7 @@ class ChangeListTests(TestCase): request, Child, *get_changelist_args(m, list_select_related=m.get_list_select_related(request)) ) - self.assertEqual(cl.queryset.query.select_related, { - 'parent': {'name': {}} - }) + self.assertEqual(cl.queryset.query.select_related, {'parent': {}}) def test_select_related_as_tuple(self): ia = InvitationAdmin(Invitation, custom_site) diff --git a/tests/gis_tests/relatedapp/tests.py b/tests/gis_tests/relatedapp/tests.py index 14828455fb2..d6950f7432e 100644 --- a/tests/gis_tests/relatedapp/tests.py +++ b/tests/gis_tests/relatedapp/tests.py @@ -62,6 +62,9 @@ class RelatedGeoModelTest(TestCase): qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point')) check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) + # Relations more than one level deep can be queried. + self.assertEqual(list(Parcel.objects.transform(srid, field_name='city__location__point')), []) + @skipUnlessDBFeature("supports_extent_aggr") def test_related_extent_aggregate(self): "Testing the `Extent` aggregate on related geographic models." diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index b8acd586a6a..93d96c96655 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -134,12 +134,6 @@ class SelectRelatedTests(TestCase): orders = [o.genus.family.order.name for o in world] self.assertEqual(orders, ['Agaricales']) - def test_single_related_field(self): - with self.assertNumQueries(1): - species = Species.objects.select_related('genus__name') - names = [s.genus.name for s in species] - self.assertEqual(sorted(names), ['Amanita', 'Drosophila', 'Homo', 'Pisum']) - def test_field_traversal(self): with self.assertNumQueries(1): s = (Species.objects.all() @@ -206,6 +200,10 @@ class SelectRelatedValidationTests(SimpleTestCase): with self.assertRaisesMessage(FieldError, self.non_relational_error % ('name', '(none)')): list(Domain.objects.select_related('name')) + def test_non_relational_field_nested(self): + with self.assertRaisesMessage(FieldError, self.non_relational_error % ('name', 'family')): + list(Species.objects.select_related('genus__name')) + def test_many_to_many_field(self): with self.assertRaisesMessage(FieldError, self.invalid_error % ('toppings', '(none)')): list(Pizza.objects.select_related('toppings'))