Compare commits

..

65 Commits

Author SHA1 Message Date
Ross Lawley
1fdc7ce6bb Releasing Version 0.6.10 2012-05-23 08:58:43 +01:00
Ross Lawley
944aa45459 Updated changelog 2012-05-21 15:21:45 +01:00
Ross Lawley
c9842ba13a Fix base classes to return
fixes hmarr/mongoengine#507
2012-05-21 15:20:46 +01:00
Ross Lawley
8840680303 Promoted BaseDynamicField to DynamicField
closes mongoengine/mongoengine#22
2012-05-17 21:54:17 +01:00
Ross Lawley
376b9b1316 updated the readme 2012-05-17 21:14:25 +01:00
Ross Lawley
54bb1cb3d9 Updated travis settings and Readme 2012-05-17 16:59:50 +01:00
Ross Lawley
43468b474e Adding travis support 2012-05-17 16:49:13 +01:00
Ross Lawley
28a957c684 Version bump 2012-05-14 12:43:00 +01:00
Ross Lawley
ec5ddbf391 Fixed sparse indexes with inheritance
fixes hmarr/mongoengine#497
2012-05-14 12:06:25 +01:00
Ross Lawley
bab186e195 Reverted document.delete auto gridfs delete 2012-05-14 12:02:07 +01:00
Ross Lawley
bc7e874476 Version 0.6.8 2012-05-09 20:53:18 +01:00
Ross Lawley
97114b5948 Fix for FileField losing ref without default
fixes hmarr/mongoengine#458
2012-05-09 20:50:11 +01:00
Ross Lawley
45e015d71d Added test for keys with spaces 2012-05-09 20:49:34 +01:00
Ross Lawley
0ff6531953 Updated changelog 2012-05-09 15:39:34 +01:00
Ross Lawley
ba298c3cfc Save can be used in assignment 2012-05-09 15:37:07 +01:00
Ross Lawley
0479bea40b Cleaned up GridFS
refs hmarr/mongoengine#465
2012-05-09 15:35:28 +01:00
Ross Lawley
a536097804 Added support for pull operations on nested EmbeddedDocs
fixes mongoengine/mongoengine#16
2012-05-09 14:38:53 +01:00
Ross Lawley
bbefd0fdf9 Added example of bi directional delete rules + test
refs mongoengine/mongoengine#15
2012-05-09 13:54:33 +01:00
Ross Lawley
2aa8b04c21 Implemented Choices for GenericReferenceFields
Refs mongoengine/mongoengine#13
2012-05-09 13:21:53 +01:00
Ross Lawley
aeebdfec51 Implemented Choices for GenericEmbeddedDocuments
Refs mongoengine/mongoengine#13
2012-05-09 12:58:45 +01:00
Ross Lawley
debfcdf498 Updated docs re: required pymongo version
refs #472
2012-05-09 12:19:59 +01:00
Ross Lawley
5c4b33e8e6 Made note stronger re: race condition
Refs #478
2012-05-09 12:13:42 +01:00
Ross Lawley
eb54037b66 Added note that get_or_create contains a race condition
Refs #478
2012-05-09 12:08:00 +01:00
Ross Lawley
f48af8db3b Django 1.4 first session save lost data
fixes #477
2012-05-09 12:00:05 +01:00
Ross Lawley
97c5b957dd Updated docs regarding GridFS
refs #492
2012-05-09 11:39:30 +01:00
Ross Lawley
95e7397803 Merge branch 'master' of https://github.com/deignacio/mongoengine 2012-05-09 11:32:26 +01:00
Ross Lawley
43a989978a Merge branch 'patch-8' of https://github.com/wpjunior/mongoengine 2012-05-09 11:21:54 +01:00
Ross Lawley
27734a7c26 Updated Author / changelog 2012-05-09 11:04:05 +01:00
Anthony Nemitz
dd786d6fc4 fix for #494 2012-05-09 02:54:08 -07:00
Ross Lawley
be1c28fc45 Updated changelog 2012-05-08 18:26:04 +01:00
Ross Lawley
20e41b3523 Make the user model extendable 2012-05-08 18:23:51 +01:00
David Ignacio
e07ecc5cf8 Cleanup referenced GridFS files when a document is deleted
Note that drop_collection is not modified since there is no
guarantee that a GridFS collection holds files for only one
Document class.  Otherwise you could drop files for other fields
or documents accidentally.
2012-05-05 01:33:08 -04:00
Wilson Júnior
3360b72531 Small fixes for GenericReferenceField 2012-05-04 14:58:06 -03:00
Ross Lawley
233b13d670 Version bump 2012-05-01 12:03:33 +01:00
Ross Lawley
5bcbb4fdaa Properly fixed indexing on _id for covered indexes
refs  #4
2012-05-01 12:02:45 +01:00
Ross Lawley
dbe2f5f2b8 Updated setup.py 2012-05-01 11:48:57 +01:00
Ross Lawley
ca8b58d66d Fixed indexing on _id for covered indexes
fixes #4
2012-05-01 11:27:37 +01:00
Ross Lawley
f80f0b416f Updated changelog 2012-05-01 11:04:01 +01:00
Ross Lawley
d7765511ee Invalid DB Data now raises an InvalidDocumentError
fixes #2
2012-05-01 11:03:23 +01:00
Ross Lawley
0240a09056 Cleaned up ValidationError Refs #459 2012-05-01 10:14:16 +01:00
Ross Lawley
ab15c4eec9 Merge branch 'master' of https://github.com/adamreeve/mongoengine into 459 2012-05-01 09:42:25 +01:00
Ross Lawley
4ce1ba81a6 Merge branch 'dev-disable-indexing' of https://github.com/colinhowe/mongoengine
Conflicts:
	mongoengine/queryset.py
2012-05-01 09:37:01 +01:00
Ross Lawley
530440b333 Fixed replicaset_connection test 2012-05-01 09:14:38 +01:00
Ross Lawley
b80fda36af Ensure session save is safe 2012-04-27 14:57:07 +01:00
Ross Lawley
42d24263ef Updated changelog 2012-04-27 10:39:35 +01:00
Ross Lawley
1e2797e7ce Merge branch 'master' of github.com:MongoEngine/mongoengine 2012-04-27 10:34:31 +01:00
Ross Lawley
f7075766fc Merge pull request #11 from gregbanks/insert_kwargs
add parameters to QuerySet.insert to allow control of lower level behavior
2012-04-27 02:34:03 -07:00
Ross Lawley
5647ca70bb Fix variable name bug 2012-04-27 09:52:57 +01:00
Ross Lawley
2b8aa6bafc Fixes read_preference
Fixes mongoengine/mongoengine#10
2012-04-27 09:15:05 +01:00
Greg Banks
410443471c TopLevelDocumentMetaClass sets _meta['index_opts'] not _meta['index_options'] 2012-04-26 14:03:30 -07:00
Greg Banks
0bb9781b91 add "safe" and "write_options" parameters to QuerySet.insert similar to Document.save 2012-04-26 13:56:52 -07:00
Ross Lawley
2769d6d7ca Updated Authors / Changelog 2012-04-25 12:43:17 +01:00
Ross Lawley
120b9433c2 Merge branch 'master' of https://github.com/swashbuckler/mongoengine 2012-04-25 12:40:53 +01:00
Ross Lawley
605092bd88 Updated change log / AUTHORS
Thanks Greg Banks
2012-04-25 12:36:37 +01:00
Ross Lawley
a4a8c94374 Merge branch 'master' of https://github.com/gregbanks/mongoengine 2012-04-25 12:31:11 +01:00
Ross Lawley
0e93f6c0db Updated changelog 2012-04-25 12:26:31 +01:00
Ross Lawley
aa2add39ad Version bump 2012-04-25 12:24:08 +01:00
Ross Lawley
a928047147 Fixing sessions for django 1.3 and django 1.4 2012-04-24 21:00:30 +01:00
Jona Andersen
c474ca0f13 Allow File-like objects to be stored.
No longer demands FileField be an actual instance of file, but instead
checks whether object has a 'read' attribute. Fixes read() on
GridFSProxy to return an empty string on read failure, or None if file
does not exist.
2012-04-22 13:49:18 +02:00
Ross Lawley
88dc64653e Fix Django 1.3 auth 2012-04-18 16:41:09 +01:00
Greg Banks
49a66ba81a whoops, don't dereference all references as the first type encountered 2012-04-12 11:42:10 -07:00
Greg Banks
a1d43fecd9 fix for issue 473 2012-04-11 16:37:22 -07:00
Colin Howe
7e376b40bb Add new meta option to Document: allow_index_creation.
Defaults to True. If set to False then MongoEngine will not ensure indexes exist
2012-03-19 20:27:08 +00:00
Adam Reeve
540a0cc59c List all document errors when raising a ValidationError 2012-03-09 22:20:59 +13:00
Adam Reeve
83eb4f6b16 Document the ValidationError class 2012-03-09 22:20:54 +13:00
25 changed files with 910 additions and 253 deletions

12
.travis.yml Normal file
View File

@@ -0,0 +1,12 @@
# http://travis-ci.org/#!/MongoEngine/mongoengine
language: python
python:
- 2.6
- 2.7
install:
- sudo apt-get install zlib1g zlib1g-dev
- sudo ln -s /usr/lib/i386-linux-gnu/libz.so /usr/lib/
- pip install PIL --use-mirrors ; true
- python setup.py install
script:
- python setup.py test

View File

@@ -99,4 +99,9 @@ that much better:
* Robert Kajic * Robert Kajic
* Jacob Peddicord * Jacob Peddicord
* Nils Hasenbanck * Nils Hasenbanck
* mostlystatic * mostlystatic
* Greg Banks
* swashbuckler
* Adam Reeve
* Anthony Nemitz
* deignacio

View File

@@ -5,6 +5,9 @@ MongoEngine
:Author: Harry Marr (http://github.com/hmarr) :Author: Harry Marr (http://github.com/hmarr)
:Maintainer: Ross Lawley (http://github.com/rozza) :Maintainer: Ross Lawley (http://github.com/rozza)
.. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master
:target: http://travis-ci.org/MongoEngine/mongoengine
About About
===== =====
MongoEngine is a Python Object-Document Mapper for working with MongoDB. MongoEngine is a Python Object-Document Mapper for working with MongoDB.
@@ -22,7 +25,7 @@ setup.py install``.
Dependencies Dependencies
============ ============
- pymongo 1.1+ - pymongo 2.1.1+
- sphinx (optional - for documentation generation) - sphinx (optional - for documentation generation)
Examples Examples
@@ -96,3 +99,4 @@ Contributing
The source is available on `GitHub <http://github.com/MongoEngine/mongoengine>`_ - to The source is available on `GitHub <http://github.com/MongoEngine/mongoengine>`_ - to
contribute to the project, fork it on GitHub and send a pull request, all contribute to the project, fork it on GitHub and send a pull request, all
contributions and suggestions are welcome! contributions and suggestions are welcome!

View File

@@ -31,6 +31,9 @@ Documents
.. autoclass:: mongoengine.document.MapReduceDocument .. autoclass:: mongoengine.document.MapReduceDocument
:members: :members:
.. autoclass:: mongoengine.ValidationError
:members:
Querying Querying
======== ========

View File

@@ -2,6 +2,51 @@
Changelog Changelog
========= =========
Changes in 0.6.10
=================
- Fixed basedict / baselist to return super(..)
- Promoted BaseDynamicField to DynamicField
Changes in 0.6.9
================
- Fixed sparse indexes on inherited docs
- Removed FileField auto deletion, needs more work maybe 0.7
Changes in 0.6.8
================
- Fixed FileField losing reference when no default set
- Removed possible race condition from FileField (grid_file)
- Added assignment to save, can now do: b = MyDoc(**kwargs).save()
- Added support for pull operations on nested EmbeddedDocuments
- Added support for choices with GenericReferenceFields
- Added support for choices with GenericEmbeddedDocumentFields
- Fixed Django 1.4 sessions first save data loss
- FileField now automatically delete files on .delete()
- Fix for GenericReference to_mongo method
- Fixed connection regression
- Updated Django User document, now allows inheritance
Changes in 0.6.7
================
- Fixed indexing on '_id' or 'pk' or 'id'
- Invalid data from the DB now raises a InvalidDocumentError
- Cleaned up the Validation Error - docs and code
- Added meta `auto_create_index` so you can disable index creation
- Added write concern options to inserts
- Fixed typo in meta for index options
- Bug fix Read preference now passed correctly
- Added support for File like objects for GridFS
- Fix for #473 - Dereferencing abstracts
Changes in 0.6.6
================
- Django 1.4 fixed (finally)
- Added tests for Django
Changes in 0.6.5
================
- More Django updates
Changes in 0.6.4 Changes in 0.6.4
================ ================

View File

@@ -98,7 +98,7 @@ arguments can be set on all fields:
:attr:`required` (Default: False) :attr:`required` (Default: False)
If set to True and the field is not set on the document instance, a If set to True and the field is not set on the document instance, a
:class:`~mongoengine.base.ValidationError` will be raised when the document is :class:`~mongoengine.ValidationError` will be raised when the document is
validated. validated.
:attr:`default` (Default: None) :attr:`default` (Default: None)

View File

@@ -91,5 +91,5 @@ is an alias to :attr:`id`::
.. note:: .. note::
If you define your own primary key field, the field implicitly becomes If you define your own primary key field, the field implicitly becomes
required, so a :class:`ValidationError` will be thrown if you don't provide required, so a :class:`~mongoengine.ValidationError` will be thrown if
it. you don't provide it.

View File

@@ -65,7 +65,7 @@ Deleting stored files is achieved with the :func:`delete` method::
marmot.photo.delete() marmot.photo.delete()
.. note:: .. warning::
The FileField in a Document actually only stores the ID of a file in a The FileField in a Document actually only stores the ID of a file in a
separate GridFS collection. This means that deleting a document separate GridFS collection. This means that deleting a document

View File

@@ -12,7 +12,7 @@ from signals import *
__all__ = (document.__all__ + fields.__all__ + connection.__all__ + __all__ = (document.__all__ + fields.__all__ + connection.__all__ +
queryset.__all__ + signals.__all__) queryset.__all__ + signals.__all__)
VERSION = (0, 6, 4) VERSION = (0, 6, 10)
def get_version(): def get_version():

View File

@@ -25,7 +25,15 @@ class InvalidDocumentError(Exception):
class ValidationError(AssertionError): class ValidationError(AssertionError):
"""Validation exception. """Validation exception.
May represent an error validating a field or a
document containing fields with validation errors.
:ivar errors: A dictionary of errors for fields within this
document or list, or None if the error is for an
individual field.
""" """
errors = {} errors = {}
field_name = None field_name = None
_message = None _message = None
@@ -43,10 +51,12 @@ class ValidationError(AssertionError):
def __getattribute__(self, name): def __getattribute__(self, name):
message = super(ValidationError, self).__getattribute__(name) message = super(ValidationError, self).__getattribute__(name)
if name == 'message' and self.field_name: if name == 'message':
return message + ' ("%s")' % self.field_name if self.field_name:
else: message = '%s ("%s")' % (message, self.field_name)
return message if self.errors:
message = '%s:\n%s' % (message, self._format_errors())
return message
def _get_message(self): def _get_message(self):
return self._message return self._message
@@ -57,6 +67,13 @@ class ValidationError(AssertionError):
message = property(_get_message, _set_message) message = property(_get_message, _set_message)
def to_dict(self): def to_dict(self):
"""Returns a dictionary of all errors within a document
Keys are field names or list indices and values are the
validation error messages, or a nested dictionary of
errors for an embedded document or list.
"""
def build_dict(source): def build_dict(source):
errors_dict = {} errors_dict = {}
if not source: if not source:
@@ -73,6 +90,21 @@ class ValidationError(AssertionError):
return {} return {}
return build_dict(self.errors) return build_dict(self.errors)
def _format_errors(self):
"""Returns a string listing all errors within a document"""
def format_error(field, value, prefix=''):
prefix = "%s.%s" % (prefix, field) if prefix else "%s" % field
if isinstance(value, dict):
return '\n'.join(
[format_error(k, value[k], prefix) for k in value])
else:
return "%s: %s" % (prefix, value)
return '\n'.join(
[format_error(k, v) for k, v in self.to_dict().items()])
_document_registry = {} _document_registry = {}
@@ -191,16 +223,18 @@ class BaseField(object):
pass pass
def _validate(self, value): def _validate(self, value):
from mongoengine import Document, EmbeddedDocument
# check choices # check choices
if self.choices: if self.choices:
is_cls = isinstance(value, (Document, EmbeddedDocument))
value_to_check = value.__class__ if is_cls else value
err_msg = 'an instance' if is_cls else 'one'
if isinstance(self.choices[0], (list, tuple)): if isinstance(self.choices[0], (list, tuple)):
option_keys = [option_key for option_key, option_value in self.choices] option_keys = [option_key for option_key, option_value in self.choices]
if value not in option_keys: if value_to_check not in option_keys:
self.error('Value must be one of %s' % unicode(option_keys)) self.error('Value must be %s of %s' % (err_msg, unicode(option_keys)))
else: elif value_to_check not in self.choices:
if value not in self.choices: self.error('Value must be %s of %s' % (err_msg, unicode(self.choices)))
self.error('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:
@@ -368,7 +402,7 @@ class ComplexBaseField(BaseField):
sequence = enumerate(value) sequence = enumerate(value)
for k, v in sequence: for k, v in sequence:
try: try:
self.field.validate(v) self.field._validate(v)
except (ValidationError, AssertionError), error: except (ValidationError, AssertionError), error:
if hasattr(error, 'errors'): if hasattr(error, 'errors'):
errors[k] = error.errors errors[k] = error.errors
@@ -401,47 +435,6 @@ class ComplexBaseField(BaseField):
owner_document = property(_get_owner_document, _set_owner_document) owner_document = property(_get_owner_document, _set_owner_document)
class BaseDynamicField(BaseField):
"""Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
def to_mongo(self, value):
"""Convert a Python type to a MongoDBcompatible type.
"""
if isinstance(value, basestring):
return value
if hasattr(value, 'to_mongo'):
return value.to_mongo()
if not isinstance(value, (dict, list, tuple)):
return value
is_list = False
if not hasattr(value, 'items'):
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
data = {}
for k, v in value.items():
data[k] = self.to_mongo(v)
if is_list: # Convert back to a list
value = [v for k, v in sorted(data.items(), key=operator.itemgetter(0))]
else:
value = data
return value
def lookup_member(self, member_name):
return member_name
def prepare_query_value(self, op, value):
if isinstance(value, basestring):
from mongoengine.fields import StringField
return StringField().prepare_query_value(op, value)
return self.to_mongo(value)
class ObjectIdField(BaseField): class ObjectIdField(BaseField):
"""An field wrapper around MongoDB's ObjectIds. """An field wrapper around MongoDB's ObjectIds.
""" """
@@ -825,7 +818,8 @@ class BaseDocument(object):
field = None field = None
if not hasattr(self, name) and not name.startswith('_'): if not hasattr(self, name) and not name.startswith('_'):
field = BaseDynamicField(db_field=name) from fields import DynamicField
field = DynamicField(db_field=name)
field.name = name field.name = name
self._dynamic_fields[name] = field self._dynamic_fields[name] = field
@@ -947,8 +941,8 @@ class BaseDocument(object):
""" """
# get the class name from the document, falling back to the given # get the class name from the document, falling back to the given
# class if unavailable # class if unavailable
class_name = son.get(u'_cls', cls._class_name) class_name = son.get('_cls', cls._class_name)
data = dict((str(key), value) for key, value in son.items()) data = dict(("%s" % key, value) for key, value in son.items())
if '_types' in data: if '_types' in data:
del data['_types'] del data['_types']
@@ -961,11 +955,16 @@ class BaseDocument(object):
cls = get_document(class_name) cls = get_document(class_name)
changed_fields = [] changed_fields = []
errors_dict = {}
for field_name, field in cls._fields.items(): for field_name, field in cls._fields.items():
if field.db_field in data: if field.db_field in data:
value = data[field.db_field] value = data[field.db_field]
data[field_name] = (value if value is None try:
data[field_name] = (value if value is None
else field.to_python(value)) else field.to_python(value))
except (AttributeError, ValueError), e:
errors_dict[field_name] = e
elif field.default: elif field.default:
default = field.default default = field.default
if callable(default): if callable(default):
@@ -973,7 +972,13 @@ class BaseDocument(object):
if isinstance(default, BaseDocument): if isinstance(default, BaseDocument):
changed_fields.append(field_name) changed_fields.append(field_name)
if errors_dict:
errors = "\n".join(["%s - %s" % (k, v) for k, v in errors_dict.items()])
raise InvalidDocumentError("""
Invalid data to create a `%s` instance.\n%s""".strip() % (cls._class_name, errors))
obj = cls(**data) obj = cls(**data)
obj._changed_fields = changed_fields obj._changed_fields = changed_fields
obj._created = False obj._created = False
return obj return obj
@@ -1212,15 +1217,15 @@ class BaseList(list):
def __init__(self, list_items, instance, name): def __init__(self, list_items, instance, name):
self._instance = instance self._instance = instance
self._name = name self._name = name
super(BaseList, self).__init__(list_items) return super(BaseList, self).__init__(list_items)
def __setitem__(self, *args, **kwargs): def __setitem__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseList, self).__setitem__(*args, **kwargs) return super(BaseList, self).__setitem__(*args, **kwargs)
def __delitem__(self, *args, **kwargs): def __delitem__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseList, self).__delitem__(*args, **kwargs) return super(BaseList, self).__delitem__(*args, **kwargs)
def __getstate__(self): def __getstate__(self):
self.observer = None self.observer = None
@@ -1274,23 +1279,23 @@ class BaseDict(dict):
def __init__(self, dict_items, instance, name): def __init__(self, dict_items, instance, name):
self._instance = instance self._instance = instance
self._name = name self._name = name
super(BaseDict, self).__init__(dict_items) return super(BaseDict, self).__init__(dict_items)
def __setitem__(self, *args, **kwargs): def __setitem__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).__setitem__(*args, **kwargs) return super(BaseDict, self).__setitem__(*args, **kwargs)
def __delete__(self, *args, **kwargs): def __delete__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).__delete__(*args, **kwargs) return super(BaseDict, self).__delete__(*args, **kwargs)
def __delitem__(self, *args, **kwargs): def __delitem__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).__delitem__(*args, **kwargs) return super(BaseDict, self).__delitem__(*args, **kwargs)
def __delattr__(self, *args, **kwargs): def __delattr__(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).__delattr__(*args, **kwargs) return super(BaseDict, self).__delattr__(*args, **kwargs)
def __getstate__(self): def __getstate__(self):
self.instance = None self.instance = None
@@ -1303,19 +1308,19 @@ class BaseDict(dict):
def clear(self, *args, **kwargs): def clear(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).clear(*args, **kwargs) return super(BaseDict, self).clear(*args, **kwargs)
def pop(self, *args, **kwargs): def pop(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).pop(*args, **kwargs) return super(BaseDict, self).pop(*args, **kwargs)
def popitem(self, *args, **kwargs): def popitem(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).popitem(*args, **kwargs) return super(BaseDict, self).popitem(*args, **kwargs)
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
self._mark_as_changed() self._mark_as_changed()
super(BaseDict, self).update(*args, **kwargs) return super(BaseDict, self).update(*args, **kwargs)
def _mark_as_changed(self): def _mark_as_changed(self):
if hasattr(self._instance, '_mark_as_changed'): if hasattr(self._instance, '_mark_as_changed'):

View File

@@ -63,7 +63,10 @@ def register_connection(alias, name, host='localhost', port=27017,
'password': uri_dict.get('password'), 'password': uri_dict.get('password'),
'read_preference': read_preference, 'read_preference': read_preference,
}) })
if "replicaSet" in host:
conn_settings['replicaSet'] = True
conn_settings.update(kwargs)
_connection_settings[alias] = conn_settings _connection_settings[alias] = conn_settings
@@ -112,7 +115,11 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
conn_settings['hosts_or_uri'] = conn_settings.pop('host', None) conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
# Discard port since it can't be used on ReplicaSetConnection # Discard port since it can't be used on ReplicaSetConnection
conn_settings.pop('port', None) conn_settings.pop('port', None)
# Discard replicaSet if not base string
if not isinstance(conn_settings['replicaSet'], basestring):
conn_settings.pop('replicaSet', None)
connection_class = ReplicaSetConnection connection_class = ReplicaSetConnection
try: try:
_connections[alias] = connection_class(**conn_settings) _connections[alias] = connection_class(**conn_settings)
except Exception, e: except Exception, e:

View File

@@ -112,6 +112,10 @@ class DeReference(object):
for ref in references: for ref in references:
if '_cls' in ref: if '_cls' in ref:
doc = get_document(ref["_cls"])._from_son(ref) doc = get_document(ref["_cls"])._from_son(ref)
elif doc_type is None:
doc = get_document(
''.join(x.capitalize()
for x in col.split('_')))._from_son(ref)
else: else:
doc = doc_type._from_son(ref) doc = doc_type._from_son(ref)
object_map[doc.id] = doc object_map[doc.id] = doc

View File

@@ -10,6 +10,7 @@ try:
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password, make_password
except ImportError: except ImportError:
"""Handle older versions of Django""" """Handle older versions of Django"""
from django.utils.hashcompat import md5_constructor, sha_constructor
def get_hexdigest(algorithm, salt, raw_password): def get_hexdigest(algorithm, salt, raw_password):
raw_password, salt = smart_str(raw_password), smart_str(salt) raw_password, salt = smart_str(raw_password), smart_str(salt)
@@ -65,6 +66,7 @@ class User(Document):
verbose_name=_('date joined')) verbose_name=_('date joined'))
meta = { meta = {
'allow_inheritance': True,
'indexes': [ 'indexes': [
{'fields': ['username'], 'unique': True} {'fields': ['username'], 'unique': True}
] ]

View File

@@ -44,7 +44,7 @@ class SessionStore(SessionBase):
def create(self): def create(self):
while True: while True:
self.session_key = self._get_new_session_key() self._session_key = self._get_new_session_key()
try: try:
self.save(must_create=True) self.save(must_create=True)
except CreateError: except CreateError:
@@ -55,7 +55,7 @@ class SessionStore(SessionBase):
def save(self, must_create=False): def save(self, must_create=False):
if self.session_key is None: if self.session_key is None:
self.create() self._session_key = self._get_new_session_key()
s = MongoSession(session_key=self.session_key) s = MongoSession(session_key=self.session_key)
s.session_data = self.encode(self._get_session(no_load=must_create)) s.session_data = self.encode(self._get_session(no_load=must_create))
s.expire_date = self.get_expiry_date() s.expire_date = self.get_expiry_date()

View File

@@ -1,4 +1,5 @@
import pymongo import pymongo
from bson.dbref import DBRef from bson.dbref import DBRef
from mongoengine import signals from mongoengine import signals
@@ -74,6 +75,12 @@ class Document(BaseDocument):
names. Index direction may be specified by prefixing the field names with names. Index direction may be specified by prefixing the field names with
a **+** or **-** sign. a **+** or **-** sign.
Automatic index creation can be disabled by specifying
attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to
False then indexes will not be created by MongoEngine. This is useful in
production systems where index creation is performed as part of a deployment
system.
By default, _types will be added to the start of every index (that By default, _types will be added to the start of every index (that
doesn't contain a list) if allow_inheritence is True. This can be doesn't contain a list) if allow_inheritence is True. This can be
disabled by either setting types to False on the specific index or disabled by either setting types to False on the specific index or
@@ -147,8 +154,9 @@ class Document(BaseDocument):
:meth:`~pymongo.collection.Collection.save` OR :meth:`~pymongo.collection.Collection.save` OR
:meth:`~pymongo.collection.Collection.insert` :meth:`~pymongo.collection.Collection.insert`
which will be used as options for the resultant ``getLastError`` command. which will be used as options for the resultant ``getLastError`` command.
For example, ``save(..., w=2, fsync=True)`` will wait until at least two servers For example, ``save(..., write_options={w: 2, fsync: True}, ...)`` will
have recorded the write and will force an fsync on each server being written to. wait until at least two servers have recorded the write and will force an
fsync on each server being written to.
:param cascade: Sets the flag for cascading saves. You can set a default by setting :param cascade: Sets the flag for cascading saves. You can set a default by setting
"cascade" in the document __meta__ "cascade" in the document __meta__
:param cascade_kwargs: optional kwargs dictionary to be passed throw to cascading saves :param cascade_kwargs: optional kwargs dictionary to be passed throw to cascading saves
@@ -226,6 +234,7 @@ class Document(BaseDocument):
self._changed_fields = [] self._changed_fields = []
self._created = False self._created = False
signals.post_save.send(self.__class__, document=self, created=created) signals.post_save.send(self.__class__, document=self, created=created)
return self
def cascade_save(self, *args, **kwargs): def cascade_save(self, *args, **kwargs):
"""Recursively saves any references / generic references on an object""" """Recursively saves any references / generic references on an object"""
@@ -351,7 +360,7 @@ class DynamicDocument(Document):
way as an ordinary document but has expando style properties. Any data way as an ordinary document but has expando style properties. Any data
passed or set against the :class:`~mongoengine.DynamicDocument` that is passed or set against the :class:`~mongoengine.DynamicDocument` that is
not a field is automatically converted into a not a field is automatically converted into a
:class:`~mongoengine.BaseDynamicField` and data can be attributed to that :class:`~mongoengine.DynamicField` and data can be attributed to that
field. field.
..note:: ..note::

View File

@@ -30,7 +30,7 @@ except ImportError:
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField', __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField', 'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
'ObjectIdField', 'ReferenceField', 'ValidationError', 'MapField', 'ObjectIdField', 'ReferenceField', 'ValidationError', 'MapField',
'DecimalField', 'ComplexDateTimeField', 'URLField', 'DecimalField', 'ComplexDateTimeField', 'URLField', 'DynamicField',
'GenericReferenceField', 'FileField', 'BinaryField', 'GenericReferenceField', 'FileField', 'BinaryField',
'SortedListField', 'EmailField', 'GeoPointField', 'ImageField', 'SortedListField', 'EmailField', 'GeoPointField', 'ImageField',
'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField'] 'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField']
@@ -441,6 +441,9 @@ class GenericEmbeddedDocumentField(BaseField):
:class:`~mongoengine.EmbeddedDocument` to be stored. :class:`~mongoengine.EmbeddedDocument` to be stored.
Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`. Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
..note :: You can use the choices param to limit the acceptable
EmbeddedDocument types
""" """
def prepare_query_value(self, op, value): def prepare_query_value(self, op, value):
@@ -470,6 +473,47 @@ class GenericEmbeddedDocumentField(BaseField):
return data return data
class DynamicField(BaseField):
"""Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
def to_mongo(self, value):
"""Convert a Python type to a MongoDBcompatible type.
"""
if isinstance(value, basestring):
return value
if hasattr(value, 'to_mongo'):
return value.to_mongo()
if not isinstance(value, (dict, list, tuple)):
return value
is_list = False
if not hasattr(value, 'items'):
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
data = {}
for k, v in value.items():
data[k] = self.to_mongo(v)
if is_list: # Convert back to a list
value = [v for k, v in sorted(data.items(), key=itemgetter(0))]
else:
value = data
return value
def lookup_member(self, member_name):
return member_name
def prepare_query_value(self, op, value):
if isinstance(value, basestring):
from mongoengine.fields import StringField
return StringField().prepare_query_value(op, value)
return self.to_mongo(value)
class ListField(ComplexBaseField): class ListField(ComplexBaseField):
"""A list field that wraps a standard field, allowing multiple instances """A list field that wraps a standard field, allowing multiple instances
of the field to be used as a list in the database. of the field to be used as a list in the database.
@@ -613,6 +657,17 @@ class ReferenceField(BaseField):
* CASCADE - Deletes the documents associated with the reference. * CASCADE - Deletes the documents associated with the reference.
* DENY - Prevent the deletion of the reference object. * DENY - Prevent the deletion of the reference object.
Alternative syntax for registering delete rules (useful when implementing
bi-directional delete rules)
.. code-block:: python
class Bar(Document):
content = StringField()
foo = ReferenceField('Foo')
Bar.register_delete_rule(Foo, 'bar', NULLIFY)
.. versionchanged:: 0.5 added `reverse_delete_rule` .. versionchanged:: 0.5 added `reverse_delete_rule`
""" """
@@ -659,7 +714,7 @@ class ReferenceField(BaseField):
def to_mongo(self, document): def to_mongo(self, document):
if isinstance(document, DBRef): if isinstance(document, DBRef):
return document return document
id_field_name = self.document_type._meta['id_field'] id_field_name = self.document_type._meta['id_field']
id_field = self.document_type._fields[id_field_name] id_field = self.document_type._fields[id_field_name]
@@ -701,6 +756,8 @@ class GenericReferenceField(BaseField):
..note :: Any documents used as a generic reference must be registered in the ..note :: Any documents used as a generic reference must be registered in the
document registry. Importing the model will automatically register it. document registry. Importing the model will automatically register it.
..note :: You can use the choices param to limit the acceptable Document types
.. versionadded:: 0.3 .. versionadded:: 0.3
""" """
@@ -735,6 +792,9 @@ class GenericReferenceField(BaseField):
if document is None: if document is None:
return None return None
if isinstance(document, (dict, SON)):
return document
id_field_name = document.__class__._meta['id_field'] id_field_name = document.__class__._meta['id_field']
id_field = document.__class__._fields[id_field_name] id_field = document.__class__._fields[id_field_name]
@@ -875,10 +935,14 @@ class GridFSProxy(object):
self.newfile.writelines(lines) self.newfile.writelines(lines)
def read(self, size=-1): def read(self, size=-1):
try: gridout = self.get()
return self.get().read(size) if gridout is None:
except:
return None return None
else:
try:
return gridout.read(size)
except:
return ""
def delete(self): def delete(self):
# Delete file from GridFS, FileField still remains # Delete file from GridFS, FileField still remains
@@ -923,19 +987,20 @@ class FileField(BaseField):
# Check if a file already exists for this model # Check if a file already exists for this model
grid_file = instance._data.get(self.name) grid_file = instance._data.get(self.name)
self.grid_file = grid_file if not isinstance(grid_file, self.proxy_class):
if isinstance(self.grid_file, self.proxy_class): grid_file = self.proxy_class(key=self.name, instance=instance,
if not self.grid_file.key: db_alias=self.db_alias,
self.grid_file.key = self.name collection_name=self.collection_name)
self.grid_file.instance = instance instance._data[self.name] = grid_file
return self.grid_file
return self.proxy_class(key=self.name, instance=instance, if not grid_file.key:
db_alias=self.db_alias, grid_file.key = self.name
collection_name=self.collection_name) grid_file.instance = instance
return grid_file
def __set__(self, instance, value): def __set__(self, instance, value):
key = self.name key = self.name
if isinstance(value, file) or isinstance(value, str): if (hasattr(value, 'read') and not isinstance(value, GridFSProxy)) or isinstance(value, str):
# using "FileField() = file/string" notation # using "FileField() = file/string" notation
grid_file = instance._data.get(self.name) grid_file = instance._data.get(self.name)
# If a file already exists, delete it # If a file already exists, delete it

View File

@@ -394,61 +394,6 @@ class QuerySet(object):
unique=index_spec.get('unique', False)) unique=index_spec.get('unique', False))
return self return self
@classmethod
def _build_index_spec(cls, doc_cls, spec):
"""Build a PyMongo index spec from a MongoEngine index spec.
"""
if isinstance(spec, basestring):
spec = {'fields': [spec]}
if isinstance(spec, (list, tuple)):
spec = {'fields': spec}
index_list = []
use_types = doc_cls._meta.get('allow_inheritance', True)
for key in spec['fields']:
# Get ASCENDING direction from +, DESCENDING from -, and GEO2D from *
direction = pymongo.ASCENDING
if key.startswith("-"):
direction = pymongo.DESCENDING
elif key.startswith("*"):
direction = pymongo.GEO2D
if key.startswith(("+", "-", "*")):
key = key[1:]
# Use real field name, do it manually because we need field
# objects for the next part (list field checking)
parts = key.split('.')
fields = QuerySet._lookup_field(doc_cls, parts)
parts = [field.db_field for field in fields]
key = '.'.join(parts)
index_list.append((key, direction))
# Check if a list field is being used, don't use _types if it is
if use_types and not all(f._index_with_types for f in fields):
use_types = False
# If _types is being used, prepend it to every specified index
index_types = doc_cls._meta.get('index_types', True)
allow_inheritance = doc_cls._meta.get('allow_inheritance')
if spec.get('types', index_types) and allow_inheritance and use_types and direction is not pymongo.GEO2D:
index_list.insert(0, ('_types', 1))
spec['fields'] = index_list
if spec.get('sparse', False) and len(spec['fields']) > 1:
raise ValueError(
'Sparse indexes can only have one field in them. '
'See https://jira.mongodb.org/browse/SERVER-2193')
return spec
@classmethod
def _reset_already_indexed(cls, document=None):
"""Helper to reset already indexed, can be useful for testing purposes"""
if document:
cls.__already_indexed.discard(document)
cls.__already_indexed.clear()
def __call__(self, q_obj=None, class_check=True, slave_okay=False, **query): def __call__(self, q_obj=None, class_check=True, slave_okay=False, **query):
"""Filter the selected documents by calling the """Filter the selected documents by calling the
:class:`~mongoengine.queryset.QuerySet` with a query. :class:`~mongoengine.queryset.QuerySet` with a query.
@@ -481,13 +426,128 @@ class QuerySet(object):
"""Returns all documents.""" """Returns all documents."""
return self.__call__() return self.__call__()
def _ensure_indexes(self):
"""Checks the document meta data and ensures all the indexes exist.
.. note:: You can disable automatic index creation by setting
`auto_create_index` to False in the documents meta data
"""
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_opts', {})
index_types = self._document._meta.get('index_types', True)
# determine if an index which we are creating includes
# _type as its first field; if so, we can avoid creating
# an extra index on _type, as mongodb will use the existing
# index to service queries against _type
types_indexed = False
def includes_types(fields):
first_field = None
if len(fields):
if isinstance(fields[0], basestring):
first_field = fields[0]
elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
first_field = fields[0][0]
return first_field == '_types'
# Ensure indexes created by uniqueness constraints
for index in self._document._meta['unique_indexes']:
types_indexed = types_indexed or includes_types(index)
self._collection.ensure_index(index, unique=True,
background=background, drop_dups=drop_dups, **index_opts)
# Ensure document-defined indexes are created
if self._document._meta['indexes']:
for spec in self._document._meta['indexes']:
types_indexed = types_indexed or includes_types(spec['fields'])
opts = index_opts.copy()
opts['unique'] = spec.get('unique', False)
opts['sparse'] = spec.get('sparse', False)
self._collection.ensure_index(spec['fields'],
background=background, **opts)
# If _types is being used (for polymorphism), it needs an index,
# only if another index doesn't begin with _types
if index_types and '_types' in self._query and not types_indexed:
self._collection.ensure_index('_types',
background=background, **index_opts)
# Add geo indicies
for field in self._document._geo_indices():
index_spec = [(field.db_field, pymongo.GEO2D)]
self._collection.ensure_index(index_spec,
background=background, **index_opts)
@classmethod
def _build_index_spec(cls, doc_cls, spec):
"""Build a PyMongo index spec from a MongoEngine index spec.
"""
if isinstance(spec, basestring):
spec = {'fields': [spec]}
if isinstance(spec, (list, tuple)):
spec = {'fields': spec}
index_list = []
use_types = doc_cls._meta.get('allow_inheritance', True)
for key in spec['fields']:
# Get ASCENDING direction from +, DESCENDING from -, and GEO2D from *
direction = pymongo.ASCENDING
if key.startswith("-"):
direction = pymongo.DESCENDING
elif key.startswith("*"):
direction = pymongo.GEO2D
if key.startswith(("+", "-", "*")):
key = key[1:]
# Use real field name, do it manually because we need field
# objects for the next part (list field checking)
parts = key.split('.')
if parts in (['pk'], ['id'], ['_id']):
key = '_id'
else:
fields = QuerySet._lookup_field(doc_cls, parts)
parts = [field if field == '_id' else field.db_field for field in fields]
key = '.'.join(parts)
index_list.append((key, direction))
# If sparse - dont include types
if spec.get('sparse', False):
use_types = False
# Check if a list field is being used, don't use _types if it is
if use_types and not all(f._index_with_types for f in fields):
use_types = False
# If _types is being used, prepend it to every specified index
index_types = doc_cls._meta.get('index_types', True)
allow_inheritance = doc_cls._meta.get('allow_inheritance')
if spec.get('types', index_types) and allow_inheritance and use_types and direction is not pymongo.GEO2D:
index_list.insert(0, ('_types', 1))
spec['fields'] = index_list
if spec.get('sparse', False) and len(spec['fields']) > 1:
raise ValueError(
'Sparse indexes can only have one field in them. '
'See https://jira.mongodb.org/browse/SERVER-2193')
return spec
@classmethod
def _reset_already_indexed(cls, document=None):
"""Helper to reset already indexed, can be useful for testing purposes"""
if document:
cls.__already_indexed.discard(document)
cls.__already_indexed.clear()
@property @property
def _collection(self): def _collection(self):
"""Property that returns the collection object. This allows us to """Property that returns the collection object. This allows us to
perform operations only if the collection is accessed. perform operations only if the collection is accessed.
""" """
if self._document not in QuerySet.__already_indexed: if self._document not in QuerySet.__already_indexed:
# Ensure collection exists # Ensure collection exists
db = self._document._get_db() db = self._document._get_db()
if self._collection_obj.name not in db.collection_names(): if self._collection_obj.name not in db.collection_names():
@@ -496,52 +556,8 @@ class QuerySet(object):
QuerySet.__already_indexed.add(self._document) QuerySet.__already_indexed.add(self._document)
background = self._document._meta.get('index_background', False) if self._document._meta.get('auto_create_index', True):
drop_dups = self._document._meta.get('index_drop_dups', False) self._ensure_indexes()
index_opts = self._document._meta.get('index_options', {})
index_types = self._document._meta.get('index_types', True)
# determine if an index which we are creating includes
# _type as its first field; if so, we can avoid creating
# an extra index on _type, as mongodb will use the existing
# index to service queries against _type
types_indexed = False
def includes_types(fields):
first_field = None
if len(fields):
if isinstance(fields[0], basestring):
first_field = fields[0]
elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
first_field = fields[0][0]
return first_field == '_types'
# Ensure indexes created by uniqueness constraints
for index in self._document._meta['unique_indexes']:
types_indexed = types_indexed or includes_types(index)
self._collection.ensure_index(index, unique=True,
background=background, drop_dups=drop_dups, **index_opts)
# Ensure document-defined indexes are created
if self._document._meta['indexes']:
for spec in self._document._meta['indexes']:
types_indexed = types_indexed or includes_types(spec['fields'])
opts = index_opts.copy()
opts['unique'] = spec.get('unique', False)
opts['sparse'] = spec.get('sparse', False)
self._collection.ensure_index(spec['fields'],
background=background, **opts)
# If _types is being used (for polymorphism), it needs an index,
# only if another index doesn't begin with _types
if index_types and '_types' in self._query and not types_indexed:
self._collection.ensure_index('_types',
background=background, **index_opts)
# Add geo indicies
for field in self._document._geo_indices():
index_spec = [(field.db_field, pymongo.GEO2D)]
self._collection.ensure_index(index_spec,
background=background, **index_opts)
return self._collection_obj return self._collection_obj
@@ -611,8 +627,8 @@ class QuerySet(object):
if field_name in document._fields: if field_name in document._fields:
field = document._fields[field_name] field = document._fields[field_name]
elif document._dynamic: elif document._dynamic:
from base import BaseDynamicField from fields import DynamicField
field = BaseDynamicField(db_field=field_name) field = DynamicField(db_field=field_name)
else: else:
raise InvalidQueryError('Cannot resolve field "%s"' raise InvalidQueryError('Cannot resolve field "%s"'
% field_name) % field_name)
@@ -781,6 +797,11 @@ class QuerySet(object):
dictionary of default values for the new document may be provided as a dictionary of default values for the new document may be provided as a
keyword argument called :attr:`defaults`. keyword argument called :attr:`defaults`.
.. note:: This requires two separate operations and therefore a
race condition exists. Because there are no transactions in mongoDB
other approaches should be investigated, to ensure you don't
accidently duplicate data when using this method.
:param write_options: optional extra keyword arguments used if we :param write_options: optional extra keyword arguments used if we
have to create a new document. have to create a new document.
Passes any write_options onto :meth:`~mongoengine.Document.save` Passes any write_options onto :meth:`~mongoengine.Document.save`
@@ -824,11 +845,21 @@ class QuerySet(object):
result = None result = None
return result return result
def insert(self, doc_or_docs, load_bulk=True): def insert(self, doc_or_docs, load_bulk=True, safe=False, write_options=None):
"""bulk insert documents """bulk insert documents
If ``safe=True`` and the operation is unsuccessful, an
:class:`~mongoengine.OperationError` will be raised.
:param docs_or_doc: a document or list of documents to be inserted :param docs_or_doc: a document or list of documents to be inserted
:param load_bulk (optional): If True returns the list of document instances :param load_bulk (optional): If True returns the list of document instances
:param safe: check if the operation succeeded before returning
:param write_options: Extra keyword arguments are passed down to
:meth:`~pymongo.collection.Collection.insert`
which will be used as options for the resultant ``getLastError`` command.
For example, ``insert(..., {w: 2, fsync: True})`` will wait until at least two
servers have recorded the write and will force an fsync on each server being
written to.
By default returns document instances, set ``load_bulk`` to False to By default returns document instances, set ``load_bulk`` to False to
return just ``ObjectIds`` return just ``ObjectIds``
@@ -837,6 +868,10 @@ class QuerySet(object):
""" """
from document import Document from document import Document
if not write_options:
write_options = {}
write_options.update({'safe': safe})
docs = doc_or_docs docs = doc_or_docs
return_one = False return_one = False
if isinstance(docs, Document) or issubclass(docs.__class__, Document): if isinstance(docs, Document) or issubclass(docs.__class__, Document):
@@ -854,7 +889,13 @@ class QuerySet(object):
raw.append(doc.to_mongo()) raw.append(doc.to_mongo())
signals.pre_bulk_insert.send(self._document, documents=docs) signals.pre_bulk_insert.send(self._document, documents=docs)
ids = self._collection.insert(raw) try:
ids = self._collection.insert(raw, **write_options)
except pymongo.errors.OperationFailure, err:
message = 'Could not save document (%s)'
if u'duplicate key' in unicode(err):
message = u'Tried to save duplicate unique keys (%s)'
raise OperationError(message % unicode(err))
if not load_bulk: if not load_bulk:
signals.post_bulk_insert.send( signals.post_bulk_insert.send(
@@ -1343,9 +1384,18 @@ class QuerySet(object):
if not op: if not op:
raise InvalidQueryError("Updates must supply an operation eg: set__FIELD=value") raise InvalidQueryError("Updates must supply an operation eg: set__FIELD=value")
if op: if 'pull' in op and '.' in key:
# Dot operators don't work on pull operations
# it uses nested dict syntax
if op == 'pullAll':
raise InvalidQueryError("pullAll operations only support a single field depth")
parts.reverse()
for key in parts:
value = {key: value}
else:
value = {key: value} value = {key: value}
key = '$' + op key = '$' + op
if key not in mongo_update: if key not in mongo_update:
mongo_update[key] = value mongo_update[key] = value

View File

@@ -5,7 +5,7 @@
%define srcname mongoengine %define srcname mongoengine
Name: python-%{srcname} Name: python-%{srcname}
Version: 0.6.4 Version: 0.6.10
Release: 1%{?dist} Release: 1%{?dist}
Summary: A Python Document-Object Mapper for working with MongoDB Summary: A Python Document-Object Mapper for working with MongoDB
@@ -51,12 +51,4 @@ rm -rf $RPM_BUILD_ROOT
# %{python_sitearch}/* # %{python_sitearch}/*
%changelog %changelog
* Mon Mar 05 2012 Ross Lawley <ross.lawley@gmail.com> 0.6 * See: http://readthedocs.org/docs/mongoengine-odm/en/latest/changelog.html
- 0.6 released
* Thu Oct 27 2011 Pau Aliagas <linuxnow@gmail.com> 0.5.3-1
- Update to latest dev version
- Add PIL dependency for ImageField
* Wed Oct 12 2011 Pau Aliagas <linuxnow@gmail.com> 0.5.2-1
- Update version
* Fri Sep 23 2011 Pau Aliagas <linuxnow@gmail.com> 0.5.0-1
- Initial version

View File

@@ -65,6 +65,16 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest2') self.assertEqual(db.name, 'mongoenginetest2')
def test_connection_kwargs(self):
"""Ensure that connection kwargs get passed to pymongo.
"""
connect('mongoenginetest', alias='t1', tz_aware=True)
conn = get_connection('t1')
self.assertTrue(conn.tz_aware)
connect('mongoenginetest2', alias='t2')
conn = get_connection('t2')
self.assertFalse(conn.tz_aware)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -810,3 +810,34 @@ class FieldTest(unittest.TestCase):
room = Room.objects.first().select_related() room = Room.objects.first().select_related()
self.assertEquals(room.staffs_with_position[0]['staff'], sarah) self.assertEquals(room.staffs_with_position[0]['staff'], sarah)
self.assertEquals(room.staffs_with_position[1]['staff'], bob) self.assertEquals(room.staffs_with_position[1]['staff'], bob)
def test_document_reload_no_inheritance(self):
class Foo(Document):
meta = {'allow_inheritance': False}
bar = ReferenceField('Bar')
baz = ReferenceField('Baz')
class Bar(Document):
meta = {'allow_inheritance': False}
msg = StringField(required=True, default='Blammo!')
class Baz(Document):
meta = {'allow_inheritance': False}
msg = StringField(required=True, default='Kaboom!')
Foo.drop_collection()
Bar.drop_collection()
Baz.drop_collection()
bar = Bar()
bar.save()
baz = Baz()
baz.save()
foo = Foo()
foo.bar = bar
foo.baz = baz
foo.save()
foo.reload()
self.assertEquals(type(foo.bar), Bar)
self.assertEquals(type(foo.baz), Baz)

View File

@@ -12,6 +12,10 @@ from django.core.paginator import Paginator
settings.configure() settings.configure()
from django.contrib.sessions.tests import SessionTestsMixin
from mongoengine.django.sessions import SessionStore, MongoSession
class QuerySetTest(unittest.TestCase): class QuerySetTest(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -88,3 +92,19 @@ class QuerySetTest(unittest.TestCase):
end = p * 2 end = p * 2
start = end - 1 start = end - 1
self.assertEqual(t.render(Context(d)), u'%d:%d:' % (start, end)) self.assertEqual(t.render(Context(d)), u'%d:%d:' % (start, end))
class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
backend = SessionStore
def setUp(self):
connect(db='mongoenginetest')
MongoSession.drop_collection()
super(MongoDBSessionTest, self).setUp()
def test_first_save(self):
session = SessionStore()
session['test'] = True
session.save()
self.assertTrue('test' in session)

View File

@@ -741,6 +741,28 @@ class DocumentTest(unittest.TestCase):
self.assertEqual(info.keys(), ['_types_1_user_guid_1', '_id_', '_types_1_name_1']) self.assertEqual(info.keys(), ['_types_1_user_guid_1', '_id_', '_types_1_name_1'])
Person.drop_collection() Person.drop_collection()
def test_disable_index_creation(self):
"""Tests setting auto_create_index to False on the connection will
disable any index generation.
"""
class User(Document):
meta = {
'indexes': ['user_guid'],
'auto_create_index': False
}
user_guid = StringField(required=True)
User.drop_collection()
u = User(user_guid='123')
u.save()
self.assertEquals(1, User.objects.count())
info = User.objects._collection.index_information()
self.assertEqual(info.keys(), ['_id_'])
User.drop_collection()
def test_embedded_document_index(self): def test_embedded_document_index(self):
"""Tests settings an index on an embedded document """Tests settings an index on an embedded document
""" """
@@ -842,6 +864,26 @@ class DocumentTest(unittest.TestCase):
query_plan = Test.objects(a=1).only('a').exclude('id').explain() query_plan = Test.objects(a=1).only('a').exclude('id').explain()
self.assertTrue(query_plan['indexOnly']) self.assertTrue(query_plan['indexOnly'])
def test_index_on_id(self):
class BlogPost(Document):
meta = {
'indexes': [
['categories', 'id']
],
'allow_inheritance': False
}
title = StringField(required=True)
description = StringField(required=True)
categories = ListField()
BlogPost.drop_collection()
indexes = BlogPost.objects._collection.index_information()
self.assertEquals(indexes['categories_1__id_1']['key'],
[('categories', 1), ('_id', 1)])
def test_hint(self): def test_hint(self):
class BlogPost(Document): class BlogPost(Document):
@@ -2376,6 +2418,22 @@ class DocumentTest(unittest.TestCase):
self.assertRaises(InvalidDocumentError, throw_invalid_document_error) self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
def test_invalid_son(self):
"""Raise an error if loading invalid data"""
class Occurrence(EmbeddedDocument):
number = IntField()
class Word(Document):
stem = StringField()
count = IntField(default=1)
forms = ListField(StringField(), default=list)
occurs = ListField(EmbeddedDocumentField(Occurrence), default=list)
def raise_invalid_document():
Word._from_son({'stem': [1,2,3], 'forms': 1, 'count': 'one', 'occurs': {"hello": None}})
self.assertRaises(InvalidDocumentError, raise_invalid_document)
def test_reverse_delete_rule_cascade_and_nullify(self): def test_reverse_delete_rule_cascade_and_nullify(self):
"""Ensure that a referenced document is also deleted upon deletion. """Ensure that a referenced document is also deleted upon deletion.
""" """
@@ -2438,6 +2496,40 @@ class DocumentTest(unittest.TestCase):
author.delete() author.delete()
self.assertEqual(len(BlogPost.objects), 0) self.assertEqual(len(BlogPost.objects), 0)
def test_two_way_reverse_delete_rule(self):
"""Ensure that Bi-Directional relationships work with
reverse_delete_rule
"""
class Bar(Document):
content = StringField()
foo = ReferenceField('Foo')
class Foo(Document):
content = StringField()
bar = ReferenceField(Bar)
Bar.register_delete_rule(Foo, 'bar', NULLIFY)
Foo.register_delete_rule(Bar, 'foo', NULLIFY)
Bar.drop_collection()
Foo.drop_collection()
b = Bar(content="Hello")
b.save()
f = Foo(content="world", bar=b)
f.save()
b.foo = f
b.save()
f.delete()
self.assertEqual(len(Bar.objects), 1) # No effect on the BlogPost
self.assertEqual(Bar.objects.get().foo, None)
def test_invalid_reverse_delete_rules_raise_errors(self): def test_invalid_reverse_delete_rules_raise_errors(self):
def throw_invalid_document_error(): def throw_invalid_document_error():
@@ -2839,5 +2931,78 @@ class DocumentTest(unittest.TestCase):
} }
) ]), "1,2") ) ]), "1,2")
class ValidatorErrorTest(unittest.TestCase):
def test_to_dict(self):
"""Ensure a ValidationError handles error to_dict correctly.
"""
error = ValidationError('root')
self.assertEquals(error.to_dict(), {})
# 1st level error schema
error.errors = {'1st': ValidationError('bad 1st'), }
self.assertTrue('1st' in error.to_dict())
self.assertEquals(error.to_dict()['1st'], 'bad 1st')
# 2nd level error schema
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd'),
})}
self.assertTrue('1st' in error.to_dict())
self.assertTrue(isinstance(error.to_dict()['1st'], dict))
self.assertTrue('2nd' in error.to_dict()['1st'])
self.assertEquals(error.to_dict()['1st']['2nd'], 'bad 2nd')
# moar levels
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd', errors={
'3rd': ValidationError('bad 3rd', errors={
'4th': ValidationError('Inception'),
}),
}),
})}
self.assertTrue('1st' in error.to_dict())
self.assertTrue('2nd' in error.to_dict()['1st'])
self.assertTrue('3rd' in error.to_dict()['1st']['2nd'])
self.assertTrue('4th' in error.to_dict()['1st']['2nd']['3rd'])
self.assertEquals(error.to_dict()['1st']['2nd']['3rd']['4th'],
'Inception')
self.assertEquals(error.message, "root:\n1st.2nd.3rd.4th: Inception")
def test_model_validation(self):
class User(Document):
username = StringField(primary_key=True)
name = StringField(required=True)
try:
User().validate()
except ValidationError, e:
expected_error_message = """Errors encountered validating document:
username: Field is required ("username")
name: Field is required ("name")"""
self.assertEquals(e.message, expected_error_message)
self.assertEquals(e.to_dict(), {
'username': 'Field is required ("username")',
'name': u'Field is required ("name")'})
def test_spaces_in_keys(self):
class Embedded(DynamicEmbeddedDocument):
pass
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
setattr(doc, 'hello world', 1)
doc.save()
one = Doc.objects.filter(**{'hello world': 1}).count()
self.assertEqual(1, one)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -2,6 +2,9 @@ import datetime
import os import os
import unittest import unittest
import uuid import uuid
import StringIO
import tempfile
import gridfs
from decimal import Decimal from decimal import Decimal
@@ -18,6 +21,10 @@ class FieldTest(unittest.TestCase):
connect(db='mongoenginetest') connect(db='mongoenginetest')
self.db = get_db() self.db = get_db()
def tearDown(self):
self.db.drop_collection('fs.files')
self.db.drop_collection('fs.chunks')
def test_default_values(self): def test_default_values(self):
"""Ensure that default field values are used when creating a document. """Ensure that default field values are used when creating a document.
""" """
@@ -1300,6 +1307,74 @@ class FieldTest(unittest.TestCase):
self.assertEquals(repr(Person.objects(city=None)), self.assertEquals(repr(Person.objects(city=None)),
"[<Person: Person object>]") "[<Person: Person object>]")
def test_generic_reference_choices(self):
"""Ensure that a GenericReferenceField can handle choices
"""
class Link(Document):
title = StringField()
class Post(Document):
title = StringField()
class Bookmark(Document):
bookmark_object = GenericReferenceField(choices=(Post,))
Link.drop_collection()
Post.drop_collection()
Bookmark.drop_collection()
link_1 = Link(title="Pitchfork")
link_1.save()
post_1 = Post(title="Behind the Scenes of the Pavement Reunion")
post_1.save()
bm = Bookmark(bookmark_object=link_1)
self.assertRaises(ValidationError, bm.validate)
bm = Bookmark(bookmark_object=post_1)
bm.save()
bm = Bookmark.objects.first()
self.assertEqual(bm.bookmark_object, post_1)
def test_generic_reference_list_choices(self):
"""Ensure that a ListField properly dereferences generic references and
respects choices.
"""
class Link(Document):
title = StringField()
class Post(Document):
title = StringField()
class User(Document):
bookmarks = ListField(GenericReferenceField(choices=(Post,)))
Link.drop_collection()
Post.drop_collection()
User.drop_collection()
link_1 = Link(title="Pitchfork")
link_1.save()
post_1 = Post(title="Behind the Scenes of the Pavement Reunion")
post_1.save()
user = User(bookmarks=[link_1])
self.assertRaises(ValidationError, user.validate)
user = User(bookmarks=[post_1])
user.save()
user = User.objects.first()
self.assertEqual(user.bookmarks, [post_1])
Link.drop_collection()
Post.drop_collection()
User.drop_collection()
def test_binary_fields(self): def test_binary_fields(self):
"""Ensure that binary fields can be stored and retrieved. """Ensure that binary fields can be stored and retrieved.
""" """
@@ -1481,6 +1556,21 @@ class FieldTest(unittest.TestCase):
self.assertEquals(result.file.read(), text) self.assertEquals(result.file.read(), text)
self.assertEquals(result.file.content_type, content_type) self.assertEquals(result.file.content_type, content_type)
result.file.delete() # Remove file from GridFS result.file.delete() # Remove file from GridFS
PutFile.objects.delete()
# Ensure file-like objects are stored
putfile = PutFile()
putstring = StringIO.StringIO()
putstring.write(text)
putstring.seek(0)
putfile.file.put(putstring, 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()
streamfile = StreamFile() streamfile = StreamFile()
streamfile.file.new_file(content_type=content_type) streamfile.file.new_file(content_type=content_type)
@@ -1530,6 +1620,49 @@ class FieldTest(unittest.TestCase):
file = FileField() file = FileField()
DemoFile.objects.create() DemoFile.objects.create()
def test_file_field_no_default(self):
class GridDocument(Document):
the_file = FileField()
GridDocument.drop_collection()
with tempfile.TemporaryFile() as f:
f.write("Hello World!")
f.flush()
# Test without default
doc_a = GridDocument()
doc_a.save()
doc_b = GridDocument.objects.with_id(doc_a.id)
doc_b.the_file.replace(f, filename='doc_b')
doc_b.save()
self.assertNotEquals(doc_b.the_file.grid_id, None)
# Test it matches
doc_c = GridDocument.objects.with_id(doc_b.id)
self.assertEquals(doc_b.the_file.grid_id, doc_c.the_file.grid_id)
# Test with default
doc_d = GridDocument(the_file='')
doc_d.save()
doc_e = GridDocument.objects.with_id(doc_d.id)
self.assertEquals(doc_d.the_file.grid_id, doc_e.the_file.grid_id)
doc_e.the_file.replace(f, filename='doc_e')
doc_e.save()
doc_f = GridDocument.objects.with_id(doc_e.id)
self.assertEquals(doc_e.the_file.grid_id, doc_f.the_file.grid_id)
db = GridDocument._get_db()
grid_fs = gridfs.GridFS(db)
self.assertEquals(['doc_b', 'doc_e'], grid_fs.list())
def test_file_uniqueness(self): def test_file_uniqueness(self):
"""Ensure that each instance of a FileField is unique """Ensure that each instance of a FileField is unique
""" """
@@ -1828,6 +1961,8 @@ class FieldTest(unittest.TestCase):
name = StringField() name = StringField()
like = GenericEmbeddedDocumentField() like = GenericEmbeddedDocumentField()
Person.drop_collection()
person = Person(name='Test User') person = Person(name='Test User')
person.like = Car(name='Fiat') person.like = Car(name='Fiat')
person.save() person.save()
@@ -1841,6 +1976,59 @@ class FieldTest(unittest.TestCase):
person = Person.objects.first() person = Person.objects.first()
self.assertTrue(isinstance(person.like, Dish)) self.assertTrue(isinstance(person.like, Dish))
def test_generic_embedded_document_choices(self):
"""Ensure you can limit GenericEmbeddedDocument choices
"""
class Car(EmbeddedDocument):
name = StringField()
class Dish(EmbeddedDocument):
food = StringField(required=True)
number = IntField()
class Person(Document):
name = StringField()
like = GenericEmbeddedDocumentField(choices=(Dish,))
Person.drop_collection()
person = Person(name='Test User')
person.like = Car(name='Fiat')
self.assertRaises(ValidationError, person.validate)
person.like = Dish(food="arroz", number=15)
person.save()
person = Person.objects.first()
self.assertTrue(isinstance(person.like, Dish))
def test_generic_list_embedded_document_choices(self):
"""Ensure you can limit GenericEmbeddedDocument choices inside a list
field
"""
class Car(EmbeddedDocument):
name = StringField()
class Dish(EmbeddedDocument):
food = StringField(required=True)
number = IntField()
class Person(Document):
name = StringField()
likes = ListField(GenericEmbeddedDocumentField(choices=(Dish,)))
Person.drop_collection()
person = Person(name='Test User')
person.likes = [Car(name='Fiat')]
self.assertRaises(ValidationError, person.validate)
person.likes = [Dish(food="arroz", number=15)]
person.save()
person = Person.objects.first()
self.assertTrue(isinstance(person.likes[0], Dish))
def test_recursive_validation(self): def test_recursive_validation(self):
"""Ensure that a validation result to_dict is available. """Ensure that a validation result to_dict is available.
""" """
@@ -1886,43 +2074,5 @@ class FieldTest(unittest.TestCase):
post.validate() post.validate()
class ValidatorErrorTest(unittest.TestCase):
def test_to_dict(self):
"""Ensure a ValidationError handles error to_dict correctly.
"""
error = ValidationError('root')
self.assertEquals(error.to_dict(), {})
# 1st level error schema
error.errors = {'1st': ValidationError('bad 1st'), }
self.assertTrue('1st' in error.to_dict())
self.assertEquals(error.to_dict()['1st'], 'bad 1st')
# 2nd level error schema
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd'),
})}
self.assertTrue('1st' in error.to_dict())
self.assertTrue(isinstance(error.to_dict()['1st'], dict))
self.assertTrue('2nd' in error.to_dict()['1st'])
self.assertEquals(error.to_dict()['1st']['2nd'], 'bad 2nd')
# moar levels
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd', errors={
'3rd': ValidationError('bad 3rd', errors={
'4th': ValidationError('Inception'),
}),
}),
})}
self.assertTrue('1st' in error.to_dict())
self.assertTrue('2nd' in error.to_dict()['1st'])
self.assertTrue('3rd' in error.to_dict()['1st']['2nd'])
self.assertTrue('4th' in error.to_dict()['1st']['2nd']['3rd'])
self.assertEquals(error.to_dict()['1st']['2nd']['3rd']['4th'],
'Inception')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -480,7 +480,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(person.name, "User C") self.assertEqual(person.name, "User C")
def test_bulk_insert(self): def test_bulk_insert(self):
"""Ensure that query by array position works. """Ensure that bulk insert works
""" """
class Comment(EmbeddedDocument): class Comment(EmbeddedDocument):
@@ -490,7 +490,7 @@ class QuerySetTest(unittest.TestCase):
comments = ListField(EmbeddedDocumentField(Comment)) comments = ListField(EmbeddedDocumentField(Comment))
class Blog(Document): class Blog(Document):
title = StringField() title = StringField(unique=True)
tags = ListField(StringField()) tags = ListField(StringField())
posts = ListField(EmbeddedDocumentField(Post)) posts = ListField(EmbeddedDocumentField(Post))
@@ -563,6 +563,23 @@ class QuerySetTest(unittest.TestCase):
obj_id = Blog.objects.insert(blog1, load_bulk=False) obj_id = Blog.objects.insert(blog1, load_bulk=False)
self.assertEquals(obj_id.__class__.__name__, 'ObjectId') self.assertEquals(obj_id.__class__.__name__, 'ObjectId')
Blog.drop_collection()
post3 = Post(comments=[comment1, comment1])
blog1 = Blog(title="foo", posts=[post1, post2])
blog2 = Blog(title="bar", posts=[post2, post3])
blog3 = Blog(title="baz", posts=[post1, post2])
Blog.objects.insert([blog1, blog2])
def throw_operation_error_not_unique():
Blog.objects.insert([blog2, blog3], safe=True)
self.assertRaises(OperationError, throw_operation_error_not_unique)
self.assertEqual(Blog.objects.count(), 2)
Blog.objects.insert([blog2, blog3], write_options={'continue_on_error': True})
self.assertEqual(Blog.objects.count(), 3)
def test_slave_okay(self): def test_slave_okay(self):
"""Ensures that a query can take slave_okay syntax """Ensures that a query can take slave_okay syntax
""" """
@@ -1454,6 +1471,35 @@ class QuerySetTest(unittest.TestCase):
post.reload() post.reload()
self.assertEqual(post.tags, ["code", "mongodb"]) self.assertEqual(post.tags, ["code", "mongodb"])
def test_pull_nested(self):
class User(Document):
name = StringField()
class Collaborator(EmbeddedDocument):
user = StringField()
def __unicode__(self):
return '%s' % self.user
class Site(Document):
name = StringField(max_length=75, unique=True, required=True)
collaborators = ListField(EmbeddedDocumentField(Collaborator))
Site.drop_collection()
c = Collaborator(user='Esteban')
s = Site(name="test", collaborators=[c])
s.save()
Site.objects(id=s.id).update_one(pull__collaborators__user='Esteban')
self.assertEqual(Site.objects.first().collaborators, [])
def pull_all():
Site.objects(id=s.id).update_one(pull_all__collaborators__user=['Ross'])
self.assertRaises(InvalidQueryError, pull_all)
def test_update_one_pop_generic_reference(self): def test_update_one_pop_generic_reference(self):

View File

@@ -0,0 +1,32 @@
import unittest
import pymongo
from pymongo import ReadPreference, ReplicaSetConnection
import mongoengine
from mongoengine import *
from mongoengine.connection import get_db, get_connection, ConnectionError
class ConnectionTest(unittest.TestCase):
def tearDown(self):
mongoengine.connection._connection_settings = {}
mongoengine.connection._connections = {}
mongoengine.connection._dbs = {}
def test_replicaset_uri_passes_read_preference(self):
"""Requires a replica set called "rs" on port 27017
"""
try:
conn = connect(db='mongoenginetest', host="mongodb://localhost/mongoenginetest?replicaSet=rs", read_preference=ReadPreference.SECONDARY_ONLY)
except ConnectionError, e:
return
if not isinstance(conn, ReplicaSetConnection):
return
self.assertEquals(conn.read_preference, ReadPreference.SECONDARY_ONLY)
if __name__ == '__main__':
unittest.main()