Merge remote branch 'hmarr/v0.4'
Conflicts: mongoengine/fields.py tests/fields.py
This commit is contained in:
commit
323c86308a
@ -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,21 +241,22 @@ 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
|
||||||
for index in self._document._meta['unique_indexes']:
|
for index in self._document._meta['unique_indexes']:
|
||||||
self._collection.ensure_index(index, unique=True)
|
self._collection.ensure_index(index, unique=True)
|
||||||
|
|
||||||
# If _types is being used (for polymorphism), it needs an index
|
# If _types is being used (for polymorphism), it needs an index
|
||||||
if '_types' in self._query:
|
if '_types' in self._query:
|
||||||
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,9 +312,10 @@ 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']
|
||||||
match_operators = ['contains', 'icontains', 'startswith',
|
geo_operators = ['within_distance', 'within_box', 'near']
|
||||||
'istartswith', 'endswith', 'iendswith',
|
match_operators = ['contains', 'icontains', 'startswith',
|
||||||
|
'istartswith', 'endswith', 'iendswith',
|
||||||
'exact', 'iexact']
|
'exact', 'iexact']
|
||||||
|
|
||||||
mongo_query = {}
|
mongo_query = {}
|
||||||
@ -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,15 +337,27 @@ 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:
|
||||||
value = {'$' + op: value}
|
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}
|
||||||
|
|
||||||
key = '.'.join(parts)
|
key = '.'.join(parts)
|
||||||
if op is None or key not in mongo_query:
|
if op is None or key not in mongo_query:
|
||||||
|
@ -674,7 +674,24 @@ class FieldTest(unittest.TestCase):
|
|||||||
PutFile.drop_collection()
|
PutFile.drop_collection()
|
||||||
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):
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user