Merge branch 'master' of git://github.com/flosch/mongoengine into v0.4
This commit is contained in:
commit
564f950037
@ -64,3 +64,5 @@ Fields
|
|||||||
.. autoclass:: mongoengine.ReferenceField
|
.. autoclass:: mongoengine.ReferenceField
|
||||||
|
|
||||||
.. autoclass:: mongoengine.GenericReferenceField
|
.. autoclass:: mongoengine.GenericReferenceField
|
||||||
|
|
||||||
|
.. autoclass:: mongoengine.FileField
|
||||||
|
@ -46,6 +46,12 @@ are as follows:
|
|||||||
* :class:`~mongoengine.EmbeddedDocumentField`
|
* :class:`~mongoengine.EmbeddedDocumentField`
|
||||||
* :class:`~mongoengine.ReferenceField`
|
* :class:`~mongoengine.ReferenceField`
|
||||||
* :class:`~mongoengine.GenericReferenceField`
|
* :class:`~mongoengine.GenericReferenceField`
|
||||||
|
* :class:`~mongoengine.BooleanField`
|
||||||
|
* :class:`~mongoengine.GeoLocationField`
|
||||||
|
* :class:`~mongoengine.FileField`
|
||||||
|
* :class:`~mongoengine.EmailField`
|
||||||
|
* :class:`~mongoengine.SortedListField`
|
||||||
|
* :class:`~mongoengine.BinaryField`
|
||||||
|
|
||||||
Field arguments
|
Field arguments
|
||||||
---------------
|
---------------
|
||||||
|
@ -174,7 +174,7 @@ custom manager methods as you like::
|
|||||||
|
|
||||||
@queryset_manager
|
@queryset_manager
|
||||||
def live_posts(doc_cls, queryset):
|
def live_posts(doc_cls, queryset):
|
||||||
return queryset(published=True).filter(published=True)
|
return queryset.filter(published=True)
|
||||||
|
|
||||||
BlogPost(title='test1', published=False).save()
|
BlogPost(title='test1', published=False).save()
|
||||||
BlogPost(title='test2', published=True).save()
|
BlogPost(title='test2', published=True).save()
|
||||||
|
@ -25,8 +25,8 @@ class BaseField(object):
|
|||||||
_geo_index = False
|
_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, validation=None,
|
unique=False, unique_with=None, primary_key=False,
|
||||||
choices=None):
|
validation=None, choices=None):
|
||||||
self.db_field = (db_field or name) if not primary_key else '_id'
|
self.db_field = (db_field or name) if not primary_key else '_id'
|
||||||
if name:
|
if name:
|
||||||
import warnings
|
import warnings
|
||||||
@ -87,13 +87,15 @@ class BaseField(object):
|
|||||||
# check choices
|
# check choices
|
||||||
if self.choices is not None:
|
if self.choices is not None:
|
||||||
if value not in self.choices:
|
if value not in self.choices:
|
||||||
raise ValidationError("Value must be one of %s."%unicode(self.choices))
|
raise ValidationError("Value must be one of %s."
|
||||||
|
% unicode(self.choices))
|
||||||
|
|
||||||
# check validation argument
|
# check validation argument
|
||||||
if self.validation is not None:
|
if self.validation is not None:
|
||||||
if callable(self.validation):
|
if callable(self.validation):
|
||||||
if not self.validation(value):
|
if not self.validation(value):
|
||||||
raise ValidationError('Value does not match custom validation method.')
|
raise ValidationError('Value does not match custom' \
|
||||||
|
'validation method.')
|
||||||
else:
|
else:
|
||||||
raise ValueError('validation argument must be a callable.')
|
raise ValueError('validation argument must be a callable.')
|
||||||
|
|
||||||
@ -337,8 +339,8 @@ class BaseDocument(object):
|
|||||||
try:
|
try:
|
||||||
field._validate(value)
|
field._validate(value)
|
||||||
except (ValueError, AttributeError, AssertionError), e:
|
except (ValueError, AttributeError, AssertionError), e:
|
||||||
raise ValidationError('Invalid value for field of type "' +
|
raise ValidationError('Invalid value for field of type "%s": %s'
|
||||||
field.__class__.__name__ + '"')
|
% (field.__class__.__name__, value))
|
||||||
elif field.required:
|
elif field.required:
|
||||||
raise ValidationError('Field "%s" is required' % field.name)
|
raise ValidationError('Field "%s" is required' % field.name)
|
||||||
|
|
||||||
|
@ -7,17 +7,19 @@ import re
|
|||||||
import pymongo
|
import pymongo
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
import gridfs
|
||||||
|
import warnings
|
||||||
|
import types
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
|
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
|
||||||
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
|
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
|
||||||
'ObjectIdField', 'ReferenceField', 'ValidationError',
|
'ObjectIdField', 'ReferenceField', 'ValidationError',
|
||||||
'DecimalField', 'URLField', 'GenericReferenceField',
|
'DecimalField', 'URLField', 'GenericReferenceField', 'FileField',
|
||||||
'BinaryField', 'SortedListField', 'EmailField', 'GeoPointField']
|
'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.
|
||||||
"""
|
"""
|
||||||
@ -261,6 +263,7 @@ class ListField(BaseField):
|
|||||||
raise ValidationError('Argument to ListField constructor must be '
|
raise ValidationError('Argument to ListField constructor must be '
|
||||||
'a valid field')
|
'a valid field')
|
||||||
self.field = field
|
self.field = field
|
||||||
|
kwargs.setdefault('default', [])
|
||||||
super(ListField, self).__init__(**kwargs)
|
super(ListField, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
@ -353,6 +356,7 @@ class DictField(BaseField):
|
|||||||
def __init__(self, basecls=None, *args, **kwargs):
|
def __init__(self, basecls=None, *args, **kwargs):
|
||||||
self.basecls = basecls or BaseField
|
self.basecls = basecls or BaseField
|
||||||
assert issubclass(self.basecls, BaseField)
|
assert issubclass(self.basecls, BaseField)
|
||||||
|
kwargs.setdefault('default', {})
|
||||||
super(DictField, self).__init__(*args, **kwargs)
|
super(DictField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
@ -369,7 +373,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 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).
|
||||||
@ -436,7 +439,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).
|
||||||
@ -505,6 +507,104 @@ class BinaryField(BaseField):
|
|||||||
if self.max_bytes is not None and len(value) > self.max_bytes:
|
if self.max_bytes is not None and len(value) > self.max_bytes:
|
||||||
raise ValidationError('Binary value is too long')
|
raise ValidationError('Binary value is too long')
|
||||||
|
|
||||||
|
class GridFSProxy(object):
|
||||||
|
"""Proxy object to handle writing and reading of files to and from GridFS
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 __getattr__(self, name):
|
||||||
|
obj = self.get()
|
||||||
|
if name in dir(obj):
|
||||||
|
return getattr(obj, name)
|
||||||
|
|
||||||
|
def __get__(self, instance, value):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get(self, id=None):
|
||||||
|
if id: self.grid_id = id
|
||||||
|
try: return self.fs.get(id or self.grid_id)
|
||||||
|
except: return None # File has been deleted
|
||||||
|
|
||||||
|
def new_file(self, **kwargs):
|
||||||
|
self.newfile = self.fs.new_file(**kwargs)
|
||||||
|
self.grid_id = self.newfile._id
|
||||||
|
|
||||||
|
def put(self, file, **kwargs):
|
||||||
|
self.grid_id = self.fs.put(file, **kwargs)
|
||||||
|
|
||||||
|
def write(self, string):
|
||||||
|
if not self.newfile:
|
||||||
|
self.new_file()
|
||||||
|
self.grid_id = self.newfile._id
|
||||||
|
self.newfile.write(string)
|
||||||
|
|
||||||
|
def writelines(self, lines):
|
||||||
|
if not self.newfile:
|
||||||
|
self.new_file()
|
||||||
|
self.grid_id = self.newfile._id
|
||||||
|
self.newfile.writelines(lines)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try: return self.get().read()
|
||||||
|
except: return None
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
# Delete file from GridFS, FileField still remains
|
||||||
|
self.fs.delete(self.grid_id)
|
||||||
|
self.grid_id = None
|
||||||
|
|
||||||
|
def replace(self, file, **kwargs):
|
||||||
|
self.delete()
|
||||||
|
self.put(file, **kwargs)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.newfile:
|
||||||
|
self.newfile.close()
|
||||||
|
else:
|
||||||
|
msg = "The close() method is only necessary after calling write()"
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
|
class FileField(BaseField):
|
||||||
|
"""A GridFS storage field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.gridfs = GridFSProxy()
|
||||||
|
super(FileField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
return self.gridfs
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
if isinstance(value, file) or isinstance(value, str):
|
||||||
|
# using "FileField() = file/string" notation
|
||||||
|
self.gridfs.put(value)
|
||||||
|
else:
|
||||||
|
instance._data[self.name] = value
|
||||||
|
|
||||||
|
def to_mongo(self, value):
|
||||||
|
# Store the GridFS file id in MongoDB
|
||||||
|
if self.gridfs.grid_id is not None:
|
||||||
|
return self.gridfs.grid_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
# Use stored value (id) to lookup file in GridFS
|
||||||
|
if self.gridfs.grid_id is not None:
|
||||||
|
return self.gridfs.get(id=value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
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):
|
class GeoPointField(BaseField):
|
||||||
"""A list storing a latitude and longitude.
|
"""A list storing a latitude and longitude.
|
||||||
@ -524,4 +624,3 @@ class GeoPointField(BaseField):
|
|||||||
if (not isinstance(value[0], (float, int)) and
|
if (not isinstance(value[0], (float, int)) and
|
||||||
not isinstance(value[1], (float, int))):
|
not isinstance(value[1], (float, int))):
|
||||||
raise ValidationError('Both values in point must be float or int.')
|
raise ValidationError('Both values in point must be float or int.')
|
||||||
|
|
||||||
|
@ -264,11 +264,12 @@ class DocumentTest(unittest.TestCase):
|
|||||||
# Indexes are lazy so use list() to perform query
|
# Indexes are lazy so use list() to perform query
|
||||||
list(BlogPost.objects)
|
list(BlogPost.objects)
|
||||||
info = BlogPost.objects._collection.index_information()
|
info = BlogPost.objects._collection.index_information()
|
||||||
|
info = [value['key'] for key, value in info.iteritems()]
|
||||||
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
||||||
in info.values())
|
in info)
|
||||||
self.assertTrue([('_types', 1), ('addDate', -1)] in info.values())
|
self.assertTrue([('_types', 1), ('addDate', -1)] in info)
|
||||||
# tags is a list field so it shouldn't have _types in the index
|
# tags is a list field so it shouldn't have _types in the index
|
||||||
self.assertTrue([('tags', 1)] in info.values())
|
self.assertTrue([('tags', 1)] in info)
|
||||||
|
|
||||||
class ExtendedBlogPost(BlogPost):
|
class ExtendedBlogPost(BlogPost):
|
||||||
title = StringField()
|
title = StringField()
|
||||||
@ -278,10 +279,11 @@ class DocumentTest(unittest.TestCase):
|
|||||||
|
|
||||||
list(ExtendedBlogPost.objects)
|
list(ExtendedBlogPost.objects)
|
||||||
info = ExtendedBlogPost.objects._collection.index_information()
|
info = ExtendedBlogPost.objects._collection.index_information()
|
||||||
|
info = [value['key'] for key, value in info.iteritems()]
|
||||||
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
self.assertTrue([('_types', 1), ('category', 1), ('addDate', -1)]
|
||||||
in info.values())
|
in info)
|
||||||
self.assertTrue([('_types', 1), ('addDate', -1)] in info.values())
|
self.assertTrue([('_types', 1), ('addDate', -1)] in info)
|
||||||
self.assertTrue([('_types', 1), ('title', 1)] in info.values())
|
self.assertTrue([('_types', 1), ('title', 1)] in info)
|
||||||
|
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
|
import gridfs
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import _get_db
|
from mongoengine.connection import _get_db
|
||||||
@ -607,6 +608,73 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
Shirt.drop_collection()
|
Shirt.drop_collection()
|
||||||
|
|
||||||
|
def test_file_fields(self):
|
||||||
|
"""Ensure that file fields can be written to and their data retrieved
|
||||||
|
"""
|
||||||
|
class PutFile(Document):
|
||||||
|
file = FileField()
|
||||||
|
|
||||||
|
class StreamFile(Document):
|
||||||
|
file = FileField()
|
||||||
|
|
||||||
|
class SetFile(Document):
|
||||||
|
file = FileField()
|
||||||
|
|
||||||
|
text = 'Hello, World!'
|
||||||
|
more_text = 'Foo Bar'
|
||||||
|
content_type = 'text/plain'
|
||||||
|
|
||||||
|
PutFile.drop_collection()
|
||||||
|
StreamFile.drop_collection()
|
||||||
|
SetFile.drop_collection()
|
||||||
|
|
||||||
|
putfile = PutFile()
|
||||||
|
putfile.file.put(text, content_type=content_type)
|
||||||
|
putfile.save()
|
||||||
|
putfile.validate()
|
||||||
|
result = PutFile.objects.first()
|
||||||
|
self.assertTrue(putfile == result)
|
||||||
|
self.assertEquals(result.file.read(), text)
|
||||||
|
self.assertEquals(result.file.content_type, content_type)
|
||||||
|
result.file.delete() # Remove file from GridFS
|
||||||
|
|
||||||
|
streamfile = StreamFile()
|
||||||
|
streamfile.file.new_file(content_type=content_type)
|
||||||
|
streamfile.file.write(text)
|
||||||
|
streamfile.file.write(more_text)
|
||||||
|
streamfile.file.close()
|
||||||
|
streamfile.save()
|
||||||
|
streamfile.validate()
|
||||||
|
result = StreamFile.objects.first()
|
||||||
|
self.assertTrue(streamfile == result)
|
||||||
|
self.assertEquals(result.file.read(), text + more_text)
|
||||||
|
self.assertEquals(result.file.content_type, content_type)
|
||||||
|
result.file.delete()
|
||||||
|
|
||||||
|
# Ensure deleted file returns None
|
||||||
|
self.assertTrue(result.file.read() == None)
|
||||||
|
|
||||||
|
setfile = SetFile()
|
||||||
|
setfile.file = text
|
||||||
|
setfile.save()
|
||||||
|
setfile.validate()
|
||||||
|
result = SetFile.objects.first()
|
||||||
|
self.assertTrue(setfile == result)
|
||||||
|
self.assertEquals(result.file.read(), text)
|
||||||
|
|
||||||
|
# Try replacing file with new one
|
||||||
|
result.file.replace(more_text)
|
||||||
|
result.save()
|
||||||
|
result.validate()
|
||||||
|
result = SetFile.objects.first()
|
||||||
|
self.assertTrue(setfile == result)
|
||||||
|
self.assertEquals(result.file.read(), more_text)
|
||||||
|
result.file.delete()
|
||||||
|
|
||||||
|
PutFile.drop_collection()
|
||||||
|
StreamFile.drop_collection()
|
||||||
|
SetFile.drop_collection()
|
||||||
|
|
||||||
def test_geo_indexes(self):
|
def test_geo_indexes(self):
|
||||||
"""Ensure that indexes are created automatically for GeoPointFields.
|
"""Ensure that indexes are created automatically for GeoPointFields.
|
||||||
"""
|
"""
|
||||||
@ -621,11 +689,9 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
info = Event.objects._collection.index_information()
|
info = Event.objects._collection.index_information()
|
||||||
self.assertTrue(u'location_2d' in info)
|
self.assertTrue(u'location_2d' in info)
|
||||||
self.assertTrue(info[u'location_2d'] == [(u'location', u'2d')])
|
self.assertTrue(info[u'location_2d']['key'] == [(u'location', u'2d')])
|
||||||
|
|
||||||
Event.drop_collection()
|
Event.drop_collection()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -1087,8 +1087,9 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
# Indexes are lazy so use list() to perform query
|
# Indexes are lazy so use list() to perform query
|
||||||
list(BlogPost.objects)
|
list(BlogPost.objects)
|
||||||
info = BlogPost.objects._collection.index_information()
|
info = BlogPost.objects._collection.index_information()
|
||||||
self.assertTrue([('_types', 1)] in info.values())
|
info = [value['key'] for key, value in info.iteritems()]
|
||||||
self.assertTrue([('_types', 1), ('date', -1)] in info.values())
|
self.assertTrue([('_types', 1)] in info)
|
||||||
|
self.assertTrue([('_types', 1), ('date', -1)] in info)
|
||||||
|
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user