summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2015-10-06 22:05:53 +0200
committerClaude Paroz <claude@2xlibre.net>2015-10-09 15:27:47 +0200
commit37d06cfc46679759c47178a3380b12fbf22a4f25 (patch)
treeea1c002d8bc9888c32a2a18e80ba8bcbe44f8970
parent4a7b58210defea33a428b748ccbc97ae8fd49838 (diff)
Fixed #25499 -- Added the ability to pass an expression in distance lookups
Thanks Bibhas Debnath for the report and Tim Graham for the review.
-rw-r--r--django/contrib/gis/db/backends/oracle/operations.py2
-rw-r--r--django/contrib/gis/db/backends/postgis/operations.py34
-rw-r--r--django/contrib/gis/db/backends/spatialite/operations.py2
-rw-r--r--django/contrib/gis/db/models/lookups.py35
-rw-r--r--docs/ref/contrib/gis/geoquerysets.txt12
-rw-r--r--docs/releases/1.10.txt3
-rw-r--r--tests/gis_tests/distapp/models.py2
-rw-r--r--tests/gis_tests/distapp/tests.py27
8 files changed, 84 insertions, 33 deletions
diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py
index 6e901129d9..b443ae0f46 100644
--- a/django/contrib/gis/db/backends/oracle/operations.py
+++ b/django/contrib/gis/db/backends/oracle/operations.py
@@ -195,7 +195,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
"""
return 'MDSYS.SDO_GEOMETRY'
- def get_distance(self, f, value, lookup_type):
+ def get_distance(self, f, value, lookup_type, **kwargs):
"""
Returns the distance parameters given the value and the lookup type.
On Oracle, geometry columns with a geodetic coordinate system behave
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index e5bb115b71..4d0c2b8865 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -34,14 +34,17 @@ class PostGISOperator(SpatialOperator):
class PostGISDistanceOperator(PostGISOperator):
- sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %%s'
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s'
def as_sql(self, connection, lookup, template_params, sql_params):
if not lookup.lhs.output_field.geography and lookup.lhs.output_field.geodetic(connection):
sql_template = self.sql_template
if len(lookup.rhs) == 3 and lookup.rhs[-1] == 'spheroid':
template_params.update({'op': self.op, 'func': 'ST_Distance_Spheroid'})
- sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s) %(op)s %%s'
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s) %(op)s %(value)s'
+ # Using distance_spheroid requires the spheroid of the field as
+ # a parameter.
+ sql_params.insert(1, lookup.lhs.output_field._spheroid)
else:
template_params.update({'op': self.op, 'func': 'ST_Distance_Sphere'})
return sql_template % template_params, sql_params
@@ -226,7 +229,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
geom_type = f.geom_type
return 'geometry(%s,%d)' % (geom_type, f.srid)
- def get_distance(self, f, dist_val, lookup_type):
+ def get_distance(self, f, dist_val, lookup_type, handle_spheroid=True):
"""
Retrieve the distance parameters for the given geometry field,
distance lookup value, and the distance lookup type.
@@ -236,11 +239,8 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
projected geometry columns. In addition, it has to take into account
the geography column type.
"""
- # Getting the distance parameter and any options.
- if len(dist_val) == 1:
- value, option = dist_val[0], None
- else:
- value, option = dist_val
+ # Getting the distance parameter
+ value = dist_val[0]
# Shorthand boolean flags.
geodetic = f.geodetic(self.connection)
@@ -260,13 +260,17 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
# Assuming the distance is in the units of the field.
dist_param = value
- if (not geography and geodetic and lookup_type != 'dwithin'
- and option == 'spheroid'):
- # using distance_spheroid requires the spheroid of the field as
- # a parameter.
- return [f._spheroid, dist_param]
- else:
- return [dist_param]
+ params = [dist_param]
+ # handle_spheroid *might* be dropped in Django 2.0 as PostGISDistanceOperator
+ # also handles it (#25524).
+ if handle_spheroid and len(dist_val) > 1:
+ option = dist_val[1]
+ if (not geography and geodetic and lookup_type != 'dwithin'
+ and option == 'spheroid'):
+ # using distance_spheroid requires the spheroid of the field as
+ # a parameter.
+ params.insert(0, f._spheroid)
+ return params
def get_geom_placeholder(self, f, value, compiler):
"""
diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py
index 6dc5048b1b..6b996735ca 100644
--- a/django/contrib/gis/db/backends/spatialite/operations.py
+++ b/django/contrib/gis/db/backends/spatialite/operations.py
@@ -175,7 +175,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
"""
return None
- def get_distance(self, f, value, lookup_type):
+ def get_distance(self, f, value, lookup_type, **kwargs):
"""
Returns the distance parameters for the given geometry field,
lookup value, and lookup type. SpatiaLite only supports regular
diff --git a/django/contrib/gis/db/models/lookups.py b/django/contrib/gis/db/models/lookups.py
index 92ff2929ff..a1c85b9bfe 100644
--- a/django/contrib/gis/db/models/lookups.py
+++ b/django/contrib/gis/db/models/lookups.py
@@ -16,6 +16,10 @@ class GISLookup(Lookup):
transform_func = None
distance = False
+ def __init__(self, *args, **kwargs):
+ super(GISLookup, self).__init__(*args, **kwargs)
+ self.template_params = {}
+
@classmethod
def _check_geo_field(cls, opts, lookup):
"""
@@ -98,7 +102,8 @@ class GISLookup(Lookup):
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
sql_params.extend(rhs_params)
- template_params = {'lhs': lhs_sql, 'rhs': rhs_sql}
+ template_params = {'lhs': lhs_sql, 'rhs': rhs_sql, 'value': '%s'}
+ template_params.update(self.template_params)
rhs_op = self.get_rhs_op(connection, rhs_sql)
return rhs_op.as_sql(connection, self, template_params, sql_params)
@@ -302,18 +307,26 @@ gis_lookups['within'] = WithinLookup
class DistanceLookupBase(GISLookup):
distance = True
- sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %%s'
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s'
- def get_db_prep_lookup(self, value, connection):
- if isinstance(value, (tuple, list)):
- if not 2 <= len(value) <= 3:
- raise ValueError("2 or 3-element tuple required for '%s' lookup." % self.lookup_name)
- params = [connection.ops.Adapter(value[0])]
- # Getting the distance parameter in the units of the field.
- params += connection.ops.get_distance(self.lhs.output_field, value[1:], self.lookup_name)
- return ('%s', params)
+ def process_rhs(self, compiler, connection):
+ if not isinstance(self.rhs, (tuple, list)) or not 2 <= len(self.rhs) <= 3:
+ raise ValueError("2 or 3-element tuple required for '%s' lookup." % self.lookup_name)
+ params = [connection.ops.Adapter(self.rhs[0])]
+ # Getting the distance parameter in the units of the field.
+ dist_param = self.rhs[1]
+ if hasattr(dist_param, 'resolve_expression'):
+ dist_param = dist_param.resolve_expression(compiler.query)
+ sql, expr_params = compiler.compile(dist_param)
+ self.template_params['value'] = sql
+ params.extend(expr_params)
else:
- return super(DistanceLookupBase, self).get_db_prep_lookup(value, connection)
+ params += connection.ops.get_distance(
+ self.lhs.output_field, (dist_param,) + self.rhs[2:],
+ self.lookup_name, handle_spheroid=False
+ )
+ rhs = connection.ops.get_geom_placeholder(self.lhs.output_field, params[0], compiler)
+ return (rhs, params)
class DWithinLookup(DistanceLookupBase):
diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt
index 9a42bd7a15..8b554ece06 100644
--- a/docs/ref/contrib/gis/geoquerysets.txt
+++ b/docs/ref/contrib/gis/geoquerysets.txt
@@ -515,14 +515,20 @@ Distance lookups take the following form::
The value passed into a distance lookup is a tuple; the first two
values are mandatory, and are the geometry to calculate distances to,
-and a distance value (either a number in units of the field or a
-:class:`~django.contrib.gis.measure.Distance` object). On every
-distance lookup but :lookup:`dwithin`, an optional
+and a distance value (either a number in units of the field, a
+:class:`~django.contrib.gis.measure.Distance` object, or a `query expression
+<ref/models/expressions>`).
+
+With PostGIS, on every distance lookup but :lookup:`dwithin`, an optional
third element, ``'spheroid'``, may be included to tell GeoDjango
to use the more accurate spheroid distance calculation functions on
fields with a geodetic coordinate system (e.g., ``ST_Distance_Spheroid``
would be used instead of ``ST_Distance_Sphere``).
+.. versionadded:: 1.10
+
+ The ability to pass an expression as the distance value was added.
+
.. fieldlookup:: distance_gt
distance_gt
diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt
index bb911bb8a0..c9978ac345 100644
--- a/docs/releases/1.10.txt
+++ b/docs/releases/1.10.txt
@@ -59,7 +59,8 @@ Minor features
:mod:`django.contrib.gis`
^^^^^^^^^^^^^^^^^^^^^^^^^^
-* ...
+* :ref:`Distance lookups <distance-lookups>` now accept expressions as the
+ distance value parameter.
:mod:`django.contrib.messages`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/tests/gis_tests/distapp/models.py b/tests/gis_tests/distapp/models.py
index c04659f524..1632988534 100644
--- a/tests/gis_tests/distapp/models.py
+++ b/tests/gis_tests/distapp/models.py
@@ -21,6 +21,7 @@ class NamedModel(models.Model):
class SouthTexasCity(NamedModel):
"City model on projected coordinate system for South Texas."
point = models.PointField(srid=32140)
+ radius = models.IntegerField(default=10000)
class SouthTexasCityFt(NamedModel):
@@ -31,6 +32,7 @@ class SouthTexasCityFt(NamedModel):
class AustraliaCity(NamedModel):
"City model for Australia, using WGS84."
point = models.PointField()
+ radius = models.IntegerField(default=10000)
class CensusZipcode(NamedModel):
diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py
index 56e4711ab7..322c8b7214 100644
--- a/tests/gis_tests/distapp/tests.py
+++ b/tests/gis_tests/distapp/tests.py
@@ -6,7 +6,7 @@ from django.contrib.gis.db.models.functions import (
from django.contrib.gis.geos import GEOSGeometry, LineString, Point
from django.contrib.gis.measure import D # alias for Distance
from django.db import connection
-from django.db.models import Q
+from django.db.models import F, Q
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
from django.utils.deprecation import RemovedInDjango20Warning
@@ -323,6 +323,31 @@ class DistanceTest(TestCase):
cities = self.get_names(qs)
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
+ @skipUnlessDBFeature("supports_distances_lookups")
+ def test_distance_lookups_with_expression_rhs(self):
+ qs = SouthTexasCity.objects.filter(
+ point__distance_lte=(self.stx_pnt, F('radius')),
+ ).order_by('name')
+ self.assertEqual(
+ self.get_names(qs),
+ ['Bellaire', 'Downtown Houston', 'Southside Place', 'West University Place']
+ )
+
+ # With a combined expression
+ qs = SouthTexasCity.objects.filter(
+ point__distance_lte=(self.stx_pnt, F('radius') * 2),
+ ).order_by('name')
+ self.assertEqual(len(qs), 5)
+ self.assertIn('Pearland', self.get_names(qs))
+
+ # With spheroid param
+ if connection.features.supports_distance_geodetic:
+ hobart = AustraliaCity.objects.get(name='Hobart')
+ qs = AustraliaCity.objects.filter(
+ point__distance_lte=(hobart.point, F('radius') * 70, 'spheroid'),
+ ).order_by('name')
+ self.assertEqual(self.get_names(qs), ['Canberra', 'Hobart', 'Melbourne'])
+
@skipUnlessDBFeature("has_area_method")
@ignore_warnings(category=RemovedInDjango20Warning)
def test_area(self):