Merge branch 'v0.4' of git://github.com/hmarr/mongoengine into v0.4

Conflicts:
	mongoengine/fields.py
	tests/fields.py
This commit is contained in:
Steve Challis
2010-08-31 00:25:10 +01:00
11 changed files with 449 additions and 74 deletions

View File

@@ -22,6 +22,7 @@ class BaseField(object):
# Fields may have _types inserted into indexes by default
_index_with_types = True
_geo_index = False
def __init__(self, db_field=None, name=None, required=False, default=None,
unique=False, unique_with=None, primary_key=False,
@@ -229,12 +230,18 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
id_field = None
base_indexes = []
base_meta = {}
# Subclassed documents inherit collection from superclass
for base in bases:
if hasattr(base, '_meta') and 'collection' in base._meta:
collection = base._meta['collection']
# Propagate index options.
for key in ('index_background', 'index_drop_dups', 'index_opts'):
if key in base._meta:
base_meta[key] = base._meta[key]
id_field = id_field or base._meta.get('id_field')
base_indexes += base._meta.get('indexes', [])
@@ -245,7 +252,11 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
'ordering': [], # default ordering applied at runtime
'indexes': [], # indexes to be ensured at runtime
'id_field': id_field,
'index_background': False,
'index_drop_dups': False,
'index_opts': {},
}
meta.update(base_meta)
# Apply document-defined meta options
meta.update(attrs.get('meta', {}))
@@ -254,7 +265,10 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
# Set up collection manager, needs the class to have fields so use
# DocumentMetaclass before instantiating CollectionManager object
new_class = super_new(cls, name, bases, attrs)
new_class.objects = QuerySetManager()
# Provide a default queryset unless one has been manually provided
if not hasattr(new_class, 'objects'):
new_class.objects = QuerySetManager()
user_indexes = [QuerySet._build_index_spec(new_class, spec)
for spec in meta['indexes']] + base_indexes
@@ -265,7 +279,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
# Generate a list of indexes needed by uniqueness constraints
if field.unique:
field.required = True
unique_fields = [field_name]
unique_fields = [field.db_field]
# Add any unique_with fields to the back of the index spec
if field.unique_with:
@@ -338,8 +352,8 @@ class BaseDocument(object):
try:
field._validate(value)
except (ValueError, AttributeError, AssertionError), e:
raise ValidationError('Invalid value for field of type "' +
field.__class__.__name__ + '"')
raise ValidationError('Invalid value for field of type "%s": %s'
% (field.__class__.__name__, value))
elif field.required:
raise ValidationError('Field "%s" is required' % field.name)
@@ -414,6 +428,8 @@ class BaseDocument(object):
self._meta.get('allow_inheritance', True) == False):
data['_cls'] = self._class_name
data['_types'] = self._superclasses.keys() + [self._class_name]
if data.has_key('_id') and not data['_id']:
del data['_id']
return data
@classmethod
@@ -445,7 +461,9 @@ class BaseDocument(object):
for field_name, field in cls._fields.items():
if field.db_field in data:
data[field_name] = field.to_python(data[field.db_field])
value = data[field.db_field]
data[field_name] = (value if value is None
else field.to_python(value))
obj = cls(**data)
obj._present_fields = present_fields

View File

@@ -32,6 +32,9 @@ class User(Document):
last_login = DateTimeField(default=datetime.datetime.now)
date_joined = DateTimeField(default=datetime.datetime.now)
def __unicode__(self):
return self.username
def get_full_name(self):
"""Returns the users first and last names, separated by a space.
"""

View File

@@ -0,0 +1,21 @@
#coding: utf-8
from django.test import TestCase
from django.conf import settings
from mongoengine import connect
class MongoTestCase(TestCase):
"""
TestCase class that clear the collection between the tests
"""
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
def __init__(self, methodName='runtest'):
self.db = connect(self.db_name)
super(MongoTestCase, self).__init__(methodName)
def _post_teardown(self):
super(MongoTestCase, self)._post_teardown()
for collection in self.db.collection_names():
if collection == 'system.indexes':
continue
self.db.drop_collection(collection)

View File

@@ -16,11 +16,14 @@ __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
'ObjectIdField', 'ReferenceField', 'ValidationError',
'DecimalField', 'URLField', 'GenericReferenceField', 'FileField',
<<<<<<< HEAD
'BinaryField', 'SortedListField', 'EmailField', 'GeoLocationField']
=======
'BinaryField', 'SortedListField', 'EmailField', 'GeoPointField']
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
RECURSIVE_REFERENCE_CONSTANT = 'self'
class StringField(BaseField):
"""A unicode string field.
"""
@@ -67,6 +70,9 @@ class StringField(BaseField):
regex = r'%s$'
elif op == 'exact':
regex = r'^%s$'
# escape unsafe characters which could lead to a re.error
value = re.escape(value)
value = re.compile(regex % value, flags)
return value
@@ -264,6 +270,7 @@ class ListField(BaseField):
raise ValidationError('Argument to ListField constructor must be '
'a valid field')
self.field = field
kwargs.setdefault('default', lambda: [])
super(ListField, self).__init__(**kwargs)
def __get__(self, instance, owner):
@@ -356,6 +363,7 @@ class DictField(BaseField):
def __init__(self, basecls=None, *args, **kwargs):
self.basecls = basecls or BaseField
assert issubclass(self.basecls, BaseField)
kwargs.setdefault('default', lambda: {})
super(DictField, self).__init__(*args, **kwargs)
def validate(self, value):
@@ -372,24 +380,6 @@ class DictField(BaseField):
def lookup_member(self, 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.keys()
class ReferenceField(BaseField):
"""A reference to a document that will be automatically dereferenced on
access (lazily).
@@ -456,7 +446,6 @@ class ReferenceField(BaseField):
def lookup_member(self, member_name):
return self.document_type._fields.get(member_name)
class GenericReferenceField(BaseField):
"""A reference to *any* :class:`~mongoengine.document.Document` subclass
that will be automatically dereferenced on access (lazily).
@@ -503,6 +492,7 @@ class GenericReferenceField(BaseField):
def prepare_query_value(self, op, value):
return self.to_mongo(value)['_ref']
class BinaryField(BaseField):
"""A binary data field.
"""
@@ -524,14 +514,25 @@ class BinaryField(BaseField):
if self.max_bytes is not None and len(value) > self.max_bytes:
raise ValidationError('Binary value is too long')
<<<<<<< HEAD
=======
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
class GridFSProxy(object):
"""Proxy object to handle writing and reading of files to and from GridFS
"""
<<<<<<< HEAD
def __init__(self):
self.fs = gridfs.GridFS(_get_db()) # Filesystem instance
self.newfile = None # Used for partial writes
self.grid_id = None # Store GridFS id for file
=======
def __init__(self, grid_id=None):
self.fs = gridfs.GridFS(_get_db()) # Filesystem instance
self.newfile = None # Used for partial writes
self.grid_id = grid_id # Store GridFS id for file
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
def __getattr__(self, name):
obj = self.get()
@@ -542,8 +543,17 @@ class GridFSProxy(object):
return self
def get(self, id=None):
<<<<<<< HEAD
try: return self.fs.get(id or self.grid_id)
except: return None # File has been deleted
=======
if id:
self.grid_id = id
try:
return self.fs.get(id or self.grid_id)
except:
return None # File has been deleted
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
def new_file(self, **kwargs):
self.newfile = self.fs.new_file(**kwargs)
@@ -565,8 +575,15 @@ class GridFSProxy(object):
self.newfile.writelines(lines)
def read(self):
<<<<<<< HEAD
try: return self.get().read()
except: return None
=======
try:
return self.get().read()
except:
return None
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
def delete(self):
# Delete file from GridFS, FileField still remains
@@ -584,29 +601,61 @@ class GridFSProxy(object):
msg = "The close() method is only necessary after calling write()"
warnings.warn(msg)
<<<<<<< HEAD
=======
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
class FileField(BaseField):
"""A GridFS storage field.
"""
def __init__(self, **kwargs):
<<<<<<< HEAD
self.gridfs = GridFSProxy()
=======
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
super(FileField, self).__init__(**kwargs)
def __get__(self, instance, owner):
if instance is None:
return self
<<<<<<< HEAD
return self.gridfs
=======
# Check if a file already exists for this model
grid_file = instance._data.get(self.name)
if grid_file:
return grid_file
return GridFSProxy()
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
def __set__(self, instance, value):
if isinstance(value, file) or isinstance(value, str):
# using "FileField() = file/string" notation
<<<<<<< HEAD
self.gridfs.put(value)
=======
grid_file = instance._data.get(self.name)
# If a file already exists, delete it
if grid_file:
try:
grid_file.delete()
except:
pass
# Create a new file with the new data
grid_file.put(value)
else:
# Create a new proxy object as we don't already have one
instance._data[self.name] = GridFSProxy()
instance._data[self.name].put(value)
>>>>>>> 32e66b29f44f3015be099851201241caee92054f
else:
instance._data[self.name] = value
def to_mongo(self, value):
# Store the GridFS file id in MongoDB
<<<<<<< HEAD
return self.gridfs.grid_id
def to_python(self, value):
@@ -617,3 +666,36 @@ class FileField(BaseField):
assert isinstance(value, GridFSProxy)
assert isinstance(value.grid_id, pymongo.objectid.ObjectId)
=======
if isinstance(value, GridFSProxy) and value.grid_id is not None:
return value.grid_id
return None
def to_python(self, value):
if value is not None:
return GridFSProxy(value)
def validate(self, value):
if value.grid_id is not None:
assert isinstance(value, GridFSProxy)
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.')
>>>>>>> 32e66b29f44f3015be099851201241caee92054f

View File

@@ -1,5 +1,6 @@
from connection import _get_db
import pprint
import pymongo
import re
import copy
@@ -114,13 +115,11 @@ class Q(object):
value, field_js = self._build_op_js(op, key, value, value_name)
js_scope[value_name] = value
js.append(field_js)
print ' && '.join(js)
return ' && '.join(js)
def _build_op_js(self, op, key, value, value_name):
"""Substitute the values in to the correct chunk of Javascript.
"""
print op, key, value, value_name
if isinstance(value, RE_TYPE):
# Regexes are handled specially
if op.strip('$') == 'ne':
@@ -134,6 +133,16 @@ class Q(object):
if isinstance(value, pymongo.objectid.ObjectId):
value = unicode(value)
# Handle DBRef
if isinstance(value, pymongo.dbref.DBRef):
op_js = '(this.%(field)s.$id == "%(id)s" &&'\
' this.%(field)s.$ref == "%(ref)s")' % {
'field': key,
'id': unicode(value.id),
'ref': unicode(value.collection)
}
value = None
# Perform the substitution
operation_js = op_js % {
'field': key,
@@ -163,7 +172,8 @@ class QuerySet(object):
self._limit = None
self._skip = None
def ensure_index(self, key_or_list):
def ensure_index(self, key_or_list, drop_dups=False, background=False,
**kwargs):
"""Ensure that the given indexes are in place.
:param key_or_list: a single index key or a list of index keys (to
@@ -171,7 +181,8 @@ class QuerySet(object):
or a **-** to determine the index ordering
"""
index_list = QuerySet._build_index_spec(self._document, key_or_list)
self._collection.ensure_index(index_list)
self._collection.ensure_index(index_list, drop_dups=drop_dups,
background=background)
return self
@classmethod
@@ -229,6 +240,10 @@ class QuerySet(object):
"""An alias of :meth:`~mongoengine.queryset.QuerySet.__call__`
"""
return self.__call__(*q_objs, **query)
def all(self):
"""Returns all documents."""
return self.__call__()
@property
def _collection(self):
@@ -237,25 +252,34 @@ class QuerySet(object):
"""
if not self._accessed_collection:
self._accessed_collection = True
background = self._document._meta.get('index_background', False)
drop_dups = self._document._meta.get('index_drop_dups', False)
index_opts = self._document._meta.get('index_options', {})
# Ensure document-defined indexes are created
if 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,
background=background, **index_opts)
# Ensure indexes created by uniqueness constraints
for index in self._document._meta['unique_indexes']:
self._collection.ensure_index(index, unique=True)
self._collection.ensure_index(index, unique=True,
background=background, drop_dups=drop_dups, **index_opts)
# If _types is being used (for polymorphism), it needs an index
if '_types' in self._query:
self._collection.ensure_index('_types')
self._collection.ensure_index('_types',
background=background, **index_opts)
# Ensure all needed field indexes are created
for field_name, field_instance in self._document._fields.iteritems():
if field_instance.__class__.__name__ == 'GeoLocationField':
self._collection.ensure_index([(field_name, pymongo.GEO2D),])
for field in self._document._fields.values():
if field.__class__._geo_index:
index_spec = [(field.db_field, pymongo.GEO2D)]
self._collection.ensure_index(index_spec,
background=background, **index_opts)
return self._collection_obj
@property
@@ -311,9 +335,10 @@ class QuerySet(object):
"""Transform a query from Django-style format to Mongo format.
"""
operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
'all', 'size', 'exists', 'near']
match_operators = ['contains', 'icontains', 'startswith',
'istartswith', 'endswith', 'iendswith',
'all', 'size', 'exists']
geo_operators = ['within_distance', 'within_box', 'near']
match_operators = ['contains', 'icontains', 'startswith',
'istartswith', 'endswith', 'iendswith',
'exact', 'iexact']
mongo_query = {}
@@ -321,7 +346,7 @@ class QuerySet(object):
parts = key.split('__')
# Check for an operator and transform to mongo-style if there is
op = None
if parts[-1] in operators + match_operators:
if parts[-1] in operators + match_operators + geo_operators:
op = parts.pop()
if _doc_cls:
@@ -335,15 +360,27 @@ class QuerySet(object):
singular_ops += match_operators
if op in singular_ops:
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
value = [field.prepare_query_value(op, v) for v in value]
if field.__class__.__name__ == 'GenericReferenceField':
parts.append('_ref')
if op and op not in match_operators:
value = {'$' + op: value}
# 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}
key = '.'.join(parts)
if op is None or key not in mongo_query:
@@ -402,6 +439,14 @@ class QuerySet(object):
message = u'%d items returned, instead of 1' % count
raise self._document.MultipleObjectsReturned(message)
def create(self, **kwargs):
"""Create new object. Returns the saved object instance.
.. versionadded:: 0.4
"""
doc = self._document(**kwargs)
doc.save()
return doc
def first(self):
"""Retrieve the first object matching the query.
"""
@@ -592,6 +637,15 @@ class QuerySet(object):
# Integer index provided
elif isinstance(key, int):
return self._document._from_son(self._cursor[key])
def distinct(self, field):
"""Return a list of distinct values for a given field.
:param field: the field to select distinct values from
.. versionadded:: 0.4
"""
return self._collection.distinct(field)
def only(self, *fields):
"""Load only a subset of this document's fields. ::
@@ -626,11 +680,13 @@ class QuerySet(object):
"""
key_list = []
for key in keys:
if not key: continue
direction = pymongo.ASCENDING
if key[0] == '-':
direction = pymongo.DESCENDING
if key[0] in ('-', '+'):
key = key[1:]
key = key.replace('__', '.')
key_list.append((key, direction))
self._ordering = key_list
@@ -646,7 +702,6 @@ class QuerySet(object):
plan = self._cursor.explain()
if format:
import pprint
plan = pprint.pformat(plan)
return plan
@@ -661,8 +716,8 @@ class QuerySet(object):
def _transform_update(cls, _doc_cls=None, **update):
"""Transform an update spec from Django-style format to Mongo format.
"""
operators = ['set', 'unset', 'inc', 'dec', 'push', 'push_all', 'pull',
'pull_all']
operators = ['set', 'unset', 'inc', 'dec', 'pop', 'push', 'push_all',
'pull', 'pull_all']
mongo_update = {}
for key, value in update.items():
@@ -688,7 +743,7 @@ class QuerySet(object):
# Convert value to proper value
field = fields[-1]
if op in (None, 'set', 'unset', 'push', 'pull'):
if op in (None, 'set', 'unset', 'pop', 'push', 'pull'):
value = field.prepare_query_value(op, value)
elif op in ('pushAll', 'pullAll'):
value = [field.prepare_query_value(op, v) for v in value]
@@ -837,7 +892,7 @@ class QuerySet(object):
var total = 0.0;
var num = 0;
db[collection].find(query).forEach(function(doc) {
if (doc[averageField]) {
if (doc[averageField] !== undefined) {
total += doc[averageField];
num += 1;
}