Merge remote branch 'hmarr/v0.4'
Conflicts: mongoengine/fields.py tests/fields.py
This commit is contained in:
		| @@ -2,6 +2,20 @@ | |||||||
| Changelog | Changelog | ||||||
| ========= | ========= | ||||||
|  |  | ||||||
|  | Changes in v0.4 | ||||||
|  | =============== | ||||||
|  | - Added ``SortedListField`` | ||||||
|  | - Added ``EmailField`` | ||||||
|  | - Added ``GeoPointField`` | ||||||
|  | - Added ``exact`` and ``iexact`` match operators to ``QuerySet`` | ||||||
|  | - Added ``get_document_or_404`` and ``get_list_or_404`` Django shortcuts | ||||||
|  | - Fixed bug in Q-objects | ||||||
|  | - Fixed document inheritance primary key issue | ||||||
|  | - Base class can now be defined for ``DictField`` | ||||||
|  | - Fixed MRO error that occured on document inheritance | ||||||
|  | - Introduced ``min_length`` for ``StringField`` | ||||||
|  | - Other minor fixes | ||||||
|  |  | ||||||
| Changes in v0.3 | Changes in v0.3 | ||||||
| =============== | =============== | ||||||
| - Added MapReduce support | - Added MapReduce support | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ class BaseField(object): | |||||||
|  |  | ||||||
|     # Fields may have _types inserted into indexes by default  |     # Fields may have _types inserted into indexes by default  | ||||||
|     _index_with_types = True |     _index_with_types = True | ||||||
|  |     _geo_index = False | ||||||
|      |      | ||||||
|     def __init__(self, db_field=None, name=None, required=False, default=None,  |     def __init__(self, db_field=None, name=None, required=False, default=None,  | ||||||
|                  unique=False, unique_with=None, primary_key=False, |                  unique=False, unique_with=None, primary_key=False, | ||||||
|   | |||||||
| @@ -16,11 +16,10 @@ __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField', | |||||||
|            'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField', |            'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField', | ||||||
|            'ObjectIdField', 'ReferenceField', 'ValidationError', |            'ObjectIdField', 'ReferenceField', 'ValidationError', | ||||||
|            'DecimalField', 'URLField', 'GenericReferenceField', 'FileField', |            'DecimalField', 'URLField', 'GenericReferenceField', 'FileField', | ||||||
|            'BinaryField', 'SortedListField', 'EmailField', 'GeoLocationField'] |            'BinaryField', 'SortedListField', 'EmailField', 'GeoPointField'] | ||||||
|  |  | ||||||
| RECURSIVE_REFERENCE_CONSTANT = 'self' | RECURSIVE_REFERENCE_CONSTANT = 'self' | ||||||
|  |  | ||||||
|  |  | ||||||
| class StringField(BaseField): | class StringField(BaseField): | ||||||
|     """A unicode string field. |     """A unicode string field. | ||||||
|     """ |     """ | ||||||
| @@ -372,24 +371,6 @@ class DictField(BaseField): | |||||||
|     def lookup_member(self, member_name): |     def lookup_member(self, member_name): | ||||||
|         return self.basecls(db_field=member_name) |         return self.basecls(db_field=member_name) | ||||||
|  |  | ||||||
| class GeoLocationField(DictField): |  | ||||||
|     """Supports geobased fields""" |  | ||||||
|      |  | ||||||
|     def validate(self, value): |  | ||||||
|         """Make sure that a geo-value is of type (x, y) |  | ||||||
|         """ |  | ||||||
|         if not isinstance(value, tuple) and not isinstance(value, list): |  | ||||||
|             raise ValidationError('GeoLocationField can only hold tuples or lists of (x, y)') |  | ||||||
|          |  | ||||||
|         if len(value) <> 2: |  | ||||||
|             raise ValidationError('GeoLocationField must have exactly two elements (x, y)') |  | ||||||
|      |  | ||||||
|     def to_mongo(self, value): |  | ||||||
|         return {'x': value[0], 'y': value[1]} |  | ||||||
|      |  | ||||||
|     def to_python(self, value): |  | ||||||
|         return (value['x'], value['y']) |  | ||||||
|  |  | ||||||
| class ReferenceField(BaseField): | class ReferenceField(BaseField): | ||||||
|     """A reference to a document that will be automatically dereferenced on |     """A reference to a document that will be automatically dereferenced on | ||||||
|     access (lazily). |     access (lazily). | ||||||
| @@ -456,7 +437,6 @@ class ReferenceField(BaseField): | |||||||
|     def lookup_member(self, member_name): |     def lookup_member(self, member_name): | ||||||
|         return self.document_type._fields.get(member_name) |         return self.document_type._fields.get(member_name) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GenericReferenceField(BaseField): | class GenericReferenceField(BaseField): | ||||||
|     """A reference to *any* :class:`~mongoengine.document.Document` subclass |     """A reference to *any* :class:`~mongoengine.document.Document` subclass | ||||||
|     that will be automatically dereferenced on access (lazily). |     that will be automatically dereferenced on access (lazily). | ||||||
| @@ -503,6 +483,7 @@ class GenericReferenceField(BaseField): | |||||||
|     def prepare_query_value(self, op, value): |     def prepare_query_value(self, op, value): | ||||||
|         return self.to_mongo(value)['_ref'] |         return self.to_mongo(value)['_ref'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class BinaryField(BaseField): | class BinaryField(BaseField): | ||||||
|     """A binary data field. |     """A binary data field. | ||||||
|     """ |     """ | ||||||
| @@ -617,3 +598,21 @@ class FileField(BaseField): | |||||||
|         assert isinstance(value, GridFSProxy) |         assert isinstance(value, GridFSProxy) | ||||||
|         assert isinstance(value.grid_id, pymongo.objectid.ObjectId) |         assert isinstance(value.grid_id, pymongo.objectid.ObjectId) | ||||||
|  |  | ||||||
|  | class GeoPointField(BaseField): | ||||||
|  |     """A list storing a latitude and longitude. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     _geo_index = True | ||||||
|  |  | ||||||
|  |     def validate(self, value): | ||||||
|  |         """Make sure that a geo-value is of type (x, y) | ||||||
|  |         """ | ||||||
|  |         if not isinstance(value, (list, tuple)): | ||||||
|  |             raise ValidationError('GeoPointField can only accept tuples or ' | ||||||
|  |                                   'lists of (x, y)') | ||||||
|  |          | ||||||
|  |         if not len(value) == 2: | ||||||
|  |             raise ValidationError('Value must be a two-dimensional point.') | ||||||
|  |         if (not isinstance(value[0], (float, int)) and | ||||||
|  |             not isinstance(value[1], (float, int))): | ||||||
|  |             raise ValidationError('Both values in point must be float or int.') | ||||||
| @@ -241,7 +241,6 @@ class QuerySet(object): | |||||||
|             # Ensure document-defined indexes are created |             # Ensure document-defined indexes are created | ||||||
|             if self._document._meta['indexes']: |             if self._document._meta['indexes']: | ||||||
|                 for key_or_list in self._document._meta['indexes']: |                 for key_or_list in self._document._meta['indexes']: | ||||||
|                     #self.ensure_index(key_or_list) |  | ||||||
|                     self._collection.ensure_index(key_or_list) |                     self._collection.ensure_index(key_or_list) | ||||||
|  |  | ||||||
|             # Ensure indexes created by uniqueness constraints |             # Ensure indexes created by uniqueness constraints | ||||||
| @@ -253,9 +252,11 @@ class QuerySet(object): | |||||||
|                 self._collection.ensure_index('_types') |                 self._collection.ensure_index('_types') | ||||||
|              |              | ||||||
|             # Ensure all needed field indexes are created |             # Ensure all needed field indexes are created | ||||||
|             for field_name, field_instance in self._document._fields.iteritems(): |             for field in self._document._fields.values(): | ||||||
|                 if field_instance.__class__.__name__ == 'GeoLocationField': |                 if field.__class__._geo_index: | ||||||
|                     self._collection.ensure_index([(field_name, pymongo.GEO2D),]) |                     index_spec = [(field.db_field, pymongo.GEO2D)] | ||||||
|  |                     self._collection.ensure_index(index_spec) | ||||||
|  |  | ||||||
|         return self._collection_obj |         return self._collection_obj | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -311,7 +312,8 @@ class QuerySet(object): | |||||||
|         """Transform a query from Django-style format to Mongo format. |         """Transform a query from Django-style format to Mongo format. | ||||||
|         """ |         """ | ||||||
|         operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', |         operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', | ||||||
|                      'all', 'size', 'exists', 'near'] |                      'all', 'size', 'exists'] | ||||||
|  |         geo_operators = ['within_distance', 'within_box', 'near'] | ||||||
|         match_operators = ['contains', 'icontains', 'startswith',  |         match_operators = ['contains', 'icontains', 'startswith',  | ||||||
|                            'istartswith', 'endswith', 'iendswith',  |                            'istartswith', 'endswith', 'iendswith',  | ||||||
|                            'exact', 'iexact'] |                            'exact', 'iexact'] | ||||||
| @@ -321,7 +323,7 @@ class QuerySet(object): | |||||||
|             parts = key.split('__') |             parts = key.split('__') | ||||||
|             # Check for an operator and transform to mongo-style if there is |             # Check for an operator and transform to mongo-style if there is | ||||||
|             op = None |             op = None | ||||||
|             if parts[-1] in operators + match_operators: |             if parts[-1] in operators + match_operators + geo_operators: | ||||||
|                 op = parts.pop() |                 op = parts.pop() | ||||||
|  |  | ||||||
|             if _doc_cls: |             if _doc_cls: | ||||||
| @@ -335,14 +337,26 @@ class QuerySet(object): | |||||||
|                 singular_ops += match_operators |                 singular_ops += match_operators | ||||||
|                 if op in singular_ops: |                 if op in singular_ops: | ||||||
|                     value = field.prepare_query_value(op, value) |                     value = field.prepare_query_value(op, value) | ||||||
|                 elif op in ('in', 'nin', 'all'): |                 elif op in ('in', 'nin', 'all', 'near'): | ||||||
|                     # 'in', 'nin' and 'all' require a list of values |                     # 'in', 'nin' and 'all' require a list of values | ||||||
|                     value = [field.prepare_query_value(op, v) for v in value] |                     value = [field.prepare_query_value(op, v) for v in value] | ||||||
|  |  | ||||||
|                 if field.__class__.__name__ == 'GenericReferenceField': |                 if field.__class__.__name__ == 'GenericReferenceField': | ||||||
|                     parts.append('_ref') |                     parts.append('_ref') | ||||||
|  |  | ||||||
|             if op and op not in match_operators: |             # if op and op not in match_operators: | ||||||
|  |             if op: | ||||||
|  |                 if op in geo_operators: | ||||||
|  |                     if op == "within_distance": | ||||||
|  |                         value = {'$within': {'$center': value}} | ||||||
|  |                     elif op == "near": | ||||||
|  |                         value = {'$near': value} | ||||||
|  |                     elif op == 'within_box': | ||||||
|  |                         value = {'$within': {'$box': value}} | ||||||
|  |                     else: | ||||||
|  |                         raise NotImplementedError("Geo method '%s' has not " | ||||||
|  |                                                   "been implemented" % op) | ||||||
|  |                 elif op not in match_operators: | ||||||
|                     value = {'$' + op: value} |                     value = {'$' + op: value} | ||||||
|  |  | ||||||
|             key = '.'.join(parts) |             key = '.'.join(parts) | ||||||
|   | |||||||
| @@ -675,6 +675,23 @@ class FieldTest(unittest.TestCase): | |||||||
|         StreamFile.drop_collection() |         StreamFile.drop_collection() | ||||||
|         SetFile.drop_collection() |         SetFile.drop_collection() | ||||||
|          |          | ||||||
|  |     def test_geo_indexes(self): | ||||||
|  |         """Ensure that indexes are created automatically for GeoPointFields. | ||||||
|  |         """ | ||||||
|  |         class Event(Document): | ||||||
|  |             title = StringField() | ||||||
|  |             location = GeoPointField() | ||||||
|  |  | ||||||
|  |         Event.drop_collection() | ||||||
|  |         event = Event(title="Coltrane Motion @ Double Door", | ||||||
|  |                       location=[41.909889, -87.677137]) | ||||||
|  |         event.save() | ||||||
|  |  | ||||||
|  |         info = Event.objects._collection.index_information() | ||||||
|  |         self.assertTrue(u'location_2d' in info) | ||||||
|  |         self.assertTrue(info[u'location_2d'] == [(u'location', u'2d')]) | ||||||
|  |  | ||||||
|  |         Event.drop_collection() | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
| @@ -1164,6 +1164,81 @@ class QuerySetTest(unittest.TestCase): | |||||||
|     def tearDown(self): |     def tearDown(self): | ||||||
|         self.Person.drop_collection() |         self.Person.drop_collection() | ||||||
|  |  | ||||||
|  |     def test_geospatial_operators(self): | ||||||
|  |         """Ensure that geospatial queries are working. | ||||||
|  |         """ | ||||||
|  |         class Event(Document): | ||||||
|  |             title = StringField() | ||||||
|  |             date = DateTimeField() | ||||||
|  |             location = GeoPointField() | ||||||
|  |              | ||||||
|  |             def __unicode__(self): | ||||||
|  |                 return self.title | ||||||
|  |              | ||||||
|  |         Event.drop_collection() | ||||||
|  |          | ||||||
|  |         event1 = Event(title="Coltrane Motion @ Double Door", | ||||||
|  |                        date=datetime.now() - timedelta(days=1), | ||||||
|  |                        location=[41.909889, -87.677137]) | ||||||
|  |         event2 = Event(title="Coltrane Motion @ Bottom of the Hill", | ||||||
|  |                        date=datetime.now() - timedelta(days=10), | ||||||
|  |                        location=[37.7749295, -122.4194155]) | ||||||
|  |         event3 = Event(title="Coltrane Motion @ Empty Bottle", | ||||||
|  |                        date=datetime.now(), | ||||||
|  |                        location=[41.900474, -87.686638]) | ||||||
|  |                         | ||||||
|  |         event1.save() | ||||||
|  |         event2.save() | ||||||
|  |         event3.save() | ||||||
|  |  | ||||||
|  |         # find all events "near" pitchfork office, chicago. | ||||||
|  |         # note that "near" will show the san francisco event, too, | ||||||
|  |         # although it sorts to last. | ||||||
|  |         events = Event.objects(location__near=[41.9120459, -87.67892]) | ||||||
|  |         self.assertEqual(events.count(), 3) | ||||||
|  |         self.assertEqual(list(events), [event1, event3, event2]) | ||||||
|  |  | ||||||
|  |         # find events within 5 miles of pitchfork office, chicago | ||||||
|  |         point_and_distance = [[41.9120459, -87.67892], 5] | ||||||
|  |         events = Event.objects(location__within_distance=point_and_distance) | ||||||
|  |         self.assertEqual(events.count(), 2) | ||||||
|  |         events = list(events) | ||||||
|  |         self.assertTrue(event2 not in events) | ||||||
|  |         self.assertTrue(event1 in events) | ||||||
|  |         self.assertTrue(event3 in events) | ||||||
|  |          | ||||||
|  |         # ensure ordering is respected by "near" | ||||||
|  |         events = Event.objects(location__near=[41.9120459, -87.67892]) | ||||||
|  |         events = events.order_by("-date") | ||||||
|  |         self.assertEqual(events.count(), 3) | ||||||
|  |         self.assertEqual(list(events), [event3, event1, event2]) | ||||||
|  |          | ||||||
|  |         # find events around san francisco | ||||||
|  |         point_and_distance = [[37.7566023, -122.415579], 10] | ||||||
|  |         events = Event.objects(location__within_distance=point_and_distance) | ||||||
|  |         self.assertEqual(events.count(), 1) | ||||||
|  |         self.assertEqual(events[0], event2) | ||||||
|  |          | ||||||
|  |         # find events within 1 mile of greenpoint, broolyn, nyc, ny | ||||||
|  |         point_and_distance = [[40.7237134, -73.9509714], 1] | ||||||
|  |         events = Event.objects(location__within_distance=point_and_distance) | ||||||
|  |         self.assertEqual(events.count(), 0) | ||||||
|  |          | ||||||
|  |         # ensure ordering is respected by "within_distance" | ||||||
|  |         point_and_distance = [[41.9120459, -87.67892], 10] | ||||||
|  |         events = Event.objects(location__within_distance=point_and_distance) | ||||||
|  |         events = events.order_by("-date") | ||||||
|  |         self.assertEqual(events.count(), 2) | ||||||
|  |         self.assertEqual(events[0], event3) | ||||||
|  |  | ||||||
|  |         # check that within_box works | ||||||
|  |         box = [(35.0, -125.0), (40.0, -100.0)] | ||||||
|  |         events = Event.objects(location__within_box=box) | ||||||
|  |         self.assertEqual(events.count(), 1) | ||||||
|  |         self.assertEqual(events[0].id, event2.id) | ||||||
|  |          | ||||||
|  |         Event.drop_collection() | ||||||
|  |  | ||||||
|  |  | ||||||
| class QTest(unittest.TestCase): | class QTest(unittest.TestCase): | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user