Skip to content

Commit

Permalink
Merge branch 'master' into remove-pushall
Browse files Browse the repository at this point in the history
  • Loading branch information
erdenezul authored Apr 17, 2018
2 parents 38fdf26 + 2d8d2e7 commit 72c4444
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 37 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,5 @@ that much better:
* Stanislav Kaledin (https://github.com/sallyruthstruik)
* Dmitry Yantsen (https://github.com/mrTable)
* Renjianxin (https://github.com/Davidrjx)
* Erdenezul Batmunkh (https://github.com/erdenezul)
* Erdenezul Batmunkh (https://github.com/erdenezul)
* Andy Yankovsky (https://github.com/werat)
2 changes: 2 additions & 0 deletions docs/apireference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ Fields
.. autoclass:: mongoengine.fields.DictField
.. autoclass:: mongoengine.fields.MapField
.. autoclass:: mongoengine.fields.ReferenceField
.. autoclass:: mongoengine.fields.LazyReferenceField
.. autoclass:: mongoengine.fields.GenericReferenceField
.. autoclass:: mongoengine.fields.GenericLazyReferenceField
.. autoclass:: mongoengine.fields.CachedReferenceField
.. autoclass:: mongoengine.fields.BinaryField
.. autoclass:: mongoengine.fields.FileField
Expand Down
8 changes: 5 additions & 3 deletions docs/guide/defining-documents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ objects** as class attributes to the document class::

class Page(Document):
title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.now)
date_modified = DateTimeField(default=datetime.datetime.utcnow)

As BSON (the binary format for storing data in mongodb) is order dependent,
documents are serialized based on their field order.
Expand Down Expand Up @@ -80,13 +80,15 @@ are as follows:
* :class:`~mongoengine.fields.FloatField`
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
* :class:`~mongoengine.fields.GenericReferenceField`
* :class:`~mongoengine.fields.GenericLazyReferenceField`
* :class:`~mongoengine.fields.GeoPointField`
* :class:`~mongoengine.fields.ImageField`
* :class:`~mongoengine.fields.IntField`
* :class:`~mongoengine.fields.ListField`
* :class:`~mongoengine.fields.MapField`
* :class:`~mongoengine.fields.ObjectIdField`
* :class:`~mongoengine.fields.ReferenceField`
* :class:`~mongoengine.fields.LazyReferenceField`
* :class:`~mongoengine.fields.SequenceField`
* :class:`~mongoengine.fields.SortedListField`
* :class:`~mongoengine.fields.StringField`
Expand Down Expand Up @@ -224,7 +226,7 @@ store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate
user = ReferenceField(User)
answers = DictField()

survey_response = SurveyResponse(date=datetime.now(), user=request.user)
survey_response = SurveyResponse(date=datetime.utcnow(), user=request.user)
response_form = ResponseForm(request.POST)
survey_response.answers = response_form.cleaned_data()
survey_response.save()
Expand Down Expand Up @@ -618,7 +620,7 @@ collection after a given period. See the official
documentation for more information. A common usecase might be session data::

class Session(Document):
created = DateTimeField(default=datetime.now)
created = DateTimeField(default=datetime.utcnow)
meta = {
'indexes': [
{'fields': ['created'], 'expireAfterSeconds': 3600}
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ of them stand out as particularly intuitive solutions.
Posts
^^^^^

Happily mongoDB *isn't* a relational database, so we're not going to do it that
Happily MongoDB *isn't* a relational database, so we're not going to do it that
way. As it turns out, we can use MongoDB's schemaless nature to provide us with
a much nicer solution. We will store all of the posts in *one collection* and
each post type will only store the fields it needs. If we later want to add
Expand Down
3 changes: 2 additions & 1 deletion mongoengine/base/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ def delete(self):

def update(self, **update):
"""
Updates the embedded documents with the given update values.
Updates the embedded documents with the given replacement values. This
function does not support mongoDB update operators such as ``inc__``.
.. note::
The embedded document changes are not automatically saved
Expand Down
3 changes: 3 additions & 0 deletions mongoengine/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ def modify(self, query=None, **update):
elif query[id_field] != self.pk:
raise InvalidQueryError('Invalid document modify query: it must modify only this document.')

# Need to add shard key to query, or you get an error
query.update(self._object_key)

updated = self._qs(**query).modify(new=True, **update)
if updated is None:
return False
Expand Down
44 changes: 32 additions & 12 deletions mongoengine/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ class EmbeddedDocumentField(BaseField):
"""

def __init__(self, document_type, **kwargs):
# XXX ValidationError raised outside of the "validate" method.
if not (
isinstance(document_type, six.string_types) or
issubclass(document_type, EmbeddedDocument)
Expand Down Expand Up @@ -919,8 +920,11 @@ def __init__(self, basecls=None, field=None, *args, **kwargs):
self.field = field
self._auto_dereference = False
self.basecls = basecls or BaseField

# XXX ValidationError raised outside of the "validate" method.
if not issubclass(self.basecls, BaseField):
self.error('DictField only accepts dict values')

kwargs.setdefault('default', lambda: {})
super(DictField, self).__init__(*args, **kwargs)

Expand Down Expand Up @@ -969,6 +973,7 @@ class MapField(DictField):
"""

def __init__(self, field=None, *args, **kwargs):
# XXX ValidationError raised outside of the "validate" method.
if not isinstance(field, BaseField):
self.error('Argument to MapField constructor must be a valid '
'field')
Expand Down Expand Up @@ -1028,6 +1033,7 @@ def __init__(self, document_type, dbref=False,
A reference to an abstract document type is always stored as a
:class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`.
"""
# XXX ValidationError raised outside of the "validate" method.
if (
not isinstance(document_type, six.string_types) and
not issubclass(document_type, Document)
Expand Down Expand Up @@ -1082,6 +1088,8 @@ def to_mongo(self, document):
if isinstance(document, Document):
# We need the id from the saved object to create the DBRef
id_ = document.pk

# XXX ValidationError raised outside of the "validate" method.
if id_ is None:
self.error('You can only reference documents once they have'
' been saved to the database')
Expand Down Expand Up @@ -1121,19 +1129,21 @@ def prepare_query_value(self, op, value):
return self.to_mongo(value)

def validate(self, value):

if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)):
self.error('A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents')

if isinstance(value, Document) and value.id is None:
self.error('You can only reference documents once they have been '
'saved to the database')

if self.document_type._meta.get('abstract') and \
not isinstance(value, self.document_type):
if (
self.document_type._meta.get('abstract') and
not isinstance(value, self.document_type)
):
self.error(
'%s is not an instance of abstract reference type %s' % (
self.document_type._class_name)
self.document_type._class_name
)
)

def lookup_member(self, member_name):
Expand All @@ -1156,6 +1166,7 @@ def __init__(self, document_type, fields=None, auto_sync=True, **kwargs):
if fields is None:
fields = []

# XXX ValidationError raised outside of the "validate" method.
if (
not isinstance(document_type, six.string_types) and
not issubclass(document_type, Document)
Expand Down Expand Up @@ -1230,6 +1241,7 @@ def to_mongo(self, document, use_db_field=True, fields=None):
id_field_name = self.document_type._meta['id_field']
id_field = self.document_type._fields[id_field_name]

# XXX ValidationError raised outside of the "validate" method.
if isinstance(document, Document):
# We need the id from the saved object to create the DBRef
id_ = document.pk
Expand All @@ -1238,7 +1250,6 @@ def to_mongo(self, document, use_db_field=True, fields=None):
' been saved to the database')
else:
self.error('Only accept a document object')
# TODO: should raise here or will fail next statement

value = SON((
('_id', id_field.to_mongo(id_)),
Expand All @@ -1256,6 +1267,7 @@ def prepare_query_value(self, op, value):
if value is None:
return None

# XXX ValidationError raised outside of the "validate" method.
if isinstance(value, Document):
if value.pk is None:
self.error('You can only reference documents once they have'
Expand All @@ -1269,7 +1281,6 @@ def prepare_query_value(self, op, value):
raise NotImplementedError

def validate(self, value):

if not isinstance(value, self.document_type):
self.error('A CachedReferenceField only accepts documents')

Expand Down Expand Up @@ -1330,6 +1341,8 @@ def __init__(self, *args, **kwargs):
elif isinstance(choice, type) and issubclass(choice, Document):
self.choices.append(choice._class_name)
else:
# XXX ValidationError raised outside of the "validate"
# method.
self.error('Invalid choices provided: must be a list of'
'Document subclasses and/or six.string_typess')

Expand Down Expand Up @@ -1393,6 +1406,7 @@ def to_mongo(self, document):
# We need the id from the saved object to create the DBRef
id_ = document.id
if id_ is None:
# XXX ValidationError raised outside of the "validate" method.
self.error('You can only reference documents once they have'
' been saved to the database')
else:
Expand Down Expand Up @@ -2190,8 +2204,11 @@ class MultiPolygonField(GeoJsonBaseField):

class LazyReferenceField(BaseField):
"""A really lazy reference to a document.
Unlike the :class:`~mongoengine.fields.ReferenceField` it must be manually
dereferenced using it ``fetch()`` method.
Unlike the :class:`~mongoengine.fields.ReferenceField` it will
**not** be automatically (lazily) dereferenced on access.
Instead, access will return a :class:`~mongoengine.base.LazyReference` class
instance, allowing access to `pk` or manual dereference by using
``fetch()`` method.
.. versionadded:: 0.15
"""
Expand All @@ -2209,6 +2226,7 @@ def __init__(self, document_type, passthrough=False, dbref=False,
automatically call `fetch()` and try to retrive the field on the fetched
document. Note this only work getting field (not setting or deleting).
"""
# XXX ValidationError raised outside of the "validate" method.
if (
not isinstance(document_type, six.string_types) and
not issubclass(document_type, Document)
Expand Down Expand Up @@ -2316,10 +2334,12 @@ def lookup_member(self, member_name):


class GenericLazyReferenceField(GenericReferenceField):
"""A reference to *any* :class:`~mongoengine.document.Document` subclass
that will be automatically dereferenced on access (lazily).
Unlike the :class:`~mongoengine.fields.GenericReferenceField` it must be
manually dereferenced using it ``fetch()`` method.
"""A reference to *any* :class:`~mongoengine.document.Document` subclass.
Unlike the :class:`~mongoengine.fields.GenericReferenceField` it will
**not** be automatically (lazily) dereferenced on access.
Instead, access will return a :class:`~mongoengine.base.LazyReference` class
instance, allowing access to `pk` or manual dereference by using
``fetch()`` method.
.. note ::
* Any documents used as a generic reference must be registered in the
Expand Down
5 changes: 3 additions & 2 deletions mongoengine/queryset/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,8 +486,9 @@ def update(self, upsert=False, multi=True, write_concern=None,
``save(..., write_concern={w: 2, fsync: True}, ...)`` will
wait until at least two servers have recorded the write and
will force an fsync on the primary server.
:param full_result: Return the full result rather than just the number
updated.
:param full_result: Return the full result dictionary rather than just the number
updated, e.g. return
``{'n': 2, 'nModified': 2, 'ok': 1.0, 'updatedExisting': True}``.
:param update: Django-style update keyword arguments
.. versionadded:: 0.2
Expand Down
46 changes: 29 additions & 17 deletions mongoengine/queryset/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,8 @@ def query(_doc_cls=None, **kwargs):
value = value['_id']

elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
# Raise an error if the in/nin/all/near param is not iterable. We need a
# special check for BaseDocument, because - although it's iterable - using
# it as such in the context of this method is most definitely a mistake.
BaseDocument = _import_class('BaseDocument')
if isinstance(value, BaseDocument):
raise TypeError("When using the `in`, `nin`, `all`, or "
"`near`-operators you can\'t use a "
"`Document`, you must wrap your object "
"in a list (object -> [object]).")
elif not hasattr(value, '__iter__'):
raise TypeError("The `in`, `nin`, `all`, or "
"`near`-operators must be applied to an "
"iterable (e.g. a list).")
else:
value = [field.prepare_query_value(op, v) for v in value]
# Raise an error if the in/nin/all/near param is not iterable.
value = _prepare_query_for_iterable(field, op, value)

# If we're querying a GenericReferenceField, we need to alter the
# key depending on the value:
Expand Down Expand Up @@ -284,9 +271,15 @@ def update(_doc_cls=None, **update):
if isinstance(field, GeoJsonBaseField):
value = field.to_mongo(value)

if op == 'push' and isinstance(value, (list, tuple, set)):
if op == 'pull':
if field.required or value is not None:
if match == 'in' and not isinstance(value, dict):
value = _prepare_query_for_iterable(field, op, value)
else:
value = field.prepare_query_value(op, value)
elif op == 'push' and isinstance(value, (list, tuple, set)):
value = [field.prepare_query_value(op, v) for v in value]
elif op in (None, 'set', 'push', 'pull'):
elif op in (None, 'set', 'push'):
if field.required or value is not None:
value = field.prepare_query_value(op, value)
elif op in ('pushAll', 'pullAll'):
Expand Down Expand Up @@ -443,3 +436,22 @@ def _infer_geometry(value):

raise InvalidQueryError('Invalid $geometry data. Can be either a '
'dictionary or (nested) lists of coordinate(s)')


def _prepare_query_for_iterable(field, op, value):
# We need a special check for BaseDocument, because - although it's iterable - using
# it as such in the context of this method is most definitely a mistake.
BaseDocument = _import_class('BaseDocument')

if isinstance(value, BaseDocument):
raise TypeError("When using the `in`, `nin`, `all`, or "
"`near`-operators you can\'t use a "
"`Document`, you must wrap your object "
"in a list (object -> [object]).")

if not hasattr(value, '__iter__'):
raise TypeError("The `in`, `nin`, `all`, or "
"`near`-operators must be applied to an "
"iterable (e.g. a list).")

return [field.prepare_query_value(op, v) for v in value]
17 changes: 17 additions & 0 deletions tests/document/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,23 @@ class Site(Document):
site = Site.objects.first()
self.assertEqual(site.page.log_message, "Error: Dummy message")

def test_update_list_field(self):
"""Test update on `ListField` with $pull + $in.
"""
class Doc(Document):
foo = ListField(StringField())

Doc.drop_collection()
doc = Doc(foo=['a', 'b', 'c'])
doc.save()

# Update
doc = Doc.objects.first()
doc.update(pull__foo__in=['a', 'c'])

doc = Doc.objects.first()
self.assertEqual(doc.foo, ['b'])

def test_embedded_update_db_field(self):
"""Test update on `EmbeddedDocumentField` fields when db_field
is other than default.
Expand Down
7 changes: 7 additions & 0 deletions tests/queryset/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ def test_transform_query(self):
{'name': {'$exists': True}})

def test_transform_update(self):
class LisDoc(Document):
foo = ListField(StringField())

class DicDoc(Document):
dictField = DictField()

class Doc(Document):
pass

LisDoc.drop_collection()
DicDoc.drop_collection()
Doc.drop_collection()

Expand All @@ -50,6 +54,9 @@ class Doc(Document):

update = transform.update(DicDoc, pull__dictField__test=doc)
self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict))

update = transform.update(LisDoc, pull__foo__in=['a'])
self.assertEqual(update, {'$pull': {'foo': {'$in': ['a']}}})

def test_transform_update_push(self):
"""Ensure the differences in behvaior between 'push' and 'push_all'"""
Expand Down

0 comments on commit 72c4444

Please sign in to comment.