From 7430b316970c57bc014caab43bf182bd4917b086 Mon Sep 17 00:00:00 2001 From: Vjacheslav Murashkin Date: Thu, 4 Sep 2014 16:54:23 +0400 Subject: [PATCH 0001/1519] handle None from model __str__; Fixes #753 --- AUTHORS | 1 + mongoengine/base/document.py | 2 +- tests/document/instance.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index ffd63227e..484ab0865 100644 --- a/AUTHORS +++ b/AUTHORS @@ -210,3 +210,4 @@ that much better: * Jay Shirley (https://github.com/jshirley) * DavidBord (https://github.com/DavidBord) * Axel Haustant (https://github.com/noirbizarre) + * Vyacheslav Murashkin (https://github.com/a4tunado) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 869449f93..c3802e1f2 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -229,7 +229,7 @@ def __repr__(self): u = self.__str__() except (UnicodeEncodeError, UnicodeDecodeError): u = '[Bad Unicode data]' - repr_type = type(u) + repr_type = str if u is None else type(u) return repr_type('<%s: %s>' % (self.__class__.__name__, u)) def __str__(self): diff --git a/tests/document/instance.py b/tests/document/instance.py index f925aeeba..3233304e1 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -103,6 +103,19 @@ def __unicode__(self): self.assertEqual('', repr(doc)) + def test_repr_none(self): + """Ensure None values handled correctly + """ + class Article(Document): + title = StringField() + + def __str__(self): + return None + + doc = Article(title=u'привет мир') + + self.assertEqual('', repr(doc)) + def test_queryset_resurrects_dropped_collection(self): self.Person.drop_collection() From aec06183e7303f3960fff1768227ed0a3aa7bd5e Mon Sep 17 00:00:00 2001 From: yjaaidi Date: Sun, 28 Sep 2014 10:09:36 +0200 Subject: [PATCH 0002/1519] Version bump 0.8.7 => 0.9.0 --- mongoengine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index ac33ad2b1..b215181ad 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 7) +VERSION = (0, 9, 0) def get_version(): From 025d3a03d66df269ead0cbed81735c7555965298 Mon Sep 17 00:00:00 2001 From: Shuuji TAKAHASHI Date: Tue, 30 Sep 2014 02:31:48 +0900 Subject: [PATCH 0003/1519] Marked up the last line as preformatted text. --- docs/guide/text-indexes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/text-indexes.rst b/docs/guide/text-indexes.rst index b597d9572..695159c62 100644 --- a/docs/guide/text-indexes.rst +++ b/docs/guide/text-indexes.rst @@ -46,4 +46,6 @@ Next, start a text search using :attr:`QuerySet.search_text` method:: Ordering by text score ====================== +:: + objects = News.objects.search('mongo').order_by('$text_score') From 4752f9aa3734897815b2b11396f9bc583e35eb0b Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Tue, 30 Sep 2014 15:15:11 +0400 Subject: [PATCH 0004/1519] Add Document.modify() method --- mongoengine/document.py | 40 +++++++++++++++++++++- tests/document/instance.py | 70 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 34bbb9f67..2eab83ef3 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -12,7 +12,7 @@ from mongoengine.base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, BaseDict, BaseList, ALLOW_INHERITANCE, get_document) -from mongoengine.errors import ValidationError +from mongoengine.errors import ValidationError, InvalidQueryError, InvalidDocumentError from mongoengine.queryset import (OperationError, NotUniqueError, QuerySet, transform) from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME @@ -192,6 +192,44 @@ def _get_collection(cls): cls.ensure_indexes() return cls._collection + def modify(self, query={}, **update): + """Perform an atomic update of the document in the database and reload + the document object using updated version. + + Returns True if the document has been updated or False if the document + in the database doesn't match the query. + + .. note:: All unsaved changes that has been made to the document are + rejected if the method returns True. + + :param query: the update will be performed only if the document in the + database matches the query + :param update: Django-style update keyword arguments + """ + + if self.pk is None: + raise InvalidDocumentError("The document does not have a primary key.") + + id_field = self._meta["id_field"] + query = query.copy() if isinstance(query, dict) else query.to_query(self) + + if id_field not in query: + query[id_field] = self.pk + elif query[id_field] != self.pk: + raise InvalidQueryError("Invalid document modify query: it must modify only this document.") + + updated = self._qs(**query).modify(new=True, **update) + if updated is None: + return False + + for field in self._fields_ordered: + setattr(self, field, self._reload(field, updated[field])) + + self._changed_fields = updated._changed_fields + self._created = False + + return True + def save(self, force_insert=False, validate=True, clean=True, write_concern=None, cascade=None, cascade_kwargs=None, _refs=None, save_condition=None, **kwargs): diff --git a/tests/document/instance.py b/tests/document/instance.py index f925aeeba..d5add95b2 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -9,7 +9,7 @@ import uuid from datetime import datetime -from bson import DBRef +from bson import DBRef, ObjectId from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, PickleDyanmicEmbedded, PickleDynamicTest) @@ -34,15 +34,21 @@ def setUp(self): connect(db='mongoenginetest') self.db = get_db() + class Job(EmbeddedDocument): + name = StringField() + years = IntField() + class Person(Document): name = StringField() age = IntField() + job = EmbeddedDocumentField(Job) non_field = True meta = {"allow_inheritance": True} self.Person = Person + self.Job = Job def tearDown(self): for collection in self.db.collection_names(): @@ -50,6 +56,9 @@ def tearDown(self): continue self.db.drop_collection(collection) + def assertDbEqual(self, docs): + self.assertEqual(list(self.Person._get_collection().find().sort("id")), sorted(docs, key=lambda doc: doc["_id"])) + def test_capped_collection(self): """Ensure that capped collections work properly. """ @@ -452,7 +461,7 @@ class Foo(Document): def test_dictionary_access(self): """Ensure that dictionary-style field access works properly. """ - person = self.Person(name='Test User', age=30) + person = self.Person(name='Test User', age=30, job=self.Job()) self.assertEqual(person['name'], 'Test User') self.assertRaises(KeyError, person.__getitem__, 'salary') @@ -462,7 +471,7 @@ def test_dictionary_access(self): self.assertEqual(person['name'], 'Another User') # Length = length(assigned fields + id) - self.assertEqual(len(person), 4) + self.assertEqual(len(person), 5) self.assertTrue('age' in person) person.age = None @@ -617,6 +626,61 @@ class TestDocument(Document): t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5)) t.save(clean=False) + def test_modify_empty(self): + doc = self.Person(id=ObjectId(), name="bob", age=10).save() + self.assertRaises(InvalidDocumentError, lambda: self.Person().modify(set__age=10)) + self.assertDbEqual([dict(doc.to_mongo())]) + + def test_modify_invalid_query(self): + doc1 = self.Person(id=ObjectId(), name="bob", age=10).save() + doc2 = self.Person(id=ObjectId(), name="jim", age=20).save() + docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] + + self.assertRaises(InvalidQueryError, lambda: + doc1.modify(dict(id=doc2.id), set__value=20)) + + self.assertDbEqual(docs) + + def test_modify_match_another_document(self): + doc1 = self.Person(id=ObjectId(), name="bob", age=10).save() + doc2 = self.Person(id=ObjectId(), name="jim", age=20).save() + docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] + + assert not doc1.modify(dict(name=doc2.name), set__age=100) + + self.assertDbEqual(docs) + + def test_modify_not_exists(self): + doc1 = self.Person(id=ObjectId(), name="bob", age=10).save() + doc2 = self.Person(id=ObjectId(), name="jim", age=20) + docs = [dict(doc1.to_mongo())] + + assert not doc2.modify(dict(name=doc2.name), set__age=100) + + self.assertDbEqual(docs) + + def test_modify_update(self): + other_doc = self.Person(id=ObjectId(), name="bob", age=10).save() + doc = self.Person(id=ObjectId(), name="jim", age=20, + job=self.Job(name="10gen", years=3)).save() + + doc_copy = doc._from_son(doc.to_mongo()) + + # these changes must go away + doc.name = "liza" + doc.job.name = "Google" + doc.job.years = 3 + + assert doc.modify(set__age=21, set__job__name="MongoDB", unset__job__years=True) + doc_copy.age = 21 + doc_copy.job.name = "MongoDB" + del doc_copy.job.years + + assert doc.to_json() == doc_copy.to_json() + assert doc._get_changed_fields() == [] + + self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) + def test_save(self): """Ensure that a document may be saved in the database. """ From 5d2ca6493de3bb273763fd77296c760ddddfbc8f Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Tue, 30 Sep 2014 19:22:29 +0400 Subject: [PATCH 0005/1519] Drop unnecessary id=ObjectId() in document creation --- tests/document/instance.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index d5add95b2..0982a5273 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -627,13 +627,13 @@ class TestDocument(Document): t.save(clean=False) def test_modify_empty(self): - doc = self.Person(id=ObjectId(), name="bob", age=10).save() + doc = self.Person(name="bob", age=10).save() self.assertRaises(InvalidDocumentError, lambda: self.Person().modify(set__age=10)) self.assertDbEqual([dict(doc.to_mongo())]) def test_modify_invalid_query(self): - doc1 = self.Person(id=ObjectId(), name="bob", age=10).save() - doc2 = self.Person(id=ObjectId(), name="jim", age=20).save() + doc1 = self.Person(name="bob", age=10).save() + doc2 = self.Person(name="jim", age=20).save() docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] self.assertRaises(InvalidQueryError, lambda: @@ -642,8 +642,8 @@ def test_modify_invalid_query(self): self.assertDbEqual(docs) def test_modify_match_another_document(self): - doc1 = self.Person(id=ObjectId(), name="bob", age=10).save() - doc2 = self.Person(id=ObjectId(), name="jim", age=20).save() + doc1 = self.Person(name="bob", age=10).save() + doc2 = self.Person(name="jim", age=20).save() docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] assert not doc1.modify(dict(name=doc2.name), set__age=100) @@ -651,7 +651,7 @@ def test_modify_match_another_document(self): self.assertDbEqual(docs) def test_modify_not_exists(self): - doc1 = self.Person(id=ObjectId(), name="bob", age=10).save() + doc1 = self.Person(name="bob", age=10).save() doc2 = self.Person(id=ObjectId(), name="jim", age=20) docs = [dict(doc1.to_mongo())] @@ -660,9 +660,8 @@ def test_modify_not_exists(self): self.assertDbEqual(docs) def test_modify_update(self): - other_doc = self.Person(id=ObjectId(), name="bob", age=10).save() - doc = self.Person(id=ObjectId(), name="jim", age=20, - job=self.Job(name="10gen", years=3)).save() + other_doc = self.Person(name="bob", age=10).save() + doc = self.Person(name="jim", age=20, job=self.Job(name="10gen", years=3)).save() doc_copy = doc._from_son(doc.to_mongo()) From 7d3146234ad21ceac5260a4e9bec13b2f0dce56a Mon Sep 17 00:00:00 2001 From: Clay McClure Date: Wed, 1 Oct 2014 15:59:13 -0400 Subject: [PATCH 0006/1519] Make `in_bulk()` respect `no_dereference()` --- mongoengine/queryset/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7094dacc2..58e776113 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -619,7 +619,9 @@ def in_bulk(self, object_ids): doc_map[doc['_id']] = self._get_as_pymongo(doc) else: for doc in docs: - doc_map[doc['_id']] = self._document._from_son(doc, only_fields=self.only_fields) + doc_map[doc['_id']] = self._document._from_son(doc, + only_fields=self.only_fields, + _auto_dereference=self._auto_dereference) return doc_map From 9a4aef03587c3e9c3643cacd2aad3a139e81887a Mon Sep 17 00:00:00 2001 From: DavidBord Date: Wed, 29 Oct 2014 13:36:42 +0200 Subject: [PATCH 0007/1519] fix-#789: abstract shouldn't be inherited in EmbeddedDocument --- docs/changelog.rst | 1 + mongoengine/base/metaclasses.py | 3 ++- tests/document/inheritance.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f17a61c2c..155348c7d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- abstract shouldn't be inherited in EmbeddedDocument # 789 - Allow specifying the '_cls' as a field for indexes #397 - Stop ensure_indexes running on a secondaries unless connection is through mongos #746 - Not overriding default values when loading a subset of fields #399 diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index e80145530..48da84f42 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -46,8 +46,9 @@ def __new__(cls, name, bases, attrs): elif hasattr(base, '_meta'): meta.merge(base._meta) attrs['_meta'] = meta + attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract - if '_meta' in attrs and attrs['_meta'].get('allow_inheritance', ALLOW_INHERITANCE): + if attrs['_meta'].get('allow_inheritance', ALLOW_INHERITANCE): StringField = _import_class('StringField') attrs['_cls'] = StringField() diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index 566d36994..e8347054c 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -397,6 +397,16 @@ class EvilHuman(Human): meta = {'abstract': True} self.assertRaises(ValueError, create_bad_abstract) + def test_abstract_embedded_documents(self): + # 789: EmbeddedDocument shouldn't inherit abstract + class A(EmbeddedDocument): + meta = {"abstract": True} + + class B(A): + pass + + self.assertFalse(B._meta["abstract"]) + def test_inherited_collections(self): """Ensure that subclassed documents don't override parents' collections From b011d48d827e051b10827e4fa07736f580da8311 Mon Sep 17 00:00:00 2001 From: DavidBord Date: Mon, 6 Oct 2014 19:05:39 +0300 Subject: [PATCH 0008/1519] fix-#778: Add Support For MongoDB 2.6.X's maxTimeMS --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 17 ++++++++++++++++- tests/queryset/queryset.py | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 155348c7d..12712cd53 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 - Allow specifying the '_cls' as a field for indexes #397 - Stop ensure_indexes running on a secondaries unless connection is through mongos #746 diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7094dacc2..f10b1e320 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -82,6 +82,7 @@ def __init__(self, document, collection): self._skip = None self._hint = -1 # Using -1 as None is a valid value for hint self.only_fields = [] + self._max_time_ms = None def __call__(self, q_obj=None, class_check=True, slave_okay=False, read_preference=None, **query): @@ -672,7 +673,7 @@ def clone_into(self, cls): '_timeout', '_class_check', '_slave_okay', '_read_preference', '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', '_limit', '_skip', '_hint', '_auto_dereference', - '_search_text', '_include_text_scores', 'only_fields') + '_search_text', '_include_text_scores', 'only_fields', '_max_time_ms') for prop in copy_props: val = getattr(self, prop) @@ -969,6 +970,13 @@ def as_pymongo(self, coerce_types=False): queryset._as_pymongo_coerce = coerce_types return queryset + def max_time_ms(self, ms): + """Wait `ms` milliseconds before killing the query on the server + + :param ms: the number of milliseconds before killing the query on the server + """ + return self._chainable_method("max_time_ms", ms) + # JSON Helpers def to_json(self, *args, **kwargs): @@ -1700,6 +1708,13 @@ def field_path_sub(match): code) return code + def _chainable_method(self, method_name, val): + queryset = self.clone() + method = getattr(queryset._cursor, method_name) + method(val) + setattr(queryset, "_" + method_name, val) + return queryset + # Deprecated def ensure_index(self, **kwargs): """Deprecated use :func:`Document.ensure_index`""" diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index fc56df7dd..8efee8b3d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4401,6 +4401,10 @@ def test_delete_count(self): self.Person.objects().delete() self.assertEqual(self.Person.objects().skip(1).delete(), 0) # test Document delete without existing documents + def test_max_time_ms(self): + # 778: max_time_ms can get only int or None as input + self.assertRaises(TypeError, self.Person.objects(name="name").max_time_ms, "not a number") + if __name__ == '__main__': unittest.main() From 4b498ae8cd845b0f6ac34f8823fff0b964703c14 Mon Sep 17 00:00:00 2001 From: David Czarnecki Date: Fri, 31 Oct 2014 11:40:20 -0400 Subject: [PATCH 0009/1519] Fix the documentation for reverse_delete_rule. --- docs/guide/signals.rst | 2 +- mongoengine/fields.py | 4 ++-- tests/document/instance.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index dc295d472..fc93e4cae 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -145,7 +145,7 @@ cleaner looking while still allowing manual execution of the callback:: ReferenceFields and Signals --------------------------- -Currently `reverse_delete_rules` do not trigger signals on the other part of +Currently `reverse_delete_rule` does not trigger signals on the other part of the relationship. If this is required you must manually handle the reverse deletion. diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 2441a7540..870d09e57 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -859,7 +859,7 @@ class ReferenceField(BaseField): Use the `reverse_delete_rule` to handle what should happen if the document the field is referencing is deleted. EmbeddedDocuments, DictFields and - MapFields do not support reverse_delete_rules and an `InvalidDocumentError` + MapFields does not support reverse_delete_rule and an `InvalidDocumentError` will be raised if trying to set on one of these Document / Field types. The options are: @@ -883,7 +883,7 @@ class Bar(Document): Bar.register_delete_rule(Foo, 'bar', NULLIFY) .. note :: - `reverse_delete_rules` do not trigger pre / post delete signals to be + `reverse_delete_rule` does not trigger pre / post delete signals to be triggered. .. versionchanged:: 0.5 added `reverse_delete_rule` diff --git a/tests/document/instance.py b/tests/document/instance.py index f925aeeba..c226b6146 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -1795,7 +1795,7 @@ class Foo(Document): self.assertEqual(Bar.objects.count(), 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_rule_raise_errors(self): def throw_invalid_document_error(): class Blog(Document): From eb4e7735c1a612431e730320b488253f6d3708b9 Mon Sep 17 00:00:00 2001 From: David Czarnecki Date: Sat, 1 Nov 2014 13:15:02 -0400 Subject: [PATCH 0010/1519] Adding myself to AUTHORS and CHANGELOG --- AUTHORS | 1 + docs/changelog.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index ffd63227e..34f8903fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -210,3 +210,4 @@ that much better: * Jay Shirley (https://github.com/jshirley) * DavidBord (https://github.com/DavidBord) * Axel Haustant (https://github.com/noirbizarre) + * David Czarnecki (https://github.com/czarneckid) diff --git a/docs/changelog.rst b/docs/changelog.rst index 12712cd53..b8467381c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -56,6 +56,8 @@ Changes in 0.9.X - DEV - Allow atomic update for the entire `DictField` #742 - Added MultiPointField, MultiLineField, MultiPolygonField - Fix multiple connections aliases being rewritten #748 +- Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791 + Changes in 0.8.7 ================ From f571a944c93dd83d6cb2a7ae66b8c5b0f31ca292 Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Sun, 2 Nov 2014 17:52:44 +0300 Subject: [PATCH 0011/1519] Add #773 to changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b8467381c..777cab8d9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -37,7 +37,7 @@ Changes in 0.9.X - DEV - Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x. - Removing support for Python < 2.6.6 - Fixed $maxDistance location for geoJSON $near queries with MongoDB 2.6+ #664 -- QuerySet.modify() method to provide find_and_modify() like behaviour #677 +- QuerySet.modify() and Document.modify() methods to provide find_and_modify() like behaviour #677 #773 - Added support for the using() method on a queryset #676 - PYPY support #673 - Connection pooling #674 From 5112fb777ecc7094d498e3512ea868c515746292 Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Sun, 2 Nov 2014 17:56:25 +0300 Subject: [PATCH 0012/1519] Mention Document.modify() in the documentation --- docs/guide/querying.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 77e35df0d..bb8176fec 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -491,8 +491,11 @@ Documents may be updated atomically by using the :meth:`~mongoengine.queryset.QuerySet.update_one`, :meth:`~mongoengine.queryset.QuerySet.update` and :meth:`~mongoengine.queryset.QuerySet.modify` methods on a -:meth:`~mongoengine.queryset.QuerySet`. There are several different "modifiers" -that you may use with these methods: +:class:`~mongoengine.queryset.QuerySet` or +:meth:`~mongoengine.Document.modify` and +:meth:`~mongoengine.Document.save` (with :attr:`save_condition` argument) on a +:class:`~mongoengine.Document`. +There are several different "modifiers" that you may use with these methods: * ``set`` -- set a particular value * ``unset`` -- delete a particular value (since MongoDB v1.3+) From 28d62009a766cd52e37e7273c5eff61a1b6b1e65 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Wed, 5 Nov 2014 14:56:13 -0500 Subject: [PATCH 0013/1519] Update changelog.rst --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 777cab8d9..6925bc306 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -57,6 +57,7 @@ Changes in 0.9.X - DEV - Added MultiPointField, MultiLineField, MultiPolygonField - Fix multiple connections aliases being rewritten #748 - Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791 +- Make `in_bulk()` respect `no_dereference()` #775 Changes in 0.8.7 From c5f23ad93d669420112f8979f4d939a577eaaaf8 Mon Sep 17 00:00:00 2001 From: DavidBord Date: Thu, 18 Sep 2014 10:35:35 +0300 Subject: [PATCH 0014/1519] fix-#744: Querying by a field defined in a subclass raises InvalidQueryError --- docs/changelog.rst | 1 + mongoengine/base/document.py | 13 +++++++++++++ tests/queryset/queryset.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6925bc306..1feec3439 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Querying by a field defined in a subclass raises InvalidQueryError #744 - Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 - Allow specifying the '_cls' as a field for indexes #397 diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 869449f93..05fd1faf5 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -884,6 +884,19 @@ def _lookup_field(cls, parts): elif cls._dynamic: DynamicField = _import_class('DynamicField') field = DynamicField(db_field=field_name) + elif cls._meta.get("allow_inheritance", False) or cls._meta.get("abstract", False): + # 744: in case the field is defined in a subclass + field = None + for subcls in cls.__subclasses__(): + try: + field = subcls._lookup_field([field_name])[0] + except LookUpError: + continue + + if field is not None: + break + else: + raise LookUpError('Cannot resolve field "%s"' % field_name) else: raise LookUpError('Cannot resolve field "%s"' % field_name) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 8efee8b3d..7f548999a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4405,6 +4405,23 @@ def test_max_time_ms(self): # 778: max_time_ms can get only int or None as input self.assertRaises(TypeError, self.Person.objects(name="name").max_time_ms, "not a number") + def test_subclass_field_query(self): + class Animal(Document): + is_mamal = BooleanField() + meta = dict(allow_inheritance=True) + + class Cat(Animal): + whiskers_length = FloatField() + + class ScottishCat(Cat): + folded_ears = BooleanField() + + Animal(is_mamal=False).save() + Cat(is_mamal=True, whiskers_length=5.1).save() + ScottishCat(is_mamal=True, folded_ears=True).save() + self.assertEquals(Animal.objects(folded_ears=True).count(), 1) + self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1) + if __name__ == '__main__': unittest.main() From 914c5752a54a39b216e627b02ec935805d59ec38 Mon Sep 17 00:00:00 2001 From: DavidBord Date: Fri, 7 Nov 2014 09:18:42 +0200 Subject: [PATCH 0015/1519] fix-#759: with_limit_and_skip for count should default like in pymongo --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 2 +- mongoengine/queryset/queryset.py | 2 +- tests/queryset/queryset.py | 10 +++++----- tests/test_django.py | 5 +++++ 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1feec3439..c216d4bbe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- with_limit_and_skip for count should default like in pymongo #759 - Querying by a field defined in a subclass raises InvalidQueryError #744 - Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 95f4ae648..e43b78530 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -384,7 +384,7 @@ def insert(self, doc_or_docs, load_bulk=True, write_concern=None): self._document, documents=results, loaded=True) return return_one and results[0] or results - def count(self, with_limit_and_skip=True): + def count(self, with_limit_and_skip=False): """Count the selected elements in the query. :param with_limit_and_skip (optional): take any :meth:`limit` or diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index cebfcc50d..e8bd66ae0 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -94,7 +94,7 @@ def _populate_cache(self): except StopIteration: self._has_more = False - def count(self, with_limit_and_skip=True): + def count(self, with_limit_and_skip=False): """Count the selected elements in the query. :param with_limit_and_skip (optional): take any :meth:`limit` or diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7f548999a..40d010c66 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -914,7 +914,7 @@ def __repr__(self): docs = docs[1:4] self.assertEqual('[, , ]', "%s" % docs) - self.assertEqual(docs.count(), 3) + self.assertEqual(docs.count(with_limit_and_skip=True), 3) for doc in docs: self.assertEqual('.. queryset mid-iteration ..', repr(docs)) @@ -3244,7 +3244,7 @@ class Post(Document): for i in xrange(10): Post(title="Post %s" % i).save() - self.assertEqual(5, Post.objects.limit(5).skip(5).count()) + self.assertEqual(5, Post.objects.limit(5).skip(5).count(with_limit_and_skip=True)) self.assertEqual( 10, Post.objects.limit(5).skip(5).count(with_limit_and_skip=False)) @@ -4013,7 +4013,7 @@ class Person(Document): self.assertEqual(100, people._len) # Caused by list calling len self.assertEqual(q, 1) - people.count() # count is cached + people.count(with_limit_and_skip=True) # count is cached self.assertEqual(q, 1) def test_no_cached_queryset(self): @@ -4109,7 +4109,7 @@ def __unicode__(self): with query_counter() as q: self.assertEqual(q, 0) - self.assertEqual(users.count(), 7) + self.assertEqual(users.count(with_limit_and_skip=True), 7) for i, outer_user in enumerate(users): self.assertEqual(outer_user.name, names[i]) @@ -4117,7 +4117,7 @@ def __unicode__(self): inner_count = 0 # Calling len might disrupt the inner loop if there are bugs - self.assertEqual(users.count(), 7) + self.assertEqual(users.count(with_limit_and_skip=True), 7) for j, inner_user in enumerate(users): self.assertEqual(inner_user.name, names[j]) diff --git a/tests/test_django.py b/tests/test_django.py index 7badbd64f..c4d3d5829 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -170,7 +170,12 @@ def test_filter(self): """Ensure that a queryset and filters work as expected """ + class LimitCountQuerySet(QuerySet): + def count(self, with_limit_and_skip=True): + return super(LimitCountQuerySet, self).count(with_limit_and_skip) + class Note(Document): + meta = dict(queryset_class=LimitCountQuerySet) text = StringField() Note.drop_collection() From 1fcf0098044698ba95312cce7f671e8f792b3764 Mon Sep 17 00:00:00 2001 From: DavidBord Date: Fri, 7 Nov 2014 15:03:11 +0200 Subject: [PATCH 0016/1519] fix-#787: Fix storing value of precision attribute in DecimalField --- docs/changelog.rst | 1 + mongoengine/fields.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1feec3439..ff227f8d7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Fix storing value of precision attribute in DecimalField #787 - Querying by a field defined in a subclass raises InvalidQueryError #744 - Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 870d09e57..d9aeb3f16 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -308,7 +308,7 @@ def __init__(self, min_value=None, max_value=None, force_string=False, self.min_value = min_value self.max_value = max_value self.force_string = force_string - self.precision = decimal.Decimal(".%s" % ("0" * precision)) + self.precision = precision self.rounding = rounding super(DecimalField, self).__init__(**kwargs) @@ -322,7 +322,7 @@ def to_python(self, value): value = decimal.Decimal("%s" % value) except decimal.InvalidOperation: return value - return value.quantize(self.precision, rounding=self.rounding) + return value.quantize(decimal.Decimal(".%s" % ("0" * self.precision)), rounding=self.rounding) def to_mongo(self, value, use_db_field=True): if value is None: From bdbd495a9e778d12157b789a74e0e00a303b5efd Mon Sep 17 00:00:00 2001 From: DavidBord Date: Sun, 17 Aug 2014 13:57:48 +0300 Subject: [PATCH 0017/1519] fix-#734: set attribute to None does not work (at least for fields with default values). Solves #735 as well --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 16 +++++++++++----- tests/document/instance.py | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1feec3439..1842dca80 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Set attribute to None does not work (at least for fields with default values) #734 - Querying by a field defined in a subclass raises InvalidQueryError #744 - Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 5bb9c7ac4..2747dde1d 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -37,7 +37,7 @@ class BaseField(object): def __init__(self, db_field=None, name=None, required=False, default=None, unique=False, unique_with=None, primary_key=False, validation=None, choices=None, verbose_name=None, - help_text=None): + help_text=None, null=False): """ :param db_field: The database field to store this field in (defaults to the name of the field) @@ -60,6 +60,8 @@ def __init__(self, db_field=None, name=None, required=False, default=None, model forms from the document model. :param help_text: (optional) The help text for this field and is often used when generating model forms from the document model. + :param null: (optional) Is the field value can be null. If no and there is a default value + then the default value is set """ self.db_field = (db_field or name) if not primary_key else '_id' @@ -75,6 +77,7 @@ def __init__(self, db_field=None, name=None, required=False, default=None, self.choices = choices self.verbose_name = verbose_name self.help_text = help_text + self.null = null # Adjust the appropriate creation counter, and save our local copy. if self.db_field == '_id': @@ -100,10 +103,13 @@ def __set__(self, instance, value): # If setting to None and theres a default # Then set the value to the default value - if value is None and self.default is not None: - value = self.default - if callable(value): - value = value() + if value is None: + if self.null: + value = None + elif self.default is not None: + value = self.default + if callable(value): + value = value() if instance._initialised: try: diff --git a/tests/document/instance.py b/tests/document/instance.py index 360d53851..6bb203002 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2706,5 +2706,26 @@ class Person(Document): self.assertEquals(p4.height, 189) self.assertEquals(Person.objects(height=189).count(), 1) + def test_null_field(self): + # 734 + class User(Document): + name = StringField() + height = IntField(default=184, null=True) + User.objects.delete() + u = User(name='user') + u.save() + u_from_db = User.objects.get(name='user') + u_from_db.height = None + u_from_db.save() + self.assertEquals(u_from_db.height, None) + + # 735 + User.objects.delete() + u = User(name='user') + u.save() + User.objects(name='user').update_one(set__height=None, upsert=True) + u_from_db = User.objects.get(name='user') + self.assertEquals(u_from_db.height, None) + if __name__ == '__main__': unittest.main() From 70942ac0f645a6736f622da22758d33140e43514 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Fri, 7 Nov 2014 11:03:49 -0500 Subject: [PATCH 0018/1519] Update changelog.rst --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b3497f9cb..809650f39 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -61,6 +61,7 @@ Changes in 0.9.X - DEV - Fix multiple connections aliases being rewritten #748 - Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791 - Make `in_bulk()` respect `no_dereference()` #775 +- Handle None from model __str__; Fixes #753 #754 Changes in 0.8.7 From 3569529a84837d42a680e13f64bc912be6d0937f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ericson?= Date: Sat, 8 Nov 2014 19:11:51 -0300 Subject: [PATCH 0019/1519] Fix KeyError on reload() from a DynamicDocument If the document is in memory and a field is deleted from the db, calling reload() would raise a KeyError. --- AUTHORS | 1 + mongoengine/document.py | 8 +++++++- tests/document/dynamic.py | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 23930cc38..34f3d63fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -212,3 +212,4 @@ that much better: * Axel Haustant (https://github.com/noirbizarre) * David Czarnecki (https://github.com/czarneckid) * Vyacheslav Murashkin (https://github.com/a4tunado) + * André Ericson (https://github.com/aericson) diff --git a/mongoengine/document.py b/mongoengine/document.py index 2eab83ef3..1891c1642 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -543,7 +543,13 @@ def reload(self, *fields, **kwargs): for field in self._fields_ordered: if not fields or field in fields: - setattr(self, field, self._reload(field, obj[field])) + try: + setattr(self, field, self._reload(field, obj[field])) + except KeyError: + # If field is removed from the database while the object + # is in memory, a reload would cause a KeyError + # i.e. obj.update(unset__field=1) followed by obj.reload() + delattr(self, field) self._changed_fields = obj._changed_fields self._created = False diff --git a/tests/document/dynamic.py b/tests/document/dynamic.py index a0bdb136d..52136efca 100644 --- a/tests/document/dynamic.py +++ b/tests/document/dynamic.py @@ -81,6 +81,13 @@ def test_delete_dynamic_field(self): obj = collection.find_one() self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'name']) + def test_reload_after_unsetting(self): + p = self.Person() + p.misc = 22 + p.save() + p.update(unset__misc=1) + p.reload() + def test_dynamic_document_queries(self): """Ensure we can query dynamic fields""" p = self.Person() From 0452eec11d7b9ca39fb58f1141582d2a742f1ddb Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Sun, 9 Nov 2014 19:23:49 +0200 Subject: [PATCH 0020/1519] fix-#771: OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved --- docs/changelog.rst | 1 + mongoengine/base/document.py | 8 ++++---- tests/document/instance.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 809650f39..e3f3ecd17 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771 - with_limit_and_skip for count should default like in pymongo #759 - Fix storing value of precision attribute in DecimalField #787 - Querying by a field defined in a subclass raises InvalidQueryError #744 diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index bf5bdf794..8995a3047 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -386,9 +386,9 @@ def to_json(self, *args, **kwargs): return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) @classmethod - def from_json(cls, json_data): + def from_json(cls, json_data, created=False): """Converts json data to an unsaved document instance""" - return cls._from_son(json_util.loads(json_data)) + return cls._from_son(json_util.loads(json_data), created=created) def __expand_dynamic_values(self, name, value): """expand any dynamic values to their correct types / values""" @@ -617,7 +617,7 @@ def _get_collection_name(cls): return cls._meta.get('collection', None) @classmethod - def _from_son(cls, son, _auto_dereference=True, only_fields=[]): + def _from_son(cls, son, _auto_dereference=True, only_fields=[], created=False): """Create an instance of a Document (subclass) from a PyMongo SON. """ @@ -667,7 +667,7 @@ def _from_son(cls, son, _auto_dereference=True, only_fields=[]): if cls.STRICT: data = dict((k, v) for k, v in data.iteritems() if k in cls._fields) - obj = cls(__auto_convert=False, _created=False, __only_fields=only_fields, **data) + obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data) obj._changed_fields = changed_fields if not _auto_dereference: obj._fields = fields diff --git a/tests/document/instance.py b/tests/document/instance.py index a22f3fbf0..b77335489 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2719,5 +2719,16 @@ class Person(Document): self.assertEquals(p4.height, 189) self.assertEquals(Person.objects(height=189).count(), 1) + def test_from_son(self): + # 771 + class MyPerson(self.Person): + meta = dict(shard_key=["id"]) + p = MyPerson.from_json('{"name": "name", "age": 27}', created=True) + self.assertEquals(p.id, None) + p.id = "12345" # in case it is not working: "OperationError: Shard Keys are immutable..." will be raised here + p = MyPerson._from_son({"name": "name", "age": 27}, created=True) + self.assertEquals(p.id, None) + p.id = "12345" # in case it is not working: "OperationError: Shard Keys are immutable..." will be raised here + if __name__ == '__main__': unittest.main() From 2af55baa9aa9c038df948b5545a2ee44d82258b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ericson?= Date: Sun, 9 Nov 2014 16:13:49 -0300 Subject: [PATCH 0021/1519] Better BaseDocument equality check when not saved When 2 instances of a Document had id = None they would be considered equal unless an __eq__ were implemented. We now return False for such case. It now behaves more similar to Django's ORM. --- mongoengine/base/document.py | 4 +++- tests/document/instance.py | 19 +++++++++++++++++++ tests/document/json_serialisation.py | 8 ++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index bf5bdf794..6be685ae3 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -241,10 +241,12 @@ def __str__(self): return txt_type('%s object' % self.__class__.__name__) def __eq__(self, other): - if isinstance(other, self.__class__) and hasattr(other, 'id'): + if isinstance(other, self.__class__) and hasattr(other, 'id') and other.id is not None: return self.id == other.id if isinstance(other, DBRef): return self._get_collection_name() == other.collection and self.id == other.id + if self.id is None: + return self is other return False def __ne__(self, other): diff --git a/tests/document/instance.py b/tests/document/instance.py index a22f3fbf0..6dae88a06 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2503,6 +2503,10 @@ class Doc(Document): doc_name = StringField() doc = EmbeddedDocumentField(Embedded) + def __eq__(self, other): + return (self.doc_name == other.doc_name and + self.doc == other.doc) + classic_doc = Doc(doc_name="my doc", doc=Embedded(name="embedded doc")) dict_doc = Doc(**{"doc_name": "my doc", "doc": {"name": "embedded doc"}}) @@ -2519,6 +2523,10 @@ class Doc(Document): doc_name = StringField() docs = ListField(EmbeddedDocumentField(Embedded)) + def __eq__(self, other): + return (self.doc_name == other.doc_name and + self.docs == other.docs) + classic_doc = Doc(doc_name="my doc", docs=[ Embedded(name="embedded doc1"), Embedded(name="embedded doc2")]) @@ -2719,5 +2727,16 @@ class Person(Document): self.assertEquals(p4.height, 189) self.assertEquals(Person.objects(height=189).count(), 1) + def test_not_saved_eq(self): + """Ensure we can compare documents not saved. + """ + class Person(Document): + pass + + p = Person() + p1 = Person() + self.assertNotEqual(p, p1) + self.assertEqual(p, p) + if __name__ == '__main__': unittest.main() diff --git a/tests/document/json_serialisation.py b/tests/document/json_serialisation.py index fd7795f7d..f47b5de5b 100644 --- a/tests/document/json_serialisation.py +++ b/tests/document/json_serialisation.py @@ -51,6 +51,10 @@ class Doc(Document): string = StringField() embedded_field = EmbeddedDocumentField(Embedded) + def __eq__(self, other): + return (self.string == other.string and + self.embedded_field == other.embedded_field) + doc = Doc(string="Hi", embedded_field=Embedded(string="Hi")) doc_json = doc.to_json(sort_keys=True, separators=(',', ':')) @@ -99,6 +103,10 @@ class Doc(Document): generic_embedded_document_field = GenericEmbeddedDocumentField( default=lambda: EmbeddedDoc()) + def __eq__(self, other): + import json + return json.loads(self.to_json()) == json.loads(other.to_json()) + doc = Doc() self.assertEqual(doc, Doc.from_json(doc.to_json())) From 5583cf0a5fc217623600c287bfb5093921f41966 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Sun, 9 Nov 2014 21:27:23 -0500 Subject: [PATCH 0022/1519] PEP8 compliance tests/document/instance.py --- tests/document/instance.py | 117 ++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 5360ba363..36118512b 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -146,10 +146,18 @@ def test_polymorphic_references(self): """ class Animal(Document): meta = {'allow_inheritance': True} - class Fish(Animal): pass - class Mammal(Animal): pass - class Dog(Mammal): pass - class Human(Mammal): pass + + class Fish(Animal): + pass + + class Mammal(Animal): + pass + + class Dog(Mammal): + pass + + class Human(Mammal): + pass class Zoo(Document): animals = ListField(ReferenceField(Animal)) @@ -461,7 +469,7 @@ class Foo(Document): f.reload() except Foo.DoesNotExist: pass - except Exception as ex: + except Exception: self.assertFalse("Threw wrong exception") f.save() @@ -505,8 +513,9 @@ class Employee(Person): self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(), ['_cls', 'name', 'age']) - self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(), - ['_cls', 'name', 'age', 'salary']) + self.assertEqual( + Employee(name="Bob", age=35, salary=0).to_mongo().keys(), + ['_cls', 'name', 'age', 'salary']) def test_embedded_document_to_mongo_id(self): class SubDoc(EmbeddedDocument): @@ -643,7 +652,8 @@ class TestDocument(Document): def test_modify_empty(self): doc = self.Person(name="bob", age=10).save() - self.assertRaises(InvalidDocumentError, lambda: self.Person().modify(set__age=10)) + self.assertRaises( + InvalidDocumentError, lambda: self.Person().modify(set__age=10)) self.assertDbEqual([dict(doc.to_mongo())]) def test_modify_invalid_query(self): @@ -651,8 +661,9 @@ def test_modify_invalid_query(self): doc2 = self.Person(name="jim", age=20).save() docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] - self.assertRaises(InvalidQueryError, lambda: - doc1.modify(dict(id=doc2.id), set__value=20)) + self.assertRaises( + InvalidQueryError, + lambda: doc1.modify(dict(id=doc2.id), set__value=20)) self.assertDbEqual(docs) @@ -676,7 +687,8 @@ def test_modify_not_exists(self): def test_modify_update(self): other_doc = self.Person(name="bob", age=10).save() - doc = self.Person(name="jim", age=20, job=self.Job(name="10gen", years=3)).save() + doc = self.Person( + name="jim", age=20, job=self.Job(name="10gen", years=3)).save() doc_copy = doc._from_son(doc.to_mongo()) @@ -685,7 +697,8 @@ def test_modify_update(self): doc.job.name = "Google" doc.job.years = 3 - assert doc.modify(set__age=21, set__job__name="MongoDB", unset__job__years=True) + assert doc.modify( + set__age=21, set__job__name="MongoDB", unset__job__years=True) doc_copy.age = 21 doc_copy.job.name = "MongoDB" del doc_copy.job.years @@ -933,7 +946,7 @@ def UUID(i): w1 = Widget(toggle=False, save_id=UUID(1)) # ignore save_condition on new record creation - w1.save(save_condition={'save_id':UUID(42)}) + w1.save(save_condition={'save_id': UUID(42)}) w1.reload() self.assertFalse(w1.toggle) self.assertEqual(w1.save_id, UUID(1)) @@ -943,7 +956,7 @@ def UUID(i): flip(w1) self.assertTrue(w1.toggle) self.assertEqual(w1.count, 1) - w1.save(save_condition={'save_id':UUID(42)}) + w1.save(save_condition={'save_id': UUID(42)}) w1.reload() self.assertFalse(w1.toggle) self.assertEqual(w1.count, 0) @@ -952,7 +965,7 @@ def UUID(i): flip(w1) self.assertTrue(w1.toggle) self.assertEqual(w1.count, 1) - w1.save(save_condition={'save_id':UUID(1)}) + w1.save(save_condition={'save_id': UUID(1)}) w1.reload() self.assertTrue(w1.toggle) self.assertEqual(w1.count, 1) @@ -965,25 +978,25 @@ def UUID(i): flip(w1) w1.save_id = UUID(2) - w1.save(save_condition={'save_id':old_id}) + w1.save(save_condition={'save_id': old_id}) w1.reload() self.assertFalse(w1.toggle) self.assertEqual(w1.count, 2) flip(w2) flip(w2) - w2.save(save_condition={'save_id':old_id}) + w2.save(save_condition={'save_id': old_id}) w2.reload() self.assertFalse(w2.toggle) self.assertEqual(w2.count, 2) # save_condition uses mongoengine-style operator syntax flip(w1) - w1.save(save_condition={'count__lt':w1.count}) + w1.save(save_condition={'count__lt': w1.count}) w1.reload() self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) flip(w1) - w1.save(save_condition={'count__gte':w1.count}) + w1.save(save_condition={'count__gte': w1.count}) w1.reload() self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) @@ -1429,7 +1442,8 @@ def test_save_custom_id(self): self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') def test_save_custom_pk(self): - """Ensure that a document may be saved with a custom _id using pk alias. + """ + Ensure that a document may be saved with a custom _id using pk alias. """ # Create person object and save it to the database person = self.Person(name='Test User', age=30, @@ -1515,9 +1529,15 @@ class Page(Document): p4 = Page(comments=[Comment(user=u2, comment="Heavy Metal song")]) p4.save() - self.assertEqual([p1, p2], list(Page.objects.filter(comments__user=u1))) - self.assertEqual([p1, p2, p4], list(Page.objects.filter(comments__user=u2))) - self.assertEqual([p1, p3], list(Page.objects.filter(comments__user=u3))) + self.assertEqual( + [p1, p2], + list(Page.objects.filter(comments__user=u1))) + self.assertEqual( + [p1, p2, p4], + list(Page.objects.filter(comments__user=u2))) + self.assertEqual( + [p1, p3], + list(Page.objects.filter(comments__user=u3))) def test_save_embedded_document(self): """Ensure that a document with an embedded document field may be @@ -1592,7 +1612,8 @@ class Employee(self.Person): self.assertEqual(promoted_employee.age, 50) # Ensure that the 'details' embedded object saved correctly - self.assertEqual(promoted_employee.details.position, 'Senior Developer') + self.assertEqual( + promoted_employee.details.position, 'Senior Developer') # Test removal promoted_employee.details = None @@ -1728,7 +1749,8 @@ class BlogPost(Document): post.save() reviewer.delete() - self.assertEqual(BlogPost.objects.count(), 1) # No effect on the BlogPost + # No effect on the BlogPost + self.assertEqual(BlogPost.objects.count(), 1) self.assertEqual(BlogPost.objects.get().reviewer, None) # Delete the Person, which should lead to deletion of the BlogPost, too @@ -1777,8 +1799,10 @@ def test_reverse_delete_rule_cascade_and_nullify_complex_field(self): class BlogPost(Document): content = StringField() - authors = ListField(ReferenceField(self.Person, reverse_delete_rule=CASCADE)) - reviewers = ListField(ReferenceField(self.Person, reverse_delete_rule=NULLIFY)) + authors = ListField(ReferenceField( + self.Person, reverse_delete_rule=CASCADE)) + reviewers = ListField(ReferenceField( + self.Person, reverse_delete_rule=NULLIFY)) self.Person.drop_collection() @@ -1878,8 +1902,12 @@ def test_invalid_reverse_delete_rule_raise_errors(self): def throw_invalid_document_error(): class Blog(Document): content = StringField() - authors = MapField(ReferenceField(self.Person, reverse_delete_rule=CASCADE)) - reviewers = DictField(field=ReferenceField(self.Person, reverse_delete_rule=NULLIFY)) + authors = MapField(ReferenceField( + self.Person, reverse_delete_rule=CASCADE)) + reviewers = DictField( + field=ReferenceField( + self.Person, + reverse_delete_rule=NULLIFY)) self.assertRaises(InvalidDocumentError, throw_invalid_document_error) @@ -1888,7 +1916,8 @@ class Parents(EmbeddedDocument): father = ReferenceField('Person', reverse_delete_rule=DENY) mother = ReferenceField('Person', reverse_delete_rule=DENY) - self.assertRaises(InvalidDocumentError, throw_invalid_document_error_embedded) + self.assertRaises( + InvalidDocumentError, throw_invalid_document_error_embedded) def test_reverse_delete_rule_cascade_recurs(self): """Ensure that a chain of documents is also deleted upon cascaded @@ -1910,16 +1939,16 @@ class Comment(Document): author = self.Person(name='Test User') author.save() - post = BlogPost(content = 'Watched some TV') + post = BlogPost(content='Watched some TV') post.author = author post.save() - comment = Comment(text = 'Kudos.') + comment = Comment(text='Kudos.') comment.post = post comment.save() - # Delete the Person, which should lead to deletion of the BlogPost, and, - # recursively to the Comment, too + # Delete the Person, which should lead to deletion of the BlogPost, + # and, recursively to the Comment, too author.delete() self.assertEqual(Comment.objects.count(), 0) @@ -1942,7 +1971,7 @@ class BlogPost(Document): author = self.Person(name='Test User') author.save() - post = BlogPost(content = 'Watched some TV') + post = BlogPost(content='Watched some TV') post.author = author post.save() @@ -2058,7 +2087,8 @@ def test_picklable(self): def test_dynamic_document_pickle(self): - pickle_doc = PickleDynamicTest(name="test", number=1, string="One", lists=['1', '2']) + pickle_doc = PickleDynamicTest( + name="test", number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar") pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved @@ -2080,7 +2110,8 @@ def test_dynamic_document_pickle(self): pickle_doc.embedded._dynamic_fields.keys()) def test_picklable_on_signals(self): - pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2']) + pickle_doc = PickleSignalsTest( + number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() pickle_doc.save() pickle_doc.delete() @@ -2235,9 +2266,15 @@ class AuthorBooks(Document): self.assertEqual(AuthorBooks._get_db(), get_db("testdb-3")) # Collections - self.assertEqual(User._get_collection(), get_db("testdb-1")[User._get_collection_name()]) - self.assertEqual(Book._get_collection(), get_db("testdb-2")[Book._get_collection_name()]) - self.assertEqual(AuthorBooks._get_collection(), get_db("testdb-3")[AuthorBooks._get_collection_name()]) + self.assertEqual( + User._get_collection(), + get_db("testdb-1")[User._get_collection_name()]) + self.assertEqual( + Book._get_collection(), + get_db("testdb-2")[Book._get_collection_name()]) + self.assertEqual( + AuthorBooks._get_collection(), + get_db("testdb-3")[AuthorBooks._get_collection_name()]) def test_db_alias_overrides(self): """db_alias can be overriden From eac4f6062edd0d7df187634d3abd9a479d222165 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Sun, 9 Nov 2014 21:28:03 -0500 Subject: [PATCH 0023/1519] Fix merge in docs/changelog.rst --- docs/changelog.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d0af955e0..d1714e788 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,13 +5,10 @@ Changelog Changes in 0.9.X - DEV ====================== -<<<<<<< HEAD - OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771 - with_limit_and_skip for count should default like in pymongo #759 - Fix storing value of precision attribute in DecimalField #787 -======= - Set attribute to None does not work (at least for fields with default values) #734 ->>>>>>> bdbd495a9e778d12157b789a74e0e00a303b5efd - Querying by a field defined in a subclass raises InvalidQueryError #744 - Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 From 2e01e0c30e7dbf373add8d97a1a76375eeacf3fe Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Sun, 9 Nov 2014 21:32:50 -0500 Subject: [PATCH 0024/1519] Added merge to changelog.rst --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d1714e788..eb8ccc346 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Better BaseDocument equality check when not saved #798 - OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771 - with_limit_and_skip for count should default like in pymongo #759 - Fix storing value of precision attribute in DecimalField #787 From 57328e55f3da6fd3504d64f2ba478cfcef3d1e57 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 10 Nov 2014 09:37:35 +0200 Subject: [PATCH 0025/1519] Bumped django versions and added 1.7.1. --- .travis.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a7f97e78..8724a8332 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,14 +10,17 @@ python: - "pypy3" env: - PYMONGO=dev DJANGO=dev - - PYMONGO=dev DJANGO=1.6.5 - - PYMONGO=dev DJANGO=1.5.8 + - PYMONGO=dev DJANGO=1.7.1 + - PYMONGO=dev DJANGO=1.6.8 + - PYMONGO=dev DJANGO=1.5.11 - PYMONGO=2.7.1 DJANGO=dev - - PYMONGO=2.7.1 DJANGO=1.6.5 - - PYMONGO=2.7.1 DJANGO=1.5.8 + - PYMONGO=dev DJANGO=1.7.1 + - PYMONGO=2.7.1 DJANGO=1.6.8 + - PYMONGO=2.7.1 DJANGO=1.5.11 - PYMONGO=2.7.2 DJANGO=dev - - PYMONGO=2.7.2 DJANGO=1.6.5 - - PYMONGO=2.7.2 DJANGO=1.5.8 + - PYMONGO=dev DJANGO=1.7.1 + - PYMONGO=2.7.2 DJANGO=1.6.8 + - PYMONGO=2.7.2 DJANGO=1.5.11 matrix: exclude: @@ -41,7 +44,7 @@ install: - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; fi - - if [[ $DJANGO == 'dev' ]]; then travis_retry pip install https://www.djangoproject.com/download/1.7c2/tarball/; fi + - if [[ $DJANGO == 'dev' ]]; then travis_retry https://github.com/django/django/tarball/master; fi - if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi - travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - travis_retry pip install coveralls From a4244defb530d01dea67e67e8dc2a5fb348a7a5f Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 10 Nov 2014 09:41:29 +0200 Subject: [PATCH 0026/1519] Fixed build matrix. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8724a8332..14a626754 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,11 +14,11 @@ env: - PYMONGO=dev DJANGO=1.6.8 - PYMONGO=dev DJANGO=1.5.11 - PYMONGO=2.7.1 DJANGO=dev - - PYMONGO=dev DJANGO=1.7.1 + - PYMONGO=2.7.1 DJANGO=1.7.1 - PYMONGO=2.7.1 DJANGO=1.6.8 - PYMONGO=2.7.1 DJANGO=1.5.11 - PYMONGO=2.7.2 DJANGO=dev - - PYMONGO=dev DJANGO=1.7.1 + - PYMONGO=2.7.2 DJANGO=1.7.1 - PYMONGO=2.7.2 DJANGO=1.6.8 - PYMONGO=2.7.2 DJANGO=1.5.11 From a22ad1ec3277de53d01f257cec9ef1b5d6298e2e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 11 Nov 2014 09:09:21 +0200 Subject: [PATCH 0027/1519] Exclude Django 1,7 and Python 2.6 since Django 1.7 doesn't support 2.6. --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 14a626754..850ecbe04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,12 @@ matrix: env: PYMONGO=2.7.1 DJANGO=dev - python: "2.6" env: PYMONGO=2.7.2 DJANGO=dev + - python: "2.6" + env: PYMONGO=dev DJANGO=1.7.1 + - python: "2.6" + env: PYMONGO=2.7.1 DJANGO=1.7.1 + - python: "2.6" + env: PYMONGO=2.7.2 DJANGO=1.7.1 allow_failures: - python: "pypy3" fast_finish: true From 96aedaa91fe437d72b931ef64f53740a76197313 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Wed, 12 Nov 2014 12:06:20 -0500 Subject: [PATCH 0028/1519] Install Django dev from repo with pip --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 850ecbe04..2d03faecc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ install: - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; fi - - if [[ $DJANGO == 'dev' ]]; then travis_retry https://github.com/django/django/tarball/master; fi + - if [[ $DJANGO == 'dev' ]]; then travis_retry pip install git+https://github.com/django/django.git; fi - if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi - travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - travis_retry pip install coveralls From b2c8c326d75a2ef61464d17ffd332e0d3d7cf896 Mon Sep 17 00:00:00 2001 From: mikhailmoshnogorsky Date: Wed, 12 Nov 2014 17:00:07 -0500 Subject: [PATCH 0029/1519] write_concern not in params of Collection#remove --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index e43b78530..8c880e973 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -459,7 +459,7 @@ def delete(self, write_concern=None, _from_doc_delete=False): write_concern=write_concern, **{'pull_all__%s' % field_name: self}) - result = queryset._collection.remove(queryset._query, write_concern=write_concern) + result = queryset._collection.remove(queryset._query, **write_concern) return result["n"] def update(self, upsert=False, multi=True, write_concern=None, From 03c2967337da63abca41e2e266d54f098d53806f Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Thu, 13 Nov 2014 20:47:31 +0200 Subject: [PATCH 0030/1519] Update changelog & authors - #801 --- AUTHORS | 5 +++-- docs/changelog.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 34f3d63fd..6079ff439 100644 --- a/AUTHORS +++ b/AUTHORS @@ -208,8 +208,9 @@ that much better: * Norberto Leite (https://github.com/nleite) * Bob Cribbs (https://github.com/bocribbz) * Jay Shirley (https://github.com/jshirley) - * DavidBord (https://github.com/DavidBord) + * David Bordeynik (https://github.com/DavidBord) * Axel Haustant (https://github.com/noirbizarre) * David Czarnecki (https://github.com/czarneckid) * Vyacheslav Murashkin (https://github.com/a4tunado) - * André Ericson (https://github.com/aericson) + * André Ericson https://github.com/aericson) + * Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky) diff --git a/docs/changelog.rst b/docs/changelog.rst index eb8ccc346..153bf42fd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- write_concern not in params of Collection#remove #801 - Better BaseDocument equality check when not saved #798 - OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771 - with_limit_and_skip for count should default like in pymongo #759 From 8965172603cad014b1c891d2518ab38658b62161 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Fri, 14 Nov 2014 21:42:41 +0200 Subject: [PATCH 0031/1519] fix-#515: sparse fields --- docs/changelog.rst | 1 + mongoengine/base/document.py | 3 +-- mongoengine/base/fields.py | 5 ++++- tests/fields/fields.py | 9 +++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 153bf42fd..35b94b1e9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Sparse fields #515 - write_concern not in params of Collection#remove #801 - Better BaseDocument equality check when not saved #798 - OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771 diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index aea251e42..854ed6b42 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -786,10 +786,9 @@ def _unique_with_indexes(cls, namespace=""): """ unique_indexes = [] for field_name, field in cls._fields.items(): - sparse = False + sparse = field.sparse # Generate a list of indexes needed by uniqueness constraints if field.unique: - field.required = True unique_fields = [field.db_field] # Add any unique_with fields to the back of the index spec diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 2747dde1d..f86d4175d 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -37,7 +37,7 @@ class BaseField(object): def __init__(self, db_field=None, name=None, required=False, default=None, unique=False, unique_with=None, primary_key=False, validation=None, choices=None, verbose_name=None, - help_text=None, null=False): + help_text=None, null=False, sparse=False): """ :param db_field: The database field to store this field in (defaults to the name of the field) @@ -62,6 +62,8 @@ def __init__(self, db_field=None, name=None, required=False, default=None, used when generating model forms from the document model. :param null: (optional) Is the field value can be null. If no and there is a default value then the default value is set + :param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False` + means that uniqueness won't be enforced for `None` values """ self.db_field = (db_field or name) if not primary_key else '_id' @@ -78,6 +80,7 @@ def __init__(self, db_field=None, name=None, required=False, default=None, self.verbose_name = verbose_name self.help_text = help_text self.null = null + self.sparse = sparse # Adjust the appropriate creation counter, and save our local copy. if self.db_field == '_id': diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 26a30bfcc..1079b2357 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3057,6 +3057,15 @@ class Human(Mammal): self.assertEquals(Animal.objects(_cls__in=["Animal.Mammal.Dog", "Animal.Fish"]).count(), 2) self.assertEquals(Animal.objects(_cls__in=["Animal.Fish.Guppy"]).count(), 0) + def test_sparse_field(self): + class Doc(Document): + name = StringField(required=False, unique=True, sparse=True) + try: + Doc().save() + Doc().save() + except Exception: + self.fail() + if __name__ == '__main__': unittest.main() From 17f7e2f8924c548ea03a100eb8e4b33a7a829969 Mon Sep 17 00:00:00 2001 From: Diego Berrocal Date: Wed, 19 Nov 2014 02:49:08 -0500 Subject: [PATCH 0032/1519] Big typo fix for allow_inheritance page --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 8db991c1c..5b8327c31 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -704,7 +704,7 @@ document.:: class DatedPage(Page): date = DateTimeField() -.. note:: From 0.8 onwards you must declare :attr:`allow_inheritance` defaults +.. note:: From 0.8 onwards :attr:`allow_inheritance` defaults to False, meaning you must set it to True to use inheritance. Working with existing data From 285da0542ec3c840728cf805304a85a07851095c Mon Sep 17 00:00:00 2001 From: Diego Berrocal Date: Wed, 19 Nov 2014 09:39:51 -0500 Subject: [PATCH 0033/1519] Update AUTHORS with @cestdiego --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 6079ff439..d377d177f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -214,3 +214,4 @@ that much better: * Vyacheslav Murashkin (https://github.com/a4tunado) * André Ericson https://github.com/aericson) * Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky) + * Diego Berrocal (https://github.com/cestdiego) From bc53dd68308c88cc41bb3fb183c1d41392b40968 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Wed, 19 Nov 2014 15:50:32 -0500 Subject: [PATCH 0034/1519] Generate Unique Indices for Lists of EmbeddedDocs - Unique indices are now created in the database for EmbeddedDocument fields when the EmbeddedDocument is in a ListField - Closes Issue #358 --- AUTHORS | 1 + mongoengine/base/document.py | 3 +++ tests/document/indexes.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/AUTHORS b/AUTHORS index d377d177f..fb9a1ce5a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -215,3 +215,4 @@ that much better: * André Ericson https://github.com/aericson) * Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky) * Diego Berrocal (https://github.com/cestdiego) + * Matthew Ellison (https://github.com/mmelliso) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 854ed6b42..7a0130215 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -816,6 +816,9 @@ def _unique_with_indexes(cls, namespace=""): index = {'fields': fields, 'unique': True, 'sparse': sparse} unique_indexes.append(index) + if field.__class__.__name__ == "ListField": + field = field.field + # Grab any embedded document field unique indexes if (field.__class__.__name__ == "EmbeddedDocumentField" and field.document_type != cls): diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 4103c7ae7..4815350f6 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -577,6 +577,38 @@ class BlogPost(Document): BlogPost.drop_collection() + def test_unique_embedded_document_in_list(self): + """ + Ensure that the uniqueness constraints are applied to fields in + embedded documents, even when the embedded documents in in a + list field. + """ + class SubDocument(EmbeddedDocument): + year = IntField(db_field='yr') + slug = StringField(unique=True) + + class BlogPost(Document): + title = StringField() + subs = ListField(EmbeddedDocumentField(SubDocument)) + + BlogPost.drop_collection() + + post1 = BlogPost( + title='test1', subs=[ + SubDocument(year=2009, slug='conflict'), + SubDocument(year=2009, slug='conflict') + ] + ) + post1.save() + + post2 = BlogPost( + title='test2', subs=[SubDocument(year=2014, slug='conflict')] + ) + + self.assertRaises(NotUniqueError, post2.save) + + BlogPost.drop_collection() + def test_unique_with_embedded_document_and_embedded_unique(self): """Ensure that uniqueness constraints are applied to fields on embedded documents. And work with unique_with as well. From 43af9f3fad06388dd02f3da32e241592fc7d22b4 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Thu, 20 Nov 2014 11:20:04 -0500 Subject: [PATCH 0035/1519] Update Tests for EmbeddedDocument Unique Indicies --- tests/fields/fields.py | 8 ++++---- tests/queryset/queryset.py | 10 +++++----- tests/test_dereference.py | 17 +++++++++-------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 1079b2357..d7edea376 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1974,14 +1974,14 @@ class Employee(Document): def test_recursive_embedding(self): """Ensure that EmbeddedDocumentFields can contain their own documents. """ - class Tree(Document): - name = StringField() - children = ListField(EmbeddedDocumentField('TreeNode')) - class TreeNode(EmbeddedDocument): name = StringField() children = ListField(EmbeddedDocumentField('self')) + class Tree(Document): + name = StringField() + children = ListField(EmbeddedDocumentField('TreeNode')) + Tree.drop_collection() tree = Tree(name="Tree") diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 40d010c66..703728fe1 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -512,16 +512,16 @@ class BlogPost(Document): def test_updates_can_have_match_operators(self): - class Post(Document): - title = StringField(required=True) - tags = ListField(StringField()) - comments = ListField(EmbeddedDocumentField("Comment")) - class Comment(EmbeddedDocument): content = StringField() name = StringField(max_length=120) vote = IntField() + class Post(Document): + title = StringField(required=True) + tags = ListField(StringField()) + comments = ListField(EmbeddedDocumentField("Comment")) + Post.drop_collection() comm1 = Comment(content="very funny indeed", name="John S", vote=1) diff --git a/tests/test_dereference.py b/tests/test_dereference.py index c37ada595..85588558e 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -318,6 +318,10 @@ class SimpleList(Document): def test_circular_reference(self): """Ensure you can handle circular references """ + class Relation(EmbeddedDocument): + name = StringField() + person = ReferenceField('Person') + class Person(Document): name = StringField() relations = ListField(EmbeddedDocumentField('Relation')) @@ -325,10 +329,6 @@ class Person(Document): def __repr__(self): return "" % self.name - class Relation(EmbeddedDocument): - name = StringField() - person = ReferenceField('Person') - Person.drop_collection() mother = Person(name="Mother") daughter = Person(name="Daughter") @@ -1220,14 +1220,15 @@ class Page(Document): self.assertEqual(page.tags[0], page.posts[0].tags[0]) def test_select_related_follows_embedded_referencefields(self): - class Playlist(Document): - items = ListField(EmbeddedDocumentField("PlaylistItem")) + + class Song(Document): + title = StringField() class PlaylistItem(EmbeddedDocument): song = ReferenceField("Song") - class Song(Document): - title = StringField() + class Playlist(Document): + items = ListField(EmbeddedDocumentField("PlaylistItem")) Playlist.drop_collection() Song.drop_collection() From d70b7d41e816d5fde6d3b2e2a3696235a2e0993e Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Fri, 21 Nov 2014 07:29:50 -0500 Subject: [PATCH 0036/1519] Update to Changelog to include Fix for #358 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 35b94b1e9..0ccb8491c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Generate Unique Indicies for Lists of EmbeddedDocuments #358 - Sparse fields #515 - write_concern not in params of Collection#remove #801 - Better BaseDocument equality check when not saved #798 From e0f1e79e6ae72939973e900482d3db2a358209e2 Mon Sep 17 00:00:00 2001 From: Slam <3lnc.slam@gmail.com> Date: Mon, 24 Nov 2014 16:57:43 +0200 Subject: [PATCH 0037/1519] Minor typos fixes in docs --- docs/changelog.rst | 20 ++++++++++---------- docs/guide/connecting.rst | 5 +++-- docs/guide/defining-documents.rst | 8 ++++---- docs/guide/querying.rst | 5 +++-- docs/upgrade.rst | 4 ++-- mongoengine/base/document.py | 2 +- mongoengine/document.py | 2 +- mongoengine/fields.py | 12 ++++++------ mongoengine/queryset/base.py | 6 +++--- mongoengine/queryset/visitor.py | 2 +- 10 files changed, 34 insertions(+), 32 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 35b94b1e9..1d9743534 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -71,7 +71,7 @@ Changes in 0.9.X - DEV Changes in 0.8.7 ================ -- Calling reload on deleted / nonexistant documents raises DoesNotExist (#538) +- Calling reload on deleted / nonexistent documents raises DoesNotExist (#538) - Stop ensure_indexes running on a secondaries (#555) - Fix circular import issue with django auth (#531) (#545) @@ -84,7 +84,7 @@ Changes in 0.8.5 - Fix multi level nested fields getting marked as changed (#523) - Django 1.6 login fix (#522) (#527) - Django 1.6 session fix (#509) -- EmbeddedDocument._instance is now set when settng the attribute (#506) +- EmbeddedDocument._instance is now set when setting the attribute (#506) - Fixed EmbeddedDocument with ReferenceField equality issue (#502) - Fixed GenericReferenceField serialization order (#499) - Fixed count and none bug (#498) @@ -174,7 +174,7 @@ Changes in 0.8.0 - Added `get_next_value` preview for SequenceFields (#319) - Added no_sub_classes context manager and queryset helper (#312) - Querysets now utilises a local cache -- Changed __len__ behavour in the queryset (#247, #311) +- Changed __len__ behaviour in the queryset (#247, #311) - Fixed querying string versions of ObjectIds issue with ReferenceField (#307) - Added $setOnInsert support for upserts (#308) - Upserts now possible with just query parameters (#309) @@ -225,7 +225,7 @@ Changes in 0.8.0 - Uses getlasterror to test created on updated saves (#163) - Fixed inheritance and unique index creation (#140) - Fixed reverse delete rule with inheritance (#197) -- Fixed validation for GenericReferences which havent been dereferenced +- Fixed validation for GenericReferences which haven't been dereferenced - Added switch_db context manager (#106) - Added switch_db method to document instances (#106) - Added no_dereference context manager (#82) (#61) @@ -307,11 +307,11 @@ Changes in 0.7.2 - Update index spec generation so its not destructive (#113) Changes in 0.7.1 -================= +================ - Fixed index spec inheritance (#111) Changes in 0.7.0 -================= +================ - Updated queryset.delete so you can use with skip / limit (#107) - Updated index creation allows kwargs to be passed through refs (#104) - Fixed Q object merge edge case (#109) @@ -392,7 +392,7 @@ Changes in 0.6.12 - Fixes error with _delta handling DBRefs 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 @@ -472,7 +472,7 @@ Changes in 0.6.1 - Fix for replicaSet connections Changes in 0.6 -================ +============== - Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7 - Added support for covered indexes when inheritance is off @@ -560,8 +560,8 @@ Changes in v0.5 - Updated default collection naming convention - Added Document Mixin support - Fixed queryet __repr__ mid iteration -- Added hint() support, so cantell Mongo the proper index to use for the query -- Fixed issue with inconsitent setting of _cls breaking inherited referencing +- Added hint() support, so can tell Mongo the proper index to use for the query +- Fixed issue with inconsistent setting of _cls breaking inherited referencing - Added help_text and verbose_name to fields to help with some form libs - Updated item_frequencies to handle embedded document lookups - Added delta tracking now only sets / unsets explicitly changed fields diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index fcdfe0a34..0e61df96c 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -23,14 +23,14 @@ arguments should be provided:: connect('project1', username='webapp', password='pwd123') -Uri style connections are also supported - just supply the uri as +URI style connections are also supported - just supply the uri as the :attr:`host` to :func:`~mongoengine.connect`:: connect('project1', host='mongodb://localhost/database_name') Note that database name from uri has priority over name -in ::func:`~mongoengine.connect` +in :func:`~mongoengine.connect` ReplicaSets =========== @@ -115,3 +115,4 @@ access to the same Group document across collection:: with switch_collection(Group, 'group2000') as Group: Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection + diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 5b8327c31..315fcde83 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -334,7 +334,7 @@ Its value can take any of the following constants: Any object's fields still referring to the object being deleted are removed (using MongoDB's "unset" operation), effectively nullifying the relationship. :const:`mongoengine.CASCADE` - Any object containing fields that are refererring to the object being deleted + Any object containing fields that are referring to the object being deleted are deleted first. :const:`mongoengine.PULL` Removes the reference to the object (using MongoDB's "pull" operation) @@ -428,7 +428,7 @@ Document collections ==================== Document classes that inherit **directly** from :class:`~mongoengine.Document` will have their own **collection** in the database. The name of the collection -is by default the name of the class, coverted to lowercase (so in the example +is by default the name of the class, converted to lowercase (so in the example above, the collection would be called `page`). If you need to change the name of the collection (e.g. to use MongoEngine with an existing database), then create a class dictionary attribute called :attr:`meta` on your document, and @@ -664,11 +664,11 @@ Shard keys ========== If your collection is sharded, then you need to specify the shard key as a tuple, -using the :attr:`shard_key` attribute of :attr:`-mongoengine.Document.meta`. +using the :attr:`shard_key` attribute of :attr:`~mongoengine.Document.meta`. This ensures that the shard key is sent with the query when calling the :meth:`~mongoengine.document.Document.save` or :meth:`~mongoengine.document.Document.update` method on an existing -:class:`-mongoengine.Document` instance:: +:class:`~mongoengine.Document` instance:: class LogEntry(Document): machine = StringField() diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index bb8176fec..a3496f54b 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -17,7 +17,7 @@ fetch documents from the database:: As of MongoEngine 0.8 the querysets utilise a local cache. So iterating it multiple times will only cause a single query. If this is not the - desired behavour you can call :class:`~mongoengine.QuerySet.no_cache` + desired behaviour you can call :class:`~mongoengine.QuerySet.no_cache` (version **0.8.3+**) to return a non-caching queryset. Filtering queries @@ -252,7 +252,7 @@ To retrieve a result that should be unique in the collection, use no document matches the query, and :class:`~mongoengine.queryset.MultipleObjectsReturned` if more than one document matched the query. These exceptions are merged into -your document defintions eg: `MyDoc.DoesNotExist` +your document definitions eg: `MyDoc.DoesNotExist` A variation of this method exists, :meth:`~mongoengine.queryset.Queryset.get_or_create`, that will create a new @@ -658,3 +658,4 @@ following example shows how the substitutions are made:: return comments; } """) + diff --git a/docs/upgrade.rst b/docs/upgrade.rst index b55fe3122..25f67c7f2 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -5,7 +5,7 @@ Upgrading 0.8.7 ***** -Calling reload on deleted / nonexistant documents now raises a DoesNotExist +Calling reload on deleted / nonexistent documents now raises a DoesNotExist exception. @@ -263,7 +263,7 @@ update your code like so: :: [m for m in mammals] # This will return all carnivores Len iterates the queryset --------------------------- +------------------------- If you ever did `len(queryset)` it previously did a `count()` under the covers, this caused some unusual issues. As `len(queryset)` is most often used by diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 854ed6b42..17a20853a 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -692,7 +692,7 @@ def merge_index_specs(index_specs, indices): spec_fields = [v['fields'] for k, v in enumerate(index_specs)] - # Merge unqiue_indexes with existing specs + # Merge unique_indexes with existing specs for k, v in enumerate(indices): if v['fields'] in spec_fields: index_specs[spec_fields.index(v['fields'])].update(v) diff --git a/mongoengine/document.py b/mongoengine/document.py index 1891c1642..d9d2774c4 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -677,7 +677,7 @@ def list_indexes(cls, go_up=True, go_down=True): if cls._meta.get('abstract'): return [] - # get all the base classes, subclasses and sieblings + # get all the base classes, subclasses and siblings classes = [] def get_classes(cls): diff --git a/mongoengine/fields.py b/mongoengine/fields.py index d9aeb3f16..5ddcf5612 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -291,7 +291,7 @@ def __init__(self, min_value=None, max_value=None, force_string=False, :param max_value: Validation rule for the maximum acceptable value. :param force_string: Store as a string. :param precision: Number of decimal places to store. - :param rounding: The rounding rule from the python decimal libary: + :param rounding: The rounding rule from the python decimal library: - decimal.ROUND_CEILING (towards Infinity) - decimal.ROUND_DOWN (towards zero) @@ -375,11 +375,11 @@ class DateTimeField(BaseField): Uses the python-dateutil library if available alternatively use time.strptime to parse the dates. Note: python-dateutil's parser is fully featured and when - installed you can utilise it to convert varing types of date formats into valid + installed you can utilise it to convert varying types of date formats into valid python datetime objects. Note: Microseconds are rounded to the nearest millisecond. - Pre UTC microsecond support is effecively broken. + Pre UTC microsecond support is effectively broken. Use :class:`~mongoengine.fields.ComplexDateTimeField` if you need accurate microsecond support. """ @@ -638,7 +638,7 @@ 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. + """Convert a Python type to a MongoDB compatible type. """ if isinstance(value, basestring): @@ -1649,7 +1649,7 @@ def __init__(self, size=None, thumbnail_size=None, class SequenceField(BaseField): - """Provides a sequental counter see: + """Provides a sequential counter see: http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers .. note:: @@ -1750,7 +1750,7 @@ def __set__(self, instance, value): def prepare_query_value(self, op, value): """ - This method is overriden in order to convert the query value into to required + This method is overridden in order to convert the query value into to required type. We need to do this in order to be able to successfully compare query values passed as string, the base implementation returns the value as is. """ diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 8c880e973..e9e87cfc0 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -272,7 +272,7 @@ def get_or_create(self, write_concern=None, auto_save=True, .. 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. This is + don't accidentally duplicate data when using this method. This is now scheduled to be removed before 1.0 :param write_concern: optional extra keyword arguments used if we @@ -992,8 +992,8 @@ def from_json(self, json_data): def aggregate(self, *pipeline, **kwargs): """ - Perform a aggreggate function based in your queryset params - :param pipeline: list of agreggation commands, + Perform a aggregate function based in your queryset params + :param pipeline: list of aggregation commands, see: http://docs.mongodb.org/manual/core/aggregation-pipeline/ .. versionadded:: 0.9 diff --git a/mongoengine/queryset/visitor.py b/mongoengine/queryset/visitor.py index a39b05f0c..e5d2e6152 100644 --- a/mongoengine/queryset/visitor.py +++ b/mongoengine/queryset/visitor.py @@ -29,7 +29,7 @@ class DuplicateQueryConditionsError(InvalidQueryError): class SimplificationVisitor(QNodeVisitor): - """Simplifies query trees by combinging unnecessary 'and' connection nodes + """Simplifies query trees by combining unnecessary 'and' connection nodes into a single Q-object. """ From d65ce6fc2c957d5b297fea724ad35ab0b34a138b Mon Sep 17 00:00:00 2001 From: mrigal Date: Fri, 28 Nov 2014 13:54:33 +0100 Subject: [PATCH 0038/1519] fixed bug for queryset.distinct to work also on embedded documents, not just on lists of embedded documents --- mongoengine/queryset/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index e9e87cfc0..f41fed39c 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -763,8 +763,7 @@ def distinct(self, field): # We may need to cast to the correct type eg. # ListField(EmbeddedDocumentField) - doc_field = getattr( - self._document._fields.get(field), "field", None) + doc_field = getattr(self._document._fields.get(field), "field", self._document._fields.get(field)) instance = getattr(doc_field, "document_type", False) EmbeddedDocumentField = _import_class('EmbeddedDocumentField') GenericEmbeddedDocumentField = _import_class( From a03262fc011e86630d046e99993ba32397f314c2 Mon Sep 17 00:00:00 2001 From: mrigal Date: Fri, 28 Nov 2014 16:23:23 +0100 Subject: [PATCH 0039/1519] implemented ability to return instances and not simple dicts for distinct on subdocuments --- mongoengine/queryset/base.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index f41fed39c..0a4b161e9 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -761,13 +761,29 @@ def distinct(self, field): distinct = self._dereference(queryset._cursor.distinct(field), 1, name=field, instance=self._document) - # We may need to cast to the correct type eg. - # ListField(EmbeddedDocumentField) - doc_field = getattr(self._document._fields.get(field), "field", self._document._fields.get(field)) - instance = getattr(doc_field, "document_type", False) + doc_field = self._document._fields.get(field.split('.', 1)[0]) + instance = False + # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField) EmbeddedDocumentField = _import_class('EmbeddedDocumentField') - GenericEmbeddedDocumentField = _import_class( - 'GenericEmbeddedDocumentField') + ListField = _import_class('ListField') + GenericEmbeddedDocumentField = _import_class('GenericEmbeddedDocumentField') + if isinstance(doc_field, ListField): + doc_field = getattr(doc_field, "field", doc_field) + if isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)): + instance = getattr(doc_field, "document_type", False) + # handle distinct on subdocuments + if '.' in field: + for field_part in field.split('.')[1:]: + # if looping on embedded document, get the document type instance + if instance and isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)): + doc_field = instance + # now get the subdocument + doc_field = getattr(doc_field, field_part, doc_field) + # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField) + if isinstance(doc_field, ListField): + doc_field = getattr(doc_field, "field", doc_field) + if isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)): + instance = getattr(doc_field, "document_type", False) if instance and isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)): distinct = [instance(**doc) for doc in distinct] From 2954017836453a87137e570f2793bfdb037ecf3e Mon Sep 17 00:00:00 2001 From: Slam <3lnc.slam@gmail.com> Date: Sun, 30 Nov 2014 00:23:40 +0200 Subject: [PATCH 0040/1519] Fixes #811. Fixes reflinks --- mongoengine/document.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index d9d2774c4..8ecb54c1c 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -462,10 +462,11 @@ def switch_db(self, db_alias): user.switch_db('archive-db') user.save() - If you need to read from another database see - :class:`~mongoengine.context_managers.switch_db` + :param str db_alias: The database alias to use for saving the document - :param db_alias: The database alias to use for saving the document + .. seealso:: + Use :class:`~mongoengine.context_managers.switch_collection` + if you need to read from another collection """ with switch_db(self.__class__, db_alias) as cls: collection = cls._get_collection() @@ -488,11 +489,12 @@ def switch_collection(self, collection_name): user.switch_collection('old-users') user.save() - If you need to read from another database see - :class:`~mongoengine.context_managers.switch_db` - - :param collection_name: The database alias to use for saving the + :param str collection_name: The database alias to use for saving the document + + .. seealso:: + Use :class:`~mongoengine.context_managers.switch_db` + if you need to read from another database """ with switch_collection(self.__class__, collection_name) as cls: collection = cls._get_collection() From 74a89223c0c877da75b8813851de11ebaf63909e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Sat, 29 Nov 2014 23:09:26 -0200 Subject: [PATCH 0041/1519] replaced `text_score` attribute to `get_text_score` method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Wilson Júnior --- mongoengine/document.py | 7 ------- mongoengine/queryset/base.py | 23 ++++++++++++++++++----- tests/queryset/queryset.py | 11 +++++------ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index d9d2774c4..1f1f81ae2 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -143,13 +143,6 @@ def fset(self, value): return property(fget, fset) pk = pk() - @property - def text_score(self): - """ - Used for text searchs - """ - return self._data.get('text_score') - @classmethod def _get_db(cls): """Some Model using other db_alias""" diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index e9e87cfc0..7887ef3fb 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -6,6 +6,7 @@ import pprint import re import warnings +import types from bson import SON from bson.code import Code @@ -37,6 +38,8 @@ RE_TYPE = type(re.compile('')) +def get_text_score(doc): + return doc._data.get('_text_score') class BaseQuerySet(object): @@ -158,8 +161,14 @@ def __getitem__(self, key): if queryset._as_pymongo: return queryset._get_as_pymongo(queryset._cursor[key]) - return queryset._document._from_son(queryset._cursor[key], + doc = queryset._document._from_son(queryset._cursor[key], _auto_dereference=self._auto_dereference, only_fields=self.only_fields) + + if self._include_text_scores: + doc.get_text_score = types.MethodType(get_text_score, doc) + + return doc + raise AttributeError def __iter__(self): @@ -192,7 +201,7 @@ def filter(self, *q_objs, **query): """ return self.__call__(*q_objs, **query) - def search_text(self, text, language=None, include_text_scores=False): + def search_text(self, text, language=None, include_text_scores=True): """ Start a text search, using text indexes. Require: MongoDB server version 2.6+. @@ -202,7 +211,7 @@ def search_text(self, text, language=None, include_text_scores=False): If not specified, the search uses the default language of the index. For supported languages, see `Text Search Languages `. - :param include_text_scores: If True, automaticaly add a text_score attribute to Document. + :param include_text_scores: If True, automatically add a get_text_score method to Document. """ queryset = self.clone() @@ -1341,6 +1350,10 @@ def next(self): return self._get_as_pymongo(raw_doc) doc = self._document._from_son(raw_doc, _auto_dereference=self._auto_dereference, only_fields=self.only_fields) + + if self._include_text_scores: + doc.get_text_score = types.MethodType(get_text_score, doc) + if self._scalar: return self._get_scalar(doc) @@ -1380,7 +1393,7 @@ def _cursor_args(self): if 'fields' not in cursor_args: cursor_args['fields'] = {} - cursor_args['fields']['text_score'] = {'$meta': "textScore"} + cursor_args['fields']['_text_score'] = {'$meta': "textScore"} return cursor_args @@ -1597,7 +1610,7 @@ def _get_order_by(self, keys): if key == '$text_score': # automatically set to include text scores self._include_text_scores = True - key_list.append(('text_score', {'$meta': "textScore"})) + key_list.append(('_text_score', {'$meta': "textScore"})) continue direction = pymongo.ASCENDING diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 703728fe1..a743a9334 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2783,13 +2783,12 @@ class News(Document): self.assertTrue('dilma' in new.content) self.assertTrue('planejamento' in new.title) - query = News.objects.search_text( - "candidata", include_text_scores=True) + query = News.objects.search_text("candidata") self.assertTrue(query._include_text_scores) new = query.first() - self.assertTrue(isinstance(new.text_score, float)) + self.assertTrue(isinstance(new.get_text_score(), float)) # count query = News.objects.search_text('brasil').order_by('$text_score') @@ -2799,9 +2798,9 @@ class News(Document): self.assertEqual(query._query, {'$text': {'$search': 'brasil'}}) cursor_args = query._cursor_args self.assertEqual( - cursor_args['fields'], {'text_score': {'$meta': 'textScore'}}) + cursor_args['fields'], {'_text_score': {'$meta': 'textScore'}}) - text_scores = [i.text_score for i in query] + text_scores = [i.get_text_score() for i in query] self.assertEqual(len(text_scores), 3) self.assertTrue(text_scores[0] > text_scores[1]) @@ -2811,7 +2810,7 @@ class News(Document): # get item item = News.objects.search_text( 'brasil').order_by('$text_score').first() - self.assertEqual(item.text_score, max_text_score) + self.assertEqual(item.get_text_score(), max_text_score) @skip_older_mongodb def test_distinct_handles_references_to_alias(self): From 4cca9f17dfa4065d1ab673fa01083a87fbfcf593 Mon Sep 17 00:00:00 2001 From: Rik Date: Mon, 2 Sep 2013 18:11:50 +0200 Subject: [PATCH 0042/1519] Check if undefined fields are supplied on document If an undefined field is supplied to a document instance, a `FieldDoesNotExist` Exception will be raised. --- mongoengine/base/document.py | 11 ++++++++++- mongoengine/errors.py | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index df06f6aed..2def35f48 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -12,7 +12,7 @@ from mongoengine import signals from mongoengine.common import _import_class from mongoengine.errors import (ValidationError, InvalidDocumentError, - LookUpError) + LookUpError, FieldDoesNotExist) from mongoengine.python_support import PY3, txt_type from mongoengine.base.common import get_document, ALLOW_INHERITANCE @@ -61,6 +61,15 @@ def __init__(self, *args, **values): signals.pre_init.send(self.__class__, document=self, values=values) + # Check if there are undefined fields supplied, if so raise an + # Exception. + for var in values.keys(): + if var not in self._fields.keys(): + msg = ( + "The field '{}' does not exist on the document '{}'" + ).format(var, self._class_name) + raise FieldDoesNotExist(msg) + if self.STRICT and not self._dynamic: self._data = StrictDict.create(allowed_keys=self._fields_ordered)() else: diff --git a/mongoengine/errors.py b/mongoengine/errors.py index 4b6b562c2..990150da9 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -40,6 +40,10 @@ class NotUniqueError(OperationError): pass +class FieldDoesNotExist(Exception): + pass + + class ValidationError(AssertionError): """Validation exception. From 06064decd2cf0da0f3c2d44852ad606dd666a6a4 Mon Sep 17 00:00:00 2001 From: Rik Date: Tue, 3 Sep 2013 11:25:45 +0200 Subject: [PATCH 0043/1519] check for dynamic document, exclude id pk and _cls --- mongoengine/base/document.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 2def35f48..ae9bdc308 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -63,12 +63,13 @@ def __init__(self, *args, **values): # Check if there are undefined fields supplied, if so raise an # Exception. - for var in values.keys(): - if var not in self._fields.keys(): - msg = ( - "The field '{}' does not exist on the document '{}'" - ).format(var, self._class_name) - raise FieldDoesNotExist(msg) + if not self._dynamic: + for var in values.keys(): + if var not in self._fields.keys() + ['id', 'pk', '_cls']: + msg = ( + "The field '{}' does not exist on the document '{}'" + ).format(var, self._class_name) + raise FieldDoesNotExist(msg) if self.STRICT and not self._dynamic: self._data = StrictDict.create(allowed_keys=self._fields_ordered)() From 67bf6afc898147fab6059b4c6b49f05deaf22765 Mon Sep 17 00:00:00 2001 From: Rik Date: Fri, 27 Jun 2014 14:43:56 +0200 Subject: [PATCH 0044/1519] fixed tests that were using undefined model fields --- tests/fields/fields.py | 2 ++ tests/queryset/queryset.py | 7 +++++-- tests/test_dereference.py | 2 ++ tests/test_django.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index d7edea376..4f7fab02f 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2717,9 +2717,11 @@ def test_multiple_sequence_fields_on_docs(self): class Animal(Document): id = SequenceField(primary_key=True) + name = StringField() class Person(Document): id = SequenceField(primary_key=True) + name = StringField() self.db['mongoengine.counters'].drop() Animal.drop_collection() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a743a9334..e37ab8810 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1492,6 +1492,7 @@ def test_update(self): """Ensure that atomic updates work properly. """ class BlogPost(Document): + name = StringField() title = StringField() hits = IntField() tags = ListField(StringField()) @@ -2878,12 +2879,14 @@ class Book(Document): self.assertEqual(authors, [mark_twain, john_tolkien]) def test_distinct_ListField_ReferenceField(self): - class Foo(Document): - bar_lst = ListField(ReferenceField('Bar')) class Bar(Document): text = StringField() + class Foo(Document): + bar = ReferenceField('Bar') + bar_lst = ListField(ReferenceField('Bar')) + Bar.drop_collection() Foo.drop_collection() diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 85588558e..2115b45a5 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -947,6 +947,8 @@ def test_multidirectional_lists(self): class Asset(Document): name = StringField(max_length=250, required=True) + path = StringField() + title = StringField() parent = GenericReferenceField(default=None) parents = ListField(GenericReferenceField()) children = ListField(GenericReferenceField()) diff --git a/tests/test_django.py b/tests/test_django.py index c4d3d5829..0070087fb 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -176,7 +176,7 @@ def count(self, with_limit_and_skip=True): class Note(Document): meta = dict(queryset_class=LimitCountQuerySet) - text = StringField() + name = StringField() Note.drop_collection() From 913952ffe1ea79f90de1994262dac74e1406ae1d Mon Sep 17 00:00:00 2001 From: Rik Date: Mon, 30 Jun 2014 18:12:23 +0200 Subject: [PATCH 0045/1519] remove unittest test_no_overwritting_no_data_loss Now that fields need to be defined explicitly, it's not possible to have another property with the same name on a model. https://github.com/MongoEngine/mongoengine/pull/457#issuecomment-47513105 --- tests/document/instance.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 40c25f8df..3c6a9940f 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2443,30 +2443,6 @@ class Group(Document): group = Group.objects.first() self.assertEqual("hello - default", group.name) - def test_no_overwritting_no_data_loss(self): - - class User(Document): - username = StringField(primary_key=True) - name = StringField() - - @property - def foo(self): - return True - - User.drop_collection() - - user = User(username="Ross", foo="bar") - self.assertTrue(user.foo) - - User._get_collection().save({"_id": "Ross", "foo": "Bar", - "data": [1, 2, 3]}) - - user = User.objects.first() - self.assertEqual("Ross", user.username) - self.assertEqual(True, user.foo) - self.assertEqual("Bar", user._data["foo"]) - self.assertEqual([1, 2, 3], user._data["data"]) - def test_spaces_in_keys(self): class Embedded(DynamicEmbeddedDocument): From 4627af3e90b924cecd0db17357b3115f8143977f Mon Sep 17 00:00:00 2001 From: Rik Date: Mon, 30 Jun 2014 18:27:55 +0200 Subject: [PATCH 0046/1519] add FieldDoesNotExist exception to __all__ So it will be available when you do: from mongoengine import * --- mongoengine/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/errors.py b/mongoengine/errors.py index 990150da9..6cde7771e 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -5,7 +5,8 @@ __all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError', 'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError', - 'OperationError', 'NotUniqueError', 'ValidationError') + 'OperationError', 'NotUniqueError', 'FieldDoesNotExist', + 'ValidationError') class NotRegistered(Exception): From 6d48100f44ae5dd5bab81a4a2a1e9002089767ad Mon Sep 17 00:00:00 2001 From: Rik Date: Mon, 30 Jun 2014 18:29:02 +0200 Subject: [PATCH 0047/1519] add test if FieldDoesNotExist is raised When trying to set an undefined field. --- tests/fields/fields.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 4f7fab02f..ae4575ad6 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3068,6 +3068,16 @@ class Doc(Document): except Exception: self.fail() + def test_undefined_field_exception(self): + """Tests if a `FieldDoesNotExist` exception is raised when trying to + set a value to a field that's not defined. + """ + + class Doc(Document): + foo = StringField(db_field='f') + + with self.assertRaises(FieldDoesNotExist): + Doc(bar='test') if __name__ == '__main__': unittest.main() From 1cac35be0376f0842f169661a63b2cb9956f5898 Mon Sep 17 00:00:00 2001 From: Rik Date: Tue, 1 Jul 2014 14:21:29 +0200 Subject: [PATCH 0048/1519] using python 2.6 compatible way of assertRaises --- tests/fields/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index ae4575ad6..75d698c7e 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3076,8 +3076,10 @@ def test_undefined_field_exception(self): class Doc(Document): foo = StringField(db_field='f') - with self.assertRaises(FieldDoesNotExist): + def test(): Doc(bar='test') + self.assertRaises(FieldDoesNotExist, test) + if __name__ == '__main__': unittest.main() From 191a4e569e15f43f3d09a335684a5522de3e3784 Mon Sep 17 00:00:00 2001 From: Rik Date: Tue, 1 Jul 2014 15:18:01 +0200 Subject: [PATCH 0049/1519] added ints in string.format() for 2.6 compability --- mongoengine/base/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index ae9bdc308..bc4840f19 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -67,7 +67,7 @@ def __init__(self, *args, **values): for var in values.keys(): if var not in self._fields.keys() + ['id', 'pk', '_cls']: msg = ( - "The field '{}' does not exist on the document '{}'" + "The field '{0}' does not exist on the document '{1}'" ).format(var, self._class_name) raise FieldDoesNotExist(msg) From 79705fbf11a59d6e18749eee806c9a5423858709 Mon Sep 17 00:00:00 2001 From: Rik Date: Thu, 10 Jul 2014 11:48:25 +0200 Subject: [PATCH 0050/1519] moved initialization of _created before FieldDoesNotExist check Because otherwise we'll get a FieldDoesNotExist error on the field _created. --- mongoengine/base/document.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index bc4840f19..5063dee5a 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -54,11 +54,14 @@ def __init__(self, *args, **values): raise TypeError( "Multiple values for keyword argument '" + name + "'") values[name] = value + __auto_convert = values.pop("__auto_convert", True) # 399: set default values only to fields loaded from DB __only_fields = set(values.pop("__only_fields", values)) + _created = values.pop("_created", True) + signals.pre_init.send(self.__class__, document=self, values=values) # Check if there are undefined fields supplied, if so raise an @@ -77,7 +80,6 @@ def __init__(self, *args, **values): self._data = SemiStrictDict.create( allowed_keys=self._fields_ordered)() - _created = values.pop("_created", True) self._data = {} self._dynamic_fields = SON() From 80f80cd31f0aff842169bbcf5a07547111937c0f Mon Sep 17 00:00:00 2001 From: Rik Date: Tue, 25 Nov 2014 19:29:12 +0100 Subject: [PATCH 0051/1519] fixed more tests that were using undefined model fields --- tests/fields/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 75d698c7e..a95001b46 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1814,7 +1814,7 @@ class Ocorrence(Document): Animal.drop_collection() Ocorrence.drop_collection() - a = Animal(nam="Leopard", tag="heavy", + a = Animal(name="Leopard", tag="heavy", owner=Owner(tp='u', name="Wilson Júnior") ) a.save() @@ -1864,7 +1864,7 @@ class Ocorrence(Document): Animal.drop_collection() Ocorrence.drop_collection() - a = Animal(nam="Leopard", tag="heavy", + a = Animal(name="Leopard", tag="heavy", owner=Owner(tags=['cool', 'funny'], name="Wilson Júnior") ) From 2b3bb81fae128bafa7b860088bd8c20bf53de942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Sat, 29 Nov 2014 23:48:58 -0200 Subject: [PATCH 0052/1519] Refactoring: `Simple is better than complex` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Wilson Júnior --- mongoengine/base/document.py | 12 +++++++++++- mongoengine/queryset/base.py | 31 +++++++------------------------ tests/queryset/queryset.py | 5 ++--- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 5063dee5a..998e366fe 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -68,7 +68,7 @@ def __init__(self, *args, **values): # Exception. if not self._dynamic: for var in values.keys(): - if var not in self._fields.keys() + ['id', 'pk', '_cls']: + if var not in self._fields.keys() + ['id', 'pk', '_cls', '_text_score']: msg = ( "The field '{0}' does not exist on the document '{1}'" ).format(var, self._class_name) @@ -281,6 +281,16 @@ def clean(self): """ pass + def get_text_score(self): + """ + Get text score from text query + """ + + if '_text_score' not in self._data: + raise InvalidDocumentError('This document is not originally built from a text query') + + return self._data['_text_score'] + def to_mongo(self, use_db_field=True, fields=[]): """ Return as SON data ready for use with MongoDB. diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7887ef3fb..39ffa88fe 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -6,7 +6,6 @@ import pprint import re import warnings -import types from bson import SON from bson.code import Code @@ -38,8 +37,6 @@ RE_TYPE = type(re.compile('')) -def get_text_score(doc): - return doc._data.get('_text_score') class BaseQuerySet(object): @@ -69,7 +66,6 @@ def __init__(self, document, collection): self._as_pymongo = False self._as_pymongo_coerce = False self._search_text = None - self._include_text_scores = False # If inheritance is allowed, only return instances and instances of # subclasses of the class being used @@ -161,13 +157,8 @@ def __getitem__(self, key): if queryset._as_pymongo: return queryset._get_as_pymongo(queryset._cursor[key]) - doc = queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference, only_fields=self.only_fields) - - if self._include_text_scores: - doc.get_text_score = types.MethodType(get_text_score, doc) - - return doc + return queryset._document._from_son(queryset._cursor[key], + _auto_dereference=self._auto_dereference, only_fields=self.only_fields) raise AttributeError @@ -201,7 +192,7 @@ def filter(self, *q_objs, **query): """ return self.__call__(*q_objs, **query) - def search_text(self, text, language=None, include_text_scores=True): + def search_text(self, text, language=None): """ Start a text search, using text indexes. Require: MongoDB server version 2.6+. @@ -210,14 +201,11 @@ def search_text(self, text, language=None, include_text_scores=True): for the search and the rules for the stemmer and tokenizer. If not specified, the search uses the default language of the index. For supported languages, see `Text Search Languages `. - - :param include_text_scores: If True, automatically add a get_text_score method to Document. - """ queryset = self.clone() if queryset._search_text: raise OperationError( - "Is not possible to use search_text two times.") + "It is not possible to use search_text two times.") query_kwargs = SON({'$search': text}) if language: @@ -227,7 +215,6 @@ def search_text(self, text, language=None, include_text_scores=True): queryset._mongo_query = None queryset._cursor_obj = None queryset._search_text = text - queryset._include_text_scores = include_text_scores return queryset @@ -684,7 +671,7 @@ def clone_into(self, cls): '_timeout', '_class_check', '_slave_okay', '_read_preference', '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', '_limit', '_skip', '_hint', '_auto_dereference', - '_search_text', '_include_text_scores', 'only_fields', '_max_time_ms') + '_search_text', 'only_fields', '_max_time_ms') for prop in copy_props: val = getattr(self, prop) @@ -1351,9 +1338,6 @@ def next(self): doc = self._document._from_son(raw_doc, _auto_dereference=self._auto_dereference, only_fields=self.only_fields) - if self._include_text_scores: - doc.get_text_score = types.MethodType(get_text_score, doc) - if self._scalar: return self._get_scalar(doc) @@ -1362,6 +1346,7 @@ def next(self): def rewind(self): """Rewind the cursor to its unevaluated state. + .. versionadded:: 0.3 """ self._iter = False @@ -1389,7 +1374,7 @@ def _cursor_args(self): if self._loaded_fields: cursor_args['fields'] = self._loaded_fields.as_dict() - if self._include_text_scores: + if self._search_text: if 'fields' not in cursor_args: cursor_args['fields'] = {} @@ -1608,8 +1593,6 @@ def _get_order_by(self, keys): continue if key == '$text_score': - # automatically set to include text scores - self._include_text_scores = True key_list.append(('_text_score', {'$meta': "textScore"})) continue diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index e37ab8810..c91600d03 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2785,15 +2785,14 @@ class News(Document): self.assertTrue('planejamento' in new.title) query = News.objects.search_text("candidata") - - self.assertTrue(query._include_text_scores) + self.assertEqual(query._search_text, "candidata") new = query.first() self.assertTrue(isinstance(new.get_text_score(), float)) # count query = News.objects.search_text('brasil').order_by('$text_score') - self.assertTrue(query._include_text_scores) + self.assertEqual(query._search_text, "brasil") self.assertEqual(query.count(), 3) self.assertEqual(query._query, {'$text': {'$search': 'brasil'}}) From 531fa30b691bebbd98a6ab9884e0afbbbb6cd5b8 Mon Sep 17 00:00:00 2001 From: mrigal Date: Mon, 1 Dec 2014 18:20:29 +0100 Subject: [PATCH 0053/1519] added test for capacity to get distinct on subdocument of a list --- tests/queryset/queryset.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 703728fe1..92b820cfd 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2878,6 +2878,36 @@ class Book(Document): self.assertEqual(authors, [mark_twain, john_tolkien]) + def test_distinct_ListField_EmbeddedDocumentField_EmbeddedDocumentField(self): + + class Country(EmbeddedDocument): + country_name = StringField() + + class Author(EmbeddedDocument): + name = StringField() + country = EmbeddedDocumentField(Country) + + class Book(Document): + title = StringField() + authors = ListField(EmbeddedDocumentField(Author)) + + Book.drop_collection() + + scotland = Country(country_name="Scotland") + tibet = Country(country_name="Tibet") + + mark_twain = Author(name="Mark Twain", country=scotland) + john_tolkien = Author(name="John Ronald Reuel Tolkien", country=tibet) + + book = Book(title="Tom Sawyer", authors=[mark_twain]).save() + book = Book( + title="The Lord of the Rings", authors=[john_tolkien]).save() + book = Book( + title="The Stories", authors=[mark_twain, john_tolkien]).save() + country_list = Book.objects.distinct("authors.country") + + self.assertEqual(country_list, [scotland, tibet]) + def test_distinct_ListField_ReferenceField(self): class Foo(Document): bar_lst = ListField(ReferenceField('Bar')) From 51f314e90726560bf2a8a081625902a4096cb3da Mon Sep 17 00:00:00 2001 From: Slam <3lnc.slam@gmail.com> Date: Fri, 28 Nov 2014 13:10:38 +0200 Subject: [PATCH 0054/1519] Doc fixes, thanks @3Inc Author: Slam <3lnc.slam@gmail.com> Date: Fri Nov 28 13:10:38 2014 +0200 --- docs/guide/connecting.rst | 70 +++++++++++++++++++------------ docs/guide/defining-documents.rst | 24 +++++++---- docs/guide/document-instances.rst | 15 +++---- docs/guide/querying.rst | 45 +++++++++++--------- docs/guide/signals.rst | 10 ++--- docs/index.rst | 2 +- mongoengine/base/fields.py | 5 ++- mongoengine/document.py | 3 +- mongoengine/fields.py | 6 +++ mongoengine/queryset/base.py | 3 +- 10 files changed, 112 insertions(+), 71 deletions(-) diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index 0e61df96c..48926499e 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -23,21 +23,32 @@ arguments should be provided:: connect('project1', username='webapp', password='pwd123') -URI style connections are also supported - just supply the uri as +URI style connections are also supported -- just supply the URI as the :attr:`host` to :func:`~mongoengine.connect`:: connect('project1', host='mongodb://localhost/database_name') -Note that database name from uri has priority over name -in :func:`~mongoengine.connect` +.. note:: Database, username and password from URI string overrides + corresponding parameters in :func:`~mongoengine.connect`: :: + + connect( + name='test', + username='user', + password='12345', + host='mongodb://admin:qwerty@localhost/production' + ) + + will establish connection to ``production`` database using + ``admin`` username and ``qwerty`` password. ReplicaSets =========== -MongoEngine supports :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. -To use them, please use a URI style connection and provide the `replicaSet` name in the -connection kwargs. +MongoEngine supports +:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them, +please use an URI style connection and provide the ``replicaSet`` name +in the connection kwargs. Read preferences are supported through the connection or via individual queries by passing the read_preference :: @@ -77,36 +88,38 @@ to point across databases and collections. Below is an example schema, using meta = {"db_alias": "users-books-db"} -Switch Database Context Manager -=============================== - -Sometimes you may want to switch the database to query against for a class -for example, archiving older data into a separate database for performance -reasons. +Context Managers +================ +Sometimes you may want to switch the database or collection to query against +for a class. +For example, archiving older data into a separate database for performance +reasons or writing functions that dynamically choose collections to write +document to. +Switch Database +--------------- The :class:`~mongoengine.context_managers.switch_db` context manager allows you to change the database alias for a given class allowing quick and easy -access to the same User document across databases:: +access the same User document across databases:: - from mongoengine.context_managers import switch_db + from mongoengine.context_managers import switch_db - class User(Document): - name = StringField() + class User(Document): + name = StringField() - meta = {"db_alias": "user-db"} + meta = {"db_alias": "user-db"} - with switch_db(User, 'archive-user-db') as User: - User(name="Ross").save() # Saves the 'archive-user-db' + with switch_db(User, 'archive-user-db') as User: + User(name="Ross").save() # Saves the 'archive-user-db' -.. note:: Make sure any aliases have been registered with - :func:`~mongoengine.register_connection` before using the context manager. -There is also a switch collection context manager as well. The -:class:`~mongoengine.context_managers.switch_collection` context manager allows -you to change the collection for a given class allowing quick and easy -access to the same Group document across collection:: +Switch Collection +----------------- +The :class:`~mongoengine.context_managers.switch_collection` context manager +allows you to change the collection for a given class allowing quick and easy +access the same Group document across collection:: - from mongoengine.context_managers import switch_db + from mongoengine.context_managers import switch_collection class Group(Document): name = StringField() @@ -116,3 +129,8 @@ access to the same Group document across collection:: with switch_collection(Group, 'group2000') as Group: Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection + + +.. note:: Make sure any aliases have been registered with + :func:`~mongoengine.register_connection` or :func:`~mongoengine.connect` + before using the context manager. diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 315fcde83..5c45c19a2 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -4,7 +4,7 @@ Defining documents In MongoDB, a **document** is roughly equivalent to a **row** in an RDBMS. When working with relational databases, rows are stored in **tables**, which have a strict **schema** that the rows follow. MongoDB stores documents in -**collections** rather than tables - the principal difference is that no schema +**collections** rather than tables --- the principal difference is that no schema is enforced at a database level. Defining a document's schema @@ -171,15 +171,15 @@ arguments can be set on all fields: size = StringField(max_length=3, choices=SIZE) :attr:`help_text` (Default: None) - Optional help text to output with the field - used by form libraries + Optional help text to output with the field -- used by form libraries :attr:`verbose_name` (Default: None) - Optional human-readable name for the field - used by form libraries + Optional human-readable name for the field -- used by form libraries List fields ----------- -MongoDB allows the storage of lists of items. To add a list of items to a +MongoDB allows storing lists of items. To add a list of items to a :class:`~mongoengine.Document`, use the :class:`~mongoengine.fields.ListField` field type. :class:`~mongoengine.fields.ListField` takes another field object as its first argument, which specifies which type elements may be stored within the list:: @@ -471,8 +471,16 @@ Text indexes may be specified by prefixing the field name with a **$**. :: class Page(Document): title = StringField() rating = StringField() + created = DateTimeField() meta = { - 'indexes': ['title', ('title', '-rating')] + 'indexes': [ + 'title', + ('title', '-rating'), + { + 'fields': ['created'], + 'expireAfterSeconds': 3600 + } + ] } If a dictionary is passed then the following options are available: @@ -690,7 +698,7 @@ defined, you may subclass it and add any extra fields or methods you may need. As this is new class is not a direct subclass of :class:`~mongoengine.Document`, it will not be stored in its own collection; it will use the same collection as its superclass uses. This allows for more -convenient and efficient retrieval of related documents - all you need do is +convenient and efficient retrieval of related documents -- all you need do is set :attr:`allow_inheritance` to True in the :attr:`meta` data for a document.:: @@ -709,7 +717,7 @@ document.:: Working with existing data -------------------------- -As MongoEngine no longer defaults to needing :attr:`_cls` you can quickly and +As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and easily get working with existing data. Just define the document to match the expected schema in your database :: @@ -732,7 +740,7 @@ Abstract classes If you want to add some extra functionality to a group of Document classes but you don't need or want the overhead of inheritance you can use the -:attr:`abstract` attribute of :attr:`-mongoengine.Document.meta`. +:attr:`abstract` attribute of :attr:`~mongoengine.Document.meta`. This won't turn on :ref:`document-inheritance` but will allow you to keep your code DRY:: diff --git a/docs/guide/document-instances.rst b/docs/guide/document-instances.rst index f9a6610ff..0e9fcef62 100644 --- a/docs/guide/document-instances.rst +++ b/docs/guide/document-instances.rst @@ -2,7 +2,7 @@ Documents instances =================== To create a new document object, create an instance of the relevant document -class, providing values for its fields as its constructor keyword arguments. +class, providing values for its fields as constructor keyword arguments. You may provide values for any of the fields on the document:: >>> page = Page(title="Test Page") @@ -32,11 +32,11 @@ already exist, then any changes will be updated atomically. For example:: Changes to documents are tracked and on the whole perform ``set`` operations. - * ``list_field.push(0)`` - *sets* the resulting list - * ``del(list_field)`` - *unsets* whole list + * ``list_field.push(0)`` --- *sets* the resulting list + * ``del(list_field)`` --- *unsets* whole list With lists its preferable to use ``Doc.update(push__list_field=0)`` as - this stops the whole list being updated - stopping any race conditions. + this stops the whole list being updated --- stopping any race conditions. .. seealso:: :ref:`guide-atomic-updates` @@ -74,7 +74,7 @@ Cascading Saves If your document contains :class:`~mongoengine.fields.ReferenceField` or :class:`~mongoengine.fields.GenericReferenceField` objects, then by default the :meth:`~mongoengine.Document.save` method will not save any changes to -those objects. If you want all references to also be saved also, noting each +those objects. If you want all references to be saved also, noting each save is a separate query, then passing :attr:`cascade` as True to the save method will cascade any saves. @@ -113,12 +113,13 @@ you may still use :attr:`id` to access the primary key if you want:: >>> bob.id == bob.email == 'bob@example.com' True -You can also access the document's "primary key" using the :attr:`pk` field; in -is an alias to :attr:`id`:: +You can also access the document's "primary key" using the :attr:`pk` field, +it's an alias to :attr:`id`:: >>> page = Page(title="Another Test Page") >>> page.save() >>> page.id == page.pk + True .. note:: diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index a3496f54b..9d1dfd760 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -42,7 +42,7 @@ syntax:: Query operators =============== -Operators other than equality may also be used in queries; just attach the +Operators other than equality may also be used in queries --- just attach the operator name to a key with a double-underscore:: # Only find users whose age is 18 or less @@ -84,19 +84,20 @@ expressions: Geo queries ----------- -There are a few special operators for performing geographical queries. The following -were added in 0.8 for: :class:`~mongoengine.fields.PointField`, +There are a few special operators for performing geographical queries. +The following were added in MongoEngine 0.8 for +:class:`~mongoengine.fields.PointField`, :class:`~mongoengine.fields.LineStringField` and :class:`~mongoengine.fields.PolygonField`: -* ``geo_within`` -- Check if a geometry is within a polygon. For ease of use - it accepts either a geojson geometry or just the polygon coordinates eg:: +* ``geo_within`` -- check if a geometry is within a polygon. For ease of use + it accepts either a geojson geometry or just the polygon coordinates eg:: loc.objects(point__geo_within=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) loc.objects(point__geo_within={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) -* ``geo_within_box`` - simplified geo_within searching with a box eg:: +* ``geo_within_box`` -- simplified geo_within searching with a box eg:: loc.objects(point__geo_within_box=[(-125.0, 35.0), (-100.0, 40.0)]) loc.objects(point__geo_within_box=[, ]) @@ -132,23 +133,21 @@ were added in 0.8 for: :class:`~mongoengine.fields.PointField`, loc.objects(poly__geo_intersects={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}) -* ``near`` -- Find all the locations near a given point:: +* ``near`` -- find all the locations near a given point:: loc.objects(point__near=[40, 5]) loc.objects(point__near={"type": "Point", "coordinates": [40, 5]}) - - You can also set the maximum distance in meters as well:: + You can also set the maximum distance in meters as well:: loc.objects(point__near=[40, 5], point__max_distance=1000) - The older 2D indexes are still supported with the :class:`~mongoengine.fields.GeoPointField`: * ``within_distance`` -- provide a list containing a point and a maximum distance (e.g. [(41.342, -87.653), 5]) -* ``within_spherical_distance`` -- Same as above but using the spherical geo model +* ``within_spherical_distance`` -- same as above but using the spherical geo model (e.g. [(41.342, -87.653), 5/earth_radius]) * ``near`` -- order the documents by how close they are to a given point * ``near_sphere`` -- Same as above but using the spherical geo model @@ -198,12 +197,14 @@ However, this doesn't map well to the syntax so you can also use a capital S ins Post.objects(comments__by="joe").update(inc__comments__S__votes=1) - .. note:: Due to Mongo currently the $ operator only applies to the first matched item in the query. +.. note:: + Due to :program:`Mongo`, currently the $ operator only applies to the + first matched item in the query. Raw queries ----------- -It is possible to provide a raw PyMongo query as a query parameter, which will +It is possible to provide a raw :mod:`PyMongo` query as a query parameter, which will be integrated directly into the query. This is done using the ``__raw__`` keyword argument:: @@ -213,12 +214,12 @@ keyword argument:: Limiting and skipping results ============================= -Just as with traditional ORMs, you may limit the number of results returned, or +Just as with traditional ORMs, you may limit the number of results returned or skip a number or results in you query. :meth:`~mongoengine.queryset.QuerySet.limit` and :meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on -:class:`~mongoengine.queryset.QuerySet` objects, but the prefered syntax for -achieving this is using array-slicing syntax:: +:class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax +is preferred for achieving this:: # Only the first 5 people users = User.objects[:5] @@ -255,7 +256,7 @@ if more than one document matched the query. These exceptions are merged into your document definitions eg: `MyDoc.DoesNotExist` A variation of this method exists, -:meth:`~mongoengine.queryset.Queryset.get_or_create`, that will create a new +:meth:`~mongoengine.queryset.QuerySet.get_or_create`, that will create a new document with the query arguments if no documents match the query. An additional keyword argument, :attr:`defaults` may be provided, which will be used as default values for the new document, in the case that it should need @@ -266,9 +267,13 @@ to be created:: >>> a.name == b.name and a.age == b.age True +.. warning:: + :meth:`~mongoengine.queryset.QuerySet.get_or_create` method is deprecated + since :mod:`mongoengine` 0.8. + Default Document queries ======================== -By default, the objects :attr:`~mongoengine.Document.objects` attribute on a +By default, the objects :attr:`~Document.objects` attribute on a document returns a :class:`~mongoengine.queryset.QuerySet` that doesn't filter the collection -- it returns all objects. This may be changed by defining a method on a document that modifies a queryset. The method should accept two @@ -311,7 +316,7 @@ Should you want to add custom methods for interacting with or filtering documents, extending the :class:`~mongoengine.queryset.QuerySet` class may be the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on a document, set ``queryset_class`` to the custom class in a -:class:`~mongoengine.Document`\ s ``meta`` dictionary:: +:class:`~mongoengine.Document`'s ``meta`` dictionary:: class AwesomerQuerySet(QuerySet): @@ -498,7 +503,7 @@ Documents may be updated atomically by using the There are several different "modifiers" that you may use with these methods: * ``set`` -- set a particular value -* ``unset`` -- delete a particular value (since MongoDB v1.3+) +* ``unset`` -- delete a particular value (since MongoDB v1.3) * ``inc`` -- increment a value by a given amount * ``dec`` -- decrement a value by a given amount * ``push`` -- append a value to a list diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index fc93e4cae..797a48699 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -35,25 +35,25 @@ Available signals include: :class:`~mongoengine.EmbeddedDocument` instance has been completed. `pre_save` - Called within :meth:`~mongoengine.document.Document.save` prior to performing + Called within :meth:`~mongoengine.Document.save` prior to performing any actions. `pre_save_post_validation` - Called within :meth:`~mongoengine.document.Document.save` after validation + Called within :meth:`~mongoengine.Document.save` after validation has taken place but before saving. `post_save` - Called within :meth:`~mongoengine.document.Document.save` after all actions + Called within :meth:`~mongoengine.Document.save` after all actions (validation, insert/update, cascades, clearing dirty flags) have completed successfully. Passed the additional boolean keyword argument `created` to indicate if the save was an insert or an update. `pre_delete` - Called within :meth:`~mongoengine.document.Document.delete` prior to + Called within :meth:`~mongoengine.Document.delete` prior to attempting the delete operation. `post_delete` - Called within :meth:`~mongoengine.document.Document.delete` upon successful + Called within :meth:`~mongoengine.Document.delete` upon successful deletion of the record. `pre_bulk_insert` diff --git a/docs/index.rst b/docs/index.rst index 77f965cae..2102df026 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ MongoDB. To install it, simply run MongoEngine. :doc:`guide/index` - The Full guide to MongoEngine - from modeling documents to storing files, + The Full guide to MongoEngine --- from modeling documents to storing files, from querying for data to firing signals and *everything* between. :doc:`apireference` diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index f86d4175d..6fad5d632 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -428,6 +428,7 @@ def validate(self, value): class GeoJsonBaseField(BaseField): """A geo json field storing a geojson style object. + .. versionadded:: 0.8 """ @@ -436,8 +437,8 @@ class GeoJsonBaseField(BaseField): def __init__(self, auto_index=True, *args, **kwargs): """ - :param auto_index: Automatically create a "2dsphere" index. Defaults - to `True`. + :param bool auto_index: Automatically create a "2dsphere" index.\ + Defaults to `True`. """ self._name = "%sField" % self._type if not auto_index: diff --git a/mongoengine/document.py b/mongoengine/document.py index 1f1f81ae2..eecdb57fa 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -193,7 +193,7 @@ def modify(self, query={}, **update): in the database doesn't match the query. .. note:: All unsaved changes that has been made to the document are - rejected if the method returns True. + rejected if the method returns True. :param query: the update will be performed only if the document in the database matches the query @@ -250,6 +250,7 @@ def save(self, force_insert=False, validate=True, clean=True, :param _refs: A list of processed references used in cascading saves :param save_condition: only perform save if matching record in db satisfies condition(s) (e.g., version number) + .. versionchanged:: 0.5 In existing documents it only saves changed fields using set / unset. Saves are cascaded and any diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 5ddcf5612..7af647417 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1860,6 +1860,7 @@ class PointField(GeoJsonBaseField): to set the value. Requires mongodb >= 2.4 + .. versionadded:: 0.8 """ _type = "Point" @@ -1879,6 +1880,7 @@ class LineStringField(GeoJsonBaseField): You can either pass a dict with the full information or a list of points. Requires mongodb >= 2.4 + .. versionadded:: 0.8 """ _type = "LineString" @@ -1901,6 +1903,7 @@ class PolygonField(GeoJsonBaseField): holes. Requires mongodb >= 2.4 + .. versionadded:: 0.8 """ _type = "Polygon" @@ -1921,6 +1924,7 @@ class MultiPointField(GeoJsonBaseField): to set the value. Requires mongodb >= 2.6 + .. versionadded:: 0.9 """ _type = "MultiPoint" @@ -1941,6 +1945,7 @@ class MultiLineStringField(GeoJsonBaseField): You can either pass a dict with the full information or a list of points. Requires mongodb >= 2.6 + .. versionadded:: 0.9 """ _type = "MultiLineString" @@ -1968,6 +1973,7 @@ class MultiPolygonField(GeoJsonBaseField): of Polygons. Requires mongodb >= 2.6 + .. versionadded:: 0.9 """ _type = "MultiPolygon" diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 39ffa88fe..3932465e5 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -402,6 +402,7 @@ def delete(self, write_concern=None, _from_doc_delete=False): will force an fsync on the primary server. :param _from_doc_delete: True when called from document delete therefore signals will have been triggered so don't loop. + :returns number of deleted documents """ queryset = self.clone() @@ -989,7 +990,7 @@ def from_json(self, json_data): def aggregate(self, *pipeline, **kwargs): """ Perform a aggregate function based in your queryset params - :param pipeline: list of aggregation commands, + :param pipeline: list of aggregation commands,\ see: http://docs.mongodb.org/manual/core/aggregation-pipeline/ .. versionadded:: 0.9 From e5a636a159b75a5672b90c021a1a2aa8c6a56c69 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 3 Dec 2014 11:09:05 +0200 Subject: [PATCH 0055/1519] Added a test that verifies distinct operations on nested embedded documents. --- tests/queryset/queryset.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 92b820cfd..a21c561d2 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2879,9 +2879,12 @@ class Book(Document): self.assertEqual(authors, [mark_twain, john_tolkien]) def test_distinct_ListField_EmbeddedDocumentField_EmbeddedDocumentField(self): - + class Continent(EmbeddedDocument): + continent_name = StringField() + class Country(EmbeddedDocument): country_name = StringField() + continent = EmbeddedDocumentField(Continent) class Author(EmbeddedDocument): name = StringField() @@ -2893,8 +2896,11 @@ class Book(Document): Book.drop_collection() - scotland = Country(country_name="Scotland") - tibet = Country(country_name="Tibet") + europe = Continent(continent_name='europe') + asia = Continent(continent_name='asia') + + scotland = Country(country_name="Scotland", continent=europe) + tibet = Country(country_name="Tibet", continent=asia) mark_twain = Author(name="Mark Twain", country=scotland) john_tolkien = Author(name="John Ronald Reuel Tolkien", country=tibet) @@ -2907,6 +2913,10 @@ class Book(Document): country_list = Book.objects.distinct("authors.country") self.assertEqual(country_list, [scotland, tibet]) + + continent_list = Book.objects.distinct("authors.country.continent") + + self.assertEqual(continent_list, [asia, europe]) def test_distinct_ListField_ReferenceField(self): class Foo(Document): From 8aaa5951cab68b0c88ec04b892de014ffe2a6930 Mon Sep 17 00:00:00 2001 From: mrigal Date: Wed, 3 Dec 2014 13:13:07 +0100 Subject: [PATCH 0056/1519] fixed order of list for the test to pass --- tests/queryset/queryset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a21c561d2..6670029d9 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2916,7 +2916,7 @@ class Book(Document): continent_list = Book.objects.distinct("authors.country.continent") - self.assertEqual(continent_list, [asia, europe]) + self.assertEqual(continent_list, [europe, asia]) def test_distinct_ListField_ReferenceField(self): class Foo(Document): From 7bb65fca4ecbfdd616e4a232b563d386a04efde9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Fran=C3=A7ais?= Date: Thu, 4 Dec 2014 11:34:23 +0100 Subject: [PATCH 0057/1519] Fix crash when applying deletion rules When deleting a document references by other, if that refence is defined on an abstract document, the operation fails, because it tries to apply deletion on the abstract class which doesn't have a QuerySet. Fix is simply to ignore document classes which are defined abstract when applying rules on all classes referencing the document. --- mongoengine/queryset/base.py | 4 ++ tests/queryset/queryset.py | 104 +++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 3932465e5..a49f9968a 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -432,6 +432,8 @@ def delete(self, write_concern=None, _from_doc_delete=False): # references for rule_entry in delete_rules: document_cls, field_name = rule_entry + if document_cls._meta.get('abstract'): + continue rule = doc._meta['delete_rules'][rule_entry] if rule == DENY and document_cls.objects( **{field_name + '__in': self}).count() > 0: @@ -441,6 +443,8 @@ def delete(self, write_concern=None, _from_doc_delete=False): for rule_entry in delete_rules: document_cls, field_name = rule_entry + if document_cls._meta.get('abstract'): + continue rule = doc._meta['delete_rules'][rule_entry] if rule == CASCADE: ref_q = document_cls.objects(**{field_name + '__in': self}) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c91600d03..7f3d5c483 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1302,6 +1302,31 @@ class BlogPost(Document): self.Person.objects(name='Test User').delete() self.assertEqual(1, BlogPost.objects.count()) + def test_reverse_delete_rule_cascade_on_abstract_document(self): + """Ensure cascading deletion of referring documents from the database + does not fail on abstract document. + """ + class AbstractBlogPost(Document): + meta = {'abstract': True} + author = ReferenceField(self.Person, reverse_delete_rule=CASCADE) + + class BlogPost(AbstractBlogPost): + content = StringField() + BlogPost.drop_collection() + + me = self.Person(name='Test User') + me.save() + someoneelse = self.Person(name='Some-one Else') + someoneelse.save() + + BlogPost(content='Watching TV', author=me).save() + BlogPost(content='Chilling out', author=me).save() + BlogPost(content='Pro Testing', author=someoneelse).save() + + self.assertEqual(3, BlogPost.objects.count()) + self.Person.objects(name='Test User').delete() + 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 @@ -1361,6 +1386,31 @@ class BlogPost(Document): self.assertEqual(1, BlogPost.objects.count()) self.assertEqual(None, BlogPost.objects.first().category) + def test_reverse_delete_rule_nullify_on_abstract_document(self): + """Ensure nullification of references to deleted documents when + reference is on an abstract document. + """ + class AbstractBlogPost(Document): + meta = {'abstract': True} + author = ReferenceField(self.Person, reverse_delete_rule=NULLIFY) + + class BlogPost(AbstractBlogPost): + content = StringField() + BlogPost.drop_collection() + + me = self.Person(name='Test User') + me.save() + someoneelse = self.Person(name='Some-one Else') + someoneelse.save() + + BlogPost(content='Watching TV', author=me).save() + + self.assertEqual(1, BlogPost.objects.count()) + self.assertEqual(me, BlogPost.objects.first().author) + self.Person.objects(name='Test User').delete() + self.assertEqual(1, BlogPost.objects.count()) + self.assertEqual(None, BlogPost.objects.first().author) + def test_reverse_delete_rule_deny(self): """Ensure deletion gets denied on documents that still have references to them. @@ -1380,6 +1430,26 @@ class BlogPost(Document): self.assertRaises(OperationError, self.Person.objects.delete) + def test_reverse_delete_rule_deny_on_abstract_document(self): + """Ensure deletion gets denied on documents that still have references + to them, when reference is on an abstract document. + """ + class AbstractBlogPost(Document): + meta = {'abstract': True} + author = ReferenceField(self.Person, reverse_delete_rule=DENY) + + class BlogPost(AbstractBlogPost): + content = StringField() + BlogPost.drop_collection() + + me = self.Person(name='Test User') + me.save() + + BlogPost(content='Watching TV', author=me).save() + + self.assertEqual(1, BlogPost.objects.count()) + self.assertRaises(OperationError, self.Person.objects.delete) + def test_reverse_delete_rule_pull(self): """Ensure pulling of references to deleted documents. """ @@ -1410,6 +1480,40 @@ class BlogPost(Document): self.assertEqual(post.authors, [me]) self.assertEqual(another.authors, []) + def test_reverse_delete_rule_pull_on_abstract_documents(self): + """Ensure pulling of references to deleted documents when reference + is defined on an abstract document.. + """ + class AbstractBlogPost(Document): + meta = {'abstract': True} + authors = ListField(ReferenceField(self.Person, + reverse_delete_rule=PULL)) + + class BlogPost(AbstractBlogPost): + content = StringField() + + 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_delete_with_limits(self): class Log(Document): From db36d0a3752cc7cddfcd9c198f52c21028040933 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Wed, 3 Dec 2014 23:50:51 -0500 Subject: [PATCH 0058/1519] Ensure Indexes before Each Save - Rely on caching within the PyMongo driver to provide lightweight calls while indices are cached. - Closes MongoEngine/mongoengine#812. --- docs/changelog.rst | 3 ++- mongoengine/document.py | 2 ++ tests/document/indexes.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 493715779..fc8b281cf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,8 @@ Changelog Changes in 0.9.X - DEV ====================== -- Generate Unique Indicies for Lists of EmbeddedDocuments #358 +- Ensure Indexes before Each Save #812 +- Generate Unique Indices for Lists of EmbeddedDocuments #358 - Sparse fields #515 - write_concern not in params of Collection#remove #801 - Better BaseDocument equality check when not saved #798 diff --git a/mongoengine/document.py b/mongoengine/document.py index 203b2659f..03318c849 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -285,6 +285,8 @@ def save(self, force_insert=False, validate=True, clean=True, try: collection = self._get_collection() + if self._meta.get('auto_create_index', True): + self.ensure_indexes() if created: if force_insert: object_id = collection.insert(doc, **write_concern) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 4815350f6..d3e372677 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -18,7 +18,7 @@ class IndexesTest(unittest.TestCase): def setUp(self): - connect(db='mongoenginetest') + self.connection = connect(db='mongoenginetest') self.db = get_db() class Person(Document): @@ -795,6 +795,33 @@ class Book(Document): key = indexes["title_text"]["key"] self.assertTrue(('_fts', 'text') in key) + def test_indexes_after_database_drop(self): + """ + Test to ensure that indexes are re-created on a collection even + after the database has been dropped. + + Issue #812 + """ + class BlogPost(Document): + title = StringField() + slug = StringField(unique=True) + + BlogPost.drop_collection() + + # Create Post #1 + post1 = BlogPost(title='test1', slug='test') + post1.save() + + # Drop the Database + self.connection.drop_database(BlogPost._get_db().name) + + # Re-create Post #1 + post1 = BlogPost(title='test1', slug='test') + post1.save() + + # Create Post #2 + post2 = BlogPost(title='test2', slug='test') + self.assertRaises(NotUniqueError, post2.save) if __name__ == '__main__': unittest.main() From d80be60e2b67386c0da07c2a7b3da71ee98ef9a1 Mon Sep 17 00:00:00 2001 From: aeroeng Date: Tue, 6 Jan 2015 14:37:40 -0500 Subject: [PATCH 0059/1519] mongo $and list should not contain list elements in order to avoid this error: $and/$or elements must be objects --- mongoengine/queryset/transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index e575d9d6c..83bd9f8e1 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -160,7 +160,7 @@ def query(_doc_cls=None, _field_operation=False, **query): if isinstance(v, list): value = [{k: val} for val in v] if '$and' in mongo_query.keys(): - mongo_query['$and'].append(value) + mongo_query['$and'].extend(value) else: mongo_query['$and'] = value From 1a24d599b31e020733f6a9e8a64a79579e6bd8d9 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Sun, 11 Jan 2015 19:12:10 -0500 Subject: [PATCH 0060/1519] Field Choices Now Accept Subclasses of Documents - Fields containing 'choices' of which a choice is an EmbeddedDocument or Document will now accept subclasses of that choice. --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 28 +++++++++++++------------ tests/fields/fields.py | 43 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fc8b281cf..ae4ba85a2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Field Choices Now Accept Subclasses of Documents - Ensure Indexes before Each Save #812 - Generate Unique Indices for Lists of EmbeddedDocuments #358 - Sparse fields #515 diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 6fad5d632..359ea6d2a 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -158,21 +158,23 @@ def validate(self, value, clean=True): def _validate(self, value, **kwargs): Document = _import_class('Document') EmbeddedDocument = _import_class('EmbeddedDocument') - # check choices + + # Check the Choices Constraint 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' + + choice_list = self.choices if isinstance(self.choices[0], (list, tuple)): - option_keys = [k for k, v in self.choices] - if value_to_check not in option_keys: - msg = ('Value must be %s of %s' % - (err_msg, unicode(option_keys))) - self.error(msg) - elif value_to_check not in self.choices: - msg = ('Value must be %s of %s' % - (err_msg, unicode(self.choices))) - self.error(msg) + choice_list = [k for k, v in self.choices] + + # Choices which are other types of Documents + if isinstance(value, (Document, EmbeddedDocument)): + if not any(isinstance(value, c) for c in choice_list): + self.error( + 'Value must be instance of %s' % unicode(choice_list) + ) + # Choices which are types other than Documents + elif value not in choice_list: + self.error('Value must be one of %s' % unicode(choice_list)) # check validation argument if self.validation is not None: diff --git a/tests/fields/fields.py b/tests/fields/fields.py index a95001b46..fedc7c385 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2446,6 +2446,49 @@ class Shirt(Document): Shirt.drop_collection() + def test_choices_validation_documents(self): + """ + Ensure that a field with choices which are documents are + validated correctly. + """ + class UserComments(EmbeddedDocument): + author = StringField() + message = StringField() + + class BlogPost(Document): + comments = ListField( + GenericEmbeddedDocumentField(choices=(UserComments,)) + ) + + BlogPost(comments=[ + UserComments(author='user2', message='message2'), + ]).save() + + def test_choices_validation_documents_inheritance(self): + """ + Ensure that a field with choices which are documents are + validated correctly when a subclass of a valid choice is used. + """ + class Comments(EmbeddedDocument): + meta = { + 'abstract': True + } + + author = StringField() + message = StringField() + + class UserComments(Comments): + pass + + class BlogPost(Document): + comments = ListField( + GenericEmbeddedDocumentField(choices=(Comments,)) + ) + + BlogPost(comments=[ + UserComments(author='user2', message='message2'), + ]).save() + def test_choices_get_field_display(self): """Test dynamic helper for returning the display value of a choices field. From 213a0a18a578c539baa83439b4719977271e7f34 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Mon, 12 Jan 2015 10:11:42 -0500 Subject: [PATCH 0061/1519] Updated Unit Tests for Field Choices of Documents - Added Unit Test with Invalid EmbeddedDocument Choice. - Updated Broken Link in Author's File --- AUTHORS | 2 +- tests/fields/fields.py | 40 +++++++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index fb9a1ce5a..18258953a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -215,4 +215,4 @@ that much better: * André Ericson https://github.com/aericson) * Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky) * Diego Berrocal (https://github.com/cestdiego) - * Matthew Ellison (https://github.com/mmelliso) + * Matthew Ellison (https://github.com/seglberg) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index fedc7c385..ab220b25c 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2448,8 +2448,7 @@ class Shirt(Document): def test_choices_validation_documents(self): """ - Ensure that a field with choices which are documents are - validated correctly. + Ensure fields with document choices validate given a valid choice. """ class UserComments(EmbeddedDocument): author = StringField() @@ -2460,20 +2459,50 @@ class BlogPost(Document): GenericEmbeddedDocumentField(choices=(UserComments,)) ) + # Ensure Validation Passes BlogPost(comments=[ UserComments(author='user2', message='message2'), ]).save() + def test_choices_validation_documents_invalid(self): + """ + Ensure fields with document choices validate given an invalid choice. + This should throw a ValidationError exception. + """ + class UserComments(EmbeddedDocument): + author = StringField() + message = StringField() + + class ModeratorComments(EmbeddedDocument): + author = StringField() + message = StringField() + + class BlogPost(Document): + comments = ListField( + GenericEmbeddedDocumentField(choices=(UserComments,)) + ) + + # Single Entry Failure + post = BlogPost(comments=[ + ModeratorComments(author='mod1', message='message1'), + ]) + self.assertRaises(ValidationError, post.save) + + # Mixed Entry Failure + post = BlogPost(comments=[ + ModeratorComments(author='mod1', message='message1'), + UserComments(author='user2', message='message2'), + ]) + self.assertRaises(ValidationError, post.save) + def test_choices_validation_documents_inheritance(self): """ - Ensure that a field with choices which are documents are - validated correctly when a subclass of a valid choice is used. + Ensure fields with document choices validate given subclass of choice. """ class Comments(EmbeddedDocument): meta = { 'abstract': True } - author = StringField() message = StringField() @@ -2485,6 +2514,7 @@ class BlogPost(Document): GenericEmbeddedDocumentField(choices=(Comments,)) ) + # Save Valid EmbeddedDocument Type BlogPost(comments=[ UserComments(author='user2', message='message2'), ]).save() From cbb0b57018b7accbd9bd4faa7a6fcb4afc9b5023 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Fri, 13 Feb 2015 11:08:52 +0200 Subject: [PATCH 0062/1519] Fix #872: No module named 'django.utils.importlib' (Django dev) --- docs/changelog.rst | 1 + mongoengine/django/mongo_auth/models.py | 6 ++++- tests/test_django.py | 29 ++++++++++++++++++++----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ae4ba85a2..436671ecb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- No module named 'django.utils.importlib' (Django dev) #872 - Field Choices Now Accept Subclasses of Documents - Ensure Indexes before Each Save #812 - Generate Unique Indices for Lists of EmbeddedDocuments #358 diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py index 60965e6d7..ad4ceff8d 100644 --- a/mongoengine/django/mongo_auth/models.py +++ b/mongoengine/django/mongo_auth/models.py @@ -3,7 +3,11 @@ from django.contrib.auth.models import UserManager from django.core.exceptions import ImproperlyConfigured from django.db import models -from django.utils.importlib import import_module +try: + from django.utils.module_loading import import_module +except ImportError: + """Handle older versions of Django""" + from django.utils.importlib import import_module from django.utils.translation import ugettext_lazy as _ diff --git a/tests/test_django.py b/tests/test_django.py index 0070087fb..cb7efe6f1 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -19,9 +19,12 @@ AUTHENTICATION_BACKENDS = ('mongoengine.django.auth.MongoEngineBackend',) ) -# For Django >= 1.7 -if hasattr(django, 'setup'): - django.setup() +try: + # For Django >= 1.7 + if hasattr(django, 'setup'): + django.setup() +except RuntimeError: + pass try: from django.contrib.auth import authenticate, get_user_model @@ -34,7 +37,6 @@ DJ15 = True except Exception: DJ15 = False -from django.contrib.sessions.tests import SessionTestsMixin from mongoengine.django.sessions import SessionStore, MongoSession from mongoengine.django.tests import MongoTestCase from datetime import tzinfo, timedelta @@ -226,13 +228,13 @@ class Note(Document): self.assertEqual(t.render(c), "10") -class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase): +class _BaseMongoDBSessionTest(unittest.TestCase): backend = SessionStore def setUp(self): connect(db='mongoenginetest') MongoSession.drop_collection() - super(MongoDBSessionTest, self).setUp() + super(_BaseMongoDBSessionTest, self).setUp() def assertIn(self, first, second, msg=None): self.assertTrue(first in second, msg) @@ -259,6 +261,21 @@ def test_session_expiration_tz(self): self.assertTrue('test_expire' in session, 'Session has expired before it is expected') +try: + # SessionTestsMixin isn't available for import on django > 1.8a1 + from django.contrib.sessions.tests import SessionTestsMixin + + class _MongoDBSessionTest(SessionTestsMixin): + pass + + class MongoDBSessionTest(_BaseMongoDBSessionTest): + pass + +except ImportError: + class MongoDBSessionTest(_BaseMongoDBSessionTest): + pass + + class MongoAuthTest(unittest.TestCase): user_data = { 'username': 'user', From 25298c72bbafb80a1814fc84496f2925c5d1700e Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Sun, 15 Feb 2015 10:02:22 +0200 Subject: [PATCH 0063/1519] Fix #877: Fix tests for pymongo 2.8+ --- docs/changelog.rst | 1 + tests/document/indexes.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 436671ecb..b5bf486c3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Fix tests for pymongo 2.8+ #877 - No module named 'django.utils.importlib' (Django dev) #872 - Field Choices Now Accept Subclasses of Documents - Ensure Indexes before Each Save #812 diff --git a/tests/document/indexes.py b/tests/document/indexes.py index d3e372677..ab156f68f 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -495,9 +495,12 @@ class BlogPost(Document): self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10) - def invalid_index(): - BlogPost.objects.hint('tags') - self.assertRaises(TypeError, invalid_index) + if pymongo.version >= '2.8': + self.assertEqual(BlogPost.objects.hint('tags').count(), 10) + else: + def invalid_index(): + BlogPost.objects.hint('tags') + self.assertRaises(TypeError, invalid_index) def invalid_index_2(): return BlogPost.objects.hint(('tags', 1)) From 225c31d5834158bd02c653ac24874fa083adec2a Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Sat, 14 Feb 2015 18:40:24 +0200 Subject: [PATCH 0064/1519] Fix #766: Add support for operator --- docs/changelog.rst | 1 + mongoengine/queryset/transform.py | 2 +- tests/queryset/transform.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b5bf486c3..e70fc8e35 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Add support for $type operator # 766 - Fix tests for pymongo 2.8+ #877 - No module named 'django.utils.importlib' (Django dev) #872 - Field Choices Now Accept Subclasses of Documents diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 83bd9f8e1..be78c411c 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -11,7 +11,7 @@ COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', - 'all', 'size', 'exists', 'not', 'elemMatch') + 'all', 'size', 'exists', 'not', 'elemMatch', 'type') GEO_OPERATORS = ('within_distance', 'within_spherical_distance', 'within_box', 'within_polygon', 'near', 'near_sphere', 'max_distance', 'geo_within', 'geo_within_box', diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 48e01919d..2d5261f25 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -197,5 +197,17 @@ class Location(Document): update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}}) + def test_type(self): + class Doc(Document): + df = DynamicField() + Doc(df=True).save() + Doc(df=7).save() + Doc(df="df").save() + self.assertEqual(Doc.objects(df__type=1).count(), 0) # double + self.assertEqual(Doc.objects(df__type=8).count(), 1) # bool + self.assertEqual(Doc.objects(df__type=2).count(), 1) # str + self.assertEqual(Doc.objects(df__type=16).count(), 1) # int + + if __name__ == '__main__': unittest.main() From 5c3d91e65e5bf38932ad54659ab60820b8cd37db Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Mon, 16 Feb 2015 12:25:37 +0200 Subject: [PATCH 0065/1519] Fix #866: does not follow --- docs/changelog.rst | 1 + mongoengine/base/datastructures.py | 4 ++++ tests/fields/fields.py | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e70fc8e35..a90b2373d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- `BaseDict` does not follow `setdefault` #866 - Add support for $type operator # 766 - Fix tests for pymongo 2.8+ #877 - No module named 'django.utils.importlib' (Django dev) #872 diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index abe41a144..022338d75 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -75,6 +75,10 @@ def popitem(self, *args, **kwargs): self._mark_as_changed() return super(BaseDict, self).popitem(*args, **kwargs) + def setdefault(self, *args, **kwargs): + self._mark_as_changed() + return super(BaseDict, self).setdefault(*args, **kwargs) + def update(self, *args, **kwargs): self._mark_as_changed() return super(BaseDict, self).update(*args, **kwargs) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index ab220b25c..8e957c683 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1176,6 +1176,11 @@ class BlogPost(Document): post.reload() self.assertEqual('updated', post.info['title']) + post.info.setdefault('authors', []) + post.save() + post.reload() + self.assertEqual([], post.info['authors']) + BlogPost.drop_collection() def test_dictfield_strict(self): From 129632cd6b4641d7b192507f9175df80733684f1 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Tue, 17 Feb 2015 14:38:10 +0200 Subject: [PATCH 0066/1519] Fix #863: Request Support for $min, $max Field update operators --- docs/changelog.rst | 1 + mongoengine/queryset/transform.py | 2 +- tests/queryset/queryset.py | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a90b2373d..1f6565dc4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Request Support for $min, $max Field update operators #863 - `BaseDict` does not follow `setdefault` #866 - Add support for $type operator # 766 - Fix tests for pymongo 2.8+ #877 diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index be78c411c..03a09dc51 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -26,7 +26,7 @@ UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push', 'push_all', 'pull', 'pull_all', 'add_to_set', - 'set_on_insert') + 'set_on_insert', 'min', 'max') def query(_doc_cls=None, _field_operation=False, **query): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 03d2bdd9f..6cbac4956 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -510,6 +510,17 @@ class BlogPost(Document): self.assertEqual(post.comments[0].by, 'joe') self.assertEqual(post.comments[0].votes.score, 4) + def test_update_min_max(self): + class Scores(Document): + high_score = IntField() + low_score = IntField() + scores = Scores(high_score=800, low_score=200) + scores.save() + Scores.objects(id=scores.id).update(min__low_score=150) + self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150) + Scores.objects(id=scores.id).update(min__low_score=250) + self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150) + def test_updates_can_have_match_operators(self): class Comment(EmbeddedDocument): From b71a9bc0978bbc951943f962b8b5eb1cdaf73b5a Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Tue, 17 Feb 2015 13:22:02 -0800 Subject: [PATCH 0067/1519] Update fields.py Type name should be "MultiPolygon". --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 7af647417..43536ef9a 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1959,7 +1959,7 @@ class MultiPolygonField(GeoJsonBaseField): .. code-block:: js - { "type" : "Polygon" , + { "type" : "MultiPolygon" , "coordinates" : [[ [[x1, y1], [x1, y1] ... [xn, yn]], [[x1, y1], [x1, y1] ... [xn, yn]] From ce9d0d7e8219f1ec6b98d46de892bae727c58c46 Mon Sep 17 00:00:00 2001 From: Jimmy Shen Date: Wed, 18 Feb 2015 10:37:13 -0500 Subject: [PATCH 0068/1519] Fix #864: ComplexDateTimeField should fall back to None when null=True --- AUTHORS | 1 + docs/changelog.rst | 2 +- mongoengine/fields.py | 2 +- tests/document/instance.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 18258953a..0ed0e0b50 100644 --- a/AUTHORS +++ b/AUTHORS @@ -216,3 +216,4 @@ that much better: * Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky) * Diego Berrocal (https://github.com/cestdiego) * Matthew Ellison (https://github.com/seglberg) + * Jimmy Shen (https://github.com/jimmyshen) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1f6565dc4..4e53234fa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- ComplexDateTimeField should fall back to None when null=True #864 - Request Support for $min, $max Field update operators #863 - `BaseDict` does not follow `setdefault` #866 - Add support for $type operator # 766 @@ -76,7 +77,6 @@ Changes in 0.9.X - DEV - Make `in_bulk()` respect `no_dereference()` #775 - Handle None from model __str__; Fixes #753 #754 - Changes in 0.8.7 ================ - Calling reload on deleted / nonexistent documents raises DoesNotExist (#538) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 7af647417..cfe66a48d 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -510,7 +510,7 @@ def _convert_from_string(self, data): def __get__(self, instance, owner): data = super(ComplexDateTimeField, self).__get__(instance, owner) if data is None: - return datetime.datetime.now() + return None if self.null else datetime.datetime.now() if isinstance(data, datetime.datetime): return data return self._convert_from_string(data) diff --git a/tests/document/instance.py b/tests/document/instance.py index 3c6a9940f..eb887dced 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2760,6 +2760,12 @@ def test_null_field(self): class User(Document): name = StringField() height = IntField(default=184, null=True) + str_fld = StringField(null=True) + int_fld = IntField(null=True) + flt_fld = FloatField(null=True) + dt_fld = DateTimeField(null=True) + cdt_fld = ComplexDateTimeField(null=True) + User.objects.delete() u = User(name='user') u.save() @@ -2767,6 +2773,12 @@ class User(Document): u_from_db.height = None u_from_db.save() self.assertEquals(u_from_db.height, None) + # 864 + self.assertEqual(u_from_db.str_fld, None) + self.assertEqual(u_from_db.int_fld, None) + self.assertEqual(u_from_db.flt_fld, None) + self.assertEqual(u_from_db.dt_fld, None) + self.assertEqual(u_from_db.cdt_fld, None) # 735 User.objects.delete() From 42721628ebc48cf0a64084e31160b43c5b6853a2 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Wed, 3 Dec 2014 17:54:07 -0500 Subject: [PATCH 0069/1519] Added EmbeddedDocumentListField Implementation - Added new field type: EmbeddedDocumentListField. - Provides additional query ability for lists of embedded documents. - Closes MongoEngine/mongoengine#503. --- docs/apireference.rst | 16 + docs/changelog.rst | 1 + mongoengine/base/datastructures.py | 166 +++++++++- mongoengine/base/document.py | 15 +- mongoengine/base/fields.py | 14 +- mongoengine/common.py | 18 +- mongoengine/dereference.py | 10 +- mongoengine/document.py | 22 +- mongoengine/fields.py | 40 ++- tests/fields/fields.py | 472 ++++++++++++++++++++++++++++- 10 files changed, 745 insertions(+), 29 deletions(-) diff --git a/docs/apireference.rst b/docs/apireference.rst index 857a14b09..7ba93408c 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -79,6 +79,7 @@ Fields .. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField .. autoclass:: mongoengine.fields.DynamicField .. autoclass:: mongoengine.fields.ListField +.. autoclass:: mongoengine.fields.EmbeddedDocumentListField .. autoclass:: mongoengine.fields.SortedListField .. autoclass:: mongoengine.fields.DictField .. autoclass:: mongoengine.fields.MapField @@ -103,6 +104,21 @@ Fields .. autoclass:: mongoengine.fields.ImageGridFsProxy .. autoclass:: mongoengine.fields.ImproperlyConfigured +Embedded Document Querying +========================== + +.. versionadded:: 0.9 + +Additional queries for Embedded Documents are available when using the +:class:`~mongoengine.EmbeddedDocumentListField` to store a list of embedded +documents. + +A list of embedded documents is returned as a special list with the +following methods: + +.. autoclass:: mongoengine.base.datastructures.EmbeddedDocumentList + :members: + Misc ==== diff --git a/docs/changelog.rst b/docs/changelog.rst index 4e53234fa..9aa6aa3a2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826 - ComplexDateTimeField should fall back to None when null=True #864 - Request Support for $min, $max Field update operators #863 - `BaseDict` does not follow `setdefault` #866 diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 022338d75..bac67ddca 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -2,8 +2,9 @@ import functools import itertools from mongoengine.common import _import_class +from mongoengine.errors import DoesNotExist, MultipleObjectsReturned -__all__ = ("BaseDict", "BaseList") +__all__ = ("BaseDict", "BaseList", "EmbeddedDocumentList") class BaseDict(dict): @@ -106,7 +107,7 @@ def __init__(self, list_items, instance, name): if isinstance(instance, (Document, EmbeddedDocument)): self._instance = weakref.proxy(instance) self._name = name - return super(BaseList, self).__init__(list_items) + super(BaseList, self).__init__(list_items) def __getitem__(self, key, *args, **kwargs): value = super(BaseList, self).__getitem__(key) @@ -191,6 +192,167 @@ def _mark_as_changed(self, key=None): self._instance._mark_as_changed(self._name) +class EmbeddedDocumentList(BaseList): + + @classmethod + def __match_all(cls, i, kwargs): + items = kwargs.items() + return all([ + getattr(i, k) == v or str(getattr(i, k)) == v for k, v in items + ]) + + @classmethod + def __only_matches(cls, obj, kwargs): + if not kwargs: + return obj + return filter(lambda i: cls.__match_all(i, kwargs), obj) + + def __init__(self, list_items, instance, name): + super(EmbeddedDocumentList, self).__init__(list_items, instance, name) + self._instance = instance + + def filter(self, **kwargs): + """ + Filters the list by only including embedded documents with the + given keyword arguments. + + :param kwargs: The keyword arguments corresponding to the fields to + filter on. *Multiple arguments are treated as if they are ANDed + together.* + :return: A new ``EmbeddedDocumentList`` containing the matching + embedded documents. + + Raises ``AttributeError`` if a given keyword is not a valid field for + the embedded document class. + """ + values = self.__only_matches(self, kwargs) + return EmbeddedDocumentList(values, self._instance, self._name) + + def exclude(self, **kwargs): + """ + Filters the list by excluding embedded documents with the given + keyword arguments. + + :param kwargs: The keyword arguments corresponding to the fields to + exclude on. *Multiple arguments are treated as if they are ANDed + together.* + :return: A new ``EmbeddedDocumentList`` containing the non-matching + embedded documents. + + Raises ``AttributeError`` if a given keyword is not a valid field for + the embedded document class. + """ + exclude = self.__only_matches(self, kwargs) + values = [item for item in self if item not in exclude] + return EmbeddedDocumentList(values, self._instance, self._name) + + def count(self): + """ + The number of embedded documents in the list. + + :return: The length of the list, equivalent to the result of ``len()``. + """ + return len(self) + + def get(self, **kwargs): + """ + Retrieves an embedded document determined by the given keyword + arguments. + + :param kwargs: The keyword arguments corresponding to the fields to + search on. *Multiple arguments are treated as if they are ANDed + together.* + :return: The embedded document matched by the given keyword arguments. + + Raises ``DoesNotExist`` if the arguments used to query an embedded + document returns no results. ``MultipleObjectsReturned`` if more + than one result is returned. + """ + values = self.__only_matches(self, kwargs) + if len(values) == 0: + raise DoesNotExist( + "%s matching query does not exist." % self._name + ) + elif len(values) > 1: + raise MultipleObjectsReturned( + "%d items returned, instead of 1" % len(values) + ) + + return values[0] + + def first(self): + """ + Returns the first embedded document in the list, or ``None`` if empty. + """ + if len(self) > 0: + return self[0] + + def create(self, **values): + """ + Creates a new embedded document and saves it to the database. + + .. note:: + The embedded document changes are not automatically saved + to the database after calling this method. + + :param values: A dictionary of values for the embedded document. + :return: The new embedded document instance. + """ + name = self._name + EmbeddedClass = self._instance._fields[name].field.document_type_obj + self._instance[self._name].append(EmbeddedClass(**values)) + + return self._instance[self._name][-1] + + def save(self, *args, **kwargs): + """ + Saves the ancestor document. + + :param args: Arguments passed up to the ancestor Document's save + method. + :param kwargs: Keyword arguments passed up to the ancestor Document's + save method. + """ + self._instance.save(*args, **kwargs) + + def delete(self): + """ + Deletes the embedded documents from the database. + + .. note:: + The embedded document changes are not automatically saved + to the database after calling this method. + + :return: The number of entries deleted. + """ + values = list(self) + for item in values: + self._instance[self._name].remove(item) + + return len(values) + + def update(self, **update): + """ + Updates the embedded documents with the given update values. + + .. note:: + The embedded document changes are not automatically saved + to the database after calling this method. + + :param update: A dictionary of update values to apply to each + embedded document. + :return: The number of entries updated. + """ + if len(update) == 0: + return 0 + values = list(self) + for item in values: + for k, v in update.items(): + setattr(item, k, v) + + return len(values) + + class StrictDict(object): __slots__ = () _special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create']) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 998e366fe..6bf38ee21 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -16,7 +16,13 @@ from mongoengine.python_support import PY3, txt_type from mongoengine.base.common import get_document, ALLOW_INHERITANCE -from mongoengine.base.datastructures import BaseDict, BaseList, StrictDict, SemiStrictDict +from mongoengine.base.datastructures import ( + BaseDict, + BaseList, + EmbeddedDocumentList, + StrictDict, + SemiStrictDict +) from mongoengine.base.fields import ComplexBaseField __all__ = ('BaseDocument', 'NON_FIELD_ERRORS') @@ -419,6 +425,8 @@ def __expand_dynamic_values(self, name, value): if not isinstance(value, (dict, list, tuple)): return value + EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField') + is_list = False if not hasattr(value, 'items'): is_list = True @@ -442,7 +450,10 @@ def __expand_dynamic_values(self, name, value): # 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, self, name) + if issubclass(type(self), EmbeddedDocumentListField): + value = EmbeddedDocumentList(value, self, name) + else: + value = BaseList(value, self, name) elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, self, name) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 359ea6d2a..aa16804e6 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -9,7 +9,9 @@ from mongoengine.errors import ValidationError from mongoengine.base.common import ALLOW_INHERITANCE -from mongoengine.base.datastructures import BaseDict, BaseList +from mongoengine.base.datastructures import ( + BaseDict, BaseList, EmbeddedDocumentList +) __all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField") @@ -210,6 +212,7 @@ def __get__(self, instance, owner): ReferenceField = _import_class('ReferenceField') GenericReferenceField = _import_class('GenericReferenceField') + EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField') dereference = (self._auto_dereference and (self.field is None or isinstance(self.field, (GenericReferenceField, ReferenceField)))) @@ -226,9 +229,12 @@ def __get__(self, instance, owner): value = super(ComplexBaseField, self).__get__(instance, owner) # 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) + if isinstance(value, (list, tuple)): + if (issubclass(type(self), EmbeddedDocumentListField) and + not isinstance(value, EmbeddedDocumentList)): + value = EmbeddedDocumentList(value, instance, self.name) + elif not isinstance(value, BaseList): + value = BaseList(value, instance, self.name) instance._data[self.name] = value elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, instance, self.name) diff --git a/mongoengine/common.py b/mongoengine/common.py index 7c0c18d2e..3e63e98ee 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -1,4 +1,5 @@ _class_registry_cache = {} +_field_list_cache = [] def _import_class(cls_name): @@ -20,13 +21,16 @@ class from the :data:`mongoengine.common._class_registry_cache`. doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument', 'MapReduceDocument') - field_classes = ('DictField', 'DynamicField', 'EmbeddedDocumentField', - 'FileField', 'GenericReferenceField', - 'GenericEmbeddedDocumentField', 'GeoPointField', - 'PointField', 'LineStringField', 'ListField', - 'PolygonField', 'ReferenceField', 'StringField', - 'CachedReferenceField', - 'ComplexBaseField', 'GeoJsonBaseField') + + # Field Classes + if not _field_list_cache: + from mongoengine.fields import __all__ as fields + _field_list_cache.extend(fields) + from mongoengine.base.fields import __all__ as fields + _field_list_cache.extend(fields) + + field_classes = _field_list_cache + queryset_classes = ('OperationError',) deref_classes = ('DeReference',) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index a22e3473e..415d56782 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -1,6 +1,9 @@ from bson import DBRef, SON -from base import (BaseDict, BaseList, TopLevelDocumentMetaclass, get_document) +from base import ( + BaseDict, BaseList, EmbeddedDocumentList, + TopLevelDocumentMetaclass, get_document +) from fields import (ReferenceField, ListField, DictField, MapField) from connection import get_db from queryset import QuerySet @@ -189,6 +192,9 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): if not hasattr(items, 'items'): is_list = True + list_type = BaseList + if isinstance(items, EmbeddedDocumentList): + list_type = EmbeddedDocumentList as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] @@ -225,7 +231,7 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): if instance and name: if is_list: - return tuple(data) if as_tuple else BaseList(data, instance, name) + return tuple(data) if as_tuple else list_type(data, instance, name) return BaseDict(data, instance, name) depth += 1 return data diff --git a/mongoengine/document.py b/mongoengine/document.py index 03318c849..5b1b313d9 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -9,9 +9,16 @@ from bson.dbref import DBRef from mongoengine import signals from mongoengine.common import _import_class -from mongoengine.base import (DocumentMetaclass, TopLevelDocumentMetaclass, - BaseDocument, BaseDict, BaseList, - ALLOW_INHERITANCE, get_document) +from mongoengine.base import ( + DocumentMetaclass, + TopLevelDocumentMetaclass, + BaseDocument, + BaseDict, + BaseList, + EmbeddedDocumentList, + ALLOW_INHERITANCE, + get_document +) from mongoengine.errors import ValidationError, InvalidQueryError, InvalidDocumentError from mongoengine.queryset import (OperationError, NotUniqueError, QuerySet, transform) @@ -76,6 +83,12 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) + def save(self, *args, **kwargs): + self._instance.save(*args, **kwargs) + + def reload(self, *args, **kwargs): + self._instance.reload(*args, **kwargs) + class Document(BaseDocument): @@ -560,6 +573,9 @@ def _reload(self, key, value): if isinstance(value, BaseDict): value = [(k, self._reload(k, v)) for k, v in value.items()] value = BaseDict(value, self, key) + elif isinstance(value, EmbeddedDocumentList): + value = [self._reload(key, v) for v in value] + value = EmbeddedDocumentList(value, self, key) elif isinstance(value, BaseList): value = [self._reload(key, v) for v in value] value = BaseList(value, self, key) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index cfe66a48d..9d66774ed 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -39,13 +39,13 @@ 'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', 'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField', 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', - 'SortedListField', 'DictField', 'MapField', 'ReferenceField', - 'CachedReferenceField', 'GenericReferenceField', 'BinaryField', - 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', - 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', - 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', - 'MultiPointField', 'MultiLineStringField', 'MultiPolygonField', - 'GeoJsonBaseField'] + 'SortedListField', 'EmbeddedDocumentListField', 'DictField', + 'MapField', 'ReferenceField', 'CachedReferenceField', + 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', + 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', + 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', + 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField', + 'MultiPolygonField', 'GeoJsonBaseField'] RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -728,6 +728,32 @@ def prepare_query_value(self, op, value): return super(ListField, self).prepare_query_value(op, value) +class EmbeddedDocumentListField(ListField): + """A :class:`~mongoengine.ListField` designed specially to hold a list of + embedded documents to provide additional query helpers. + + .. note:: + The only valid list values are subclasses of + :class:`~mongoengine.EmbeddedDocument`. + + .. versionadded:: 0.9 + + """ + + def __init__(self, document_type, *args, **kwargs): + """ + :param document_type: The type of + :class:`~mongoengine.EmbeddedDocument` the list will hold. + :param args: Arguments passed directly into the parent + :class:`~mongoengine.ListField`. + :param kwargs: Keyword arguments passed directly into the parent + :class:`~mongoengine.ListField`. + """ + super(EmbeddedDocumentListField, self).__init__( + field=EmbeddedDocumentField(document_type), **kwargs + ) + + class SortedListField(ListField): """A ListField that sorts the contents of its list before writing to diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 8e957c683..64898995f 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -18,11 +18,11 @@ from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import _document_registry -from mongoengine.base.datastructures import BaseDict +from mongoengine.base.datastructures import BaseDict, EmbeddedDocumentList from mongoengine.errors import NotRegistered from mongoengine.python_support import PY3, b, bin_type -__all__ = ("FieldTest", ) +__all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") class FieldTest(unittest.TestCase): @@ -3159,5 +3159,473 @@ def test(): self.assertRaises(FieldDoesNotExist, test) + +class EmbeddedDocumentListFieldTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.db = connect(db='EmbeddedDocumentListFieldTestCase') + + class Comments(EmbeddedDocument): + author = StringField() + message = StringField() + + class BlogPost(Document): + comments = EmbeddedDocumentListField(Comments) + + cls.Comments = Comments + cls.BlogPost = BlogPost + + def setUp(self): + """ + Create two BlogPost entries in the database, each with + several EmbeddedDocuments. + """ + self.post1 = self.BlogPost(comments=[ + self.Comments(author='user1', message='message1'), + self.Comments(author='user2', message='message1') + ]).save() + + self.post2 = self.BlogPost(comments=[ + self.Comments(author='user2', message='message2'), + self.Comments(author='user2', message='message3'), + self.Comments(author='user3', message='message1') + ]).save() + + def tearDown(self): + self.BlogPost.drop_collection() + + @classmethod + def tearDownClass(cls): + cls.db.drop_database('EmbeddedDocumentListFieldTestCase') + + def test_no_keyword_filter(self): + """ + Tests the filter method of a List of Embedded Documents + with a no keyword. + """ + filtered = self.post1.comments.filter() + + # Ensure nothing was changed + # < 2.6 Incompatible > + # self.assertListEqual(filtered, self.post1.comments) + self.assertEqual(filtered, self.post1.comments) + + def test_single_keyword_filter(self): + """ + Tests the filter method of a List of Embedded Documents + with a single keyword. + """ + filtered = self.post1.comments.filter(author='user1') + + # Ensure only 1 entry was returned. + self.assertEqual(len(filtered), 1) + + # Ensure the entry returned is the correct entry. + self.assertEqual(filtered[0].author, 'user1') + + def test_multi_keyword_filter(self): + """ + Tests the filter method of a List of Embedded Documents + with multiple keywords. + """ + filtered = self.post2.comments.filter( + author='user2', message='message2' + ) + + # Ensure only 1 entry was returned. + self.assertEqual(len(filtered), 1) + + # Ensure the entry returned is the correct entry. + self.assertEqual(filtered[0].author, 'user2') + self.assertEqual(filtered[0].message, 'message2') + + def test_chained_filter(self): + """ + Tests chained filter methods of a List of Embedded Documents + """ + filtered = self.post2.comments.filter(author='user2').filter( + message='message2' + ) + + # Ensure only 1 entry was returned. + self.assertEqual(len(filtered), 1) + + # Ensure the entry returned is the correct entry. + self.assertEqual(filtered[0].author, 'user2') + self.assertEqual(filtered[0].message, 'message2') + + def test_unknown_keyword_filter(self): + """ + Tests the filter method of a List of Embedded Documents + when the keyword is not a known keyword. + """ + # < 2.6 Incompatible > + # with self.assertRaises(AttributeError): + # self.post2.comments.filter(year=2) + self.assertRaises(AttributeError, self.post2.comments.filter, year=2) + + def test_no_keyword_exclude(self): + """ + Tests the exclude method of a List of Embedded Documents + with a no keyword. + """ + filtered = self.post1.comments.exclude() + + # Ensure everything was removed + # < 2.6 Incompatible > + # self.assertListEqual(filtered, []) + self.assertEqual(filtered, []) + + def test_single_keyword_exclude(self): + """ + Tests the exclude method of a List of Embedded Documents + with a single keyword. + """ + excluded = self.post1.comments.exclude(author='user1') + + # Ensure only 1 entry was returned. + self.assertEqual(len(excluded), 1) + + # Ensure the entry returned is the correct entry. + self.assertEqual(excluded[0].author, 'user2') + + def test_multi_keyword_exclude(self): + """ + Tests the exclude method of a List of Embedded Documents + with multiple keywords. + """ + excluded = self.post2.comments.exclude( + author='user3', message='message1' + ) + + # Ensure only 2 entries were returned. + self.assertEqual(len(excluded), 2) + + # Ensure the entries returned are the correct entries. + self.assertEqual(excluded[0].author, 'user2') + self.assertEqual(excluded[1].author, 'user2') + + def test_non_matching_exclude(self): + """ + Tests the exclude method of a List of Embedded Documents + when the keyword does not match any entries. + """ + excluded = self.post2.comments.exclude(author='user4') + + # Ensure the 3 entries still exist. + self.assertEqual(len(excluded), 3) + + def test_unknown_keyword_exclude(self): + """ + Tests the exclude method of a List of Embedded Documents + when the keyword is not a known keyword. + """ + # < 2.6 Incompatible > + # with self.assertRaises(AttributeError): + # self.post2.comments.exclude(year=2) + self.assertRaises(AttributeError, self.post2.comments.exclude, year=2) + + def test_chained_filter_exclude(self): + """ + Tests the exclude method after a filter method of a List of + Embedded Documents. + """ + excluded = self.post2.comments.filter(author='user2').exclude( + message='message2' + ) + + # Ensure only 1 entry was returned. + self.assertEqual(len(excluded), 1) + + # Ensure the entry returned is the correct entry. + self.assertEqual(excluded[0].author, 'user2') + self.assertEqual(excluded[0].message, 'message3') + + def test_count(self): + """ + Tests the count method of a List of Embedded Documents. + """ + self.assertEqual(self.post1.comments.count(), 2) + self.assertEqual(self.post1.comments.count(), len(self.post1.comments)) + + def test_filtered_count(self): + """ + Tests the filter + count method of a List of Embedded Documents. + """ + count = self.post1.comments.filter(author='user1').count() + self.assertEqual(count, 1) + + def test_single_keyword_get(self): + """ + Tests the get method of a List of Embedded Documents using a + single keyword. + """ + comment = self.post1.comments.get(author='user1') + + # < 2.6 Incompatible > + # self.assertIsInstance(comment, self.Comments) + self.assertTrue(isinstance(comment, self.Comments)) + self.assertEqual(comment.author, 'user1') + + def test_multi_keyword_get(self): + """ + Tests the get method of a List of Embedded Documents using + multiple keywords. + """ + comment = self.post2.comments.get(author='user2', message='message2') + + # < 2.6 Incompatible > + # self.assertIsInstance(comment, self.Comments) + self.assertTrue(isinstance(comment, self.Comments)) + self.assertEqual(comment.author, 'user2') + self.assertEqual(comment.message, 'message2') + + def test_no_keyword_multiple_return_get(self): + """ + Tests the get method of a List of Embedded Documents without + a keyword to return multiple documents. + """ + # < 2.6 Incompatible > + # with self.assertRaises(MultipleObjectsReturned): + # self.post1.comments.get() + self.assertRaises(MultipleObjectsReturned, self.post1.comments.get) + + def test_keyword_multiple_return_get(self): + """ + Tests the get method of a List of Embedded Documents with a keyword + to return multiple documents. + """ + # < 2.6 Incompatible > + # with self.assertRaises(MultipleObjectsReturned): + # self.post2.comments.get(author='user2') + self.assertRaises( + MultipleObjectsReturned, self.post2.comments.get, author='user2' + ) + + def test_unknown_keyword_get(self): + """ + Tests the get method of a List of Embedded Documents with an + unknown keyword. + """ + # < 2.6 Incompatible > + # with self.assertRaises(AttributeError): + # self.post2.comments.get(year=2020) + self.assertRaises(AttributeError, self.post2.comments.get, year=2020) + + def test_no_result_get(self): + """ + Tests the get method of a List of Embedded Documents where get + returns no results. + """ + # < 2.6 Incompatible > + # with self.assertRaises(DoesNotExist): + # self.post1.comments.get(author='user3') + self.assertRaises( + DoesNotExist, self.post1.comments.get, author='user3' + ) + + def test_first(self): + """ + Tests the first method of a List of Embedded Documents to + ensure it returns the first comment. + """ + comment = self.post1.comments.first() + + # Ensure a Comment object was returned. + # < 2.6 Incompatible > + # self.assertIsInstance(comment, self.Comments) + self.assertTrue(isinstance(comment, self.Comments)) + self.assertEqual(comment, self.post1.comments[0]) + + def test_create(self): + """ + Test the create method of a List of Embedded Documents. + """ + comment = self.post1.comments.create( + author='user4', message='message1' + ) + self.post1.save() + + # Ensure the returned value is the comment object. + # < 2.6 Incompatible > + # self.assertIsInstance(comment, self.Comments) + self.assertTrue(isinstance(comment, self.Comments)) + self.assertEqual(comment.author, 'user4') + self.assertEqual(comment.message, 'message1') + + # Ensure the new comment was actually saved to the database. + # < 2.6 Incompatible > + # self.assertIn( + # comment, + # self.BlogPost.objects(comments__author='user4')[0].comments + # ) + self.assertTrue( + comment in self.BlogPost.objects( + comments__author='user4' + )[0].comments + ) + + def test_filtered_create(self): + """ + Test the create method of a List of Embedded Documents chained + to a call to the filter method. Filtering should have no effect + on creation. + """ + comment = self.post1.comments.filter(author='user1').create( + author='user4', message='message1' + ) + self.post1.save() + + # Ensure the returned value is the comment object. + # < 2.6 Incompatible > + # self.assertIsInstance(comment, self.Comments) + self.assertTrue(isinstance(comment, self.Comments)) + self.assertEqual(comment.author, 'user4') + self.assertEqual(comment.message, 'message1') + + # Ensure the new comment was actually saved to the database. + # < 2.6 Incompatible > + # self.assertIn( + # comment, + # self.BlogPost.objects(comments__author='user4')[0].comments + # ) + self.assertTrue( + comment in self.BlogPost.objects( + comments__author='user4' + )[0].comments + ) + + def test_no_keyword_update(self): + """ + Tests the update method of a List of Embedded Documents with + no keywords. + """ + original = list(self.post1.comments) + number = self.post1.comments.update() + self.post1.save() + + # Ensure that nothing was altered. + # < 2.6 Incompatible > + # self.assertIn( + # original[0], + # self.BlogPost.objects(id=self.post1.id)[0].comments + # ) + self.assertTrue( + original[0] in self.BlogPost.objects(id=self.post1.id)[0].comments + ) + + # < 2.6 Incompatible > + # self.assertIn( + # original[1], + # self.BlogPost.objects(id=self.post1.id)[0].comments + # ) + self.assertTrue( + original[1] in self.BlogPost.objects(id=self.post1.id)[0].comments + ) + + # Ensure the method returned 0 as the number of entries + # modified + self.assertEqual(number, 0) + + def test_single_keyword_update(self): + """ + Tests the update method of a List of Embedded Documents with + a single keyword. + """ + number = self.post1.comments.update(author='user4') + self.post1.save() + + comments = self.BlogPost.objects(id=self.post1.id)[0].comments + + # Ensure that the database was updated properly. + self.assertEqual(comments[0].author, 'user4') + self.assertEqual(comments[1].author, 'user4') + + # Ensure the method returned 2 as the number of entries + # modified + self.assertEqual(number, 2) + + def test_save(self): + """ + Tests the save method of a List of Embedded Documents. + """ + comments = self.post1.comments + new_comment = self.Comments(author='user4') + comments.append(new_comment) + comments.save() + + # Ensure that the new comment has been added to the database. + # < 2.6 Incompatible > + # self.assertIn( + # new_comment, + # self.BlogPost.objects(id=self.post1.id)[0].comments + # ) + self.assertTrue( + new_comment in self.BlogPost.objects(id=self.post1.id)[0].comments + ) + + def test_delete(self): + """ + Tests the delete method of a List of Embedded Documents. + """ + number = self.post1.comments.delete() + self.post1.save() + + # Ensure that all the comments under post1 were deleted in the + # database. + # < 2.6 Incompatible > + # self.assertListEqual( + # self.BlogPost.objects(id=self.post1.id)[0].comments, [] + # ) + self.assertEqual( + self.BlogPost.objects(id=self.post1.id)[0].comments, [] + ) + + # Ensure that post1 comments were deleted from the list. + # < 2.6 Incompatible > + # self.assertListEqual(self.post1.comments, []) + self.assertEqual(self.post1.comments, []) + + # Ensure that comments still returned a EmbeddedDocumentList object. + # < 2.6 Incompatible > + # self.assertIsInstance(self.post1.comments, EmbeddedDocumentList) + self.assertTrue(isinstance(self.post1.comments, EmbeddedDocumentList)) + + # Ensure that the delete method returned 2 as the number of entries + # deleted from the database + self.assertEqual(number, 2) + + def test_filtered_delete(self): + """ + Tests the delete method of a List of Embedded Documents + after the filter method has been called. + """ + comment = self.post1.comments[1] + number = self.post1.comments.filter(author='user2').delete() + self.post1.save() + + # Ensure that only the user2 comment was deleted. + # < 2.6 Incompatible > + # self.assertNotIn( + # comment, self.BlogPost.objects(id=self.post1.id)[0].comments + # ) + self.assertTrue( + comment not in self.BlogPost.objects(id=self.post1.id)[0].comments + ) + self.assertEqual( + len(self.BlogPost.objects(id=self.post1.id)[0].comments), 1 + ) + + # Ensure that the user2 comment no longer exists in the list. + # < 2.6 Incompatible > + # self.assertNotIn(comment, self.post1.comments) + self.assertTrue(comment not in self.post1.comments) + self.assertEqual(len(self.post1.comments), 1) + + # Ensure that the delete method returned 1 as the number of entries + # deleted from the database + self.assertEqual(number, 1) + if __name__ == '__main__': unittest.main() From 47f995bda3dd3525478b4743443ba2dc9b553e65 Mon Sep 17 00:00:00 2001 From: Vladimir Rutsky Date: Fri, 20 Feb 2015 19:41:20 +0300 Subject: [PATCH 0070/1519] fix typo: "a the structure" -> "the structure" --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 0c592a0d6..311d2888e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -65,7 +65,7 @@ which fields a :class:`User` may have, and what types of data they might store:: first_name = StringField(max_length=50) last_name = StringField(max_length=50) -This looks similar to how a the structure of a table would be defined in a +This looks similar to how the structure of a table would be defined in a regular ORM. The key difference is that this schema will never be passed on to MongoDB --- this will only be enforced at the application level, making future changes easy to manage. Also, the User documents will be stored in a From 4ae60da58dff1542c0ce8bc10c62cb4cfbecea5d Mon Sep 17 00:00:00 2001 From: Vladimir Rutsky Date: Fri, 20 Feb 2015 19:59:36 +0300 Subject: [PATCH 0071/1519] fix reference format: "attr:`auto_create_index`" --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 03318c849..c46e4e18f 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -113,7 +113,7 @@ class Document(BaseDocument): a **+** or **-** sign. Automatic index creation can be disabled by specifying - attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to + :attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to False then indexes will not be created by MongoEngine. This is useful in production systems where index creation is performed as part of a deployment system. From e5986e0ae20424c1b97c540ec5cbe630f364b99b Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Fri, 27 Feb 2015 11:18:09 +0200 Subject: [PATCH 0072/1519] Use dict.pop() default argument instead of checking if the key exists ourselves. --- mongoengine/base/document.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 6bf38ee21..07b0a1402 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -411,8 +411,7 @@ def to_json(self, *args, **kwargs): """Converts a document to JSON. :param use_db_field: Set to True by default but enables the output of the json structure with the field names and not the mongodb store db_names in case of set to False """ - use_db_field = kwargs.pop('use_db_field') if kwargs.has_key( - 'use_db_field') else True + use_db_field = kwargs.pop('use_db_field', True) return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) @classmethod From 4bd72ebc636c84f2c92ffe0e8e8f7dfbe43e4b19 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Fri, 27 Feb 2015 11:32:06 +0200 Subject: [PATCH 0073/1519] Use None instead of mutable arguments. --- mongoengine/base/document.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 6bf38ee21..5bc848f5b 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -297,10 +297,13 @@ def get_text_score(self): return self._data['_text_score'] - def to_mongo(self, use_db_field=True, fields=[]): + def to_mongo(self, use_db_field=True, fields=None): """ Return as SON data ready for use with MongoDB. """ + if not fields: + fields = [] + data = SON() data["_id"] = None data['_cls'] = self._class_name @@ -652,9 +655,11 @@ def _get_collection_name(cls): return cls._meta.get('collection', None) @classmethod - def _from_son(cls, son, _auto_dereference=True, only_fields=[], created=False): + def _from_son(cls, son, _auto_dereference=True, only_fields=None, created=False): """Create an instance of a Document (subclass) from a PyMongo SON. """ + if not only_fields: + only_fields = [] # get the class name from the document, falling back to the given # class if unavailable From 9ae2c731ed075929b6046b699a2cc355d03ff0a6 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 3 Mar 2015 14:30:09 -0800 Subject: [PATCH 0074/1519] dont drop any system collections --- mongoengine/django/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/django/tests.py b/mongoengine/django/tests.py index d7fc3939d..b130acc82 100644 --- a/mongoengine/django/tests.py +++ b/mongoengine/django/tests.py @@ -23,7 +23,7 @@ def __init__(self, methodName='runtest'): def dropCollections(self): for collection in self.db.collection_names(): - if collection == 'system.indexes': + if collection.startswith('system.'): continue self.db.drop_collection(collection) From 82a9e43b6f0ee4b29c2bf6801008a9a3e62ec386 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 18 Mar 2015 10:34:46 +0000 Subject: [PATCH 0075/1519] Updated travis build Fixed pymongo versions to 2.7.2 and 2.8 The dev build for pymongo is the master branch which points to the new 3.0 driver. Support for 3.0 will need to be added separately in the future as its a major version change and will contain some backwards breaking changes --- .travis.yml | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d03faecc..e8e6eec24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,33 +9,25 @@ python: - "pypy" - "pypy3" env: - - PYMONGO=dev DJANGO=dev - - PYMONGO=dev DJANGO=1.7.1 - - PYMONGO=dev DJANGO=1.6.8 - - PYMONGO=dev DJANGO=1.5.11 - - PYMONGO=2.7.1 DJANGO=dev - - PYMONGO=2.7.1 DJANGO=1.7.1 - - PYMONGO=2.7.1 DJANGO=1.6.8 - - PYMONGO=2.7.1 DJANGO=1.5.11 - PYMONGO=2.7.2 DJANGO=dev - PYMONGO=2.7.2 DJANGO=1.7.1 - PYMONGO=2.7.2 DJANGO=1.6.8 - PYMONGO=2.7.2 DJANGO=1.5.11 + - PYMONGO=2.8 DJANGO=dev + - PYMONGO=2.8 DJANGO=1.7.1 + - PYMONGO=2.8 DJANGO=1.6.8 + - PYMONGO=2.8 DJANGO=1.5.11 matrix: exclude: - - python: "2.6" - env: PYMONGO=dev DJANGO=dev - - python: "2.6" - env: PYMONGO=2.7.1 DJANGO=dev - python: "2.6" env: PYMONGO=2.7.2 DJANGO=dev - python: "2.6" - env: PYMONGO=dev DJANGO=1.7.1 - - python: "2.6" - env: PYMONGO=2.7.1 DJANGO=1.7.1 + env: PYMONGO=2.8 DJANGO=dev - python: "2.6" env: PYMONGO=2.7.2 DJANGO=1.7.1 + - python: "2.6" + env: PYMONGO=2.8 DJANGO=1.7.1 allow_failures: - python: "pypy3" fast_finish: true From bcd1841f7163d796d12d12f1bf59cc6a1b5fae63 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 23 Mar 2015 11:52:24 +0200 Subject: [PATCH 0076/1519] Enabled to release mongoengine to PyPi when tagging. --- .travis.yml | 114 ++++++++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/.travis.yml b/.travis.yml index e8e6eec24..474836720 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,63 +1,71 @@ -# http://travis-ci.org/#!/MongoEngine/mongoengine language: python python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" - - "pypy" - - "pypy3" +- '2.6' +- '2.7' +- '3.2' +- '3.3' +- '3.4' +- pypy +- pypy3 env: - - PYMONGO=2.7.2 DJANGO=dev - - PYMONGO=2.7.2 DJANGO=1.7.1 - - PYMONGO=2.7.2 DJANGO=1.6.8 - - PYMONGO=2.7.2 DJANGO=1.5.11 - - PYMONGO=2.8 DJANGO=dev - - PYMONGO=2.8 DJANGO=1.7.1 - - PYMONGO=2.8 DJANGO=1.6.8 - - PYMONGO=2.8 DJANGO=1.5.11 - +- PYMONGO=2.7.2 DJANGO=dev +- PYMONGO=2.7.2 DJANGO=1.7.1 +- PYMONGO=2.7.2 DJANGO=1.6.8 +- PYMONGO=2.7.2 DJANGO=1.5.11 +- PYMONGO=2.8 DJANGO=dev +- PYMONGO=2.8 DJANGO=1.7.1 +- PYMONGO=2.8 DJANGO=1.6.8 +- PYMONGO=2.8 DJANGO=1.5.11 matrix: - exclude: - - python: "2.6" - env: PYMONGO=2.7.2 DJANGO=dev - - python: "2.6" - env: PYMONGO=2.8 DJANGO=dev - - python: "2.6" - env: PYMONGO=2.7.2 DJANGO=1.7.1 - - python: "2.6" - env: PYMONGO=2.8 DJANGO=1.7.1 - allow_failures: - - python: "pypy3" - fast_finish: true - + exclude: + - python: '2.6' + env: PYMONGO=2.7.2 DJANGO=dev + - python: '2.6' + env: PYMONGO=2.8 DJANGO=dev + - python: '2.6' + env: PYMONGO=2.7.2 DJANGO=1.7.1 + - python: '2.6' + env: PYMONGO=2.8 DJANGO=1.7.1 + allow_failures: + - python: pypy3 + fast_finish: true before_install: - - "travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10" - - "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list" - - "travis_retry sudo apt-get update" - - "travis_retry sudo apt-get install mongodb-org-server" - +- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 +- echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | + sudo tee /etc/apt/sources.list.d/mongodb.list +- travis_retry sudo apt-get update +- travis_retry sudo apt-get install mongodb-org-server install: - - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - - if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - - if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; fi - - if [[ $DJANGO == 'dev' ]]; then travis_retry pip install git+https://github.com/django/django.git; fi - - if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi - - travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - - travis_retry pip install coveralls - - travis_retry python setup.py install - +- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev + libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev + python-tk +- if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master; + true; fi +- if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; + fi +- if [[ $DJANGO == 'dev' ]]; then travis_retry pip install git+https://github.com/django/django.git; + fi +- if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi +- travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b +- travis_retry pip install coveralls +- travis_retry python setup.py install script: - - travis_retry python setup.py test - - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then 2to3 . -w; fi; - - coverage run --source=mongoengine setup.py test - - coverage report -m - - python benchmark.py -after_script: - coveralls --verbose +- travis_retry python setup.py test +- if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then 2to3 . -w; fi; +- coverage run --source=mongoengine setup.py test +- coverage report -m +- python benchmark.py +after_script: coveralls --verbose notifications: - irc: "irc.freenode.org#mongoengine" + irc: irc.freenode.org#mongoengine branches: only: - - master + - master +deploy: + provider: pypi + user: the_drow + password: + secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek= + on: + tags: true + repo: MongoEngine/mongoengine From 2d075c4dd6e9ca8661b073c024502955391aa38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Fernando=20S=C3=A1nchez?= Date: Wed, 30 Jul 2014 10:55:52 +0200 Subject: [PATCH 0077/1519] Added test for new_file after saved as none. #714 --- tests/fields/file_tests.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 7ae53e8a3..811f17edd 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -114,6 +114,42 @@ class StreamFile(Document): # Ensure deleted file returns None self.assertTrue(result.the_file.read() == None) + def test_file_fields_stream_after_none(self): + """Ensure that a file field can be written to after it has been saved as + None + """ + class StreamFile(Document): + the_file = FileField() + + StreamFile.drop_collection() + + text = b('Hello, World!') + more_text = b('Foo Bar') + content_type = 'text/plain' + + streamfile = StreamFile() + streamfile.save() + streamfile.the_file.new_file() + streamfile.the_file.write(text) + streamfile.the_file.write(more_text) + streamfile.the_file.close() + streamfile.save() + + result = StreamFile.objects.first() + self.assertTrue(streamfile == result) + self.assertEqual(result.the_file.read(), text + more_text) + #self.assertEqual(result.the_file.content_type, content_type) + result.the_file.seek(0) + self.assertEqual(result.the_file.tell(), 0) + self.assertEqual(result.the_file.read(len(text)), text) + self.assertEqual(result.the_file.tell(), len(text)) + self.assertEqual(result.the_file.read(len(more_text)), more_text) + self.assertEqual(result.the_file.tell(), len(text + more_text)) + result.the_file.delete() + + # Ensure deleted file returns None + self.assertTrue(result.the_file.read() == None) + def test_file_fields_set(self): class SetFile(Document): From ce2b148dd2ddd4c64a764debc89f07d776ca8e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Fernando=20S=C3=A1nchez?= Date: Wed, 30 Jul 2014 11:00:04 +0200 Subject: [PATCH 0078/1519] Fixes #714 --- mongoengine/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 3c4d3f276..3c1db4ac3 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1357,6 +1357,7 @@ def get(self, id=None): def new_file(self, **kwargs): self.newfile = self.fs.new_file(**kwargs) self.grid_id = self.newfile._id + self._mark_as_changed() def put(self, file_obj, **kwargs): if self.grid_id: From d13552208717c8a573be0818a05e0c1d9ec5e5a6 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Mon, 23 Mar 2015 15:46:12 +0200 Subject: [PATCH 0079/1519] Adding #714 to changelog and AUTHORS --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 0ed0e0b50..446a34735 100644 --- a/AUTHORS +++ b/AUTHORS @@ -217,3 +217,4 @@ that much better: * Diego Berrocal (https://github.com/cestdiego) * Matthew Ellison (https://github.com/seglberg) * Jimmy Shen (https://github.com/jimmyshen) + * J. Fernando Sánchez (https://github.com/balkian) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9aa6aa3a2..59398339d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Update FileField when creating a new file #714 - Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826 - ComplexDateTimeField should fall back to None when null=True #864 - Request Support for $min, $max Field update operators #863 From c888e461ba290098006131c9a2bd1e2caff74874 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 24 Mar 2015 17:09:26 +0000 Subject: [PATCH 0080/1519] Updated travis.yml to build release tags --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 474836720..38b72cc9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,6 +61,7 @@ notifications: branches: only: - master + - /^v.*$/ deploy: provider: pypi user: the_drow From cbd2a44350a83eaf06e5aa23cf813ab5460d5395 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sat, 28 Mar 2015 11:34:22 +0300 Subject: [PATCH 0081/1519] Changed an invalid classifier to a valid one. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f0819fbbf..88519788e 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def get_version(version_tuple): 'Operating System :: OS Independent', 'Programming Language :: Python', "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6.6", + "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", From de0e5583a558238386043ebc25e3863a32190f51 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Sun, 29 Mar 2015 09:28:26 +0300 Subject: [PATCH 0082/1519] Fix #595: Support += and *= for ListField --- docs/changelog.rst | 4 ++++ mongoengine/base/datastructures.py | 8 ++++++++ tests/fields/fields.py | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 59398339d..8f5904b89 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,10 @@ Changelog Changes in 0.9.X - DEV ====================== +- Support += and *= for ListField #595 + +Changes in 0.9.0 +================ - Update FileField when creating a new file #714 - Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826 - ComplexDateTimeField should fall back to None when null=True #864 diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index bac67ddca..c9feb71a1 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -156,6 +156,14 @@ def __setstate__(self, state): self = state return self + def __iadd__(self, other): + self._mark_as_changed() + return super(BaseList, self).__iadd__(other) + + def __imul__(self, other): + self._mark_as_changed() + return super(BaseList, self).__imul__(other) + def append(self, *args, **kwargs): self._mark_as_changed() return super(BaseList, self).append(*args, **kwargs) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 64898995f..5e7f34b58 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -946,6 +946,18 @@ class BlogPost(Document): BlogPost.objects.filter(info__0__test__exact='5').count(), 0) self.assertEqual( BlogPost.objects.filter(info__100__test__exact='test').count(), 0) + + post = BlogPost() + post.info = ['1', '2'] + post.save() + post = BlogPost.objects(info=['1', '2']).get() + post.info += ['3', '4'] + post.save() + self.assertEqual(BlogPost.objects(info=['1', '2', '3', '4']).count(), 1) + post = BlogPost.objects(info=['1', '2', '3', '4']).get() + post.info *= 2 + post.save() + self.assertEqual(BlogPost.objects(info=['1', '2', '3', '4', '1', '2', '3', '4']).count(), 1) BlogPost.drop_collection() def test_list_field_passed_in_value(self): From bb77838b3e7f1bd98ff58bf12760c4cdb8845b86 Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Mon, 30 Mar 2015 15:28:28 +0300 Subject: [PATCH 0083/1519] fix-#914: ListField of embedded docs doesn't set the _instance attribute when iterating over it --- docs/changelog.rst | 1 + mongoengine/base/datastructures.py | 4 ++++ tests/document/instance.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8f5904b89..72d2b49cd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 - Support += and *= for ListField #595 Changes in 0.9.0 diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index c9feb71a1..3f7354a3a 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -125,6 +125,10 @@ def __getitem__(self, key, *args, **kwargs): value._instance = self._instance return value + def __iter__(self): + for i in xrange(self.__len__()): + yield self[i] + def __setitem__(self, key, value, *args, **kwargs): if isinstance(key, slice): self._mark_as_changed() diff --git a/tests/document/instance.py b/tests/document/instance.py index eb887dced..8704bad9b 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2799,5 +2799,21 @@ class Person(Document): self.assertNotEqual(p, p1) self.assertEqual(p, p) + def test_list_iter(self): + # 914 + class B(EmbeddedDocument): + v = StringField() + + class A(Document): + l = ListField(EmbeddedDocumentField(B)) + + A.objects.delete() + A(l=[B(v='1'), B(v='2'), B(v='3')]).save() + a = A.objects.get() + self.assertEqual(a.l._instance, a) + for idx, b in enumerate(a.l): + self.assertEqual(b._instance, a) + self.assertEqual(idx, 2) + if __name__ == '__main__': unittest.main() From 1ce6a7f4beaee3237a01ab31c20d141815c781e4 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 8 Apr 2015 10:49:23 +0100 Subject: [PATCH 0084/1519] Update upgrade.rst Add note to the upgrade guide about 0.8.7 bad package issue. #919 #929 --- docs/upgrade.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 25f67c7f2..2648c4bbc 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -2,6 +2,18 @@ Upgrading ######### +0.9.0 +***** + +The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: + +.. code-block:: + + pip uninstall pymongo + pip uninstall mongoengine + pip install pymongo==2.8 + pip install mongoengine + 0.8.7 ***** From 7a749b88c7e9bcbaf083f520163f952317c713b2 Mon Sep 17 00:00:00 2001 From: mrigal Date: Wed, 10 Dec 2014 16:44:16 +0100 Subject: [PATCH 0085/1519] added new test like defined in issue #712 and changed ObjectIdField to_python() method to use a try except similar to other Field classes --- mongoengine/base/fields.py | 7 +++++-- tests/queryset/queryset.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index aa16804e6..bee02879f 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -410,8 +410,11 @@ class ObjectIdField(BaseField): """ def to_python(self, value): - if not isinstance(value, ObjectId): - value = ObjectId(value) + try: + if not isinstance(value, ObjectId): + value = ObjectId(value) + except: + pass return value def to_mongo(self, value): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 6cbac4956..5c721f0b1 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4578,6 +4578,13 @@ class ScottishCat(Cat): self.assertEquals(Animal.objects(folded_ears=True).count(), 1) self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1) + def test_loop_via_invalid_id_does_not_crash(self): + class Person(Document): + name = StringField() + Person.objects.delete() + Person._get_collection().update({"name": "a"}, {"$set": {"_id": ""}}, upsert=True) + for p in Person.objects(): + self.assertEqual(p.name, 'a') if __name__ == '__main__': unittest.main() From 7fff635a3f2bd4957ad406f433dc942d2acec357 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Wed, 8 Apr 2015 11:29:55 -0500 Subject: [PATCH 0086/1519] Remove support to PIL. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8bc575ee2..1d01d8b72 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ Dependencies Optional Dependencies --------------------- - **Django Integration:** Django>=1.4.0 for Python 2.x or PyPy and Django>=1.5.0 for Python 3.x -- **Image Fields**: Pillow>=2.0.0 or PIL (not recommended since MongoEngine is tested with Pillow) +- **Image Fields**: Pillow>=2.0.0 - dateutil>=2.1.0 .. note From a6a7cba12142f58c804704572f3740869b57b4c7 Mon Sep 17 00:00:00 2001 From: Michael Chase Date: Wed, 8 Apr 2015 19:40:43 -0700 Subject: [PATCH 0087/1519] Current class fields when unpickling. Fixes #888 Optimize dereferencing map by using sets. --- AUTHORS | 1 + docs/changelog.rst | 2 ++ mongoengine/base/document.py | 7 ++++++- mongoengine/dereference.py | 16 ++++++++-------- tests/document/instance.py | 24 ++++++++++++++++++++++++ tests/fixtures.py | 9 +++++++++ 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/AUTHORS b/AUTHORS index 446a34735..1880cade5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -218,3 +218,4 @@ that much better: * Matthew Ellison (https://github.com/seglberg) * Jimmy Shen (https://github.com/jimmyshen) * J. Fernando Sánchez (https://github.com/balkian) + * Michael Chase (https://github.com/rxsegrxup) diff --git a/docs/changelog.rst b/docs/changelog.rst index 72d2b49cd..90f1138db 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ Changes in 0.9.X - DEV ====================== - ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 - Support += and *= for ListField #595 +- Use sets for populating dbrefs to dereference +- Fixed unpickled documents replacing the global field's list. #888 Changes in 0.9.0 ================ diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index bcd761720..bd9e6ba64 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -206,7 +206,12 @@ def __setstate__(self, data): if k in data: setattr(self, k, data[k]) if '_fields_ordered' in data: - setattr(type(self), '_fields_ordered', data['_fields_ordered']) + if self._dynamic: + setattr(self, '_fields_ordered', data['_fields_ordered']) + else: + _super_fields_ordered = type(self)._fields_ordered + setattr(self, '_fields_ordered', _super_fields_ordered) + dynamic_fields = data.get('_dynamic_fields') or SON() for k in dynamic_fields.keys(): setattr(self, k, data["_data"].get(k)) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 415d56782..453ba14f9 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -102,24 +102,24 @@ def _find_references(self, items, depth=0): for field_name, field in item._fields.iteritems(): v = item._data.get(field_name, None) if isinstance(v, (DBRef)): - reference_map.setdefault(field.document_type, []).append(v.id) + reference_map.setdefault(field.document_type, set()).add(v.id) elif isinstance(v, (dict, SON)) and '_ref' in v: - reference_map.setdefault(get_document(v['_cls']), []).append(v['_ref'].id) + reference_map.setdefault(get_document(v['_cls']), set()).add(v['_ref'].id) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: field_cls = getattr(getattr(field, 'field', None), 'document_type', None) references = self._find_references(v, depth) for key, refs in references.iteritems(): if isinstance(field_cls, (Document, TopLevelDocumentMetaclass)): key = field_cls - reference_map.setdefault(key, []).extend(refs) + reference_map.setdefault(key, set()).update(refs) elif isinstance(item, (DBRef)): - reference_map.setdefault(item.collection, []).append(item.id) + reference_map.setdefault(item.collection, set()).add(item.id) elif isinstance(item, (dict, SON)) and '_ref' in item: - reference_map.setdefault(get_document(item['_cls']), []).append(item['_ref'].id) + reference_map.setdefault(get_document(item['_cls']), set()).add(item['_ref'].id) elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth: references = self._find_references(item, depth - 1) for key, refs in references.iteritems(): - reference_map.setdefault(key, []).extend(refs) + reference_map.setdefault(key, set()).update(refs) return reference_map @@ -128,8 +128,8 @@ def _fetch_objects(self, doc_type=None): """ object_map = {} for collection, dbrefs in self.reference_map.iteritems(): - keys = object_map.keys() - refs = list(set([dbref for dbref in dbrefs if unicode(dbref).encode('utf-8') not in keys])) + refs = [dbref for dbref in dbrefs + if unicode(dbref).encode('utf-8') not in object_map] if hasattr(collection, 'objects'): # We have a document class for the refs references = collection.objects.in_bulk(refs) for key, doc in references.iteritems(): diff --git a/tests/document/instance.py b/tests/document/instance.py index 8704bad9b..10e38d496 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -10,6 +10,7 @@ from datetime import datetime from bson import DBRef, ObjectId +from tests import fixtures from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, PickleDyanmicEmbedded, PickleDynamicTest) @@ -2085,6 +2086,29 @@ def test_picklable(self): self.assertEqual(pickle_doc.string, "Two") self.assertEqual(pickle_doc.lists, ["1", "2", "3"]) + def test_regular_document_pickle(self): + + pickle_doc = PickleTest(number=1, string="One", lists=['1', '2']) + pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved + pickle_doc.save() + + pickled_doc = pickle.dumps(pickle_doc) + + # Test that when a document's definition changes the new + # definition is used + fixtures.PickleTest = fixtures.NewDocumentPickleTest + + resurrected = pickle.loads(pickled_doc) + self.assertEqual(resurrected.__class__, + fixtures.NewDocumentPickleTest) + self.assertEqual(resurrected._fields_ordered, + fixtures.NewDocumentPickleTest._fields_ordered) + self.assertNotEqual(resurrected._fields_ordered, + pickle_doc._fields_ordered) + + # The local PickleTest is still a ref to the original + fixtures.PickleTest = PickleTest + def test_dynamic_document_pickle(self): pickle_doc = PickleDynamicTest( diff --git a/tests/fixtures.py b/tests/fixtures.py index f1344d7cd..b3bf73e88 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -17,6 +17,15 @@ class PickleTest(Document): photo = FileField() +class NewDocumentPickleTest(Document): + number = IntField() + string = StringField(choices=(('One', '1'), ('Two', '2'))) + embedded = EmbeddedDocumentField(PickleEmbedded) + lists = ListField(StringField()) + photo = FileField() + new_field = StringField() + + class PickleDyanmicEmbedded(DynamicEmbeddedDocument): date = DateTimeField(default=datetime.now) From f77f45b70c4f87647b07ac278fe58ebf2fc451c3 Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Wed, 1 Apr 2015 10:10:08 +0500 Subject: [PATCH 0088/1519] _get_changed_fields fix for embedded documents with id field. removed commented out piece of code added author and record to changelog --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/base/document.py | 3 +-- tests/document/delta.py | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1880cade5..0fd40b519 100644 --- a/AUTHORS +++ b/AUTHORS @@ -219,3 +219,4 @@ that much better: * Jimmy Shen (https://github.com/jimmyshen) * J. Fernando Sánchez (https://github.com/balkian) * Michael Chase (https://github.com/rxsegrxup) + * Eremeev Danil (https://github.com/elephanter) diff --git a/docs/changelog.rst b/docs/changelog.rst index 90f1138db..ba952d8e9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -85,6 +85,7 @@ Changes in 0.9.0 - Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791 - Make `in_bulk()` respect `no_dereference()` #775 - Handle None from model __str__; Fixes #753 #754 +- _get_changed_fields fix for embedded documents with id field. #925 Changes in 0.8.7 ================ diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index bd9e6ba64..b23104398 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -308,7 +308,7 @@ def to_mongo(self, use_db_field=True, fields=None): """ if not fields: fields = [] - + data = SON() data["_id"] = None data['_cls'] = self._class_name @@ -553,7 +553,6 @@ def _get_changed_fields(self, inspected=None): if hasattr(data, 'id'): if data.id in inspected: continue - inspected.add(data.id) if isinstance(field, ReferenceField): continue elif (isinstance(data, (EmbeddedDocument, DynamicEmbeddedDocument)) diff --git a/tests/document/delta.py b/tests/document/delta.py index 24910627b..58df2c828 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -93,6 +93,7 @@ def test_delta_recursive(self): def delta_recursive(self, DocClass, EmbeddedClass): class Embedded(EmbeddedClass): + id = StringField() string_field = StringField() int_field = IntField() dict_field = DictField() @@ -114,6 +115,7 @@ class Doc(DocClass): self.assertEqual(doc._delta(), ({}, {})) embedded_1 = Embedded() + embedded_1.id = "010101" embedded_1.string_field = 'hello' embedded_1.int_field = 1 embedded_1.dict_field = {'hello': 'world'} @@ -123,6 +125,7 @@ class Doc(DocClass): self.assertEqual(doc._get_changed_fields(), ['embedded_field']) embedded_delta = { + 'id': "010101", 'string_field': 'hello', 'int_field': 1, 'dict_field': {'hello': 'world'}, From 175659628d2150556ee33e19dec088816802ddbe Mon Sep 17 00:00:00 2001 From: Catstyle Date: Fri, 10 Apr 2015 11:31:31 +0800 Subject: [PATCH 0089/1519] fix mark_as_changed: handle higher/lower level changed fields correctly to avoid conflict update error --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/base/document.py | 14 +++++++++- tests/document/delta.py | 50 ++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 0fd40b519..af87d5039 100644 --- a/AUTHORS +++ b/AUTHORS @@ -220,3 +220,4 @@ that much better: * J. Fernando Sánchez (https://github.com/balkian) * Michael Chase (https://github.com/rxsegrxup) * Eremeev Danil (https://github.com/elephanter) + * Catstyle Lee (https://github.com/Catstyle) diff --git a/docs/changelog.rst b/docs/changelog.rst index ba952d8e9..188247e65 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Fixed mark_as_changed to handle higher/lower level fields changed. #927 - ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 - Support += and *= for ListField #595 - Use sets for populating dbrefs to dereference diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index b23104398..33a6d8265 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -483,7 +483,19 @@ def _mark_as_changed(self, key): key = self._db_field_map.get(key, key) if key not in self._changed_fields: - self._changed_fields.append(key) + levels, idx = key.split('.'), 1 + while idx <= len(levels): + if '.'.join(levels[:idx]) in self._changed_fields: + break + idx += 1 + else: + self._changed_fields.append(key) + # remove lower level changed fields + level = '.'.join(levels[:idx]) + '.' + remove = self._changed_fields.remove + for field in self._changed_fields: + if field.startswith(level): + remove(field) def _clear_changed_fields(self): """Using get_changed_fields iterate and remove any fields that are diff --git a/tests/document/delta.py b/tests/document/delta.py index 58df2c828..7ec6ee56a 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -735,6 +735,56 @@ class MyDoc(Document): mydoc._clear_changed_fields() self.assertEqual([], mydoc._get_changed_fields()) + def test_lower_level_mark_as_changed(self): + class EmbeddedDoc(EmbeddedDocument): + name = StringField() + + class MyDoc(Document): + subs = MapField(EmbeddedDocumentField(EmbeddedDoc)) + + MyDoc.drop_collection() + + MyDoc().save() + + mydoc = MyDoc.objects.first() + mydoc.subs['a'] = EmbeddedDoc() + self.assertEqual(["subs.a"], mydoc._get_changed_fields()) + + subdoc = mydoc.subs['a'] + subdoc.name = 'bar' + + self.assertEqual(["name"], subdoc._get_changed_fields()) + self.assertEqual(["subs.a"], mydoc._get_changed_fields()) + mydoc.save() + + mydoc._clear_changed_fields() + self.assertEqual([], mydoc._get_changed_fields()) + + def test_upper_level_mark_as_changed(self): + class EmbeddedDoc(EmbeddedDocument): + name = StringField() + + class MyDoc(Document): + subs = MapField(EmbeddedDocumentField(EmbeddedDoc)) + + MyDoc.drop_collection() + + MyDoc(subs={'a': EmbeddedDoc(name='foo')}).save() + + mydoc = MyDoc.objects.first() + subdoc = mydoc.subs['a'] + subdoc.name = 'bar' + + self.assertEqual(["name"], subdoc._get_changed_fields()) + self.assertEqual(["subs.a.name"], mydoc._get_changed_fields()) + + mydoc.subs['a'] = EmbeddedDoc() + self.assertEqual(["subs.a"], mydoc._get_changed_fields()) + mydoc.save() + + mydoc._clear_changed_fields() + self.assertEqual([], mydoc._get_changed_fields()) + def test_referenced_object_changed_attributes(self): """Ensures that when you save a new reference to a field, the referenced object isn't altered""" From cd2d9517a034d0534a87520cf84daacad5b07fd3 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Mon, 13 Apr 2015 17:49:08 +0200 Subject: [PATCH 0090/1519] Added 'strict' meta parameter --- mongoengine/base/document.py | 2 +- tests/document/instance.py | 114 ++++++++++++++++++++++++++++++++++- tests/fields/fields.py | 2 +- 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 33a6d8265..0ca3fcdeb 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -72,7 +72,7 @@ def __init__(self, *args, **values): # Check if there are undefined fields supplied, if so raise an # Exception. - if not self._dynamic: + if not self._dynamic and self._meta.get('strict', True): for var in values.keys(): if var not in self._fields.keys() + ['id', 'pk', '_cls', '_text_score']: msg = ( diff --git a/tests/document/instance.py b/tests/document/instance.py index 10e38d496..6a34b749a 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -16,7 +16,8 @@ from mongoengine import * from mongoengine.errors import (NotRegistered, InvalidDocumentError, - InvalidQueryError, NotUniqueError) + InvalidQueryError, NotUniqueError, + FieldDoesNotExist) from mongoengine.queryset import NULLIFY, Q from mongoengine.connection import get_db from mongoengine.base import get_document @@ -2467,6 +2468,117 @@ class Group(Document): group = Group.objects.first() self.assertEqual("hello - default", group.name) + def test_load_undefined_fields(self): + class User(Document): + name = StringField() + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'foo': 'Bar', + 'data': [1, 2, 3] + }) + + with self.assertRaises(FieldDoesNotExist): + User.objects.first() + + def test_load_undefined_fields_with_strict_false(self): + class User(Document): + name = StringField() + + meta = {'strict': False} + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'foo': 'Bar', + 'data': [1, 2, 3] + }) + + user = User.objects.first() + self.assertEqual(user.name, 'John') + self.assertFalse(hasattr(user, 'foo')) + self.assertEqual(user._data['foo'], 'Bar') + self.assertFalse(hasattr(user, 'data')) + self.assertEqual(user._data['data'], [1, 2, 3]) + + def test_load_undefined_fields_on_embedded_document(self): + class Thing(EmbeddedDocument): + name = StringField() + + class User(Document): + name = StringField() + thing = EmbeddedDocumentField(Thing) + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'thing': { + 'name': 'My thing', + 'foo': 'Bar', + 'data': [1, 2, 3] + } + }) + + with self.assertRaises(FieldDoesNotExist): + User.objects.first() + + def test_load_undefined_fields_on_embedded_document_with_strict_false_on_doc(self): + class Thing(EmbeddedDocument): + name = StringField() + + class User(Document): + name = StringField() + thing = EmbeddedDocumentField(Thing) + + meta = {'strict': False} + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'thing': { + 'name': 'My thing', + 'foo': 'Bar', + 'data': [1, 2, 3] + } + }) + + with self.assertRaises(FieldDoesNotExist): + User.objects.first() + + def test_load_undefined_fields_on_embedded_document_with_strict_false(self): + class Thing(EmbeddedDocument): + name = StringField() + + meta = {'strict': False} + + class User(Document): + name = StringField() + thing = EmbeddedDocumentField(Thing) + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'thing': { + 'name': 'My thing', + 'foo': 'Bar', + 'data': [1, 2, 3] + } + }) + + user = User.objects.first() + self.assertEqual(user.name, 'John') + self.assertEqual(user.thing.name, 'My thing') + self.assertFalse(hasattr(user.thing, 'foo')) + self.assertEqual(user.thing._data['foo'], 'Bar') + self.assertFalse(hasattr(user.thing, 'data')) + self.assertEqual(user.thing._data['data'], [1, 2, 3]) + def test_spaces_in_keys(self): class Embedded(DynamicEmbeddedDocument): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5e7f34b58..887291db4 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3160,7 +3160,7 @@ class Doc(Document): def test_undefined_field_exception(self): """Tests if a `FieldDoesNotExist` exception is raised when trying to - set a value to a field that's not defined. + instanciate a document with a field that's not defined. """ class Doc(Document): From 2bfb195ad671aee104111f3fe2a324530df6b1d3 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Mon, 13 Apr 2015 18:07:48 +0200 Subject: [PATCH 0091/1519] Document FieldDoesNotExist and meta.strict --- docs/apireference.rst | 3 +++ mongoengine/document.py | 5 +++++ mongoengine/errors.py | 9 ++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/apireference.rst b/docs/apireference.rst index 7ba93408c..625d4a8bd 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -34,6 +34,9 @@ Documents .. autoclass:: mongoengine.ValidationError :members: +.. autoclass:: mongoengine.FieldDoesNotExist + + Context Managers ================ diff --git a/mongoengine/document.py b/mongoengine/document.py index eea5dabc1..f8275021d 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -135,6 +135,11 @@ class Document(BaseDocument): doesn't contain a list) if allow_inheritance is True. This can be disabled by either setting cls to False on the specific index or by setting index_cls to False on the meta dictionary for the document. + + By default, any extra attribute existing in stored data but not declared + in your model will raise a :class:`~mongoengine.FieldDoesNotExist` error. + This can be disabled by setting :attr:`strict` to ``False`` + in the :attr:`meta` dictionnary. """ # The __metaclass__ attribute is removed by 2to3 when running with Python3 diff --git a/mongoengine/errors.py b/mongoengine/errors.py index 6cde7771e..a411ac47a 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -42,7 +42,14 @@ class NotUniqueError(OperationError): class FieldDoesNotExist(Exception): - pass + """Raised when trying to set a field + not declared in a :class:`~mongoengine.Document` + or an :class:`~mongoengine.EmbeddedDocument`. + + To avoid this behavior on data loading, + you should the :attr:`strict` to ``False`` + in the :attr:`meta` dictionnary. + """ class ValidationError(AssertionError): From ddbcc8e84ba45539a01ab622161e28350268e327 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Mon, 13 Apr 2015 18:48:42 +0200 Subject: [PATCH 0092/1519] Ensure meta.strict does not bypass constructor check --- mongoengine/base/document.py | 6 +++--- tests/fields/fields.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 0ca3fcdeb..d31d75ba9 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -70,9 +70,9 @@ def __init__(self, *args, **values): signals.pre_init.send(self.__class__, document=self, values=values) - # Check if there are undefined fields supplied, if so raise an - # Exception. - if not self._dynamic and self._meta.get('strict', True): + # Check if there are undefined fields supplied to the constructor, + # if so raise an Exception. + if not self._dynamic and (self._meta.get('strict', True) or _created): for var in values.keys(): if var not in self._fields.keys() + ['id', 'pk', '_cls', '_text_score']: msg = ( diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 887291db4..f0a739517 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3171,6 +3171,21 @@ def test(): self.assertRaises(FieldDoesNotExist, test) + def test_undefined_field_exception_with_strict(self): + """Tests if a `FieldDoesNotExist` exception is raised when trying to + instanciate a document with a field that's not defined, + even when strict is set to False. + """ + + class Doc(Document): + foo = StringField(db_field='f') + meta = {'strict': False} + + def test(): + Doc(bar='test') + + self.assertRaises(FieldDoesNotExist, test) + class EmbeddedDocumentListFieldTestCase(unittest.TestCase): From 52c7b68cc31a5de0736d291b51906a0d639c0efc Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Mon, 13 Apr 2015 22:28:58 +0200 Subject: [PATCH 0093/1519] Restore Py26 compatibility on assertRaises --- tests/document/instance.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 6a34b749a..2cfdef652 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2480,8 +2480,7 @@ class User(Document): 'data': [1, 2, 3] }) - with self.assertRaises(FieldDoesNotExist): - User.objects.first() + self.assertRaises(FieldDoesNotExist, User.objects.first) def test_load_undefined_fields_with_strict_false(self): class User(Document): @@ -2523,8 +2522,7 @@ class User(Document): } }) - with self.assertRaises(FieldDoesNotExist): - User.objects.first() + self.assertRaises(FieldDoesNotExist, User.objects.first) def test_load_undefined_fields_on_embedded_document_with_strict_false_on_doc(self): class Thing(EmbeddedDocument): @@ -2547,8 +2545,7 @@ class User(Document): } }) - with self.assertRaises(FieldDoesNotExist): - User.objects.first() + self.assertRaises(FieldDoesNotExist, User.objects.first) def test_load_undefined_fields_on_embedded_document_with_strict_false(self): class Thing(EmbeddedDocument): From 2ffdbc7fc0e01905af8edc284039ab713b43e9ed Mon Sep 17 00:00:00 2001 From: Jimmy Shen Date: Sun, 19 Apr 2015 03:26:14 -0400 Subject: [PATCH 0094/1519] fixed microsecond-level ordering/filtering bug with ComplexDateTimeField as well as unused separator option --- docs/changelog.rst | 1 + mongoengine/fields.py | 41 +++++++++++------------------------------ tests/fields/fields.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 188247e65..3dbcc2f87 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ Changes in 0.9.X - DEV - Support += and *= for ListField #595 - Use sets for populating dbrefs to dereference - Fixed unpickled documents replacing the global field's list. #888 +- Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 Changes in 0.9.0 ================ diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 3c1db4ac3..b5ed8bb2f 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -458,37 +458,22 @@ class ComplexDateTimeField(StringField): """ def __init__(self, separator=',', **kwargs): - self.names = ['year', 'month', 'day', 'hour', 'minute', 'second', - 'microsecond'] - self.separtor = separator + self.names = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] + self.separator = separator + self.format = separator.join(['%Y', '%m', '%d', '%H', '%M', '%S', '%f']) super(ComplexDateTimeField, self).__init__(**kwargs) - def _leading_zero(self, number): - """ - Converts the given number to a string. - - If it has only one digit, a leading zero so as it has always at least - two digits. - """ - if int(number) < 10: - return "0%s" % number - else: - return str(number) - def _convert_from_datetime(self, val): """ Convert a `datetime` object to a string representation (which will be stored in MongoDB). This is the reverse function of `_convert_from_string`. - >>> a = datetime(2011, 6, 8, 20, 26, 24, 192284) - >>> RealDateTimeField()._convert_from_datetime(a) - '2011,06,08,20,26,24,192284' + >>> a = datetime(2011, 6, 8, 20, 26, 24, 92284) + >>> ComplexDateTimeField()._convert_from_datetime(a) + '2011,06,08,20,26,24,092284' """ - data = [] - for name in self.names: - data.append(self._leading_zero(getattr(val, name))) - return ','.join(data) + return val.strftime(self.format) def _convert_from_string(self, data): """ @@ -496,16 +481,12 @@ def _convert_from_string(self, data): will manipulate). This is the reverse function of `_convert_from_datetime`. - >>> a = '2011,06,08,20,26,24,192284' + >>> a = '2011,06,08,20,26,24,092284' >>> ComplexDateTimeField()._convert_from_string(a) - datetime.datetime(2011, 6, 8, 20, 26, 24, 192284) + datetime.datetime(2011, 6, 8, 20, 26, 24, 92284) """ - data = data.split(',') - data = map(int, data) - values = {} - for i in range(7): - values[self.names[i]] = data[i] - return datetime.datetime(**values) + values = map(int, data.split(self.separator)) + return datetime.datetime(*values) def __get__(self, instance, owner): data = super(ComplexDateTimeField, self).__get__(instance, owner) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5e7f34b58..96686a8b7 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -5,6 +5,9 @@ import datetime import unittest import uuid +import math +import itertools +import re try: import dateutil @@ -689,6 +692,7 @@ def test_complexdatetime_storage(self): """ class LogEntry(Document): date = ComplexDateTimeField() + date_with_dots = ComplexDateTimeField(separator='.') LogEntry.drop_collection() @@ -729,6 +733,18 @@ class LogEntry(Document): log1 = LogEntry.objects.get(date=d1) self.assertEqual(log, log1) + # Test string padding + microsecond = map(int, [math.pow(10, x) for x in xrange(6)]) + mm = dd = hh = ii = ss = [1, 10] + + for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond): + stored = LogEntry(date=datetime.datetime(*values)).to_mongo()['date'] + self.assertTrue(re.match('^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$', stored) is not None) + + # Test separator + stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()['date_with_dots'] + self.assertTrue(re.match('^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$', stored) is not None) + LogEntry.drop_collection() def test_complexdatetime_usage(self): @@ -787,6 +803,25 @@ class LogEntry(Document): LogEntry.drop_collection() + # Test microsecond-level ordering/filtering + for microsecond in (99, 999, 9999, 10000): + LogEntry(date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond)).save() + + logs = list(LogEntry.objects.order_by('date')) + for next_idx, log in enumerate(logs[:-1], start=1): + next_log = logs[next_idx] + self.assertTrue(log.date < next_log.date) + + logs = list(LogEntry.objects.order_by('-date')) + for next_idx, log in enumerate(logs[:-1], start=1): + next_log = logs[next_idx] + self.assertTrue(log.date > next_log.date) + + logs = LogEntry.objects.filter(date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000)) + self.assertEqual(logs.count(), 4) + + LogEntry.drop_collection() + def test_list_validation(self): """Ensure that a list field only accepts lists with valid elements. """ From 24a9633edc33872d13c09d30a1062b1f83d15976 Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Mon, 20 Apr 2015 16:03:25 +0800 Subject: [PATCH 0095/1519] Override `authentication_source` by "authSource" in URI --- mongoengine/connection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index dcecdd9ad..5e18efb76 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -1,4 +1,3 @@ -import pymongo from pymongo import MongoClient, MongoReplicaSetClient, uri_parser @@ -58,8 +57,11 @@ def register_connection(alias, name=None, host=None, port=None, 'password': uri_dict.get('password'), 'read_preference': read_preference, }) - if "replicaSet" in conn_settings['host']: + uri_options = uri_dict['options'] + if 'replicaset' in uri_options: conn_settings['replicaSet'] = True + if 'authsource' in uri_options: + conn_settings['authentication_source'] = uri_options['authsource'] # Deprecated parameters that should not be passed on kwargs.pop('slaves', None) From 7530f03bf60d7f7f2cf2eb66b0e275d01dfc623b Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Fri, 24 Apr 2015 13:36:41 -0400 Subject: [PATCH 0096/1519] Removed Django Support from MongoEngine Django support has now been split out of MongoEngine and will be revisted as a new but separate module. Closes #958 --- .travis.yml | 21 +- docs/changelog.rst | 1 + docs/django.rst | 173 +-------- mongoengine/__init__.py | 1 - mongoengine/django/__init__.py | 0 mongoengine/django/auth.py | 412 ---------------------- mongoengine/django/mongo_auth/__init__.py | 0 mongoengine/django/mongo_auth/models.py | 119 ------- mongoengine/django/sessions.py | 124 ------- mongoengine/django/shortcuts.py | 47 --- mongoengine/django/storage.py | 112 ------ mongoengine/django/tests.py | 31 -- mongoengine/django/utils.py | 6 - setup.py | 4 +- tests/test_django.py | 330 ----------------- tests/test_jinja.py | 47 --- 16 files changed, 10 insertions(+), 1418 deletions(-) delete mode 100644 mongoengine/django/__init__.py delete mode 100644 mongoengine/django/auth.py delete mode 100644 mongoengine/django/mongo_auth/__init__.py delete mode 100644 mongoengine/django/mongo_auth/models.py delete mode 100644 mongoengine/django/sessions.py delete mode 100644 mongoengine/django/shortcuts.py delete mode 100644 mongoengine/django/storage.py delete mode 100644 mongoengine/django/tests.py delete mode 100644 mongoengine/django/utils.py delete mode 100644 tests/test_django.py delete mode 100644 tests/test_jinja.py diff --git a/.travis.yml b/.travis.yml index 38b72cc9a..37ec8cf72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,24 +8,18 @@ python: - pypy - pypy3 env: -- PYMONGO=2.7.2 DJANGO=dev -- PYMONGO=2.7.2 DJANGO=1.7.1 -- PYMONGO=2.7.2 DJANGO=1.6.8 -- PYMONGO=2.7.2 DJANGO=1.5.11 -- PYMONGO=2.8 DJANGO=dev -- PYMONGO=2.8 DJANGO=1.7.1 -- PYMONGO=2.8 DJANGO=1.6.8 -- PYMONGO=2.8 DJANGO=1.5.11 +- PYMONGO=2.7.2 +- PYMONGO=2.8 matrix: exclude: - python: '2.6' - env: PYMONGO=2.7.2 DJANGO=dev + env: PYMONGO=2.7.2 - python: '2.6' - env: PYMONGO=2.8 DJANGO=dev + env: PYMONGO=2.8 - python: '2.6' - env: PYMONGO=2.7.2 DJANGO=1.7.1 + env: PYMONGO=2.7.2 - python: '2.6' - env: PYMONGO=2.8 DJANGO=1.7.1 + env: PYMONGO=2.8 allow_failures: - python: pypy3 fast_finish: true @@ -43,9 +37,6 @@ install: true; fi - if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; fi -- if [[ $DJANGO == 'dev' ]]; then travis_retry pip install git+https://github.com/django/django.git; - fi -- if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi - travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - travis_retry pip install coveralls - travis_retry python setup.py install diff --git a/docs/changelog.rst b/docs/changelog.rst index 3dbcc2f87..5617a5b11 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ Changes in 0.9.X - DEV - Use sets for populating dbrefs to dereference - Fixed unpickled documents replacing the global field's list. #888 - Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 +- Django support was removed and will be available as a separate extension. #958 Changes in 0.9.0 ================ diff --git a/docs/django.rst b/docs/django.rst index 5d2423f23..4b9a04bdb 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -2,176 +2,5 @@ Django Support ============== -.. note:: Updated to support Django 1.5 - -Connecting -========== -In your **settings.py** file, ignore the standard database settings (unless you -also plan to use the ORM in your project), and instead call -:func:`~mongoengine.connect` somewhere in the settings module. - -.. note:: - If you are not using another Database backend you may need to add a dummy - database backend to ``settings.py`` eg:: - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.dummy' - } - } - -Authentication -============== -MongoEngine includes a Django authentication backend, which uses MongoDB. The -:class:`~mongoengine.django.auth.User` model is a MongoEngine -:class:`~mongoengine.Document`, but implements most of the methods and -attributes that the standard Django :class:`User` model does - so the two are -moderately compatible. Using this backend will allow you to store users in -MongoDB but still use many of the Django authentication infrastructure (such as -the :func:`login_required` decorator and the :func:`authenticate` function). To -enable the MongoEngine auth backend, add the following to your **settings.py** -file:: - - AUTHENTICATION_BACKENDS = ( - 'mongoengine.django.auth.MongoEngineBackend', - ) - -The :mod:`~mongoengine.django.auth` module also contains a -:func:`~mongoengine.django.auth.get_user` helper function, that takes a user's -:attr:`id` and returns a :class:`~mongoengine.django.auth.User` object. - -.. versionadded:: 0.1.3 - -Custom User model -================= -Django 1.5 introduced `Custom user Models -`_ -which can be used as an alternative to the MongoEngine authentication backend. - -The main advantage of this option is that other components relying on -:mod:`django.contrib.auth` and supporting the new swappable user model are more -likely to work. For example, you can use the ``createsuperuser`` management -command as usual. - -To enable the custom User model in Django, add ``'mongoengine.django.mongo_auth'`` -in your ``INSTALLED_APPS`` and set ``'mongo_auth.MongoUser'`` as the custom user -user model to use. In your **settings.py** file you will have:: - - INSTALLED_APPS = ( - ... - 'django.contrib.auth', - 'mongoengine.django.mongo_auth', - ... - ) - - AUTH_USER_MODEL = 'mongo_auth.MongoUser' - -An additional ``MONGOENGINE_USER_DOCUMENT`` setting enables you to replace the -:class:`~mongoengine.django.auth.User` class with another class of your choice:: - - MONGOENGINE_USER_DOCUMENT = 'mongoengine.django.auth.User' - -The custom :class:`User` must be a :class:`~mongoengine.Document` class, but -otherwise has the same requirements as a standard custom user model, -as specified in the `Django Documentation -`_. -In particular, the custom class must define :attr:`USERNAME_FIELD` and -:attr:`REQUIRED_FIELDS` attributes. - -Sessions -======== -Django allows the use of different backend stores for its sessions. MongoEngine -provides a MongoDB-based session backend for Django, which allows you to use -sessions in your Django application with just MongoDB. To enable the MongoEngine -session backend, ensure that your settings module has -``'django.contrib.sessions.middleware.SessionMiddleware'`` in the -``MIDDLEWARE_CLASSES`` field and ``'django.contrib.sessions'`` in your -``INSTALLED_APPS``. From there, all you need to do is add the following line -into your settings module:: - - SESSION_ENGINE = 'mongoengine.django.sessions' - SESSION_SERIALIZER = 'mongoengine.django.sessions.BSONSerializer' - -Django provides session cookie, which expires after ```SESSION_COOKIE_AGE``` seconds, but doesn't delete cookie at sessions backend, so ``'mongoengine.django.sessions'`` supports `mongodb TTL -`_. - -.. note:: ``SESSION_SERIALIZER`` is only necessary in Django 1.6 as the default - serializer is based around JSON and doesn't know how to convert - ``bson.objectid.ObjectId`` instances to strings. - -.. versionadded:: 0.2.1 - -Storage -======= -With MongoEngine's support for GridFS via the :class:`~mongoengine.fields.FileField`, -it is useful to have a Django file storage backend that wraps this. The new -storage module is called :class:`~mongoengine.django.storage.GridFSStorage`. -Using it is very similar to using the default FileSystemStorage.:: - - from mongoengine.django.storage import GridFSStorage - fs = GridFSStorage() - - filename = fs.save('hello.txt', 'Hello, World!') - -All of the `Django Storage API methods -`_ have been -implemented except :func:`path`. If the filename provided already exists, an -underscore and a number (before # the file extension, if one exists) will be -appended to the filename until the generated filename doesn't exist. The -:func:`save` method will return the new filename.:: - - >>> fs.exists('hello.txt') - True - >>> fs.open('hello.txt').read() - 'Hello, World!' - >>> fs.size('hello.txt') - 13 - >>> fs.url('http://github.com/hello.txt') - 'http://your_media_url/hello.txt' - >>> fs.open('hello.txt').name - 'hello.txt' - >>> fs.listdir() - ([], [u'hello.txt']) - -All files will be saved and retrieved in GridFS via the :class:`FileDocument` -document, allowing easy access to the files without the GridFSStorage -backend.:: - - >>> from mongoengine.django.storage import FileDocument - >>> FileDocument.objects() - [] - -.. versionadded:: 0.4 - -Shortcuts -========= -Inspired by the `Django shortcut get_object_or_404 -`_, -the :func:`~mongoengine.django.shortcuts.get_document_or_404` method returns -a document or raises an Http404 exception if the document does not exist:: - - from mongoengine.django.shortcuts import get_document_or_404 - - admin_user = get_document_or_404(User, username='root') - -The first argument may be a Document or QuerySet object. All other passed arguments -and keyword arguments are used in the query:: - - foo_email = get_document_or_404(User.objects.only('email'), username='foo', is_active=True).email - -.. note:: Like with :func:`get`, a MultipleObjectsReturned will be raised if more than one - object is found. - - -Also inspired by the `Django shortcut get_list_or_404 -`_, -the :func:`~mongoengine.django.shortcuts.get_list_or_404` method returns a list of -documents or raises an Http404 exception if the list is empty:: - - from mongoengine.django.shortcuts import get_list_or_404 - - active_users = get_list_or_404(User, is_active=True) - -The first argument may be a Document or QuerySet object. All other passed -arguments and keyword arguments are used to filter the query. +.. note:: Django support has been split from MongoEngine and will be available on Github as a separate module. `See Here `_. diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index b215181ad..f1b2b30d8 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -10,7 +10,6 @@ from signals import * from errors import * import errors -import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) diff --git a/mongoengine/django/__init__.py b/mongoengine/django/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py deleted file mode 100644 index 0a309c4cd..000000000 --- a/mongoengine/django/auth.py +++ /dev/null @@ -1,412 +0,0 @@ -from mongoengine import * - -from django.utils.encoding import smart_str -from django.contrib.auth.models import _user_has_perm, _user_get_all_permissions, _user_has_module_perms -from django.db import models -from django.contrib.contenttypes.models import ContentTypeManager -from django.contrib import auth -from django.contrib.auth.models import AnonymousUser -from django.utils.translation import ugettext_lazy as _ - -from .utils import datetime_now - -REDIRECT_FIELD_NAME = 'next' - -try: - from django.contrib.auth.hashers import check_password, make_password -except ImportError: - """Handle older versions of Django""" - from django.utils.hashcompat import md5_constructor, sha_constructor - - def get_hexdigest(algorithm, salt, raw_password): - raw_password, salt = smart_str(raw_password), smart_str(salt) - if algorithm == 'md5': - return md5_constructor(salt + raw_password).hexdigest() - elif algorithm == 'sha1': - return sha_constructor(salt + raw_password).hexdigest() - raise ValueError('Got unknown password algorithm type in password') - - def check_password(raw_password, password): - algo, salt, hash = password.split('$') - return hash == get_hexdigest(algo, salt, raw_password) - - def make_password(raw_password): - from random import random - algo = 'sha1' - salt = get_hexdigest(algo, str(random()), str(random()))[:5] - hash = get_hexdigest(algo, salt, raw_password) - return '%s$%s$%s' % (algo, salt, hash) - - -class ContentType(Document): - name = StringField(max_length=100) - app_label = StringField(max_length=100) - model = StringField(max_length=100, verbose_name=_('python model class name'), - unique_with='app_label') - objects = ContentTypeManager() - - class Meta: - verbose_name = _('content type') - verbose_name_plural = _('content types') - # db_table = 'django_content_type' - # ordering = ('name',) - # unique_together = (('app_label', 'model'),) - - def __unicode__(self): - return self.name - - def model_class(self): - "Returns the Python model class for this type of content." - from django.db import models - return models.get_model(self.app_label, self.model) - - def get_object_for_this_type(self, **kwargs): - """ - Returns an object of this type for the keyword arguments given. - Basically, this is a proxy around this object_type's get_object() model - method. The ObjectNotExist exception, if thrown, will not be caught, - so code that calls this method should catch it. - """ - return self.model_class()._default_manager.using(self._state.db).get(**kwargs) - - def natural_key(self): - return (self.app_label, self.model) - - -class SiteProfileNotAvailable(Exception): - pass - - -class PermissionManager(models.Manager): - def get_by_natural_key(self, codename, app_label, model): - return self.get( - codename=codename, - content_type=ContentType.objects.get_by_natural_key(app_label, model) - ) - - -class Permission(Document): - """The permissions system provides a way to assign permissions to specific - users and groups of users. - - The permission system is used by the Django admin site, but may also be - useful in your own code. The Django admin site uses permissions as follows: - - - The "add" permission limits the user's ability to view the "add" - form and add an object. - - The "change" permission limits a user's ability to view the change - list, view the "change" form and change an object. - - The "delete" permission limits the ability to delete an object. - - Permissions are set globally per type of object, not per specific object - instance. It is possible to say "Mary may change news stories," but it's - not currently possible to say "Mary may change news stories, but only the - ones she created herself" or "Mary may only change news stories that have - a certain status or publication date." - - Three basic permissions -- add, change and delete -- are automatically - created for each Django model. - """ - name = StringField(max_length=50, verbose_name=_('username')) - content_type = ReferenceField(ContentType) - codename = StringField(max_length=100, verbose_name=_('codename')) - # FIXME: don't access field of the other class - # unique_with=['content_type__app_label', 'content_type__model']) - - objects = PermissionManager() - - class Meta: - verbose_name = _('permission') - verbose_name_plural = _('permissions') - # unique_together = (('content_type', 'codename'),) - # ordering = ('content_type__app_label', 'content_type__model', 'codename') - - def __unicode__(self): - return u"%s | %s | %s" % ( - unicode(self.content_type.app_label), - unicode(self.content_type), - unicode(self.name)) - - def natural_key(self): - return (self.codename,) + self.content_type.natural_key() - natural_key.dependencies = ['contenttypes.contenttype'] - - -class Group(Document): - """Groups are a generic way of categorizing users to apply permissions, - or some other label, to those users. A user can belong to any number of - groups. - - A user in a group automatically has all the permissions granted to that - group. For example, if the group Site editors has the permission - can_edit_home_page, any user in that group will have that permission. - - Beyond permissions, groups are a convenient way to categorize users to - apply some label, or extended functionality, to them. For example, you - could create a group 'Special users', and you could write code that would - do special things to those users -- such as giving them access to a - members-only portion of your site, or sending them members-only - e-mail messages. - """ - name = StringField(max_length=80, unique=True, verbose_name=_('name')) - permissions = ListField(ReferenceField(Permission, verbose_name=_('permissions'), required=False)) - - class Meta: - verbose_name = _('group') - verbose_name_plural = _('groups') - - def __unicode__(self): - return self.name - - -class UserManager(models.Manager): - def create_user(self, username, email, password=None): - """ - Creates and saves a User with the given username, e-mail and password. - """ - now = datetime_now() - - # Normalize the address by lowercasing the domain part of the email - # address. - try: - email_name, domain_part = email.strip().split('@', 1) - except ValueError: - pass - else: - email = '@'.join([email_name, domain_part.lower()]) - - user = self.model(username=username, email=email, is_staff=False, - is_active=True, is_superuser=False, last_login=now, - date_joined=now) - - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, email, password): - u = self.create_user(username, email, password) - u.is_staff = True - u.is_active = True - u.is_superuser = True - u.save(using=self._db) - return u - - def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'): - "Generates a random password with the given length and given allowed_chars" - # Note that default value of allowed_chars does not have "I" or letters - # that look like it -- just to avoid confusion. - from random import choice - return ''.join([choice(allowed_chars) for i in range(length)]) - - -class User(Document): - """A User document that aims to mirror most of the API specified by Django - at http://docs.djangoproject.com/en/dev/topics/auth/#users - """ - username = StringField(max_length=30, required=True, - verbose_name=_('username'), - help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters")) - - first_name = StringField(max_length=30, - verbose_name=_('first name')) - - last_name = StringField(max_length=30, - verbose_name=_('last name')) - email = EmailField(verbose_name=_('e-mail address')) - password = StringField(max_length=128, - verbose_name=_('password'), - help_text=_("Use '[algo]$[iterations]$[salt]$[hexdigest]' or use the change password form.")) - is_staff = BooleanField(default=False, - verbose_name=_('staff status'), - help_text=_("Designates whether the user can log into this admin site.")) - is_active = BooleanField(default=True, - verbose_name=_('active'), - help_text=_("Designates whether this user should be treated as active. Unselect this instead of deleting accounts.")) - is_superuser = BooleanField(default=False, - verbose_name=_('superuser status'), - help_text=_("Designates that this user has all permissions without explicitly assigning them.")) - last_login = DateTimeField(default=datetime_now, - verbose_name=_('last login')) - date_joined = DateTimeField(default=datetime_now, - verbose_name=_('date joined')) - - user_permissions = ListField(ReferenceField(Permission), verbose_name=_('user permissions'), - help_text=_('Permissions for the user.')) - - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] - - meta = { - 'allow_inheritance': True, - 'indexes': [ - {'fields': ['username'], 'unique': True, 'sparse': True} - ] - } - - def __unicode__(self): - return self.username - - def get_full_name(self): - """Returns the users first and last names, separated by a space. - """ - full_name = u'%s %s' % (self.first_name or '', self.last_name or '') - return full_name.strip() - - def is_anonymous(self): - return False - - def is_authenticated(self): - return True - - def set_password(self, raw_password): - """Sets the user's password - always use this rather than directly - assigning to :attr:`~mongoengine.django.auth.User.password` as the - password is hashed before storage. - """ - self.password = make_password(raw_password) - self.save() - return self - - def check_password(self, raw_password): - """Checks the user's password against a provided password - always use - this rather than directly comparing to - :attr:`~mongoengine.django.auth.User.password` as the password is - hashed before storage. - """ - return check_password(raw_password, self.password) - - @classmethod - def create_user(cls, username, password, email=None): - """Create (and save) a new user with the given username, password and - email address. - """ - now = datetime_now() - - # Normalize the address by lowercasing the domain part of the email - # address. - if email is not None: - try: - email_name, domain_part = email.strip().split('@', 1) - except ValueError: - pass - else: - email = '@'.join([email_name, domain_part.lower()]) - - user = cls(username=username, email=email, date_joined=now) - user.set_password(password) - user.save() - return user - - def get_group_permissions(self, obj=None): - """ - Returns a list of permission strings that this user has through his/her - groups. This method queries all available auth backends. If an object - is passed in, only permissions matching this object are returned. - """ - permissions = set() - for backend in auth.get_backends(): - if hasattr(backend, "get_group_permissions"): - permissions.update(backend.get_group_permissions(self, obj)) - return permissions - - def get_all_permissions(self, obj=None): - return _user_get_all_permissions(self, obj) - - def has_perm(self, perm, obj=None): - """ - Returns True if the user has the specified permission. This method - queries all available auth backends, but returns immediately if any - backend returns True. Thus, a user who has permission from a single - auth backend is assumed to have permission in general. If an object is - provided, permissions for this specific object are checked. - """ - - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - # Otherwise we need to check the backends. - return _user_has_perm(self, perm, obj) - - def has_module_perms(self, app_label): - """ - Returns True if the user has any permissions in the given app label. - Uses pretty much the same logic as has_perm, above. - """ - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - return _user_has_module_perms(self, app_label) - - def email_user(self, subject, message, from_email=None): - "Sends an e-mail to this User." - from django.core.mail import send_mail - send_mail(subject, message, from_email, [self.email]) - - def get_profile(self): - """ - Returns site-specific profile for this user. Raises - SiteProfileNotAvailable if this site does not allow profiles. - """ - if not hasattr(self, '_profile_cache'): - from django.conf import settings - if not getattr(settings, 'AUTH_PROFILE_MODULE', False): - raise SiteProfileNotAvailable('You need to set AUTH_PROFILE_MO' - 'DULE in your project settings') - try: - app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.') - except ValueError: - raise SiteProfileNotAvailable('app_label and model_name should' - ' be separated by a dot in the AUTH_PROFILE_MODULE set' - 'ting') - - try: - model = models.get_model(app_label, model_name) - if model is None: - raise SiteProfileNotAvailable('Unable to load the profile ' - 'model, check AUTH_PROFILE_MODULE in your project sett' - 'ings') - self._profile_cache = model._default_manager.using(self._state.db).get(user__id__exact=self.id) - self._profile_cache.user = self - except (ImportError, ImproperlyConfigured): - raise SiteProfileNotAvailable - return self._profile_cache - - -class MongoEngineBackend(object): - """Authenticate using MongoEngine and mongoengine.django.auth.User. - """ - - supports_object_permissions = False - supports_anonymous_user = False - supports_inactive_user = False - _user_doc = False - - def authenticate(self, username=None, password=None): - user = self.user_document.objects(username=username).first() - if user: - if password and user.check_password(password): - backend = auth.get_backends()[0] - user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) - return user - return None - - def get_user(self, user_id): - return self.user_document.objects.with_id(user_id) - - @property - def user_document(self): - if self._user_doc is False: - from .mongo_auth.models import get_user_document - self._user_doc = get_user_document() - return self._user_doc - -def get_user(userid): - """Returns a User object from an id (User.id). Django's equivalent takes - request, but taking an id instead leaves it up to the developer to store - the id in any way they want (session, signed cookie, etc.) - """ - if not userid: - return AnonymousUser() - return MongoEngineBackend().get_user(userid) or AnonymousUser() diff --git a/mongoengine/django/mongo_auth/__init__.py b/mongoengine/django/mongo_auth/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py deleted file mode 100644 index ad4ceff8d..000000000 --- a/mongoengine/django/mongo_auth/models.py +++ /dev/null @@ -1,119 +0,0 @@ -from django.conf import settings -from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import UserManager -from django.core.exceptions import ImproperlyConfigured -from django.db import models -try: - from django.utils.module_loading import import_module -except ImportError: - """Handle older versions of Django""" - from django.utils.importlib import import_module -from django.utils.translation import ugettext_lazy as _ - - -__all__ = ( - 'get_user_document', -) - - -MONGOENGINE_USER_DOCUMENT = getattr( - settings, 'MONGOENGINE_USER_DOCUMENT', 'mongoengine.django.auth.User') - - -def get_user_document(): - """Get the user document class used for authentication. - - This is the class defined in settings.MONGOENGINE_USER_DOCUMENT, which - defaults to `mongoengine.django.auth.User`. - - """ - - name = MONGOENGINE_USER_DOCUMENT - dot = name.rindex('.') - module = import_module(name[:dot]) - return getattr(module, name[dot + 1:]) - - -class MongoUserManager(UserManager): - """A User manager wich allows the use of MongoEngine documents in Django. - - To use the manager, you must tell django.contrib.auth to use MongoUser as - the user model. In you settings.py, you need: - - INSTALLED_APPS = ( - ... - 'django.contrib.auth', - 'mongoengine.django.mongo_auth', - ... - ) - AUTH_USER_MODEL = 'mongo_auth.MongoUser' - - Django will use the model object to access the custom Manager, which will - replace the original queryset with MongoEngine querysets. - - By default, mongoengine.django.auth.User will be used to store users. You - can specify another document class in MONGOENGINE_USER_DOCUMENT in your - settings.py. - - The User Document class has the same requirements as a standard custom user - model: https://docs.djangoproject.com/en/dev/topics/auth/customizing/ - - In particular, the User Document class must define USERNAME_FIELD and - REQUIRED_FIELDS. - - `AUTH_USER_MODEL` has been added in Django 1.5. - - """ - - def contribute_to_class(self, model, name): - super(MongoUserManager, self).contribute_to_class(model, name) - self.dj_model = self.model - self.model = get_user_document() - - self.dj_model.USERNAME_FIELD = self.model.USERNAME_FIELD - username = models.CharField(_('username'), max_length=30, unique=True) - username.contribute_to_class(self.dj_model, self.dj_model.USERNAME_FIELD) - - self.dj_model.REQUIRED_FIELDS = self.model.REQUIRED_FIELDS - for name in self.dj_model.REQUIRED_FIELDS: - field = models.CharField(_(name), max_length=30) - field.contribute_to_class(self.dj_model, name) - - - def get(self, *args, **kwargs): - try: - return self.get_query_set().get(*args, **kwargs) - except self.model.DoesNotExist: - # ModelBackend expects this exception - raise self.dj_model.DoesNotExist - - @property - def db(self): - raise NotImplementedError - - def get_empty_query_set(self): - return self.model.objects.none() - - def get_query_set(self): - return self.model.objects - - -class MongoUser(models.Model): - """"Dummy user model for Django. - - MongoUser is used to replace Django's UserManager with MongoUserManager. - The actual user document class is mongoengine.django.auth.User or any - other document class specified in MONGOENGINE_USER_DOCUMENT. - - To get the user document class, use `get_user_document()`. - - """ - - objects = MongoUserManager() - - class Meta: - app_label = 'mongo_auth' - - def set_password(self, password): - """Doesn't do anything, but works around the issue with Django 1.6.""" - make_password(password) diff --git a/mongoengine/django/sessions.py b/mongoengine/django/sessions.py deleted file mode 100644 index f260951be..000000000 --- a/mongoengine/django/sessions.py +++ /dev/null @@ -1,124 +0,0 @@ -from bson import json_util -from django.conf import settings -from django.contrib.sessions.backends.base import SessionBase, CreateError -from django.core.exceptions import SuspiciousOperation -try: - from django.utils.encoding import force_unicode -except ImportError: - from django.utils.encoding import force_text as force_unicode - -from mongoengine.document import Document -from mongoengine import fields -from mongoengine.queryset import OperationError -from mongoengine.connection import DEFAULT_CONNECTION_NAME - -from .utils import datetime_now - - -MONGOENGINE_SESSION_DB_ALIAS = getattr( - settings, 'MONGOENGINE_SESSION_DB_ALIAS', - DEFAULT_CONNECTION_NAME) - -# a setting for the name of the collection used to store sessions -MONGOENGINE_SESSION_COLLECTION = getattr( - settings, 'MONGOENGINE_SESSION_COLLECTION', - 'django_session') - -# a setting for whether session data is stored encoded or not -MONGOENGINE_SESSION_DATA_ENCODE = getattr( - settings, 'MONGOENGINE_SESSION_DATA_ENCODE', - True) - - -class MongoSession(Document): - session_key = fields.StringField(primary_key=True, max_length=40) - session_data = fields.StringField() if MONGOENGINE_SESSION_DATA_ENCODE \ - else fields.DictField() - expire_date = fields.DateTimeField() - - meta = { - 'collection': MONGOENGINE_SESSION_COLLECTION, - 'db_alias': MONGOENGINE_SESSION_DB_ALIAS, - 'allow_inheritance': False, - 'indexes': [ - { - 'fields': ['expire_date'], - 'expireAfterSeconds': 0 - } - ] - } - - def get_decoded(self): - return SessionStore().decode(self.session_data) - - -class SessionStore(SessionBase): - """A MongoEngine-based session store for Django. - """ - - def _get_session(self, *args, **kwargs): - sess = super(SessionStore, self)._get_session(*args, **kwargs) - if sess.get('_auth_user_id', None): - sess['_auth_user_id'] = str(sess.get('_auth_user_id')) - return sess - - def load(self): - try: - s = MongoSession.objects(session_key=self.session_key, - expire_date__gt=datetime_now)[0] - if MONGOENGINE_SESSION_DATA_ENCODE: - return self.decode(force_unicode(s.session_data)) - else: - return s.session_data - except (IndexError, SuspiciousOperation): - self.create() - return {} - - def exists(self, session_key): - return bool(MongoSession.objects(session_key=session_key).first()) - - def create(self): - while True: - self._session_key = self._get_new_session_key() - try: - self.save(must_create=True) - except CreateError: - continue - self.modified = True - self._session_cache = {} - return - - def save(self, must_create=False): - if self.session_key is None: - self._session_key = self._get_new_session_key() - s = MongoSession(session_key=self.session_key) - if MONGOENGINE_SESSION_DATA_ENCODE: - s.session_data = self.encode(self._get_session(no_load=must_create)) - else: - s.session_data = self._get_session(no_load=must_create) - s.expire_date = self.get_expiry_date() - try: - s.save(force_insert=must_create) - except OperationError: - if must_create: - raise CreateError - raise - - def delete(self, session_key=None): - if session_key is None: - if self.session_key is None: - return - session_key = self.session_key - MongoSession.objects(session_key=session_key).delete() - - -class BSONSerializer(object): - """ - Serializer that can handle BSON types (eg ObjectId). - """ - def dumps(self, obj): - return json_util.dumps(obj, separators=(',', ':')).encode('ascii') - - def loads(self, data): - return json_util.loads(data.decode('ascii')) - diff --git a/mongoengine/django/shortcuts.py b/mongoengine/django/shortcuts.py deleted file mode 100644 index 9cc8370b0..000000000 --- a/mongoengine/django/shortcuts.py +++ /dev/null @@ -1,47 +0,0 @@ -from mongoengine.queryset import QuerySet -from mongoengine.base import BaseDocument -from mongoengine.errors import ValidationError - -def _get_queryset(cls): - """Inspired by django.shortcuts.*""" - if isinstance(cls, QuerySet): - return cls - else: - return cls.objects - -def get_document_or_404(cls, *args, **kwargs): - """ - Uses get() to return an document, or raises a Http404 exception if the document - does not exist. - - cls may be a Document or QuerySet object. All other passed - arguments and keyword arguments are used in the get() query. - - Note: Like with get(), an MultipleObjectsReturned will be raised if more than one - object is found. - - Inspired by django.shortcuts.* - """ - queryset = _get_queryset(cls) - try: - return queryset.get(*args, **kwargs) - except (queryset._document.DoesNotExist, ValidationError): - from django.http import Http404 - raise Http404('No %s matches the given query.' % queryset._document._class_name) - -def get_list_or_404(cls, *args, **kwargs): - """ - Uses filter() to return a list of documents, or raise a Http404 exception if - the list is empty. - - cls may be a Document or QuerySet object. All other passed - arguments and keyword arguments are used in the filter() query. - - Inspired by django.shortcuts.* - """ - queryset = _get_queryset(cls) - obj_list = list(queryset.filter(*args, **kwargs)) - if not obj_list: - from django.http import Http404 - raise Http404('No %s matches the given query.' % queryset._document._class_name) - return obj_list diff --git a/mongoengine/django/storage.py b/mongoengine/django/storage.py deleted file mode 100644 index 9df6f9e80..000000000 --- a/mongoengine/django/storage.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -import itertools -import urlparse - -from mongoengine import * -from django.conf import settings -from django.core.files.storage import Storage -from django.core.exceptions import ImproperlyConfigured - - -class FileDocument(Document): - """A document used to store a single file in GridFS. - """ - file = FileField() - - -class GridFSStorage(Storage): - """A custom storage backend to store files in GridFS - """ - - def __init__(self, base_url=None): - - if base_url is None: - base_url = settings.MEDIA_URL - self.base_url = base_url - self.document = FileDocument - self.field = 'file' - - def delete(self, name): - """Deletes the specified file from the storage system. - """ - if self.exists(name): - doc = self.document.objects.first() - field = getattr(doc, self.field) - self._get_doc_with_name(name).delete() # Delete the FileField - field.delete() # Delete the FileDocument - - def exists(self, name): - """Returns True if a file referened by the given name already exists in the - storage system, or False if the name is available for a new file. - """ - doc = self._get_doc_with_name(name) - if doc: - field = getattr(doc, self.field) - return bool(field.name) - else: - return False - - def listdir(self, path=None): - """Lists the contents of the specified path, returning a 2-tuple of lists; - the first item being directories, the second item being files. - """ - def name(doc): - return getattr(doc, self.field).name - docs = self.document.objects - return [], [name(d) for d in docs if name(d)] - - def size(self, name): - """Returns the total size, in bytes, of the file specified by name. - """ - doc = self._get_doc_with_name(name) - if doc: - return getattr(doc, self.field).length - else: - raise ValueError("No such file or directory: '%s'" % name) - - def url(http://github.com/self,%20name): - """Returns an absolute URL where the file's contents can be accessed - directly by a web browser. - """ - if self.base_url is None: - raise ValueError("This file is not accessible via a URL.") - return urlparse.urljoin(self.base_url, name).replace('\\', '/') - - def _get_doc_with_name(self, name): - """Find the documents in the store with the given name - """ - docs = self.document.objects - doc = [d for d in docs if hasattr(getattr(d, self.field), 'name') and getattr(d, self.field).name == name] - if doc: - return doc[0] - else: - return None - - def _open(self, name, mode='rb'): - doc = self._get_doc_with_name(name) - if doc: - return getattr(doc, self.field) - else: - raise ValueError("No file found with the name '%s'." % name) - - def get_available_name(self, name): - """Returns a filename that's free on the target storage system, and - available for new content to be written to. - """ - file_root, file_ext = os.path.splitext(name) - # If the filename already exists, add an underscore and a number (before - # the file extension, if one exists) to the filename until the generated - # filename doesn't exist. - count = itertools.count(1) - while self.exists(name): - # file_ext includes the dot. - name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext)) - - return name - - def _save(self, name, content): - doc = self.document() - getattr(doc, self.field).put(content, filename=name) - doc.save() - - return name diff --git a/mongoengine/django/tests.py b/mongoengine/django/tests.py deleted file mode 100644 index b130acc82..000000000 --- a/mongoengine/django/tests.py +++ /dev/null @@ -1,31 +0,0 @@ -#coding: utf-8 - -from unittest import TestCase - -from mongoengine import connect -from mongoengine.connection import get_db - - -class MongoTestCase(TestCase): - """ - TestCase class that clear the collection between the tests - """ - - @property - def db_name(self): - from django.conf import settings - return 'test_%s' % getattr(settings, 'MONGO_DATABASE_NAME', 'dummy') - - def __init__(self, methodName='runtest'): - connect(self.db_name) - self.db = get_db() - super(MongoTestCase, self).__init__(methodName) - - def dropCollections(self): - for collection in self.db.collection_names(): - if collection.startswith('system.'): - continue - self.db.drop_collection(collection) - - def tearDown(self): - self.dropCollections() diff --git a/mongoengine/django/utils.py b/mongoengine/django/utils.py deleted file mode 100644 index d3ef8a4b7..000000000 --- a/mongoengine/django/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -try: - # django >= 1.4 - from django.utils.timezone import now as datetime_now -except ImportError: - from datetime import datetime - datetime_now = datetime.now diff --git a/setup.py b/setup.py index 88519788e..3ae5aeacf 100644 --- a/setup.py +++ b/setup.py @@ -53,12 +53,12 @@ def get_version(version_tuple): extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'Pillow>=2.0.0', 'django>=1.5.1'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'Pillow>=2.0.0'] if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'] = find_packages() extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} else: - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'Pillow>=2.0.0', 'jinja2>=2.6', 'python-dateutil'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'Pillow>=2.0.0', 'python-dateutil'] if sys.version_info[0] == 2 and sys.version_info[1] == 6: extra_opts['tests_require'].append('unittest2') diff --git a/tests/test_django.py b/tests/test_django.py deleted file mode 100644 index cb7efe6f1..000000000 --- a/tests/test_django.py +++ /dev/null @@ -1,330 +0,0 @@ -import sys -sys.path[0:0] = [""] -import unittest -from nose.plugins.skip import SkipTest - -from mongoengine import * -from mongoengine.django.shortcuts import get_document_or_404 - -import django -from django.http import Http404 -from django.template import Context, Template -from django.conf import settings -from django.core.paginator import Paginator - -settings.configure( - USE_TZ=True, - INSTALLED_APPS=('django.contrib.auth', 'mongoengine.django.mongo_auth'), - AUTH_USER_MODEL=('mongo_auth.MongoUser'), - AUTHENTICATION_BACKENDS = ('mongoengine.django.auth.MongoEngineBackend',) -) - -try: - # For Django >= 1.7 - if hasattr(django, 'setup'): - django.setup() -except RuntimeError: - pass - -try: - from django.contrib.auth import authenticate, get_user_model - from mongoengine.django.auth import User - from mongoengine.django.mongo_auth.models import ( - MongoUser, - MongoUserManager, - get_user_document, - ) - DJ15 = True -except Exception: - DJ15 = False -from mongoengine.django.sessions import SessionStore, MongoSession -from mongoengine.django.tests import MongoTestCase -from datetime import tzinfo, timedelta -ZERO = timedelta(0) - - -class FixedOffset(tzinfo): - """Fixed offset in minutes east from UTC.""" - - def __init__(self, offset, name): - self.__offset = timedelta(minutes=offset) - self.__name = name - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return ZERO - - -def activate_timezone(tz): - """Activate Django timezone support if it is available. - """ - try: - from django.utils import timezone - timezone.deactivate() - timezone.activate(tz) - except ImportError: - pass - - -class QuerySetTest(unittest.TestCase): - - def setUp(self): - connect(db='mongoenginetest') - - class Person(Document): - name = StringField() - age = IntField() - self.Person = Person - - def test_order_by_in_django_template(self): - """Ensure that QuerySets are properly ordered in Django template. - """ - self.Person.drop_collection() - - self.Person(name="A", age=20).save() - self.Person(name="D", age=10).save() - self.Person(name="B", age=40).save() - self.Person(name="C", age=30).save() - - t = Template("{% for o in ol %}{{ o.name }}-{{ o.age }}:{% endfor %}") - - d = {"ol": self.Person.objects.order_by('-name')} - self.assertEqual(t.render(Context(d)), u'D-10:C-30:B-40:A-20:') - d = {"ol": self.Person.objects.order_by('+name')} - self.assertEqual(t.render(Context(d)), u'A-20:B-40:C-30:D-10:') - d = {"ol": self.Person.objects.order_by('-age')} - self.assertEqual(t.render(Context(d)), u'B-40:C-30:A-20:D-10:') - d = {"ol": self.Person.objects.order_by('+age')} - self.assertEqual(t.render(Context(d)), u'D-10:A-20:C-30:B-40:') - - self.Person.drop_collection() - - def test_q_object_filter_in_template(self): - - self.Person.drop_collection() - - self.Person(name="A", age=20).save() - self.Person(name="D", age=10).save() - self.Person(name="B", age=40).save() - self.Person(name="C", age=30).save() - - t = Template("{% for o in ol %}{{ o.name }}-{{ o.age }}:{% endfor %}") - - d = {"ol": self.Person.objects.filter(Q(age=10) | Q(name="C"))} - self.assertEqual(t.render(Context(d)), 'D-10:C-30:') - - # Check double rendering doesn't throw an error - self.assertEqual(t.render(Context(d)), 'D-10:C-30:') - - def test_get_document_or_404(self): - p = self.Person(name="G404") - p.save() - - self.assertRaises(Http404, get_document_or_404, self.Person, pk='1234') - self.assertEqual(p, get_document_or_404(self.Person, pk=p.pk)) - - def test_pagination(self): - """Ensure that Pagination works as expected - """ - class Page(Document): - name = StringField() - - Page.drop_collection() - - for i in xrange(1, 11): - Page(name=str(i)).save() - - paginator = Paginator(Page.objects.all(), 2) - - t = Template("{% for i in page.object_list %}{{ i.name }}:{% endfor %}") - for p in paginator.page_range: - d = {"page": paginator.page(p)} - end = p * 2 - start = end - 1 - self.assertEqual(t.render(Context(d)), u'%d:%d:' % (start, end)) - - def test_nested_queryset_template_iterator(self): - # Try iterating the same queryset twice, nested, in a Django template. - names = ['A', 'B', 'C', 'D'] - - class CustomUser(Document): - name = StringField() - - def __unicode__(self): - return self.name - - CustomUser.drop_collection() - - for name in names: - CustomUser(name=name).save() - - users = CustomUser.objects.all().order_by('name') - template = Template("{% for user in users %}{{ user.name }}{% ifequal forloop.counter 2 %} {% for inner_user in users %}{{ inner_user.name }}{% endfor %} {% endifequal %}{% endfor %}") - rendered = template.render(Context({'users': users})) - self.assertEqual(rendered, 'AB ABCD CD') - - def test_filter(self): - """Ensure that a queryset and filters work as expected - """ - - class LimitCountQuerySet(QuerySet): - def count(self, with_limit_and_skip=True): - return super(LimitCountQuerySet, self).count(with_limit_and_skip) - - class Note(Document): - meta = dict(queryset_class=LimitCountQuerySet) - name = StringField() - - Note.drop_collection() - - for i in xrange(1, 101): - Note(name="Note: %s" % i).save() - - # Check the count - self.assertEqual(Note.objects.count(), 100) - - # Get the first 10 and confirm - notes = Note.objects[:10] - self.assertEqual(notes.count(), 10) - - # Test djangos template filters - # self.assertEqual(length(notes), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - # Test with skip - notes = Note.objects.skip(90) - self.assertEqual(notes.count(), 10) - - # Test djangos template filters - self.assertEqual(notes.count(), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - # Test with limit - notes = Note.objects.skip(90) - self.assertEqual(notes.count(), 10) - - # Test djangos template filters - self.assertEqual(notes.count(), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - # Test with skip and limit - notes = Note.objects.skip(10).limit(10) - - # Test djangos template filters - self.assertEqual(notes.count(), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - -class _BaseMongoDBSessionTest(unittest.TestCase): - backend = SessionStore - - def setUp(self): - connect(db='mongoenginetest') - MongoSession.drop_collection() - super(_BaseMongoDBSessionTest, self).setUp() - - def assertIn(self, first, second, msg=None): - self.assertTrue(first in second, msg) - - def assertNotIn(self, first, second, msg=None): - self.assertFalse(first in second, msg) - - def test_first_save(self): - session = SessionStore() - session['test'] = True - session.save() - self.assertTrue('test' in session) - - def test_session_expiration_tz(self): - activate_timezone(FixedOffset(60, 'UTC+1')) - # create and save new session - session = SessionStore() - session.set_expiry(600) # expire in 600 seconds - session['test_expire'] = True - session.save() - # reload session with key - key = session.session_key - session = SessionStore(key) - self.assertTrue('test_expire' in session, 'Session has expired before it is expected') - - -try: - # SessionTestsMixin isn't available for import on django > 1.8a1 - from django.contrib.sessions.tests import SessionTestsMixin - - class _MongoDBSessionTest(SessionTestsMixin): - pass - - class MongoDBSessionTest(_BaseMongoDBSessionTest): - pass - -except ImportError: - class MongoDBSessionTest(_BaseMongoDBSessionTest): - pass - - -class MongoAuthTest(unittest.TestCase): - user_data = { - 'username': 'user', - 'email': 'user@example.com', - 'password': 'test', - } - - def setUp(self): - if not DJ15: - raise SkipTest('mongo_auth requires Django 1.5') - connect(db='mongoenginetest') - User.drop_collection() - super(MongoAuthTest, self).setUp() - - def test_get_user_model(self): - self.assertEqual(get_user_model(), MongoUser) - - def test_get_user_document(self): - self.assertEqual(get_user_document(), User) - - def test_user_manager(self): - manager = get_user_model()._default_manager - self.assertTrue(isinstance(manager, MongoUserManager)) - - def test_user_manager_exception(self): - manager = get_user_model()._default_manager - self.assertRaises(MongoUser.DoesNotExist, manager.get, - username='not found') - - def test_create_user(self): - manager = get_user_model()._default_manager - user = manager.create_user(**self.user_data) - self.assertTrue(isinstance(user, User)) - db_user = User.objects.get(username='user') - self.assertEqual(user.id, db_user.id) - - def test_authenticate(self): - get_user_model()._default_manager.create_user(**self.user_data) - user = authenticate(username='user', password='fail') - self.assertEqual(None, user) - user = authenticate(username='user', password='test') - db_user = User.objects.get(username='user') - self.assertEqual(user.id, db_user.id) - - -class MongoTestCaseTest(MongoTestCase): - def test_mongo_test_case(self): - self.db.dummy_collection.insert({'collection': 'will be dropped'}) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_jinja.py b/tests/test_jinja.py deleted file mode 100644 index 0449f8688..000000000 --- a/tests/test_jinja.py +++ /dev/null @@ -1,47 +0,0 @@ -import sys -sys.path[0:0] = [""] - -import unittest - -from mongoengine import * - -import jinja2 - - -class TemplateFilterTest(unittest.TestCase): - - def setUp(self): - connect(db='mongoenginetest') - - def test_jinja2(self): - env = jinja2.Environment() - - class TestData(Document): - title = StringField() - description = StringField() - - TestData.drop_collection() - - examples = [('A', '1'), - ('B', '2'), - ('C', '3')] - - for title, description in examples: - TestData(title=title, description=description).save() - - tmpl = """ -{%- for record in content -%} - {%- if loop.first -%}{ {%- endif -%} - "{{ record.title }}": "{{ record.description }}" - {%- if loop.last -%} }{%- else -%},{% endif -%} -{%- endfor -%} -""" - ctx = {'content': TestData.objects} - template = env.from_string(tmpl) - rendered = template.render(**ctx) - - self.assertEqual('{"A": "1","B": "2","C": "3"}', rendered) - - -if __name__ == '__main__': - unittest.main() From 6eb0bc50e237fb47e0ffa97b9d79c1cccd3bc61a Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Sat, 25 Apr 2015 08:01:24 +0800 Subject: [PATCH 0097/1519] Add a test for "authSource" feature --- tests/test_connection.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 2b3258430..aa7d59603 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -11,7 +11,10 @@ import pymongo from bson.tz_util import utc -from mongoengine import * +from mongoengine import ( + connect, register_connection, + Document, DateTimeField +) import mongoengine.connection from mongoengine.connection import get_db, get_connection, ConnectionError @@ -101,6 +104,38 @@ def test_connect_uri_without_db(self): c.admin.system.users.remove({}) c.mongoenginetest.system.users.remove({}) + def test_connect_uri_with_authsource(self): + """Ensure that the connect() method works well with + the option `authSource` in URI. + """ + # Create users + c = connect(db='mongoenginetest', alias='test') + c.admin.system.users.remove({}) + c.admin.add_user('username', 'password') + + # Authentication fails without "authSource" + self.assertRaises( + ConnectionError, connect, 'mongoenginetest', + host='mongodb://username:password@localhost/mongoenginetest' + ) + + # Authentication succeeds with "authSource" + connect( + 'mongoenginetest', + host=('mongodb://username:password@localhost/' + 'mongoenginetest?authSource=admin') + ) + + conn = get_connection('test') + self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + + db = get_db('test') + self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertEqual(db.name, 'mongoenginetest') + + # Clear all users + c.admin.system.users.remove({}) + def test_register_connection(self): """Ensure that connections with different aliases may be registered. """ From b847bc0abab9cfaece3bbf17dbd5947875cbcc02 Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Sat, 25 Apr 2015 10:22:24 +0800 Subject: [PATCH 0098/1519] Make `test_connect_uri_with_authsource` to focus on the key point --- tests/test_connection.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index aa7d59603..9204d80cc 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -109,27 +109,24 @@ def test_connect_uri_with_authsource(self): the option `authSource` in URI. """ # Create users - c = connect(db='mongoenginetest', alias='test') + c = connect('mongoenginetest') c.admin.system.users.remove({}) c.admin.add_user('username', 'password') # Authentication fails without "authSource" self.assertRaises( - ConnectionError, connect, 'mongoenginetest', + ConnectionError, connect, 'mongoenginetest', alias='test1', host='mongodb://username:password@localhost/mongoenginetest' ) + self.assertRaises(ConnectionError, get_db, 'test1') # Authentication succeeds with "authSource" connect( - 'mongoenginetest', + 'mongoenginetest', alias='test2', host=('mongodb://username:password@localhost/' 'mongoenginetest?authSource=admin') ) - - conn = get_connection('test') - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) - - db = get_db('test') + db = get_db('test2') self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'mongoenginetest') From 27111e7b29e1b8918c6b52d0a79a1fdc72301e96 Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Sat, 25 Apr 2015 20:57:26 +0800 Subject: [PATCH 0099/1519] Update changelog for added `authSource` support --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3dbcc2f87..40df04194 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Added support for specifying authentication source as option `authSource` in URI. #967 - Fixed mark_as_changed to handle higher/lower level fields changed. #927 - ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 - Support += and *= for ListField #595 From d737fca295c7469e5adc3511b0abc5e44c8c1001 Mon Sep 17 00:00:00 2001 From: Eli Boyarski Date: Sun, 26 Apr 2015 17:23:13 +0300 Subject: [PATCH 0100/1519] Spelling --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 3c1db4ac3..5498392d2 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1016,7 +1016,7 @@ def lookup_member(self, member_name): class CachedReferenceField(BaseField): """ - A referencefield with cache fields to porpuse pseudo-joins + A referencefield with cache fields to purpose pseudo-joins .. versionadded:: 0.9 """ From 1a0cad7f5f0c661f4fdf2ae7cd21ab756c2cf428 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Tue, 28 Apr 2015 10:02:39 -0400 Subject: [PATCH 0101/1519] Updated Django Support Documentation Added "Call to Arms" for new Django Extension. --- docs/django.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/django.rst b/docs/django.rst index 4b9a04bdb..6982fe64b 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -2,5 +2,18 @@ Django Support ============== -.. note:: Django support has been split from MongoEngine and will be available on Github as a separate module. `See Here `_. +.. note:: Django support has been split from the main MongoEngine + repository. The *legacy* Django extension may be found bundled with the + 0.9 release of MongoEngine. + + +Help Wanted! +------------ + +The MongoEngine team is looking for help contributing and maintaining a new +Django extension for MongoEngine! If you have Django experience and would like +to help contribute to the project, please get in touch on the +`mailing list `_ or by +simpily contributing on +`GitHub `_. From 57f301815dcddbb896b18701959458421b1e3097 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Thu, 16 Apr 2015 14:09:32 +0200 Subject: [PATCH 0102/1519] Added a tox.ini file allowing to test with different versions --- tox.ini | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..6a3e0600d --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = + {py27,py32,py33,py34,pypy,pypy3}-{mg27,mg28,mg30}-{dj15,dj16,dj17,djdev}, + py26-{mg27,mg28,mg30}-{dj15,dj16} + +[testenv] +commands = + python setup.py test +deps = + mg27: PyMongo<2.8 + mg28: PyMongo>=2.8,<3.0 + mg30: PyMongo>=3.0 + mgdev: https://github.com/mongodb/mongo-python-driver/tarball/master + dj15: Django<1.6 + dj16: Django>=1.6,<1.7 + dj17: Django>=1.7,<1.8 + djdev: https://github.com/django/django/tarball/master From 8e8d9426df25536ce4b6aa6f18c7b45821f2ca73 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Thu, 16 Apr 2015 14:26:19 +0200 Subject: [PATCH 0103/1519] Document about tox testing --- CONTRIBUTING.rst | 2 ++ README.rst | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8754896ad..0ad7fd5cd 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -46,6 +46,8 @@ General Guidelines - Write tests and make sure they pass (make sure you have a mongod running on the default port, then execute ``python setup.py test`` from the cmd line to run the test suite). +- Ensure tests pass on every Python and PyMongo versions. + You can test on these versions locally by executing ``tox`` - Add yourself to AUTHORS :) Documentation diff --git a/README.rst b/README.rst index 1d01d8b72..59fbe1944 100644 --- a/README.rst +++ b/README.rst @@ -8,10 +8,10 @@ MongoEngine .. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master :target: http://travis-ci.org/MongoEngine/mongoengine - -.. image:: https://coveralls.io/repos/MongoEngine/mongoengine/badge.png?branch=master + +.. image:: https://coveralls.io/repos/MongoEngine/mongoengine/badge.png?branch=master :target: https://coveralls.io/r/MongoEngine/mongoengine?branch=master - + .. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.png :target: https://landscape.io/github/MongoEngine/mongoengine/master :alt: Code Health @@ -98,6 +98,17 @@ Tests To run the test suite, ensure you are running a local instance of MongoDB on the standard port, and run: ``python setup.py test``. +To run the test suite on every supported Python version and every supported PyMongo version, +you can use ``tox``. +tox and each supported Python version should be installed in your environment: + +.. code-block:: shell + + # Install tox + $ pip install tox + # Run the test suites + $ tox + Community ========= - `MongoEngine Users mailing list From 02c1ba39ad8a5cfbb27f51ef1844e3afd9f9d570 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Thu, 16 Apr 2015 16:13:01 +0200 Subject: [PATCH 0104/1519] Added Django 1.8 to tox --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6a3e0600d..61692f7c5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - {py27,py32,py33,py34,pypy,pypy3}-{mg27,mg28,mg30}-{dj15,dj16,dj17,djdev}, + {py27,py32,py33,py34,pypy,pypy3}-{mg27,mg28,mg30}-{dj15,dj16,dj17,dj18,djdev}, py26-{mg27,mg28,mg30}-{dj15,dj16} [testenv] @@ -14,4 +14,5 @@ deps = dj15: Django<1.6 dj16: Django>=1.6,<1.7 dj17: Django>=1.7,<1.8 + dj18: Django>=1.8,<1.9 djdev: https://github.com/django/django/tarball/master From aa7bf7af1e7228c169fc788f3196966700c7055a Mon Sep 17 00:00:00 2001 From: mrigal Date: Fri, 17 Apr 2015 10:36:26 +0200 Subject: [PATCH 0105/1519] adapted setup.cfg to use nosetests standard and allow usage of --tests argument, documenting it in the readme --- README.rst | 5 +++++ setup.cfg | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 59fbe1944..6e32d3f35 100644 --- a/README.rst +++ b/README.rst @@ -109,6 +109,11 @@ tox and each supported Python version should be installed in your environment: # Run the test suites $ tox +If you wish to run one single or selected tests, use the nosetest convention. It will find the folder, +eventually the file, go to the TestClass specified after the colon and eventually right to the single test. +Also use the -s argument if you want to print out whatever or access pdb while testing. +``python setup.py nosetests --tests tests/test_django.py:QuerySetTest.test_get_document_or_404 -s`` + Community ========= - `MongoEngine Users mailing list diff --git a/setup.cfg b/setup.cfg index 3f3faa8ca..2ee0f4992 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,5 +7,6 @@ detailed-errors = 1 #cover-html-dir = ../htmlcov #cover-package = mongoengine py3where = build -where = tests +#where = tests +tests = tests #tests = document/__init__.py \ No newline at end of file From 91ee85152c8ac830a4f01c9a6a1b856f77d2f9d0 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Fri, 17 Apr 2015 11:26:47 +0200 Subject: [PATCH 0106/1519] Tests/Tox/TravisCI improvements --- .travis.yml | 38 +++++++++++++++++++------------------- CONTRIBUTING.rst | 2 +- README.rst | 2 +- setup.cfg | 15 ++++++--------- setup.py | 2 +- tox.ini | 2 +- 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37ec8cf72..cd5d953b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,20 +8,30 @@ python: - pypy - pypy3 env: -- PYMONGO=2.7.2 -- PYMONGO=2.8 +- PYMONGO=2.7 DJANGO=dev +- PYMONGO=2.7 DJANGO=1.8 +- PYMONGO=2.7 DJANGO=1.7 +- PYMONGO=2.7 DJANGO=1.6 +- PYMONGO=2.7 DJANGO=1.5 +- PYMONGO=2.8 DJANGO=dev +- PYMONGO=2.8 DJANGO=1.8 +- PYMONGO=2.8 DJANGO=1.7 +- PYMONGO=2.8 DJANGO=1.6 +- PYMONGO=2.8 DJANGO=1.5 matrix: exclude: - python: '2.6' - env: PYMONGO=2.7.2 + env: PYMONGO=2.7 DJANGO=dev - python: '2.6' env: PYMONGO=2.8 - python: '2.6' - env: PYMONGO=2.7.2 + env: PYMONGO=2.7 DJANGO=1.7 - python: '2.6' - env: PYMONGO=2.8 - allow_failures: - - python: pypy3 + env: PYMONGO=2.8 DJANGO=1.7 + - python: '2.6' + env: PYMONGO=2.7 DJANGO=1.8 + - python: '2.6' + env: PYMONGO=2.8 DJANGO=1.8 fast_finish: true before_install: - travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 @@ -33,19 +43,9 @@ install: - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk -- if [[ $PYMONGO == 'dev' ]]; then travis_retry pip install https://github.com/mongodb/mongo-python-driver/tarball/master; - true; fi -- if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; - fi -- travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b -- travis_retry pip install coveralls -- travis_retry python setup.py install +- travis_retry pip install tox>=1.9 script: -- travis_retry python setup.py test -- if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then 2to3 . -w; fi; -- coverage run --source=mongoengine setup.py test -- coverage report -m -- python benchmark.py +- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- with-coverage after_script: coveralls --verbose notifications: irc: irc.freenode.org#mongoengine diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0ad7fd5cd..c935d5a34 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -44,7 +44,7 @@ General Guidelines - Avoid backward breaking changes if at all possible. - Write inline documentation for new classes and methods. - Write tests and make sure they pass (make sure you have a mongod - running on the default port, then execute ``python setup.py test`` + running on the default port, then execute ``python setup.py nosetests`` from the cmd line to run the test suite). - Ensure tests pass on every Python and PyMongo versions. You can test on these versions locally by executing ``tox`` diff --git a/README.rst b/README.rst index 6e32d3f35..2414ed16a 100644 --- a/README.rst +++ b/README.rst @@ -96,7 +96,7 @@ Some simple examples of what MongoEngine code looks like:: Tests ===== To run the test suite, ensure you are running a local instance of MongoDB on -the standard port, and run: ``python setup.py test``. +the standard port, and run: ``python setup.py nosetests``. To run the test suite on every supported Python version and every supported PyMongo version, you can use ``tox``. diff --git a/setup.cfg b/setup.cfg index 2ee0f4992..2aac6e1ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,9 @@ [nosetests] -verbosity = 3 +rednose = 1 +verbosity = 2 detailed-errors = 1 -#with-coverage = 1 -#cover-erase = 1 -#cover-html = 1 -#cover-html-dir = ../htmlcov -#cover-package = mongoengine +cover-erase = 1 +cover-branches = 1 +cover-package = mongoengine py3where = build -#where = tests -tests = tests -#tests = document/__init__.py \ No newline at end of file +where = tests diff --git a/setup.py b/setup.py index 3ae5aeacf..42794452a 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,6 @@ def get_version(version_tuple): version_line = list(filter(lambda l: l.startswith('VERSION'), open(init)))[0] VERSION = get_version(eval(version_line.split('=')[-1])) -print(VERSION) CLASSIFIERS = [ 'Development Status :: 4 - Beta', @@ -79,5 +78,6 @@ def get_version(version_tuple): classifiers=CLASSIFIERS, install_requires=['pymongo>=2.7.1'], test_suite='nose.collector', + setup_requires=['nose', 'rednose'], # Allow proper nose usage with setuptols and tox **extra_opts ) diff --git a/tox.ini b/tox.ini index 61692f7c5..38517bdcd 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = [testenv] commands = - python setup.py test + python setup.py nosetests {posargs} deps = mg27: PyMongo<2.8 mg28: PyMongo>=2.8,<3.0 From 15fcb57e2fe57a3b0b88a0440ca119fa9bd5d1ab Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Fri, 17 Apr 2015 16:08:31 +0200 Subject: [PATCH 0107/1519] Fix typo in travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cd5d953b1..eda63217e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,7 @@ install: python-tk - travis_retry pip install tox>=1.9 script: -- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- with-coverage +- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage after_script: coveralls --verbose notifications: irc: irc.freenode.org#mongoengine From dc45920afbddf5873563b4f73841426c8605639a Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Fri, 17 Apr 2015 16:29:16 +0200 Subject: [PATCH 0108/1519] Added missing coveralls install --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eda63217e..1012d286f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,7 +43,7 @@ install: - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk -- travis_retry pip install tox>=1.9 +- travis_retry pip install tox>=1.9 coveralls script: - tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage after_script: coveralls --verbose From 29d858d58c13a8e47636ea95bad6c1b76fe13764 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Fri, 17 Apr 2015 17:20:37 +0200 Subject: [PATCH 0109/1519] Removed the deprecated py3where parameter --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2aac6e1ef..e59f0fe26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,5 +5,4 @@ detailed-errors = 1 cover-erase = 1 cover-branches = 1 cover-package = mongoengine -py3where = build -where = tests +tests = tests From 050378fa72014c4ba6b0ff2f4ae92f516afefa2e Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Fri, 17 Apr 2015 17:20:57 +0200 Subject: [PATCH 0110/1519] Little README tuning --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2414ed16a..5a873e225 100644 --- a/README.rst +++ b/README.rst @@ -112,7 +112,10 @@ tox and each supported Python version should be installed in your environment: If you wish to run one single or selected tests, use the nosetest convention. It will find the folder, eventually the file, go to the TestClass specified after the colon and eventually right to the single test. Also use the -s argument if you want to print out whatever or access pdb while testing. -``python setup.py nosetests --tests tests/test_django.py:QuerySetTest.test_get_document_or_404 -s`` + +.. code-block:: shell + + $ python setup.py nosetests --tests tests/test_django.py:QuerySetTest.test_get_document_or_404 -s Community ========= From 76ea8c86b73cdbbadd4d80cf80fe606fc4d56389 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Sat, 18 Apr 2015 13:40:32 +0200 Subject: [PATCH 0111/1519] Use travis_retry on tox execution --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1012d286f..51d33c59b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,7 @@ install: python-tk - travis_retry pip install tox>=1.9 coveralls script: -- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage +- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage after_script: coveralls --verbose notifications: irc: irc.freenode.org#mongoengine From 58c8cf1a3a83d0f61c4e23102aa199a68c49136e Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Wed, 22 Apr 2015 13:10:44 +0200 Subject: [PATCH 0112/1519] Split dependencies installation and test running avoing travis_retry on tests --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 51d33c59b..46a71a2fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,8 +44,9 @@ install: libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - travis_retry pip install tox>=1.9 coveralls +- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test script: -- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage +- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage after_script: coveralls --verbose notifications: irc: irc.freenode.org#mongoengine From c804c395ed293048c4e52ccd0717812c2438a73a Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Tue, 28 Apr 2015 20:31:13 +0200 Subject: [PATCH 0113/1519] Post rebase and Django removal tuning (and prepare for PyMongo 3) --- .travis.yml | 31 ++++++------------------------- README.rst | 3 +-- tox.ini | 10 ++-------- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/.travis.yml b/.travis.yml index 46a71a2fc..74f409298 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,30 +8,11 @@ python: - pypy - pypy3 env: -- PYMONGO=2.7 DJANGO=dev -- PYMONGO=2.7 DJANGO=1.8 -- PYMONGO=2.7 DJANGO=1.7 -- PYMONGO=2.7 DJANGO=1.6 -- PYMONGO=2.7 DJANGO=1.5 -- PYMONGO=2.8 DJANGO=dev -- PYMONGO=2.8 DJANGO=1.8 -- PYMONGO=2.8 DJANGO=1.7 -- PYMONGO=2.8 DJANGO=1.6 -- PYMONGO=2.8 DJANGO=1.5 +- PYMONGO=2.7 +- PYMONGO=2.8 +# - PYMONGO=3.0 +# - PYMONGO=dev matrix: - exclude: - - python: '2.6' - env: PYMONGO=2.7 DJANGO=dev - - python: '2.6' - env: PYMONGO=2.8 - - python: '2.6' - env: PYMONGO=2.7 DJANGO=1.7 - - python: '2.6' - env: PYMONGO=2.8 DJANGO=1.7 - - python: '2.6' - env: PYMONGO=2.7 DJANGO=1.8 - - python: '2.6' - env: PYMONGO=2.8 DJANGO=1.8 fast_finish: true before_install: - travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 @@ -44,9 +25,9 @@ install: libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - travis_retry pip install tox>=1.9 coveralls -- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test +- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test script: -- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO-dj$DJANGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage +- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage after_script: coveralls --verbose notifications: irc: irc.freenode.org#mongoengine diff --git a/README.rst b/README.rst index 5a873e225..2d17313f0 100644 --- a/README.rst +++ b/README.rst @@ -38,12 +38,11 @@ Dependencies Optional Dependencies --------------------- -- **Django Integration:** Django>=1.4.0 for Python 2.x or PyPy and Django>=1.5.0 for Python 3.x - **Image Fields**: Pillow>=2.0.0 - dateutil>=2.1.0 .. note - MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: Django 1.6.5 + MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: PyMongo 3.0.1 Examples ======== diff --git a/tox.ini b/tox.ini index 38517bdcd..3f69b5e85 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] -envlist = - {py27,py32,py33,py34,pypy,pypy3}-{mg27,mg28,mg30}-{dj15,dj16,dj17,dj18,djdev}, - py26-{mg27,mg28,mg30}-{dj15,dj16} +envlist = {py26,py27,py32,py33,py34,pypy,pypy3}-{mg27,mg28} +#envlist = {py26,py27,py32,py33,py34,pypy,pypy3}-{mg27,mg28,mg30,mgdev} [testenv] commands = @@ -11,8 +10,3 @@ deps = mg28: PyMongo>=2.8,<3.0 mg30: PyMongo>=3.0 mgdev: https://github.com/mongodb/mongo-python-driver/tarball/master - dj15: Django<1.6 - dj16: Django>=1.6,<1.7 - dj17: Django>=1.7,<1.8 - dj18: Django>=1.8,<1.9 - djdev: https://github.com/django/django/tarball/master From 319f1deceb75499dc0c962a0fe8fdd7436f22894 Mon Sep 17 00:00:00 2001 From: Sridhar Sundarraman Date: Sun, 12 Apr 2015 17:28:26 +0300 Subject: [PATCH 0114/1519] Unit Test to Demonstrate #954 --- tests/document/validation.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/document/validation.py b/tests/document/validation.py index 573c2e578..646efddbe 100644 --- a/tests/document/validation.py +++ b/tests/document/validation.py @@ -165,6 +165,48 @@ class Doc(Document): self.assertRaises(ValidationError, lambda: d2.validate()) + def test_parent_reference_in_child_document(self): + """ Test to demonstrate behavior in Issue #954 + """ + class Parent(Document): + meta = {'allow_inheritance': True} + reference = ReferenceField('self') + + class Child(Parent): + pass + + parent = Parent() + parent.save() + + child = Child(reference=parent) + try: + # Saving child should not raise a ValidationError + child.save() + except ValidationError as e: + self.fail("test should not throw validation error. %s" % e.message) + + def test_parent_reference_set_as_attribute_in_child_document_(self): + """ Test to demonstrate behavior (when set as attribute) in Issue #954 + """ + class Parent(Document): + meta = {'allow_inheritance': True} + reference = ReferenceField('self') + + class Child(Parent): + pass + + parent = Parent() + parent.save() + + child = Child() + child.reference = parent + + try: + # Saving the child should not raise a ValidationError + child.save() + except ValidationError as e: + self.fail("test should not throw validation error. %s" % e.message) + if __name__ == '__main__': unittest.main() From 5d6a28954b70fd14b140978040d343d1f2f28982 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Tue, 28 Apr 2015 15:07:22 -0400 Subject: [PATCH 0115/1519] Reflect Inheritance in Field's 'owner_document' The 'owner_document' property of a Field now reflects the parent field which first contained the Field when a Document in inherited. Fixes #954 Closes #955 --- mongoengine/base/fields.py | 17 ++++++++++++----- mongoengine/base/metaclasses.py | 3 ++- tests/document/validation.py | 26 ++++++++++++-------------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index aa16804e6..08fbc9b7b 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -83,6 +83,7 @@ def __init__(self, db_field=None, name=None, required=False, default=None, self.help_text = help_text self.null = null self.sparse = sparse + self._owner_document = None # Adjust the appropriate creation counter, and save our local copy. if self.db_field == '_id': @@ -189,6 +190,17 @@ def _validate(self, value, **kwargs): self.validate(value, **kwargs) + @property + def owner_document(self): + return self._owner_document + + def _set_owner_document(self, owner_document): + self._owner_document = owner_document + + @owner_document.setter + def owner_document(self, owner_document): + self._set_owner_document(owner_document) + class ComplexBaseField(BaseField): @@ -398,11 +410,6 @@ def _set_owner_document(self, owner_document): self.field.owner_document = owner_document self._owner_document = owner_document - def _get_owner_document(self, owner_document): - self._owner_document = owner_document - - owner_document = property(_get_owner_document, _set_owner_document) - class ObjectIdField(BaseField): diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index 48da84f42..7a104da97 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -176,7 +176,8 @@ def __new__(cls, name, bases, attrs): # Handle delete rules for field in new_class._fields.itervalues(): f = field - f.owner_document = new_class + if f.owner_document is None: + f.owner_document = new_class delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING) if isinstance(f, CachedReferenceField): diff --git a/tests/document/validation.py b/tests/document/validation.py index 646efddbe..8498a7405 100644 --- a/tests/document/validation.py +++ b/tests/document/validation.py @@ -166,7 +166,9 @@ class Doc(Document): self.assertRaises(ValidationError, lambda: d2.validate()) def test_parent_reference_in_child_document(self): - """ Test to demonstrate behavior in Issue #954 + """ + Test to ensure a ReferenceField can store a reference to a parent + class when inherited. Issue #954. """ class Parent(Document): meta = {'allow_inheritance': True} @@ -179,14 +181,14 @@ class Child(Parent): parent.save() child = Child(reference=parent) - try: - # Saving child should not raise a ValidationError - child.save() - except ValidationError as e: - self.fail("test should not throw validation error. %s" % e.message) - def test_parent_reference_set_as_attribute_in_child_document_(self): - """ Test to demonstrate behavior (when set as attribute) in Issue #954 + # Saving child should not raise a ValidationError + child.save() + + def test_parent_reference_set_as_attribute_in_child_document(self): + """ + Test to ensure a ReferenceField can store a reference to a parent + class when inherited and when set via attribute. Issue #954. """ class Parent(Document): meta = {'allow_inheritance': True} @@ -201,12 +203,8 @@ class Child(Parent): child = Child() child.reference = parent - try: - # Saving the child should not raise a ValidationError - child.save() - except ValidationError as e: - self.fail("test should not throw validation error. %s" % e.message) - + # Saving the child should not raise a ValidationError + child.save() if __name__ == '__main__': unittest.main() From f2cbcea6d77e5fd9408aa5ec6c6e6af5c3683f46 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Wed, 29 Apr 2015 14:26:05 -0400 Subject: [PATCH 0116/1519] Unit Tests for #954 Fail on Exception, not Error --- tests/document/validation.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/document/validation.py b/tests/document/validation.py index 8498a7405..ba03366e5 100644 --- a/tests/document/validation.py +++ b/tests/document/validation.py @@ -183,7 +183,10 @@ class Child(Parent): child = Child(reference=parent) # Saving child should not raise a ValidationError - child.save() + try: + child.save() + except ValidationError as e: + self.fail("ValidationError raised: %s" % e.message) def test_parent_reference_set_as_attribute_in_child_document(self): """ @@ -204,7 +207,11 @@ class Child(Parent): child.reference = parent # Saving the child should not raise a ValidationError - child.save() + try: + child.save() + except ValidationError as e: + self.fail("ValidationError raised: %s" % e.message) + if __name__ == '__main__': unittest.main() From 7726be94be5e86b74e28d40af272a9365566cbea Mon Sep 17 00:00:00 2001 From: elephant Date: Mon, 6 Apr 2015 20:21:36 +0500 Subject: [PATCH 0117/1519] fixed wrong _delta results on nested MapFields #931 --- mongoengine/dereference.py | 3 ++- tests/document/delta.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 453ba14f9..0f60c2f54 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -220,7 +220,8 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): elif isinstance(v, (dict, SON)) and '_ref' in v: data[k]._data[field_name] = self.object_map.get(v['_ref'].id, v) elif isinstance(v, dict) and depth <= self.max_depth: - data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name) + item_name = "%s.%s.%s" % (name, k, field_name) + data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name) elif isinstance(v, (list, tuple)) and depth <= self.max_depth: data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: diff --git a/tests/document/delta.py b/tests/document/delta.py index 7ec6ee56a..95a03861c 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -827,5 +827,29 @@ class User(Document): org2.reload() self.assertEqual(org2.name, 'New Org 2') + def test_delta_for_nested_map_fields(self): + class EmbeddedRole(EmbeddedDocument): + type = StringField() + + class EmbeddedUser(EmbeddedDocument): + name = StringField() + roles = MapField(field=EmbeddedDocumentField(EmbeddedRole)) + + class Doc(Document): + users = MapField(field=EmbeddedDocumentField(EmbeddedUser)) + num = IntField(default=-1) + + Doc.drop_collection() + + doc = Doc(num=1) + doc.users["007"] = EmbeddedUser(name="Agent007") + doc.save() + + d = Doc.objects(num=1).first() + d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin") + delta = d._delta() + self.assertEqual(True, "users.007.roles.666" in delta[0]) + self.assertEqual('superadmin', delta[0]["users.007.roles.666"]["type"]) + if __name__ == '__main__': unittest.main() From 31f7769199b1c0128ec66c0612a568061d4f22b3 Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Thu, 9 Apr 2015 09:14:12 +0500 Subject: [PATCH 0118/1519] percent string formatting changed to format method --- mongoengine/dereference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 0f60c2f54..cc710f4d1 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -220,7 +220,7 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): elif isinstance(v, (dict, SON)) and '_ref' in v: data[k]._data[field_name] = self.object_map.get(v['_ref'].id, v) elif isinstance(v, dict) and depth <= self.max_depth: - item_name = "%s.%s.%s" % (name, k, field_name) + item_name = "{0}.{1}.{2}".format(name, k, field_name) data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name) elif isinstance(v, (list, tuple)) and depth <= self.max_depth: data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name) From ebaba95eb376c761dd4d439a6836f2353dea674b Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Thu, 9 Apr 2015 09:34:40 +0500 Subject: [PATCH 0119/1519] fixed same bug for nested List inside MapField, little code refactoring, added test for nested list and nested reference fields --- mongoengine/dereference.py | 4 +--- tests/document/delta.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index cc710f4d1..0428397c0 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -219,11 +219,9 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): data[k]._data[field_name] = self.object_map.get(v.id, v) elif isinstance(v, (dict, SON)) and '_ref' in v: data[k]._data[field_name] = self.object_map.get(v['_ref'].id, v) - elif isinstance(v, dict) and depth <= self.max_depth: + elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: item_name = "{0}.{1}.{2}".format(name, k, field_name) data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name) - elif isinstance(v, (list, tuple)) and depth <= self.max_depth: - data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: item_name = '%s.%s' % (name, k) if name else name data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name) diff --git a/tests/document/delta.py b/tests/document/delta.py index 95a03861c..e5a81c066 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -828,12 +828,17 @@ class User(Document): self.assertEqual(org2.name, 'New Org 2') def test_delta_for_nested_map_fields(self): + class UInfoDocument(Document): + phone = StringField() + class EmbeddedRole(EmbeddedDocument): type = StringField() class EmbeddedUser(EmbeddedDocument): name = StringField() roles = MapField(field=EmbeddedDocumentField(EmbeddedRole)) + rolist = ListField(field=EmbeddedDocumentField(EmbeddedRole)) + info = ReferenceField(UInfoDocument) class Doc(Document): users = MapField(field=EmbeddedDocumentField(EmbeddedUser)) @@ -845,11 +850,21 @@ class Doc(Document): doc.users["007"] = EmbeddedUser(name="Agent007") doc.save() + uinfo = UInfoDocument(phone="79089269066") + uinfo.save() + d = Doc.objects(num=1).first() d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin") + d.users["007"]["rolist"].append(EmbeddedRole(type="oops")) + d.users["007"]["info"] = uinfo delta = d._delta() + print delta self.assertEqual(True, "users.007.roles.666" in delta[0]) + self.assertEqual(True, "users.007.rolist" in delta[0]) + self.assertEqual(True, "users.007.info" in delta[0]) self.assertEqual('superadmin', delta[0]["users.007.roles.666"]["type"]) + self.assertEqual('oops', delta[0]["users.007.rolist"][0]["type"]) + self.assertEqual(uinfo.id, delta[0]["users.007.info"]) if __name__ == '__main__': unittest.main() From a4f2f811d3e383b6e58a2a2ba9a6506d3a0bf08e Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Thu, 9 Apr 2015 09:47:38 +0500 Subject: [PATCH 0120/1519] removed forgotten print --- tests/document/delta.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/document/delta.py b/tests/document/delta.py index e5a81c066..de9405772 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -858,7 +858,6 @@ class Doc(Document): d.users["007"]["rolist"].append(EmbeddedRole(type="oops")) d.users["007"]["info"] = uinfo delta = d._delta() - print delta self.assertEqual(True, "users.007.roles.666" in delta[0]) self.assertEqual(True, "users.007.rolist" in delta[0]) self.assertEqual(True, "users.007.info" in delta[0]) From ca16050681b888e61fb2dc484845482176f4e7b7 Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 30 Apr 2015 16:02:50 -0400 Subject: [PATCH 0121/1519] Update docs with NotUniqueError This was changed in https://github.com/MongoEngine/mongoengine/pull/62. It is present in versions > 0.7 http://docs.mongoengine.org/changelog.html#changes-in-0-7-0. I can reopen against an older branch if preferred. --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 5c45c19a2..6f2495351 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -401,7 +401,7 @@ MongoEngine allows you to specify that a field should be unique across a collection by providing ``unique=True`` to a :class:`~mongoengine.fields.Field`\ 's constructor. If you try to save a document that has the same value for a unique field as a document that is already in the database, a -:class:`~mongoengine.OperationError` will be raised. You may also specify +:class:`~mongoengine.NotUniqueError` will be raised. You may also specify multi-field uniqueness constraints by using :attr:`unique_with`, which may be either a single field name, or a list or tuple of field names:: From a512ccca28ea2b9645b72f7e2a6593409dd02f2b Mon Sep 17 00:00:00 2001 From: David Bordeynik Date: Wed, 29 Apr 2015 15:11:48 +0300 Subject: [PATCH 0122/1519] fix-#453: Queryset update doesn't go through field validation --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 7 +++++++ mongoengine/fields.py | 24 +++++++++++++++--------- mongoengine/queryset/transform.py | 5 +---- tests/queryset/queryset.py | 14 ++++++++++++++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ed08c3913..2a6f78025 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Queryset update doesn't go through field validation #453 - Added support for specifying authentication source as option `authSource` in URI. #967 - Fixed mark_as_changed to handle higher/lower level fields changed. #927 - ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index aa16804e6..6de2bfc6c 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -17,6 +17,11 @@ "ObjectIdField", "GeoJsonBaseField") +UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push', + 'push_all', 'pull', 'pull_all', 'add_to_set', + 'set_on_insert', 'min', 'max']) + + class BaseField(object): """A base class for fields in a MongoDB document. Instances of this class @@ -150,6 +155,8 @@ def to_mongo(self, value): def prepare_query_value(self, op, value): """Prepare a value that is being used in a query for PyMongo. """ + if op in UPDATE_OPERATORS: + self.validate(value) return value def validate(self, value, clean=True): diff --git a/mongoengine/fields.py b/mongoengine/fields.py index dbed6b996..bd6c88d6a 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -108,7 +108,7 @@ def prepare_query_value(self, op, value): # escape unsafe characters which could lead to a re.error value = re.escape(value) value = re.compile(regex % value, flags) - return value + return super(StringField, self).prepare_query_value(op, value) class URLField(StringField): @@ -203,7 +203,7 @@ def prepare_query_value(self, op, value): if value is None: return value - return int(value) + return super(IntField, self).prepare_query_value(op, int(value)) class LongField(BaseField): @@ -238,7 +238,7 @@ def prepare_query_value(self, op, value): if value is None: return value - return long(value) + return super(LongField, self).prepare_query_value(op, long(value)) class FloatField(BaseField): @@ -273,7 +273,7 @@ def prepare_query_value(self, op, value): if value is None: return value - return float(value) + return super(FloatField, self).prepare_query_value(op, float(value)) class DecimalField(BaseField): @@ -347,7 +347,7 @@ def validate(self, value): self.error('Decimal value is too large') def prepare_query_value(self, op, value): - return self.to_mongo(value) + return super(DecimalField, self).prepare_query_value(op, self.to_mongo(value)) class BooleanField(BaseField): @@ -434,7 +434,7 @@ def to_mongo(self, value): return None def prepare_query_value(self, op, value): - return self.to_mongo(value) + return super(DateTimeField, self).prepare_query_value(op, self.to_mongo(value)) class ComplexDateTimeField(StringField): @@ -518,7 +518,7 @@ def to_mongo(self, value): return self._convert_from_datetime(value) def prepare_query_value(self, op, value): - return self._convert_from_datetime(value) + return super(ComplexDateTimeField, self).prepare_query_value(op, self._convert_from_datetime(value)) class EmbeddedDocumentField(BaseField): @@ -569,6 +569,9 @@ def lookup_member(self, member_name): return self.document_type._fields.get(member_name) def prepare_query_value(self, op, value): + if not isinstance(value, self.document_type): + value = self.document_type._from_son(value) + super(EmbeddedDocumentField, self).prepare_query_value(op, value) return self.to_mongo(value) @@ -585,7 +588,7 @@ class GenericEmbeddedDocumentField(BaseField): """ def prepare_query_value(self, op, value): - return self.to_mongo(value) + return super(GenericEmbeddedDocumentField, self).prepare_query_value(op, self.to_mongo(value)) def to_python(self, value): if isinstance(value, dict): @@ -668,7 +671,8 @@ 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) + return super(DynamicField, self).prepare_query_value(op, self.to_mongo(value)) + def validate(self, value, clean=True): if hasattr(value, "validate"): @@ -979,8 +983,10 @@ def to_python(self, value): def prepare_query_value(self, op, value): if value is None: return None + super(ReferenceField, self).prepare_query_value(op, value) return self.to_mongo(value) + def validate(self, value): if not isinstance(value, (self.document_type, DBRef)): diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 03a09dc51..007cf865b 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -3,6 +3,7 @@ import pymongo from bson import SON +from mongoengine.base.fields import UPDATE_OPERATORS from mongoengine.connection import get_connection from mongoengine.common import _import_class from mongoengine.errors import InvalidQueryError, LookUpError @@ -24,10 +25,6 @@ MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS + STRING_OPERATORS + CUSTOM_OPERATORS) -UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push', - 'push_all', 'pull', 'pull_all', 'add_to_set', - 'set_on_insert', 'min', 'max') - def query(_doc_cls=None, _field_operation=False, **query): """Transform a query from Django-style format to Mongo format. diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 6cbac4956..ac282c445 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -602,6 +602,20 @@ def test_update_results(self): set__name="bobby", multi=True) self.assertEqual(result, 2) + def test_update_validate(self): + class EmDoc(EmbeddedDocument): + str_f = StringField() + + class Doc(Document): + str_f = StringField() + dt_f = DateTimeField() + cdt_f = ComplexDateTimeField() + ed_f = EmbeddedDocumentField(EmDoc) + + self.assertRaises(ValidationError, Doc.objects().update, str_f=1, upsert=True) + self.assertRaises(ValidationError, Doc.objects().update, dt_f="datetime", upsert=True) + self.assertRaises(ValidationError, Doc.objects().update, ed_f__str_f=1, upsert=True) + def test_upsert(self): self.Person.drop_collection() From e5cf76b460c423a86e43451ff9d123b02e7afe82 Mon Sep 17 00:00:00 2001 From: Olivier Le Floch Date: Mon, 4 May 2015 15:02:01 -0700 Subject: [PATCH 0123/1519] Match other code blocks This fixes rendering on the documentation website: http://docs.mongoengine.org/upgrade.html --- docs/upgrade.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 2648c4bbc..b1a2217ff 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -5,9 +5,7 @@ Upgrading 0.9.0 ***** -The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: - -.. code-block:: +The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: :: pip uninstall pymongo pip uninstall mongoengine From 4b8344082f3f47e7fdcc31ddce407ab812d62ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20Kali=C5=A1nik?= Date: Tue, 5 May 2015 12:49:45 +0200 Subject: [PATCH 0124/1519] Testing if we can query embedded document's field inside MapField. Part of #912, which is fixed in 0.9. --- tests/fields/fields.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index e7a87ed0c..c29678565 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1431,16 +1431,26 @@ class Test(Document): def test_map_field_lookup(self): """Ensure MapField lookups succeed on Fields without a lookup method""" + class Action(EmbeddedDocument): + operation = StringField() + object = StringField() + class Log(Document): name = StringField() visited = MapField(DateTimeField()) + actions = MapField(EmbeddedDocumentField(Action)) Log.drop_collection() - Log(name="wilson", visited={'friends': datetime.datetime.now()}).save() + Log(name="wilson", visited={'friends': datetime.datetime.now()}, + actions={'friends': Action(operation='drink', object='beer')}).save() self.assertEqual(1, Log.objects( visited__friends__exists=True).count()) + self.assertEqual(1, Log.objects( + actions__friends__operation='drink', + actions__friends__object='beer').count()) + def test_embedded_db_field(self): class Embedded(EmbeddedDocument): From 9bdc320cf84a2d796ddeb024101db2d9e344754d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 6 May 2015 11:25:45 +0200 Subject: [PATCH 0125/1519] dont send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769) --- AUTHORS | 3 +- docs/changelog.rst | 1 + mongoengine/document.py | 12 ++++++++ tests/document/indexes.py | 58 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index af87d5039..9d105a606 100644 --- a/AUTHORS +++ b/AUTHORS @@ -119,7 +119,7 @@ that much better: * Anton Kolechkin * Sergey Nikitin * psychogenic - * Stefan Wójcik + * Stefan Wójcik (https://github.com/wojcikstefan) * dimonb * Garry Polley * James Slagle @@ -138,7 +138,6 @@ that much better: * hellysmile * Jaepil Jeong * Daniil Sharou - * Stefan Wójcik * Pete Campton * Martyn Smith * Marcelo Anton diff --git a/docs/changelog.rst b/docs/changelog.rst index 2a6f78025..f4b5ec6c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,7 @@ Changes in 0.9.X - DEV - Fixed unpickled documents replacing the global field's list. #888 - Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 - Django support was removed and will be available as a separate extension. #958 +- Don't send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769) Changes in 0.9.0 ================ diff --git a/mongoengine/document.py b/mongoengine/document.py index f8275021d..01083d243 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -677,6 +677,12 @@ def ensure_indexes(cls): cls_indexed = cls_indexed or includes_cls(fields) opts = index_opts.copy() opts.update(spec) + + # we shouldn't pass 'cls' to the collection.ensureIndex options + # because of https://jira.mongodb.org/browse/SERVER-769 + if 'cls' in opts: + del opts['cls'] + collection.ensure_index(fields, background=background, drop_dups=drop_dups, **opts) @@ -684,6 +690,12 @@ def ensure_indexes(cls): # only if another index doesn't begin with _cls if (index_cls and not cls_indexed and cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True): + + # we shouldn't pass 'cls' to the collection.ensureIndex options + # because of https://jira.mongodb.org/browse/SERVER-769 + if 'cls' in index_opts: + del index_opts['cls'] + collection.ensure_index('_cls', background=background, **index_opts) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index ab156f68f..6256cde30 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -826,5 +826,63 @@ class BlogPost(Document): post2 = BlogPost(title='test2', slug='test') self.assertRaises(NotUniqueError, post2.save) + def test_index_dont_send_cls_option(self): + """ + Ensure that 'cls' option is not sent through ensureIndex. We shouldn't + send internal MongoEngine arguments that are not a part of the index + spec. + + This is directly related to the fact that MongoDB doesn't validate the + options that are passed to ensureIndex. For more details, see: + https://jira.mongodb.org/browse/SERVER-769 + """ + class TestDoc(Document): + txt = StringField() + + meta = { + 'allow_inheritance': True, + 'indexes': [ + { 'fields': ('txt',), 'cls': False } + ] + } + + class TestChildDoc(TestDoc): + txt2 = StringField() + + meta = { + 'indexes': [ + { 'fields': ('txt2',), 'cls': False } + ] + } + + TestDoc.drop_collection() + TestDoc.ensure_indexes() + TestChildDoc.ensure_indexes() + + index_info = TestDoc._get_collection().index_information() + for key in index_info: + del index_info[key]['v'] # drop the index version - we don't care about that here + + self.assertEqual(index_info, { + 'txt_1': { + 'key': [('txt', 1)], + 'dropDups': False, + 'background': False + }, + '_id_': { + 'key': [('_id', 1)], + }, + 'txt2_1': { + 'key': [('txt2', 1)], + 'dropDups': False, + 'background': False + }, + '_cls_1': { + 'key': [('_cls', 1)], + 'background': False, + } + }) + + if __name__ == '__main__': unittest.main() From 0949df014b7f686f7e2b705fdcc6a556dd11bf51 Mon Sep 17 00:00:00 2001 From: rma4ok Date: Wed, 29 Apr 2015 01:14:48 -0400 Subject: [PATCH 0126/1519] [fix] SortedListField: update whole list if order is changed --- mongoengine/base/document.py | 7 +++++++ tests/fields/fields.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index d31d75ba9..3eba16ca8 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -547,6 +547,7 @@ def _get_changed_fields(self, inspected=None): EmbeddedDocument = _import_class("EmbeddedDocument") DynamicEmbeddedDocument = _import_class("DynamicEmbeddedDocument") ReferenceField = _import_class("ReferenceField") + SortedListField = _import_class("SortedListField") changed_fields = [] changed_fields += getattr(self, '_changed_fields', []) @@ -577,6 +578,12 @@ def _get_changed_fields(self, inspected=None): if (hasattr(field, 'field') and isinstance(field.field, ReferenceField)): continue + elif (isinstance(field, SortedListField) and field._ordering): + # if ordering is affected whole list is changed + if any(map(lambda d: field._ordering in d._changed_fields, data)): + changed_fields.append(db_field_name) + continue + self._nestable_types_changed_fields( changed_fields, key, data, inspected) return changed_fields diff --git a/tests/fields/fields.py b/tests/fields/fields.py index c29678565..7a99bd78a 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -916,6 +916,13 @@ class BlogPost(Document): self.assertEqual(post.comments[0].content, comment2.content) self.assertEqual(post.comments[1].content, comment1.content) + post.comments[0].order = 2 + post.save() + post.reload() + + self.assertEqual(post.comments[0].content, comment1.content) + self.assertEqual(post.comments[1].content, comment2.content) + BlogPost.drop_collection() def test_reverse_list_sorting(self): From 43a5df87807d138d7b6c4b97a3059ab3c996057b Mon Sep 17 00:00:00 2001 From: rma4ok Date: Wed, 29 Apr 2015 11:23:51 -0400 Subject: [PATCH 0127/1519] [dist] Adding rma4ok to contributors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 9d105a606..6745e14b3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -220,3 +220,4 @@ that much better: * Michael Chase (https://github.com/rxsegrxup) * Eremeev Danil (https://github.com/elephanter) * Catstyle Lee (https://github.com/Catstyle) + * Kiryl Yermakou (https://github.com/rma4ok) From 0ee3203a5a6d3ac1ed43575e2ac4334578000b8b Mon Sep 17 00:00:00 2001 From: rma4ok Date: Wed, 29 Apr 2015 11:34:04 -0400 Subject: [PATCH 0128/1519] [docs] Adding SortedListField fix to changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index f4b5ec6c6..a27db5a4f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ Changes in 0.9.X - DEV - Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 - Django support was removed and will be available as a separate extension. #958 - Don't send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769) +- Fix for updating sorting in SortedListField. #978 Changes in 0.9.0 ================ From fc39fd75193d2ba11811409217e0e1b2d25447a8 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 6 May 2015 18:30:49 +0200 Subject: [PATCH 0129/1519] Update defining-documents.rst Add primary_key notice according to issue #985 --- docs/guide/defining-documents.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 6f2495351..274f0f43e 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -146,6 +146,8 @@ arguments can be set on all fields: When True, use this field as a primary key for the collection. `DictField` and `EmbeddedDocuments` both support being the primary key for a document. + .. note:: If set, this field is also accessible throught the `pk` field. + :attr:`choices` (Default: None) An iterable (e.g. a list or tuple) of choices to which the value of this field should be limited. From 5f33d298d70f283ffcec3a0320efcaac1b6858ec Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 6 May 2015 21:32:36 +0200 Subject: [PATCH 0130/1519] Fix typo in guide/defining-documents.rst --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 274f0f43e..56370c14f 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -146,7 +146,7 @@ arguments can be set on all fields: When True, use this field as a primary key for the collection. `DictField` and `EmbeddedDocuments` both support being the primary key for a document. - .. note:: If set, this field is also accessible throught the `pk` field. + .. note:: If set, this field is also accessible through the `pk` field. :attr:`choices` (Default: None) An iterable (e.g. a list or tuple) of choices to which the value of this From 63d55cb797cf835b34db2bbb3bc7a23d0235e6d7 Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Thu, 9 Apr 2015 16:23:28 +0500 Subject: [PATCH 0131/1519] solution for #949 --- mongoengine/queryset/transform.py | 3 +++ tests/queryset/queryset.py | 37 +++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 007cf865b..b08d450ac 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -44,6 +44,9 @@ def query(_doc_cls=None, _field_operation=False, **query): if len(parts) > 1 and parts[-1] in MATCH_OPERATORS: op = parts.pop() + if len(parts) > 1 and not parts[-1]: + parts.pop() + negate = False if len(parts) > 1 and parts[-1] == 'not': parts.pop() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index ac282c445..5d21cb0c5 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1328,7 +1328,7 @@ class BlogPost(Document): self.assertEqual(1, BlogPost.objects.count()) def test_reverse_delete_rule_cascade_on_abstract_document(self): - """Ensure cascading deletion of referring documents from the database + """Ensure cascading deletion of referring documents from the database does not fail on abstract document. """ class AbstractBlogPost(Document): @@ -1350,7 +1350,7 @@ class BlogPost(AbstractBlogPost): self.assertEqual(3, BlogPost.objects.count()) 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 @@ -1411,8 +1411,8 @@ class BlogPost(Document): self.assertEqual(1, BlogPost.objects.count()) self.assertEqual(None, BlogPost.objects.first().category) - def test_reverse_delete_rule_nullify_on_abstract_document(self): - """Ensure nullification of references to deleted documents when + def test_reverse_delete_rule_nullify_on_abstract_document(self): + """Ensure nullification of references to deleted documents when reference is on an abstract document. """ class AbstractBlogPost(Document): @@ -1474,7 +1474,7 @@ class BlogPost(AbstractBlogPost): self.assertEqual(1, BlogPost.objects.count()) self.assertRaises(OperationError, self.Person.objects.delete) - + def test_reverse_delete_rule_pull(self): """Ensure pulling of references to deleted documents. """ @@ -1511,9 +1511,9 @@ def test_reverse_delete_rule_pull_on_abstract_documents(self): """ class AbstractBlogPost(Document): meta = {'abstract': True} - authors = ListField(ReferenceField(self.Person, + authors = ListField(ReferenceField(self.Person, reverse_delete_rule=PULL)) - + class BlogPost(AbstractBlogPost): content = StringField() @@ -1538,7 +1538,7 @@ class BlogPost(AbstractBlogPost): self.assertEqual(post.authors, [me]) self.assertEqual(another.authors, []) - + def test_delete_with_limits(self): class Log(Document): @@ -3009,7 +3009,7 @@ class Book(Document): def test_distinct_ListField_EmbeddedDocumentField_EmbeddedDocumentField(self): class Continent(EmbeddedDocument): continent_name = StringField() - + class Country(EmbeddedDocument): country_name = StringField() continent = EmbeddedDocumentField(Continent) @@ -3026,7 +3026,7 @@ class Book(Document): europe = Continent(continent_name='europe') asia = Continent(continent_name='asia') - + scotland = Country(country_name="Scotland", continent=europe) tibet = Country(country_name="Tibet", continent=asia) @@ -3041,9 +3041,9 @@ class Book(Document): country_list = Book.objects.distinct("authors.country") self.assertEqual(country_list, [scotland, tibet]) - + continent_list = Book.objects.distinct("authors.country.continent") - + self.assertEqual(continent_list, [europe, asia]) def test_distinct_ListField_ReferenceField(self): @@ -4592,6 +4592,19 @@ class ScottishCat(Cat): self.assertEquals(Animal.objects(folded_ears=True).count(), 1) self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1) + def test_last_field_name_like_operator(self): + class EmbeddedItem(EmbeddedDocument): + type = StringField() + + class Doc(Document): + item = EmbeddedDocumentField(EmbeddedItem) + + Doc.drop_collection() + + doc = Doc(item=EmbeddedItem(type="axe")) + doc.save() + + self.assertEqual(1, Doc.objects(item__type__="axe").count()) if __name__ == '__main__': unittest.main() From dfa8eaf24e5fd2de6673807a8b6959f256270aa1 Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Tue, 5 May 2015 12:00:46 +0500 Subject: [PATCH 0132/1519] Added changeset, updated documentation and tests, changed test condition --- docs/changelog.rst | 1 + docs/guide/querying.rst | 9 ++++++++- mongoengine/queryset/transform.py | 3 ++- tests/queryset/queryset.py | 4 +++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a27db5a4f..536765628 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,7 @@ Changes in 0.9.X - DEV - Django support was removed and will be available as a separate extension. #958 - Don't send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769) - Fix for updating sorting in SortedListField. #978 +- Added __ support to escape field name in fields lookup keywords that match operators names #949 Changes in 0.9.0 ================ diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 9d1dfd760..0a0354696 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -39,6 +39,14 @@ syntax:: # been written by a user whose 'country' field is set to 'uk' uk_pages = Page.objects(author__country='uk') +.. note:: + + (version **0.9.0+**) if your field name is like mongodb operator name (for example + type, lte, lt...) and you want to place it at the end of lookup keyword + mongoengine automatically prepend $ to it. To avoid this use __ at the end of + your lookup keyword. For example if you field name is ``type`` and you want to + query by this field you must use ``.objects(user__type__="admin")`` instead of + ``.objects(user__type="admin")`` Query operators =============== @@ -663,4 +671,3 @@ following example shows how the substitutions are made:: return comments; } """) - diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index b08d450ac..8ca3203c2 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -44,7 +44,8 @@ def query(_doc_cls=None, _field_operation=False, **query): if len(parts) > 1 and parts[-1] in MATCH_OPERATORS: op = parts.pop() - if len(parts) > 1 and not parts[-1]: + #if user escape field name by __ + if len(parts) > 1 and parts[-1]=="": parts.pop() negate = False diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 5d21cb0c5..30c59bfa0 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4595,16 +4595,18 @@ class ScottishCat(Cat): def test_last_field_name_like_operator(self): class EmbeddedItem(EmbeddedDocument): type = StringField() + name = StringField() class Doc(Document): item = EmbeddedDocumentField(EmbeddedItem) Doc.drop_collection() - doc = Doc(item=EmbeddedItem(type="axe")) + doc = Doc(item=EmbeddedItem(type="axe", name="Heroic axe")) doc.save() self.assertEqual(1, Doc.objects(item__type__="axe").count()) + self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count()) if __name__ == '__main__': unittest.main() From aab0599280be45e202191ec668db1beba31a79a1 Mon Sep 17 00:00:00 2001 From: Eremeev Danil Date: Thu, 7 May 2015 10:53:25 +0500 Subject: [PATCH 0133/1519] test moved to another file, cosmetical fixes --- docs/guide/querying.rst | 4 ++-- mongoengine/queryset/transform.py | 2 +- tests/queryset/queryset.py | 15 --------------- tests/queryset/transform.py | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 0a0354696..9861ce564 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -41,10 +41,10 @@ syntax:: .. note:: - (version **0.9.0+**) if your field name is like mongodb operator name (for example + (version **0.9.1+**) if your field name is like mongodb operator name (for example type, lte, lt...) and you want to place it at the end of lookup keyword mongoengine automatically prepend $ to it. To avoid this use __ at the end of - your lookup keyword. For example if you field name is ``type`` and you want to + your lookup keyword. For example if your field name is ``type`` and you want to query by this field you must use ``.objects(user__type__="admin")`` instead of ``.objects(user__type="admin")`` diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 8ca3203c2..68adefbcb 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -45,7 +45,7 @@ def query(_doc_cls=None, _field_operation=False, **query): op = parts.pop() #if user escape field name by __ - if len(parts) > 1 and parts[-1]=="": + if len(parts) > 1 and parts[-1] == "": parts.pop() negate = False diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 30c59bfa0..f407c0b74 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4592,21 +4592,6 @@ class ScottishCat(Cat): self.assertEquals(Animal.objects(folded_ears=True).count(), 1) self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1) - def test_last_field_name_like_operator(self): - class EmbeddedItem(EmbeddedDocument): - type = StringField() - name = StringField() - - class Doc(Document): - item = EmbeddedDocumentField(EmbeddedItem) - - Doc.drop_collection() - - doc = Doc(item=EmbeddedItem(type="axe", name="Heroic axe")) - doc.save() - - self.assertEqual(1, Doc.objects(item__type__="axe").count()) - self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count()) if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 2d5261f25..77d3593c2 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -208,6 +208,22 @@ class Doc(Document): self.assertEqual(Doc.objects(df__type=2).count(), 1) # str self.assertEqual(Doc.objects(df__type=16).count(), 1) # int + def test_last_field_name_like_operator(self): + class EmbeddedItem(EmbeddedDocument): + type = StringField() + name = StringField() + + class Doc(Document): + item = EmbeddedDocumentField(EmbeddedItem) + + Doc.drop_collection() + + doc = Doc(item=EmbeddedItem(type="axe", name="Heroic axe")) + doc.save() + + self.assertEqual(1, Doc.objects(item__type__="axe").count()) + self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count()) + if __name__ == '__main__': unittest.main() From a34fa74eaa74a8e0eede9cbb109dae2136f075c7 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 03:40:42 +0200 Subject: [PATCH 0134/1519] fix connection problems with pymongo3 and added tests --- mongoengine/connection.py | 10 ++++++--- tests/test_connection.py | 32 +++++++++++++++++++++++------ tests/test_replicaset_connection.py | 27 +++++++++++++++++++----- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 5e18efb76..31f4cbcc8 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -1,4 +1,5 @@ -from pymongo import MongoClient, MongoReplicaSetClient, uri_parser +import pymongo +from pymongo import MongoClient, ReadPreference, uri_parser __all__ = ['ConnectionError', 'connect', 'register_connection', @@ -6,6 +7,10 @@ DEFAULT_CONNECTION_NAME = 'default' +if pymongo.version_tuple[0] >= 3: + READ_PREFERENCE = ReadPreference.SECONDARY_PREFERRED +else: + READ_PREFERENCE = False class ConnectionError(Exception): @@ -18,7 +23,7 @@ class ConnectionError(Exception): def register_connection(alias, name=None, host=None, port=None, - read_preference=False, + read_preference=READ_PREFERENCE, username=None, password=None, authentication_source=None, **kwargs): """Add a connection. @@ -109,7 +114,6 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): # Discard replicaSet if not base string if not isinstance(conn_settings['replicaSet'], basestring): conn_settings.pop('replicaSet', None) - connection_class = MongoReplicaSetClient try: connection = None diff --git a/tests/test_connection.py b/tests/test_connection.py index 9204d80cc..88e039943 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,4 +1,6 @@ import sys +from time import sleep + sys.path[0:0] = [""] try: @@ -19,6 +21,13 @@ from mongoengine.connection import get_db, get_connection, ConnectionError +def get_tz_awareness(connection): + if pymongo.version_tuple[0] < 3: + return connection.tz_aware + else: + return connection.codec_options.tz_aware + + class ConnectionTest(unittest.TestCase): def tearDown(self): @@ -51,6 +60,9 @@ def test_sharing_connections(self): connect('mongoenginetest', alias='testdb2') actual_connection = get_connection('testdb2') + + # horrible, but since PyMongo3+, connection are created asynchronously + sleep(0.1) self.assertEqual(expected_connection, actual_connection) def test_connect_uri(self): @@ -64,7 +76,8 @@ def test_connect_uri(self): c.admin.authenticate("admin", "password") c.mongoenginetest.add_user("username", "password") - self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') + if pymongo.version_tuple[0] < 3: + self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest') @@ -90,7 +103,8 @@ def test_connect_uri_without_db(self): c.admin.authenticate("admin", "password") c.mongoenginetest.add_user("username", "password") - self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') + if pymongo.version_tuple[0] < 3: + self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') connect("mongoenginetest", host='mongodb://localhost/') @@ -160,11 +174,11 @@ def test_connection_kwargs(self): connect('mongoenginetest', alias='t1', tz_aware=True) conn = get_connection('t1') - self.assertTrue(conn.tz_aware) + self.assertTrue(get_tz_awareness(conn)) connect('mongoenginetest2', alias='t2') conn = get_connection('t2') - self.assertFalse(conn.tz_aware) + self.assertFalse(get_tz_awareness(conn)) def test_datetime(self): connect('mongoenginetest', tz_aware=True) @@ -188,8 +202,14 @@ def test_multiple_connection_settings(self): self.assertEqual(len(mongo_connections.items()), 2) self.assertTrue('t1' in mongo_connections.keys()) self.assertTrue('t2' in mongo_connections.keys()) - self.assertEqual(mongo_connections['t1'].host, 'localhost') - self.assertEqual(mongo_connections['t2'].host, '127.0.0.1') + if pymongo.version_tuple[0] < 3: + self.assertEqual(mongo_connections['t1'].host, 'localhost') + self.assertEqual(mongo_connections['t2'].host, '127.0.0.1') + else: + # horrible, but since PyMongo3+, connection are created asynchronously + sleep(0.1) + self.assertEqual(mongo_connections['t1'].address[0], 'localhost') + self.assertEqual(mongo_connections['t2'].address[0], '127.0.0.1') if __name__ == '__main__': diff --git a/tests/test_replicaset_connection.py b/tests/test_replicaset_connection.py index d27960f7a..b3a7e1bf1 100644 --- a/tests/test_replicaset_connection.py +++ b/tests/test_replicaset_connection.py @@ -3,15 +3,29 @@ import unittest import pymongo -from pymongo import ReadPreference, ReplicaSetConnection +from pymongo import ReadPreference + +if pymongo.version_tuple[0] >= 3: + from pymongo import MongoClient + CONN_CLASS = MongoClient + READ_PREF = ReadPreference.SECONDARY +else: + from pymongo import ReplicaSetConnection + CONN_CLASS = ReplicaSetConnection + READ_PREF = ReadPreference.SECONDARY_ONLY import mongoengine from mongoengine import * -from mongoengine.connection import get_db, get_connection, ConnectionError +from mongoengine.connection import ConnectionError class ConnectionTest(unittest.TestCase): + def setUp(self): + mongoengine.connection._connection_settings = {} + mongoengine.connection._connections = {} + mongoengine.connection._dbs = {} + def tearDown(self): mongoengine.connection._connection_settings = {} mongoengine.connection._connections = {} @@ -22,14 +36,17 @@ def test_replicaset_uri_passes_read_preference(self): """ try: - conn = connect(db='mongoenginetest', host="mongodb://localhost/mongoenginetest?replicaSet=rs", read_preference=ReadPreference.SECONDARY_ONLY) + conn = connect(db='mongoenginetest', + host="mongodb://localhost/mongoenginetest?replicaSet=rs", + read_preference=READ_PREF) except ConnectionError, e: return - if not isinstance(conn, ReplicaSetConnection): + if not isinstance(conn, CONN_CLASS): + # really??? return - self.assertEqual(conn.read_preference, ReadPreference.SECONDARY_ONLY) + self.assertEqual(conn.read_preference, READ_PREF) if __name__ == '__main__': unittest.main() From 3b8f31c888b0e74e818de61bae83f5d3ca1ff23c Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 03:43:49 +0200 Subject: [PATCH 0135/1519] fix problems with cursor arguments --- mongoengine/queryset/base.py | 38 ++++++++++++++++++++++++------------ tests/queryset/queryset.py | 29 ++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7ffb9976f..11c92ece5 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -158,7 +158,8 @@ def __getitem__(self, key): if queryset._as_pymongo: return queryset._get_as_pymongo(queryset._cursor[key]) return queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference, only_fields=self.only_fields) + _auto_dereference=self._auto_dereference, + only_fields=self.only_fields) raise AttributeError @@ -423,7 +424,9 @@ def delete(self, write_concern=None, _from_doc_delete=False): if call_document_delete: cnt = 0 for doc in queryset: - doc.delete(write_concern=write_concern) + # How the fuck did this worked before ??? + # doc.delete(write_concern=write_concern) + doc.delete(**write_concern) cnt += 1 return cnt @@ -929,6 +932,7 @@ def timeout(self, enabled): queryset._timeout = enabled return queryset + # DEPRECATED. Has no more impact on PyMongo 3+ def slave_okay(self, enabled): """Enable or disable the slave_okay when querying. @@ -1383,22 +1387,30 @@ def _collection(self): @property def _cursor_args(self): - cursor_args = { - 'snapshot': self._snapshot, - 'timeout': self._timeout - } - if self._read_preference is not None: - cursor_args['read_preference'] = self._read_preference + if pymongo.version_tuple[0] < 3: + fields_name = 'fields' + cursor_args = { + 'timeout': self._timeout, + 'snapshot': self._snapshot + } + if self._read_preference is not None: + cursor_args['read_preference'] = self._read_preference + else: + cursor_args['slave_okay'] = self._slave_okay else: - cursor_args['slave_okay'] = self._slave_okay + fields_name = 'projection' + # snapshot seems not to be handled at all by PyMongo 3+ + cursor_args = { + 'no_cursor_timeout': self._timeout + } if self._loaded_fields: - cursor_args['fields'] = self._loaded_fields.as_dict() + cursor_args[fields_name] = self._loaded_fields.as_dict() if self._search_text: - if 'fields' not in cursor_args: - cursor_args['fields'] = {} + if fields_name not in cursor_args: + cursor_args[fields_name] = {} - cursor_args['fields']['_text_score'] = {'$meta': "textScore"} + cursor_args[fields_name]['_text_score'] = {'$meta': "textScore"} return cursor_args diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index f407c0b74..a02683615 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -51,6 +51,20 @@ def _inner(*args, **kwargs): return _inner +def skip_pymongo3(f): + def _inner(*args, **kwargs): + + if pymongo.version_tuple[0] >= 3: + raise SkipTest("Useless with PyMongo 3+") + + return f(*args, **kwargs) + + _inner.__name__ = f.__name__ + _inner.__doc__ = f.__doc__ + + return _inner + + class QuerySetTest(unittest.TestCase): def setUp(self): @@ -869,6 +883,8 @@ def test_slave_okay(self): self.assertEqual(person.name, "User A") self.assertEqual(person.age, 20) + @skip_older_mongodb + @skip_pymongo3 def test_cursor_args(self): """Ensures the cursor args can be set as expected """ @@ -2926,8 +2942,12 @@ class News(Document): self.assertEqual(query.count(), 3) self.assertEqual(query._query, {'$text': {'$search': 'brasil'}}) cursor_args = query._cursor_args + if pymongo.version_tuple[0] < 3: + cursor_args_fields = cursor_args['fields'] + else: + cursor_args_fields = cursor_args['projection'] self.assertEqual( - cursor_args['fields'], {'_text_score': {'$meta': 'textScore'}}) + cursor_args_fields, {'_text_score': {'$meta': 'textScore'}}) text_scores = [i.get_text_score() for i in query] self.assertEqual(len(text_scores), 3) @@ -3992,8 +4012,11 @@ class Bar(Document): bars = list(Bar.objects(read_preference=ReadPreference.PRIMARY)) self.assertEqual([], bars) - self.assertRaises(ConfigurationError, Bar.objects, - read_preference='Primary') + if pymongo.version_tuple[0] < 3: + error_class = ConfigurationError + else: + error_class = TypeError + self.assertRaises(error_class, Bar.objects, read_preference='Primary') bars = Bar.objects(read_preference=ReadPreference.SECONDARY_PREFERRED) self.assertEqual( From 36eedc987c1ae7ee03de1392c8550c354f6d3803 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 03:45:25 +0200 Subject: [PATCH 0136/1519] adapted index test to new explain output in pymongo3 and added comment to a possible pymongo3 bug --- tests/document/indexes.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 6256cde30..b1ea37079 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -432,6 +432,7 @@ def test_covered_index(self): class Test(Document): a = IntField() + b = IntField() meta = { 'indexes': ['a'], @@ -446,13 +447,30 @@ class Test(Document): # Need to be explicit about covered indexes as mongoDB doesn't know if # the documents returned might have more keys in that here. query_plan = Test.objects(id=obj.id).exclude('a').explain() - self.assertFalse(query_plan['indexOnly']) + if pymongo.version_tuple[0] < 3: + self.assertFalse(query_plan['indexOnly']) + else: + self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK') query_plan = Test.objects(id=obj.id).only('id').explain() - self.assertTrue(query_plan['indexOnly']) + if pymongo.version_tuple[0] < 3: + self.assertTrue(query_plan['indexOnly']) + else: + self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK') query_plan = Test.objects(a=1).only('a').exclude('id').explain() - self.assertTrue(query_plan['indexOnly']) + if pymongo.version_tuple[0] < 3: + self.assertTrue(query_plan['indexOnly']) + else: + self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN') + self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('stage'), 'PROJECTION') + + query_plan = Test.objects(a=1).explain() + if pymongo.version_tuple[0] < 3: + self.assertFalse(query_plan['indexOnly']) + else: + self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN') + self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('stage'), 'FETCH') def test_index_on_id(self): @@ -491,6 +509,9 @@ class BlogPost(Document): self.assertEqual(BlogPost.objects.count(), 10) self.assertEqual(BlogPost.objects.hint().count(), 10) + # here we seem to have find a bug in PyMongo 3. + # The cursor first makes a SON out of the list of tuples + # Then later reuses it and wonders why is it not a list of tuples self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10) self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10) From 775c8624d42a4f167d20cd792c434b84fd69ed9d Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 03:46:42 +0200 Subject: [PATCH 0137/1519] change to try to address issues due to new save() behaviour, not satisfying, some tests are still failing --- mongoengine/document.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 01083d243..c9304c2b5 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,11 +1,8 @@ -import warnings -import hashlib import pymongo import re from pymongo.read_preferences import ReadPreference -from bson import ObjectId from bson.dbref import DBRef from mongoengine import signals from mongoengine.common import _import_class @@ -19,7 +16,7 @@ ALLOW_INHERITANCE, get_document ) -from mongoengine.errors import ValidationError, InvalidQueryError, InvalidDocumentError +from mongoengine.errors import InvalidQueryError, InvalidDocumentError from mongoengine.queryset import (OperationError, NotUniqueError, QuerySet, transform) from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME @@ -296,7 +293,12 @@ def save(self, force_insert=False, validate=True, clean=True, doc = self.to_mongo() - created = ('_id' not in doc or self._created or force_insert) + # I think the self._created flag is not necessarily required in PyMongo3 + # but may cause test test_collection_name_and_primary to fail + if pymongo.version_tuple[0] < 3: + created = ('_id' not in doc or self._created or force_insert) + else: + created = ('_id' not in doc or force_insert) signals.pre_save_post_validation.send(self.__class__, document=self, created=created) From 46817caa68ac401d9e5084162095840ab8e4b7ff Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 03:47:13 +0200 Subject: [PATCH 0138/1519] various unused imports removed (I am allergic) --- mongoengine/base/datastructures.py | 1 - mongoengine/base/metaclasses.py | 4 +--- mongoengine/python_support.py | 2 +- mongoengine/queryset/transform.py | 2 +- mongoengine/queryset/visitor.py | 3 --- 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 3f7354a3a..91403de90 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -1,5 +1,4 @@ import weakref -import functools import itertools from mongoengine.common import _import_class from mongoengine.errors import DoesNotExist, MultipleObjectsReturned diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index 7a104da97..8a25ff3dc 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -1,13 +1,11 @@ import warnings -import pymongo - from mongoengine.common import _import_class from mongoengine.errors import InvalidDocumentError from mongoengine.python_support import PY3 from mongoengine.queryset import (DO_NOTHING, DoesNotExist, MultipleObjectsReturned, - QuerySet, QuerySetManager) + QuerySetManager) from mongoengine.base.common import _document_registry, ALLOW_INHERITANCE from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField diff --git a/mongoengine/python_support.py b/mongoengine/python_support.py index 2c4df00c2..1214b4909 100644 --- a/mongoengine/python_support.py +++ b/mongoengine/python_support.py @@ -12,7 +12,7 @@ def b(s): return codecs.latin_1_encode(s)[0] bin_type = bytes - txt_type = str + txt_type = str else: try: from cStringIO import StringIO diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 68adefbcb..e8cd2721d 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -6,7 +6,7 @@ from mongoengine.base.fields import UPDATE_OPERATORS from mongoengine.connection import get_connection from mongoengine.common import _import_class -from mongoengine.errors import InvalidQueryError, LookUpError +from mongoengine.errors import InvalidQueryError __all__ = ('query', 'update') diff --git a/mongoengine/queryset/visitor.py b/mongoengine/queryset/visitor.py index e5d2e6152..84365f56b 100644 --- a/mongoengine/queryset/visitor.py +++ b/mongoengine/queryset/visitor.py @@ -1,8 +1,5 @@ import copy -from itertools import product -from functools import reduce - from mongoengine.errors import InvalidQueryError from mongoengine.queryset import transform From ccbd128fa2a297eaf82c5ec8cb4cc34afda07654 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 10:57:19 +0200 Subject: [PATCH 0139/1519] first adaptations after comments and find-outs --- mongoengine/connection.py | 5 ++++- mongoengine/document.py | 8 ++++---- mongoengine/queryset/base.py | 2 -- tests/document/indexes.py | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 31f4cbcc8..0aa040ed6 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -8,8 +8,9 @@ DEFAULT_CONNECTION_NAME = 'default' if pymongo.version_tuple[0] >= 3: - READ_PREFERENCE = ReadPreference.SECONDARY_PREFERRED + READ_PREFERENCE = ReadPreference.PRIMARY else: + from pymongo import MongoReplicaSetClient READ_PREFERENCE = False @@ -126,6 +127,8 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): if conn_settings == connection_settings and _connections.get(db_alias, None): connection = _connections[db_alias] break + if pymongo.version_tuple[0] < 3: + connection_class = MongoReplicaSetClient _connections[alias] = connection if connection else connection_class(**conn_settings) except Exception, e: diff --git a/mongoengine/document.py b/mongoengine/document.py index c9304c2b5..b27bd086e 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -295,10 +295,10 @@ def save(self, force_insert=False, validate=True, clean=True, # I think the self._created flag is not necessarily required in PyMongo3 # but may cause test test_collection_name_and_primary to fail - if pymongo.version_tuple[0] < 3: - created = ('_id' not in doc or self._created or force_insert) - else: - created = ('_id' not in doc or force_insert) + # if pymongo.version_tuple[0] < 3: + created = ('_id' not in doc or self._created or force_insert) + # else: + # created = ('_id' not in doc or force_insert) signals.pre_save_post_validation.send(self.__class__, document=self, created=created) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 11c92ece5..09a4c3bca 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -424,8 +424,6 @@ def delete(self, write_concern=None, _from_doc_delete=False): if call_document_delete: cnt = 0 for doc in queryset: - # How the fuck did this worked before ??? - # doc.delete(write_concern=write_concern) doc.delete(**write_concern) cnt += 1 return cnt diff --git a/tests/document/indexes.py b/tests/document/indexes.py index b1ea37079..b8b3ba0c7 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -509,12 +509,12 @@ class BlogPost(Document): self.assertEqual(BlogPost.objects.count(), 10) self.assertEqual(BlogPost.objects.hint().count(), 10) - # here we seem to have find a bug in PyMongo 3. - # The cursor first makes a SON out of the list of tuples - # Then later reuses it and wonders why is it not a list of tuples - self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10) - self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10) + # PyMongo 3.0 bug + if pymongo.version != '3.0': + self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10) + + self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10) if pymongo.version >= '2.8': self.assertEqual(BlogPost.objects.hint('tags').count(), 10) From c0f149347335e31be35742977de280283c3b8557 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 9 Apr 2015 15:50:16 +0200 Subject: [PATCH 0140/1519] fix revert situated at the wrong location --- mongoengine/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 0aa040ed6..897dcc2af 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -115,6 +115,8 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): # Discard replicaSet if not base string if not isinstance(conn_settings['replicaSet'], basestring): conn_settings.pop('replicaSet', None) + if pymongo.version_tuple[0] < 3: + connection_class = MongoReplicaSetClient try: connection = None @@ -127,8 +129,6 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): if conn_settings == connection_settings and _connections.get(db_alias, None): connection = _connections[db_alias] break - if pymongo.version_tuple[0] < 3: - connection_class = MongoReplicaSetClient _connections[alias] = connection if connection else connection_class(**conn_settings) except Exception, e: From 48316ba60d4051d38d156c5704d422b077ce11bd Mon Sep 17 00:00:00 2001 From: mrigal Date: Fri, 10 Apr 2015 22:23:56 +0200 Subject: [PATCH 0141/1519] implemented global IS_PYMONGO_3 --- mongoengine/connection.py | 7 +++---- mongoengine/document.py | 2 +- mongoengine/python_support.py | 7 +++++++ mongoengine/queryset/base.py | 3 ++- tests/document/indexes.py | 11 ++++++----- tests/queryset/queryset.py | 8 ++++---- tests/test_connection.py | 9 +++++---- tests/test_replicaset_connection.py | 6 ++++-- 8 files changed, 32 insertions(+), 21 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 897dcc2af..b203e168b 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -1,13 +1,12 @@ -import pymongo from pymongo import MongoClient, ReadPreference, uri_parser - +from mongoengine.python_support import IS_PYMONGO_3 __all__ = ['ConnectionError', 'connect', 'register_connection', 'DEFAULT_CONNECTION_NAME'] DEFAULT_CONNECTION_NAME = 'default' -if pymongo.version_tuple[0] >= 3: +if IS_PYMONGO_3: READ_PREFERENCE = ReadPreference.PRIMARY else: from pymongo import MongoReplicaSetClient @@ -115,7 +114,7 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): # Discard replicaSet if not base string if not isinstance(conn_settings['replicaSet'], basestring): conn_settings.pop('replicaSet', None) - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: connection_class = MongoReplicaSetClient try: diff --git a/mongoengine/document.py b/mongoengine/document.py index b27bd086e..8cc928666 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -295,7 +295,7 @@ def save(self, force_insert=False, validate=True, clean=True, # I think the self._created flag is not necessarily required in PyMongo3 # but may cause test test_collection_name_and_primary to fail - # if pymongo.version_tuple[0] < 3: + # if not IS_PYMONGO_3: created = ('_id' not in doc or self._created or force_insert) # else: # created = ('_id' not in doc or force_insert) diff --git a/mongoengine/python_support.py b/mongoengine/python_support.py index 1214b4909..3412c8414 100644 --- a/mongoengine/python_support.py +++ b/mongoengine/python_support.py @@ -1,6 +1,13 @@ """Helper functions and types to aid with Python 2.5 - 3 support.""" import sys +import pymongo + + +if pymongo.version_tuple[0] < 3: + IS_PYMONGO_3 = False +else: + IS_PYMONGO_3 = True PY3 = sys.version_info[0] == 3 diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 09a4c3bca..6867cef38 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -21,6 +21,7 @@ from mongoengine.base.common import get_document from mongoengine.errors import (OperationError, NotUniqueError, InvalidQueryError, LookUpError) +from mongoengine.python_support import IS_PYMONGO_3 from mongoengine.queryset import transform from mongoengine.queryset.field_list import QueryFieldList from mongoengine.queryset.visitor import Q, QNode @@ -1385,7 +1386,7 @@ def _collection(self): @property def _cursor_args(self): - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: fields_name = 'fields' cursor_args = { 'timeout': self._timeout, diff --git a/tests/document/indexes.py b/tests/document/indexes.py index b8b3ba0c7..7f40e2fd1 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import unittest import sys + sys.path[0:0] = [""] -import os import pymongo from nose.plugins.skip import SkipTest @@ -11,6 +11,7 @@ from mongoengine import * from mongoengine.connection import get_db, get_connection +from mongoengine.python_support import IS_PYMONGO_3 __all__ = ("IndexesTest", ) @@ -447,26 +448,26 @@ class Test(Document): # Need to be explicit about covered indexes as mongoDB doesn't know if # the documents returned might have more keys in that here. query_plan = Test.objects(id=obj.id).exclude('a').explain() - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertFalse(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK') query_plan = Test.objects(id=obj.id).only('id').explain() - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertTrue(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK') query_plan = Test.objects(a=1).only('a').exclude('id').explain() - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertTrue(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN') self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('stage'), 'PROJECTION') query_plan = Test.objects(a=1).explain() - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertFalse(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN') diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a02683615..b757ddda1 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -17,7 +17,7 @@ from mongoengine import * from mongoengine.connection import get_connection, get_db -from mongoengine.python_support import PY3 +from mongoengine.python_support import PY3, IS_PYMONGO_3 from mongoengine.context_managers import query_counter, switch_db from mongoengine.queryset import (QuerySet, QuerySetManager, MultipleObjectsReturned, DoesNotExist, @@ -54,7 +54,7 @@ def _inner(*args, **kwargs): def skip_pymongo3(f): def _inner(*args, **kwargs): - if pymongo.version_tuple[0] >= 3: + if IS_PYMONGO_3: raise SkipTest("Useless with PyMongo 3+") return f(*args, **kwargs) @@ -2942,7 +2942,7 @@ class News(Document): self.assertEqual(query.count(), 3) self.assertEqual(query._query, {'$text': {'$search': 'brasil'}}) cursor_args = query._cursor_args - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: cursor_args_fields = cursor_args['fields'] else: cursor_args_fields = cursor_args['projection'] @@ -4012,7 +4012,7 @@ class Bar(Document): bars = list(Bar.objects(read_preference=ReadPreference.PRIMARY)) self.assertEqual([], bars) - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: error_class = ConfigurationError else: error_class = TypeError diff --git a/tests/test_connection.py b/tests/test_connection.py index 88e039943..f9cc9c78e 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -17,12 +17,13 @@ connect, register_connection, Document, DateTimeField ) +from mongoengine.python_support import IS_PYMONGO_3 import mongoengine.connection from mongoengine.connection import get_db, get_connection, ConnectionError def get_tz_awareness(connection): - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: return connection.tz_aware else: return connection.codec_options.tz_aware @@ -76,7 +77,7 @@ def test_connect_uri(self): c.admin.authenticate("admin", "password") c.mongoenginetest.add_user("username", "password") - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest') @@ -103,7 +104,7 @@ def test_connect_uri_without_db(self): c.admin.authenticate("admin", "password") c.mongoenginetest.add_user("username", "password") - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') connect("mongoenginetest", host='mongodb://localhost/') @@ -202,7 +203,7 @@ def test_multiple_connection_settings(self): self.assertEqual(len(mongo_connections.items()), 2) self.assertTrue('t1' in mongo_connections.keys()) self.assertTrue('t2' in mongo_connections.keys()) - if pymongo.version_tuple[0] < 3: + if not IS_PYMONGO_3: self.assertEqual(mongo_connections['t1'].host, 'localhost') self.assertEqual(mongo_connections['t2'].host, '127.0.0.1') else: diff --git a/tests/test_replicaset_connection.py b/tests/test_replicaset_connection.py index b3a7e1bf1..361cff412 100644 --- a/tests/test_replicaset_connection.py +++ b/tests/test_replicaset_connection.py @@ -1,11 +1,13 @@ import sys + sys.path[0:0] = [""] import unittest -import pymongo from pymongo import ReadPreference -if pymongo.version_tuple[0] >= 3: +from mongoengine.python_support import IS_PYMONGO_3 + +if IS_PYMONGO_3: from pymongo import MongoClient CONN_CLASS = MongoClient READ_PREF = ReadPreference.SECONDARY From e80322021a5d41a1ee1ac889d11237a56c9f6f0e Mon Sep 17 00:00:00 2001 From: mrigal Date: Fri, 10 Apr 2015 23:20:22 +0200 Subject: [PATCH 0142/1519] corrected and enhanced geo_index test --- tests/fields/geo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/fields/geo.py b/tests/fields/geo.py index 8193d87e9..a0d2237ab 100644 --- a/tests/fields/geo.py +++ b/tests/fields/geo.py @@ -336,12 +336,11 @@ class Parent(Document): Location.drop_collection() Parent.drop_collection() - list(Parent.objects) - - collection = Parent._get_collection() - info = collection.index_information() - + Parent(name='Berlin').save() + info = Parent._get_collection().index_information() self.assertFalse('location_2d' in info) + info = Location._get_collection().index_information() + self.assertTrue('location_2d' in info) self.assertEqual(len(Parent._geo_indices()), 0) self.assertEqual(len(Location._geo_indices()), 1) From 3db896c4e210448aa16fda2a6cef835fd875d62a Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 01:31:13 +0200 Subject: [PATCH 0143/1519] work-around for pymongo 3 bug --- mongoengine/document.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 8cc928666..ae5f585d6 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -166,6 +166,7 @@ def _get_db(cls): @classmethod def _get_collection(cls): """Returns the collection for the document.""" + #TODO: use new get_collection() with PyMongo3 ? if not hasattr(cls, '_collection') or cls._collection is None: db = cls._get_db() collection_name = cls._get_collection_name() @@ -293,12 +294,7 @@ def save(self, force_insert=False, validate=True, clean=True, doc = self.to_mongo() - # I think the self._created flag is not necessarily required in PyMongo3 - # but may cause test test_collection_name_and_primary to fail - # if not IS_PYMONGO_3: created = ('_id' not in doc or self._created or force_insert) - # else: - # created = ('_id' not in doc or force_insert) signals.pre_save_post_validation.send(self.__class__, document=self, created=created) @@ -312,6 +308,10 @@ def save(self, force_insert=False, validate=True, clean=True, object_id = collection.insert(doc, **write_concern) else: object_id = collection.save(doc, **write_concern) + # Pymongo 3.0 bug, fix scheduled for 3.0.1 + if not object_id and pymongo.version_tuple == (3, 0): + object_id = self._qs.filter(**self._object_key).first() and \ + self._qs.filter(**self._object_key).first().pk else: object_id = doc['_id'] updates, removals = self._delta() From 0a65006bb4f3b356391de5b4258792a7bbf763e9 Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 01:31:54 +0200 Subject: [PATCH 0144/1519] replaced find_and_modify by PyMongo3 equivalents --- mongoengine/queryset/base.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6867cef38..0ad0e03bf 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -13,6 +13,7 @@ import pymongo import pymongo.errors from pymongo.common import validate_read_preference +from pymongo.collection import ReturnDocument from mongoengine import signals from mongoengine.connection import get_db @@ -547,7 +548,7 @@ def modify(self, upsert=False, full_response=False, remove=False, new=False, **u :param upsert: insert if document doesn't exist (default ``False``) :param full_response: return the entire response object from the - server (default ``False``) + server (default ``False``, not available for PyMongo 3+) :param remove: remove rather than updating (default ``False``) :param new: return updated rather than original document (default ``False``) @@ -565,13 +566,31 @@ def modify(self, upsert=False, full_response=False, remove=False, new=False, **u queryset = self.clone() query = queryset._query - update = transform.update(queryset._document, **update) + if not remove and IS_PYMONGO_3: + update = transform.update(queryset._document, **update) sort = queryset._ordering try: - result = queryset._collection.find_and_modify( - query, update, upsert=upsert, sort=sort, remove=remove, new=new, - full_response=full_response, **self._cursor_args) + if IS_PYMONGO_3: + if full_response: + msg = ("With PyMongo 3+, it is not possible anymore to get the full response.") + warnings.warn(msg, DeprecationWarning) + if remove: + result = queryset._collection.find_one_and_delete( + query, sort=sort, **self._cursor_args) + else: + if new: + return_doc = ReturnDocument.AFTER + else: + return_doc = ReturnDocument.BEFORE + result = queryset._collection.find_one_and_update( + query, update, upsert=upsert, sort=sort, return_document=return_doc, + **self._cursor_args) + + else: + result = queryset._collection.find_and_modify( + query, update, upsert=upsert, sort=sort, remove=remove, new=new, + full_response=full_response, **self._cursor_args) except pymongo.errors.DuplicateKeyError, err: raise NotUniqueError(u"Update failed (%s)" % err) except pymongo.errors.OperationFailure, err: From a5c2fc4f9da37d85bc59677187d6a289b4f2c2d7 Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 01:54:05 +0200 Subject: [PATCH 0145/1519] reinforced test for BinaryField being a Primary Key --- tests/fields/fields.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 7a99bd78a..830c98827 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2488,10 +2488,11 @@ class Attachment(Document): id = BinaryField(primary_key=True) Attachment.drop_collection() - - att = Attachment(id=uuid.uuid4().bytes).save() + binary_id = uuid.uuid4().bytes + att = Attachment(id=binary_id).save() + self.assertEqual(1, Attachment.objects.count()) + self.assertIsNotNone(Attachment.objects.filter(id=binary_id).first()) att.delete() - self.assertEqual(0, Attachment.objects.count()) def test_choices_validation(self): From 6ad9a56bd914f48a877f1696af52d7385f23d0b4 Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 11:08:26 +0200 Subject: [PATCH 0146/1519] corrected bad import preventing to run on PyMongo 2.X versions --- mongoengine/queryset/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 0ad0e03bf..81da98d7e 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -13,7 +13,6 @@ import pymongo import pymongo.errors from pymongo.common import validate_read_preference -from pymongo.collection import ReturnDocument from mongoengine import signals from mongoengine.connection import get_db @@ -27,6 +26,9 @@ from mongoengine.queryset.field_list import QueryFieldList from mongoengine.queryset.visitor import Q, QNode +if IS_PYMONGO_3: + from pymongo.collection import ReturnDocument + __all__ = ('BaseQuerySet', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL') From f31f52ff1c7c033c3eef91f035883b41276568c4 Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 22:46:24 +0200 Subject: [PATCH 0147/1519] corrected test condition, depending on mongodb and not pymongo version --- tests/document/indexes.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 7f40e2fd1..f88b903aa 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -11,7 +11,6 @@ from mongoengine import * from mongoengine.connection import get_db, get_connection -from mongoengine.python_support import IS_PYMONGO_3 __all__ = ("IndexesTest", ) @@ -445,29 +444,32 @@ class Test(Document): obj = Test(a=1) obj.save() + connection = get_connection() + IS_MONGODB_3 = connection.server_info()['versionArray'][0] >= 3 + # Need to be explicit about covered indexes as mongoDB doesn't know if # the documents returned might have more keys in that here. query_plan = Test.objects(id=obj.id).exclude('a').explain() - if not IS_PYMONGO_3: + if not IS_MONGODB_3: self.assertFalse(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK') query_plan = Test.objects(id=obj.id).only('id').explain() - if not IS_PYMONGO_3: + if not IS_MONGODB_3: self.assertTrue(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK') query_plan = Test.objects(a=1).only('a').exclude('id').explain() - if not IS_PYMONGO_3: + if not IS_MONGODB_3: self.assertTrue(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN') self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('stage'), 'PROJECTION') query_plan = Test.objects(a=1).explain() - if not IS_PYMONGO_3: + if not IS_MONGODB_3: self.assertFalse(query_plan['indexOnly']) else: self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN') From c44891a1a8b50fdff3ab7e4ffea049fa96d604ae Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 22:55:08 +0200 Subject: [PATCH 0148/1519] changed unittest to call for compatibility with Python 2.6 --- tests/fields/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 830c98827..3b860cc78 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2491,7 +2491,7 @@ class Attachment(Document): binary_id = uuid.uuid4().bytes att = Attachment(id=binary_id).save() self.assertEqual(1, Attachment.objects.count()) - self.assertIsNotNone(Attachment.objects.filter(id=binary_id).first()) + self.assertNotEqual(None, Attachment.objects.filter(id=binary_id).first()) att.delete() self.assertEqual(0, Attachment.objects.count()) From 33b1eed3619b853fce9cb8a845f6664fee523ac0 Mon Sep 17 00:00:00 2001 From: mrigal Date: Sat, 11 Apr 2015 22:55:33 +0200 Subject: [PATCH 0149/1519] corrected logical test for not Pymongo3 versions --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 81da98d7e..2e087e100 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -568,7 +568,7 @@ def modify(self, upsert=False, full_response=False, remove=False, new=False, **u queryset = self.clone() query = queryset._query - if not remove and IS_PYMONGO_3: + if not IS_PYMONGO_3 or not remove: update = transform.update(queryset._document, **update) sort = queryset._ordering From 76adb13a64d72bc2e243e22e3853c98e0e8898be Mon Sep 17 00:00:00 2001 From: mrigal Date: Sun, 12 Apr 2015 12:29:25 +0200 Subject: [PATCH 0150/1519] Minor text and comments enhancements --- mongoengine/document.py | 6 ++++-- mongoengine/queryset/base.py | 4 +++- tests/fields/fields.py | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index ae5f585d6..2e3eee9df 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -166,7 +166,7 @@ def _get_db(cls): @classmethod def _get_collection(cls): """Returns the collection for the document.""" - #TODO: use new get_collection() with PyMongo3 ? + # TODO: use new get_collection() with PyMongo3 ? if not hasattr(cls, '_collection') or cls._collection is None: db = cls._get_db() collection_name = cls._get_collection_name() @@ -308,7 +308,9 @@ def save(self, force_insert=False, validate=True, clean=True, object_id = collection.insert(doc, **write_concern) else: object_id = collection.save(doc, **write_concern) - # Pymongo 3.0 bug, fix scheduled for 3.0.1 + # TODO: Pymongo 3.0 bug, fix scheduled for 3.0.1 + # In PyMongo 3.0, the save() call calls internally the _update() call + # but they forget to return the _id value passed back, therefore getting it back here if not object_id and pymongo.version_tuple == (3, 0): object_id = self._qs.filter(**self._object_key).first() and \ self._qs.filter(**self._object_key).first().pk diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 2e087e100..803374bd8 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -930,6 +930,7 @@ def explain(self, format=False): plan = pprint.pformat(plan) return plan + # DEPRECATED. Has no more impact on PyMongo 3+ def snapshot(self, enabled): """Enable or disable snapshot mode when querying. @@ -1419,7 +1420,8 @@ def _cursor_args(self): cursor_args['slave_okay'] = self._slave_okay else: fields_name = 'projection' - # snapshot seems not to be handled at all by PyMongo 3+ + # snapshot is not to handled at all by PyMongo 3+ + # TODO: raise a warning? cursor_args = { 'no_cursor_timeout': self._timeout } diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 3b860cc78..0dc239c4f 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2491,7 +2491,8 @@ class Attachment(Document): binary_id = uuid.uuid4().bytes att = Attachment(id=binary_id).save() self.assertEqual(1, Attachment.objects.count()) - self.assertNotEqual(None, Attachment.objects.filter(id=binary_id).first()) + # TODO use assertIsNotNone once Python 2.6 support is dropped + self.assertFalse(Attachment.objects.filter(id=binary_id).first() is not None) att.delete() self.assertEqual(0, Attachment.objects.count()) From c25619fd6314ada89893c97dacd231e57a67e54b Mon Sep 17 00:00:00 2001 From: mrigal Date: Sun, 12 Apr 2015 14:07:45 +0200 Subject: [PATCH 0151/1519] improved deprecation documentation and added warning when using snapshot with PyMongo3 --- mongoengine/queryset/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 803374bd8..3752902a7 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -937,6 +937,7 @@ def snapshot(self, enabled): :param enabled: whether or not snapshot mode is enabled ..versionchanged:: 0.5 - made chainable + .. deprecated:: Ignored with PyMongo 3+ """ queryset = self.clone() queryset._snapshot = enabled @@ -958,6 +959,8 @@ def slave_okay(self, enabled): """Enable or disable the slave_okay when querying. :param enabled: whether or not the slave_okay is enabled + + .. deprecated:: Ignored with PyMongo 3+ """ queryset = self.clone() queryset._slave_okay = enabled @@ -1420,8 +1423,11 @@ def _cursor_args(self): cursor_args['slave_okay'] = self._slave_okay else: fields_name = 'projection' - # snapshot is not to handled at all by PyMongo 3+ - # TODO: raise a warning? + # snapshot is not handled at all by PyMongo 3+ + # TODO: evaluate similar possibilities using modifiers + if self._snapshot: + msg = "The snapshot option is not anymore available with PyMongo 3+" + warnings.warn(msg, DeprecationWarning) cursor_args = { 'no_cursor_timeout': self._timeout } From 3421fffa9be637c8fa6eb5d023da5d3d2fb19e9e Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 11:18:29 +0200 Subject: [PATCH 0152/1519] reactivated unnecessarily skipped test --- tests/queryset/geo.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 5148a48e3..dad511324 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -1,4 +1,6 @@ import sys +from mongoengine.connection import get_connection + sys.path[0:0] = [""] import unittest @@ -141,7 +143,13 @@ class Event(Document): def test_spherical_geospatial_operators(self): """Ensure that spherical geospatial queries are working """ - raise SkipTest("https://jira.mongodb.org/browse/SERVER-14039") + # Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039 + connection = get_connection() + info = connection.test.command('buildInfo') + mongodb_version = tuple([int(i) for i in info['version'].split('.')]) + if mongodb_version < (2, 6, 4): + raise SkipTest("Need MongoDB version 2.6.4+") + class Point(Document): location = GeoPointField() From 571a7dc42d0de14c097b0429f76c8a1131b9a48a Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 11:45:04 +0200 Subject: [PATCH 0153/1519] Fix last issue with binary field as primary key and skipped new test --- mongoengine/document.py | 5 +++-- tests/fields/fields.py | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 2e3eee9df..ff39b13ab 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -312,8 +312,9 @@ def save(self, force_insert=False, validate=True, clean=True, # In PyMongo 3.0, the save() call calls internally the _update() call # but they forget to return the _id value passed back, therefore getting it back here if not object_id and pymongo.version_tuple == (3, 0): - object_id = self._qs.filter(**self._object_key).first() and \ - self._qs.filter(**self._object_key).first().pk + pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk) + object_id = self._qs.filter(pk=pk_as_mongo_obj).first() and \ + self._qs.filter(pk=pk_as_mongo_obj).first().pk else: object_id = doc['_id'] updates, removals = self._delta() diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 0dc239c4f..071f1dd09 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- import sys +from nose.plugins.skip import SkipTest + sys.path[0:0] = [""] import datetime @@ -2491,8 +2493,25 @@ class Attachment(Document): binary_id = uuid.uuid4().bytes att = Attachment(id=binary_id).save() self.assertEqual(1, Attachment.objects.count()) + self.assertEqual(1, Attachment.objects.filter(id=att.id).count()) + # TODO use assertIsNotNone once Python 2.6 support is dropped + self.assertTrue(Attachment.objects.filter(id=att.id).first() is not None) + att.delete() + self.assertEqual(0, Attachment.objects.count()) + + def test_binary_field_primary_filter_by_binary_pk_as_str(self): + + raise SkipTest("Querying by id as string is not currently supported") + + class Attachment(Document): + id = BinaryField(primary_key=True) + + Attachment.drop_collection() + binary_id = uuid.uuid4().bytes + att = Attachment(id=binary_id).save() + self.assertEqual(1, Attachment.objects.filter(id=binary_id).count()) # TODO use assertIsNotNone once Python 2.6 support is dropped - self.assertFalse(Attachment.objects.filter(id=binary_id).first() is not None) + self.assertTrue(Attachment.objects.filter(id=binary_id).first() is not None) att.delete() self.assertEqual(0, Attachment.objects.count()) From 9b2fde962c09eaf22520cf1eeb268e08dce29bc8 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 13:14:20 +0200 Subject: [PATCH 0154/1519] added try except to geo test to catch random mongo internal errors --- tests/queryset/geo.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index dad511324..12e96a043 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -1,14 +1,16 @@ import sys -from mongoengine.connection import get_connection sys.path[0:0] = [""] import unittest from datetime import datetime, timedelta -from mongoengine import * +from pymongo.errors import OperationFailure +from mongoengine import * +from mongoengine.connection import get_connection from nose.plugins.skip import SkipTest + __all__ = ("GeoQueriesTest",) @@ -175,6 +177,13 @@ class Point(Document): points = Point.objects(location__near_sphere=[-122, 37.5], location__max_distance=60 / earth_radius) + # This test is sometimes failing with Mongo internals non-sense. + # See https://travis-ci.org/MongoEngine/mongoengine/builds/58729101 + try: + points.count() + except OperationFailure: + raise SkipTest("Sometimes MongoDB ignores its capacities on maxDistance") + self.assertEqual(points.count(), 2) # Finds both points, but orders the north point first because it's From 3ab5ba61493f0b6ef0e46fbcf7a3c06d8a9add4e Mon Sep 17 00:00:00 2001 From: mrigal Date: Wed, 29 Apr 2015 15:51:17 +0200 Subject: [PATCH 0155/1519] added explicit warnings when calling methods having no effect anymore with PyMongo3+ --- mongoengine/queryset/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 3752902a7..d65c8cd25 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -939,6 +939,9 @@ def snapshot(self, enabled): ..versionchanged:: 0.5 - made chainable .. deprecated:: Ignored with PyMongo 3+ """ + if IS_PYMONGO_3: + msg = ("snapshot is deprecated as it has no impact when using PyMongo 3+.") + warnings.warn(msg, DeprecationWarning) queryset = self.clone() queryset._snapshot = enabled return queryset @@ -962,6 +965,9 @@ def slave_okay(self, enabled): .. deprecated:: Ignored with PyMongo 3+ """ + if IS_PYMONGO_3: + msg = ("slave_okay is deprecated as it has no impact when using PyMongo 3+.") + warnings.warn(msg, DeprecationWarning) queryset = self.clone() queryset._slave_okay = enabled return queryset From c5ed308ea5ee408477cfa7985c8438ae5e1315da Mon Sep 17 00:00:00 2001 From: mrigal Date: Wed, 29 Apr 2015 17:12:47 +0200 Subject: [PATCH 0156/1519] comments update after having tested PyMongo 3.0.1 --- mongoengine/document.py | 2 +- tests/document/indexes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index ff39b13ab..838feb81f 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -308,9 +308,9 @@ def save(self, force_insert=False, validate=True, clean=True, object_id = collection.insert(doc, **write_concern) else: object_id = collection.save(doc, **write_concern) - # TODO: Pymongo 3.0 bug, fix scheduled for 3.0.1 # In PyMongo 3.0, the save() call calls internally the _update() call # but they forget to return the _id value passed back, therefore getting it back here + # Correct behaviour in 2.X and in 3.0.1+ versions if not object_id and pymongo.version_tuple == (3, 0): pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk) object_id = self._qs.filter(pk=pk_as_mongo_obj).first() and \ diff --git a/tests/document/indexes.py b/tests/document/indexes.py index f88b903aa..593fe8771 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -513,7 +513,7 @@ class BlogPost(Document): self.assertEqual(BlogPost.objects.count(), 10) self.assertEqual(BlogPost.objects.hint().count(), 10) - # PyMongo 3.0 bug + # PyMongo 3.0 bug only, works correctly with 2.X and 3.0.1+ versions if pymongo.version != '3.0': self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10) From f4478fc76235ce2f1dd5b24759558a576d885467 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 09:54:24 +0200 Subject: [PATCH 0157/1519] removed sleep thanks to @seglberg suggestion --- tests/test_connection.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index f9cc9c78e..3f92ffd6a 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,5 +1,5 @@ import sys -from time import sleep +import datetime sys.path[0:0] = [""] @@ -8,8 +8,6 @@ except ImportError: import unittest -import datetime - import pymongo from bson.tz_util import utc @@ -62,8 +60,12 @@ def test_sharing_connections(self): connect('mongoenginetest', alias='testdb2') actual_connection = get_connection('testdb2') - # horrible, but since PyMongo3+, connection are created asynchronously - sleep(0.1) + # Handle PyMongo 3+ Async Connection + if IS_PYMONGO_3: + # Ensure we are connected, throws ServerSelectionTimeoutError otherwise. + # Purposely not catching exception to fail test if thrown. + expected_connection.server_info() + self.assertEqual(expected_connection, actual_connection) def test_connect_uri(self): @@ -207,8 +209,11 @@ def test_multiple_connection_settings(self): self.assertEqual(mongo_connections['t1'].host, 'localhost') self.assertEqual(mongo_connections['t2'].host, '127.0.0.1') else: - # horrible, but since PyMongo3+, connection are created asynchronously - sleep(0.1) + # Handle PyMongo 3+ Async Connection + # Ensure we are connected, throws ServerSelectionTimeoutError otherwise. + # Purposely not catching exception to fail test if thrown. + mongo_connections['t1'].server_info() + mongo_connections['t2'].server_info() self.assertEqual(mongo_connections['t1'].address[0], 'localhost') self.assertEqual(mongo_connections['t2'].address[0], '127.0.0.1') From 1005c99e9c01c72227ff374d7c03369c3028969c Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 10:47:09 +0200 Subject: [PATCH 0158/1519] corrected index test for MongoDB 3+ --- tests/document/indexes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 593fe8771..54fd4ded5 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -866,7 +866,7 @@ class TestDoc(Document): meta = { 'allow_inheritance': True, 'indexes': [ - { 'fields': ('txt',), 'cls': False } + {'fields': ('txt',), 'cls': False} ] } @@ -875,7 +875,7 @@ class TestChildDoc(TestDoc): meta = { 'indexes': [ - { 'fields': ('txt2',), 'cls': False } + {'fields': ('txt2',), 'cls': False} ] } @@ -886,11 +886,14 @@ class TestChildDoc(TestDoc): index_info = TestDoc._get_collection().index_information() for key in index_info: del index_info[key]['v'] # drop the index version - we don't care about that here + del index_info[key]['ns'] # drop the index namespace - we don't care about that here + if 'dropDups' in index_info[key]: + del index_info[key]['dropDups'] # drop the index dropDups - it is deprecated in MongoDB 3+ + print index_info self.assertEqual(index_info, { 'txt_1': { 'key': [('txt', 1)], - 'dropDups': False, 'background': False }, '_id_': { @@ -898,7 +901,6 @@ class TestChildDoc(TestDoc): }, 'txt2_1': { 'key': [('txt2', 1)], - 'dropDups': False, 'background': False }, '_cls_1': { From c41dd6495d1cb47e346644164622031f5861628f Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 10:47:28 +0200 Subject: [PATCH 0159/1519] corrected connection test for PyMongo3+ --- tests/test_connection.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 3f92ffd6a..4a02696af 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,5 +1,6 @@ import sys import datetime +from pymongo.errors import OperationFailure sys.path[0:0] = [""] @@ -124,6 +125,7 @@ def test_connect_uri_without_db(self): def test_connect_uri_with_authsource(self): """Ensure that the connect() method works well with the option `authSource` in URI. + This feature was introduced in MongoDB 2.4 and removed in 2.6 """ # Create users c = connect('mongoenginetest') @@ -131,18 +133,25 @@ def test_connect_uri_with_authsource(self): c.admin.add_user('username', 'password') # Authentication fails without "authSource" - self.assertRaises( - ConnectionError, connect, 'mongoenginetest', alias='test1', - host='mongodb://username:password@localhost/mongoenginetest' - ) - self.assertRaises(ConnectionError, get_db, 'test1') + if IS_PYMONGO_3: + test_conn = connect('mongoenginetest', alias='test2', + host='mongodb://username:password@localhost/mongoenginetest') + self.assertRaises(OperationFailure, test_conn.server_info) + else: + self.assertRaises( + ConnectionError, connect, 'mongoenginetest', alias='test1', + host='mongodb://username:password@localhost/mongoenginetest' + ) + self.assertRaises(ConnectionError, get_db, 'test1') # Authentication succeeds with "authSource" - connect( + test_conn2 = connect( 'mongoenginetest', alias='test2', host=('mongodb://username:password@localhost/' 'mongoenginetest?authSource=admin') ) + # This will fail starting from MongoDB 2.6+ + # test_conn2.server_info() db = get_db('test2') self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'mongoenginetest') From 14f82ea0a94fcaa733b2584a24521c934739217a Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 10:56:22 +0200 Subject: [PATCH 0160/1519] enabled PYMONGO 3 and DEV for travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74f409298..34702192f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ python: env: - PYMONGO=2.7 - PYMONGO=2.8 -# - PYMONGO=3.0 -# - PYMONGO=dev +- PYMONGO=3.0 +- PYMONGO=dev matrix: fast_finish: true before_install: From d36708933c5cb3fb6a747dead7c0d6ecb7003c1c Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 11:03:44 +0200 Subject: [PATCH 0161/1519] author and changelog --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 6745e14b3..f424dbc2f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -221,3 +221,4 @@ that much better: * Eremeev Danil (https://github.com/elephanter) * Catstyle Lee (https://github.com/Catstyle) * Kiryl Yermakou (https://github.com/rma4ok) + * Matthieu Rigal (https://github.com/MRigal) diff --git a/docs/changelog.rst b/docs/changelog.rst index 536765628..19d306986 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,7 @@ Changes in 0.9.X - DEV - Don't send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769) - Fix for updating sorting in SortedListField. #978 - Added __ support to escape field name in fields lookup keywords that match operators names #949 +- Support for PyMongo 3+ #946 Changes in 0.9.0 ================ From f97db93212798c8ce1f766aaf45bd222b46be4b3 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 12:34:41 +0200 Subject: [PATCH 0162/1519] corrected test for MongoDB 2.X --- tests/document/indexes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 54fd4ded5..d43b22e56 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -886,10 +886,10 @@ class TestChildDoc(TestDoc): index_info = TestDoc._get_collection().index_information() for key in index_info: del index_info[key]['v'] # drop the index version - we don't care about that here - del index_info[key]['ns'] # drop the index namespace - we don't care about that here + if 'ns' in index_info[key]: + del index_info[key]['ns'] # drop the index namespace - we don't care about that here, MongoDB 3+ if 'dropDups' in index_info[key]: del index_info[key]['dropDups'] # drop the index dropDups - it is deprecated in MongoDB 3+ - print index_info self.assertEqual(index_info, { 'txt_1': { From 73f08670612d89f728fd79e6c92bc0d3122fb5d8 Mon Sep 17 00:00:00 2001 From: Eli Boyarski Date: Fri, 10 Apr 2015 15:49:29 +0200 Subject: [PATCH 0163/1519] Unit Test - Unique Multikey Index Adds a unit test to exhibit the behavior of MongoDB when using a unique multikey index. MongoDB treats any missing unique multikey index value as NULL, thus throwing a Duplicate Key Error when saving multiple missing values. See #930 for more information. - Closes #930 - Closes #952 --- tests/fields/fields.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 7a99bd78a..e9532a5a3 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3675,6 +3675,31 @@ def test_delete(self): # deleted from the database self.assertEqual(number, 2) + def test_empty_list_embedded_documents_with_unique_field(self): + """ + Tests that only one document with an empty list of embedded documents + that have a unique field can be saved, but if the unique field is + also sparse than multiple documents with an empty list can be saved. + """ + class EmbeddedWithUnique(EmbeddedDocument): + number = IntField(unique=True) + + class A(Document): + my_list = ListField(EmbeddedDocumentField(EmbeddedWithUnique)) + + a1 = A(my_list=[]).save() + self.assertRaises(NotUniqueError, lambda: A(my_list=[]).save()) + + class EmbeddedWithSparseUnique(EmbeddedDocument): + number = IntField(unique=True, sparse=True) + + class B(Document): + my_list = ListField(EmbeddedDocumentField(EmbeddedWithSparseUnique)) + + b1 = B(my_list=[]).save() + b2 = B(my_list=[]).save() + + def test_filtered_delete(self): """ Tests the delete method of a List of Embedded Documents From 794101691cdbbaddeb889b5a62ef46a50ddc90ec Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 19:34:31 +0200 Subject: [PATCH 0164/1519] removed wire_concern usage and cosmetics --- mongoengine/queryset/base.py | 4 ++-- mongoengine/queryset/transform.py | 11 +++-------- tests/queryset/queryset.py | 13 ++++++++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index d65c8cd25..89eb9afa6 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -940,7 +940,7 @@ def snapshot(self, enabled): .. deprecated:: Ignored with PyMongo 3+ """ if IS_PYMONGO_3: - msg = ("snapshot is deprecated as it has no impact when using PyMongo 3+.") + msg = "snapshot is deprecated as it has no impact when using PyMongo 3+." warnings.warn(msg, DeprecationWarning) queryset = self.clone() queryset._snapshot = enabled @@ -966,7 +966,7 @@ def slave_okay(self, enabled): .. deprecated:: Ignored with PyMongo 3+ """ if IS_PYMONGO_3: - msg = ("slave_okay is deprecated as it has no impact when using PyMongo 3+.") + msg = "slave_okay is deprecated as it has no impact when using PyMongo 3+." warnings.warn(msg, DeprecationWarning) queryset = self.clone() queryset._slave_okay = enabled diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index e8cd2721d..c43c4b401 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -128,20 +128,15 @@ def query(_doc_cls=None, _field_operation=False, **query): mongo_query[key].update(value) # $maxDistance needs to come last - convert to SON value_dict = mongo_query[key] - if ('$maxDistance' in value_dict and '$near' in value_dict): + if '$maxDistance' in value_dict and '$near' in value_dict: value_son = SON() if isinstance(value_dict['$near'], dict): for k, v in value_dict.iteritems(): if k == '$maxDistance': continue value_son[k] = v - if (get_connection().max_wire_version <= 1): - value_son['$maxDistance'] = value_dict[ - '$maxDistance'] - else: - value_son['$near'] = SON(value_son['$near']) - value_son['$near'][ - '$maxDistance'] = value_dict['$maxDistance'] + value_son['$near'] = SON(value_son['$near']) + value_son['$near']['$maxDistance'] = value_dict['$maxDistance'] else: for k, v in value_dict.iteritems(): if k == '$maxDistance': diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b757ddda1..65d843059 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -708,6 +708,11 @@ class Blog(Document): Blog.drop_collection() + # get MongoDB version info + connection = get_connection() + info = connection.test.command('buildInfo') + mongodb_version = tuple([int(i) for i in info['version'].split('.')]) + # Recreates the collection self.assertEqual(0, Blog.objects.count()) @@ -724,7 +729,7 @@ class Blog(Document): blogs.append(Blog(title="post %s" % i, posts=[post1, post2])) Blog.objects.insert(blogs, load_bulk=False) - if (get_connection().max_wire_version <= 1): + if mongodb_version < (2, 6): self.assertEqual(q, 1) else: # profiling logs each doc now in the bulk op @@ -737,7 +742,7 @@ class Blog(Document): self.assertEqual(q, 0) Blog.objects.insert(blogs) - if (get_connection().max_wire_version <= 1): + if mongodb_version < (2, 6): self.assertEqual(q, 2) # 1 for insert, and 1 for in bulk fetch else: # 99 for insert, and 1 for in bulk fetch @@ -869,8 +874,10 @@ class Project(Document): self.assertEqual(q, 3) + @skip_pymongo3 def test_slave_okay(self): - """Ensures that a query can take slave_okay syntax + """Ensures that a query can take slave_okay syntax. + Useless with PyMongo 3+ as well as with MongoDB 3+. """ person1 = self.Person(name="User A", age=20) person1.save() From 7d7d0ea00191c026e37bae8e92faf653b2cebb8e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Fri, 27 Feb 2015 11:21:55 +0200 Subject: [PATCH 0165/1519] Use not in instead of not (x in y). --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index bd6c88d6a..53d4ac956 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -609,7 +609,7 @@ def to_mongo(self, document, use_db_field=True): return None data = document.to_mongo(use_db_field) - if not '_cls' in data: + if '_cls' not in data: data['_cls'] = document._class_name return data From f730591f2cc6cb63ed8be69b1d814d9d3a5c1e2f Mon Sep 17 00:00:00 2001 From: Marcel van den Elst Date: Wed, 20 May 2015 13:01:41 +0200 Subject: [PATCH 0166/1519] added passing test for updates on related models ref #570: test would fail from v0.8.5 up, but fixed in master --- tests/queryset/queryset.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 65d843059..6e4d86d2e 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -630,6 +630,40 @@ class Doc(Document): self.assertRaises(ValidationError, Doc.objects().update, dt_f="datetime", upsert=True) self.assertRaises(ValidationError, Doc.objects().update, ed_f__str_f=1, upsert=True) + def test_update_related_models( self ): + class TestPerson( Document ): + name = StringField() + + class TestOrganization( Document ): + name = StringField() + owner = ReferenceField( TestPerson ) + + TestPerson.drop_collection() + TestOrganization.drop_collection() + + p = TestPerson( name='p1' ) + p.save() + o = TestOrganization( name='o1' ) + o.save() + + o.owner = p + p.name = 'p2' + + self.assertListEqual( o._get_changed_fields(), [ 'owner' ] ) + self.assertListEqual( p._get_changed_fields(), [ 'name' ] ) + + o.save() + + self.assertListEqual( o._get_changed_fields(), [] ) + self.assertListEqual( p._get_changed_fields(), [ 'name' ] ) # Fails; it's empty + + # This will do NOTHING at all, even though we changed the name + p.save() + + p.reload() + + self.assertEqual( p.name, 'p2' ) # Fails; it's still `p1` + def test_upsert(self): self.Person.drop_collection() From fde733c205a3d879ca5e74452104946d0aaa89fa Mon Sep 17 00:00:00 2001 From: Bruno Pierri Galvao Date: Thu, 21 May 2015 10:22:02 -0400 Subject: [PATCH 0167/1519] spelling change definion to definition --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 56370c14f..79332493d 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -114,7 +114,7 @@ arguments can be set on all fields: :attr:`default` (Default: None) A value to use when no value is set for this field. - The definion of default parameters follow `the general rules on Python + The definition of default parameters follow `the general rules on Python `__, which means that some care should be taken when dealing with default mutable objects (like in :class:`~mongoengine.fields.ListField` or :class:`~mongoengine.fields.DictField`):: From 2753e02cda4f3a1aae7ba1eba13c7ff31c496781 Mon Sep 17 00:00:00 2001 From: Charanpal Date: Sat, 23 May 2015 14:46:56 +0100 Subject: [PATCH 0168/1519] Fix for case where Document is deleted and it's files (FieldFields) in GridFS remain. --- mongoengine/document.py | 6 ++++ tests/fields/file_tests.py | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/mongoengine/document.py b/mongoengine/document.py index 838feb81f..813fcb2e6 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -463,6 +463,12 @@ def delete(self, **write_concern): """ signals.pre_delete.send(self.__class__, document=self) + #Delete FileFields seperately + FileField = _import_class('FileField') + for name, field in self._fields.iteritems(): + if isinstance(field, FileField): + getattr(self, name).delete() + try: self._qs.filter( **self._object_key).delete(write_concern=write_concern, _from_doc_delete=True) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 811f17edd..7db900596 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -297,6 +297,71 @@ class TestFile(Document): test_file = TestFile() self.assertFalse(test_file.the_file in [{"test": 1}]) + def test_file_disk_space(self): + """ Test disk space usage when we delete/replace a file """ + class TestFile(Document): + the_file = FileField() + + text = b('Hello, World!') + content_type = 'text/plain' + + testfile = TestFile() + testfile.the_file.put(text, content_type=content_type, filename="hello") + testfile.save() + + #Now check fs.files and fs.chunks + db = TestFile._get_db() + + files = db.fs.files.find() + chunks = db.fs.chunks.find() + self.assertEquals(len(list(files)), 1) + self.assertEquals(len(list(chunks)), 1) + + #Deleting the docoument should delete the files + testfile.delete() + + files = db.fs.files.find() + chunks = db.fs.chunks.find() + self.assertEquals(len(list(files)), 0) + self.assertEquals(len(list(chunks)), 0) + + #Test case where we don't store a file in the first place + testfile = TestFile() + testfile.save() + + files = db.fs.files.find() + chunks = db.fs.chunks.find() + self.assertEquals(len(list(files)), 0) + self.assertEquals(len(list(chunks)), 0) + + testfile.delete() + + files = db.fs.files.find() + chunks = db.fs.chunks.find() + self.assertEquals(len(list(files)), 0) + self.assertEquals(len(list(chunks)), 0) + + #Test case where we overwrite the file + testfile = TestFile() + testfile.the_file.put(text, content_type=content_type, filename="hello") + testfile.save() + + text = b('Bonjour, World!') + testfile.the_file.replace(text, content_type=content_type, filename="hello") + testfile.save() + + files = db.fs.files.find() + chunks = db.fs.chunks.find() + self.assertEquals(len(list(files)), 1) + self.assertEquals(len(list(chunks)), 1) + + testfile.delete() + + files = db.fs.files.find() + chunks = db.fs.chunks.find() + self.assertEquals(len(list(files)), 0) + self.assertEquals(len(list(chunks)), 0) + def test_image_field(self): if not HAS_PIL: raise SkipTest('PIL not installed') From c82b26d334d467128a4c9bc025959df602089999 Mon Sep 17 00:00:00 2001 From: Eliecer Daza Date: Sun, 31 May 2015 18:28:12 -0500 Subject: [PATCH 0169/1519] little object name fix replace little object name HtmlPost with TextPost that is the one used on the example --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2d17313f0..f2829cd3e 100644 --- a/README.rst +++ b/README.rst @@ -81,7 +81,7 @@ Some simple examples of what MongoEngine code looks like:: >>> len(BlogPost.objects) 2 - >>> len(HtmlPost.objects) + >>> len(TextPost.objects) 1 >>> len(LinkPost.objects) 1 From 283e92d55d1acc219c1dd1f8f0e374300b6bfdbe Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 7 May 2015 15:11:33 +0200 Subject: [PATCH 0170/1519] Added hashed index, a bit more of geo-indexes, possibility to give _cls and docs --- docs/guide/defining-documents.rst | 22 ++++++--- mongoengine/base/document.py | 22 ++++++--- mongoengine/document.py | 7 +++ tests/document/indexes.py | 79 +++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 12 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 56370c14f..13519a287 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -465,19 +465,26 @@ You can specify indexes on collections to make querying faster. This is done by creating a list of index specifications called :attr:`indexes` in the :attr:`~mongoengine.Document.meta` dictionary, where an index specification may either be a single field name, a tuple containing multiple field names, or a -dictionary containing a full index definition. A direction may be specified on -fields by prefixing the field name with a **+** (for ascending) or a **-** sign -(for descending). Note that direction only matters on multi-field indexes. -Text indexes may be specified by prefixing the field name with a **$**. :: +dictionary containing a full index definition. + +A direction may be specified on fields by prefixing the field name with a +**+** (for ascending) or a **-** sign (for descending). Note that direction +only matters on multi-field indexes. Text indexes may be specified by prefixing +the field name with a **$**. Hashed indexes may be specified by prefixing +the field name with a **#**:: class Page(Document): + category = IntField() title = StringField() rating = StringField() created = DateTimeField() meta = { 'indexes': [ 'title', + '$title', # text index + '#title', # hashed index ('title', '-rating'), + ('category', '_cls'), { 'fields': ['created'], 'expireAfterSeconds': 3600 @@ -532,11 +539,14 @@ There are a few top level defaults for all indexes that can be set:: :attr:`index_background` (Optional) Set the default value for if an index should be indexed in the background +:attr:`index_cls` (Optional) + A way to turn off a specific index for _cls. + :attr:`index_drop_dups` (Optional) Set the default value for if an index should drop duplicates -:attr:`index_cls` (Optional) - A way to turn off a specific index for _cls. +.. note:: Since MongoDB 3.0 drop_dups is not supported anymore. Raises a Warning + and has no effect Compound Indexes and Indexing sub documents diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 3eba16ca8..5707992a4 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -782,7 +782,7 @@ def _build_index_spec(cls, spec): allow_inheritance = cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) include_cls = (allow_inheritance and not spec.get('sparse', False) and - spec.get('cls', True)) + spec.get('cls', True) and '_cls' not in spec['fields']) # 733: don't include cls if index_cls is False unless there is an explicit cls with the index include_cls = include_cls and (spec.get('cls', False) or cls._meta.get('index_cls', True)) @@ -795,16 +795,25 @@ def _build_index_spec(cls, spec): # ASCENDING from + # DESCENDING from - - # GEO2D from * # TEXT from $ + # HASHED from # + # GEOSPHERE from ( + # GEOHAYSTACK from ) + # GEO2D from * direction = pymongo.ASCENDING if key.startswith("-"): direction = pymongo.DESCENDING - elif key.startswith("*"): - direction = pymongo.GEO2D elif key.startswith("$"): direction = pymongo.TEXT - if key.startswith(("+", "-", "*", "$")): + elif key.startswith("#"): + direction = pymongo.HASHED + elif key.startswith("("): + direction = pymongo.GEOSPHERE + elif key.startswith(")"): + direction = pymongo.GEOHAYSTACK + elif key.startswith("*"): + direction = pymongo.GEO2D + if key.startswith(("+", "-", "*", "$", "#", "(", ")")): key = key[1:] # Use real field name, do it manually because we need field @@ -827,7 +836,8 @@ def _build_index_spec(cls, spec): index_list.append((key, direction)) # Don't add cls to a geo index - if include_cls and direction is not pymongo.GEO2D: + if include_cls and direction not in ( + pymongo.GEO2D, pymongo.GEOHAYSTACK, pymongo.GEOSPHERE): index_list.insert(0, ('_cls', 1)) if index_list: diff --git a/mongoengine/document.py b/mongoengine/document.py index 838feb81f..3d919e7dc 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -637,14 +637,19 @@ def ensure_index(cls, key_or_list, drop_dups=False, background=False, :param key_or_list: a single index key or a list of index keys (to construct a multi-field index); keys may be prefixed with a **+** or a **-** to determine the index ordering + :param background: Allows index creation in the background + :param drop_dups: Was removed with MongoDB 3. The value will be + removed if PyMongo3+ is used """ index_spec = cls._build_index_spec(key_or_list) index_spec = index_spec.copy() fields = index_spec.pop('fields') index_spec['drop_dups'] = drop_dups + # TODO: raise warning if dropdups given and remove with PyMongo3+ index_spec['background'] = background index_spec.update(kwargs) + # TODO: ensure_index is deprecated return cls._get_collection().ensure_index(fields, **index_spec) @classmethod @@ -688,6 +693,7 @@ def ensure_indexes(cls): if 'cls' in opts: del opts['cls'] + # TODO: ensure_index is deprecated in PyMongo 3+ and drop_dups removed collection.ensure_index(fields, background=background, drop_dups=drop_dups, **opts) @@ -701,6 +707,7 @@ def ensure_indexes(cls): if 'cls' in index_opts: del index_opts['cls'] + # TODO: ensure_index is deprecated in PyMongo 3+ collection.ensure_index('_cls', background=background, **index_opts) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index d43b22e56..cecb8bf28 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -275,6 +275,49 @@ class Place(Document): info = [value['key'] for key, value in info.iteritems()] self.assertTrue([('current.location.point', '2d')] in info) + def test_explicit_geosphere_index(self): + """Ensure that geosphere indexes work when created via meta[indexes] + """ + class Place(Document): + location = DictField() + meta = { + 'allow_inheritance': True, + 'indexes': [ + '(location.point', + ] + } + + self.assertEqual([{'fields': [('location.point', '2dsphere')]}], + Place._meta['index_specs']) + + Place.ensure_indexes() + info = Place._get_collection().index_information() + info = [value['key'] for key, value in info.iteritems()] + self.assertTrue([('location.point', '2dsphere')] in info) + + def test_explicit_geohaystack_index(self): + """Ensure that geohaystack indexes work when created via meta[indexes] + """ + raise SkipTest('GeoHaystack index creation seems broken on PyMongo' + 'side, as it requires a bucketSize parameter.') + + class Place(Document): + location = DictField() + meta = { + 'allow_inheritance': True, + 'indexes': [ + ')location.point', + ] + } + + self.assertEqual([{'fields': [('location.point', 'geoHaystack')]}], + Place._meta['index_specs']) + + Place.ensure_indexes() + info = Place._get_collection().index_information() + info = [value['key'] for key, value in info.iteritems()] + self.assertTrue([('location.point', 'geoHaystack')] in info) + def test_dictionary_indexes(self): """Ensure that indexes are used when meta[indexes] contains dictionaries instead of lists. @@ -822,6 +865,18 @@ class Book(Document): key = indexes["title_text"]["key"] self.assertTrue(('_fts', 'text') in key) + def test_hashed_indexes(self): + + class Book(Document): + ref_id = StringField() + meta = { + "indexes": ["#ref_id"], + } + + indexes = Book.objects._collection.index_information() + self.assertTrue("ref_id_hashed" in indexes) + self.assertTrue(('ref_id', 'hashed') in indexes["ref_id_hashed"]["key"]) + def test_indexes_after_database_drop(self): """ Test to ensure that indexes are re-created on a collection even @@ -909,6 +964,30 @@ class TestChildDoc(TestDoc): } }) + def test_compound_index_underscore_cls_not_overwritten(self): + """ + Test that the compound index doesn't get another _cls when it is specified + """ + class TestDoc(Document): + shard_1 = StringField() + txt_1 = StringField() + + meta = { + 'collection': 'test', + 'allow_inheritance': True, + 'sparse': True, + 'shard_key': 'shard_1', + 'indexes': [ + ('shard_1', '_cls', 'txt_1'), + ] + } + + TestDoc.drop_collection() + TestDoc.ensure_indexes() + + index_info = TestDoc._get_collection().index_information() + self.assertTrue('shard_1_1__cls_1_txt_1_1' in index_info) + if __name__ == '__main__': unittest.main() From f35d0b2b3778f249b7b49f42e9a372ecf6deb53a Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Mon, 1 Jun 2015 23:12:43 +0200 Subject: [PATCH 0171/1519] Added create_index method, warnings for drop_dups and a geohaystack test --- mongoengine/document.py | 68 ++++++++++++++++++++++++++++----------- tests/document/indexes.py | 23 +++++++++---- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 3d919e7dc..e4507c1c1 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,4 +1,4 @@ - +import warnings import pymongo import re @@ -17,6 +17,7 @@ get_document ) from mongoengine.errors import InvalidQueryError, InvalidDocumentError +from mongoengine.python_support import IS_PYMONGO_3 from mongoengine.queryset import (OperationError, NotUniqueError, QuerySet, transform) from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME @@ -630,27 +631,50 @@ def drop_collection(cls): db.drop_collection(cls._get_collection_name()) @classmethod - def ensure_index(cls, key_or_list, drop_dups=False, background=False, - **kwargs): - """Ensure that the given indexes are in place. + def create_index(cls, keys, background=False, **kwargs): + """Creates the given indexes if required. - :param key_or_list: a single index key or a list of index keys (to + :param keys: a single index key or a list of index keys (to construct a multi-field index); keys may be prefixed with a **+** or a **-** to determine the index ordering :param background: Allows index creation in the background - :param drop_dups: Was removed with MongoDB 3. The value will be - removed if PyMongo3+ is used """ - index_spec = cls._build_index_spec(key_or_list) + index_spec = cls._build_index_spec(keys) index_spec = index_spec.copy() fields = index_spec.pop('fields') - index_spec['drop_dups'] = drop_dups - # TODO: raise warning if dropdups given and remove with PyMongo3+ + drop_dups = kwargs.get('drop_dups', False) + if IS_PYMONGO_3 and drop_dups: + msg = "drop_dups is deprecated and is removed when using PyMongo 3+." + warnings.warn(msg, DeprecationWarning) + elif not IS_PYMONGO_3: + index_spec['drop_dups'] = drop_dups index_spec['background'] = background index_spec.update(kwargs) - # TODO: ensure_index is deprecated - return cls._get_collection().ensure_index(fields, **index_spec) + if IS_PYMONGO_3: + return cls._get_collection().create_index(fields, **index_spec) + else: + return cls._get_collection().ensure_index(fields, **index_spec) + + @classmethod + def ensure_index(cls, key_or_list, drop_dups=False, background=False, + **kwargs): + """Ensure that the given indexes are in place. Deprecated in favour + of create_index. + + :param key_or_list: a single index key or a list of index keys (to + construct a multi-field index); keys may be prefixed with a **+** + or a **-** to determine the index ordering + :param background: Allows index creation in the background + :param drop_dups: Was removed/ignored with MongoDB >2.7.5. The value + will be removed if PyMongo3+ is used + """ + if IS_PYMONGO_3 and drop_dups: + msg = "drop_dups is deprecated and is removed when using PyMongo 3+." + warnings.warn(msg, DeprecationWarning) + elif not IS_PYMONGO_3: + kwargs.update({'drop_dups': drop_dups}) + return cls.create_index(key_or_list, background=background, **kwargs) @classmethod def ensure_indexes(cls): @@ -665,6 +689,9 @@ def ensure_indexes(cls): drop_dups = cls._meta.get('index_drop_dups', False) index_opts = cls._meta.get('index_opts') or {} index_cls = cls._meta.get('index_cls', True) + if IS_PYMONGO_3 and drop_dups: + msg = "drop_dups is deprecated and is removed when using PyMongo 3+." + warnings.warn(msg, DeprecationWarning) collection = cls._get_collection() # 746: when connection is via mongos, the read preference is not necessarily an indication that @@ -693,9 +720,11 @@ def ensure_indexes(cls): if 'cls' in opts: del opts['cls'] - # TODO: ensure_index is deprecated in PyMongo 3+ and drop_dups removed - collection.ensure_index(fields, background=background, - drop_dups=drop_dups, **opts) + if IS_PYMONGO_3: + collection.create_index(fields, background=background, **opts) + else: + collection.ensure_index(fields, background=background, + drop_dups=drop_dups, **opts) # If _cls is being used (for polymorphism), it needs an index, # only if another index doesn't begin with _cls @@ -707,9 +736,12 @@ def ensure_indexes(cls): if 'cls' in index_opts: del index_opts['cls'] - # TODO: ensure_index is deprecated in PyMongo 3+ - collection.ensure_index('_cls', background=background, - **index_opts) + if IS_PYMONGO_3: + collection.create_index('_cls', background=background, + **index_opts) + else: + collection.ensure_index('_cls', background=background, + **index_opts) @classmethod def list_indexes(cls, go_up=True, go_down=True): diff --git a/tests/document/indexes.py b/tests/document/indexes.py index cecb8bf28..f9f68e573 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -298,19 +298,18 @@ class Place(Document): def test_explicit_geohaystack_index(self): """Ensure that geohaystack indexes work when created via meta[indexes] """ - raise SkipTest('GeoHaystack index creation seems broken on PyMongo' - 'side, as it requires a bucketSize parameter.') + raise SkipTest('GeoHaystack index creation is not supported for now' + 'from meta, as it requires a bucketSize parameter.') class Place(Document): location = DictField() + name = StringField() meta = { - 'allow_inheritance': True, 'indexes': [ - ')location.point', + (')location.point', 'name') ] } - - self.assertEqual([{'fields': [('location.point', 'geoHaystack')]}], + self.assertEqual([{'fields': [('location.point', 'geoHaystack'), ('name', 1)]}], Place._meta['index_specs']) Place.ensure_indexes() @@ -318,6 +317,18 @@ class Place(Document): info = [value['key'] for key, value in info.iteritems()] self.assertTrue([('location.point', 'geoHaystack')] in info) + def test_create_geohaystack_index(self): + """Ensure that geohaystack indexes can be created + """ + class Place(Document): + location = DictField() + name = StringField() + + Place.ensure_index({'fields': (')location.point', 'name')}, bucketSize=10) + info = Place._get_collection().index_information() + info = [value['key'] for key, value in info.iteritems()] + self.assertTrue([('location.point', 'geoHaystack'), ('name', 1)] in info) + def test_dictionary_indexes(self): """Ensure that indexes are used when meta[indexes] contains dictionaries instead of lists. From a0257ed7e73383a9bd395877c83351bea8311cd3 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Tue, 2 Jun 2015 00:14:18 +0200 Subject: [PATCH 0172/1519] Updated test to use new create_index method --- tests/document/indexes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/document/indexes.py b/tests/document/indexes.py index f9f68e573..dcc71e5b0 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -324,7 +324,7 @@ class Place(Document): location = DictField() name = StringField() - Place.ensure_index({'fields': (')location.point', 'name')}, bucketSize=10) + Place.create_index({'fields': (')location.point', 'name')}, bucketSize=10) info = Place._get_collection().index_information() info = [value['key'] for key, value in info.iteritems()] self.assertTrue([('location.point', 'geoHaystack'), ('name', 1)] in info) From fc140d04ef666930709ac185fc23a319372b443a Mon Sep 17 00:00:00 2001 From: Charanpal Dhanjal Date: Tue, 2 Jun 2015 10:15:27 +0100 Subject: [PATCH 0173/1519] Fix comment in delete --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 813fcb2e6..f83f9fa14 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -463,7 +463,7 @@ def delete(self, **write_concern): """ signals.pre_delete.send(self.__class__, document=self) - #Delete FileFields seperately + # Delete FileFields separately FileField = _import_class('FileField') for name, field in self._fields.iteritems(): if isinstance(field, FileField): From ad81470d350d427b7c10e8c584e51af7c81e7c52 Mon Sep 17 00:00:00 2001 From: Charanpal Dhanjal Date: Tue, 2 Jun 2015 10:17:17 +0100 Subject: [PATCH 0174/1519] Put space after hash --- tests/fields/file_tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 7db900596..4bd1b164a 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -309,7 +309,7 @@ class TestFile(Document): testfile.the_file.put(text, content_type=content_type, filename="hello") testfile.save() - #Now check fs.files and fs.chunks + # Now check fs.files and fs.chunks db = TestFile._get_db() files = db.fs.files.find() @@ -317,7 +317,7 @@ class TestFile(Document): self.assertEquals(len(list(files)), 1) self.assertEquals(len(list(chunks)), 1) - #Deleting the docoument should delete the files + # Deleting the docoument should delete the files testfile.delete() files = db.fs.files.find() @@ -325,7 +325,7 @@ class TestFile(Document): self.assertEquals(len(list(files)), 0) self.assertEquals(len(list(chunks)), 0) - #Test case where we don't store a file in the first place + # Test case where we don't store a file in the first place testfile = TestFile() testfile.save() @@ -341,7 +341,7 @@ class TestFile(Document): self.assertEquals(len(list(files)), 0) self.assertEquals(len(list(chunks)), 0) - #Test case where we overwrite the file + # Test case where we overwrite the file testfile = TestFile() testfile.the_file.put(text, content_type=content_type, filename="hello") testfile.save() From 73893f2a335c4389b544020f71b95d05922779c8 Mon Sep 17 00:00:00 2001 From: Charanpal Dhanjal Date: Tue, 2 Jun 2015 10:20:37 +0100 Subject: [PATCH 0175/1519] Added charanpald --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index f424dbc2f..37170ffa5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -222,3 +222,4 @@ that much better: * Catstyle Lee (https://github.com/Catstyle) * Kiryl Yermakou (https://github.com/rma4ok) * Matthieu Rigal (https://github.com/MRigal) + * Charanpal Dhanjal (https://github.com/charanpald) From 380d869195f42e63c3e5f9b23eb3c22b4ad4466f Mon Sep 17 00:00:00 2001 From: Charanpal Dhanjal Date: Tue, 2 Jun 2015 10:23:37 +0100 Subject: [PATCH 0176/1519] Add fix to FileField deletion --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 19d306986..ffbc8c54a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,7 @@ Changes in 0.9.X - DEV - Fix for updating sorting in SortedListField. #978 - Added __ support to escape field name in fields lookup keywords that match operators names #949 - Support for PyMongo 3+ #946 +- Fix for issue where FileField deletion did not free space in GridFS. Changes in 0.9.0 ================ From 65d6f8c01817ce7a7c104391db087f9a90f91269 Mon Sep 17 00:00:00 2001 From: Frederik Creemers Date: Tue, 2 Jun 2015 12:35:25 +0200 Subject: [PATCH 0177/1519] Solution for documentation issue #1003 Solution for documentation issue #1003. The explanation about reverse_delete_rule was a bit mixed up. --- docs/guide/defining-documents.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 79332493d..62676983a 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -315,9 +315,9 @@ reference with a delete rule specification. A delete rule is specified by supplying the :attr:`reverse_delete_rule` attributes on the :class:`ReferenceField` definition, like this:: - class Employee(Document): + class ProfilePage(Document): ... - profile_page = ReferenceField('ProfilePage', reverse_delete_rule=mongoengine.NULLIFY) + profile_page = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE) The declaration in this example means that when an :class:`Employee` object is removed, the :class:`ProfilePage` that belongs to that employee is removed as From 621350515e60118d309767974d57d6a2b5729dd7 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Wed, 3 Jun 2015 01:02:19 +0200 Subject: [PATCH 0178/1519] Added test was still failing and implemented solution as described in #517 --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 1 + mongoengine/fields.py | 2 +- tests/queryset/queryset.py | 37 ++++++++++++++++++++++++++++++++++++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ffbc8c54a..42d98597d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,7 @@ Changes in 0.9.X - DEV - Added __ support to escape field name in fields lookup keywords that match operators names #949 - Support for PyMongo 3+ #946 - Fix for issue where FileField deletion did not free space in GridFS. +- No_dereference() not respected on embedded docs containing reference. #517 Changes in 0.9.0 ================ diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 91f95b4f4..b3ec07639 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -290,6 +290,7 @@ def to_python(self, value): return value if self.field: + self.field._auto_dereference = self._auto_dereference value_dict = dict([(key, self.field.to_python(item)) for key, item in value.items()]) else: diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 53d4ac956..9176828c6 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -546,7 +546,7 @@ def document_type(self): def to_python(self, value): if not isinstance(value, self.document_type): - return self.document_type._from_son(value) + return self.document_type._from_son(value, _auto_dereference=self._auto_dereference) return value def to_mongo(self, value, use_db_field=True, fields=[]): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 65d843059..3dc3ef205 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -13,7 +13,7 @@ from pymongo.errors import ConfigurationError from pymongo.read_preferences import ReadPreference -from bson import ObjectId +from bson import ObjectId, DBRef from mongoengine import * from mongoengine.connection import get_connection, get_db @@ -4185,6 +4185,41 @@ class User(Document): Organization)) self.assertTrue(isinstance(qs.first().organization, Organization)) + def test_no_dereference_embedded_doc(self): + + class User(Document): + name = StringField() + + class Member(EmbeddedDocument): + name = StringField() + user = ReferenceField(User) + + class Organization(Document): + name = StringField() + members = ListField(EmbeddedDocumentField(Member)) + ceo = ReferenceField(User) + member = EmbeddedDocumentField(Member) + admin = ListField(ReferenceField(User)) + + Organization.drop_collection() + User.drop_collection() + + user = User(name="Flash") + user.save() + + member = Member(name="Flash", user=user) + + company = Organization(name="Mongo Inc", ceo=user, member=member) + company.admin.append(user) + company.members.append(member) + company.save() + + result = Organization.objects().no_dereference().first() + + self.assertTrue(isinstance(result.admin[0], (DBRef, ObjectId))) + self.assertTrue(isinstance(result.member.user, (DBRef, ObjectId))) + self.assertTrue(isinstance(result.members[0].user, (DBRef, ObjectId))) + def test_cached_queryset(self): class Person(Document): name = StringField() From ab2ef69c6a5574dcabfaffa8331d71e5a827fba3 Mon Sep 17 00:00:00 2001 From: "Breeze.kay" Date: Wed, 3 Jun 2015 18:13:54 +0800 Subject: [PATCH 0179/1519] improve _created status when switch collection and db --- mongoengine/document.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index f83f9fa14..791572a42 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -477,7 +477,7 @@ def delete(self, **write_concern): raise OperationError(message) signals.post_delete.send(self.__class__, document=self) - def switch_db(self, db_alias): + def switch_db(self, db_alias, keep_created=True): """ Temporarily switch the database for a document instance. @@ -499,12 +499,12 @@ def switch_db(self, db_alias): self._get_collection = lambda: collection self._get_db = lambda: db self._collection = collection - self._created = True + self._created = True if not keep_created else self._created self.__objects = self._qs self.__objects._collection_obj = collection return self - def switch_collection(self, collection_name): + def switch_collection(self, collection_name, keep_created=True): """ Temporarily switch the collection for a document instance. @@ -525,7 +525,7 @@ def switch_collection(self, collection_name): collection = cls._get_collection() self._get_collection = lambda: collection self._collection = collection - self._created = True + self._created = True if not keep_created else self._created self.__objects = self._qs self.__objects._collection_obj = collection return self From 0f63e26641a113feae8e21a81beb0c55ffc996a2 Mon Sep 17 00:00:00 2001 From: Marcel van den Elst Date: Thu, 4 Jun 2015 15:02:32 +0200 Subject: [PATCH 0180/1519] use AssertEqual instead of AssertListEqual for py2.6 compatibility --- tests/queryset/queryset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 6e4d86d2e..8daccfd0a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -649,13 +649,13 @@ class TestOrganization( Document ): o.owner = p p.name = 'p2' - self.assertListEqual( o._get_changed_fields(), [ 'owner' ] ) - self.assertListEqual( p._get_changed_fields(), [ 'name' ] ) + self.assertEqual( o._get_changed_fields(), [ 'owner' ] ) + self.assertEqual( p._get_changed_fields(), [ 'name' ] ) o.save() - self.assertListEqual( o._get_changed_fields(), [] ) - self.assertListEqual( p._get_changed_fields(), [ 'name' ] ) # Fails; it's empty + self.assertEqual( o._get_changed_fields(), [] ) + self.assertEqual( p._get_changed_fields(), [ 'name' ] ) # Fails; it's empty # This will do NOTHING at all, even though we changed the name p.save() From 91aa4586e2a7684ff3a73f21a032e1702c04dbc5 Mon Sep 17 00:00:00 2001 From: Frederik Creemers Date: Thu, 4 Jun 2015 22:38:11 +0200 Subject: [PATCH 0181/1519] Fixes after code review --- docs/guide/defining-documents.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 62676983a..4d81b0546 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -317,10 +317,10 @@ supplying the :attr:`reverse_delete_rule` attributes on the class ProfilePage(Document): ... - profile_page = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE) + employee = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE) The declaration in this example means that when an :class:`Employee` object is -removed, the :class:`ProfilePage` that belongs to that employee is removed as +removed, the :class:`ProfilePage` that references that employee is removed as well. If a whole batch of employees is removed, all profile pages that are linked are removed as well. From 3e000f9be1d7e4fdd93317b6da0ab4a0e07b5431 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Thu, 21 May 2015 17:35:51 +0200 Subject: [PATCH 0182/1519] Raise error if save_condition fails #991 --- mongoengine/document.py | 6 +++++- tests/document/instance.py | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 060919ca6..2c4872e91 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -266,7 +266,8 @@ def save(self, force_insert=False, validate=True, clean=True, to cascading saves. Implies ``cascade=True``. :param _refs: A list of processed references used in cascading saves :param save_condition: only perform save if matching record in db - satisfies condition(s) (e.g., version number) + satisfies condition(s) (e.g., version number). + Raises :class:`OperationError` if the conditions are not satisfied .. versionchanged:: 0.5 In existing documents it only saves changed fields using @@ -348,6 +349,9 @@ def is_new_object(last_error): upsert = save_condition is None last_error = collection.update(select_dict, update_query, upsert=upsert, **write_concern) + if save_condition is not None and last_error['nModified'] == 0: + raise OperationError('Race condition preventing' + ' document update detected') created = is_new_object(last_error) if cascade is None: diff --git a/tests/document/instance.py b/tests/document/instance.py index 2cfdef652..3ccabacdb 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -954,11 +954,12 @@ def UUID(i): self.assertEqual(w1.save_id, UUID(1)) self.assertEqual(w1.count, 0) - # mismatch in save_condition prevents save + # mismatch in save_condition prevents save and raise exception flip(w1) self.assertTrue(w1.toggle) self.assertEqual(w1.count, 1) - w1.save(save_condition={'save_id': UUID(42)}) + self.assertRaises(OperationError, + w1.save, save_condition={'save_id': UUID(42)}) w1.reload() self.assertFalse(w1.toggle) self.assertEqual(w1.count, 0) @@ -986,7 +987,8 @@ def UUID(i): self.assertEqual(w1.count, 2) flip(w2) flip(w2) - w2.save(save_condition={'save_id': old_id}) + self.assertRaises(OperationError, + w2.save, save_condition={'save_id': old_id}) w2.reload() self.assertFalse(w2.toggle) self.assertEqual(w2.count, 2) @@ -998,7 +1000,8 @@ def UUID(i): self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) flip(w1) - w1.save(save_condition={'count__gte': w1.count}) + self.assertRaises(OperationError, + w1.save, save_condition={'count__gte': w1.count}) w1.reload() self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) From 682db9b81f24d9160bc5707296ac3a1b507f200a Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 2 Jun 2015 17:44:57 +0200 Subject: [PATCH 0183/1519] Add versionchanged to document save_condition --- mongoengine/document.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 2c4872e91..f798780e0 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -266,7 +266,7 @@ def save(self, force_insert=False, validate=True, clean=True, to cascading saves. Implies ``cascade=True``. :param _refs: A list of processed references used in cascading saves :param save_condition: only perform save if matching record in db - satisfies condition(s) (e.g., version number). + satisfies condition(s) (e.g. version number). Raises :class:`OperationError` if the conditions are not satisfied .. versionchanged:: 0.5 @@ -285,6 +285,8 @@ def save(self, force_insert=False, validate=True, clean=True, .. versionchanged:: 0.8.5 Optional save_condition that only overwrites existing documents if the condition is satisfied in the current db record. + .. versionchanged:: 0.10 + :class:`OperationError` exception raised if save_condition fails. """ signals.pre_save.send(self.__class__, document=self) From cca0222e1d415346eb1ee439ddba21daaf530851 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 2 Jun 2015 17:45:10 +0200 Subject: [PATCH 0184/1519] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 37170ffa5..f64093d81 100644 --- a/AUTHORS +++ b/AUTHORS @@ -223,3 +223,4 @@ that much better: * Kiryl Yermakou (https://github.com/rma4ok) * Matthieu Rigal (https://github.com/MRigal) * Charanpal Dhanjal (https://github.com/charanpald) + * Emmanuel Leblond (https://github.com/touilleMan) From 9c917c3bd389a222b3400dc61fb95de4c322cb75 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 2 Jun 2015 17:48:31 +0200 Subject: [PATCH 0185/1519] Update changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 42d98597d..52e76ffcb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,7 @@ Changes in 0.9.X - DEV - Support for PyMongo 3+ #946 - Fix for issue where FileField deletion did not free space in GridFS. - No_dereference() not respected on embedded docs containing reference. #517 +- Document save raise an exception if save_condition fails Changes in 0.9.0 ================ From 4034ab41829ba7da18af5dc2e8cc1544dbff8614 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Mon, 8 Jun 2015 18:40:28 +0200 Subject: [PATCH 0186/1519] Clean save_condition exception implementation and related tests --- mongoengine/document.py | 2 +- tests/document/instance.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index f798780e0..eedd01d21 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -351,7 +351,7 @@ def is_new_object(last_error): upsert = save_condition is None last_error = collection.update(select_dict, update_query, upsert=upsert, **write_concern) - if save_condition is not None and last_error['nModified'] == 0: + if not upsert and last_error['nModified'] == 0: raise OperationError('Race condition preventing' ' document update detected') created = is_new_object(last_error) diff --git a/tests/document/instance.py b/tests/document/instance.py index 3ccabacdb..29692c204 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -958,8 +958,9 @@ def UUID(i): flip(w1) self.assertTrue(w1.toggle) self.assertEqual(w1.count, 1) - self.assertRaises(OperationError, - w1.save, save_condition={'save_id': UUID(42)}) + self.assertRaisesRegexp(OperationError, + "Race condition preventing document update detected", + w1.save, save_condition={'save_id': UUID(42)}) w1.reload() self.assertFalse(w1.toggle) self.assertEqual(w1.count, 0) @@ -987,8 +988,9 @@ def UUID(i): self.assertEqual(w1.count, 2) flip(w2) flip(w2) - self.assertRaises(OperationError, - w2.save, save_condition={'save_id': old_id}) + self.assertRaisesRegexp(OperationError, + "Race condition preventing document update detected", + w2.save, save_condition={'save_id': old_id}) w2.reload() self.assertFalse(w2.toggle) self.assertEqual(w2.count, 2) @@ -1000,8 +1002,9 @@ def UUID(i): self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) flip(w1) - self.assertRaises(OperationError, - w1.save, save_condition={'count__gte': w1.count}) + self.assertRaisesRegexp(OperationError, + "Race condition preventing document update detected", + w1.save, save_condition={'count__gte': w1.count}) w1.reload() self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) From 153c239c9bcb9acbc3f87c0d89995a128e4e934f Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Thu, 11 Jun 2015 14:36:51 +0200 Subject: [PATCH 0187/1519] Replace assertRaisesRegexp by assertRaises (python2.6 compatibility) --- tests/document/instance.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 29692c204..e1710b9f2 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -958,8 +958,7 @@ def UUID(i): flip(w1) self.assertTrue(w1.toggle) self.assertEqual(w1.count, 1) - self.assertRaisesRegexp(OperationError, - "Race condition preventing document update detected", + self.assertRaises(OperationError, w1.save, save_condition={'save_id': UUID(42)}) w1.reload() self.assertFalse(w1.toggle) @@ -988,8 +987,7 @@ def UUID(i): self.assertEqual(w1.count, 2) flip(w2) flip(w2) - self.assertRaisesRegexp(OperationError, - "Race condition preventing document update detected", + self.assertRaises(OperationError, w2.save, save_condition={'save_id': old_id}) w2.reload() self.assertFalse(w2.toggle) @@ -1002,8 +1000,7 @@ def UUID(i): self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) flip(w1) - self.assertRaisesRegexp(OperationError, - "Race condition preventing document update detected", + self.assertRaises(OperationError, w1.save, save_condition={'count__gte': w1.count}) w1.reload() self.assertTrue(w1.toggle) From cd7a9345ec624094e3811bf0c5ec2ed92d4ccc4d Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Thu, 11 Jun 2015 14:45:19 +0200 Subject: [PATCH 0188/1519] Add issue related in changelog.rst --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 52e76ffcb..65e236bab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,7 +20,7 @@ Changes in 0.9.X - DEV - Support for PyMongo 3+ #946 - Fix for issue where FileField deletion did not free space in GridFS. - No_dereference() not respected on embedded docs containing reference. #517 -- Document save raise an exception if save_condition fails +- Document save raise an exception if save_condition fails #1005 Changes in 0.9.0 ================ From 1951b52aa5b62839f000deacb5ef9a02592c03f4 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 9 Jun 2015 16:20:35 +0200 Subject: [PATCH 0189/1519] Fix #1017 (document clash between same ids but different collections) --- mongoengine/dereference.py | 25 ++++++++++++++++--------- tests/test_dereference.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 0428397c0..8e8920d4a 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -128,21 +128,25 @@ def _fetch_objects(self, doc_type=None): """ object_map = {} for collection, dbrefs in self.reference_map.iteritems(): - refs = [dbref for dbref in dbrefs - if unicode(dbref).encode('utf-8') not in object_map] if hasattr(collection, 'objects'): # We have a document class for the refs + col_name = collection._get_collection_name() + refs = [dbref for dbref in dbrefs + if (col_name, dbref) not in object_map] references = collection.objects.in_bulk(refs) for key, doc in references.iteritems(): - object_map[key] = doc + object_map[(col_name, key)] = doc else: # Generic reference: use the refs data to convert to document if isinstance(doc_type, (ListField, DictField, MapField,)): continue + refs = [dbref for dbref in dbrefs + if (collection, dbref) not in object_map] + if doc_type: references = doc_type._get_db()[collection].find({'_id': {'$in': refs}}) for ref in references: doc = doc_type._from_son(ref) - object_map[doc.id] = doc + object_map[(collection, doc.id)] = doc else: references = get_db()[collection].find({'_id': {'$in': refs}}) for ref in references: @@ -154,7 +158,7 @@ def _fetch_objects(self, doc_type=None): for x in collection.split('_')))._from_son(ref) else: doc = doc_type._from_son(ref) - object_map[doc.id] = doc + object_map[(collection, doc.id)] = doc return object_map def _attach_objects(self, items, depth=0, instance=None, name=None): @@ -180,7 +184,8 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): if isinstance(items, (dict, SON)): if '_ref' in items: - return self.object_map.get(items['_ref'].id, items) + return self.object_map.get( + (items['_ref'].collection, items['_ref'].id), items) elif '_cls' in items: doc = get_document(items['_cls'])._from_son(items) _cls = doc._data.pop('_cls', None) @@ -216,9 +221,11 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): for field_name, field in v._fields.iteritems(): v = data[k]._data.get(field_name, None) if isinstance(v, (DBRef)): - data[k]._data[field_name] = self.object_map.get(v.id, v) + data[k]._data[field_name] = self.object_map.get( + (v.collection, v.id), v) elif isinstance(v, (dict, SON)) and '_ref' in v: - data[k]._data[field_name] = self.object_map.get(v['_ref'].id, v) + data[k]._data[field_name] = self.object_map.get( + (v['_ref'].collection , v['_ref'].id), v) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: item_name = "{0}.{1}.{2}".format(name, k, field_name) data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name) @@ -226,7 +233,7 @@ def _attach_objects(self, items, depth=0, instance=None, name=None): item_name = '%s.%s' % (name, k) if name else name data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name) elif hasattr(v, 'id'): - data[k] = self.object_map.get(v.id, v) + data[k] = self.object_map.get((v.collection, v.id), v) if instance and name: if is_list: diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 2115b45a5..e1ae37408 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -1026,6 +1026,43 @@ class Baz(Document): self.assertEqual(type(foo.bar), Bar) self.assertEqual(type(foo.baz), Baz) + + def test_document_reload_reference_integrity(self): + """ + Ensure reloading a document with multiple similar id + in different collections doesn't mix them. + """ + class Topic(Document): + id = IntField(primary_key=True) + class User(Document): + id = IntField(primary_key=True) + name = StringField() + class Message(Document): + id = IntField(primary_key=True) + topic = ReferenceField(Topic) + author = ReferenceField(User) + + Topic.drop_collection() + User.drop_collection() + Message.drop_collection() + + # All objects share the same id, but each in a different collection + topic = Topic(id=1).save() + user = User(id=1, name='user-name').save() + Message(id=1, topic=topic, author=user).save() + + concurrent_change_user = User.objects.get(id=1) + concurrent_change_user.name = 'new-name' + concurrent_change_user.save() + self.assertNotEqual(user.name, 'new-name') + + msg = Message.objects.get(id=1) + msg.reload() + self.assertEqual(msg.topic, topic) + self.assertEqual(msg.author, user) + self.assertEqual(msg.author.name, 'new-name') + + def test_list_lookup_not_checked_in_map(self): """Ensure we dereference list data correctly """ From a2f0f20284ed28c8633ebc6b01a6e9ca71b04f83 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 11 Jun 2015 17:48:34 +0200 Subject: [PATCH 0190/1519] Improve error message for invalid query --- mongoengine/base/document.py | 9 +++++++-- mongoengine/queryset/transform.py | 2 +- tests/queryset/transform.py | 9 +++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 5707992a4..1ab19e754 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -983,8 +983,13 @@ def _lookup_field(cls, parts): if hasattr(getattr(field, 'field', None), 'lookup_member'): new_field = field.field.lookup_member(field_name) else: - # Look up subfield on the previous field - new_field = field.lookup_member(field_name) + # Look up subfield on the previous field or raise + try: + new_field = field.lookup_member(field_name) + except AttributeError: + raise LookUpError('Cannot resolve subfield or operator {} ' + 'on the field {}'.format( + field_name, field.name)) if not new_field and isinstance(field, ComplexBaseField): if hasattr(field.field, 'document_type') and cls._dynamic \ and field.field.document_type._dynamic: diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index c43c4b401..f6cfa87e5 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -44,7 +44,7 @@ def query(_doc_cls=None, _field_operation=False, **query): if len(parts) > 1 and parts[-1] in MATCH_OPERATORS: op = parts.pop() - #if user escape field name by __ + # if user escape field name by __ if len(parts) > 1 and parts[-1] == "": parts.pop() diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 77d3593c2..a543317a9 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -224,6 +224,15 @@ class Doc(Document): self.assertEqual(1, Doc.objects(item__type__="axe").count()) self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count()) + def test_understandable_error_raised(self): + class Event(Document): + title = StringField() + location = GeoPointField() + + box = [(35.0, -125.0), (40.0, -100.0)] + # I *meant* to execute location__within_box=box + events = Event.objects(location__within=box) + self.assertRaises(InvalidQueryError, lambda: events.count()) if __name__ == '__main__': unittest.main() From 1862bcf86709c7e58379ee5c5a0baa52b0347d78 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 14:33:16 +0200 Subject: [PATCH 0191/1519] added test for abstract document without pk creation and adapted behaviour --- mongoengine/base/document.py | 2 +- mongoengine/document.py | 2 ++ tests/document/inheritance.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 5707992a4..7142a3de2 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -184,7 +184,7 @@ def __setattr__(self, name, value): self__initialised = False # Check if the user has created a new instance of a class if (self._is_document and self__initialised - and self__created and name == self._meta['id_field']): + and self__created and name == self._meta.get('id_field')): super(BaseDocument, self).__setattr__('_created', False) super(BaseDocument, self).__setattr__(name, value) diff --git a/mongoengine/document.py b/mongoengine/document.py index eedd01d21..01b69fb2b 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -152,6 +152,8 @@ def pk(): """ def fget(self): + if not 'id_field' in self._meta: + return None return getattr(self, self._meta['id_field']) def fset(self, value): diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index e8347054c..afa3ccea0 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -307,6 +307,17 @@ class Mammal(Animal): doc = Animal(name='dog') self.assertFalse('_cls' in doc.to_mongo()) + def test_abstract_document_creation_does_not_fail(self): + + class City(Document): + continent = StringField() + meta = {'abstract': True, + 'allow_inheritance': False} + bkk = City(continent='asia') + self.assertEqual(None, bkk.pk) + # TODO: expected error? Shouldn'twe created a new error type + self.assertRaises(KeyError, lambda: setattr(bkk, 'pk', 1)) + def test_allow_inheritance_embedded_document(self): """Ensure embedded documents respect inheritance """ From 53fbc165ba84b0314d2c10f16bf9425bbba44fa2 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 14:37:46 +0200 Subject: [PATCH 0192/1519] added content of PR #688 with a test to proove it is a bit right --- mongoengine/base/metaclasses.py | 14 ++++++++++---- tests/document/inheritance.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index 8a25ff3dc..6fdbe19c3 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -390,10 +390,16 @@ def __new__(cls, name, bases, attrs): new_class._fields['id'] = ObjectIdField(db_field='_id') new_class._fields['id'].name = 'id' new_class.id = new_class._fields['id'] - - # Prepend id field to _fields_ordered - if 'id' in new_class._fields and 'id' not in new_class._fields_ordered: - new_class._fields_ordered = ('id', ) + new_class._fields_ordered + new_class._db_field_map['id'] = '_id' + new_class._reverse_db_field_map['_id'] = 'id' + if 'id' in new_class._fields_ordered: + # An existing id field will be overwritten anyway, so remove it + loc = new_class._fields_ordered.index('id') + new_class._fields_ordered = new_class._fields_ordered[:loc] + \ + new_class._fields_ordered[loc+1:] + else: + # Prepend id field to _fields_ordered + new_class._fields_ordered = ('id', ) + new_class._fields_ordered # Merge in exceptions with parent hierarchy exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned) diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index afa3ccea0..e408e6110 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -307,6 +307,23 @@ class Mammal(Animal): doc = Animal(name='dog') self.assertFalse('_cls' in doc.to_mongo()) + def test_abstract_handle_ids_in_metaclass_properly(self): + + class City(Document): + continent = StringField() + meta = {'abstract': True, + 'allow_inheritance': False} + + class EuropeanCity(City): + name = StringField() + country = StringField() + + berlin = EuropeanCity(name='Berlin', continent='Europe') + self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered)) + self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered)) + self.assertEqual(len(berlin._fields_ordered), 4) + self.assertEqual(berlin._fields_ordered[0], 'id') + def test_abstract_document_creation_does_not_fail(self): class City(Document): From 051cd744adbe9a00ee3e3630167bfd80bc431589 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 14:38:30 +0200 Subject: [PATCH 0193/1519] added another test to proove we still do not handle all cases well --- tests/document/inheritance.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index e408e6110..cbc0d0b76 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -324,6 +324,24 @@ class EuropeanCity(City): self.assertEqual(len(berlin._fields_ordered), 4) self.assertEqual(berlin._fields_ordered[0], 'id') + def test_auto_id_vs_non_pk_id_field(self): + + class City(Document): + continent = StringField() + id = IntField() + meta = {'abstract': True, + 'allow_inheritance': False} + + class EuropeanCity(City): + name = StringField() + country = StringField() + + berlin = EuropeanCity(name='Berlin', continent='Europe') + self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered)) + self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered)) + self.assertEqual(len(berlin._fields_ordered), 5) + self.assertEqual(berlin._fields_ordered[0], 'id') + def test_abstract_document_creation_does_not_fail(self): class City(Document): From 2e963023368372bb975bd671124686b20db822a6 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 16 Apr 2015 14:53:28 +0200 Subject: [PATCH 0194/1519] not in fix --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 01b69fb2b..7b05ef8c3 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -152,7 +152,7 @@ def pk(): """ def fget(self): - if not 'id_field' in self._meta: + if 'id_field' not in self._meta: return None return getattr(self, self._meta['id_field']) From 915849b2ce7706dbf091207f92ff7d9e37291823 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 11 Jun 2015 16:39:20 +0200 Subject: [PATCH 0195/1519] Implemented method to auto-generate non-collisioning auto_id names --- docs/changelog.rst | 1 + docs/guide/defining-documents.rst | 2 +- mongoengine/base/metaclasses.py | 38 +++++++++++++++++++------------ tests/document/inheritance.py | 29 ++++++++++++++++++----- tests/queryset/queryset.py | 2 -- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 65e236bab..dfa9c7a60 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,7 @@ Changes in 0.9.X - DEV - Fix for issue where FileField deletion did not free space in GridFS. - No_dereference() not respected on embedded docs containing reference. #517 - Document save raise an exception if save_condition fails #1005 +- Fixes some internal _id handling issue. #961 Changes in 0.9.0 ================ diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index d1448158a..4572d30b7 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -310,7 +310,7 @@ Dealing with deletion of referred documents By default, MongoDB doesn't check the integrity of your data, so deleting documents that other documents still hold references to will lead to consistency issues. Mongoengine's :class:`ReferenceField` adds some functionality to -safeguard against these kinds of database integrity problems, providing each +safeguard against these kinds of database integrit2y problems, providing each reference with a delete rule specification. A delete rule is specified by supplying the :attr:`reverse_delete_rule` attributes on the :class:`ReferenceField` definition, like this:: diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index 6fdbe19c3..f608940db 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -385,21 +385,17 @@ def __new__(cls, name, bases, attrs): new_class._auto_id_field = getattr(parent_doc_cls, '_auto_id_field', False) if not new_class._meta.get('id_field'): + # After 0.10, find not existing names, instead of overwriting + id_name, id_db_name = cls.get_auto_id_names(new_class) new_class._auto_id_field = True - new_class._meta['id_field'] = 'id' - new_class._fields['id'] = ObjectIdField(db_field='_id') - new_class._fields['id'].name = 'id' - new_class.id = new_class._fields['id'] - new_class._db_field_map['id'] = '_id' - new_class._reverse_db_field_map['_id'] = 'id' - if 'id' in new_class._fields_ordered: - # An existing id field will be overwritten anyway, so remove it - loc = new_class._fields_ordered.index('id') - new_class._fields_ordered = new_class._fields_ordered[:loc] + \ - new_class._fields_ordered[loc+1:] - else: - # Prepend id field to _fields_ordered - new_class._fields_ordered = ('id', ) + new_class._fields_ordered + new_class._meta['id_field'] = id_name + new_class._fields[id_name] = ObjectIdField(db_field=id_db_name) + new_class._fields[id_name].name = id_name + new_class.id = new_class._fields[id_name] + new_class._db_field_map[id_name] = id_db_name + new_class._reverse_db_field_map[id_db_name] = id_name + # Prepend id field to _fields_ordered + new_class._fields_ordered = (id_name, ) + new_class._fields_ordered # Merge in exceptions with parent hierarchy exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned) @@ -414,6 +410,20 @@ def __new__(cls, name, bases, attrs): return new_class + def get_auto_id_names(self): + id_name, id_db_name = ('id', '_id') + if id_name not in self._fields and \ + id_db_name not in (v.db_field for v in self._fields.values()): + return id_name, id_db_name + id_basename, id_db_basename, i = 'auto_id', '_auto_id', 0 + while id_name in self._fields or \ + id_db_name in (v.db_field for v in self._fields.values()): + id_name = '{}_{}'.format(id_basename, i) + id_db_name = '{}_{}'.format(id_db_basename, i) + i += 1 + return id_name, id_db_name + + class MetaDict(dict): diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index cbc0d0b76..7673a103d 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -316,14 +316,30 @@ class City(Document): class EuropeanCity(City): name = StringField() - country = StringField() berlin = EuropeanCity(name='Berlin', continent='Europe') self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered)) self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered)) - self.assertEqual(len(berlin._fields_ordered), 4) + self.assertEqual(len(berlin._fields_ordered), 3) self.assertEqual(berlin._fields_ordered[0], 'id') + def test_auto_id_not_set_if_specific_in_parent_class(self): + + class City(Document): + continent = StringField() + city_id = IntField(primary_key=True) + meta = {'abstract': True, + 'allow_inheritance': False} + + class EuropeanCity(City): + name = StringField() + + berlin = EuropeanCity(name='Berlin', continent='Europe') + self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered)) + self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered)) + self.assertEqual(len(berlin._fields_ordered), 3) + self.assertEqual(berlin._fields_ordered[0], 'city_id') + def test_auto_id_vs_non_pk_id_field(self): class City(Document): @@ -334,13 +350,14 @@ class City(Document): class EuropeanCity(City): name = StringField() - country = StringField() berlin = EuropeanCity(name='Berlin', continent='Europe') self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered)) self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered)) - self.assertEqual(len(berlin._fields_ordered), 5) - self.assertEqual(berlin._fields_ordered[0], 'id') + self.assertEqual(len(berlin._fields_ordered), 4) + self.assertEqual(berlin._fields_ordered[0], 'auto_id_0') + berlin.save() + self.assertEqual(berlin.pk, berlin.auto_id_0) def test_abstract_document_creation_does_not_fail(self): @@ -350,7 +367,7 @@ class City(Document): 'allow_inheritance': False} bkk = City(continent='asia') self.assertEqual(None, bkk.pk) - # TODO: expected error? Shouldn'twe created a new error type + # TODO: expected error? Shouldn't we create a new error type? self.assertRaises(KeyError, lambda: setattr(bkk, 'pk', 1)) def test_allow_inheritance_embedded_document(self): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 0965e40f0..83fb52df3 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3689,11 +3689,9 @@ def invalid_where(): def test_scalar(self): class Organization(Document): - id = ObjectIdField('_id') name = StringField() class User(Document): - id = ObjectIdField('_id') name = StringField() organization = ObjectIdField() From 810819861354914a217a58e6f7e9c772dca4549a Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Thu, 11 Jun 2015 22:38:05 +0200 Subject: [PATCH 0196/1519] corrected formatting for Python 2.6 compatibility --- mongoengine/base/metaclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index f608940db..16e6bd298 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -418,8 +418,8 @@ def get_auto_id_names(self): id_basename, id_db_basename, i = 'auto_id', '_auto_id', 0 while id_name in self._fields or \ id_db_name in (v.db_field for v in self._fields.values()): - id_name = '{}_{}'.format(id_basename, i) - id_db_name = '{}_{}'.format(id_db_basename, i) + id_name = '{0}_{1}'.format(id_basename, i) + id_db_name = '{0}_{1}'.format(id_db_basename, i) i += 1 return id_name, id_db_name From e1da83a8f6e835478e2e039cc241529c2384d2f2 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Fri, 12 Jun 2015 09:12:19 +0200 Subject: [PATCH 0197/1519] Cosmetic --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 4572d30b7..d1448158a 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -310,7 +310,7 @@ Dealing with deletion of referred documents By default, MongoDB doesn't check the integrity of your data, so deleting documents that other documents still hold references to will lead to consistency issues. Mongoengine's :class:`ReferenceField` adds some functionality to -safeguard against these kinds of database integrit2y problems, providing each +safeguard against these kinds of database integrity problems, providing each reference with a delete rule specification. A delete rule is specified by supplying the :attr:`reverse_delete_rule` attributes on the :class:`ReferenceField` definition, like this:: From 3093175f544e9f976caada8d4dbe6dc2f99a0108 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Fri, 12 Jun 2015 11:03:52 +0200 Subject: [PATCH 0198/1519] SequenceField for abstract classes now have a proper name --- mongoengine/base/document.py | 2 +- mongoengine/base/metaclasses.py | 1 - mongoengine/document.py | 8 ++--- mongoengine/fields.py | 4 +-- tests/fields/fields.py | 52 +++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 9357fde26..1049eabdc 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -672,7 +672,7 @@ def _delta(self): @classmethod def _get_collection_name(cls): - """Returns the collection name for this class. + """Returns the collection name for this class. None for abstract class """ return cls._meta.get('collection', None) diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index 16e6bd298..d4c26bfe7 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -424,7 +424,6 @@ def get_auto_id_names(self): return id_name, id_db_name - class MetaDict(dict): """Custom dictionary for meta classes. diff --git a/mongoengine/document.py b/mongoengine/document.py index 7b05ef8c3..885f7eeda 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -145,7 +145,7 @@ class Document(BaseDocument): my_metaclass = TopLevelDocumentMetaclass __metaclass__ = TopLevelDocumentMetaclass - __slots__ = ('__objects') + __slots__ = ('__objects',) def pk(): """Primary key alias @@ -174,10 +174,10 @@ def _get_collection(cls): db = cls._get_db() collection_name = cls._get_collection_name() # Create collection as a capped collection if specified - if cls._meta['max_size'] or cls._meta['max_documents']: + if cls._meta.get('max_size') or cls._meta.get('max_documents'): # Get max document limit and max byte size from meta - max_size = cls._meta['max_size'] or 10000000 # 10MB default - max_documents = cls._meta['max_documents'] + max_size = cls._meta.get('max_size') or 10000000 # 10MB default + max_documents = cls._meta.get('max_documents') if collection_name in db.collection_names(): cls._collection = db[collection_name] diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 9176828c6..94f41d281 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1694,7 +1694,7 @@ def __init__(self, collection_name=None, db_alias=None, sequence_name=None, self.sequence_name = sequence_name self.value_decorator = (callable(value_decorator) and value_decorator or self.VALUE_DECORATOR) - return super(SequenceField, self).__init__(*args, **kwargs) + super(SequenceField, self).__init__(*args, **kwargs) def generate(self): """ @@ -1740,7 +1740,7 @@ def get_sequence_name(self): if self.sequence_name: return self.sequence_name owner = self.owner_document - if issubclass(owner, Document): + if issubclass(owner, Document) and not owner._meta.get('abstract'): return owner._get_collection_name() else: return ''.join('_%s' % c if c.isupper() else c diff --git a/tests/fields/fields.py b/tests/fields/fields.py index fd083c730..9f9bf1ddd 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -39,6 +39,7 @@ def setUp(self): def tearDown(self): self.db.drop_collection('fs.files') self.db.drop_collection('fs.chunks') + self.db.drop_collection('mongoengine.counters') def test_default_values_nothing_set(self): """Ensure that default field values are used when creating a document. @@ -2954,6 +2955,57 @@ class Post(Document): self.assertEqual(1, post.comments[0].id) self.assertEqual(2, post.comments[1].id) + def test_inherited_sequencefield(self): + class Base(Document): + name = StringField() + counter = SequenceField() + meta = {'abstract': True} + + class Foo(Base): + pass + + class Bar(Base): + pass + + bar = Bar(name='Bar') + bar.save() + + foo = Foo(name='Foo') + foo.save() + + self.assertTrue('base.counter' in + self.db['mongoengine.counters'].find().distinct('_id')) + self.assertFalse(('foo.counter' or 'bar.counter') in + self.db['mongoengine.counters'].find().distinct('_id')) + self.assertNotEqual(foo.counter, bar.counter) + self.assertEqual(foo._fields['counter'].owner_document, Base) + self.assertEqual(bar._fields['counter'].owner_document, Base) + + def test_no_inherited_sequencefield(self): + class Base(Document): + name = StringField() + meta = {'abstract': True} + + class Foo(Base): + counter = SequenceField() + + class Bar(Base): + counter = SequenceField() + + bar = Bar(name='Bar') + bar.save() + + foo = Foo(name='Foo') + foo.save() + + self.assertFalse('base.counter' in + self.db['mongoengine.counters'].find().distinct('_id')) + self.assertTrue(('foo.counter' and 'bar.counter') in + self.db['mongoengine.counters'].find().distinct('_id')) + self.assertEqual(foo.counter, bar.counter) + self.assertEqual(foo._fields['counter'].owner_document, Foo) + self.assertEqual(bar._fields['counter'].owner_document, Bar) + def test_generic_embedded_document(self): class Car(EmbeddedDocument): name = StringField() From eec876295d1e3feb5fd2e360b3b775a15ab2b14a Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Fri, 12 Jun 2015 12:13:28 +0200 Subject: [PATCH 0199/1519] Added passing test to prove save and only problem was fixed --- tests/queryset/queryset.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 83fb52df3..981d47f46 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -693,6 +693,43 @@ def test_set_on_insert(self): self.assertEqual("Bob", bob.name) self.assertEqual(30, bob.age) + def test_save_and_only_on_fields_with_default(self): + class Embed(EmbeddedDocument): + field = IntField() + + class B(Document): + meta = {'collection': 'b'} + + field = IntField(default=1) + embed = EmbeddedDocumentField(Embed, default=Embed) + embed_no_default = EmbeddedDocumentField(Embed) + + # Creating {field : 2, embed : {field: 2}, embed_no_default: {field: 2}} + val = 2 + embed = Embed() + embed.field = val + record = B() + record.field = val + record.embed = embed + record.embed_no_default = embed + record.save() + + # Checking it was saved correctly + record.reload() + self.assertEqual(record.field, 2) + self.assertEqual(record.embed_no_default.field, 2) + self.assertEqual(record.embed.field, 2) + + # Request only the _id field and save + clone = B.objects().only('id').first() + clone.save() + + # Reload the record and see that the embed data is not lost + record.reload() + self.assertEqual(record.field, 2) + self.assertEqual(record.embed_no_default.field, 2) + self.assertEqual(record.embed.field, 2) + def test_get_or_create(self): """Ensure that ``get_or_create`` returns one result or creates a new document. From 4c1496b4a4d380fa62ada223819ec3cd0d523bc8 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Fri, 12 Jun 2015 13:10:36 +0200 Subject: [PATCH 0200/1519] Updated URL and Email field regex validators, added schemes arg to urlfield --- mongoengine/fields.py | 23 ++++++++++++++++------- tests/fields/fields.py | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 9176828c6..b685534dc 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -119,22 +119,31 @@ class URLField(StringField): """ _URL_REGEX = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - # domain... - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'^(?:[a-z0-9\.\-]*)://' # scheme is validated separately + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(? Date: Fri, 12 Jun 2015 13:12:35 +0200 Subject: [PATCH 0201/1519] Added to changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index dfa9c7a60..aba3f6b26 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ Changes in 0.9.X - DEV - No_dereference() not respected on embedded docs containing reference. #517 - Document save raise an exception if save_condition fails #1005 - Fixes some internal _id handling issue. #961 +- Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652 Changes in 0.9.0 ================ From 7714cca5995a1f26e3e7d4bccbd2464591304fe4 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Fri, 12 Jun 2015 13:34:28 +0200 Subject: [PATCH 0202/1519] Removed get_or_create() method, deprecated since 0.8 --- docs/changelog.rst | 1 + docs/guide/querying.rst | 20 ++++----------- mongoengine/queryset/base.py | 48 ------------------------------------ tests/fields/fields.py | 4 +-- tests/queryset/queryset.py | 35 +------------------------- 5 files changed, 8 insertions(+), 100 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index aba3f6b26..e3d7c8dfc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,7 @@ Changes in 0.9.X - DEV - Document save raise an exception if save_condition fails #1005 - Fixes some internal _id handling issue. #961 - Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652 +- Removed get_or_create() deprecated since 0.8.0. #300 Changes in 0.9.0 ================ diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 9861ce564..1cde82cb7 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -263,21 +263,11 @@ no document matches the query, and if more than one document matched the query. These exceptions are merged into your document definitions eg: `MyDoc.DoesNotExist` -A variation of this method exists, -:meth:`~mongoengine.queryset.QuerySet.get_or_create`, that will create a new -document with the query arguments if no documents match the query. An -additional keyword argument, :attr:`defaults` may be provided, which will be -used as default values for the new document, in the case that it should need -to be created:: - - >>> a, created = User.objects.get_or_create(name='User A', defaults={'age': 30}) - >>> b, created = User.objects.get_or_create(name='User A', defaults={'age': 40}) - >>> a.name == b.name and a.age == b.age - True - -.. warning:: - :meth:`~mongoengine.queryset.QuerySet.get_or_create` method is deprecated - since :mod:`mongoengine` 0.8. +A variation of this method, get_or_create() existed, but it was unsafe. It +could not be made safe, because there are no transactions in mongoDB. Other +approaches should be investigated, to ensure you don't accidentally duplicate +data when using something similar to this method. Therefore it was deprecated +in 0.8 and removed in 0.10. Default Document queries ======================== diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 89eb9afa6..c8a30783f 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -258,54 +258,6 @@ def create(self, **kwargs): """ return self._document(**kwargs).save() - def get_or_create(self, write_concern=None, auto_save=True, - *q_objs, **query): - """Retrieve unique object or create, if it doesn't exist. Returns a - tuple of ``(object, created)``, where ``object`` is the retrieved or - created object and ``created`` is a boolean specifying whether a new - object was created. Raises - :class:`~mongoengine.queryset.MultipleObjectsReturned` or - `DocumentName.MultipleObjectsReturned` if multiple results are found. - A new document will be created if the document doesn't exists; a - dictionary of default values for the new document may be provided as a - 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 accidentally duplicate data when using this method. This is - now scheduled to be removed before 1.0 - - :param write_concern: optional extra keyword arguments used if we - have to create a new document. - Passes any write_concern onto :meth:`~mongoengine.Document.save` - - :param auto_save: if the object is to be saved automatically if - not found. - - .. deprecated:: 0.8 - .. versionchanged:: 0.6 - added `auto_save` - .. versionadded:: 0.3 - """ - msg = ("get_or_create is scheduled to be deprecated. The approach is " - "flawed without transactions. Upserts should be preferred.") - warnings.warn(msg, DeprecationWarning) - - defaults = query.get('defaults', {}) - if 'defaults' in query: - del query['defaults'] - - try: - doc = self.get(*q_objs, **query) - return doc, False - except self._document.DoesNotExist: - query.update(defaults) - doc = self._document(**query) - - if auto_save: - doc.save(write_concern=write_concern) - return doc, True - def first(self): """Retrieve the first object matching the query. """ diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 1a5c25619..35e3ca39a 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2152,9 +2152,7 @@ class Company(Document): obj = Product.objects(company=None).first() self.assertEqual(obj, me) - obj, created = Product.objects.get_or_create(company=None) - - self.assertEqual(created, False) + obj = Product.objects.get(company=None) self.assertEqual(obj, me) def test_reference_query_conversion(self): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 981d47f46..fbc8ce4a7 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -340,8 +340,7 @@ def test_update_write_concern(self): write_concern = {"fsync": True} - author, created = self.Person.objects.get_or_create( - name='Test User', write_concern=write_concern) + author = self.Person.objects.create(name='Test User') author.save(write_concern=write_concern) result = self.Person.objects.update( @@ -730,38 +729,6 @@ class B(Document): self.assertEqual(record.embed_no_default.field, 2) self.assertEqual(record.embed.field, 2) - def test_get_or_create(self): - """Ensure that ``get_or_create`` returns one result or creates a new - document. - """ - person1 = self.Person(name="User A", age=20) - person1.save() - person2 = self.Person(name="User B", age=30) - person2.save() - - # Retrieve the first person from the database - self.assertRaises(MultipleObjectsReturned, - self.Person.objects.get_or_create) - self.assertRaises(self.Person.MultipleObjectsReturned, - self.Person.objects.get_or_create) - - # Use a query to filter the people found to just person2 - person, created = self.Person.objects.get_or_create(age=30) - self.assertEqual(person.name, "User B") - self.assertEqual(created, False) - - person, created = self.Person.objects.get_or_create(age__lt=30) - self.assertEqual(person.name, "User A") - self.assertEqual(created, False) - - # Try retrieving when no objects exists - new doc should be created - kwargs = dict(age=50, defaults={'name': 'User C'}) - person, created = self.Person.objects.get_or_create(**kwargs) - self.assertEqual(created, True) - - person = self.Person.objects.get(age=50) - self.assertEqual(person.name, "User C") - def test_bulk_insert(self): """Ensure that bulk insert works """ From 2b647d24051f1eac5ae8d5dcf42010dea083b2d6 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Fri, 12 Jun 2015 21:20:59 +0200 Subject: [PATCH 0203/1519] Improved doc for SequenceField Related to issue #497 --- mongoengine/fields.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 8e6c9b07e..c27d02b24 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1013,6 +1013,7 @@ class CachedReferenceField(BaseField): """ A referencefield with cache fields to purpose pseudo-joins + .. versionadded:: 0.9 """ @@ -1683,12 +1684,21 @@ class SequenceField(BaseField): cluster of machines, it is easier to create an object ID than have global, uniformly increasing sequence numbers. + :param collection_name: Name of the counter collection (default 'mongoengine.counters') + :param sequence_name: Name of the sequence in the collection (default 'ClassName.counter') + :param value_decorator: Any callable to use as a counter (default int) + Use any callable as `value_decorator` to transform calculated counter into any value suitable for your needs, e.g. string or hexadecimal representation of the default integer counter value. - + + .. note:: + + In case the counter is defined in the abstract document, it will be + common to all inherited documents and the default sequence name will + be the class name of the abstract document. + .. versionadded:: 0.5 - .. versionchanged:: 0.8 added `value_decorator` """ From 2a3d3de0b2be6fbedf975fe99c6aaa721de8453f Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Mon, 15 Jun 2015 00:22:07 +0200 Subject: [PATCH 0204/1519] CappedCollection max_size normalized to multiple of 256 --- docs/changelog.rst | 1 + docs/guide/defining-documents.rst | 6 ++- mongoengine/document.py | 13 +++++-- tests/document/instance.py | 65 ++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e3d7c8dfc..48e8b9aac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,7 @@ Changes in 0.9.X - DEV - Fixes some internal _id handling issue. #961 - Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652 - Removed get_or_create() deprecated since 0.8.0. #300 +- Capped collection multiple of 256. #1011 Changes in 0.9.0 ================ diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index d1448158a..8f7382eeb 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -447,8 +447,10 @@ A :class:`~mongoengine.Document` may use a **Capped Collection** by specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta` dictionary. :attr:`max_documents` is the maximum number of documents that is allowed to be stored in the collection, and :attr:`max_size` is the maximum size of the -collection in bytes. If :attr:`max_size` is not specified and -:attr:`max_documents` is, :attr:`max_size` defaults to 10000000 bytes (10MB). +collection in bytes. :attr:`max_size` is rounded up to the next multiple of 256 +by MongoDB internally and mongoengine before. Use also a multiple of 256 to +avoid confusions. If :attr:`max_size` is not specified and +:attr:`max_documents` is, :attr:`max_size` defaults to 10485760 bytes (10MB). The following example shows a :class:`Log` document that will be limited to 1000 entries and 2MB of disk space:: diff --git a/mongoengine/document.py b/mongoengine/document.py index 885f7eeda..dee6cc10a 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -114,9 +114,11 @@ class Document(BaseDocument): specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta` dictionary. :attr:`max_documents` is the maximum number of documents that is allowed to be stored in the collection, and :attr:`max_size` is the - maximum size of the collection in bytes. If :attr:`max_size` is not + maximum size of the collection in bytes. :attr:`max_size` is rounded up + to the next multiple of 256 by MongoDB internally and mongoengine before. + Use also a multiple of 256 to avoid confusions. If :attr:`max_size` is not specified and :attr:`max_documents` is, :attr:`max_size` defaults to - 10000000 bytes (10MB). + 10485760 bytes (10MB). Indexes may be created by specifying :attr:`indexes` in the :attr:`meta` dictionary. The value should be a list of field names or tuples of field @@ -137,7 +139,7 @@ class Document(BaseDocument): By default, any extra attribute existing in stored data but not declared in your model will raise a :class:`~mongoengine.FieldDoesNotExist` error. This can be disabled by setting :attr:`strict` to ``False`` - in the :attr:`meta` dictionnary. + in the :attr:`meta` dictionary. """ # The __metaclass__ attribute is removed by 2to3 when running with Python3 @@ -176,8 +178,11 @@ def _get_collection(cls): # Create collection as a capped collection if specified if cls._meta.get('max_size') or cls._meta.get('max_documents'): # Get max document limit and max byte size from meta - max_size = cls._meta.get('max_size') or 10000000 # 10MB default + max_size = cls._meta.get('max_size') or 10 * 2 ** 20 # 10MB default max_documents = cls._meta.get('max_documents') + # Round up to next 256 bytes as MongoDB would do it to avoid exception + if max_size % 256: + max_size = (max_size / 256 + 1) * 256 if collection_name in db.collection_names(): cls._collection = db[collection_name] diff --git a/tests/document/instance.py b/tests/document/instance.py index e1710b9f2..fceec02f5 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -88,7 +88,7 @@ class Log(Document): options = Log.objects._collection.options() self.assertEqual(options['capped'], True) self.assertEqual(options['max'], 10) - self.assertTrue(options['size'] >= 4096) + self.assertEqual(options['size'], 4096) # Check that the document cannot be redefined with different options def recreate_log_document(): @@ -103,6 +103,69 @@ class Log(Document): Log.drop_collection() + def test_capped_collection_default(self): + """Ensure that capped collections defaults work properly. + """ + class Log(Document): + date = DateTimeField(default=datetime.now) + meta = { + 'max_documents': 10, + } + + Log.drop_collection() + + # Create a doc to create the collection + Log().save() + + options = Log.objects._collection.options() + self.assertEqual(options['capped'], True) + self.assertEqual(options['max'], 10) + self.assertEqual(options['size'], 10 * 2**20) + + # Check that the document with default value can be recreated + def recreate_log_document(): + class Log(Document): + date = DateTimeField(default=datetime.now) + meta = { + 'max_documents': 10, + } + # Create the collection by accessing Document.objects + Log.objects + recreate_log_document() + Log.drop_collection() + + def test_capped_collection_no_max_size_problems(self): + """Ensure that capped collections with odd max_size work properly. + MongoDB rounds up max_size to next multiple of 256, recreating a doc + with the same spec failed in mongoengine <0.10 + """ + class Log(Document): + date = DateTimeField(default=datetime.now) + meta = { + 'max_size': 10000, + } + + Log.drop_collection() + + # Create a doc to create the collection + Log().save() + + options = Log.objects._collection.options() + self.assertEqual(options['capped'], True) + self.assertTrue(options['size'] >= 10000) + + # Check that the document with odd max_size value can be recreated + def recreate_log_document(): + class Log(Document): + date = DateTimeField(default=datetime.now) + meta = { + 'max_size': 10000, + } + # Create the collection by accessing Document.objects + Log.objects + recreate_log_document() + Log.drop_collection() + def test_repr(self): """Ensure that unicode representation works """ From 1bcd675ead609b8b423d227b9266df752f3c409a Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Mon, 15 Jun 2015 13:44:11 +0200 Subject: [PATCH 0205/1519] Python 3 fix, uses floor division --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index dee6cc10a..429f60658 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -182,7 +182,7 @@ def _get_collection(cls): max_documents = cls._meta.get('max_documents') # Round up to next 256 bytes as MongoDB would do it to avoid exception if max_size % 256: - max_size = (max_size / 256 + 1) * 256 + max_size = (max_size // 256 + 1) * 256 if collection_name in db.collection_names(): cls._collection = db[collection_name] From dd095279c86d55f0497f751f8ca4cb56aeb36d55 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 Jun 2015 14:15:23 -0700 Subject: [PATCH 0206/1519] aggregate_sum/average + unit tests --- mongoengine/queryset/base.py | 34 ++++++++++++++++++ tests/queryset/queryset.py | 68 ++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c8a30783f..38389fbf2 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1248,6 +1248,23 @@ def sum(self, field): else: return 0 + def aggregate_sum(self, field): + """Sum over the values of the specified field. + + :param field: the field to sum over; use dot-notation to refer to + embedded document fields + + This method is more performant than the regular `sum`, because it uses + the aggregation framework instead of map-reduce. + """ + result = self._document._get_collection().aggregate([ + { '$match': self._query }, + { '$group': { '_id': 'sum', 'total': { '$sum': '$' + field } } } + ]) + if result['result']: + return result['result'][0]['total'] + return 0 + def average(self, field): """Average over the values of the specified field. @@ -1303,6 +1320,23 @@ def average(self, field): else: return 0 + def aggregate_average(self, field): + """Average over the values of the specified field. + + :param field: the field to average over; use dot-notation to refer to + embedded document fields + + This method is more performant than the regular `average`, because it + uses the aggregation framework instead of map-reduce. + """ + result = self._document._get_collection().aggregate([ + { '$match': self._query }, + { '$group': { '_id': 'avg', 'total': { '$avg': '$' + field } } } + ]) + if result['result']: + return result['result'][0]['total'] + return 0 + def item_frequencies(self, field, normalize=False, map_reduce=True): """Returns a dictionary of all items present in a field across the whole queried set of documents, and their corresponding frequency. diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index e7eb49018..d43486788 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2706,26 +2706,58 @@ def test_average(self): avg = float(sum(ages)) / (len(ages) + 1) # take into account the 0 self.assertAlmostEqual(int(self.Person.objects.average('age')), avg) + self.assertAlmostEqual( + int(self.Person.objects.aggregate_average('age')), avg + ) self.Person(name='ageless person').save() self.assertEqual(int(self.Person.objects.average('age')), avg) + self.assertEqual( + int(self.Person.objects.aggregate_average('age')), avg + ) # dot notation self.Person( name='person meta', person_meta=self.PersonMeta(weight=0)).save() self.assertAlmostEqual( int(self.Person.objects.average('person_meta.weight')), 0) + self.assertAlmostEqual( + int(self.Person.objects.aggregate_average('person_meta.weight')), + 0 + ) for i, weight in enumerate(ages): self.Person( name='test meta%i', person_meta=self.PersonMeta(weight=weight)).save() self.assertAlmostEqual( - int(self.Person.objects.average('person_meta.weight')), avg) + int(self.Person.objects.average('person_meta.weight')), avg + ) + self.assertAlmostEqual( + int(self.Person.objects.aggregate_average('person_meta.weight')), + avg + ) self.Person(name='test meta none').save() self.assertEqual( - int(self.Person.objects.average('person_meta.weight')), avg) + int(self.Person.objects.average('person_meta.weight')), avg + ) + self.assertEqual( + int(self.Person.objects.aggregate_average('person_meta.weight')), + avg + ) + + # test summing over a filtered queryset + over_50 = [a for a in ages if a >= 50] + avg = float(sum(over_50)) / len(over_50) + self.assertEqual( + self.Person.objects.filter(age__gte=50).average('age'), + avg + ) + self.assertEqual( + self.Person.objects.filter(age__gte=50).aggregate_average('age'), + avg + ) def test_sum(self): """Ensure that field can be summed over correctly. @@ -2734,20 +2766,44 @@ def test_sum(self): for i, age in enumerate(ages): self.Person(name='test%s' % i, age=age).save() - self.assertEqual(int(self.Person.objects.sum('age')), sum(ages)) + self.assertEqual(self.Person.objects.sum('age'), sum(ages)) + self.assertEqual( + self.Person.objects.aggregate_sum('age'), sum(ages) + ) self.Person(name='ageless person').save() - self.assertEqual(int(self.Person.objects.sum('age')), sum(ages)) + self.assertEqual(self.Person.objects.sum('age'), sum(ages)) + self.assertEqual( + self.Person.objects.aggregate_sum('age'), sum(ages) + ) for i, age in enumerate(ages): self.Person(name='test meta%s' % i, person_meta=self.PersonMeta(weight=age)).save() self.assertEqual( - int(self.Person.objects.sum('person_meta.weight')), sum(ages)) + self.Person.objects.sum('person_meta.weight'), sum(ages) + ) + self.assertEqual( + self.Person.objects.aggregate_sum('person_meta.weight'), + sum(ages) + ) self.Person(name='weightless person').save() - self.assertEqual(int(self.Person.objects.sum('age')), sum(ages)) + self.assertEqual(self.Person.objects.sum('age'), sum(ages)) + self.assertEqual( + self.Person.objects.aggregate_sum('age'), sum(ages) + ) + + # test summing over a filtered queryset + self.assertEqual( + self.Person.objects.filter(age__gte=50).sum('age'), + sum([a for a in ages if a >= 50]) + ) + self.assertEqual( + self.Person.objects.filter(age__gte=50).aggregate_sum('age'), + sum([a for a in ages if a >= 50]) + ) def test_embedded_average(self): class Pay(EmbeddedDocument): From 12337802657d7e6d03a1fd19e7f412d54b3ef05d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 8 Jun 2015 13:46:19 -0700 Subject: [PATCH 0207/1519] make aggregate_sum/average compatible with pymongo 3.x --- mongoengine/queryset/base.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 38389fbf2..b949e1219 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1261,8 +1261,13 @@ def aggregate_sum(self, field): { '$match': self._query }, { '$group': { '_id': 'sum', 'total': { '$sum': '$' + field } } } ]) - if result['result']: - return result['result'][0]['total'] + if IS_PYMONGO_3: + result = list(result) + if result: + return result[0]['total'] + else: + if result['result']: + return result['result'][0]['total'] return 0 def average(self, field): @@ -1333,8 +1338,13 @@ def aggregate_average(self, field): { '$match': self._query }, { '$group': { '_id': 'avg', 'total': { '$avg': '$' + field } } } ]) - if result['result']: - return result['result'][0]['total'] + if IS_PYMONGO_3: + result = list(result) + if result: + return result[0]['total'] + else: + if result['result']: + return result['result'][0]['total'] return 0 def item_frequencies(self, field, normalize=False, map_reduce=True): From b7ef82cb67d11787f7b305028690c542ad048301 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 17 Jun 2015 17:05:10 -0700 Subject: [PATCH 0208/1519] style tweaks + changelog entry --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 48e8b9aac..b9ad5b0e9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,7 @@ Changes in 0.9.X - DEV - Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652 - Removed get_or_create() deprecated since 0.8.0. #300 - Capped collection multiple of 256. #1011 +- Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods. Changes in 0.9.0 ================ diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index b949e1219..c3abd46a8 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1263,11 +1263,10 @@ def aggregate_sum(self, field): ]) if IS_PYMONGO_3: result = list(result) - if result: - return result[0]['total'] else: - if result['result']: - return result['result'][0]['total'] + result = result.get('result') + if result: + return result[0]['total'] return 0 def average(self, field): @@ -1340,11 +1339,10 @@ def aggregate_average(self, field): ]) if IS_PYMONGO_3: result = list(result) - if result: - return result[0]['total'] else: - if result['result']: - return result['result'][0]['total'] + result = result.get('result') + if result: + return result[0]['total'] return 0 def item_frequencies(self, field, normalize=False, map_reduce=True): From dfc7f35ef1717049a2d26d1bf5a9754f016ae4de Mon Sep 17 00:00:00 2001 From: "Breeze.kay" Date: Fri, 19 Jun 2015 15:40:05 +0800 Subject: [PATCH 0209/1519] add testcase and changelog for pull:#1020 'improve _created status when switch collection and db' --- AUTHORS | 2 ++ docs/changelog.rst | 1 + mongoengine/document.py | 8 +++++ tests/test_signals.py | 73 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/AUTHORS b/AUTHORS index f64093d81..42082278a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -224,3 +224,5 @@ that much better: * Matthieu Rigal (https://github.com/MRigal) * Charanpal Dhanjal (https://github.com/charanpald) * Emmanuel Leblond (https://github.com/touilleMan) + * Breeze.Kay (https://github.com/9nix00) + diff --git a/docs/changelog.rst b/docs/changelog.rst index b9ad5b0e9..76e682546 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- improve Document._created status when switch collection and db #1020 - Queryset update doesn't go through field validation #453 - Added support for specifying authentication source as option `authSource` in URI. #967 - Fixed mark_as_changed to handle higher/lower level fields changed. #927 diff --git a/mongoengine/document.py b/mongoengine/document.py index 654d7a4ca..dec721149 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -503,6 +503,10 @@ def switch_db(self, db_alias, keep_created=True): :param str db_alias: The database alias to use for saving the document + :param bool keep_created: keep self._created value after call `swith_db()` when True, + else will always set self._created value to True + + .. seealso:: Use :class:`~mongoengine.context_managers.switch_collection` if you need to read from another collection @@ -531,6 +535,10 @@ def switch_collection(self, collection_name, keep_created=True): :param str collection_name: The database alias to use for saving the document + :param bool keep_created: keep self._created value after call `swith_db()` when True, + else will always set self._created value to True + + .. seealso:: Use :class:`~mongoengine.context_managers.switch_db` if you need to read from another database diff --git a/tests/test_signals.py b/tests/test_signals.py index 6ab061d1d..8165e255f 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -279,5 +279,78 @@ def test_signals_with_explicit_doc_ids(self): # second time, it must be an update self.assertEqual(self.get_signal_output(ei.save), ['Is updated']) + def test_signals_with_switch_collection(self): + ei = self.ExplicitId(id=123) + ei.switch_collection("explicit__1") + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + ei.switch_collection("explicit__1") + self.assertEqual(self.get_signal_output(ei.save), ['Is updated']) + + ei.switch_collection("explicit__1", keep_created=False) + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + ei.switch_collection("explicit__1", keep_created=False) + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + + def test_signals_with_switch_db(self): + connect('mongoenginetest') + register_connection('testdb-1', 'mongoenginetest2') + + ei = self.ExplicitId(id=123) + ei.switch_db("testdb-1") + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + ei.switch_db("testdb-1") + self.assertEqual(self.get_signal_output(ei.save), ['Is updated']) + + ei.switch_db("testdb-1", keep_created=False) + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + ei.switch_db("testdb-1", keep_created=False) + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + + def test_signals_with_switch_sharding_db(self): + + import pymongo + from mongoengine.connection import get_connection + + connect('mongoenginetest', alias='testdb1') + expected_connection = get_connection('testdb1') + + connect('mongoenginetest', alias='testdb2') + actual_connection = get_connection('testdb2') + + if pymongo.version_tuple[0] < 3: + IS_PYMONGO_3 = False + else: + IS_PYMONGO_3 = True + + ei = self.ExplicitId(id=123) + ei.switch_db("testdb1") + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + ei.switch_db("testdb1") + self.assertEqual(self.get_signal_output(ei.save), ['Is updated']) + + ei.switch_db("testdb2", keep_created=False) + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + ei.switch_db("testdb2", keep_created=False) + self.assertEqual(self.get_signal_output(ei.save), ['Is created']) + + # Handle PyMongo 3+ Async Connection + if IS_PYMONGO_3: + # Ensure we are connected, throws ServerSelectionTimeoutError otherwise. + # Purposely not catching exception to fail test if thrown. + expected_connection.server_info() + + self.assertEqual(expected_connection, actual_connection) + + + + + + + + + + + + if __name__ == '__main__': unittest.main() From 41bff0b2932c45f9e5d0343a2c1328d7a0f1cea4 Mon Sep 17 00:00:00 2001 From: "Breeze.kay" Date: Sun, 21 Jun 2015 09:32:31 +0800 Subject: [PATCH 0210/1519] remove testcase:test_signals_with_switch_sharding_db() and fix code style error for pull#1020 --- docs/changelog.rst | 2 +- mongoengine/document.py | 6 ++---- tests/test_signals.py | 45 ----------------------------------------- 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 76e682546..55ff77542 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog Changes in 0.9.X - DEV ====================== -- improve Document._created status when switch collection and db #1020 +- Improve Document._created status when switch collection and db #1020 - Queryset update doesn't go through field validation #453 - Added support for specifying authentication source as option `authSource` in URI. #967 - Fixed mark_as_changed to handle higher/lower level fields changed. #927 diff --git a/mongoengine/document.py b/mongoengine/document.py index dec721149..0ceedfc19 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -503,8 +503,7 @@ def switch_db(self, db_alias, keep_created=True): :param str db_alias: The database alias to use for saving the document - :param bool keep_created: keep self._created value after call `swith_db()` when True, - else will always set self._created value to True + :param bool keep_created: keep self._created value after switching db, else is reset to True .. seealso:: @@ -535,8 +534,7 @@ def switch_collection(self, collection_name, keep_created=True): :param str collection_name: The database alias to use for saving the document - :param bool keep_created: keep self._created value after call `swith_db()` when True, - else will always set self._created value to True + :param bool keep_created: keep self._created value after switching collection, else is reset to True .. seealso:: diff --git a/tests/test_signals.py b/tests/test_signals.py index 8165e255f..8672925c9 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -306,51 +306,6 @@ def test_signals_with_switch_db(self): ei.switch_db("testdb-1", keep_created=False) self.assertEqual(self.get_signal_output(ei.save), ['Is created']) - def test_signals_with_switch_sharding_db(self): - - import pymongo - from mongoengine.connection import get_connection - - connect('mongoenginetest', alias='testdb1') - expected_connection = get_connection('testdb1') - - connect('mongoenginetest', alias='testdb2') - actual_connection = get_connection('testdb2') - - if pymongo.version_tuple[0] < 3: - IS_PYMONGO_3 = False - else: - IS_PYMONGO_3 = True - - ei = self.ExplicitId(id=123) - ei.switch_db("testdb1") - self.assertEqual(self.get_signal_output(ei.save), ['Is created']) - ei.switch_db("testdb1") - self.assertEqual(self.get_signal_output(ei.save), ['Is updated']) - - ei.switch_db("testdb2", keep_created=False) - self.assertEqual(self.get_signal_output(ei.save), ['Is created']) - ei.switch_db("testdb2", keep_created=False) - self.assertEqual(self.get_signal_output(ei.save), ['Is created']) - - # Handle PyMongo 3+ Async Connection - if IS_PYMONGO_3: - # Ensure we are connected, throws ServerSelectionTimeoutError otherwise. - # Purposely not catching exception to fail test if thrown. - expected_connection.server_info() - - self.assertEqual(expected_connection, actual_connection) - - - - - - - - - - - if __name__ == '__main__': unittest.main() From bcc4d4e8c6820045b2fc7e58f747da78ae4dd5fd Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 03:40:45 +0200 Subject: [PATCH 0211/1519] Added test and fix for delete with write_concern w:0 --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 3 ++- tests/queryset/queryset.py | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b9ad5b0e9..f9f68f502 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,6 +26,7 @@ Changes in 0.9.X - DEV - Removed get_or_create() deprecated since 0.8.0. #300 - Capped collection multiple of 256. #1011 - Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods. +- Fix for delete with write_concern {'w': 0}. #1008 Changes in 0.9.0 ================ diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c3abd46a8..99d960444 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -418,7 +418,8 @@ def delete(self, write_concern=None, _from_doc_delete=False): **{'pull_all__%s' % field_name: self}) result = queryset._collection.remove(queryset._query, **write_concern) - return result["n"] + if result: + return result.get("n") def update(self, upsert=False, multi=True, write_concern=None, full_result=False, **update): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d43486788..7b0902381 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1634,6 +1634,11 @@ class BlogPost(Document): self.Person.objects()[:1].delete() self.assertEqual(1, BlogPost.objects.count()) + def test_limit_with_write_concern_0(self): + + p1 = self.Person(name="User Z", age=20).save() + p1.delete(w=0) + def test_reference_field_find(self): """Ensure cascading deletion of referring documents from the database. """ From d0b749a43c856b4103f0dea0974b30e74b459f76 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 13:02:59 +0200 Subject: [PATCH 0212/1519] Made test explicit with an assert --- tests/queryset/queryset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7b0902381..aaa14ca0d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1637,7 +1637,8 @@ class BlogPost(Document): def test_limit_with_write_concern_0(self): p1 = self.Person(name="User Z", age=20).save() - p1.delete(w=0) + del_result = p1.delete(w=0) + self.assertEqual(None, del_result) def test_reference_field_find(self): """Ensure cascading deletion of referring documents from the database. From 49c0c9f44ca0fb01ed14a3ff390f6666b9303385 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 01:37:25 +0200 Subject: [PATCH 0213/1519] Simplified lookup-field method, allowing dynamic lookup for more than two parts --- mongoengine/base/document.py | 20 ++++++-------------- tests/document/dynamic.py | 9 +++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 1049eabdc..73939fa99 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -935,6 +935,7 @@ def _lookup_field(cls, parts): """ ListField = _import_class("ListField") + DynamicField = _import_class('DynamicField') if not isinstance(parts, (list, tuple)): parts = [parts] @@ -944,7 +945,6 @@ def _lookup_field(cls, parts): for field_name in parts: # Handle ListField indexing: if field_name.isdigit() and isinstance(field, ListField): - new_field = field.field fields.append(field_name) continue @@ -956,11 +956,9 @@ def _lookup_field(cls, parts): if field_name in cls._fields: field = cls._fields[field_name] elif cls._dynamic: - DynamicField = _import_class('DynamicField') field = DynamicField(db_field=field_name) elif cls._meta.get("allow_inheritance", False) or cls._meta.get("abstract", False): # 744: in case the field is defined in a subclass - field = None for subcls in cls.__subclasses__(): try: field = subcls._lookup_field([field_name])[0] @@ -982,6 +980,9 @@ def _lookup_field(cls, parts): '__'.join(parts)) if hasattr(getattr(field, 'field', None), 'lookup_member'): new_field = field.field.lookup_member(field_name) + elif cls._dynamic and (isinstance(field, DynamicField) or + getattr(getattr(field, 'document_type'), '_dynamic')): + new_field = DynamicField(db_field=field_name) else: # Look up subfield on the previous field or raise try: @@ -991,17 +992,8 @@ def _lookup_field(cls, parts): 'on the field {}'.format( field_name, field.name)) if not new_field and isinstance(field, ComplexBaseField): - if hasattr(field.field, 'document_type') and cls._dynamic \ - and field.field.document_type._dynamic: - DynamicField = _import_class('DynamicField') - new_field = DynamicField(db_field=field_name) - else: - fields.append(field_name) - continue - elif not new_field and hasattr(field, 'document_type') and cls._dynamic \ - and field.document_type._dynamic: - DynamicField = _import_class('DynamicField') - new_field = DynamicField(db_field=field_name) + fields.append(field_name) + continue elif not new_field: raise LookUpError('Cannot resolve field "%s"' % field_name) diff --git a/tests/document/dynamic.py b/tests/document/dynamic.py index 52136efca..b2947df46 100644 --- a/tests/document/dynamic.py +++ b/tests/document/dynamic.py @@ -129,6 +129,15 @@ def test_complex_data_lookups(self): self.assertEqual(1, self.Person.objects(misc__hello='world').count()) + def test_three_level_complex_data_lookups(self): + """Ensure you can query three level document dynamic fields""" + p = self.Person() + p.misc = {'hello': {'hello2': 'world'}} + p.save() + # from pprint import pprint as pp; import pdb; pdb.set_trace(); + print self.Person.objects(misc__hello__hello2='world') + self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count()) + def test_complex_embedded_document_validation(self): """Ensure embedded dynamic documents may be validated""" class Embedded(DynamicEmbeddedDocument): From 45dc379d9a3a7e4600d30435b91fb6dcab9b2af3 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 01:39:10 +0200 Subject: [PATCH 0214/1519] Added to changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6bff946dc..0dcd1e081 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,7 @@ Changes in 0.9.X - DEV - Capped collection multiple of 256. #1011 - Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods. - Fix for delete with write_concern {'w': 0}. #1008 +- Allow dynamic lookup for more than two parts. #882 Changes in 0.9.0 ================ From 576629f825587029059f831957142c63df525f9d Mon Sep 17 00:00:00 2001 From: lihorne Date: Wed, 28 Jan 2015 08:09:23 -0500 Subject: [PATCH 0215/1519] Added support for $minDistance query --- mongoengine/queryset/transform.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index f6cfa87e5..3171681b1 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -15,7 +15,7 @@ 'all', 'size', 'exists', 'not', 'elemMatch', 'type') GEO_OPERATORS = ('within_distance', 'within_spherical_distance', 'within_box', 'within_polygon', 'near', 'near_sphere', - 'max_distance', 'geo_within', 'geo_within_box', + 'max_distance', 'min_distance', 'geo_within', 'geo_within_box', 'geo_within_polygon', 'geo_within_center', 'geo_within_sphere', 'geo_intersects') STRING_OPERATORS = ('contains', 'icontains', 'startswith', @@ -128,21 +128,29 @@ def query(_doc_cls=None, _field_operation=False, **query): mongo_query[key].update(value) # $maxDistance needs to come last - convert to SON value_dict = mongo_query[key] - if '$maxDistance' in value_dict and '$near' in value_dict: + if ('$maxDistance' in value_dict and '$near' in value_dict): value_son = SON() if isinstance(value_dict['$near'], dict): for k, v in value_dict.iteritems(): - if k == '$maxDistance': + if k == '$maxDistance' or k == '$minDistance': continue value_son[k] = v - value_son['$near'] = SON(value_son['$near']) - value_son['$near']['$maxDistance'] = value_dict['$maxDistance'] + if (get_connection().max_wire_version <= 1): + value_son['$maxDistance'] = value_dict[ + '$maxDistance'] + else: + value_son['$near'] = SON(value_son['$near']) + value_son['$near'][ + '$maxDistance'] = value_dict['$maxDistance'] else: for k, v in value_dict.iteritems(): - if k == '$maxDistance': + if k == '$maxDistance' or k == '$minDistance': continue value_son[k] = v - value_son['$maxDistance'] = value_dict['$maxDistance'] + if '$maxDistance' in value_dict: + value_son['$maxDistance'] = value_dict['$maxDistance'] + if '$minDistance' in value_dict: + value_son['$minDistance'] = value_dict['$minDistance'] mongo_query[key] = value_son else: @@ -312,6 +320,8 @@ def _geo_operator(field, op, value): value = {'$within': {'$box': value}} elif op == "max_distance": value = {'$maxDistance': value} + elif op == "min_distance": + value = {'$minDistance': value} else: raise NotImplementedError("Geo method '%s' has not " "been implemented for a GeoPointField" % op) @@ -332,6 +342,8 @@ def _geo_operator(field, op, value): value = {'$near': _infer_geometry(value)} elif op == "max_distance": value = {'$maxDistance': value} + elif op == "min_distance": + value = {'$minDistance': value} else: raise NotImplementedError("Geo method '%s' has not " "been implemented for a %s " % (op, field._name)) From 2d57dc056533c75fdd988698bea14db1de3c17e1 Mon Sep 17 00:00:00 2001 From: Liam Horne Date: Wed, 28 Jan 2015 08:16:31 -0500 Subject: [PATCH 0216/1519] Fixed an indentation mistake --- mongoengine/queryset/transform.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 3171681b1..637e1b4bb 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -147,10 +147,10 @@ def query(_doc_cls=None, _field_operation=False, **query): if k == '$maxDistance' or k == '$minDistance': continue value_son[k] = v - if '$maxDistance' in value_dict: - value_son['$maxDistance'] = value_dict['$maxDistance'] - if '$minDistance' in value_dict: - value_son['$minDistance'] = value_dict['$minDistance'] + if '$maxDistance' in value_dict: + value_son['$maxDistance'] = value_dict['$maxDistance'] + if '$minDistance' in value_dict: + value_son['$minDistance'] = value_dict['$minDistance'] mongo_query[key] = value_son else: From 5efabdcea39922c1723d7978c35bc603975c3f8d Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 03:03:50 +0200 Subject: [PATCH 0217/1519] Added tests, documentation and simplified code --- docs/changelog.rst | 1 + docs/guide/querying.rst | 6 ++-- mongoengine/queryset/transform.py | 50 +++++++++++++------------------ tests/queryset/geo.py | 31 ++++++++++++++++++- tests/queryset/queryset.py | 14 +++++++++ 5 files changed, 70 insertions(+), 32 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0dcd1e081..ac6837503 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,7 @@ Changes in 0.9.X - DEV - Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods. - Fix for delete with write_concern {'w': 0}. #1008 - Allow dynamic lookup for more than two parts. #882 +- Added support for min_distance on geo queries. #831 Changes in 0.9.0 ================ diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 1cde82cb7..688fc9e95 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -146,9 +146,10 @@ The following were added in MongoEngine 0.8 for loc.objects(point__near=[40, 5]) loc.objects(point__near={"type": "Point", "coordinates": [40, 5]}) - You can also set the maximum distance in meters as well:: + You can also set the maximum and/or the minimum distance in meters as well:: loc.objects(point__near=[40, 5], point__max_distance=1000) + loc.objects(point__near=[40, 5], point__min_distance=100) The older 2D indexes are still supported with the :class:`~mongoengine.fields.GeoPointField`: @@ -168,7 +169,8 @@ The older 2D indexes are still supported with the * ``max_distance`` -- can be added to your location queries to set a maximum distance. - +* ``min_distance`` -- can be added to your location queries to set a minimum + distance. Querying lists -------------- diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 637e1b4bb..919157030 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -6,7 +6,7 @@ from mongoengine.base.fields import UPDATE_OPERATORS from mongoengine.connection import get_connection from mongoengine.common import _import_class -from mongoengine.errors import InvalidQueryError +from mongoengine.errors import InvalidQueryError, LookUpError __all__ = ('query', 'update') @@ -44,8 +44,8 @@ def query(_doc_cls=None, _field_operation=False, **query): if len(parts) > 1 and parts[-1] in MATCH_OPERATORS: op = parts.pop() - # if user escape field name by __ - if len(parts) > 1 and parts[-1] == "": + #if user escape field name by __ + if len(parts) > 1 and parts[-1]=="": parts.pop() negate = False @@ -126,32 +126,28 @@ def query(_doc_cls=None, _field_operation=False, **query): elif key in mongo_query: if key in mongo_query and isinstance(mongo_query[key], dict): mongo_query[key].update(value) - # $maxDistance needs to come last - convert to SON + # $max/minDistance needs to come last - convert to SON value_dict = mongo_query[key] - if ('$maxDistance' in value_dict and '$near' in value_dict): + if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and '$near' in value_dict: value_son = SON() - if isinstance(value_dict['$near'], dict): - for k, v in value_dict.iteritems(): - if k == '$maxDistance' or k == '$minDistance': - continue - value_son[k] = v - if (get_connection().max_wire_version <= 1): - value_son['$maxDistance'] = value_dict[ - '$maxDistance'] - else: - value_son['$near'] = SON(value_son['$near']) + for k, v in value_dict.iteritems(): + if k == '$maxDistance' or k == '$minDistance': + continue + value_son[k] = v + if isinstance(value_dict['$near'], dict) and\ + get_connection().max_wire_version > 1: + value_son['$near'] = SON(value_son['$near']) + if '$maxDistance' in value_dict: value_son['$near'][ '$maxDistance'] = value_dict['$maxDistance'] + if '$minDistance' in value_dict: + value_son['$near'][ + '$minDistance'] = value_dict['$minDistance'] else: - for k, v in value_dict.iteritems(): - if k == '$maxDistance' or k == '$minDistance': - continue - value_son[k] = v if '$maxDistance' in value_dict: value_son['$maxDistance'] = value_dict['$maxDistance'] if '$minDistance' in value_dict: value_son['$minDistance'] = value_dict['$minDistance'] - mongo_query[key] = value_son else: # Store for manually merging later @@ -305,7 +301,11 @@ def update(_doc_cls=None, **update): def _geo_operator(field, op, value): """Helper to return the query for a given geo query""" - if field._geo_index == pymongo.GEO2D: + if op == "max_distance": + value = {'$maxDistance': value} + elif op == "min_distance": + value = {'$minDistance': value} + elif field._geo_index == pymongo.GEO2D: if op == "within_distance": value = {'$within': {'$center': value}} elif op == "within_spherical_distance": @@ -318,10 +318,6 @@ def _geo_operator(field, op, value): value = {'$nearSphere': value} elif op == 'within_box': value = {'$within': {'$box': value}} - elif op == "max_distance": - value = {'$maxDistance': value} - elif op == "min_distance": - value = {'$minDistance': value} else: raise NotImplementedError("Geo method '%s' has not " "been implemented for a GeoPointField" % op) @@ -340,10 +336,6 @@ def _geo_operator(field, op, value): value = {"$geoIntersects": _infer_geometry(value)} elif op == "near": value = {'$near': _infer_geometry(value)} - elif op == "max_distance": - value = {'$maxDistance': value} - elif op == "min_distance": - value = {'$minDistance': value} else: raise NotImplementedError("Geo method '%s' has not " "been implemented for a %s " % (op, field._name)) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 12e96a043..889dac38d 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -70,6 +70,11 @@ def __unicode__(self): self.assertEqual(events.count(), 1) self.assertEqual(events[0], event2) + # find events at least 10 degrees away of san francisco + point = [-122.415579, 37.7566023] + events = Event.objects(location__near=point, location__min_distance=10) + self.assertEqual(events.count(), 2) + # find events within 10 degrees of san francisco point_and_distance = [[-122.415579, 37.7566023], 10] events = Event.objects(location__within_distance=point_and_distance) @@ -171,7 +176,7 @@ class Point(Document): # Same behavior for _within_spherical_distance points = Point.objects( - location__within_spherical_distance=[[-122, 37.5], 60/earth_radius] + location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius] ) self.assertEqual(points.count(), 2) @@ -186,6 +191,16 @@ class Point(Document): self.assertEqual(points.count(), 2) + # Test query works with max_distance being farer from one point + points = Point.objects(location__near_sphere=[-122, 37.8], + location__min_distance=60 / earth_radius) + self.assertEqual(points.count(), 1) + + # Test query works with min_distance being farer from one point + points = Point.objects(location__near_sphere=[-122, 37.8], + location__min_distance=60 / earth_radius) + self.assertEqual(points.count(), 1) + # Finds both points, but orders the north point first because it's # closer to the reference point to the north. points = Point.objects(location__near_sphere=[-122, 38.5]) @@ -268,6 +283,20 @@ def __unicode__(self): self.assertEqual(events.count(), 2) self.assertEqual(events[0], event3) + # ensure min_distance and max_distance combine well + events = Event.objects(location__near=[-87.67892, 41.9120459], + location__min_distance=1000, + location__max_distance=10000).order_by("-date") + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event3) + + # ensure ordering is respected by "near" + events = Event.objects(location__near=[-87.67892, 41.9120459], + # location__min_distance=10000 + location__min_distance=10000).order_by("-date") + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + # check that within_box works box = [(-125.0, 35.0), (-100.0, 40.0)] events = Event.objects(location__geo_within_box=box) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index aaa14ca0d..4f00e1c6a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4763,5 +4763,19 @@ class Person(Document): for p in Person.objects(): self.assertEqual(p.name, 'a') + def test_last_field_name_like_operator(self): + class EmbeddedItem(EmbeddedDocument): + type = StringField() + + class Doc(Document): + item = EmbeddedDocumentField(EmbeddedItem) + + Doc.drop_collection() + + doc = Doc(item=EmbeddedItem(type="axe")) + doc.save() + + self.assertEqual(1, Doc.objects(item__type__="axe").count()) + if __name__ == '__main__': unittest.main() From d96fcdb35cfcf083997c07a8f92afa022abf95df Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 03:24:05 +0200 Subject: [PATCH 0218/1519] Fixed problem of ordering when using near_sphere operator --- mongoengine/queryset/transform.py | 3 ++- tests/queryset/geo.py | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 919157030..7ede82129 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -128,7 +128,8 @@ def query(_doc_cls=None, _field_operation=False, **query): mongo_query[key].update(value) # $max/minDistance needs to come last - convert to SON value_dict = mongo_query[key] - if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and '$near' in value_dict: + if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and \ + ('$near' in value_dict or '$nearSphere' in value_dict): value_son = SON() for k, v in value_dict.iteritems(): if k == '$maxDistance' or k == '$minDistance': diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 889dac38d..4a8001148 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -182,13 +182,6 @@ class Point(Document): points = Point.objects(location__near_sphere=[-122, 37.5], location__max_distance=60 / earth_radius) - # This test is sometimes failing with Mongo internals non-sense. - # See https://travis-ci.org/MongoEngine/mongoengine/builds/58729101 - try: - points.count() - except OperationFailure: - raise SkipTest("Sometimes MongoDB ignores its capacities on maxDistance") - self.assertEqual(points.count(), 2) # Test query works with max_distance being farer from one point From 95165aa92f817f23b287925b02a1756a94085208 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 13:29:54 +0200 Subject: [PATCH 0219/1519] Logic and test adaptations for MongoDB < 3 --- mongoengine/queryset/transform.py | 24 ++++++++++++++---------- tests/queryset/geo.py | 7 ++++++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 7ede82129..dc1c2f585 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -135,16 +135,20 @@ def query(_doc_cls=None, _field_operation=False, **query): if k == '$maxDistance' or k == '$minDistance': continue value_son[k] = v - if isinstance(value_dict['$near'], dict) and\ - get_connection().max_wire_version > 1: - value_son['$near'] = SON(value_son['$near']) - if '$maxDistance' in value_dict: - value_son['$near'][ - '$maxDistance'] = value_dict['$maxDistance'] - if '$minDistance' in value_dict: - value_son['$near'][ - '$minDistance'] = value_dict['$minDistance'] - else: + # seems only required for 2.6= 1: + value_son[near_op] = SON(value_son[near_op]) + if '$maxDistance' in value_dict: + value_son[near_op][ + '$maxDistance'] = value_dict['$maxDistance'] + if '$minDistance' in value_dict: + value_son[near_op][ + '$minDistance'] = value_dict['$minDistance'] + near_embedded = True + if not near_embedded: if '$maxDistance' in value_dict: value_son['$maxDistance'] = value_dict['$maxDistance'] if '$minDistance' in value_dict: diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 4a8001148..2ab99dd80 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -73,7 +73,12 @@ def __unicode__(self): # find events at least 10 degrees away of san francisco point = [-122.415579, 37.7566023] events = Event.objects(location__near=point, location__min_distance=10) - self.assertEqual(events.count(), 2) + # The following real test passes on MongoDB 3 but minDistance seems + # buggy on older MongoDB versions + if get_connection().server_info()['versionArray'][0] > 2: + self.assertEqual(events.count(), 2) + else: + self.assertTrue(events.count() >= 2) # find events within 10 degrees of san francisco point_and_distance = [[-122.415579, 37.7566023], 10] From 40f6df7160c5f140ca40deed04d40386e7c0db27 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Sun, 21 Jun 2015 13:41:41 +0200 Subject: [PATCH 0220/1519] Adapted one more test for MongoDB < 3 --- mongoengine/queryset/transform.py | 2 +- tests/queryset/geo.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index dc1c2f585..4ff387690 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -6,7 +6,7 @@ from mongoengine.base.fields import UPDATE_OPERATORS from mongoengine.connection import get_connection from mongoengine.common import _import_class -from mongoengine.errors import InvalidQueryError, LookUpError +from mongoengine.errors import InvalidQueryError __all__ = ('query', 'update') diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 2ab99dd80..9aac44f53 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -189,15 +189,23 @@ class Point(Document): location__max_distance=60 / earth_radius) self.assertEqual(points.count(), 2) - # Test query works with max_distance being farer from one point + # Test query works with max_distance, being farer from one point points = Point.objects(location__near_sphere=[-122, 37.8], - location__min_distance=60 / earth_radius) + location__max_distance=60 / earth_radius) + close_point = points.first() self.assertEqual(points.count(), 1) - # Test query works with min_distance being farer from one point + # Test query works with min_distance, being farer from one point points = Point.objects(location__near_sphere=[-122, 37.8], location__min_distance=60 / earth_radius) - self.assertEqual(points.count(), 1) + # The following real test passes on MongoDB 3 but minDistance seems + # buggy on older MongoDB versions + if get_connection().server_info()['versionArray'][0] > 2: + self.assertEqual(points.count(), 1) + far_point = points.first() + self.assertNotEqual(close_point, far_point) + else: + self.assertTrue(points.count() >= 1) # Finds both points, but orders the north point first because it's # closer to the reference point to the north. From 9063b559c4683ff2607cf929b40e46d4f85205c6 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Mon, 22 Jun 2015 16:40:50 +0200 Subject: [PATCH 0221/1519] Fix for PyMongo3+ --- mongoengine/queryset/transform.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 4ff387690..e7f5988cd 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -7,6 +7,7 @@ from mongoengine.connection import get_connection from mongoengine.common import _import_class from mongoengine.errors import InvalidQueryError +from mongoengine.python_support import IS_PYMONGO_3 __all__ = ('query', 'update') @@ -135,11 +136,12 @@ def query(_doc_cls=None, _field_operation=False, **query): if k == '$maxDistance' or k == '$minDistance': continue value_son[k] = v - # seems only required for 2.6== 2.6, may fail when combining + # PyMongo 3+ and MongoDB < 2.6 near_embedded = False for near_op in ('$near', '$nearSphere'): - if isinstance(value_dict.get(near_op), dict) and \ - get_connection().max_wire_version > 1: + if isinstance(value_dict.get(near_op), dict) and ( + IS_PYMONGO_3 or get_connection().max_wire_version > 1): value_son[near_op] = SON(value_son[near_op]) if '$maxDistance' in value_dict: value_son[near_op][ From 5c807f3dc86264cd80b63d74974a387c473d27f7 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Mon, 22 Jun 2015 16:41:36 +0200 Subject: [PATCH 0222/1519] Various test adjustments to improve stability independantly of execution order --- tests/test_connection.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 4a02696af..e9477b79c 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -54,11 +54,10 @@ def test_connect(self): def test_sharing_connections(self): """Ensure that connections are shared when the connection settings are exactly the same """ - connect('mongoenginetest', alias='testdb1') - + connect('mongoenginetests', alias='testdb1') expected_connection = get_connection('testdb1') - connect('mongoenginetest', alias='testdb2') + connect('mongoenginetests', alias='testdb2') actual_connection = get_connection('testdb2') # Handle PyMongo 3+ Async Connection @@ -96,8 +95,7 @@ def test_connect_uri(self): c.mongoenginetest.system.users.remove({}) def test_connect_uri_without_db(self): - """Ensure that the connect() method works properly with uri's - without database_name + """Ensure connect() method works properly with uri's without database_name """ c = connect(db='mongoenginetest', alias='admin') c.admin.system.users.remove({}) @@ -130,28 +128,27 @@ def test_connect_uri_with_authsource(self): # Create users c = connect('mongoenginetest') c.admin.system.users.remove({}) - c.admin.add_user('username', 'password') + c.admin.add_user('username2', 'password') # Authentication fails without "authSource" if IS_PYMONGO_3: - test_conn = connect('mongoenginetest', alias='test2', - host='mongodb://username:password@localhost/mongoenginetest') + test_conn = connect('mongoenginetest', alias='test1', + host='mongodb://username2:password@localhost/mongoenginetest') self.assertRaises(OperationFailure, test_conn.server_info) else: self.assertRaises( ConnectionError, connect, 'mongoenginetest', alias='test1', - host='mongodb://username:password@localhost/mongoenginetest' + host='mongodb://username2:password@localhost/mongoenginetest' ) self.assertRaises(ConnectionError, get_db, 'test1') # Authentication succeeds with "authSource" test_conn2 = connect( 'mongoenginetest', alias='test2', - host=('mongodb://username:password@localhost/' + host=('mongodb://username2:password@localhost/' 'mongoenginetest?authSource=admin') ) # This will fail starting from MongoDB 2.6+ - # test_conn2.server_info() db = get_db('test2') self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'mongoenginetest') From 6bd9529a66181893c167a5f304fc8f86901eea36 Mon Sep 17 00:00:00 2001 From: Vicky Donchenko Date: Mon, 1 Jun 2015 17:57:40 +0300 Subject: [PATCH 0223/1519] Allow to add custom metadata to fields --- AUTHORS | 1 + docs/changelog.rst | 3 ++- mongoengine/base/fields.py | 4 +++- tests/fields/fields.py | 17 +++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 42082278a..c776a43ce 100644 --- a/AUTHORS +++ b/AUTHORS @@ -225,4 +225,5 @@ that much better: * Charanpal Dhanjal (https://github.com/charanpald) * Emmanuel Leblond (https://github.com/touilleMan) * Breeze.Kay (https://github.com/9nix00) + * Vicki Donchenko (https://github.com/kivistein) diff --git a/docs/changelog.rst b/docs/changelog.rst index ac6837503..3dfe82f66 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,7 +19,7 @@ Changes in 0.9.X - DEV - Fix for updating sorting in SortedListField. #978 - Added __ support to escape field name in fields lookup keywords that match operators names #949 - Support for PyMongo 3+ #946 -- Fix for issue where FileField deletion did not free space in GridFS. +- Fix for issue where FileField deletion did not free space in GridFS. - No_dereference() not respected on embedded docs containing reference. #517 - Document save raise an exception if save_condition fails #1005 - Fixes some internal _id handling issue. #961 @@ -30,6 +30,7 @@ Changes in 0.9.X - DEV - Fix for delete with write_concern {'w': 0}. #1008 - Allow dynamic lookup for more than two parts. #882 - Added support for min_distance on geo queries. #831 +- Allow to add custom metadata to fields #705 Changes in 0.9.0 ================ diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 5fae26952..d1f4434e8 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -44,7 +44,7 @@ class BaseField(object): def __init__(self, db_field=None, name=None, required=False, default=None, unique=False, unique_with=None, primary_key=False, validation=None, choices=None, verbose_name=None, - help_text=None, null=False, sparse=False): + help_text=None, null=False, sparse=False, custom_data=None): """ :param db_field: The database field to store this field in (defaults to the name of the field) @@ -71,6 +71,7 @@ def __init__(self, db_field=None, name=None, required=False, default=None, then the default value is set :param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False` means that uniqueness won't be enforced for `None` values + :param custom_data: (optional) Custom metadata for this field. """ self.db_field = (db_field or name) if not primary_key else '_id' @@ -89,6 +90,7 @@ def __init__(self, db_field=None, name=None, required=False, default=None, self.null = null self.sparse = sparse self._owner_document = None + self.custom_data = custom_data # Adjust the appropriate creation counter, and save our local copy. if self.db_field == '_id': diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 35e3ca39a..9b682fac8 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3818,5 +3818,22 @@ def test_filtered_delete(self): # deleted from the database self.assertEqual(number, 1) + def test_custom_data(self): + """ + Tests that custom data is saved in the field object + and doesn't interfere with the rest of field functionalities. + """ + custom_data = {'a': 'a_value', 'b': [1, 2]} + class CustomData(Document): + a_field = IntField() + c_field = IntField(custom_data=custom_data) + + a1 = CustomData(a_field=1, c_field=2).save() + self.assertEqual(2, a1.c_field) + self.assertFalse(hasattr(a1.c_field, 'custom_data')) + self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) + self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) + + if __name__ == '__main__': unittest.main() From 778c7dc5f2e22eef420c3b99fbcfef92d675c33e Mon Sep 17 00:00:00 2001 From: mrigal Date: Wed, 29 Apr 2015 18:59:27 +0200 Subject: [PATCH 0224/1519] general pep8 and more clean-up --- mongoengine/base/datastructures.py | 30 +++++++++++++---- mongoengine/base/document.py | 12 +++---- mongoengine/base/fields.py | 13 +++----- mongoengine/base/metaclasses.py | 14 +++----- mongoengine/connection.py | 3 +- mongoengine/dereference.py | 7 ++-- mongoengine/document.py | 30 ++++++++--------- mongoengine/errors.py | 3 +- mongoengine/fields.py | 53 +++++------------------------- mongoengine/python_support.py | 1 + mongoengine/queryset/base.py | 15 ++++----- mongoengine/queryset/field_list.py | 1 - mongoengine/queryset/queryset.py | 3 +- mongoengine/queryset/transform.py | 4 +-- mongoengine/signals.py | 2 ++ 15 files changed, 81 insertions(+), 110 deletions(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 91403de90..7ec67797d 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -1,5 +1,6 @@ import weakref import itertools + from mongoengine.common import _import_class from mongoengine.errors import DoesNotExist, MultipleObjectsReturned @@ -20,7 +21,7 @@ def __init__(self, dict_items, instance, name): if isinstance(instance, (Document, EmbeddedDocument)): self._instance = weakref.proxy(instance) self._name = name - return super(BaseDict, self).__init__(dict_items) + super(BaseDict, self).__init__(dict_items) def __getitem__(self, key, *args, **kwargs): value = super(BaseDict, self).__getitem__(key) @@ -65,7 +66,7 @@ def __setstate__(self, state): def clear(self, *args, **kwargs): self._mark_as_changed() - return super(BaseDict, self).clear(*args, **kwargs) + return super(BaseDict, self).clear() def pop(self, *args, **kwargs): self._mark_as_changed() @@ -73,7 +74,7 @@ def pop(self, *args, **kwargs): def popitem(self, *args, **kwargs): self._mark_as_changed() - return super(BaseDict, self).popitem(*args, **kwargs) + return super(BaseDict, self).popitem() def setdefault(self, *args, **kwargs): self._mark_as_changed() @@ -189,7 +190,7 @@ def remove(self, *args, **kwargs): def reverse(self, *args, **kwargs): self._mark_as_changed() - return super(BaseList, self).reverse(*args, **kwargs) + return super(BaseList, self).reverse() def sort(self, *args, **kwargs): self._mark_as_changed() @@ -368,25 +369,31 @@ class StrictDict(object): __slots__ = () _special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create']) _classes = {} + def __init__(self, **kwargs): - for k,v in kwargs.iteritems(): + for k, v in kwargs.iteritems(): setattr(self, k, v) + def __getitem__(self, key): key = '_reserved_' + key if key in self._special_fields else key try: return getattr(self, key) except AttributeError: raise KeyError(key) + def __setitem__(self, key, value): key = '_reserved_' + key if key in self._special_fields else key return setattr(self, key, value) + def __contains__(self, key): return hasattr(self, key) + def get(self, key, default=None): try: return self[key] except KeyError: return default + def pop(self, key, default=None): v = self.get(key, default) try: @@ -394,19 +401,26 @@ def pop(self, key, default=None): except AttributeError: pass return v + def iteritems(self): for key in self: yield key, self[key] + def items(self): return [(k, self[k]) for k in iter(self)] + def keys(self): return list(iter(self)) + def __iter__(self): return (key for key in self.__slots__ if hasattr(self, key)) + def __len__(self): return len(list(self.iteritems())) + def __eq__(self, other): return self.items() == other.items() + def __neq__(self, other): return self.items() != other.items() @@ -417,8 +431,10 @@ def create(cls, allowed_keys): if allowed_keys not in cls._classes: class SpecificStrictDict(cls): __slots__ = allowed_keys_tuple + def __repr__(self): - return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k,v) for (k,v) in self.iteritems()) + return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k, v) for (k, v) in self.iteritems()) + cls._classes[allowed_keys] = SpecificStrictDict return cls._classes[allowed_keys] @@ -426,6 +442,7 @@ def __repr__(self): class SemiStrictDict(StrictDict): __slots__ = ('_extras') _classes = {} + def __getattr__(self, attr): try: super(SemiStrictDict, self).__getattr__(attr) @@ -434,6 +451,7 @@ def __getattr__(self, attr): return self.__getattribute__('_extras')[attr] except KeyError as e: raise AttributeError(e) + def __setattr__(self, attr, value): try: super(SemiStrictDict, self).__setattr__(attr, value) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 73939fa99..747d3970a 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -14,7 +14,6 @@ from mongoengine.errors import (ValidationError, InvalidDocumentError, LookUpError, FieldDoesNotExist) from mongoengine.python_support import PY3, txt_type - from mongoengine.base.common import get_document, ALLOW_INHERITANCE from mongoengine.base.datastructures import ( BaseDict, @@ -420,7 +419,7 @@ def to_json(self, *args, **kwargs): :param use_db_field: Set to True by default but enables the output of the json structure with the field names and not the mongodb store db_names in case of set to False """ use_db_field = kwargs.pop('use_db_field', True) - return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) + return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) @classmethod def from_json(cls, json_data, created=False): @@ -570,7 +569,7 @@ def _get_changed_fields(self, inspected=None): continue elif (isinstance(data, (EmbeddedDocument, DynamicEmbeddedDocument)) and db_field_name not in changed_fields): - # Find all embedded fields that have been changed + # Find all embedded fields that have been changed changed = data._get_changed_fields(inspected) changed_fields += ["%s%s" % (key, k) for k in changed if k] elif (isinstance(data, (list, tuple, dict)) and @@ -621,7 +620,7 @@ def _delta(self): else: set_data = doc if '_id' in set_data: - del(set_data['_id']) + del set_data['_id'] # Determine if any changed items were actually unset. for path, value in set_data.items(): @@ -632,7 +631,7 @@ def _delta(self): default = None if (self._dynamic and len(parts) and parts[0] in self._dynamic_fields): - del(set_data[path]) + del set_data[path] unset_data[path] = 1 continue elif path in self._fields: @@ -666,7 +665,7 @@ def _delta(self): if default != value: continue - del(set_data[path]) + del set_data[path] unset_data[path] = 1 return set_data, unset_data @@ -821,7 +820,6 @@ def _build_index_spec(cls, spec): parts = key.split('.') if parts in (['pk'], ['id'], ['_id']): key = '_id' - fields = [] else: fields = cls._lookup_field(parts) parts = [] diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index d1f4434e8..02934d47c 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -7,7 +7,6 @@ from mongoengine.common import _import_class from mongoengine.errors import ValidationError - from mongoengine.base.common import ALLOW_INHERITANCE from mongoengine.base.datastructures import ( BaseDict, BaseList, EmbeddedDocumentList @@ -23,7 +22,6 @@ class BaseField(object): - """A base class for fields in a MongoDB document. Instances of this class may be added to subclasses of `Document` to define a document's schema. @@ -212,7 +210,6 @@ def owner_document(self, owner_document): class ComplexBaseField(BaseField): - """Handles complex fields, such as lists / dictionaries. Allows for nesting of embedded documents inside complex types. @@ -330,8 +327,8 @@ def to_mongo(self, value): return GenericReferenceField().to_mongo(value) cls = value.__class__ val = value.to_mongo() - # If we its a document thats not inherited add _cls - if (isinstance(value, EmbeddedDocument)): + # If it's a document that is not inherited add _cls + if isinstance(value, EmbeddedDocument): val['_cls'] = cls.__name__ return val @@ -370,8 +367,8 @@ def to_mongo(self, value): elif hasattr(v, 'to_mongo'): cls = v.__class__ val = v.to_mongo() - # If we its a document thats not inherited add _cls - if (isinstance(v, (Document, EmbeddedDocument))): + # If it's a document that is not inherited add _cls + if isinstance(v, (Document, EmbeddedDocument)): val['_cls'] = cls.__name__ value_dict[k] = val else: @@ -422,7 +419,6 @@ def _set_owner_document(self, owner_document): class ObjectIdField(BaseField): - """A field wrapper around MongoDB's ObjectIds. """ @@ -454,7 +450,6 @@ def validate(self, value): class GeoJsonBaseField(BaseField): - """A geo json field storing a geojson style object. .. versionadded:: 0.8 diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index d4c26bfe7..664c849ef 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -14,7 +14,6 @@ class DocumentMetaclass(type): - """Metaclass for all documents. """ @@ -144,7 +143,7 @@ def __new__(cls, name, bases, attrs): for base in document_bases: if _cls not in base._subclasses: base._subclasses += (_cls,) - base._types = base._subclasses # TODO depreciate _types + base._types = base._subclasses # TODO depreciate _types (Document, EmbeddedDocument, DictField, CachedReferenceField) = cls._import_classes() @@ -250,7 +249,6 @@ def _import_classes(cls): class TopLevelDocumentMetaclass(DocumentMetaclass): - """Metaclass for top-level documents (i.e. documents that have their own collection in the database. """ @@ -260,7 +258,7 @@ def __new__(cls, name, bases, attrs): super_new = super(TopLevelDocumentMetaclass, cls).__new__ # Set default _meta data if base class, otherwise get user defined meta - if (attrs.get('my_metaclass') == TopLevelDocumentMetaclass): + if attrs.get('my_metaclass') == TopLevelDocumentMetaclass: # defaults attrs['_meta'] = { 'abstract': True, @@ -279,7 +277,7 @@ def __new__(cls, name, bases, attrs): attrs['_meta'].update(attrs.get('meta', {})) else: attrs['_meta'] = attrs.get('meta', {}) - # Explictly set abstract to false unless set + # Explicitly set abstract to false unless set attrs['_meta']['abstract'] = attrs['_meta'].get('abstract', False) attrs['_is_base_cls'] = False @@ -294,7 +292,7 @@ def __new__(cls, name, bases, attrs): # Clean up top level meta if 'meta' in attrs: - del(attrs['meta']) + del attrs['meta'] # Find the parent document class parent_doc_cls = [b for b in flattened_bases @@ -307,7 +305,7 @@ def __new__(cls, name, bases, attrs): and not parent_doc_cls._meta.get('abstract', True)): msg = "Trying to set a collection on a subclass (%s)" % name warnings.warn(msg, SyntaxWarning) - del(attrs['_meta']['collection']) + del attrs['_meta']['collection'] # Ensure abstract documents have abstract bases if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'): @@ -425,7 +423,6 @@ def get_auto_id_names(self): class MetaDict(dict): - """Custom dictionary for meta classes. Handles the merging of set indexes """ @@ -440,6 +437,5 @@ def merge(self, new_options): class BasesTuple(tuple): - """Special class to handle introspection of bases tuple in __new__""" pass diff --git a/mongoengine/connection.py b/mongoengine/connection.py index b203e168b..cb1a731f7 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -120,7 +120,8 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): try: connection = None # check for shared connections - connection_settings_iterator = ((db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems()) + connection_settings_iterator = ( + (db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems()) for db_alias, connection_settings in connection_settings_iterator: connection_settings.pop('name', None) connection_settings.pop('username', None) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 8e8920d4a..4449acb68 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -11,7 +11,6 @@ class DeReference(object): - def __call__(self, items, max_depth=1, instance=None, name=None): """ Cheaply dereferences the items to a set depth. @@ -49,8 +48,8 @@ def __call__(self, items, max_depth=1, instance=None, name=None): if is_list and all([i.__class__ == doc_type for i in items]): return items - elif not is_list and all([i.__class__ == doc_type - for i in items.values()]): + elif not is_list and all( + [i.__class__ == doc_type for i in items.values()]): return items elif not field.dbref: if not hasattr(items, 'items'): @@ -155,7 +154,7 @@ def _fetch_objects(self, doc_type=None): elif doc_type is None: doc = get_document( ''.join(x.capitalize() - for x in collection.split('_')))._from_son(ref) + for x in collection.split('_')))._from_son(ref) else: doc = doc_type._from_son(ref) object_map[(collection, doc.id)] = doc diff --git a/mongoengine/document.py b/mongoengine/document.py index 0ceedfc19..07f85f167 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -3,6 +3,7 @@ import re from pymongo.read_preferences import ReadPreference +from bson import ObjectId from bson.dbref import DBRef from mongoengine import signals from mongoengine.common import _import_class @@ -46,7 +47,6 @@ class InvalidCollectionError(Exception): class EmbeddedDocument(BaseDocument): - """A :class:`~mongoengine.Document` that isn't stored in its own collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as fields on :class:`~mongoengine.Document`\ s through the @@ -89,7 +89,6 @@ def reload(self, *args, **kwargs): class Document(BaseDocument): - """The base class used for defining the structure and properties of collections of documents stored in MongoDB. Inherit from this class, and add fields as class attributes to define a document's structure. @@ -160,7 +159,9 @@ def fget(self): def fset(self, value): return setattr(self, self._meta['id_field'], value) + return property(fget, fset) + pk = pk() @classmethod @@ -190,7 +191,7 @@ def _get_collection(cls): # options match the specified capped options options = cls._collection.options() if options.get('max') != max_documents or \ - options.get('size') != max_size: + options.get('size') != max_size: msg = (('Cannot create collection "%s" as a capped ' 'collection as it already exists') % cls._collection) @@ -248,7 +249,7 @@ def modify(self, query={}, **update): return True def save(self, force_insert=False, validate=True, clean=True, - write_concern=None, cascade=None, cascade_kwargs=None, + write_concern=None, cascade=None, cascade_kwargs=None, _refs=None, save_condition=None, **kwargs): """Save the :class:`~mongoengine.Document` to the database. If the document already exists, it will be updated, otherwise it will be @@ -455,7 +456,7 @@ def update(self, **kwargs): if kwargs.get('upsert', False): query = self.to_mongo() if "_cls" in query: - del(query["_cls"]) + del query["_cls"] return self._qs.filter(**query).update_one(**kwargs) else: raise OperationError( @@ -580,8 +581,8 @@ def reload(self, *fields, **kwargs): if not self.pk: raise self.DoesNotExist("Document does not exist") obj = self._qs.read_preference(ReadPreference.PRIMARY).filter( - **self._object_key).only(*fields).limit(1 - ).select_related(max_depth=max_depth) + **self._object_key).only(*fields).limit( + 1).select_related(max_depth=max_depth) if obj: obj = obj[0] @@ -640,11 +641,11 @@ def register_delete_rule(cls, document_cls, field_name, rule): for class_name in document_cls._subclasses if class_name != document_cls.__name__] + [document_cls] - for cls in classes: + for klass in classes: for document_cls in documents: - delete_rules = cls._meta.get('delete_rules') or {} + delete_rules = klass._meta.get('delete_rules') or {} delete_rules[(document_cls, field_name)] = rule - cls._meta['delete_rules'] = delete_rules + klass._meta['delete_rules'] = delete_rules @classmethod def drop_collection(cls): @@ -769,7 +770,7 @@ def ensure_indexes(cls): **index_opts) @classmethod - def list_indexes(cls, go_up=True, go_down=True): + def list_indexes(cls): """ Lists all of the indexes that should be created for given collection. It includes all the indexes from super- and sub-classes. """ @@ -816,8 +817,8 @@ def get_indexes_spec(cls): return indexes indexes = [] - for cls in classes: - for index in get_indexes_spec(cls): + for klass in classes: + for index in get_indexes_spec(klass): if index not in indexes: indexes.append(index) @@ -856,7 +857,6 @@ def compare_indexes(cls): 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 @@ -888,7 +888,6 @@ def __delattr__(self, *args, **kwargs): class DynamicEmbeddedDocument(EmbeddedDocument): - """A Dynamic Embedded Document class allowing flexible, expandable and uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more information about dynamic documents. @@ -915,7 +914,6 @@ def __delattr__(self, *args, **kwargs): class MapReduceDocument(object): - """A document returned from a map/reduce query. :param collection: An instance of :class:`~pymongo.Collection` diff --git a/mongoengine/errors.py b/mongoengine/errors.py index a411ac47a..ba33b57f1 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -115,6 +115,7 @@ def build_dict(source): else: return unicode(source) return errors_dict + if not self.errors: return {} return build_dict(self.errors) @@ -127,7 +128,7 @@ def generate_key(value, prefix=''): value = ' '.join([generate_key(k) for k in value]) if isinstance(value, dict): value = ' '.join( - [generate_key(v, k) for k, v in value.iteritems()]) + [generate_key(v, k) for k, v in value.iteritems()]) results = "%s.%s" % (prefix, value) if prefix else value return results diff --git a/mongoengine/fields.py b/mongoengine/fields.py index c27d02b24..4a50593e2 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -47,12 +47,10 @@ 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField', 'MultiPolygonField', 'GeoJsonBaseField'] - RECURSIVE_REFERENCE_CONSTANT = 'self' class StringField(BaseField): - """A unicode string field. """ @@ -112,7 +110,6 @@ def prepare_query_value(self, op, value): class URLField(StringField): - """A field that validates input as an URL. .. versionadded:: 0.3 @@ -159,7 +156,6 @@ def validate(self, value): class EmailField(StringField): - """A field that validates input as an E-Mail-Address. .. versionadded:: 0.4 @@ -181,7 +177,6 @@ def validate(self, value): class IntField(BaseField): - """An 32-bit integer field. """ @@ -216,7 +211,6 @@ def prepare_query_value(self, op, value): class LongField(BaseField): - """An 64-bit integer field. """ @@ -251,7 +245,6 @@ def prepare_query_value(self, op, value): class FloatField(BaseField): - """An floating point number field. """ @@ -286,7 +279,6 @@ def prepare_query_value(self, op, value): class DecimalField(BaseField): - """A fixed-point decimal number field. .. versionchanged:: 0.8 @@ -360,7 +352,6 @@ def prepare_query_value(self, op, value): class BooleanField(BaseField): - """A boolean field type. .. versionadded:: 0.1.2 @@ -379,7 +370,6 @@ def validate(self, value): class DateTimeField(BaseField): - """A datetime field. Uses the python-dateutil library if available alternatively use time.strptime @@ -447,7 +437,6 @@ def prepare_query_value(self, op, value): class ComplexDateTimeField(StringField): - """ ComplexDateTimeField handles microseconds exactly instead of rounding like DateTimeField does. @@ -531,7 +520,6 @@ def prepare_query_value(self, op, value): class EmbeddedDocumentField(BaseField): - """An embedded document field - with a declared document_type. Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`. """ @@ -585,7 +573,6 @@ def prepare_query_value(self, op, value): class GenericEmbeddedDocumentField(BaseField): - """A generic embedded document field - allows any :class:`~mongoengine.EmbeddedDocument` to be stored. @@ -624,7 +611,6 @@ def to_mongo(self, document, use_db_field=True): class DynamicField(BaseField): - """A truly dynamic field type capable of handling different and varying types of data. @@ -641,9 +627,9 @@ def to_mongo(self, value): cls = value.__class__ val = value.to_mongo() # If we its a document thats not inherited add _cls - if (isinstance(value, Document)): + if isinstance(value, Document): val = {"_ref": value.to_dbref(), "_cls": cls.__name__} - if (isinstance(value, EmbeddedDocument)): + if isinstance(value, EmbeddedDocument): val['_cls'] = cls.__name__ return val @@ -678,7 +664,6 @@ def lookup_member(self, 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 super(DynamicField, self).prepare_query_value(op, self.to_mongo(value)) @@ -689,7 +674,6 @@ def validate(self, value, clean=True): class ListField(ComplexBaseField): - """A list field that wraps a standard field, allowing multiple instances of the field to be used as a list in the database. @@ -749,7 +733,6 @@ def __init__(self, document_type, *args, **kwargs): class SortedListField(ListField): - """A ListField that sorts the contents of its list before writing to the database in order to ensure that a sorted list is always retrieved. @@ -801,7 +784,6 @@ def key_has_dot_or_dollar(d): class DictField(ComplexBaseField): - """A dictionary field that wraps a standard Python dictionary. This is similar to an embedded document, but the structure is not defined. @@ -857,7 +839,6 @@ def prepare_query_value(self, op, value): class MapField(DictField): - """A field that maps a name to a specified field type. Similar to a DictField, except the 'value' of each item must match the specified field type. @@ -873,7 +854,6 @@ def __init__(self, field=None, *args, **kwargs): class ReferenceField(BaseField): - """A reference to a document that will be automatically dereferenced on access (lazily). @@ -1010,7 +990,6 @@ def lookup_member(self, member_name): class CachedReferenceField(BaseField): - """ A referencefield with cache fields to purpose pseudo-joins @@ -1025,7 +1004,6 @@ def __init__(self, document_type, fields=[], auto_sync=True, **kwargs): """ if not isinstance(document_type, basestring) and \ not issubclass(document_type, (Document, basestring)): - self.error('Argument to CachedReferenceField constructor must be a' ' document class or a string') @@ -1036,6 +1014,7 @@ def __init__(self, document_type, fields=[], auto_sync=True, **kwargs): def start_listener(self): from mongoengine import signals + signals.post_save.connect(self.on_document_pre_save, sender=self.document_type) @@ -1089,7 +1068,6 @@ def __get__(self, instance, owner): def to_mongo(self, document): id_field_name = self.document_type._meta['id_field'] id_field = self.document_type._fields[id_field_name] - doc_tipe = self.document_type if isinstance(document, Document): # We need the id from the saved object to create the DBRef @@ -1099,6 +1077,7 @@ def to_mongo(self, document): ' 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_)), @@ -1150,7 +1129,6 @@ def sync_all(self): class GenericReferenceField(BaseField): - """A reference to *any* :class:`~mongoengine.document.Document` subclass that will be automatically dereferenced on access (lazily). @@ -1232,7 +1210,6 @@ def prepare_query_value(self, op, value): class BinaryField(BaseField): - """A binary data field. """ @@ -1264,7 +1241,6 @@ class GridFSError(Exception): class GridFSProxy(object): - """Proxy object to handle writing and reading of files to and from GridFS .. versionadded:: 0.4 @@ -1278,12 +1254,12 @@ def __init__(self, grid_id=None, key=None, instance=None, db_alias=DEFAULT_CONNECTION_NAME, collection_name='fs'): - self.grid_id = grid_id # Store GridFS id for file + self.grid_id = grid_id # Store GridFS id for file self.key = key self.instance = instance self.db_alias = db_alias self.collection_name = collection_name - self.newfile = None # Used for partial writes + self.newfile = None # Used for partial writes self.gridout = None def __getattr__(self, name): @@ -1410,7 +1386,6 @@ def _mark_as_changed(self): class FileField(BaseField): - """A GridFS storage field. .. versionadded:: 0.4 @@ -1444,7 +1419,7 @@ def __get__(self, instance, owner): def __set__(self, instance, value): key = self.name if ((hasattr(value, 'read') and not - isinstance(value, GridFSProxy)) or isinstance(value, str_types)): + isinstance(value, GridFSProxy)) or isinstance(value, str_types)): # using "FileField() = file/string" notation grid_file = instance._data.get(self.name) # If a file already exists, delete it @@ -1494,7 +1469,6 @@ def validate(self, value): class ImageGridFsProxy(GridFSProxy): - """ Proxy for ImageField @@ -1518,6 +1492,7 @@ def put(self, file_obj, **kwargs): raise ValidationError('Invalid image: %s' % e) # Progressive JPEG + # TODO: fixme, at least unused, at worst bad implementation progressive = img.info.get('progressive') or False if (kwargs.get('progressive') and @@ -1633,7 +1608,6 @@ class ImproperlyConfigured(Exception): class ImageField(FileField): - """ A Image File storage field. @@ -1672,7 +1646,6 @@ def __init__(self, size=None, thumbnail_size=None, class SequenceField(BaseField): - """Provides a sequential counter see: http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers @@ -1796,7 +1769,6 @@ def to_python(self, value): class UUIDField(BaseField): - """A UUID field. .. versionadded:: 0.6 @@ -1843,13 +1815,12 @@ def validate(self, value): if not isinstance(value, basestring): value = str(value) try: - value = uuid.UUID(value) + uuid.UUID(value) except Exception, exc: self.error('Could not convert to UUID: %s' % exc) class GeoPointField(BaseField): - """A list storing a longitude and latitude coordinate. .. note:: this represents a generic point in a 2D plane and a legacy way of @@ -1879,7 +1850,6 @@ def validate(self, value): class PointField(GeoJsonBaseField): - """A GeoJSON field storing a longitude and latitude coordinate. The data is represented as: @@ -1900,7 +1870,6 @@ class PointField(GeoJsonBaseField): class LineStringField(GeoJsonBaseField): - """A GeoJSON field storing a line of longitude and latitude coordinates. The data is represented as: @@ -1920,7 +1889,6 @@ class LineStringField(GeoJsonBaseField): class PolygonField(GeoJsonBaseField): - """A GeoJSON field storing a polygon of longitude and latitude coordinates. The data is represented as: @@ -1943,7 +1911,6 @@ class PolygonField(GeoJsonBaseField): class MultiPointField(GeoJsonBaseField): - """A GeoJSON field storing a list of Points. The data is represented as: @@ -1964,7 +1931,6 @@ class MultiPointField(GeoJsonBaseField): class MultiLineStringField(GeoJsonBaseField): - """A GeoJSON field storing a list of LineStrings. The data is represented as: @@ -1985,7 +1951,6 @@ class MultiLineStringField(GeoJsonBaseField): class MultiPolygonField(GeoJsonBaseField): - """A GeoJSON field storing list of Polygons. The data is represented as: diff --git a/mongoengine/python_support.py b/mongoengine/python_support.py index 3412c8414..5bb9038dd 100644 --- a/mongoengine/python_support.py +++ b/mongoengine/python_support.py @@ -14,6 +14,7 @@ if PY3: import codecs from io import BytesIO as StringIO + # return s converted to binary. b('test') should be equivalent to b'test' def b(s): return codecs.latin_1_encode(s)[0] diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 99d960444..a8f55bfd4 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -43,7 +43,6 @@ class BaseQuerySet(object): - """A set of results returned from a query. Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as the results. """ @@ -162,8 +161,8 @@ def __getitem__(self, key): if queryset._as_pymongo: return queryset._get_as_pymongo(queryset._cursor[key]) return queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference, - only_fields=self.only_fields) + _auto_dereference=self._auto_dereference, + only_fields=self.only_fields) raise AttributeError @@ -597,9 +596,10 @@ def in_bulk(self, object_ids): doc_map[doc['_id']] = self._get_as_pymongo(doc) else: for doc in docs: - doc_map[doc['_id']] = self._document._from_son(doc, - only_fields=self.only_fields, - _auto_dereference=self._auto_dereference) + doc_map[doc['_id']] = self._document._from_son( + doc, + only_fields=self.only_fields, + _auto_dereference=self._auto_dereference) return doc_map @@ -830,7 +830,6 @@ def fields(self, _only_called=False, **kwargs): cleaned_fields = [] for key, value in kwargs.items(): parts = key.split('__') - op = None if parts[0] in operators: op = parts.pop(0) value = {'$' + op: value} @@ -1616,7 +1615,7 @@ def _item_frequencies_exec_js(self, field, normalize=False): return frequencies - def _fields_to_dbfields(self, fields, subdoc=False): + def _fields_to_dbfields(self, fields): """Translate fields paths to its db equivalents""" ret = [] subclasses = [] diff --git a/mongoengine/queryset/field_list.py b/mongoengine/queryset/field_list.py index 140a71e05..c10ad5525 100644 --- a/mongoengine/queryset/field_list.py +++ b/mongoengine/queryset/field_list.py @@ -1,4 +1,3 @@ - __all__ = ('QueryFieldList',) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index e8bd66ae0..1056de9b5 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -61,7 +61,6 @@ def __repr__(self): data[-1] = "...(remaining elements truncated)..." return repr(data) - def _iter_results(self): """A generator for iterating over the result cache. @@ -74,7 +73,7 @@ def _iter_results(self): upper = len(self._result_cache) while pos < upper: yield self._result_cache[pos] - pos = pos + 1 + pos += 1 if not self._has_more: raise StopIteration if len(self._result_cache) <= pos: diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index e7f5988cd..43542ac40 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -11,7 +11,6 @@ __all__ = ('query', 'update') - COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', 'all', 'size', 'exists', 'not', 'elemMatch', 'type') GEO_OPERATORS = ('within_distance', 'within_spherical_distance', @@ -27,7 +26,7 @@ STRING_OPERATORS + CUSTOM_OPERATORS) -def query(_doc_cls=None, _field_operation=False, **query): +def query(_doc_cls=None, **query): """Transform a query from Django-style format to Mongo format. """ mongo_query = {} @@ -359,6 +358,7 @@ def _infer_geometry(value): raise InvalidQueryError("Invalid $geometry dictionary should have " "type and coordinates keys") elif isinstance(value, (list, set)): + # TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon? try: value[0][0][0] return {"$geometry": {"type": "Polygon", "coordinates": value}} diff --git a/mongoengine/signals.py b/mongoengine/signals.py index 06fb8b4f1..95532f03d 100644 --- a/mongoengine/signals.py +++ b/mongoengine/signals.py @@ -6,6 +6,7 @@ signals_available = False try: from blinker import Namespace + signals_available = True except ImportError: class Namespace(object): @@ -27,6 +28,7 @@ def _fail(self, *args, **kwargs): raise RuntimeError('signalling support is unavailable ' 'because the blinker library is ' 'not installed.') + send = lambda *a, **kw: None connect = disconnect = has_receivers_for = receivers_for = \ temporarily_connected_to = _fail From 217c9720eaba7c1692bb8d9e6c8dbdbc64fc97b1 Mon Sep 17 00:00:00 2001 From: mrigal Date: Wed, 29 Apr 2015 19:47:47 +0200 Subject: [PATCH 0225/1519] added iterkeys method and optimized repr, still very ugly --- mongoengine/base/datastructures.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 7ec67797d..8c71c5878 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -409,6 +409,9 @@ def iteritems(self): def items(self): return [(k, self[k]) for k in iter(self)] + def iterkeys(self): + return iter(self) + def keys(self): return list(iter(self)) @@ -433,7 +436,7 @@ class SpecificStrictDict(cls): __slots__ = allowed_keys_tuple def __repr__(self): - return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k, v) for (k, v) in self.iteritems()) + return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k) for k in self.iterkeys()) cls._classes[allowed_keys] = SpecificStrictDict return cls._classes[allowed_keys] From 307b35a5bfce9337de7e1a4b6583946b4bf4a4fe Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 30 Apr 2015 14:52:08 +0200 Subject: [PATCH 0226/1519] some more update, mainly docs --- CONTRIBUTING.rst | 10 +++++++++- README.rst | 6 ++++-- docs/django.rst | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c935d5a34..c7f581a1c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -29,7 +29,10 @@ Style Guide ----------- MongoEngine aims to follow `PEP8 `_ -including 4 space indents and 79 character line limits. +including 4 space indents. When possible we try to stick to 79 character line limits. +However, screens got bigger and an ORM has a strong focus on readability and +if it can help, we accept 119 as maximum line length, in a similar way as +`django does `_ Testing ------- @@ -38,6 +41,10 @@ All tests are run on `Travis `_ and any pull requests are automatically tested by Travis. Any pull requests without tests will take longer to be integrated and might be refused. +You may also submit a simple failing test as a PullRequest if you don't know +how to fix it, it will be easier for other peopl to work on it and it may get +fixed faster. + General Guidelines ------------------ @@ -48,6 +55,7 @@ General Guidelines from the cmd line to run the test suite). - Ensure tests pass on every Python and PyMongo versions. You can test on these versions locally by executing ``tox`` +- Add enhancements or problematic bug fixes to docs/changelog.rst - Add yourself to AUTHORS :) Documentation diff --git a/README.rst b/README.rst index f2829cd3e..f4c92d5f8 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,9 @@ a `tutorial `_ +We recommend the use of `virtualenv `_ and of +`pip `_. You can then use ``pip install -U mongoengine``. +You may also have `setuptools `_ and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the source from `GitHub `_ and run ``python setup.py install``. @@ -114,7 +116,7 @@ Also use the -s argument if you want to print out whatever or access pdb while t .. code-block:: shell - $ python setup.py nosetests --tests tests/test_django.py:QuerySetTest.test_get_document_or_404 -s + $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest.test_cls_field -s Community ========= diff --git a/docs/django.rst b/docs/django.rst index 6982fe64b..b8a52165d 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -15,5 +15,5 @@ The MongoEngine team is looking for help contributing and maintaining a new Django extension for MongoEngine! If you have Django experience and would like to help contribute to the project, please get in touch on the `mailing list `_ or by -simpily contributing on +simply contributing on `GitHub `_. From e438491938f0bfe5042d1bfc4798bc222c39fe64 Mon Sep 17 00:00:00 2001 From: mrigal Date: Thu, 30 Apr 2015 15:00:57 +0200 Subject: [PATCH 0227/1519] typos --- AUTHORS | 2 +- CONTRIBUTING.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index c776a43ce..29993c843 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,7 +12,7 @@ Laine Herron https://github.com/LaineHerron CONTRIBUTORS -Dervived from the git logs, inevitably incomplete but all of whom and others +Derived from the git logs, inevitably incomplete but all of whom and others have submitted patches, reported bugs and generally helped make MongoEngine that much better: diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c7f581a1c..aeba41f73 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -42,7 +42,7 @@ and any pull requests are automatically tested by Travis. Any pull requests without tests will take longer to be integrated and might be refused. You may also submit a simple failing test as a PullRequest if you don't know -how to fix it, it will be easier for other peopl to work on it and it may get +how to fix it, it will be easier for other people to work on it and it may get fixed faster. General Guidelines From cd76a906f4a4e88d7511f233c7fb7f61996e88e3 Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Wed, 24 Jun 2015 00:49:39 +0200 Subject: [PATCH 0228/1519] Set coverage to specific version as 4+ is not Python 3.2 compatible --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 42794452a..f2afa8eef 100644 --- a/setup.py +++ b/setup.py @@ -52,12 +52,13 @@ def get_version(version_tuple): extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'Pillow>=2.0.0'] + extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0'] if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'] = find_packages() extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} else: - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'Pillow>=2.0.0', 'python-dateutil'] + # coverage 4 does not support Python 3.2 anymore + extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0', 'python-dateutil'] if sys.version_info[0] == 2 and sys.version_info[1] == 6: extra_opts['tests_require'].append('unittest2') From 0aeb1ca408c6e3cf15eaf51aabddfbe72fb3e72a Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Wed, 24 Jun 2015 00:50:36 +0200 Subject: [PATCH 0229/1519] Various fixes again --- docs/_themes/sphinx_rtd_theme/footer.html | 2 +- docs/guide/querying.rst | 2 +- mongoengine/base/datastructures.py | 2 +- mongoengine/base/document.py | 16 +++++------ mongoengine/base/fields.py | 6 ++-- mongoengine/base/metaclasses.py | 21 +++++++------- mongoengine/dereference.py | 8 +++--- mongoengine/document.py | 3 +- mongoengine/errors.py | 2 +- mongoengine/fields.py | 19 ++++++------- mongoengine/queryset/base.py | 34 +++++++++++------------ mongoengine/queryset/queryset.py | 2 +- mongoengine/queryset/transform.py | 4 +-- tests/document/delta.py | 10 +++---- tests/document/dynamic.py | 5 ++-- tests/document/indexes.py | 2 +- tests/document/instance.py | 4 +-- tests/fields/__init__.py | 2 +- tests/fields/fields.py | 15 +++++----- tests/fields/file_tests.py | 8 +++--- tests/fields/geo.py | 4 +-- tests/test_datastructures.py | 6 +++- 22 files changed, 88 insertions(+), 89 deletions(-) diff --git a/docs/_themes/sphinx_rtd_theme/footer.html b/docs/_themes/sphinx_rtd_theme/footer.html index e42d753ff..1fa05eaad 100755 --- a/docs/_themes/sphinx_rtd_theme/footer.html +++ b/docs/_themes/sphinx_rtd_theme/footer.html @@ -2,7 +2,7 @@ {% if next or prev %}