Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
efeaba39a4 | ||
|
1a97dfd479 | ||
|
9fecf2b303 | ||
|
3d0d2f48ad | ||
|
581605e0e2 | ||
|
45d3a7f6ff | ||
|
7ca2ea0766 | ||
|
89220c142b | ||
|
c73ce3d220 | ||
|
b0f127af4e | ||
|
766d54795f | ||
|
bd41c6eea4 | ||
|
2435786713 | ||
|
9e7ea64bd2 | ||
|
89a6eee6af | ||
|
2ec1476e50 | ||
|
2d9b581f34 | ||
|
5bb63f645b | ||
|
a856c7cc37 | ||
|
26db9d8a9d | ||
|
8060179f6d | ||
|
77ebd87fed | ||
|
e4bc92235d | ||
|
27a4d83ce8 | ||
|
ece9b902f8 | ||
|
65a2f8a68b | ||
|
9c212306b8 | ||
|
1fdc7ce6bb | ||
|
0b22c140c5 | ||
|
944aa45459 | ||
|
c9842ba13a | ||
|
8840680303 | ||
|
376b9b1316 | ||
|
54bb1cb3d9 | ||
|
43468b474e | ||
|
28a957c684 | ||
|
ec5ddbf391 | ||
|
bab186e195 | ||
|
bc7e874476 | ||
|
97114b5948 | ||
|
45e015d71d | ||
|
0ff6531953 | ||
|
ba298c3cfc | ||
|
0479bea40b | ||
|
a536097804 | ||
|
bbefd0fdf9 | ||
|
2aa8b04c21 | ||
|
aeebdfec51 | ||
|
debfcdf498 | ||
|
5c4b33e8e6 | ||
|
eb54037b66 | ||
|
f48af8db3b | ||
|
97c5b957dd | ||
|
95e7397803 | ||
|
43a989978a | ||
|
27734a7c26 | ||
|
dd786d6fc4 | ||
|
be1c28fc45 | ||
|
20e41b3523 | ||
|
e07ecc5cf8 | ||
|
3360b72531 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,4 +13,5 @@ env/
|
|||||||
.settings
|
.settings
|
||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
tests/bugfix.py
|
tests/test_bugfix.py
|
||||||
|
htmlcov/
|
12
.travis.yml
Normal file
12
.travis.yml
Normal 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
|
7
AUTHORS
7
AUTHORS
@@ -102,4 +102,9 @@ that much better:
|
|||||||
* mostlystatic
|
* mostlystatic
|
||||||
* Greg Banks
|
* Greg Banks
|
||||||
* swashbuckler
|
* swashbuckler
|
||||||
* Adam Reeve
|
* Adam Reeve
|
||||||
|
* Anthony Nemitz
|
||||||
|
* deignacio
|
||||||
|
* shaunduncan
|
||||||
|
* Meir Kriheli
|
||||||
|
* Andrey Fedoseev
|
@@ -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!
|
||||||
|
|
||||||
|
@@ -2,6 +2,42 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Changes in 0.6.11
|
||||||
|
==================
|
||||||
|
- Fixed inconsistency handling None values field attrs
|
||||||
|
- Fixed map_field embedded db_field issue
|
||||||
|
- Fixed .save() _delta issue with DbRefs
|
||||||
|
- Fixed Django TestCase
|
||||||
|
- Added cmp to Embedded Document
|
||||||
|
- Added PULL reverse_delete_rule
|
||||||
|
- Fixed CASCADE delete bug
|
||||||
|
- Fixed db_field data load error
|
||||||
|
- Fixed recursive save with FileField
|
||||||
|
|
||||||
|
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
|
Changes in 0.6.7
|
||||||
================
|
================
|
||||||
- Fixed indexing on '_id' or 'pk' or 'id'
|
- Fixed indexing on '_id' or 'pk' or 'id'
|
||||||
|
@@ -289,6 +289,10 @@ Its value can take any of the following constants:
|
|||||||
:const:`mongoengine.CASCADE`
|
:const:`mongoengine.CASCADE`
|
||||||
Any object containing fields that are refererring to the object being deleted
|
Any object containing fields that are refererring to the object being deleted
|
||||||
are deleted first.
|
are deleted first.
|
||||||
|
:const:`mongoengine.PULL`
|
||||||
|
Removes the reference to the object (using MongoDB's "pull" operation)
|
||||||
|
from any object's fields of
|
||||||
|
:class:`~mongoengine.ListField` (:class:`~mongoengine.ReferenceField`).
|
||||||
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
@@ -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
|
||||||
|
@@ -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, 7)
|
VERSION = (0, 6, 11)
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
|
@@ -223,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:
|
||||||
@@ -400,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
|
||||||
@@ -433,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.
|
||||||
"""
|
"""
|
||||||
@@ -837,6 +798,7 @@ class BaseDocument(object):
|
|||||||
dynamic_data[key] = value
|
dynamic_data[key] = value
|
||||||
else:
|
else:
|
||||||
for key, value in values.items():
|
for key, value in values.items():
|
||||||
|
key = self._reverse_db_field_map.get(key, key)
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
# Set any get_fieldname_display methods
|
# Set any get_fieldname_display methods
|
||||||
@@ -857,7 +819,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
|
||||||
|
|
||||||
@@ -870,13 +833,6 @@ class BaseDocument(object):
|
|||||||
if hasattr(self, '_changed_fields'):
|
if hasattr(self, '_changed_fields'):
|
||||||
self._mark_as_changed(name)
|
self._mark_as_changed(name)
|
||||||
|
|
||||||
# Handle None values for required fields
|
|
||||||
if value is None and name in getattr(self, '_fields', {}):
|
|
||||||
self._data[name] = value
|
|
||||||
if hasattr(self, '_changed_fields'):
|
|
||||||
self._mark_as_changed(name)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._created and name in self._meta.get('shard_key', tuple()):
|
if not self._created and name in self._meta.get('shard_key', tuple()):
|
||||||
from queryset import OperationError
|
from queryset import OperationError
|
||||||
raise OperationError("Shard Keys are immutable. Tried to update %s" % name)
|
raise OperationError("Shard Keys are immutable. Tried to update %s" % name)
|
||||||
@@ -1088,9 +1044,7 @@ Invalid data to create a `%s` instance.\n%s""".strip() % (cls._class_name, error
|
|||||||
parts = path.split('.')
|
parts = path.split('.')
|
||||||
d = doc
|
d = doc
|
||||||
for p in parts:
|
for p in parts:
|
||||||
if hasattr(d, '__getattr__'):
|
if p.isdigit():
|
||||||
d = getattr(p, d)
|
|
||||||
elif p.isdigit():
|
|
||||||
d = d[int(p)]
|
d = d[int(p)]
|
||||||
else:
|
else:
|
||||||
d = d.get(p)
|
d = d.get(p)
|
||||||
@@ -1255,15 +1209,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
|
||||||
@@ -1317,23 +1271,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
|
||||||
@@ -1346,19 +1300,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'):
|
||||||
|
@@ -65,6 +65,8 @@ def register_connection(alias, name, host='localhost', port=27017,
|
|||||||
})
|
})
|
||||||
if "replicaSet" in host:
|
if "replicaSet" in host:
|
||||||
conn_settings['replicaSet'] = True
|
conn_settings['replicaSet'] = True
|
||||||
|
|
||||||
|
conn_settings.update(kwargs)
|
||||||
_connection_settings[alias] = conn_settings
|
_connection_settings[alias] = conn_settings
|
||||||
|
|
||||||
|
|
||||||
|
@@ -114,7 +114,7 @@ class DeReference(object):
|
|||||||
doc = get_document(ref["_cls"])._from_son(ref)
|
doc = get_document(ref["_cls"])._from_son(ref)
|
||||||
elif doc_type is None:
|
elif doc_type is None:
|
||||||
doc = get_document(
|
doc = get_document(
|
||||||
''.join(x.capitalize()
|
''.join(x.capitalize()
|
||||||
for x in col.split('_')))._from_son(ref)
|
for x in col.split('_')))._from_son(ref)
|
||||||
else:
|
else:
|
||||||
doc = doc_type._from_son(ref)
|
doc = doc_type._from_son(ref)
|
||||||
|
@@ -66,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}
|
||||||
]
|
]
|
||||||
|
@@ -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()
|
||||||
|
@@ -10,7 +10,7 @@ class MongoTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
|
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
|
||||||
def __init__(self, methodName='runtest'):
|
def __init__(self, methodName='runtest'):
|
||||||
self.db = connect(self.db_name)
|
self.db = connect(self.db_name).get_db()
|
||||||
super(MongoTestCase, self).__init__(methodName)
|
super(MongoTestCase, self).__init__(methodName)
|
||||||
|
|
||||||
def _post_teardown(self):
|
def _post_teardown(self):
|
||||||
|
@@ -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
|
||||||
@@ -39,6 +40,11 @@ class EmbeddedDocument(BaseDocument):
|
|||||||
else:
|
else:
|
||||||
super(EmbeddedDocument, self).__delattr__(*args, **kwargs)
|
super(EmbeddedDocument, self).__delattr__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
return self._data == other._data
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Document(BaseDocument):
|
class Document(BaseDocument):
|
||||||
"""The base class used for defining the structure and properties of
|
"""The base class used for defining the structure and properties of
|
||||||
@@ -220,6 +226,7 @@ class Document(BaseDocument):
|
|||||||
if cascade_kwargs: # Allow granular control over cascades
|
if cascade_kwargs: # Allow granular control over cascades
|
||||||
kwargs.update(cascade_kwargs)
|
kwargs.update(cascade_kwargs)
|
||||||
kwargs['_refs'] = _refs
|
kwargs['_refs'] = _refs
|
||||||
|
self._changed_fields = []
|
||||||
self.cascade_save(**kwargs)
|
self.cascade_save(**kwargs)
|
||||||
|
|
||||||
except pymongo.errors.OperationFailure, err:
|
except pymongo.errors.OperationFailure, err:
|
||||||
@@ -233,6 +240,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"""
|
||||||
@@ -358,7 +366,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::
|
||||||
|
@@ -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']
|
||||||
@@ -182,7 +182,7 @@ class FloatField(BaseField):
|
|||||||
if isinstance(value, int):
|
if isinstance(value, int):
|
||||||
value = float(value)
|
value = float(value)
|
||||||
if not isinstance(value, float):
|
if not isinstance(value, float):
|
||||||
self.error('FoatField only accepts float values')
|
self.error('FloatField only accepts float values')
|
||||||
|
|
||||||
if self.min_value is not None and value < self.min_value:
|
if self.min_value is not None and value < self.min_value:
|
||||||
self.error('Float value is too small')
|
self.error('Float value is too small')
|
||||||
@@ -369,7 +369,7 @@ class ComplexDateTimeField(StringField):
|
|||||||
return self._convert_from_string(data)
|
return self._convert_from_string(data)
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
value = self._convert_from_datetime(value)
|
value = self._convert_from_datetime(value) if value else value
|
||||||
return super(ComplexDateTimeField, self).__set__(instance, value)
|
return super(ComplexDateTimeField, self).__set__(instance, value)
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
@@ -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.
|
||||||
@@ -612,6 +656,18 @@ class ReferenceField(BaseField):
|
|||||||
* NULLIFY - Updates the reference to null.
|
* NULLIFY - Updates the reference to null.
|
||||||
* 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.
|
||||||
|
* PULL - Pull the reference from a :class:`~mongoengine.ListField` of references
|
||||||
|
|
||||||
|
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 +715,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 +757,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 +793,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]
|
||||||
|
|
||||||
@@ -829,6 +890,13 @@ class GridFSProxy(object):
|
|||||||
self_dict['_fs'] = None
|
self_dict['_fs'] = None
|
||||||
return self_dict
|
return self_dict
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<%s: %s>' % (self.__class__.__name__, self.grid_id)
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
return cmp((self.grid_id, self.collection_name, self.db_alias),
|
||||||
|
(other.grid_id, other.collection_name, other.db_alias))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fs(self):
|
def fs(self):
|
||||||
if not self._fs:
|
if not self._fs:
|
||||||
@@ -927,15 +995,16 @@ 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
|
||||||
|
@@ -10,7 +10,7 @@ from bson.code import Code
|
|||||||
from mongoengine import signals
|
from mongoengine import signals
|
||||||
|
|
||||||
__all__ = ['queryset_manager', 'Q', 'InvalidQueryError',
|
__all__ = ['queryset_manager', 'Q', 'InvalidQueryError',
|
||||||
'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY']
|
'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL']
|
||||||
|
|
||||||
|
|
||||||
# The maximum number of items to display in a QuerySet.__repr__
|
# The maximum number of items to display in a QuerySet.__repr__
|
||||||
@@ -21,6 +21,7 @@ DO_NOTHING = 0
|
|||||||
NULLIFY = 1
|
NULLIFY = 1
|
||||||
CASCADE = 2
|
CASCADE = 2
|
||||||
DENY = 3
|
DENY = 3
|
||||||
|
PULL = 4
|
||||||
|
|
||||||
|
|
||||||
class DoesNotExist(Exception):
|
class DoesNotExist(Exception):
|
||||||
@@ -512,6 +513,10 @@ class QuerySet(object):
|
|||||||
key = '.'.join(parts)
|
key = '.'.join(parts)
|
||||||
index_list.append((key, direction))
|
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
|
# 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):
|
if use_types and not all(f._index_with_types for f in fields):
|
||||||
use_types = False
|
use_types = False
|
||||||
@@ -615,6 +620,7 @@ class QuerySet(object):
|
|||||||
"Can't use index on unsubscriptable field (%s)" % err)
|
"Can't use index on unsubscriptable field (%s)" % err)
|
||||||
fields.append(field_name)
|
fields.append(field_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if field is None:
|
if field is None:
|
||||||
# Look up first field from the document
|
# Look up first field from the document
|
||||||
if field_name == 'pk':
|
if field_name == 'pk':
|
||||||
@@ -623,8 +629,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)
|
||||||
@@ -632,8 +638,11 @@ class QuerySet(object):
|
|||||||
from mongoengine.fields import ReferenceField, GenericReferenceField
|
from mongoengine.fields import ReferenceField, GenericReferenceField
|
||||||
if isinstance(field, (ReferenceField, GenericReferenceField)):
|
if isinstance(field, (ReferenceField, GenericReferenceField)):
|
||||||
raise InvalidQueryError('Cannot perform join in mongoDB: %s' % '__'.join(parts))
|
raise InvalidQueryError('Cannot perform join in mongoDB: %s' % '__'.join(parts))
|
||||||
# Look up subfield on the previous field
|
if getattr(field, 'field', None):
|
||||||
new_field = field.lookup_member(field_name)
|
new_field = field.field.lookup_member(field_name)
|
||||||
|
else:
|
||||||
|
# Look up subfield on the previous field
|
||||||
|
new_field = field.lookup_member(field_name)
|
||||||
from base import ComplexBaseField
|
from base import ComplexBaseField
|
||||||
if not new_field and isinstance(field, ComplexBaseField):
|
if not new_field and isinstance(field, ComplexBaseField):
|
||||||
fields.append(field_name)
|
fields.append(field_name)
|
||||||
@@ -793,6 +802,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`
|
||||||
@@ -1305,11 +1319,17 @@ class QuerySet(object):
|
|||||||
document_cls, field_name = rule_entry
|
document_cls, field_name = rule_entry
|
||||||
rule = doc._meta['delete_rules'][rule_entry]
|
rule = doc._meta['delete_rules'][rule_entry]
|
||||||
if rule == CASCADE:
|
if rule == CASCADE:
|
||||||
document_cls.objects(**{field_name + '__in': self}).delete(safe=safe)
|
ref_q = document_cls.objects(**{field_name + '__in': self})
|
||||||
|
if doc != document_cls or (doc == document_cls and ref_q.count() > 0):
|
||||||
|
ref_q.delete(safe=safe)
|
||||||
elif rule == NULLIFY:
|
elif rule == NULLIFY:
|
||||||
document_cls.objects(**{field_name + '__in': self}).update(
|
document_cls.objects(**{field_name + '__in': self}).update(
|
||||||
safe_update=safe,
|
safe_update=safe,
|
||||||
**{'unset__%s' % field_name: 1})
|
**{'unset__%s' % field_name: 1})
|
||||||
|
elif rule == PULL:
|
||||||
|
document_cls.objects(**{field_name + '__in': self}).update(
|
||||||
|
safe_update=safe,
|
||||||
|
**{'pull_all__%s' % field_name: self})
|
||||||
|
|
||||||
self._collection.remove(self._query, safe=safe)
|
self._collection.remove(self._query, safe=safe)
|
||||||
|
|
||||||
@@ -1375,9 +1395,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
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
%define srcname mongoengine
|
%define srcname mongoengine
|
||||||
|
|
||||||
Name: python-%{srcname}
|
Name: python-%{srcname}
|
||||||
Version: 0.6.7
|
Version: 0.6.11
|
||||||
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
|
||||||
|
|
||||||
|
13
setup.cfg
Normal file
13
setup.cfg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[aliases]
|
||||||
|
test = nosetests
|
||||||
|
|
||||||
|
[nosetests]
|
||||||
|
verbosity = 2
|
||||||
|
detailed-errors = 1
|
||||||
|
#with-coverage = 1
|
||||||
|
cover-html = 1
|
||||||
|
cover-html-dir = ../htmlcov
|
||||||
|
cover-package = mongoengine
|
||||||
|
cover-erase = 1
|
||||||
|
where = tests
|
||||||
|
#tests = test_bugfix.py
|
3
setup.py
3
setup.py
@@ -48,6 +48,5 @@ setup(name='mongoengine',
|
|||||||
platforms=['any'],
|
platforms=['any'],
|
||||||
classifiers=CLASSIFIERS,
|
classifiers=CLASSIFIERS,
|
||||||
install_requires=['pymongo'],
|
install_requires=['pymongo'],
|
||||||
test_suite='tests',
|
tests_require=['nose', 'coverage', 'blinker', 'django>=1.3', 'PIL']
|
||||||
tests_require=['blinker', 'django>=1.3', 'PIL']
|
|
||||||
)
|
)
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
import unittest
|
import datetime
|
||||||
import pymongo
|
import pymongo
|
||||||
|
import unittest
|
||||||
|
|
||||||
import mongoengine.connection
|
import mongoengine.connection
|
||||||
|
|
||||||
|
from bson.tz_util import utc
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import get_db, get_connection, ConnectionError
|
from mongoengine.connection import get_db, get_connection, ConnectionError
|
||||||
|
|
||||||
@@ -65,6 +68,31 @@ 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)
|
||||||
|
|
||||||
|
def test_datetime(self):
|
||||||
|
connect('mongoenginetest', tz_aware=True)
|
||||||
|
d = datetime.datetime(2010, 5, 5, tzinfo=utc)
|
||||||
|
|
||||||
|
class DateDoc(Document):
|
||||||
|
the_date = DateTimeField(required=True)
|
||||||
|
|
||||||
|
DateDoc.drop_collection()
|
||||||
|
DateDoc(the_date=d).save()
|
||||||
|
|
||||||
|
date_doc = DateDoc.objects.first()
|
||||||
|
self.assertEqual(d, date_doc.the_date)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
@@ -103,3 +103,8 @@ class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
|
|||||||
MongoSession.drop_collection()
|
MongoSession.drop_collection()
|
||||||
super(MongoDBSessionTest, self).setUp()
|
super(MongoDBSessionTest, self).setUp()
|
||||||
|
|
||||||
|
def test_first_save(self):
|
||||||
|
session = SessionStore()
|
||||||
|
session['test'] = True
|
||||||
|
session.save()
|
||||||
|
self.assertTrue('test' in session)
|
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import pymongo
|
import pymongo
|
||||||
import bson
|
import bson
|
||||||
@@ -6,13 +7,15 @@ import warnings
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fixtures import Base, Mixin, PickleEmbedded, PickleTest
|
from tests.fixtures import Base, Mixin, PickleEmbedded, PickleTest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.base import NotRegistered, InvalidDocumentError
|
from mongoengine.base import NotRegistered, InvalidDocumentError
|
||||||
from mongoengine.queryset import InvalidQueryError
|
from mongoengine.queryset import InvalidQueryError
|
||||||
from mongoengine.connection import get_db
|
from mongoengine.connection import get_db
|
||||||
|
|
||||||
|
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png')
|
||||||
|
|
||||||
|
|
||||||
class DocumentTest(unittest.TestCase):
|
class DocumentTest(unittest.TestCase):
|
||||||
|
|
||||||
@@ -661,6 +664,26 @@ class DocumentTest(unittest.TestCase):
|
|||||||
|
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
def test_db_field_load(self):
|
||||||
|
"""Ensure we load data correctly
|
||||||
|
"""
|
||||||
|
class Person(Document):
|
||||||
|
name = StringField(required=True)
|
||||||
|
_rank = StringField(required=False, db_field="rank")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rank(self):
|
||||||
|
return self._rank or "Private"
|
||||||
|
|
||||||
|
Person.drop_collection()
|
||||||
|
|
||||||
|
Person(name="Jack", _rank="Corporal").save()
|
||||||
|
|
||||||
|
Person(name="Fred").save()
|
||||||
|
|
||||||
|
self.assertEquals(Person.objects.get(name="Jack").rank, "Corporal")
|
||||||
|
self.assertEquals(Person.objects.get(name="Fred").rank, "Private")
|
||||||
|
|
||||||
def test_explicit_geo2d_index(self):
|
def test_explicit_geo2d_index(self):
|
||||||
"""Ensure that geo2d indexes work when created via meta[indexes]
|
"""Ensure that geo2d indexes work when created via meta[indexes]
|
||||||
"""
|
"""
|
||||||
@@ -1328,6 +1351,30 @@ class DocumentTest(unittest.TestCase):
|
|||||||
p0.name = 'wpjunior'
|
p0.name = 'wpjunior'
|
||||||
p0.save()
|
p0.save()
|
||||||
|
|
||||||
|
def test_save_max_recursion_not_hit_with_file_field(self):
|
||||||
|
|
||||||
|
class Foo(Document):
|
||||||
|
name = StringField()
|
||||||
|
picture = FileField()
|
||||||
|
bar = ReferenceField('self')
|
||||||
|
|
||||||
|
Foo.drop_collection()
|
||||||
|
|
||||||
|
a = Foo(name='hello')
|
||||||
|
a.save()
|
||||||
|
|
||||||
|
a.bar = a
|
||||||
|
a.picture = open(TEST_IMAGE_PATH, 'rb')
|
||||||
|
a.save()
|
||||||
|
|
||||||
|
# Confirm can save and it resets the changed fields without hitting
|
||||||
|
# max recursion error
|
||||||
|
b = Foo.objects.with_id(a.id)
|
||||||
|
b.name='world'
|
||||||
|
b.save()
|
||||||
|
|
||||||
|
self.assertEquals(b.picture, b.bar.picture, b.bar.bar.picture)
|
||||||
|
|
||||||
def test_save_cascades(self):
|
def test_save_cascades(self):
|
||||||
|
|
||||||
class Person(Document):
|
class Person(Document):
|
||||||
@@ -1591,6 +1638,35 @@ class DocumentTest(unittest.TestCase):
|
|||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
self.assertEqual(site.page.log_message, "Error: Dummy message")
|
self.assertEqual(site.page.log_message, "Error: Dummy message")
|
||||||
|
|
||||||
|
def test_circular_reference_deltas(self):
|
||||||
|
|
||||||
|
class Person(Document):
|
||||||
|
name = StringField()
|
||||||
|
owns = ListField(ReferenceField('Organization'))
|
||||||
|
|
||||||
|
class Organization(Document):
|
||||||
|
name = StringField()
|
||||||
|
owner = ReferenceField('Person')
|
||||||
|
|
||||||
|
Person.drop_collection()
|
||||||
|
Organization.drop_collection()
|
||||||
|
|
||||||
|
person = Person(name="owner")
|
||||||
|
person.save()
|
||||||
|
organization = Organization(name="company")
|
||||||
|
organization.save()
|
||||||
|
|
||||||
|
person.owns.append(organization)
|
||||||
|
organization.owner = person
|
||||||
|
|
||||||
|
person.save()
|
||||||
|
organization.save()
|
||||||
|
|
||||||
|
p = Person.objects[0].select_related()
|
||||||
|
o = Organization.objects.first()
|
||||||
|
self.assertEquals(p.owns[0], o)
|
||||||
|
self.assertEquals(o.owner, p)
|
||||||
|
|
||||||
def test_delta(self):
|
def test_delta(self):
|
||||||
|
|
||||||
class Doc(Document):
|
class Doc(Document):
|
||||||
@@ -2496,6 +2572,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():
|
||||||
@@ -2954,5 +3064,21 @@ name: Field is required ("name")"""
|
|||||||
'username': 'Field is required ("username")',
|
'username': 'Field is required ("username")',
|
||||||
'name': u'Field is required ("name")'})
|
'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()
|
@@ -3,6 +3,8 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
import uuid
|
import uuid
|
||||||
import StringIO
|
import StringIO
|
||||||
|
import tempfile
|
||||||
|
import gridfs
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
@@ -19,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.
|
||||||
"""
|
"""
|
||||||
@@ -76,7 +82,6 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Retrive data from db and verify it.
|
# Retrive data from db and verify it.
|
||||||
ret = HandleNoneFields.objects.all()[0]
|
ret = HandleNoneFields.objects.all()[0]
|
||||||
|
|
||||||
self.assertEqual(ret.str_fld, None)
|
self.assertEqual(ret.str_fld, None)
|
||||||
self.assertEqual(ret.int_fld, None)
|
self.assertEqual(ret.int_fld, None)
|
||||||
self.assertEqual(ret.flt_fld, None)
|
self.assertEqual(ret.flt_fld, None)
|
||||||
@@ -907,6 +912,48 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
Extensible.drop_collection()
|
Extensible.drop_collection()
|
||||||
|
|
||||||
|
def test_embedded_mapfield_db_field(self):
|
||||||
|
|
||||||
|
class Embedded(EmbeddedDocument):
|
||||||
|
number = IntField(default=0, db_field='i')
|
||||||
|
|
||||||
|
class Test(Document):
|
||||||
|
my_map = MapField(field=EmbeddedDocumentField(Embedded), db_field='x')
|
||||||
|
|
||||||
|
Test.drop_collection()
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.my_map['DICTIONARY_KEY'] = Embedded(number=1)
|
||||||
|
test.save()
|
||||||
|
|
||||||
|
Test.objects.update_one(inc__my_map__DICTIONARY_KEY__number=1)
|
||||||
|
|
||||||
|
test = Test.objects.get()
|
||||||
|
self.assertEqual(test.my_map['DICTIONARY_KEY'].number, 2)
|
||||||
|
doc = self.db.test.find_one()
|
||||||
|
self.assertEqual(doc['x']['DICTIONARY_KEY']['i'], 2)
|
||||||
|
|
||||||
|
def test_embedded_db_field(self):
|
||||||
|
|
||||||
|
class Embedded(EmbeddedDocument):
|
||||||
|
number = IntField(default=0, db_field='i')
|
||||||
|
|
||||||
|
class Test(Document):
|
||||||
|
embedded = EmbeddedDocumentField(Embedded, db_field='x')
|
||||||
|
|
||||||
|
Test.drop_collection()
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.embedded = Embedded(number=1)
|
||||||
|
test.save()
|
||||||
|
|
||||||
|
Test.objects.update_one(inc__embedded__number=1)
|
||||||
|
|
||||||
|
test = Test.objects.get()
|
||||||
|
self.assertEqual(test.embedded.number, 2)
|
||||||
|
doc = self.db.test.find_one()
|
||||||
|
self.assertEqual(doc['x']['i'], 2)
|
||||||
|
|
||||||
def test_embedded_document_validation(self):
|
def test_embedded_document_validation(self):
|
||||||
"""Ensure that invalid embedded documents cannot be assigned to
|
"""Ensure that invalid embedded documents cannot be assigned to
|
||||||
embedded document fields.
|
embedded document fields.
|
||||||
@@ -1301,6 +1348,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.
|
||||||
"""
|
"""
|
||||||
@@ -1546,6 +1661,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
|
||||||
"""
|
"""
|
||||||
@@ -1844,6 +2002,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()
|
||||||
@@ -1857,6 +2017,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.
|
||||||
"""
|
"""
|
@@ -572,7 +572,7 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
|
|
||||||
def throw_operation_error_not_unique():
|
def throw_operation_error_not_unique():
|
||||||
Blog.objects.insert([blog2, blog3], safe=True)
|
Blog.objects.insert([blog2, blog3], safe=True)
|
||||||
|
|
||||||
self.assertRaises(OperationError, throw_operation_error_not_unique)
|
self.assertRaises(OperationError, throw_operation_error_not_unique)
|
||||||
self.assertEqual(Blog.objects.count(), 2)
|
self.assertEqual(Blog.objects.count(), 2)
|
||||||
|
|
||||||
@@ -1344,6 +1344,37 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
self.Person.objects(name='Test User').delete()
|
self.Person.objects(name='Test User').delete()
|
||||||
self.assertEqual(1, BlogPost.objects.count())
|
self.assertEqual(1, BlogPost.objects.count())
|
||||||
|
|
||||||
|
def test_reverse_delete_rule_cascade_self_referencing(self):
|
||||||
|
"""Ensure self-referencing CASCADE deletes do not result in infinite loop
|
||||||
|
"""
|
||||||
|
class Category(Document):
|
||||||
|
name = StringField()
|
||||||
|
parent = ReferenceField('self', reverse_delete_rule=CASCADE)
|
||||||
|
|
||||||
|
num_children = 3
|
||||||
|
base = Category(name='Root')
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
# Create a simple parent-child tree
|
||||||
|
for i in range(num_children):
|
||||||
|
child_name = 'Child-%i' % i
|
||||||
|
child = Category(name=child_name, parent=base)
|
||||||
|
child.save()
|
||||||
|
|
||||||
|
for i in range(num_children):
|
||||||
|
child_child_name = 'Child-Child-%i' % i
|
||||||
|
child_child = Category(name=child_child_name, parent=child)
|
||||||
|
child_child.save()
|
||||||
|
|
||||||
|
tree_size = 1 + num_children + (num_children * num_children)
|
||||||
|
self.assertEquals(tree_size, Category.objects.count())
|
||||||
|
self.assertEquals(num_children, Category.objects(parent=base).count())
|
||||||
|
|
||||||
|
# The delete should effectively wipe out the Category collection
|
||||||
|
# without resulting in infinite parent-child cascade recursion
|
||||||
|
base.delete()
|
||||||
|
self.assertEquals(0, Category.objects.count())
|
||||||
|
|
||||||
def test_reverse_delete_rule_nullify(self):
|
def test_reverse_delete_rule_nullify(self):
|
||||||
"""Ensure nullification of references to deleted documents.
|
"""Ensure nullification of references to deleted documents.
|
||||||
"""
|
"""
|
||||||
@@ -1388,6 +1419,36 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(OperationError, self.Person.objects.delete)
|
self.assertRaises(OperationError, self.Person.objects.delete)
|
||||||
|
|
||||||
|
def test_reverse_delete_rule_pull(self):
|
||||||
|
"""Ensure pulling of references to deleted documents.
|
||||||
|
"""
|
||||||
|
class BlogPost(Document):
|
||||||
|
content = StringField()
|
||||||
|
authors = ListField(ReferenceField(self.Person,
|
||||||
|
reverse_delete_rule=PULL))
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
self.Person.drop_collection()
|
||||||
|
|
||||||
|
me = self.Person(name='Test User')
|
||||||
|
me.save()
|
||||||
|
|
||||||
|
someoneelse = self.Person(name='Some-one Else')
|
||||||
|
someoneelse.save()
|
||||||
|
|
||||||
|
post = BlogPost(content='Watching TV', authors=[me, someoneelse])
|
||||||
|
post.save()
|
||||||
|
|
||||||
|
another = BlogPost(content='Chilling Out', authors=[someoneelse])
|
||||||
|
another.save()
|
||||||
|
|
||||||
|
someoneelse.delete()
|
||||||
|
post.reload()
|
||||||
|
another.reload()
|
||||||
|
|
||||||
|
self.assertEqual(post.authors, [me])
|
||||||
|
self.assertEqual(another.authors, [])
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
"""Ensure that atomic updates work properly.
|
"""Ensure that atomic updates work properly.
|
||||||
"""
|
"""
|
||||||
@@ -1471,6 +1532,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):
|
||||||
|
|
Reference in New Issue
Block a user