Repository: MongoEngine/mongoengine

Issue:
How can I set a min_distance in a geo query?
I see this https://github.com/MongoEngine/mongoengine/blob/4814066c67c3d97acb8e15aa6ef1ca374b6d9aa3/mongoengine/queryset/transform.py#L317

It is only max_distance supported. How can I make it works like set min_distance? 



Discussion:

Resolving patch:
diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py
--- a/mongoengine/queryset/transform.py
+++ b/mongoengine/queryset/transform.py
@@ -7,6 +7,7 @@
 from mongoengine.connection import get_connection
 from mongoengine.common import _import_class
 from mongoengine.errors import InvalidQueryError
+from mongoengine.python_support import IS_PYMONGO_3
 
 __all__ = ('query', 'update')
 
@@ -15,7 +16,7 @@
                         'all', 'size', 'exists', 'not', 'elemMatch', 'type')
 GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
                  'within_box', 'within_polygon', 'near', 'near_sphere',
-                 'max_distance', 'geo_within', 'geo_within_box',
+                 'max_distance', 'min_distance', 'geo_within', 'geo_within_box',
                  'geo_within_polygon', 'geo_within_center',
                  'geo_within_sphere', 'geo_intersects')
 STRING_OPERATORS = ('contains', 'icontains', 'startswith',
@@ -44,8 +45,8 @@ def query(_doc_cls=None, _field_operation=False, **query):
         if len(parts) > 1 and parts[-1] in MATCH_OPERATORS:
             op = parts.pop()
 
-        # if user escape field name by __
-        if len(parts) > 1 and parts[-1] == "":
+        #if user escape field name by __
+        if len(parts) > 1 and parts[-1]=="":
             parts.pop()
 
         negate = False
@@ -126,24 +127,34 @@ def query(_doc_cls=None, _field_operation=False, **query):
         elif key in mongo_query:
             if key in mongo_query and isinstance(mongo_query[key], dict):
                 mongo_query[key].update(value)
-                # $maxDistance needs to come last - convert to SON
+                # $max/minDistance needs to come last - convert to SON
                 value_dict = mongo_query[key]
-                if '$maxDistance' in value_dict and '$near' in value_dict:
+                if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and \
+                        ('$near' in value_dict or '$nearSphere' in value_dict):
                     value_son = SON()
-                    if isinstance(value_dict['$near'], dict):
-                        for k, v in value_dict.iteritems():
-                            if k == '$maxDistance':
-                                continue
-                            value_son[k] = v
-                        value_son['$near'] = SON(value_son['$near'])
-                        value_son['$near']['$maxDistance'] = value_dict['$maxDistance']
-                    else:
-                        for k, v in value_dict.iteritems():
-                            if k == '$maxDistance':
-                                continue
-                            value_son[k] = v
-                        value_son['$maxDistance'] = value_dict['$maxDistance']
-
+                    for k, v in value_dict.iteritems():
+                        if k == '$maxDistance' or k == '$minDistance':
+                            continue
+                        value_son[k] = v
+                    # Required for MongoDB >= 2.6, may fail when combining
+                    # PyMongo 3+ and MongoDB < 2.6
+                    near_embedded = False
+                    for near_op in ('$near', '$nearSphere'):
+                        if isinstance(value_dict.get(near_op), dict) and (
+                                IS_PYMONGO_3 or get_connection().max_wire_version > 1):
+                            value_son[near_op] = SON(value_son[near_op])
+                            if '$maxDistance' in value_dict:
+                                value_son[near_op][
+                                    '$maxDistance'] = value_dict['$maxDistance']
+                            if '$minDistance' in value_dict:
+                                value_son[near_op][
+                                    '$minDistance'] = value_dict['$minDistance']
+                            near_embedded = True
+                    if not near_embedded:
+                        if '$maxDistance' in value_dict:
+                            value_son['$maxDistance'] = value_dict['$maxDistance']
+                        if '$minDistance' in value_dict:
+                            value_son['$minDistance'] = value_dict['$minDistance']
                     mongo_query[key] = value_son
             else:
                 # Store for manually merging later
@@ -297,7 +308,11 @@ def update(_doc_cls=None, **update):
 
 def _geo_operator(field, op, value):
     """Helper to return the query for a given geo query"""
-    if field._geo_index == pymongo.GEO2D:
+    if op == "max_distance":
+        value = {'$maxDistance': value}
+    elif op == "min_distance":
+        value = {'$minDistance': value}
+    elif field._geo_index == pymongo.GEO2D:
         if op == "within_distance":
             value = {'$within': {'$center': value}}
         elif op == "within_spherical_distance":
@@ -310,8 +325,6 @@ def _geo_operator(field, op, value):
             value = {'$nearSphere': value}
         elif op == 'within_box':
             value = {'$within': {'$box': value}}
-        elif op == "max_distance":
-            value = {'$maxDistance': value}
         else:
             raise NotImplementedError("Geo method '%s' has not "
                                       "been implemented for a GeoPointField" % op)
@@ -330,8 +343,6 @@ def _geo_operator(field, op, value):
             value = {"$geoIntersects": _infer_geometry(value)}
         elif op == "near":
             value = {'$near': _infer_geometry(value)}
-        elif op == "max_distance":
-            value = {'$maxDistance': value}
         else:
             raise NotImplementedError("Geo method '%s' has not "
                                       "been implemented for a %s " % (op, field._name))


Test patch:
diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py
--- a/tests/queryset/geo.py
+++ b/tests/queryset/geo.py
@@ -70,6 +70,16 @@ def __unicode__(self):
         self.assertEqual(events.count(), 1)
         self.assertEqual(events[0], event2)
 
+        # find events at least 10 degrees away of san francisco
+        point = [-122.415579, 37.7566023]
+        events = Event.objects(location__near=point, location__min_distance=10)
+        # The following real test passes on MongoDB 3 but minDistance seems
+        # buggy on older MongoDB versions
+        if get_connection().server_info()['versionArray'][0] > 2:
+            self.assertEqual(events.count(), 2)
+        else:
+            self.assertTrue(events.count() >= 2)
+
         # find events within 10 degrees of san francisco
         point_and_distance = [[-122.415579, 37.7566023], 10]
         events = Event.objects(location__within_distance=point_and_distance)
@@ -171,21 +181,32 @@ class Point(Document):
 
         # Same behavior for _within_spherical_distance
         points = Point.objects(
-            location__within_spherical_distance=[[-122, 37.5], 60/earth_radius]
+            location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius]
         )
         self.assertEqual(points.count(), 2)
 
         points = Point.objects(location__near_sphere=[-122, 37.5],
                                location__max_distance=60 / earth_radius)
-        # This test is sometimes failing with Mongo internals non-sense.
-        # See https://travis-ci.org/MongoEngine/mongoengine/builds/58729101
-        try:
-            points.count()
-        except OperationFailure:
-            raise SkipTest("Sometimes MongoDB ignores its capacities on maxDistance")
-
         self.assertEqual(points.count(), 2)
 
+        # Test query works with max_distance, being farer from one point
+        points = Point.objects(location__near_sphere=[-122, 37.8],
+                               location__max_distance=60 / earth_radius)
+        close_point = points.first()
+        self.assertEqual(points.count(), 1)
+
+        # Test query works with min_distance, being farer from one point
+        points = Point.objects(location__near_sphere=[-122, 37.8],
+                               location__min_distance=60 / earth_radius)
+        # The following real test passes on MongoDB 3 but minDistance seems
+        # buggy on older MongoDB versions
+        if get_connection().server_info()['versionArray'][0] > 2:
+            self.assertEqual(points.count(), 1)
+            far_point = points.first()
+            self.assertNotEqual(close_point, far_point)
+        else:
+            self.assertTrue(points.count() >= 1)
+
         # Finds both points, but orders the north point first because it's
         # closer to the reference point to the north.
         points = Point.objects(location__near_sphere=[-122, 38.5])
@@ -268,6 +289,20 @@ def __unicode__(self):
         self.assertEqual(events.count(), 2)
         self.assertEqual(events[0], event3)
 
+        # ensure min_distance and max_distance combine well
+        events = Event.objects(location__near=[-87.67892, 41.9120459],
+                               location__min_distance=1000,
+                               location__max_distance=10000).order_by("-date")
+        self.assertEqual(events.count(), 1)
+        self.assertEqual(events[0], event3)
+
+        # ensure ordering is respected by "near"
+        events = Event.objects(location__near=[-87.67892, 41.9120459],
+                               # location__min_distance=10000
+                               location__min_distance=10000).order_by("-date")
+        self.assertEqual(events.count(), 1)
+        self.assertEqual(events[0], event2)
+
         # check that within_box works
         box = [(-125.0, 35.0), (-100.0, 40.0)]
         events = Event.objects(location__geo_within_box=box)
diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py
--- a/tests/queryset/queryset.py
+++ b/tests/queryset/queryset.py
@@ -4763,5 +4763,19 @@ class Person(Document):
         for p in Person.objects():
             self.assertEqual(p.name, 'a')
 
+    def test_last_field_name_like_operator(self):
+        class EmbeddedItem(EmbeddedDocument):
+            type = StringField()
+
+        class Doc(Document):
+            item = EmbeddedDocumentField(EmbeddedItem)
+
+        Doc.drop_collection()
+
+        doc = Doc(item=EmbeddedItem(type="axe"))
+        doc.save()
+
+        self.assertEqual(1, Doc.objects(item__type__="axe").count())
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_connection.py b/tests/test_connection.py
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -54,11 +54,10 @@ def test_connect(self):
     def test_sharing_connections(self):
         """Ensure that connections are shared when the connection settings are exactly the same
         """
-        connect('mongoenginetest', alias='testdb1')
-
+        connect('mongoenginetests', alias='testdb1')
         expected_connection = get_connection('testdb1')
 
-        connect('mongoenginetest', alias='testdb2')
+        connect('mongoenginetests', alias='testdb2')
         actual_connection = get_connection('testdb2')
 
         # Handle PyMongo 3+ Async Connection
@@ -96,8 +95,7 @@ def test_connect_uri(self):
         c.mongoenginetest.system.users.remove({})
 
     def test_connect_uri_without_db(self):
-        """Ensure that the connect() method works properly with uri's
-        without database_name
+        """Ensure connect() method works properly with uri's without database_name
         """
         c = connect(db='mongoenginetest', alias='admin')
         c.admin.system.users.remove({})
@@ -130,28 +128,27 @@ def test_connect_uri_with_authsource(self):
         # Create users
         c = connect('mongoenginetest')
         c.admin.system.users.remove({})
-        c.admin.add_user('username', 'password')
+        c.admin.add_user('username2', 'password')
 
         # Authentication fails without "authSource"
         if IS_PYMONGO_3:
-            test_conn = connect('mongoenginetest', alias='test2',
-                                host='mongodb://username:password@localhost/mongoenginetest')
+            test_conn = connect('mongoenginetest', alias='test1',
+                                host='mongodb://username2:password@localhost/mongoenginetest')
             self.assertRaises(OperationFailure, test_conn.server_info)
         else:
             self.assertRaises(
                 ConnectionError, connect, 'mongoenginetest', alias='test1',
-                host='mongodb://username:password@localhost/mongoenginetest'
+                host='mongodb://username2:password@localhost/mongoenginetest'
             )
             self.assertRaises(ConnectionError, get_db, 'test1')
 
         # Authentication succeeds with "authSource"
         test_conn2 = connect(
             'mongoenginetest', alias='test2',
-            host=('mongodb://username:password@localhost/'
+            host=('mongodb://username2:password@localhost/'
                   'mongoenginetest?authSource=admin')
         )
         # This will fail starting from MongoDB 2.6+
-        # test_conn2.server_info()
         db = get_db('test2')
         self.assertTrue(isinstance(db, pymongo.database.Database))
         self.assertEqual(db.name, 'mongoenginetest')