Skip to content

Commit 33ea2b4

Browse files
committed
Merge pull request #1036 from MRigal/snario-min-distance
Added test, doc to implementation of min_distance query
2 parents 5badb9d + 5c807f3 commit 33ea2b4

File tree

6 files changed

+105
-45
lines changed

6 files changed

+105
-45
lines changed

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Changes in 0.9.X - DEV
2929
- Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods.
3030
- Fix for delete with write_concern {'w': 0}. #1008
3131
- Allow dynamic lookup for more than two parts. #882
32+
- Added support for min_distance on geo queries. #831
3233
3334
Changes in 0.9.0
3435
================

docs/guide/querying.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,10 @@ The following were added in MongoEngine 0.8 for
146146
loc.objects(point__near=[40, 5])
147147
loc.objects(point__near={"type": "Point", "coordinates": [40, 5]})
148148

149-
You can also set the maximum distance in meters as well::
149+
You can also set the maximum and/or the minimum distance in meters as well::
150150

151151
loc.objects(point__near=[40, 5], point__max_distance=1000)
152+
loc.objects(point__near=[40, 5], point__min_distance=100)
152153

153154
The older 2D indexes are still supported with the
154155
:class:`~mongoengine.fields.GeoPointField`:
@@ -168,7 +169,8 @@ The older 2D indexes are still supported with the
168169

169170
* ``max_distance`` -- can be added to your location queries to set a maximum
170171
distance.
171-
172+
* ``min_distance`` -- can be added to your location queries to set a minimum
173+
distance.
172174

173175
Querying lists
174176
--------------

mongoengine/queryset/transform.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mongoengine.connection import get_connection
88
from mongoengine.common import _import_class
99
from mongoengine.errors import InvalidQueryError
10+
from mongoengine.python_support import IS_PYMONGO_3
1011

1112
__all__ = ('query', 'update')
1213

@@ -15,7 +16,7 @@
1516
'all', 'size', 'exists', 'not', 'elemMatch', 'type')
1617
GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
1718
'within_box', 'within_polygon', 'near', 'near_sphere',
18-
'max_distance', 'geo_within', 'geo_within_box',
19+
'max_distance', 'min_distance', 'geo_within', 'geo_within_box',
1920
'geo_within_polygon', 'geo_within_center',
2021
'geo_within_sphere', 'geo_intersects')
2122
STRING_OPERATORS = ('contains', 'icontains', 'startswith',
@@ -44,8 +45,8 @@ def query(_doc_cls=None, _field_operation=False, **query):
4445
if len(parts) > 1 and parts[-1] in MATCH_OPERATORS:
4546
op = parts.pop()
4647

47-
# if user escape field name by __
48-
if len(parts) > 1 and parts[-1] == "":
48+
#if user escape field name by __
49+
if len(parts) > 1 and parts[-1]=="":
4950
parts.pop()
5051

5152
negate = False
@@ -126,24 +127,34 @@ def query(_doc_cls=None, _field_operation=False, **query):
126127
elif key in mongo_query:
127128
if key in mongo_query and isinstance(mongo_query[key], dict):
128129
mongo_query[key].update(value)
129-
# $maxDistance needs to come last - convert to SON
130+
# $max/minDistance needs to come last - convert to SON
130131
value_dict = mongo_query[key]
131-
if '$maxDistance' in value_dict and '$near' in value_dict:
132+
if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and \
133+
('$near' in value_dict or '$nearSphere' in value_dict):
132134
value_son = SON()
133-
if isinstance(value_dict['$near'], dict):
134-
for k, v in value_dict.iteritems():
135-
if k == '$maxDistance':
136-
continue
137-
value_son[k] = v
138-
value_son['$near'] = SON(value_son['$near'])
139-
value_son['$near']['$maxDistance'] = value_dict['$maxDistance']
140-
else:
141-
for k, v in value_dict.iteritems():
142-
if k == '$maxDistance':
143-
continue
144-
value_son[k] = v
145-
value_son['$maxDistance'] = value_dict['$maxDistance']
146-
135+
for k, v in value_dict.iteritems():
136+
if k == '$maxDistance' or k == '$minDistance':
137+
continue
138+
value_son[k] = v
139+
# Required for MongoDB >= 2.6, may fail when combining
140+
# PyMongo 3+ and MongoDB < 2.6
141+
near_embedded = False
142+
for near_op in ('$near', '$nearSphere'):
143+
if isinstance(value_dict.get(near_op), dict) and (
144+
IS_PYMONGO_3 or get_connection().max_wire_version > 1):
145+
value_son[near_op] = SON(value_son[near_op])
146+
if '$maxDistance' in value_dict:
147+
value_son[near_op][
148+
'$maxDistance'] = value_dict['$maxDistance']
149+
if '$minDistance' in value_dict:
150+
value_son[near_op][
151+
'$minDistance'] = value_dict['$minDistance']
152+
near_embedded = True
153+
if not near_embedded:
154+
if '$maxDistance' in value_dict:
155+
value_son['$maxDistance'] = value_dict['$maxDistance']
156+
if '$minDistance' in value_dict:
157+
value_son['$minDistance'] = value_dict['$minDistance']
147158
mongo_query[key] = value_son
148159
else:
149160
# Store for manually merging later
@@ -297,7 +308,11 @@ def update(_doc_cls=None, **update):
297308

298309
def _geo_operator(field, op, value):
299310
"""Helper to return the query for a given geo query"""
300-
if field._geo_index == pymongo.GEO2D:
311+
if op == "max_distance":
312+
value = {'$maxDistance': value}
313+
elif op == "min_distance":
314+
value = {'$minDistance': value}
315+
elif field._geo_index == pymongo.GEO2D:
301316
if op == "within_distance":
302317
value = {'$within': {'$center': value}}
303318
elif op == "within_spherical_distance":
@@ -310,8 +325,6 @@ def _geo_operator(field, op, value):
310325
value = {'$nearSphere': value}
311326
elif op == 'within_box':
312327
value = {'$within': {'$box': value}}
313-
elif op == "max_distance":
314-
value = {'$maxDistance': value}
315328
else:
316329
raise NotImplementedError("Geo method '%s' has not "
317330
"been implemented for a GeoPointField" % op)
@@ -330,8 +343,6 @@ def _geo_operator(field, op, value):
330343
value = {"$geoIntersects": _infer_geometry(value)}
331344
elif op == "near":
332345
value = {'$near': _infer_geometry(value)}
333-
elif op == "max_distance":
334-
value = {'$maxDistance': value}
335346
else:
336347
raise NotImplementedError("Geo method '%s' has not "
337348
"been implemented for a %s " % (op, field._name))

tests/queryset/geo.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ def __unicode__(self):
7070
self.assertEqual(events.count(), 1)
7171
self.assertEqual(events[0], event2)
7272

73+
# find events at least 10 degrees away of san francisco
74+
point = [-122.415579, 37.7566023]
75+
events = Event.objects(location__near=point, location__min_distance=10)
76+
# The following real test passes on MongoDB 3 but minDistance seems
77+
# buggy on older MongoDB versions
78+
if get_connection().server_info()['versionArray'][0] > 2:
79+
self.assertEqual(events.count(), 2)
80+
else:
81+
self.assertTrue(events.count() >= 2)
82+
7383
# find events within 10 degrees of san francisco
7484
point_and_distance = [[-122.415579, 37.7566023], 10]
7585
events = Event.objects(location__within_distance=point_and_distance)
@@ -171,21 +181,32 @@ class Point(Document):
171181

172182
# Same behavior for _within_spherical_distance
173183
points = Point.objects(
174-
location__within_spherical_distance=[[-122, 37.5], 60/earth_radius]
184+
location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius]
175185
)
176186
self.assertEqual(points.count(), 2)
177187

178188
points = Point.objects(location__near_sphere=[-122, 37.5],
179189
location__max_distance=60 / earth_radius)
180-
# This test is sometimes failing with Mongo internals non-sense.
181-
# See https://travis-ci.org/MongoEngine/mongoengine/builds/58729101
182-
try:
183-
points.count()
184-
except OperationFailure:
185-
raise SkipTest("Sometimes MongoDB ignores its capacities on maxDistance")
186-
187190
self.assertEqual(points.count(), 2)
188191

192+
# Test query works with max_distance, being farer from one point
193+
points = Point.objects(location__near_sphere=[-122, 37.8],
194+
location__max_distance=60 / earth_radius)
195+
close_point = points.first()
196+
self.assertEqual(points.count(), 1)
197+
198+
# Test query works with min_distance, being farer from one point
199+
points = Point.objects(location__near_sphere=[-122, 37.8],
200+
location__min_distance=60 / earth_radius)
201+
# The following real test passes on MongoDB 3 but minDistance seems
202+
# buggy on older MongoDB versions
203+
if get_connection().server_info()['versionArray'][0] > 2:
204+
self.assertEqual(points.count(), 1)
205+
far_point = points.first()
206+
self.assertNotEqual(close_point, far_point)
207+
else:
208+
self.assertTrue(points.count() >= 1)
209+
189210
# Finds both points, but orders the north point first because it's
190211
# closer to the reference point to the north.
191212
points = Point.objects(location__near_sphere=[-122, 38.5])
@@ -268,6 +289,20 @@ def __unicode__(self):
268289
self.assertEqual(events.count(), 2)
269290
self.assertEqual(events[0], event3)
270291

292+
# ensure min_distance and max_distance combine well
293+
events = Event.objects(location__near=[-87.67892, 41.9120459],
294+
location__min_distance=1000,
295+
location__max_distance=10000).order_by("-date")
296+
self.assertEqual(events.count(), 1)
297+
self.assertEqual(events[0], event3)
298+
299+
# ensure ordering is respected by "near"
300+
events = Event.objects(location__near=[-87.67892, 41.9120459],
301+
# location__min_distance=10000
302+
location__min_distance=10000).order_by("-date")
303+
self.assertEqual(events.count(), 1)
304+
self.assertEqual(events[0], event2)
305+
271306
# check that within_box works
272307
box = [(-125.0, 35.0), (-100.0, 40.0)]
273308
events = Event.objects(location__geo_within_box=box)

tests/queryset/queryset.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4763,5 +4763,19 @@ class Person(Document):
47634763
for p in Person.objects():
47644764
self.assertEqual(p.name, 'a')
47654765

4766+
def test_last_field_name_like_operator(self):
4767+
class EmbeddedItem(EmbeddedDocument):
4768+
type = StringField()
4769+
4770+
class Doc(Document):
4771+
item = EmbeddedDocumentField(EmbeddedItem)
4772+
4773+
Doc.drop_collection()
4774+
4775+
doc = Doc(item=EmbeddedItem(type="axe"))
4776+
doc.save()
4777+
4778+
self.assertEqual(1, Doc.objects(item__type__="axe").count())
4779+
47664780
if __name__ == '__main__':
47674781
unittest.main()

tests/test_connection.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,10 @@ def test_connect(self):
5454
def test_sharing_connections(self):
5555
"""Ensure that connections are shared when the connection settings are exactly the same
5656
"""
57-
connect('mongoenginetest', alias='testdb1')
58-
57+
connect('mongoenginetests', alias='testdb1')
5958
expected_connection = get_connection('testdb1')
6059

61-
connect('mongoenginetest', alias='testdb2')
60+
connect('mongoenginetests', alias='testdb2')
6261
actual_connection = get_connection('testdb2')
6362

6463
# Handle PyMongo 3+ Async Connection
@@ -96,8 +95,7 @@ def test_connect_uri(self):
9695
c.mongoenginetest.system.users.remove({})
9796

9897
def test_connect_uri_without_db(self):
99-
"""Ensure that the connect() method works properly with uri's
100-
without database_name
98+
"""Ensure connect() method works properly with uri's without database_name
10199
"""
102100
c = connect(db='mongoenginetest', alias='admin')
103101
c.admin.system.users.remove({})
@@ -130,28 +128,27 @@ def test_connect_uri_with_authsource(self):
130128
# Create users
131129
c = connect('mongoenginetest')
132130
c.admin.system.users.remove({})
133-
c.admin.add_user('username', 'password')
131+
c.admin.add_user('username2', 'password')
134132

135133
# Authentication fails without "authSource"
136134
if IS_PYMONGO_3:
137-
test_conn = connect('mongoenginetest', alias='test2',
138-
host='mongodb://username:password@localhost/mongoenginetest')
135+
test_conn = connect('mongoenginetest', alias='test1',
136+
host='mongodb://username2:password@localhost/mongoenginetest')
139137
self.assertRaises(OperationFailure, test_conn.server_info)
140138
else:
141139
self.assertRaises(
142140
ConnectionError, connect, 'mongoenginetest', alias='test1',
143-
host='mongodb://username:password@localhost/mongoenginetest'
141+
host='mongodb://username2:password@localhost/mongoenginetest'
144142
)
145143
self.assertRaises(ConnectionError, get_db, 'test1')
146144

147145
# Authentication succeeds with "authSource"
148146
test_conn2 = connect(
149147
'mongoenginetest', alias='test2',
150-
host=('mongodb://username:password@localhost/'
148+
host=('mongodb://username2:password@localhost/'
151149
'mongoenginetest?authSource=admin')
152150
)
153151
# This will fail starting from MongoDB 2.6+
154-
# test_conn2.server_info()
155152
db = get_db('test2')
156153
self.assertTrue(isinstance(db, pymongo.database.Database))
157154
self.assertEqual(db.name, 'mongoenginetest')

0 commit comments

Comments
 (0)