From 5efabdcea39922c1723d7978c35bc603975c3f8d Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 03:03:50 +0200 Subject: [PATCH] Added tests, documentation and simplified code --- docs/changelog.rst | 1 + docs/guide/querying.rst | 6 ++-- mongoengine/queryset/transform.py | 50 +++++++++++++------------------ tests/queryset/geo.py | 31 ++++++++++++++++++- tests/queryset/queryset.py | 14 +++++++++ 5 files changed, 70 insertions(+), 32 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0dcd1e08..ac683750 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,7 @@ Changes in 0.9.X - DEV - Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods. - Fix for delete with write_concern {'w': 0}. #1008 - Allow dynamic lookup for more than two parts. #882 +- Added support for min_distance on geo queries. #831 Changes in 0.9.0 ================ diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 1cde82cb..688fc9e9 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -146,9 +146,10 @@ The following were added in MongoEngine 0.8 for loc.objects(point__near=[40, 5]) loc.objects(point__near={"type": "Point", "coordinates": [40, 5]}) - You can also set the maximum distance in meters as well:: + You can also set the maximum and/or the minimum distance in meters as well:: loc.objects(point__near=[40, 5], point__max_distance=1000) + loc.objects(point__near=[40, 5], point__min_distance=100) The older 2D indexes are still supported with the :class:`~mongoengine.fields.GeoPointField`: @@ -168,7 +169,8 @@ The older 2D indexes are still supported with the * ``max_distance`` -- can be added to your location queries to set a maximum distance. - +* ``min_distance`` -- can be added to your location queries to set a minimum + distance. Querying lists -------------- diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 637e1b4b..91915703 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -6,7 +6,7 @@ from bson import SON from mongoengine.base.fields import UPDATE_OPERATORS from mongoengine.connection import get_connection from mongoengine.common import _import_class -from mongoengine.errors import InvalidQueryError +from mongoengine.errors import InvalidQueryError, LookUpError __all__ = ('query', 'update') @@ -44,8 +44,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,32 +126,28 @@ 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: value_son = SON() - if isinstance(value_dict['$near'], dict): - for k, v in value_dict.iteritems(): - if k == '$maxDistance' or k == '$minDistance': - continue - value_son[k] = v - if (get_connection().max_wire_version <= 1): - value_son['$maxDistance'] = value_dict[ - '$maxDistance'] - else: - value_son['$near'] = SON(value_son['$near']) + for k, v in value_dict.iteritems(): + if k == '$maxDistance' or k == '$minDistance': + continue + value_son[k] = v + if isinstance(value_dict['$near'], dict) and\ + get_connection().max_wire_version > 1: + value_son['$near'] = SON(value_son['$near']) + if '$maxDistance' in value_dict: value_son['$near'][ '$maxDistance'] = value_dict['$maxDistance'] + if '$minDistance' in value_dict: + value_son['$near'][ + '$minDistance'] = value_dict['$minDistance'] else: - for k, v in value_dict.iteritems(): - if k == '$maxDistance' or k == '$minDistance': - continue - value_son[k] = v 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 @@ -305,7 +301,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": @@ -318,10 +318,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} - elif op == "min_distance": - value = {'$minDistance': value} else: raise NotImplementedError("Geo method '%s' has not " "been implemented for a GeoPointField" % op) @@ -340,10 +336,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} - elif op == "min_distance": - value = {'$minDistance': value} else: raise NotImplementedError("Geo method '%s' has not " "been implemented for a %s " % (op, field._name)) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 12e96a04..889dac38 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -70,6 +70,11 @@ class GeoQueriesTest(unittest.TestCase): 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) + self.assertEqual(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,7 +176,7 @@ class GeoQueriesTest(unittest.TestCase): # 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) @@ -186,6 +191,16 @@ class GeoQueriesTest(unittest.TestCase): 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__min_distance=60 / earth_radius) + 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) + self.assertEqual(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 +283,20 @@ class GeoQueriesTest(unittest.TestCase): 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 index aaa14ca0..4f00e1c6 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4763,5 +4763,19 @@ class QuerySetTest(unittest.TestCase): 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()