Added support for expando style dynamic documents.
Added two new classes: DynamicDocument and DynamicEmbeddedDocument for handling expando style setting of attributes. [closes #112]
This commit is contained in:
parent
1af54f93f5
commit
a7edd8602c
@ -21,6 +21,12 @@ Documents
|
|||||||
.. autoclass:: mongoengine.EmbeddedDocument
|
.. autoclass:: mongoengine.EmbeddedDocument
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: mongoengine.DynamicDocument
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: mongoengine.DynamicEmbeddedDocument
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: mongoengine.document.MapReduceDocument
|
.. autoclass:: mongoengine.document.MapReduceDocument
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Changes in dev
|
||||||
|
==============
|
||||||
|
|
||||||
|
- Added DynamicDocument and EmbeddedDynamicDocument classes for expando schemas
|
||||||
|
|
||||||
Changes in v0.5
|
Changes in v0.5
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
@ -24,6 +24,34 @@ objects** as class attributes to the document class::
|
|||||||
title = StringField(max_length=200, required=True)
|
title = StringField(max_length=200, required=True)
|
||||||
date_modified = DateTimeField(default=datetime.datetime.now)
|
date_modified = DateTimeField(default=datetime.datetime.now)
|
||||||
|
|
||||||
|
Dynamic document schemas
|
||||||
|
========================
|
||||||
|
One of the benefits of MongoDb is dynamic schemas for a collection, whilst data
|
||||||
|
should be planned and organised (after all explicit is better than implicit!)
|
||||||
|
there are scenarios where having dynamic / expando style documents is desirable.
|
||||||
|
|
||||||
|
:class:`~mongoengine.DynamicDocument` documents work in the same way as
|
||||||
|
:class:`~mongoengine.Document` but any data / attributes set to them will also
|
||||||
|
be saved ::
|
||||||
|
|
||||||
|
from mongoengine import *
|
||||||
|
|
||||||
|
class Page(DynamicDocument):
|
||||||
|
title = StringField(max_length=200, required=True)
|
||||||
|
|
||||||
|
# Create a new page and add tags
|
||||||
|
>>> page = Page(title='Using MongoEngine')
|
||||||
|
>>> page.tags = ['mongodb', 'mongoengine']
|
||||||
|
>>> page.save()
|
||||||
|
|
||||||
|
>>> Page.objects(tags='mongoengine').count()
|
||||||
|
>>> 1
|
||||||
|
|
||||||
|
..note::
|
||||||
|
|
||||||
|
There is one caveat on Dynamic Documents: fields cannot start with `_`
|
||||||
|
|
||||||
|
|
||||||
Fields
|
Fields
|
||||||
======
|
======
|
||||||
By default, fields are not required. To make a field mandatory, set the
|
By default, fields are not required. To make a field mandatory, set the
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
Upgrading
|
Upgrading
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.5 to 0.6
|
||||||
|
==========
|
||||||
|
|
||||||
|
TBC
|
||||||
|
|
||||||
0.4 to 0.5
|
0.4 to 0.5
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
@ -301,6 +301,40 @@ 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
|
||||||
|
|
||||||
class ObjectIdField(BaseField):
|
class ObjectIdField(BaseField):
|
||||||
"""An field wrapper around MongoDB's ObjectIds.
|
"""An field wrapper around MongoDB's ObjectIds.
|
||||||
"""
|
"""
|
||||||
@ -585,30 +619,98 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
|
|
||||||
class BaseDocument(object):
|
class BaseDocument(object):
|
||||||
|
|
||||||
|
_dynamic = False
|
||||||
|
|
||||||
def __init__(self, **values):
|
def __init__(self, **values):
|
||||||
signals.pre_init.send(self.__class__, document=self, values=values)
|
signals.pre_init.send(self.__class__, document=self, values=values)
|
||||||
|
|
||||||
self._data = {}
|
self._data = {}
|
||||||
self._initialised = False
|
self._initialised = False
|
||||||
|
|
||||||
# Assign default values to instance
|
# Assign default values to instance
|
||||||
for attr_name, field in self._fields.items():
|
for attr_name, field in self._fields.items():
|
||||||
value = getattr(self, attr_name, None)
|
value = getattr(self, attr_name, None)
|
||||||
setattr(self, attr_name, value)
|
setattr(self, attr_name, value)
|
||||||
|
|
||||||
# Assign initial values to instance
|
# Set passed values after initialisation
|
||||||
for attr_name in values.keys():
|
if self._dynamic:
|
||||||
try:
|
self._dynamic_fields = {}
|
||||||
value = values.pop(attr_name)
|
dynamic_data = {}
|
||||||
setattr(self, attr_name, value)
|
for key, value in values.items():
|
||||||
except AttributeError:
|
if key in self._fields or key == '_id':
|
||||||
pass
|
setattr(self, key, value)
|
||||||
|
elif self._dynamic:
|
||||||
|
dynamic_data[key] = value
|
||||||
|
else:
|
||||||
|
for key, value in values.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
# Set any get_fieldname_display methods
|
# Set any get_fieldname_display methodsF
|
||||||
self.__set_field_display()
|
self.__set_field_display()
|
||||||
# Flag initialised
|
# Flag initialised
|
||||||
self._initialised = True
|
self._initialised = True
|
||||||
|
|
||||||
|
if self._dynamic:
|
||||||
|
for key, value in dynamic_data.items():
|
||||||
|
setattr(self, key, value)
|
||||||
signals.post_init.send(self.__class__, document=self)
|
signals.post_init.send(self.__class__, document=self)
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
# Handle dynamic data only if an intialised dynamic document
|
||||||
|
if self._dynamic and getattr(self, '_initialised', False):
|
||||||
|
|
||||||
|
field = None
|
||||||
|
if not hasattr(self, name) and not name.startswith('_'):
|
||||||
|
field = BaseDynamicField(db_field=name)
|
||||||
|
field.name = name
|
||||||
|
self._dynamic_fields[name] = field
|
||||||
|
|
||||||
|
if not name.startswith('_'):
|
||||||
|
value = self.__expand_dynamic_values(name, value)
|
||||||
|
|
||||||
|
# Handle marking data as changed
|
||||||
|
if name in self._dynamic_fields:
|
||||||
|
self._data[name] = value
|
||||||
|
if hasattr(self, '_changed_fields'):
|
||||||
|
self._mark_as_changed(name)
|
||||||
|
|
||||||
|
super(BaseDocument, self).__setattr__(name, value)
|
||||||
|
|
||||||
|
def __expand_dynamic_values(self, name, value):
|
||||||
|
"""expand any dynamic values to their correct types / values"""
|
||||||
|
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)])
|
||||||
|
|
||||||
|
if not is_list and '_cls' in value:
|
||||||
|
cls = get_document(value['_cls'])
|
||||||
|
value = cls(**value)
|
||||||
|
value._dynamic = True
|
||||||
|
value._changed_fields = []
|
||||||
|
return value
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
for k, v in value.items():
|
||||||
|
key = name if is_list else k
|
||||||
|
data[k] = self.__expand_dynamic_values(key, 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
|
||||||
|
|
||||||
|
# Convert lists / values so we can watch for any changes on them
|
||||||
|
if isinstance(value, (list, tuple)) and not isinstance(value, BaseList):
|
||||||
|
value = BaseList(value, instance=self, name=name)
|
||||||
|
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||||
|
value = BaseDict(value, instance=self, name=name)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
"""Ensure that all fields' values are valid and that required fields
|
"""Ensure that all fields' values are valid and that required fields
|
||||||
are present.
|
are present.
|
||||||
@ -653,6 +755,12 @@ class BaseDocument(object):
|
|||||||
data['_types'] = self._superclasses.keys() + [self._class_name]
|
data['_types'] = self._superclasses.keys() + [self._class_name]
|
||||||
if '_id' in data and data['_id'] is None:
|
if '_id' in data and data['_id'] is None:
|
||||||
del data['_id']
|
del data['_id']
|
||||||
|
|
||||||
|
if not self._dynamic:
|
||||||
|
return data
|
||||||
|
|
||||||
|
for name, field in self._dynamic_fields.items():
|
||||||
|
data[name] = field.to_mongo(self._data.get(name, None))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -727,14 +835,19 @@ class BaseDocument(object):
|
|||||||
def _get_changed_fields(self, key=''):
|
def _get_changed_fields(self, key=''):
|
||||||
"""Returns a list of all fields that have explicitly been changed.
|
"""Returns a list of all fields that have explicitly been changed.
|
||||||
"""
|
"""
|
||||||
from mongoengine import EmbeddedDocument
|
from mongoengine import EmbeddedDocument, DynamicEmbeddedDocument
|
||||||
_changed_fields = []
|
_changed_fields = []
|
||||||
_changed_fields += getattr(self, '_changed_fields', [])
|
_changed_fields += getattr(self, '_changed_fields', [])
|
||||||
for field_name in self._fields:
|
|
||||||
|
field_list = self._fields.copy()
|
||||||
|
if self._dynamic:
|
||||||
|
field_list.update(self._dynamic_fields)
|
||||||
|
|
||||||
|
for field_name in field_list:
|
||||||
db_field_name = self._db_field_map.get(field_name, field_name)
|
db_field_name = self._db_field_map.get(field_name, field_name)
|
||||||
key = '%s.' % db_field_name
|
key = '%s.' % db_field_name
|
||||||
field = getattr(self, field_name, None)
|
field = getattr(self, field_name, None)
|
||||||
if isinstance(field, EmbeddedDocument) and db_field_name not in _changed_fields: # Grab all embedded fields that have been changed
|
if isinstance(field, (EmbeddedDocument, DynamicEmbeddedDocument)) and db_field_name not in _changed_fields: # Grab all embedded fields that have been changed
|
||||||
_changed_fields += ["%s%s" % (key, k) for k in field._get_changed_fields(key) if k]
|
_changed_fields += ["%s%s" % (key, k) for k in field._get_changed_fields(key) if k]
|
||||||
elif isinstance(field, (list, tuple, dict)) and db_field_name not in _changed_fields: # Loop list / dict fields as they contain documents
|
elif isinstance(field, (list, tuple, dict)) and db_field_name not in _changed_fields: # Loop list / dict fields as they contain documents
|
||||||
# Determine the iterator to use
|
# Determine the iterator to use
|
||||||
@ -747,7 +860,6 @@ class BaseDocument(object):
|
|||||||
continue
|
continue
|
||||||
list_key = "%s%s." % (key, index)
|
list_key = "%s%s." % (key, index)
|
||||||
_changed_fields += ["%s%s" % (list_key, k) for k in value._get_changed_fields(list_key) if k]
|
_changed_fields += ["%s%s" % (list_key, k) for k in value._get_changed_fields(list_key) if k]
|
||||||
|
|
||||||
return _changed_fields
|
return _changed_fields
|
||||||
|
|
||||||
def _delta(self):
|
def _delta(self):
|
||||||
@ -785,8 +897,11 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
# If we've set a value that ain't the default value dont unset it.
|
# If we've set a value that ain't the default value dont unset it.
|
||||||
default = None
|
default = None
|
||||||
|
if self._dynamic and parts[0] in self._dynamic_fields:
|
||||||
if path in self._fields:
|
del(set_data[path])
|
||||||
|
unset_data[path] = 1
|
||||||
|
continue
|
||||||
|
elif path in self._fields:
|
||||||
default = self._fields[path].default
|
default = self._fields[path].default
|
||||||
else: # Perform a full lookup for lists / embedded lookups
|
else: # Perform a full lookup for lists / embedded lookups
|
||||||
d = self
|
d = self
|
||||||
@ -805,7 +920,10 @@ class BaseDocument(object):
|
|||||||
field_name = d._reverse_db_field_map.get(db_field_name,
|
field_name = d._reverse_db_field_map.get(db_field_name,
|
||||||
db_field_name)
|
db_field_name)
|
||||||
|
|
||||||
default = d._fields[field_name].default
|
if field_name in d._fields:
|
||||||
|
default = d._fields.get(field_name).default
|
||||||
|
else:
|
||||||
|
default = None
|
||||||
|
|
||||||
if default is not None:
|
if default is not None:
|
||||||
if callable(default):
|
if callable(default):
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
import operator
|
||||||
from mongoengine import signals
|
from mongoengine import signals
|
||||||
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
|
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
|
||||||
ValidationError, BaseDict, BaseList)
|
ValidationError, BaseDict, BaseList, BaseDynamicField)
|
||||||
from queryset import OperationError
|
from queryset import OperationError
|
||||||
from connection import _get_db
|
from connection import _get_db
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
|
|
||||||
__all__ = ['Document', 'EmbeddedDocument', 'ValidationError',
|
__all__ = ['Document', 'EmbeddedDocument', 'DynamicDocument', 'DynamicEmbeddedDocument',
|
||||||
'OperationError', 'InvalidCollectionError']
|
'ValidationError', 'OperationError', 'InvalidCollectionError']
|
||||||
|
|
||||||
|
|
||||||
class InvalidCollectionError(Exception):
|
class InvalidCollectionError(Exception):
|
||||||
@ -198,6 +199,7 @@ class Document(BaseDocument):
|
|||||||
reset_changed_fields(field, inspected_docs)
|
reset_changed_fields(field, inspected_docs)
|
||||||
|
|
||||||
reset_changed_fields(self)
|
reset_changed_fields(self)
|
||||||
|
self._changed_fields = []
|
||||||
signals.post_save.send(self.__class__, document=self, created=creation_mode)
|
signals.post_save.send(self.__class__, document=self, created=creation_mode)
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
@ -247,8 +249,12 @@ class Document(BaseDocument):
|
|||||||
"""
|
"""
|
||||||
id_field = self._meta['id_field']
|
id_field = self._meta['id_field']
|
||||||
obj = self.__class__.objects(**{id_field: self[id_field]}).first()
|
obj = self.__class__.objects(**{id_field: self[id_field]}).first()
|
||||||
|
|
||||||
for field in self._fields:
|
for field in self._fields:
|
||||||
setattr(self, field, self._reload(field, obj[field]))
|
setattr(self, field, self._reload(field, obj[field]))
|
||||||
|
if self._dynamic:
|
||||||
|
for name in self._dynamic_fields.keys():
|
||||||
|
setattr(self, name, self._reload(name, obj._data[name]))
|
||||||
self._changed_fields = []
|
self._changed_fields = []
|
||||||
|
|
||||||
def _reload(self, key, value):
|
def _reload(self, key, value):
|
||||||
@ -261,7 +267,7 @@ class Document(BaseDocument):
|
|||||||
elif isinstance(value, BaseList):
|
elif isinstance(value, BaseList):
|
||||||
value = [self._reload(key, v) for v in value]
|
value = [self._reload(key, v) for v in value]
|
||||||
value = BaseList(value, instance=self, name=key)
|
value = BaseList(value, instance=self, name=key)
|
||||||
elif isinstance(value, EmbeddedDocument):
|
elif isinstance(value, (EmbeddedDocument, DynamicEmbeddedDocument)):
|
||||||
value._changed_fields = []
|
value._changed_fields = []
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -289,6 +295,39 @@ class Document(BaseDocument):
|
|||||||
db.drop_collection(cls._get_collection_name())
|
db.drop_collection(cls._get_collection_name())
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicDocument(Document):
|
||||||
|
"""A Dynamic Document class allowing flexible, expandable and uncontrolled
|
||||||
|
schemas. As a :class:`~mongoengine.Document` subclass, acts in the same
|
||||||
|
way as an ordinary document but has expando style properties. Any data
|
||||||
|
passed or set against the :class:`~mongoengine.DynamicDocument` that is
|
||||||
|
not a field is automatically converted into a
|
||||||
|
:class:`~mongoengine.BaseDynamicField` and data can be attributed to that
|
||||||
|
field.
|
||||||
|
|
||||||
|
..note::
|
||||||
|
|
||||||
|
There is one caveat on Dynamic Documents: fields cannot start with `_`
|
||||||
|
"""
|
||||||
|
__metaclass__ = TopLevelDocumentMetaclass
|
||||||
|
_dynamic = True
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicEmbeddedDocument(EmbeddedDocument):
|
||||||
|
"""A Dynamic Embedded Document class allowing flexible, expandable and
|
||||||
|
uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more
|
||||||
|
information about dynamic documents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__metaclass__ = DocumentMetaclass
|
||||||
|
_dynamic = True
|
||||||
|
|
||||||
|
def __delattr__(self, *args, **kwargs):
|
||||||
|
"""Deletes the attribute by setting to None and allowing _delta to unset
|
||||||
|
it"""
|
||||||
|
field_name = args[0]
|
||||||
|
setattr(self, field_name, None)
|
||||||
|
|
||||||
|
|
||||||
class MapReduceDocument(object):
|
class MapReduceDocument(object):
|
||||||
"""A document returned from a map/reduce query.
|
"""A document returned from a map/reduce query.
|
||||||
|
|
||||||
|
@ -590,7 +590,14 @@ class QuerySet(object):
|
|||||||
if field_name == 'pk':
|
if field_name == 'pk':
|
||||||
# Deal with "primary key" alias
|
# Deal with "primary key" alias
|
||||||
field_name = document._meta['id_field']
|
field_name = document._meta['id_field']
|
||||||
field = document._fields[field_name]
|
if field_name in document._fields:
|
||||||
|
field = document._fields[field_name]
|
||||||
|
elif document._dynamic:
|
||||||
|
from base import BaseDynamicField
|
||||||
|
field = BaseDynamicField(db_field=field_name)
|
||||||
|
else:
|
||||||
|
raise InvalidQueryError('Cannot resolve field "%s"'
|
||||||
|
% field_name)
|
||||||
else:
|
else:
|
||||||
# Look up subfield on the previous field
|
# Look up subfield on the previous field
|
||||||
new_field = field.lookup_member(field_name)
|
new_field = field.lookup_member(field_name)
|
||||||
@ -603,7 +610,6 @@ class QuerySet(object):
|
|||||||
% field_name)
|
% field_name)
|
||||||
field = new_field # update field to the new field type
|
field = new_field # update field to the new field type
|
||||||
fields.append(field)
|
fields.append(field)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
413
tests/dynamic_document.py
Normal file
413
tests/dynamic_document.py
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from mongoengine import *
|
||||||
|
from mongoengine.connection import _get_db
|
||||||
|
|
||||||
|
class DynamicDocTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
connect(db='mongoenginetest')
|
||||||
|
self.db = _get_db()
|
||||||
|
|
||||||
|
class Person(DynamicDocument):
|
||||||
|
name = StringField()
|
||||||
|
|
||||||
|
Person.drop_collection()
|
||||||
|
|
||||||
|
self.Person = Person
|
||||||
|
|
||||||
|
def test_simple_dynamic_document(self):
|
||||||
|
"""Ensures simple dynamic documents are saved correctly"""
|
||||||
|
|
||||||
|
p = self.Person()
|
||||||
|
p.name = "James"
|
||||||
|
p.age = 34
|
||||||
|
|
||||||
|
self.assertEquals(p.to_mongo(),
|
||||||
|
{"_types": ["Person"], "_cls": "Person",
|
||||||
|
"name": "James", "age": 34}
|
||||||
|
)
|
||||||
|
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
self.assertEquals(self.Person.objects.first().age, 34)
|
||||||
|
|
||||||
|
# Confirm no changes to self.Person
|
||||||
|
self.assertFalse(hasattr(self.Person, 'age'))
|
||||||
|
|
||||||
|
def test_change_scope_of_variable(self):
|
||||||
|
"""Test changing the scope of a dynamic field has no adverse effects"""
|
||||||
|
p = self.Person()
|
||||||
|
p.name = "Dean"
|
||||||
|
p.misc = 22
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
p = self.Person.objects.get()
|
||||||
|
p.misc = {'hello': 'world'}
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
p = self.Person.objects.get()
|
||||||
|
self.assertEquals(p.misc, {'hello': 'world'})
|
||||||
|
|
||||||
|
def test_dynamic_document_queries(self):
|
||||||
|
"""Ensure we can query dynamic fields"""
|
||||||
|
p = self.Person()
|
||||||
|
p.name = "Dean"
|
||||||
|
p.age = 22
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
self.assertEquals(1, self.Person.objects(age=22).count())
|
||||||
|
p = self.Person.objects(age=22)
|
||||||
|
p = p.get()
|
||||||
|
self.assertEquals(22, p.age)
|
||||||
|
|
||||||
|
def test_complex_data_lookups(self):
|
||||||
|
"""Ensure you can query dynamic document dynamic fields"""
|
||||||
|
p = self.Person()
|
||||||
|
p.misc = {'hello': 'world'}
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
self.assertEquals(1, self.Person.objects(misc__hello='world').count())
|
||||||
|
|
||||||
|
def test_inheritance(self):
|
||||||
|
"""Ensure that dynamic document plays nice with inheritance"""
|
||||||
|
class Employee(self.Person):
|
||||||
|
salary = IntField()
|
||||||
|
|
||||||
|
Employee.drop_collection()
|
||||||
|
|
||||||
|
self.assertTrue('name' in Employee._fields)
|
||||||
|
self.assertTrue('salary' in Employee._fields)
|
||||||
|
self.assertEqual(Employee._get_collection_name(),
|
||||||
|
self.Person._get_collection_name())
|
||||||
|
|
||||||
|
joe_bloggs = Employee()
|
||||||
|
joe_bloggs.name = "Joe Bloggs"
|
||||||
|
joe_bloggs.salary = 10
|
||||||
|
joe_bloggs.age = 20
|
||||||
|
joe_bloggs.save()
|
||||||
|
|
||||||
|
self.assertEquals(1, self.Person.objects(age=20).count())
|
||||||
|
self.assertEquals(1, Employee.objects(age=20).count())
|
||||||
|
|
||||||
|
joe_bloggs = self.Person.objects.first()
|
||||||
|
self.assertTrue(isinstance(joe_bloggs, Employee))
|
||||||
|
|
||||||
|
def test_embedded_dynamic_document(self):
|
||||||
|
"""Test dynamic embedded documents"""
|
||||||
|
class Embedded(DynamicEmbeddedDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Doc(DynamicDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Doc.drop_collection()
|
||||||
|
doc = Doc()
|
||||||
|
|
||||||
|
embedded_1 = Embedded()
|
||||||
|
embedded_1.string_field = 'hello'
|
||||||
|
embedded_1.int_field = 1
|
||||||
|
embedded_1.dict_field = {'hello': 'world'}
|
||||||
|
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||||
|
doc.embedded_field = embedded_1
|
||||||
|
|
||||||
|
self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc",
|
||||||
|
"embedded_field": {
|
||||||
|
"_types": ['Embedded'], "_cls": "Embedded",
|
||||||
|
"string_field": "hello",
|
||||||
|
"int_field": 1,
|
||||||
|
"dict_field": {"hello": "world"},
|
||||||
|
"list_field": ['1', 2, {'hello': 'world'}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
doc = Doc.objects.first()
|
||||||
|
self.assertEquals(doc.embedded_field.__class__, Embedded)
|
||||||
|
self.assertEquals(doc.embedded_field.string_field, "hello")
|
||||||
|
self.assertEquals(doc.embedded_field.int_field, 1)
|
||||||
|
self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'})
|
||||||
|
self.assertEquals(doc.embedded_field.list_field, ['1', 2, {'hello': 'world'}])
|
||||||
|
|
||||||
|
def test_complex_embedded_documents(self):
|
||||||
|
"""Test complex dynamic embedded documents setups"""
|
||||||
|
class Embedded(DynamicEmbeddedDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Doc(DynamicDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Doc.drop_collection()
|
||||||
|
doc = Doc()
|
||||||
|
|
||||||
|
embedded_1 = Embedded()
|
||||||
|
embedded_1.string_field = 'hello'
|
||||||
|
embedded_1.int_field = 1
|
||||||
|
embedded_1.dict_field = {'hello': 'world'}
|
||||||
|
|
||||||
|
embedded_2 = Embedded()
|
||||||
|
embedded_2.string_field = 'hello'
|
||||||
|
embedded_2.int_field = 1
|
||||||
|
embedded_2.dict_field = {'hello': 'world'}
|
||||||
|
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||||
|
|
||||||
|
embedded_1.list_field = ['1', 2, embedded_2]
|
||||||
|
doc.embedded_field = embedded_1
|
||||||
|
|
||||||
|
self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc",
|
||||||
|
"embedded_field": {
|
||||||
|
"_types": ['Embedded'], "_cls": "Embedded",
|
||||||
|
"string_field": "hello",
|
||||||
|
"int_field": 1,
|
||||||
|
"dict_field": {"hello": "world"},
|
||||||
|
"list_field": ['1', 2,
|
||||||
|
{"_types": ['Embedded'], "_cls": "Embedded",
|
||||||
|
"string_field": "hello",
|
||||||
|
"int_field": 1,
|
||||||
|
"dict_field": {"hello": "world"},
|
||||||
|
"list_field": ['1', 2, {'hello': 'world'}]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
doc.save()
|
||||||
|
doc = Doc.objects.first()
|
||||||
|
self.assertEquals(doc.embedded_field.__class__, Embedded)
|
||||||
|
self.assertEquals(doc.embedded_field.string_field, "hello")
|
||||||
|
self.assertEquals(doc.embedded_field.int_field, 1)
|
||||||
|
self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'})
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[0], '1')
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[1], 2)
|
||||||
|
|
||||||
|
embedded_field = doc.embedded_field.list_field[2]
|
||||||
|
|
||||||
|
self.assertEquals(embedded_field.__class__, Embedded)
|
||||||
|
self.assertEquals(embedded_field.string_field, "hello")
|
||||||
|
self.assertEquals(embedded_field.int_field, 1)
|
||||||
|
self.assertEquals(embedded_field.dict_field, {'hello': 'world'})
|
||||||
|
self.assertEquals(embedded_field.list_field, ['1', 2, {'hello': 'world'}])
|
||||||
|
|
||||||
|
def test_delta_for_dynamic_documents(self):
|
||||||
|
p = self.Person()
|
||||||
|
p.name = "Dean"
|
||||||
|
p.age = 22
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
p.age = 24
|
||||||
|
self.assertEquals(p.age, 24)
|
||||||
|
self.assertEquals(p._get_changed_fields(), ['age'])
|
||||||
|
self.assertEquals(p._delta(), ({'age': 24}, {}))
|
||||||
|
|
||||||
|
p = self.Person.objects(age=22).get()
|
||||||
|
p.age = 24
|
||||||
|
self.assertEquals(p.age, 24)
|
||||||
|
self.assertEquals(p._get_changed_fields(), ['age'])
|
||||||
|
self.assertEquals(p._delta(), ({'age': 24}, {}))
|
||||||
|
|
||||||
|
p.save()
|
||||||
|
self.assertEquals(1, self.Person.objects(age=24).count())
|
||||||
|
|
||||||
|
def test_delta(self):
|
||||||
|
|
||||||
|
class Doc(DynamicDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Doc.drop_collection()
|
||||||
|
doc = Doc()
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
doc = Doc.objects.first()
|
||||||
|
self.assertEquals(doc._get_changed_fields(), [])
|
||||||
|
self.assertEquals(doc._delta(), ({}, {}))
|
||||||
|
|
||||||
|
doc.string_field = 'hello'
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['string_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({'string_field': 'hello'}, {}))
|
||||||
|
|
||||||
|
doc._changed_fields = []
|
||||||
|
doc.int_field = 1
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['int_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({'int_field': 1}, {}))
|
||||||
|
|
||||||
|
doc._changed_fields = []
|
||||||
|
dict_value = {'hello': 'world', 'ping': 'pong'}
|
||||||
|
doc.dict_field = dict_value
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['dict_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({'dict_field': dict_value}, {}))
|
||||||
|
|
||||||
|
doc._changed_fields = []
|
||||||
|
list_value = ['1', 2, {'hello': 'world'}]
|
||||||
|
doc.list_field = list_value
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['list_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({'list_field': list_value}, {}))
|
||||||
|
|
||||||
|
# Test unsetting
|
||||||
|
doc._changed_fields = []
|
||||||
|
doc.dict_field = {}
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['dict_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({}, {'dict_field': 1}))
|
||||||
|
|
||||||
|
doc._changed_fields = []
|
||||||
|
doc.list_field = []
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['list_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({}, {'list_field': 1}))
|
||||||
|
|
||||||
|
def test_delta_recursive(self):
|
||||||
|
"""Testing deltaing works with dynamic documents"""
|
||||||
|
class Embedded(DynamicEmbeddedDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Doc(DynamicDocument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Doc.drop_collection()
|
||||||
|
doc = Doc()
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
doc = Doc.objects.first()
|
||||||
|
self.assertEquals(doc._get_changed_fields(), [])
|
||||||
|
self.assertEquals(doc._delta(), ({}, {}))
|
||||||
|
|
||||||
|
embedded_1 = Embedded()
|
||||||
|
embedded_1.string_field = 'hello'
|
||||||
|
embedded_1.int_field = 1
|
||||||
|
embedded_1.dict_field = {'hello': 'world'}
|
||||||
|
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||||
|
doc.embedded_field = embedded_1
|
||||||
|
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['embedded_field'])
|
||||||
|
|
||||||
|
embedded_delta = {
|
||||||
|
'_types': ['Embedded'],
|
||||||
|
'_cls': 'Embedded',
|
||||||
|
'string_field': 'hello',
|
||||||
|
'int_field': 1,
|
||||||
|
'dict_field': {'hello': 'world'},
|
||||||
|
'list_field': ['1', 2, {'hello': 'world'}]
|
||||||
|
}
|
||||||
|
self.assertEquals(doc.embedded_field._delta(), (embedded_delta, {}))
|
||||||
|
self.assertEquals(doc._delta(), ({'embedded_field': embedded_delta}, {}))
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
doc.embedded_field.dict_field = {}
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['embedded_field.dict_field'])
|
||||||
|
self.assertEquals(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
|
||||||
|
|
||||||
|
self.assertEquals(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
doc.embedded_field.list_field = []
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
|
||||||
|
self.assertEquals(doc.embedded_field._delta(), ({}, {'list_field': 1}))
|
||||||
|
self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field': 1}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
embedded_2 = Embedded()
|
||||||
|
embedded_2.string_field = 'hello'
|
||||||
|
embedded_2.int_field = 1
|
||||||
|
embedded_2.dict_field = {'hello': 'world'}
|
||||||
|
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||||
|
|
||||||
|
doc.embedded_field.list_field = ['1', 2, embedded_2]
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
|
||||||
|
self.assertEquals(doc.embedded_field._delta(), ({
|
||||||
|
'list_field': ['1', 2, {
|
||||||
|
'_cls': 'Embedded',
|
||||||
|
'_types': ['Embedded'],
|
||||||
|
'string_field': 'hello',
|
||||||
|
'dict_field': {'hello': 'world'},
|
||||||
|
'int_field': 1,
|
||||||
|
'list_field': ['1', 2, {'hello': 'world'}],
|
||||||
|
}]
|
||||||
|
}, {}))
|
||||||
|
|
||||||
|
self.assertEquals(doc._delta(), ({
|
||||||
|
'embedded_field.list_field': ['1', 2, {
|
||||||
|
'_cls': 'Embedded',
|
||||||
|
'_types': ['Embedded'],
|
||||||
|
'string_field': 'hello',
|
||||||
|
'dict_field': {'hello': 'world'},
|
||||||
|
'int_field': 1,
|
||||||
|
'list_field': ['1', 2, {'hello': 'world'}],
|
||||||
|
}]
|
||||||
|
}, {}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[2]._changed_fields, [])
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[0], '1')
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[1], 2)
|
||||||
|
for k in doc.embedded_field.list_field[2]._fields:
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[2][k], embedded_2[k])
|
||||||
|
|
||||||
|
doc.embedded_field.list_field[2].string_field = 'world'
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field.2.string_field'])
|
||||||
|
self.assertEquals(doc.embedded_field._delta(), ({'list_field.2.string_field': 'world'}, {}))
|
||||||
|
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.string_field': 'world'}, {}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[2].string_field, 'world')
|
||||||
|
|
||||||
|
# Test multiple assignments
|
||||||
|
doc.embedded_field.list_field[2].string_field = 'hello world'
|
||||||
|
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
|
||||||
|
self.assertEquals(doc.embedded_field._delta(), ({
|
||||||
|
'list_field': ['1', 2, {
|
||||||
|
'_types': ['Embedded'],
|
||||||
|
'_cls': 'Embedded',
|
||||||
|
'string_field': 'hello world',
|
||||||
|
'int_field': 1,
|
||||||
|
'list_field': ['1', 2, {'hello': 'world'}],
|
||||||
|
'dict_field': {'hello': 'world'}}]}, {}))
|
||||||
|
self.assertEquals(doc._delta(), ({
|
||||||
|
'embedded_field.list_field': ['1', 2, {
|
||||||
|
'_types': ['Embedded'],
|
||||||
|
'_cls': 'Embedded',
|
||||||
|
'string_field': 'hello world',
|
||||||
|
'int_field': 1,
|
||||||
|
'list_field': ['1', 2, {'hello': 'world'}],
|
||||||
|
'dict_field': {'hello': 'world'}}
|
||||||
|
]}, {}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[2].string_field, 'hello world')
|
||||||
|
|
||||||
|
# Test list native methods
|
||||||
|
doc.embedded_field.list_field[2].list_field.pop(0)
|
||||||
|
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}]}, {}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
doc.embedded_field.list_field[2].list_field.append(1)
|
||||||
|
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}, 1]}, {}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[2].list_field, [2, {'hello': 'world'}, 1])
|
||||||
|
|
||||||
|
doc.embedded_field.list_field[2].list_field.sort()
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
self.assertEquals(doc.embedded_field.list_field[2].list_field, [1, 2, {'hello': 'world'}])
|
||||||
|
|
||||||
|
del(doc.embedded_field.list_field[2].list_field[2]['hello'])
|
||||||
|
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [1, 2, {}]}, {}))
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
del(doc.embedded_field.list_field[2].list_field)
|
||||||
|
self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field.2.list_field': 1}))
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
doc.dict_field = {'embedded': embedded_1}
|
||||||
|
doc.save()
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
|
doc.dict_field['embedded'].string_field = 'Hello World'
|
||||||
|
self.assertEquals(doc._get_changed_fields(), ['dict_field.embedded.string_field'])
|
||||||
|
self.assertEquals(doc._delta(), ({'dict_field.embedded.string_field': 'Hello World'}, {}))
|
Loading…
x
Reference in New Issue
Block a user