From 782d48594a3fabaad8dfe4b5444f9d18839b309a Mon Sep 17 00:00:00 2001 From: "bool.dev" Date: Thu, 4 Apr 2013 08:24:35 +0530 Subject: [PATCH 001/222] Fixes resolving to db_field from class field name, in distinct() query. --- mongoengine/queryset.py | 2 ++ tests/test_queryset.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index bff05fcf..ff168c77 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -1214,6 +1214,8 @@ class QuerySet(object): .. versionchanged:: 0.5 - Fixed handling references .. versionchanged:: 0.6 - Improved db_field refrence handling """ + field = [field] + field = self._fields_to_dbfields(field).pop() return self._dereference(self._cursor.distinct(field), 1, name=field, instance=self._document) diff --git a/tests/test_queryset.py b/tests/test_queryset.py index 5234cea2..df58e21e 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -2481,6 +2481,23 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Foo.objects.distinct("bar"), [bar]) + def test_distinct_handles_db_field(self): + """Ensure that distinct resolves field name to db_field as expected. + """ + class Product(Document): + product_id=IntField(db_field='pid') + + Product.drop_collection() + + product_one = Product(product_id=1).save() + product_two = Product(product_id=2).save() + product_one_dup = Product(product_id=1).save() + + self.assertEqual(set(Product.objects.distinct('product_id')), + set([1, 2])) + + Product.drop_collection() + def test_custom_manager(self): """Ensure that custom QuerySetManager instances work as expected. """ From d92f992c011cec92e983cf7a1da45dc31624cc92 Mon Sep 17 00:00:00 2001 From: "bool.dev" Date: Sun, 14 Apr 2013 13:48:11 +0530 Subject: [PATCH 002/222] Removed merge trackers in code, merged correctly now. --- mongoengine/queryset.py | 7 ------- tests/test_queryset.py | 13 ------------- 2 files changed, 20 deletions(-) diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 8d18942f..727f56ea 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -1212,18 +1212,11 @@ class QuerySet(object): .. versionchanged:: 0.5 - Fixed handling references .. versionchanged:: 0.6 - Improved db_field refrence handling """ -<<<<<<< HEAD - field = [field] - field = self._fields_to_dbfields(field).pop() - return self._dereference(self._cursor.distinct(field), 1, - name=field, instance=self._document) -======= try: field = self._fields_to_dbfields([field]).pop() finally: return self._dereference(self._cursor.distinct(field), 1, name=field, instance=self._document) ->>>>>>> upstream/master def only(self, *fields): """Load only a subset of this document's fields. :: diff --git a/tests/test_queryset.py b/tests/test_queryset.py index 7bb55b08..6b569261 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -2565,18 +2565,6 @@ class QuerySetTest(unittest.TestCase): """Ensure that distinct resolves field name to db_field as expected. """ class Product(Document): -<<<<<<< HEAD - product_id=IntField(db_field='pid') - - Product.drop_collection() - - product_one = Product(product_id=1).save() - product_two = Product(product_id=2).save() - product_one_dup = Product(product_id=1).save() - - self.assertEqual(set(Product.objects.distinct('product_id')), - set([1, 2])) -======= product_id = IntField(db_field='pid') Product.drop_collection() @@ -2589,7 +2577,6 @@ class QuerySetTest(unittest.TestCase): set([1, 2])) self.assertEqual(set(Product.objects.distinct('pid')), set([1, 2])) ->>>>>>> upstream/master Product.drop_collection() From 44a2a164c0fa46bb3839a08d9b240243b9ab71fe Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 13 Jun 2013 10:54:39 +0000 Subject: [PATCH 003/222] Doc updates --- docs/changelog.rst | 2 +- docs/guide/defining-documents.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f86a629..3a397524 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,7 +9,7 @@ Changes in 0.8.2 - Fixed cascading saves which weren't turned off as planned (#291) - Fixed Datastructures so instances are a Document or EmbeddedDocument (#363) - Improved cascading saves write performance (#361) -- Fixed amibiguity and differing behaviour regarding field defaults (#349) +- Fixed ambiguity and differing behaviour regarding field defaults (#349) - ImageFields now include PIL error messages if invalid error (#353) - Added lock when calling doc.Delete() for when signals have no sender (#350) - Reload forces read preference to be PRIMARY (#355) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index b3b1e592..a61d8fe8 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -450,8 +450,8 @@ 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 **+** or a **-** sign. Note that -direction only matters on multi-field indexes. :: +fields by prefixing the field name with a **+** (for ascending) or a **-** sign +(for descending). Note that direction only matters on multi-field indexes. :: class Page(Document): title = StringField() From c31d6a68985e9807795aaf60bc796ac86a227e84 Mon Sep 17 00:00:00 2001 From: kelvinhammond Date: Wed, 19 Jun 2013 10:34:33 -0400 Subject: [PATCH 004/222] Fixed sum and average mapreduce function for issue #375 --- AUTHORS | 3 ++- mongoengine/queryset/queryset.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7788139e..780c9e6c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Dervived 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: + * Kelvin Hammond (https://github.com/kelvinhammond) * Harry Marr * Ross Lawley * blackbrrr @@ -169,4 +170,4 @@ that much better: * Massimo Santini (https://github.com/mapio) * Nigel McNie (https://github.com/nigelmcnie) * ygbourhis (https://github.com/ygbourhis) - * Bob Dickinson (https://github.com/BobDickinson) \ No newline at end of file + * Bob Dickinson (https://github.com/BobDickinson) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index d58a13b7..e2ff43f7 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1062,7 +1062,7 @@ class QuerySet(object): """ map_func = Code(""" function() { - emit(1, this[field] || 0); + emit(1, eval("this." + field) || 0); } """, scope={'field': field}) @@ -1093,7 +1093,7 @@ class QuerySet(object): map_func = Code(""" function() { if (this.hasOwnProperty(field)) - emit(1, {t: this[field] || 0, c: 1}); + emit(1, {t: eval("this." + field) || 0, c: 1}); } """, scope={'field': field}) From 574f3c23d3ce633d830f55918a009c30305e3dbd Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 21 Jun 2013 09:35:22 +0000 Subject: [PATCH 005/222] get should clone before calling --- mongoengine/queryset/queryset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index d58a13b7..9b53df2a 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -245,8 +245,10 @@ class QuerySet(object): .. versionadded:: 0.3 """ - queryset = self.__call__(*q_objs, **query) + queryset = self.clone() queryset = queryset.limit(2) + queryset = queryset.filter(*q_objs, **query) + try: result = queryset.next() except StopIteration: From f1a1aa54d8a31336d2a76a21f2e4b166d776f526 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 21 Jun 2013 10:19:40 +0000 Subject: [PATCH 006/222] Added full_result kwarg to update (#380) --- docs/changelog.rst | 3 +++ mongoengine/document.py | 8 +++++++- mongoengine/queryset/queryset.py | 16 ++++++++++------ tests/queryset/queryset.py | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3a397524..8fa5af05 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +Changes in 0.8.3 +================ +- Added full_result kwarg to update (#380) Changes in 0.8.2 ================ diff --git a/mongoengine/document.py b/mongoengine/document.py index a61ed079..a1bac195 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -353,7 +353,13 @@ class Document(BaseDocument): been saved. """ if not self.pk: - raise OperationError('attempt to update a document not yet saved') + if kwargs.get('upsert', False): + query = self.to_mongo() + if "_cls" in query: + del(query["_cls"]) + return self._qs.filter(**query).update_one(**kwargs) + else: + raise OperationError('attempt to update a document not yet saved') # Need to add shard key to query, or you get an error return self._qs.filter(**self._object_key).update_one(**kwargs) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 9b53df2a..4b32ab12 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -474,7 +474,8 @@ class QuerySet(object): queryset._collection.remove(queryset._query, write_concern=write_concern) - def update(self, upsert=False, multi=True, write_concern=None, **update): + def update(self, upsert=False, multi=True, write_concern=None, + full_result=False, **update): """Perform an atomic update on the fields matched by the query. :param upsert: Any existing document with that "_id" is overwritten. @@ -485,6 +486,8 @@ class QuerySet(object): ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. + :param full_result: Return the full result rather than just the number + updated. :param update: Django-style update keyword arguments .. versionadded:: 0.2 @@ -506,12 +509,13 @@ class QuerySet(object): update["$set"]["_cls"] = queryset._document._class_name else: update["$set"] = {"_cls": queryset._document._class_name} - try: - ret = queryset._collection.update(query, update, multi=multi, - upsert=upsert, **write_concern) - if ret is not None and 'n' in ret: - return ret['n'] + result = queryset._collection.update(query, update, multi=multi, + upsert=upsert, **write_concern) + if full_result: + return result + elif result: + return result['n'] except pymongo.errors.OperationFailure, err: if unicode(err) == u'multi not coded yet': message = u'update() method requires MongoDB 1.1.3+' diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 21df22c5..bd231e33 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -536,6 +536,23 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(club.members['John']['gender'], "F") self.assertEqual(club.members['John']['age'], 14) + def test_update_results(self): + self.Person.drop_collection() + + result = self.Person(name="Bob", age=25).update(upsert=True, full_result=True) + self.assertIsInstance(result, dict) + self.assertTrue("upserted" in result) + self.assertFalse(result["updatedExisting"]) + + bob = self.Person.objects.first() + result = bob.update(set__age=30, full_result=True) + self.assertIsInstance(result, dict) + self.assertTrue(result["updatedExisting"]) + + self.Person(name="Bob", age=20).save() + result = self.Person.objects(name="Bob").update(set__name="bobby", multi=True) + self.assertEqual(result, 2) + def test_upsert(self): self.Person.drop_collection() From e116bb92272246528df242c587f1b8e5e6fb6f48 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 21 Jun 2013 10:39:10 +0000 Subject: [PATCH 007/222] Fixed queryset.get() respecting no_dereference (#373) --- docs/changelog.rst | 1 + mongoengine/queryset/queryset.py | 4 ++-- tests/queryset/queryset.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8fa5af05..b04bba53 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Fixed queryset.get() respecting no_dereference (#373) - Added full_result kwarg to update (#380) Changes in 0.8.2 diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 4b32ab12..ded8d5eb 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1165,8 +1165,8 @@ class QuerySet(object): raw_doc = self._cursor.next() if self._as_pymongo: return self._get_as_pymongo(raw_doc) - - doc = self._document._from_son(raw_doc) + doc = self._document._from_son(raw_doc, + _auto_dereference=self._auto_dereference) if self._scalar: return self._get_scalar(doc) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index bd231e33..7c473603 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3258,6 +3258,8 @@ class QuerySetTest(unittest.TestCase): self.assertTrue(isinstance(qs.first().organization, Organization)) self.assertFalse(isinstance(qs.no_dereference().first().organization, Organization)) + self.assertFalse(isinstance(qs.no_dereference().get().organization, + Organization)) self.assertTrue(isinstance(qs.first().organization, Organization)) def test_cached_queryset(self): From e6374ab425d45c3d4007a1773b364bd29a40bd24 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 21 Jun 2013 10:40:15 +0000 Subject: [PATCH 008/222] Added Michael Bartnett to Authors (#373) --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 7788139e..a50eb570 100644 --- a/AUTHORS +++ b/AUTHORS @@ -169,4 +169,5 @@ that much better: * Massimo Santini (https://github.com/mapio) * Nigel McNie (https://github.com/nigelmcnie) * ygbourhis (https://github.com/ygbourhis) - * Bob Dickinson (https://github.com/BobDickinson) \ No newline at end of file + * Bob Dickinson (https://github.com/BobDickinson) + * Michael Bartnett (https://github.com/michaelbartnett) \ No newline at end of file From 9867e918fa58f99cceca8a8917c9673251cc309d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 21 Jun 2013 11:04:29 +0000 Subject: [PATCH 009/222] Fixed weakref being valid after reload (#374) --- docs/changelog.rst | 1 + mongoengine/document.py | 1 + tests/queryset/queryset.py | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b04bba53..265ad13d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Fixed weakref being valid after reload (#374) - Fixed queryset.get() respecting no_dereference (#373) - Added full_result kwarg to update (#380) diff --git a/mongoengine/document.py b/mongoengine/document.py index a1bac195..ab8fa2ac 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -480,6 +480,7 @@ class Document(BaseDocument): value = [self._reload(key, v) for v in value] value = BaseList(value, self, key) elif isinstance(value, (EmbeddedDocument, DynamicEmbeddedDocument)): + value._instance = None value._changed_fields = [] return value diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7c473603..6dcbd9f3 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1613,6 +1613,32 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(message.authors[1].name, "Ross") self.assertEqual(message.authors[2].name, "Adam") + def test_reload_embedded_docs_instance(self): + + class SubDoc(EmbeddedDocument): + val = IntField() + + class Doc(Document): + embedded = EmbeddedDocumentField(SubDoc) + + doc = Doc(embedded=SubDoc(val=0)).save() + doc.reload() + + self.assertEqual(doc.pk, doc.embedded._instance.pk) + + def test_reload_list_embedded_docs_instance(self): + + class SubDoc(EmbeddedDocument): + val = IntField() + + class Doc(Document): + embedded = ListField(EmbeddedDocumentField(SubDoc)) + + doc = Doc(embedded=[SubDoc(val=0)]).save() + doc.reload() + + self.assertEqual(doc.pk, doc.embedded[0]._instance.pk) + def test_order_by(self): """Ensure that QuerySets may be ordered. """ From d6edef98c6b08383cff7384f0548cc8991adc7bb Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 21 Jun 2013 11:29:23 +0000 Subject: [PATCH 010/222] Added match ($elemMatch) support for EmbeddedDocuments (#379) --- docs/changelog.rst | 1 + mongoengine/queryset/transform.py | 1 + tests/queryset/queryset.py | 13 ++++++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 265ad13d..1927beea 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Added match ($elemMatch) support for EmbeddedDocuments (#379) - Fixed weakref being valid after reload (#374) - Fixed queryset.get() respecting no_dereference (#373) - Added full_result kwarg to update (#380) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 4062fc1e..352774ff 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -95,6 +95,7 @@ def query(_doc_cls=None, _field_operation=False, **query): value = _geo_operator(field, op, value) elif op in CUSTOM_OPERATORS: if op == 'match': + value = field.prepare_query_value(op, value) value = {"$elemMatch": value} else: NotImplementedError("Custom method '%s' has not " diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 6dcbd9f3..eabb3c5c 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3091,7 +3091,7 @@ class QuerySetTest(unittest.TestCase): class Foo(EmbeddedDocument): shape = StringField() color = StringField() - trick = BooleanField() + thick = BooleanField() meta = {'allow_inheritance': False} class Bar(Document): @@ -3100,17 +3100,20 @@ class QuerySetTest(unittest.TestCase): Bar.drop_collection() - b1 = Bar(foo=[Foo(shape= "square", color ="purple", thick = False), - Foo(shape= "circle", color ="red", thick = True)]) + b1 = Bar(foo=[Foo(shape="square", color="purple", thick=False), + Foo(shape="circle", color="red", thick=True)]) b1.save() - b2 = Bar(foo=[Foo(shape= "square", color ="red", thick = True), - Foo(shape= "circle", color ="purple", thick = False)]) + b2 = Bar(foo=[Foo(shape="square", color="red", thick=True), + Foo(shape="circle", color="purple", thick=False)]) b2.save() ak = list(Bar.objects(foo__match={'shape': "square", "color": "purple"})) self.assertEqual([b1], ak) + ak = list(Bar.objects(foo__match=Foo(shape="square", color="purple"))) + self.assertEqual([b1], ak) + def test_upsert_includes_cls(self): """Upserts should include _cls information for inheritable classes """ From caff44c663295322ba62c1dc26ff488b1d13b651 Mon Sep 17 00:00:00 2001 From: kelvinhammond Date: Fri, 21 Jun 2013 09:39:11 -0400 Subject: [PATCH 011/222] Fixed sum and average queryset function * Fixed sum and average map reduce functions for sum and average so that it works with mongo dot notation. * Added unittest cases / updated them for the new changes --- mongoengine/queryset/queryset.py | 37 +++++++++++++++++++++++++++++--- tests/queryset/queryset.py | 26 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index bf80c697..e5026fd4 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1068,7 +1068,22 @@ class QuerySet(object): """ map_func = Code(""" function() { - emit(1, eval("this." + field) || 0); + function deepFind(obj, path) { + var paths = path.split('.') + , current = obj + , i; + + for (i = 0; i < paths.length; ++i) { + if (current[paths[i]] == undefined) { + return undefined; + } else { + current = current[paths[i]]; + } + } + return current; + } + + emit(1, deepFind(this, field) || 0); } """, scope={'field': field}) @@ -1098,8 +1113,24 @@ class QuerySet(object): """ map_func = Code(""" function() { - if (this.hasOwnProperty(field)) - emit(1, {t: eval("this." + field) || 0, c: 1}); + function deepFind(obj, path) { + var paths = path.split('.') + , current = obj + , i; + + for (i = 0; i < paths.length; ++i) { + if (current[paths[i]] == undefined) { + return undefined; + } else { + current = current[paths[i]]; + } + } + return current; + } + + val = deepFind(this, field) + if (val !== undefined) + emit(1, {t: val || 0, c: 1}); } """, scope={'field': field}) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 6dcbd9f3..de66ddc4 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -30,12 +30,17 @@ class QuerySetTest(unittest.TestCase): def setUp(self): connect(db='mongoenginetest') + class PersonMeta(EmbeddedDocument): + weight = IntField() + class Person(Document): name = StringField() age = IntField() + person_meta = EmbeddedDocumentField(PersonMeta) meta = {'allow_inheritance': True} Person.drop_collection() + self.PersonMeta = PersonMeta self.Person = Person def test_initialisation(self): @@ -2208,6 +2213,19 @@ class QuerySetTest(unittest.TestCase): self.Person(name='ageless person').save() self.assertEqual(int(self.Person.objects.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) + + 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) + + self.Person(name='test meta none').save() + self.assertEqual(int(self.Person.objects.average('person_meta.weight')), avg) + + def test_sum(self): """Ensure that field can be summed over correctly. """ @@ -2220,6 +2238,14 @@ class QuerySetTest(unittest.TestCase): self.Person(name='ageless person').save() self.assertEqual(int(self.Person.objects.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(name='weightless person').save() + self.assertEqual(int(self.Person.objects.sum('age')), sum(ages)) + def test_distinct(self): """Ensure that the QuerySet.distinct method works. """ From fbe5df84c0f5d49396093f0549b9b3fab28c454d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 25 Jun 2013 09:30:28 +0000 Subject: [PATCH 012/222] Remove users post uri test --- tests/test_connection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index d7926489..d27a66db 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -56,6 +56,9 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'mongoenginetest') + c.admin.system.users.remove({}) + c.mongoenginetest.system.users.remove({}) + def test_register_connection(self): """Ensure that connections with different aliases may be registered. """ From 8d21e5f3c10a0341db2687aef56385ec245f3bae Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 2 Jul 2013 09:47:54 +0000 Subject: [PATCH 013/222] Fix tests for py2.6 --- tests/queryset/queryset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index eabb3c5c..4d91b55b 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -540,13 +540,13 @@ class QuerySetTest(unittest.TestCase): self.Person.drop_collection() result = self.Person(name="Bob", age=25).update(upsert=True, full_result=True) - self.assertIsInstance(result, dict) + self.assertTrue(isinstance(result, dict)) self.assertTrue("upserted" in result) self.assertFalse(result["updatedExisting"]) bob = self.Person.objects.first() result = bob.update(set__age=30, full_result=True) - self.assertIsInstance(result, dict) + self.assertTrue(isinstance(result, dict)) self.assertTrue(result["updatedExisting"]) self.Person(name="Bob", age=20).save() From 43d6e64cfa959e31ff6e983c74534c06ac5b3108 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 2 Jul 2013 17:04:15 +0200 Subject: [PATCH 014/222] Added a get_proxy_obj method to FileField and handle FileFields in container fields properly in ImageGridFsProxy. --- mongoengine/fields.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 451f7aca..727803f4 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1190,9 +1190,7 @@ class FileField(BaseField): # Check if a file already exists for this model grid_file = instance._data.get(self.name) if not isinstance(grid_file, self.proxy_class): - grid_file = self.proxy_class(key=self.name, instance=instance, - db_alias=self.db_alias, - collection_name=self.collection_name) + grid_file = self.get_proxy_obj(key=key, instance=instance) instance._data[self.name] = grid_file if not grid_file.key: @@ -1214,14 +1212,22 @@ class FileField(BaseField): pass # Create a new proxy object as we don't already have one - instance._data[key] = self.proxy_class(key=key, instance=instance, - db_alias=self.db_alias, - collection_name=self.collection_name) + instance._data[key] = self.get_proxy_obj(key=key, instance=instance) instance._data[key].put(value) else: instance._data[key] = value instance._mark_as_changed(key) + + def get_proxy_obj(self, key, instance, db_alias=None, collection_name=None): + if db_alias is None: + db_alias = self.db_alias + if collection_name is None: + collection_name = self.collection_name + + return self.proxy_class(key=key, instance=instance, + db_alias=db_alias, + collection_name=collection_name) def to_mongo(self, value): # Store the GridFS file id in MongoDB @@ -1255,6 +1261,11 @@ class ImageGridFsProxy(GridFSProxy): applying field properties (size, thumbnail_size) """ field = self.instance._fields[self.key] + # if the field from the instance has an attribute field + # we use that one and hope for the best. Usually only container + # fields have a field attribute. + if hasattr(field, 'field'): + field = field.field try: img = Image.open(file_obj) From 5021b1053599a53a9b50d02236fa701230065a71 Mon Sep 17 00:00:00 2001 From: Serge Matveenko Date: Wed, 3 Jul 2013 01:17:40 +0400 Subject: [PATCH 015/222] Fix crash on Python 3.x and Django >= 1.5 --- mongoengine/django/sessions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongoengine/django/sessions.py b/mongoengine/django/sessions.py index c90807e5..7e4e182f 100644 --- a/mongoengine/django/sessions.py +++ b/mongoengine/django/sessions.py @@ -1,7 +1,10 @@ from django.conf import settings from django.contrib.sessions.backends.base import SessionBase, CreateError from django.core.exceptions import SuspiciousOperation -from django.utils.encoding import force_unicode +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 592c654916c38eb3b01bf98db7160f4c871ab936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Fri, 5 Jul 2013 10:36:11 -0300 Subject: [PATCH 016/222] extending support for queryset.sum and queryset.average methods --- mongoengine/queryset/queryset.py | 51 ++++++++--- tests/queryset/queryset.py | 140 +++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 10 deletions(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index ded8d5eb..86a14b50 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1066,11 +1066,27 @@ class QuerySet(object): .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work with sharding. """ - map_func = Code(""" + map_func = """ function() { - emit(1, this[field] || 0); + var path = '{{~%(field)s}}'.split('.'), + field = this; + + for (p in path) { + if (typeof field != 'undefined') + field = field[path[p]]; + else + break; + } + + if (field && field.constructor == Array) { + field.forEach(function(item) { + emit(1, item||0); + }); + } else if (typeof field != 'undefined') { + emit(1, field||0); + } } - """, scope={'field': field}) + """ % dict(field=field) reduce_func = Code(""" function(key, values) { @@ -1096,13 +1112,28 @@ class QuerySet(object): .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work with sharding. """ - map_func = Code(""" + map_func = """ function() { - if (this.hasOwnProperty(field)) - emit(1, {t: this[field] || 0, c: 1}); - } - """, scope={'field': field}) + var path = '{{~%(field)s}}'.split('.'), + field = this; + for (p in path) { + if (typeof field != 'undefined') + field = field[path[p]]; + else + break; + } + + if (field && field.constructor == Array) { + field.forEach(function(item) { + emit(1, {t: item||0, c: 1}); + }); + } else if (typeof field != 'undefined') { + emit(1, {t: field||0, c: 1}); + } + } + """ % dict(field=field) + reduce_func = Code(""" function(key, values) { var out = {t: 0, c: 0}; @@ -1263,8 +1294,8 @@ class QuerySet(object): def _item_frequencies_map_reduce(self, field, normalize=False): map_func = """ function() { - var path = '{{~%(field)s}}'.split('.'); - var field = this; + var path = '{{~%(field)s}}'.split('.'), + field = this; for (p in path) { if (typeof field != 'undefined') diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 4d91b55b..3f9bd236 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2208,6 +2208,75 @@ class QuerySetTest(unittest.TestCase): self.Person(name='ageless person').save() self.assertEqual(int(self.Person.objects.average('age')), avg) + def test_embedded_average(self): + class Pay(EmbeddedDocument): + value = DecimalField() + + class Doc(Document): + name = StringField() + pay = EmbeddedDocumentField( + Pay) + + Doc.drop_collection() + + Doc(name=u"Wilson Junior", + pay=Pay(value=150)).save() + + Doc(name=u"Isabella Luanna", + pay=Pay(value=530)).save() + + Doc(name=u"Tayza mariana", + pay=Pay(value=165)).save() + + Doc(name=u"Eliana Costa", + pay=Pay(value=115)).save() + + self.assertEqual( + Doc.objects.average('pay.value'), + 240) + + def test_embedded_array_average(self): + class Pay(EmbeddedDocument): + values = ListField(DecimalField()) + + class Doc(Document): + name = StringField() + pay = EmbeddedDocumentField( + Pay) + + Doc.drop_collection() + + Doc(name=u"Wilson Junior", + pay=Pay(values=[150, 100])).save() + + Doc(name=u"Isabella Luanna", + pay=Pay(values=[530, 100])).save() + + Doc(name=u"Tayza mariana", + pay=Pay(values=[165, 100])).save() + + Doc(name=u"Eliana Costa", + pay=Pay(values=[115, 100])).save() + + self.assertEqual( + Doc.objects.average('pay.values'), + 170) + + def test_array_average(self): + class Doc(Document): + values = ListField(DecimalField()) + + Doc.drop_collection() + + Doc(values=[150, 100]).save() + Doc(values=[530, 100]).save() + Doc(values=[165, 100]).save() + Doc(values=[115, 100]).save() + + self.assertEqual( + Doc.objects.average('values'), + 170) + def test_sum(self): """Ensure that field can be summed over correctly. """ @@ -2220,6 +2289,77 @@ class QuerySetTest(unittest.TestCase): self.Person(name='ageless person').save() self.assertEqual(int(self.Person.objects.sum('age')), sum(ages)) + def test_embedded_sum(self): + class Pay(EmbeddedDocument): + value = DecimalField() + + class Doc(Document): + name = StringField() + pay = EmbeddedDocumentField( + Pay) + + Doc.drop_collection() + + Doc(name=u"Wilson Junior", + pay=Pay(value=150)).save() + + Doc(name=u"Isabella Luanna", + pay=Pay(value=530)).save() + + Doc(name=u"Tayza mariana", + pay=Pay(value=165)).save() + + Doc(name=u"Eliana Costa", + pay=Pay(value=115)).save() + + self.assertEqual( + Doc.objects.sum('pay.value'), + 960) + + + def test_embedded_array_sum(self): + class Pay(EmbeddedDocument): + values = ListField(DecimalField()) + + class Doc(Document): + name = StringField() + pay = EmbeddedDocumentField( + Pay) + + Doc.drop_collection() + + Doc(name=u"Wilson Junior", + pay=Pay(values=[150, 100])).save() + + Doc(name=u"Isabella Luanna", + pay=Pay(values=[530, 100])).save() + + Doc(name=u"Tayza mariana", + pay=Pay(values=[165, 100])).save() + + Doc(name=u"Eliana Costa", + pay=Pay(values=[115, 100])).save() + + self.assertEqual( + Doc.objects.sum('pay.values'), + 1360) + + def test_array_sum(self): + class Doc(Document): + values = ListField(DecimalField()) + + Doc.drop_collection() + + Doc(values=[150, 100]).save() + Doc(values=[530, 100]).save() + Doc(values=[165, 100]).save() + Doc(values=[115, 100]).save() + + self.assertEqual( + Doc.objects.sum('values'), + 1360) + + def test_distinct(self): """Ensure that the QuerySet.distinct method works. """ From a1d142d3a4bcfb6dd2d9df5d3adf5eec2c51edb5 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 08:38:13 +0000 Subject: [PATCH 017/222] Prep for django and py3 support --- .travis.yml | 2 ++ setup.py | 2 +- tests/test_django.py | 60 ++++++++++++++++++-------------------------- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7c56a02..2bb58638 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ env: - PYMONGO=dev DJANGO=1.4.2 - PYMONGO=2.5 DJANGO=1.5.1 - PYMONGO=2.5 DJANGO=1.4.2 + - PYMONGO=3.2 DJANGO=1.5.1 + - PYMONGO=3.3 DJANGO=1.5.1 install: - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi diff --git a/setup.py b/setup.py index effb6f11..f6b3c1bf 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ CLASSIFIERS = [ extra_opts = {} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'django>=1.5.1'] extra_opts['packages'] = find_packages(exclude=('tests',)) if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'].append("tests") diff --git a/tests/test_django.py b/tests/test_django.py index 63e3245a..d67b126d 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -2,48 +2,42 @@ import sys sys.path[0:0] = [""] import unittest from nose.plugins.skip import SkipTest -from mongoengine.python_support import PY3 from mongoengine import * + +from mongoengine.django.shortcuts import get_document_or_404 + +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'), +) + try: - from mongoengine.django.shortcuts import get_document_or_404 - - 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'), - ) - - 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 - DJ15 = True - except Exception: - DJ15 = False - from django.contrib.sessions.tests import SessionTestsMixin - from mongoengine.django.sessions import SessionStore, MongoSession -except Exception, err: - if PY3: - SessionTestsMixin = type # dummy value so no error - SessionStore = None # dummy value so no error - else: - raise err + from django.contrib.auth import authenticate, get_user_model + from mongoengine.django.auth import User + from mongoengine.django.mongo_auth.models import MongoUser, MongoUserManager + DJ15 = True +except Exception: + DJ15 = False +from django.contrib.sessions.tests import SessionTestsMixin +from mongoengine.django.sessions import SessionStore, MongoSession 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.__offset = timedelta(minutes=offset) self.__name = name def utcoffset(self, dt): @@ -70,8 +64,6 @@ def activate_timezone(tz): class QuerySetTest(unittest.TestCase): def setUp(self): - if PY3: - raise SkipTest('django does not have Python 3 support') connect(db='mongoenginetest') class Person(Document): @@ -223,8 +215,6 @@ class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase): backend = SessionStore def setUp(self): - if PY3: - raise SkipTest('django does not have Python 3 support') connect(db='mongoenginetest') MongoSession.drop_collection() super(MongoDBSessionTest, self).setUp() @@ -262,8 +252,6 @@ class MongoAuthTest(unittest.TestCase): } def setUp(self): - if PY3: - raise SkipTest('django does not have Python 3 support') if not DJ15: raise SkipTest('mongo_auth requires Django 1.5') connect(db='mongoenginetest') From 0cb40703641d94f6334b92ae23148a9b7331bfa4 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 08:53:56 +0000 Subject: [PATCH 018/222] Added Django 1.5 PY3 support (#392) --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1927beea..f433f21e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Added Django 1.5 PY3 support (#392) - Added match ($elemMatch) support for EmbeddedDocuments (#379) - Fixed weakref being valid after reload (#374) - Fixed queryset.get() respecting no_dereference (#373) From 7cb46d0761e6b058d55f8ad93f584fa6bb6ade15 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 09:11:50 +0000 Subject: [PATCH 019/222] Fixed ListField setslice and delslice dirty tracking (#390) --- AUTHORS | 3 ++- docs/changelog.rst | 1 + mongoengine/base/datastructures.py | 8 ++++++++ tests/fields/fields.py | 26 ++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index a50eb570..e88de8c6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -170,4 +170,5 @@ that much better: * Nigel McNie (https://github.com/nigelmcnie) * ygbourhis (https://github.com/ygbourhis) * Bob Dickinson (https://github.com/BobDickinson) - * Michael Bartnett (https://github.com/michaelbartnett) \ No newline at end of file + * Michael Bartnett (https://github.com/michaelbartnett) + * Alon Horev (https://github.com/alonho) \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index f433f21e..27d51a43 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Fixed ListField setslice and delslice dirty tracking (#390) - Added Django 1.5 PY3 support (#392) - Added match ($elemMatch) support for EmbeddedDocuments (#379) - Fixed weakref being valid after reload (#374) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index adcd8d04..4652fb56 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -108,6 +108,14 @@ class BaseList(list): self._mark_as_changed() return super(BaseList, self).__delitem__(*args, **kwargs) + def __setslice__(self, *args, **kwargs): + self._mark_as_changed() + return super(BaseList, self).__setslice__(*args, **kwargs) + + def __delslice__(self, *args, **kwargs): + self._mark_as_changed() + return super(BaseList, self).__delslice__(*args, **kwargs) + def __getstate__(self): self.instance = None self._dereferenced = False diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 3e48a214..8f024991 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1018,6 +1018,32 @@ class FieldTest(unittest.TestCase): e.mapping = {} self.assertEqual([], e._changed_fields) + def test_slice_marks_field_as_changed(self): + + class Simple(Document): + widgets = ListField() + + simple = Simple(widgets=[1, 2, 3, 4]).save() + simple.widgets[:3] = [] + self.assertEqual(['widgets'], simple._changed_fields) + simple.save() + + simple = simple.reload() + self.assertEqual(simple.widgets, [4]) + + def test_del_slice_marks_field_as_changed(self): + + class Simple(Document): + widgets = ListField() + + simple = Simple(widgets=[1, 2, 3, 4]).save() + del simple.widgets[:3] + self.assertEqual(['widgets'], simple._changed_fields) + simple.save() + + simple = simple.reload() + self.assertEqual(simple.widgets, [4]) + def test_list_field_complex(self): """Ensure that the list fields can handle the complex types.""" From af86aee9700504458ae945914a2b9412c5da8ea6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 10:57:24 +0000 Subject: [PATCH 020/222] _dynamic field updates - fixed pickling and creation order Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) Fixed pickling dynamic documents `_dynamic_fields` (#387) --- docs/changelog.rst | 2 ++ docs/guide/defining-documents.rst | 2 +- docs/upgrade.rst | 10 +++++++ mongoengine/base/document.py | 44 ++++++++++++------------------- mongoengine/base/metaclasses.py | 11 ++++++-- mongoengine/document.py | 5 +--- tests/document/delta.py | 13 ++++----- tests/document/instance.py | 36 ++++++++++++++++++++++++- tests/fixtures.py | 8 ++++++ 9 files changed, 90 insertions(+), 41 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 27d51a43..78deafb9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ Changelog Changes in 0.8.3 ================ +- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) +- Fixed pickling dynamic documents `_dynamic_fields` (#387) - Fixed ListField setslice and delslice dirty tracking (#390) - Added Django 1.5 PY3 support (#392) - Added match ($elemMatch) support for EmbeddedDocuments (#379) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index a61d8fe8..a50450e3 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -54,7 +54,7 @@ be saved :: There is one caveat on Dynamic Documents: fields cannot start with `_` -Dynamic fields are stored in alphabetical order *after* any declared fields. +Dynamic fields are stored in creation order *after* any declared fields. Fields ====== diff --git a/docs/upgrade.rst b/docs/upgrade.rst index c3d31824..b8864b0d 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -2,6 +2,16 @@ Upgrading ######### + +0.8.2 to 0.8.2 +************** + +Minor change that may impact users: + +DynamicDocument fields are now stored in creation order after any declared +fields. Previously they were stored alphabetically. + + 0.7 to 0.8 ********** diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index ca154a29..04b0c050 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -42,6 +42,9 @@ class BaseDocument(object): # Combine positional arguments with named arguments. # We only want named arguments. field = iter(self._fields_ordered) + # If its an automatic id field then skip to the first defined field + if self._auto_id_field: + next(field) for value in args: name = next(field) if name in values: @@ -51,6 +54,7 @@ class BaseDocument(object): signals.pre_init.send(self.__class__, document=self, values=values) self._data = {} + self._dynamic_fields = SON() # Assign default values to instance for key, field in self._fields.iteritems(): @@ -61,7 +65,6 @@ class BaseDocument(object): # Set passed values after initialisation if self._dynamic: - self._dynamic_fields = {} dynamic_data = {} for key, value in values.iteritems(): if key in self._fields or key == '_id': @@ -116,6 +119,7 @@ class BaseDocument(object): field = DynamicField(db_field=name) field.name = name self._dynamic_fields[name] = field + self._fields_ordered += (name,) if not name.startswith('_'): value = self.__expand_dynamic_values(name, value) @@ -142,7 +146,8 @@ class BaseDocument(object): def __getstate__(self): data = {} - for k in ('_changed_fields', '_initialised', '_created'): + for k in ('_changed_fields', '_initialised', '_created', + '_dynamic_fields', '_fields_ordered'): if hasattr(self, k): data[k] = getattr(self, k) data['_data'] = self.to_mongo() @@ -151,21 +156,21 @@ class BaseDocument(object): def __setstate__(self, data): if isinstance(data["_data"], SON): data["_data"] = self.__class__._from_son(data["_data"])._data - for k in ('_changed_fields', '_initialised', '_created', '_data'): + for k in ('_changed_fields', '_initialised', '_created', '_data', + '_fields_ordered', '_dynamic_fields'): if k in data: setattr(self, k, data[k]) + for k in data.get('_dynamic_fields').keys(): + setattr(self, k, data["_data"].get(k)) def __iter__(self): - if 'id' in self._fields and 'id' not in self._fields_ordered: - return iter(('id', ) + self._fields_ordered) - return iter(self._fields_ordered) def __getitem__(self, name): """Dictionary-style field access, return a field's value if present. """ try: - if name in self._fields: + if name in self._fields_ordered: return getattr(self, name) except AttributeError: pass @@ -241,6 +246,8 @@ class BaseDocument(object): for field_name in self: value = self._data.get(field_name, None) field = self._fields.get(field_name) + if field is None and self._dynamic: + field = self._dynamic_fields.get(field_name) if value is not None: value = field.to_mongo(value) @@ -265,15 +272,6 @@ class BaseDocument(object): not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)): data.pop('_cls') - if not self._dynamic: - return data - - # Sort dynamic fields by key - dynamic_fields = sorted(self._dynamic_fields.iteritems(), - key=operator.itemgetter(0)) - for name, field in dynamic_fields: - data[name] = field.to_mongo(self._data.get(name, None)) - return data def validate(self, clean=True): @@ -289,11 +287,8 @@ class BaseDocument(object): errors[NON_FIELD_ERRORS] = error # Get a list of tuples of field names and their current values - fields = [(field, self._data.get(name)) - for name, field in self._fields.items()] - if self._dynamic: - fields += [(field, self._data.get(name)) - for name, field in self._dynamic_fields.items()] + fields = [(self._fields.get(name, self._dynamic_fields.get(name)), + self._data.get(name)) for name in self._fields_ordered] EmbeddedDocumentField = _import_class("EmbeddedDocumentField") GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField") @@ -406,11 +401,7 @@ class BaseDocument(object): return _changed_fields inspected.add(self.id) - field_list = self._fields.copy() - if self._dynamic: - field_list.update(self._dynamic_fields) - - for field_name in field_list: + for field_name in self._fields_ordered: db_field_name = self._db_field_map.get(field_name, field_name) key = '%s.' % db_field_name @@ -450,7 +441,6 @@ class BaseDocument(object): doc = self.to_mongo() set_fields = self._get_changed_fields() - set_data = {} unset_data = {} parts = [] if hasattr(self, '_changed_fields'): diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index 444d9a25..ff5afddf 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -91,11 +91,12 @@ class DocumentMetaclass(type): attrs['_fields'] = doc_fields attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k)) for k, v in doc_fields.iteritems()]) + attrs['_reverse_db_field_map'] = dict( + (v, k) for k, v in attrs['_db_field_map'].iteritems()) + attrs['_fields_ordered'] = tuple(i[1] for i in sorted( (v.creation_counter, v.name) for v in doc_fields.itervalues())) - attrs['_reverse_db_field_map'] = dict( - (v, k) for k, v in attrs['_db_field_map'].iteritems()) # # Set document hierarchy @@ -358,12 +359,18 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): new_class.id = field # Set primary key if not defined by the document + new_class._auto_id_field = False if not new_class._meta.get('id_field'): + 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'] + # 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 + # Merge in exceptions with parent hierarchy exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned) module = attrs.get('__module__') diff --git a/mongoengine/document.py b/mongoengine/document.py index ab8fa2ac..d0c9e616 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -460,11 +460,8 @@ class Document(BaseDocument): else: msg = "Reloaded document has been deleted" raise OperationError(msg) - for field in self._fields: + for field in self._fields_ordered: setattr(self, field, self._reload(field, obj[field])) - if self._dynamic: - for name in self._dynamic_fields.keys(): - setattr(self, name, self._reload(name, obj._data[name])) self._changed_fields = obj._changed_fields self._created = False return obj diff --git a/tests/document/delta.py b/tests/document/delta.py index 16ab609b..3656d9e3 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -3,6 +3,7 @@ import sys sys.path[0:0] = [""] import unittest +from bson import SON from mongoengine import * from mongoengine.connection import get_db @@ -613,13 +614,13 @@ class DeltaTest(unittest.TestCase): Person.drop_collection() p = Person(name="James", age=34) - self.assertEqual(p._delta(), ({'age': 34, 'name': 'James', - '_cls': 'Person'}, {})) + self.assertEqual(p._delta(), ( + SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {})) p.doc = 123 del(p.doc) - self.assertEqual(p._delta(), ({'age': 34, 'name': 'James', - '_cls': 'Person'}, {'doc': 1})) + self.assertEqual(p._delta(), ( + SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {})) p = Person() p.name = "Dean" @@ -631,14 +632,14 @@ class DeltaTest(unittest.TestCase): self.assertEqual(p._get_changed_fields(), ['age']) self.assertEqual(p._delta(), ({'age': 24}, {})) - p = self.Person.objects(age=22).get() + p = Person.objects(age=22).get() p.age = 24 self.assertEqual(p.age, 24) self.assertEqual(p._get_changed_fields(), ['age']) self.assertEqual(p._delta(), ({'age': 24}, {})) p.save() - self.assertEqual(1, self.Person.objects(age=24).count()) + self.assertEqual(1, Person.objects(age=24).count()) def test_dynamic_delta(self): diff --git a/tests/document/instance.py b/tests/document/instance.py index 81734aa0..e85c9d86 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -10,7 +10,8 @@ import uuid from datetime import datetime from bson import DBRef -from tests.fixtures import PickleEmbedded, PickleTest, PickleSignalsTest +from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, + PickleDyanmicEmbedded, PickleDynamicTest) from mongoengine import * from mongoengine.errors import (NotRegistered, InvalidDocumentError, @@ -1827,6 +1828,29 @@ class InstanceTest(unittest.TestCase): self.assertEqual(pickle_doc.string, "Two") self.assertEqual(pickle_doc.lists, ["1", "2", "3"]) + def test_dynamic_document_pickle(self): + + 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 + + pickle_doc.save() + + pickled_doc = pickle.dumps(pickle_doc) + resurrected = pickle.loads(pickled_doc) + + self.assertEqual(resurrected, pickle_doc) + self.assertEqual(resurrected._fields_ordered, + pickle_doc._fields_ordered) + self.assertEqual(resurrected._dynamic_fields.keys(), + pickle_doc._dynamic_fields.keys()) + + self.assertEqual(resurrected.embedded, pickle_doc.embedded) + self.assertEqual(resurrected.embedded._fields_ordered, + pickle_doc.embedded._fields_ordered) + self.assertEqual(resurrected.embedded._dynamic_fields.keys(), + pickle_doc.embedded._dynamic_fields.keys()) + def test_picklable_on_signals(self): pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() @@ -2289,6 +2313,16 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) + def test_mixed_creation_dynamic(self): + """Ensure that document may be created using mixed arguments. + """ + class Person(DynamicDocument): + name = StringField() + + person = Person("Test User", age=42) + self.assertEqual(person.name, "Test User") + self.assertEqual(person.age, 42) + def test_bad_mixed_creation(self): """Ensure that document gives correct error when duplicating arguments """ diff --git a/tests/fixtures.py b/tests/fixtures.py index e2070443..f1344d7c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -17,6 +17,14 @@ class PickleTest(Document): photo = FileField() +class PickleDyanmicEmbedded(DynamicEmbeddedDocument): + date = DateTimeField(default=datetime.now) + + +class PickleDynamicTest(DynamicDocument): + number = IntField() + + class PickleSignalsTest(Document): number = IntField() string = StringField(choices=(('One', '1'), ('Two', '2'))) From fa83fba6374d0da5774157012b20b74310c37984 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 11:18:49 +0000 Subject: [PATCH 021/222] Reload uses shard_key if applicable (#384) --- docs/changelog.rst | 1 + mongoengine/document.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 78deafb9..42fd9bb1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Reload uses shard_key if applicable (#384) - Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) - Fixed pickling dynamic documents `_dynamic_fields` (#387) - Fixed ListField setslice and delslice dirty tracking (#390) diff --git a/mongoengine/document.py b/mongoengine/document.py index d0c9e616..c9901a2a 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -266,7 +266,6 @@ class Document(BaseDocument): upsert=True, **write_concern) created = is_new_object(last_error) - if cascade is None: cascade = self._meta.get('cascade', False) or cascade_kwargs is not None @@ -451,9 +450,8 @@ class Document(BaseDocument): .. versionadded:: 0.1.2 .. versionchanged:: 0.6 Now chainable """ - id_field = self._meta['id_field'] obj = self._qs.read_preference(ReadPreference.PRIMARY).filter( - **{id_field: self[id_field]}).limit(1).select_related(max_depth=max_depth) + **self._object_key).limit(1).select_related(max_depth=max_depth) if obj: obj = obj[0] From 4209d61b1368717047927cee40a9d64768def93a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 12:49:19 +0000 Subject: [PATCH 022/222] Document.select_related() now respects `db_alias` (#377) --- docs/changelog.rst | 1 + mongoengine/document.py | 4 ++-- tests/fields/fields.py | 31 +++++++++++++++++++++++++++ tests/test_dereference.py | 45 +++++++++++++++++---------------------- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 42fd9bb1..926c6cb1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Document.select_related() now respects `db_alias` (#377) - Reload uses shard_key if applicable (#384) - Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) - Fixed pickling dynamic documents `_dynamic_fields` (#387) diff --git a/mongoengine/document.py b/mongoengine/document.py index c9901a2a..e331aa1f 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -440,8 +440,8 @@ class Document(BaseDocument): .. versionadded:: 0.5 """ - import dereference - self._data = dereference.DeReference()(self._data, max_depth) + DeReference = _import_class('DeReference') + DeReference()([self], max_depth + 1) return self def reload(self, max_depth=1): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 8f024991..b3d8d52e 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2474,6 +2474,37 @@ class FieldTest(unittest.TestCase): user = User(email='me@example.com') self.assertTrue(user.validate() is None) + def test_tuples_as_tuples(self): + """ + Ensure that tuples remain tuples when they are + inside a ComplexBaseField + """ + from mongoengine.base import BaseField + + class EnumField(BaseField): + + def __init__(self, **kwargs): + super(EnumField, self).__init__(**kwargs) + + def to_mongo(self, value): + return value + + def to_python(self, value): + return tuple(value) + + class TestDoc(Document): + items = ListField(EnumField()) + + TestDoc.drop_collection() + tuples = [(100, 'Testing')] + doc = TestDoc() + doc.items = tuples + doc.save() + x = TestDoc.objects().get() + self.assertTrue(x is not None) + self.assertTrue(len(x.items) == 1) + self.assertTrue(tuple(x.items[0]) in tuples) + self.assertTrue(x.items[0] in tuples) if __name__ == '__main__': unittest.main() diff --git a/tests/test_dereference.py b/tests/test_dereference.py index e146963f..db9868a0 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -1121,37 +1121,32 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) - def test_tuples_as_tuples(self): - """ - Ensure that tuples remain tuples when they are - inside a ComplexBaseField - """ - from mongoengine.base import BaseField + def test_objectid_reference_across_databases(self): + # mongoenginetest - Is default connection alias from setUp() + # Register Aliases + register_connection('testdb-1', 'mongoenginetest2') - class EnumField(BaseField): + class User(Document): + name = StringField() + meta = {"db_alias": "testdb-1"} - def __init__(self, **kwargs): - super(EnumField, self).__init__(**kwargs) + class Book(Document): + name = StringField() + author = ReferenceField(User) - def to_mongo(self, value): - return value + # Drops + User.drop_collection() + Book.drop_collection() - def to_python(self, value): - return tuple(value) + user = User(name="Ross").save() + Book(name="MongoEngine for pros", author=user).save() - class TestDoc(Document): - items = ListField(EnumField()) + # Can't use query_counter across databases - so test the _data object + book = Book.objects.first() + self.assertFalse(isinstance(book._data['author'], User)) - TestDoc.drop_collection() - tuples = [(100, 'Testing')] - doc = TestDoc() - doc.items = tuples - doc.save() - x = TestDoc.objects().get() - self.assertTrue(x is not None) - self.assertTrue(len(x.items) == 1) - self.assertTrue(tuple(x.items[0]) in tuples) - self.assertTrue(x.items[0] in tuples) + book.select_related() + self.assertTrue(isinstance(book._data['author'], User)) def test_non_ascii_pk(self): """ From f34e8a0ff6ca84d1afd8de0bf78f7696c0c7aed2 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 13:38:53 +0000 Subject: [PATCH 023/222] Fixed as_pymongo to return the id (#386) --- docs/changelog.rst | 1 + mongoengine/queryset/queryset.py | 3 ++- tests/queryset/queryset.py | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 926c6cb1..cbc2c945 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Fixed as_pymongo to return the id (#386) - Document.select_related() now respects `db_alias` (#377) - Reload uses shard_key if applicable (#384) - Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index ded8d5eb..c040e391 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1423,7 +1423,8 @@ class QuerySet(object): # used. If not, handle all fields. if not getattr(self, '__as_pymongo_fields', None): self.__as_pymongo_fields = [] - for field in self._loaded_fields.fields - set(['_cls', '_id']): + + for field in self._loaded_fields.fields - set(['_cls']): self.__as_pymongo_fields.append(field) while '.' in field: field, _ = field.rsplit('.', 1) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 4d91b55b..566c14e4 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3227,6 +3227,9 @@ class QuerySetTest(unittest.TestCase): User(name="Bob Dole", age=89, price=Decimal('1.11')).save() User(name="Barack Obama", age=51, price=Decimal('2.22')).save() + results = User.objects.only('id', 'name').as_pymongo() + self.assertEqual(results[0].keys(), ['_id', 'name']) + users = User.objects.only('name', 'price').as_pymongo() results = list(users) self.assertTrue(isinstance(results[0], dict)) From 8131f0a752d3e1b48713afe90301d77f224b9ace Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 13:53:18 +0000 Subject: [PATCH 024/222] Fixed sum and average mapreduce dot notation support (#375, #376) --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index cbc2c945..8bbc4b4f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Fixed sum and average mapreduce dot notation support (#375, #376) - Fixed as_pymongo to return the id (#386) - Document.select_related() now respects `db_alias` (#377) - Reload uses shard_key if applicable (#384) From daeecef59e47a5d23f9774f4dab70472b35465f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Wed, 10 Jul 2013 10:59:41 -0300 Subject: [PATCH 025/222] Update fields.py Typo in documentation for DecimalField --- mongoengine/fields.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 451f7aca..7f24be24 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -279,14 +279,14 @@ class DecimalField(BaseField): :param precision: Number of decimal places to store. :param rounding: The rounding rule from the python decimal libary: - - decimial.ROUND_CEILING (towards Infinity) - - decimial.ROUND_DOWN (towards zero) - - decimial.ROUND_FLOOR (towards -Infinity) - - decimial.ROUND_HALF_DOWN (to nearest with ties going towards zero) - - decimial.ROUND_HALF_EVEN (to nearest with ties going to nearest even integer) - - decimial.ROUND_HALF_UP (to nearest with ties going away from zero) - - decimial.ROUND_UP (away from zero) - - decimial.ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero) + - decimal.ROUND_CEILING (towards Infinity) + - decimal.ROUND_DOWN (towards zero) + - decimal.ROUND_FLOOR (towards -Infinity) + - decimal.ROUND_HALF_DOWN (to nearest with ties going towards zero) + - decimal.ROUND_HALF_EVEN (to nearest with ties going to nearest even integer) + - decimal.ROUND_HALF_UP (to nearest with ties going away from zero) + - decimal.ROUND_UP (away from zero) + - decimal.ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero) Defaults to: ``decimal.ROUND_HALF_UP`` From 634b874c469e9a91f199d74c4f71464ed1d20da1 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 16:16:50 +0000 Subject: [PATCH 026/222] Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) --- docs/apireference.rst | 5 + docs/changelog.rst | 1 + docs/guide/querying.rst | 4 +- mongoengine/queryset/base.py | 1479 ++++++++++++++++++++++++++++ mongoengine/queryset/queryset.py | 1545 ++---------------------------- tests/queryset/queryset.py | 30 +- 6 files changed, 1586 insertions(+), 1478 deletions(-) create mode 100644 mongoengine/queryset/base.py diff --git a/docs/apireference.rst b/docs/apireference.rst index d0627278..774d3b89 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -49,6 +49,11 @@ Querying .. automethod:: mongoengine.queryset.QuerySet.__call__ +.. autoclass:: mongoengine.queryset.QuerySetNoCache + :members: + + .. automethod:: mongoengine.queryset.QuerySetNoCache.__call__ + .. autofunction:: mongoengine.queryset.queryset_manager Fields diff --git a/docs/changelog.rst b/docs/changelog.rst index 8bbc4b4f..d875040b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) - Fixed sum and average mapreduce dot notation support (#375, #376) - Fixed as_pymongo to return the id (#386) - Document.select_related() now respects `db_alias` (#377) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 1350130e..5fd03601 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -16,7 +16,9 @@ fetch documents from the database:: .. note:: As of MongoEngine 0.8 the querysets utilise a local cache. So iterating - it multiple times will only cause a single query. + it multiple times will only cause a single query. If this is not the + desired behavour you can call :class:`~mongoengine.QuerySet.no_cache` to + return a non-caching queryset. Filtering queries ================= diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py new file mode 100644 index 00000000..0b2898f2 --- /dev/null +++ b/mongoengine/queryset/base.py @@ -0,0 +1,1479 @@ +from __future__ import absolute_import + +import copy +import itertools +import operator +import pprint +import re +import warnings + +from bson.code import Code +from bson import json_util +import pymongo +from pymongo.common import validate_read_preference + +from mongoengine import signals +from mongoengine.common import _import_class +from mongoengine.errors import (OperationError, NotUniqueError, + InvalidQueryError) + +from mongoengine.queryset import transform +from mongoengine.queryset.field_list import QueryFieldList +from mongoengine.queryset.visitor import Q, QNode + + +__all__ = ('BaseQuerySet', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL') + +# Delete rules +DO_NOTHING = 0 +NULLIFY = 1 +CASCADE = 2 +DENY = 3 +PULL = 4 + +RE_TYPE = type(re.compile('')) + + +class BaseQuerySet(object): + """A set of results returned from a query. Wraps a MongoDB cursor, + providing :class:`~mongoengine.Document` objects as the results. + """ + __dereference = False + _auto_dereference = True + + def __init__(self, document, collection): + self._document = document + self._collection_obj = collection + self._mongo_query = None + self._query_obj = Q() + self._initial_query = {} + self._where_clause = None + self._loaded_fields = QueryFieldList() + self._ordering = [] + self._snapshot = False + self._timeout = True + self._class_check = True + self._slave_okay = False + self._read_preference = None + self._iter = False + self._scalar = [] + self._none = False + self._as_pymongo = False + self._as_pymongo_coerce = False + self._len = None + + # If inheritance is allowed, only return instances and instances of + # subclasses of the class being used + if document._meta.get('allow_inheritance') is True: + if len(self._document._subclasses) == 1: + self._initial_query = {"_cls": self._document._subclasses[0]} + else: + self._initial_query = {"_cls": {"$in": self._document._subclasses}} + self._loaded_fields = QueryFieldList(always_include=['_cls']) + self._cursor_obj = None + self._limit = None + self._skip = None + self._hint = -1 # Using -1 as None is a valid value for hint + + def __call__(self, q_obj=None, class_check=True, slave_okay=False, + read_preference=None, **query): + """Filter the selected documents by calling the + :class:`~mongoengine.queryset.QuerySet` with a query. + + :param q_obj: a :class:`~mongoengine.queryset.Q` object to be used in + the query; the :class:`~mongoengine.queryset.QuerySet` is filtered + multiple times with different :class:`~mongoengine.queryset.Q` + objects, only the last one will be used + :param class_check: If set to False bypass class name check when + querying collection + :param slave_okay: if True, allows this query to be run against a + replica secondary. + :params read_preference: if set, overrides connection-level + read_preference from `ReplicaSetConnection`. + :param query: Django-style query keyword arguments + """ + query = Q(**query) + if q_obj: + # make sure proper query object is passed + if not isinstance(q_obj, QNode): + msg = ("Not a query object: %s. " + "Did you intend to use key=value?" % q_obj) + raise InvalidQueryError(msg) + query &= q_obj + + if read_preference is None: + queryset = self.clone() + else: + # Use the clone provided when setting read_preference + queryset = self.read_preference(read_preference) + + queryset._query_obj &= query + queryset._mongo_query = None + queryset._cursor_obj = None + queryset._class_check = class_check + + return queryset + + def __getitem__(self, key): + """Support skip and limit using getitem and slicing syntax. + """ + queryset = self.clone() + + # Slice provided + if isinstance(key, slice): + try: + queryset._cursor_obj = queryset._cursor[key] + queryset._skip, queryset._limit = key.start, key.stop + if key.start and key.stop: + queryset._limit = key.stop - key.start + except IndexError, err: + # PyMongo raises an error if key.start == key.stop, catch it, + # bin it, kill it. + start = key.start or 0 + if start >= 0 and key.stop >= 0 and key.step is None: + if start == key.stop: + queryset.limit(0) + queryset._skip = key.start + queryset._limit = key.stop - start + return queryset + raise err + # Allow further QuerySet modifications to be performed + return queryset + # Integer index provided + elif isinstance(key, int): + if queryset._scalar: + return queryset._get_scalar( + queryset._document._from_son(queryset._cursor[key], + _auto_dereference=self._auto_dereference)) + if queryset._as_pymongo: + return queryset._get_as_pymongo(queryset._cursor.next()) + return queryset._document._from_son(queryset._cursor[key], + _auto_dereference=self._auto_dereference) + raise AttributeError + + def __iter__(self): + raise NotImplementedError + + # Core functions + + def all(self): + """Returns all documents.""" + return self.__call__() + + def filter(self, *q_objs, **query): + """An alias of :meth:`~mongoengine.queryset.QuerySet.__call__` + """ + return self.__call__(*q_objs, **query) + + def get(self, *q_objs, **query): + """Retrieve the the matching object raising + :class:`~mongoengine.queryset.MultipleObjectsReturned` or + `DocumentName.MultipleObjectsReturned` exception if multiple results + and :class:`~mongoengine.queryset.DoesNotExist` or + `DocumentName.DoesNotExist` if no results are found. + + .. versionadded:: 0.3 + """ + queryset = self.clone() + queryset = queryset.limit(2) + queryset = queryset.filter(*q_objs, **query) + + try: + result = queryset.next() + except StopIteration: + msg = ("%s matching query does not exist." + % queryset._document._class_name) + raise queryset._document.DoesNotExist(msg) + try: + queryset.next() + except StopIteration: + return result + + queryset.rewind() + message = u'%d items returned, instead of 1' % queryset.count() + raise queryset._document.MultipleObjectsReturned(message) + + def create(self, **kwargs): + """Create new object. Returns the saved object instance. + + .. versionadded:: 0.4 + """ + 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 accidently 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. + """ + queryset = self.clone() + try: + result = queryset[0] + except IndexError: + result = None + return result + + def insert(self, doc_or_docs, load_bulk=True, write_concern=None): + """bulk insert documents + + :param docs_or_doc: a document or list of documents to be inserted + :param load_bulk (optional): If True returns the list of document + instances + :param write_concern: Extra keyword arguments are passed down to + :meth:`~pymongo.collection.Collection.insert` + which will be used as options for the resultant + ``getLastError`` command. For example, + ``insert(..., {w: 2, fsync: True})`` will wait until at least + two servers have recorded the write and will force an fsync on + each server being written to. + + By default returns document instances, set ``load_bulk`` to False to + return just ``ObjectIds`` + + .. versionadded:: 0.5 + """ + Document = _import_class('Document') + + if write_concern is None: + write_concern = {} + + docs = doc_or_docs + return_one = False + if isinstance(docs, Document) or issubclass(docs.__class__, Document): + return_one = True + docs = [docs] + + raw = [] + for doc in docs: + if not isinstance(doc, self._document): + msg = ("Some documents inserted aren't instances of %s" + % str(self._document)) + raise OperationError(msg) + if doc.pk and not doc._created: + msg = "Some documents have ObjectIds use doc.update() instead" + raise OperationError(msg) + raw.append(doc.to_mongo()) + + signals.pre_bulk_insert.send(self._document, documents=docs) + try: + ids = self._collection.insert(raw, **write_concern) + except pymongo.errors.OperationFailure, err: + message = 'Could not save document (%s)' + if re.match('^E1100[01] duplicate key', unicode(err)): + # E11000 - duplicate key error index + # E11001 - duplicate key on update + message = u'Tried to save duplicate unique keys (%s)' + raise NotUniqueError(message % unicode(err)) + raise OperationError(message % unicode(err)) + + if not load_bulk: + signals.post_bulk_insert.send( + self._document, documents=docs, loaded=False) + return return_one and ids[0] or ids + + documents = self.in_bulk(ids) + results = [] + for obj_id in ids: + results.append(documents.get(obj_id)) + signals.post_bulk_insert.send( + self._document, documents=results, loaded=True) + return return_one and results[0] or results + + def count(self, with_limit_and_skip=True): + """Count the selected elements in the query. + + :param with_limit_and_skip (optional): take any :meth:`limit` or + :meth:`skip` that has been applied to this cursor into account when + getting the count + """ + if self._limit == 0: + return 0 + if with_limit_and_skip and self._len is not None: + return self._len + count = self._cursor.count(with_limit_and_skip=with_limit_and_skip) + if with_limit_and_skip: + self._len = count + return count + + def delete(self, write_concern=None, _from_doc_delete=False): + """Delete the documents matched by the query. + + :param write_concern: Extra keyword arguments are passed down which + will be used as options for the resultant + ``getLastError`` command. For example, + ``save(..., write_concern={w: 2, fsync: True}, ...)`` will + wait until at least two servers have recorded the write and + will force an fsync on the primary server. + :param _from_doc_delete: True when called from document delete therefore + signals will have been triggered so don't loop. + """ + queryset = self.clone() + doc = queryset._document + + if write_concern is None: + write_concern = {} + + # Handle deletes where skips or limits have been applied or + # there is an untriggered delete signal + has_delete_signal = signals.signals_available and ( + signals.pre_delete.has_receivers_for(self._document) or + signals.post_delete.has_receivers_for(self._document)) + + call_document_delete = (queryset._skip or queryset._limit or + has_delete_signal) and not _from_doc_delete + + if call_document_delete: + for doc in queryset: + doc.delete(write_concern=write_concern) + return + + delete_rules = doc._meta.get('delete_rules') or {} + # Check for DENY rules before actually deleting/nullifying any other + # references + for rule_entry in delete_rules: + document_cls, field_name = rule_entry + rule = doc._meta['delete_rules'][rule_entry] + if rule == DENY and document_cls.objects( + **{field_name + '__in': self}).count() > 0: + msg = ("Could not delete document (%s.%s refers to it)" + % (document_cls.__name__, field_name)) + raise OperationError(msg) + + for rule_entry in delete_rules: + document_cls, field_name = rule_entry + rule = doc._meta['delete_rules'][rule_entry] + if rule == CASCADE: + ref_q = document_cls.objects(**{field_name + '__in': self}) + ref_q_count = ref_q.count() + if (doc != document_cls and ref_q_count > 0 + or (doc == document_cls and ref_q_count > 0)): + ref_q.delete(write_concern=write_concern) + elif rule == NULLIFY: + document_cls.objects(**{field_name + '__in': self}).update( + write_concern=write_concern, **{'unset__%s' % field_name: 1}) + elif rule == PULL: + document_cls.objects(**{field_name + '__in': self}).update( + write_concern=write_concern, + **{'pull_all__%s' % field_name: self}) + + queryset._collection.remove(queryset._query, write_concern=write_concern) + + def update(self, upsert=False, multi=True, write_concern=None, + full_result=False, **update): + """Perform an atomic update on the fields matched by the query. + + :param upsert: Any existing document with that "_id" is overwritten. + :param multi: Update multiple documents. + :param write_concern: Extra keyword arguments are passed down which + will be used as options for the resultant + ``getLastError`` command. For example, + ``save(..., write_concern={w: 2, fsync: True}, ...)`` will + wait until at least two servers have recorded the write and + will force an fsync on the primary server. + :param full_result: Return the full result rather than just the number + updated. + :param update: Django-style update keyword arguments + + .. versionadded:: 0.2 + """ + if not update and not upsert: + raise OperationError("No update parameters, would remove data") + + if write_concern is None: + write_concern = {} + + queryset = self.clone() + query = queryset._query + update = transform.update(queryset._document, **update) + + # If doing an atomic upsert on an inheritable class + # then ensure we add _cls to the update operation + if upsert and '_cls' in query: + if '$set' in update: + update["$set"]["_cls"] = queryset._document._class_name + else: + update["$set"] = {"_cls": queryset._document._class_name} + try: + result = queryset._collection.update(query, update, multi=multi, + upsert=upsert, **write_concern) + if full_result: + return result + elif result: + return result['n'] + except pymongo.errors.OperationFailure, err: + if unicode(err) == u'multi not coded yet': + message = u'update() method requires MongoDB 1.1.3+' + raise OperationError(message) + raise OperationError(u'Update failed (%s)' % unicode(err)) + + def update_one(self, upsert=False, write_concern=None, **update): + """Perform an atomic update on first field matched by the query. + + :param upsert: Any existing document with that "_id" is overwritten. + :param write_concern: Extra keyword arguments are passed down which + will be used as options for the resultant + ``getLastError`` command. For example, + ``save(..., write_concern={w: 2, fsync: True}, ...)`` will + wait until at least two servers have recorded the write and + will force an fsync on the primary server. + :param update: Django-style update keyword arguments + + .. versionadded:: 0.2 + """ + return self.update( + upsert=upsert, multi=False, write_concern=write_concern, **update) + + def with_id(self, object_id): + """Retrieve the object matching the id provided. Uses `object_id` only + and raises InvalidQueryError if a filter has been applied. Returns + `None` if no document exists with that id. + + :param object_id: the value for the id of the document to look up + + .. versionchanged:: 0.6 Raises InvalidQueryError if filter has been set + """ + queryset = self.clone() + if not queryset._query_obj.empty: + msg = "Cannot use a filter whilst using `with_id`" + raise InvalidQueryError(msg) + return queryset.filter(pk=object_id).first() + + def in_bulk(self, object_ids): + """Retrieve a set of documents by their ids. + + :param object_ids: a list or tuple of ``ObjectId``\ s + :rtype: dict of ObjectIds as keys and collection-specific + Document subclasses as values. + + .. versionadded:: 0.3 + """ + doc_map = {} + + docs = self._collection.find({'_id': {'$in': object_ids}}, + **self._cursor_args) + if self._scalar: + for doc in docs: + doc_map[doc['_id']] = self._get_scalar( + self._document._from_son(doc)) + elif self._as_pymongo: + for doc in docs: + doc_map[doc['_id']] = self._get_as_pymongo(doc) + else: + for doc in docs: + doc_map[doc['_id']] = self._document._from_son(doc) + + return doc_map + + def none(self): + """Helper that just returns a list""" + queryset = self.clone() + queryset._none = True + return queryset + + def no_sub_classes(self): + """ + Only return instances of this document and not any inherited documents + """ + if self._document._meta.get('allow_inheritance') is True: + self._initial_query = {"_cls": self._document._class_name} + + return self + + def clone(self): + """Creates a copy of the current + :class:`~mongoengine.queryset.QuerySet` + + .. versionadded:: 0.5 + """ + return self.clone_into(self.__class__(self._document, self._collection_obj)) + + def clone_into(self, cls): + """Creates a copy of the current + :class:`~mongoengine.queryset.base.BaseQuerySet` into another child class + """ + if not isinstance(cls, BaseQuerySet): + raise OperationError('%s is not a subclass of BaseQuerySet' % cls.__name__) + + copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj', + '_where_clause', '_loaded_fields', '_ordering', '_snapshot', + '_timeout', '_class_check', '_slave_okay', '_read_preference', + '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', + '_limit', '_skip', '_hint', '_auto_dereference') + + for prop in copy_props: + val = getattr(self, prop) + setattr(cls, prop, copy.copy(val)) + + if self._cursor_obj: + cls._cursor_obj = self._cursor_obj.clone() + + return cls + + def select_related(self, max_depth=1): + """Handles dereferencing of :class:`~bson.dbref.DBRef` objects or + :class:`~bson.object_id.ObjectId` a maximum depth in order to cut down + the number queries to mongodb. + + .. versionadded:: 0.5 + """ + # Make select related work the same for querysets + max_depth += 1 + queryset = self.clone() + return queryset._dereference(queryset, max_depth=max_depth) + + def limit(self, n): + """Limit the number of returned documents to `n`. This may also be + achieved using array-slicing syntax (e.g. ``User.objects[:5]``). + + :param n: the maximum number of objects to return + """ + queryset = self.clone() + if n == 0: + queryset._cursor.limit(1) + else: + queryset._cursor.limit(n) + queryset._limit = n + # Return self to allow chaining + return queryset + + def skip(self, n): + """Skip `n` documents before returning the results. This may also be + achieved using array-slicing syntax (e.g. ``User.objects[5:]``). + + :param n: the number of objects to skip before returning results + """ + queryset = self.clone() + queryset._cursor.skip(n) + queryset._skip = n + return queryset + + def hint(self, index=None): + """Added 'hint' support, telling Mongo the proper index to use for the + query. + + Judicious use of hints can greatly improve query performance. When + doing a query on multiple fields (at least one of which is indexed) + pass the indexed field as a hint to the query. + + Hinting will not do anything if the corresponding index does not exist. + The last hint applied to this cursor takes precedence over all others. + + .. versionadded:: 0.5 + """ + queryset = self.clone() + queryset._cursor.hint(index) + queryset._hint = index + return queryset + + def distinct(self, field): + """Return a list of distinct values for a given field. + + :param field: the field to select distinct values from + + .. note:: This is a command and won't take ordering or limit into + account. + + .. versionadded:: 0.4 + .. versionchanged:: 0.5 - Fixed handling references + .. versionchanged:: 0.6 - Improved db_field refrence handling + """ + queryset = self.clone() + try: + field = self._fields_to_dbfields([field]).pop() + finally: + return self._dereference(queryset._cursor.distinct(field), 1, + name=field, instance=self._document) + + def only(self, *fields): + """Load only a subset of this document's fields. :: + + post = BlogPost.objects(...).only("title", "author.name") + + .. note :: `only()` is chainable and will perform a union :: + So with the following it will fetch both: `title` and `author.name`:: + + post = BlogPost.objects.only("title").only("author.name") + + :func:`~mongoengine.queryset.QuerySet.all_fields` will reset any + field filters. + + :param fields: fields to include + + .. versionadded:: 0.3 + .. versionchanged:: 0.5 - Added subfield support + """ + fields = dict([(f, QueryFieldList.ONLY) for f in fields]) + return self.fields(True, **fields) + + def exclude(self, *fields): + """Opposite to .only(), exclude some document's fields. :: + + post = BlogPost.objects(...).exclude("comments") + + .. note :: `exclude()` is chainable and will perform a union :: + So with the following it will exclude both: `title` and `author.name`:: + + post = BlogPost.objects.exclude("title").exclude("author.name") + + :func:`~mongoengine.queryset.QuerySet.all_fields` will reset any + field filters. + + :param fields: fields to exclude + + .. versionadded:: 0.5 + """ + fields = dict([(f, QueryFieldList.EXCLUDE) for f in fields]) + return self.fields(**fields) + + def fields(self, _only_called=False, **kwargs): + """Manipulate how you load this document's fields. Used by `.only()` + and `.exclude()` to manipulate which fields to retrieve. Fields also + allows for a greater level of control for example: + + Retrieving a Subrange of Array Elements: + + You can use the $slice operator to retrieve a subrange of elements in + an array. For example to get the first 5 comments:: + + post = BlogPost.objects(...).fields(slice__comments=5) + + :param kwargs: A dictionary identifying what to include + + .. versionadded:: 0.5 + """ + + # Check for an operator and transform to mongo-style if there is + operators = ["slice"] + 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} + key = '.'.join(parts) + cleaned_fields.append((key, value)) + + fields = sorted(cleaned_fields, key=operator.itemgetter(1)) + queryset = self.clone() + for value, group in itertools.groupby(fields, lambda x: x[1]): + fields = [field for field, value in group] + fields = queryset._fields_to_dbfields(fields) + queryset._loaded_fields += QueryFieldList(fields, value=value, _only_called=_only_called) + + return queryset + + def all_fields(self): + """Include all fields. Reset all previously calls of .only() or + .exclude(). :: + + post = BlogPost.objects.exclude("comments").all_fields() + + .. versionadded:: 0.5 + """ + queryset = self.clone() + queryset._loaded_fields = QueryFieldList( + always_include=queryset._loaded_fields.always_include) + return queryset + + def order_by(self, *keys): + """Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The + order may be specified by prepending each of the keys by a + or a -. + Ascending order is assumed. + + :param keys: fields to order the query results by; keys may be + prefixed with **+** or **-** to determine the ordering direction + """ + queryset = self.clone() + queryset._ordering = queryset._get_order_by(keys) + return queryset + + def explain(self, format=False): + """Return an explain plan record for the + :class:`~mongoengine.queryset.QuerySet`\ 's cursor. + + :param format: format the plan before returning it + """ + plan = self._cursor.explain() + if format: + plan = pprint.pformat(plan) + return plan + + def snapshot(self, enabled): + """Enable or disable snapshot mode when querying. + + :param enabled: whether or not snapshot mode is enabled + + ..versionchanged:: 0.5 - made chainable + """ + queryset = self.clone() + queryset._snapshot = enabled + return queryset + + def timeout(self, enabled): + """Enable or disable the default mongod timeout when querying. + + :param enabled: whether or not the timeout is used + + ..versionchanged:: 0.5 - made chainable + """ + queryset = self.clone() + queryset._timeout = enabled + return queryset + + def slave_okay(self, enabled): + """Enable or disable the slave_okay when querying. + + :param enabled: whether or not the slave_okay is enabled + """ + queryset = self.clone() + queryset._slave_okay = enabled + return queryset + + def read_preference(self, read_preference): + """Change the read_preference when querying. + + :param read_preference: override ReplicaSetConnection-level + preference. + """ + validate_read_preference('read_preference', read_preference) + queryset = self.clone() + queryset._read_preference = read_preference + return queryset + + def scalar(self, *fields): + """Instead of returning Document instances, return either a specific + value or a tuple of values in order. + + Can be used along with + :func:`~mongoengine.queryset.QuerySet.no_dereference` to turn off + dereferencing. + + .. note:: This effects all results and can be unset by calling + ``scalar`` without arguments. Calls ``only`` automatically. + + :param fields: One or more fields to return instead of a Document. + """ + queryset = self.clone() + queryset._scalar = list(fields) + + if fields: + queryset = queryset.only(*fields) + else: + queryset = queryset.all_fields() + + return queryset + + def values_list(self, *fields): + """An alias for scalar""" + return self.scalar(*fields) + + def as_pymongo(self, coerce_types=False): + """Instead of returning Document instances, return raw values from + pymongo. + + :param coerce_type: Field types (if applicable) would be use to + coerce types. + """ + queryset = self.clone() + queryset._as_pymongo = True + queryset._as_pymongo_coerce = coerce_types + return queryset + + # JSON Helpers + + def to_json(self): + """Converts a queryset to JSON""" + return json_util.dumps(self.as_pymongo()) + + def from_json(self, json_data): + """Converts json data to unsaved objects""" + son_data = json_util.loads(json_data) + return [self._document._from_son(data) for data in son_data] + + # JS functionality + + def map_reduce(self, map_f, reduce_f, output, finalize_f=None, limit=None, + scope=None): + """Perform a map/reduce query using the current query spec + and ordering. While ``map_reduce`` respects ``QuerySet`` chaining, + it must be the last call made, as it does not return a maleable + ``QuerySet``. + + See the :meth:`~mongoengine.tests.QuerySetTest.test_map_reduce` + and :meth:`~mongoengine.tests.QuerySetTest.test_map_advanced` + tests in ``tests.queryset.QuerySetTest`` for usage examples. + + :param map_f: map function, as :class:`~bson.code.Code` or string + :param reduce_f: reduce function, as + :class:`~bson.code.Code` or string + :param output: output collection name, if set to 'inline' will try to + use :class:`~pymongo.collection.Collection.inline_map_reduce` + This can also be a dictionary containing output options + see: http://docs.mongodb.org/manual/reference/commands/#mapReduce + :param finalize_f: finalize function, an optional function that + performs any post-reduction processing. + :param scope: values to insert into map/reduce global scope. Optional. + :param limit: number of objects from current query to provide + to map/reduce method + + Returns an iterator yielding + :class:`~mongoengine.document.MapReduceDocument`. + + .. note:: + + Map/Reduce changed in server version **>= 1.7.4**. The PyMongo + :meth:`~pymongo.collection.Collection.map_reduce` helper requires + PyMongo version **>= 1.11**. + + .. versionchanged:: 0.5 + - removed ``keep_temp`` keyword argument, which was only relevant + for MongoDB server versions older than 1.7.4 + + .. versionadded:: 0.3 + """ + queryset = self.clone() + + MapReduceDocument = _import_class('MapReduceDocument') + + if not hasattr(self._collection, "map_reduce"): + raise NotImplementedError("Requires MongoDB >= 1.7.1") + + map_f_scope = {} + if isinstance(map_f, Code): + map_f_scope = map_f.scope + map_f = unicode(map_f) + map_f = Code(queryset._sub_js_fields(map_f), map_f_scope) + + reduce_f_scope = {} + if isinstance(reduce_f, Code): + reduce_f_scope = reduce_f.scope + reduce_f = unicode(reduce_f) + reduce_f_code = queryset._sub_js_fields(reduce_f) + reduce_f = Code(reduce_f_code, reduce_f_scope) + + mr_args = {'query': queryset._query} + + if finalize_f: + finalize_f_scope = {} + if isinstance(finalize_f, Code): + finalize_f_scope = finalize_f.scope + finalize_f = unicode(finalize_f) + finalize_f_code = queryset._sub_js_fields(finalize_f) + finalize_f = Code(finalize_f_code, finalize_f_scope) + mr_args['finalize'] = finalize_f + + if scope: + mr_args['scope'] = scope + + if limit: + mr_args['limit'] = limit + + if output == 'inline' and not queryset._ordering: + map_reduce_function = 'inline_map_reduce' + else: + map_reduce_function = 'map_reduce' + mr_args['out'] = output + + results = getattr(queryset._collection, map_reduce_function)( + map_f, reduce_f, **mr_args) + + if map_reduce_function == 'map_reduce': + results = results.find() + + if queryset._ordering: + results = results.sort(queryset._ordering) + + for doc in results: + yield MapReduceDocument(queryset._document, queryset._collection, + doc['_id'], doc['value']) + + def exec_js(self, code, *fields, **options): + """Execute a Javascript function on the server. A list of fields may be + provided, which will be translated to their correct names and supplied + as the arguments to the function. A few extra variables are added to + the function's scope: ``collection``, which is the name of the + collection in use; ``query``, which is an object representing the + current query; and ``options``, which is an object containing any + options specified as keyword arguments. + + As fields in MongoEngine may use different names in the database (set + using the :attr:`db_field` keyword argument to a :class:`Field` + constructor), a mechanism exists for replacing MongoEngine field names + with the database field names in Javascript code. When accessing a + field, use square-bracket notation, and prefix the MongoEngine field + name with a tilde (~). + + :param code: a string of Javascript code to execute + :param fields: fields that you will be using in your function, which + will be passed in to your function as arguments + :param options: options that you want available to the function + (accessed in Javascript through the ``options`` object) + """ + queryset = self.clone() + + code = queryset._sub_js_fields(code) + + fields = [queryset._document._translate_field_name(f) for f in fields] + collection = queryset._document._get_collection_name() + + scope = { + 'collection': collection, + 'options': options or {}, + } + + query = queryset._query + if queryset._where_clause: + query['$where'] = queryset._where_clause + + scope['query'] = query + code = Code(code, scope=scope) + + db = queryset._document._get_db() + return db.eval(code, *fields) + + def where(self, where_clause): + """Filter ``QuerySet`` results with a ``$where`` clause (a Javascript + expression). Performs automatic field name substitution like + :meth:`mongoengine.queryset.Queryset.exec_js`. + + .. note:: When using this mode of query, the database will call your + function, or evaluate your predicate clause, for each object + in the collection. + + .. versionadded:: 0.5 + """ + queryset = self.clone() + where_clause = queryset._sub_js_fields(where_clause) + queryset._where_clause = where_clause + return queryset + + def 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 + + .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work + with sharding. + """ + map_func = Code(""" + function() { + function deepFind(obj, path) { + var paths = path.split('.') + , current = obj + , i; + + for (i = 0; i < paths.length; ++i) { + if (current[paths[i]] == undefined) { + return undefined; + } else { + current = current[paths[i]]; + } + } + return current; + } + + emit(1, deepFind(this, field) || 0); + } + """, scope={'field': field}) + + reduce_func = Code(""" + function(key, values) { + var sum = 0; + for (var i in values) { + sum += values[i]; + } + return sum; + } + """) + + for result in self.map_reduce(map_func, reduce_func, output='inline'): + return result.value + else: + return 0 + + def 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 + + .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work + with sharding. + """ + map_func = Code(""" + function() { + function deepFind(obj, path) { + var paths = path.split('.') + , current = obj + , i; + + for (i = 0; i < paths.length; ++i) { + if (current[paths[i]] == undefined) { + return undefined; + } else { + current = current[paths[i]]; + } + } + return current; + } + + val = deepFind(this, field) + if (val !== undefined) + emit(1, {t: val || 0, c: 1}); + } + """, scope={'field': field}) + + reduce_func = Code(""" + function(key, values) { + var out = {t: 0, c: 0}; + for (var i in values) { + var value = values[i]; + out.t += value.t; + out.c += value.c; + } + return out; + } + """) + + finalize_func = Code(""" + function(key, value) { + return value.t / value.c; + } + """) + + for result in self.map_reduce(map_func, reduce_func, + finalize_f=finalize_func, output='inline'): + return result.value + else: + 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. + This is useful for generating tag clouds, or searching documents. + + .. note:: + + Can only do direct simple mappings and cannot map across + :class:`~mongoengine.fields.ReferenceField` or + :class:`~mongoengine.fields.GenericReferenceField` for more complex + counting a manual map reduce call would is required. + + If the field is a :class:`~mongoengine.fields.ListField`, the items within + each list will be counted individually. + + :param field: the field to use + :param normalize: normalize the results so they add to 1.0 + :param map_reduce: Use map_reduce over exec_js + + .. versionchanged:: 0.5 defaults to map_reduce and can handle embedded + document lookups + """ + if map_reduce: + return self._item_frequencies_map_reduce(field, + normalize=normalize) + return self._item_frequencies_exec_js(field, normalize=normalize) + + # Iterator helpers + + def next(self): + """Wrap the result in a :class:`~mongoengine.Document` object. + """ + if self._limit == 0 or self._none: + raise StopIteration + + raw_doc = self._cursor.next() + if self._as_pymongo: + return self._get_as_pymongo(raw_doc) + doc = self._document._from_son(raw_doc, + _auto_dereference=self._auto_dereference) + if self._scalar: + return self._get_scalar(doc) + + return doc + + def rewind(self): + """Rewind the cursor to its unevaluated state. + + .. versionadded:: 0.3 + """ + self._iter = False + self._cursor.rewind() + + # Properties + + @property + def _collection(self): + """Property that returns the collection object. This allows us to + perform operations only if the collection is accessed. + """ + return self._collection_obj + + @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 + else: + cursor_args['slave_okay'] = self._slave_okay + if self._loaded_fields: + cursor_args['fields'] = self._loaded_fields.as_dict() + return cursor_args + + @property + def _cursor(self): + if self._cursor_obj is None: + + self._cursor_obj = self._collection.find(self._query, + **self._cursor_args) + # Apply where clauses to cursor + if self._where_clause: + where_clause = self._sub_js_fields(self._where_clause) + self._cursor_obj.where(where_clause) + + if self._ordering: + # Apply query ordering + self._cursor_obj.sort(self._ordering) + elif self._document._meta['ordering']: + # Otherwise, apply the ordering from the document model + order = self._get_order_by(self._document._meta['ordering']) + self._cursor_obj.sort(order) + + if self._limit is not None: + self._cursor_obj.limit(self._limit) + + if self._skip is not None: + self._cursor_obj.skip(self._skip) + + if self._hint != -1: + self._cursor_obj.hint(self._hint) + + return self._cursor_obj + + def __deepcopy__(self, memo): + """Essential for chained queries with ReferenceFields involved""" + return self.clone() + + @property + def _query(self): + if self._mongo_query is None: + self._mongo_query = self._query_obj.to_query(self._document) + if self._class_check: + self._mongo_query.update(self._initial_query) + return self._mongo_query + + @property + def _dereference(self): + if not self.__dereference: + self.__dereference = _import_class('DeReference')() + return self.__dereference + + def no_dereference(self): + """Turn off any dereferencing for the results of this queryset. + """ + queryset = self.clone() + queryset._auto_dereference = False + return queryset + + # Helper Functions + + def _item_frequencies_map_reduce(self, field, normalize=False): + map_func = """ + function() { + var path = '{{~%(field)s}}'.split('.'); + var field = this; + + for (p in path) { + if (typeof field != 'undefined') + field = field[path[p]]; + else + break; + } + if (field && field.constructor == Array) { + field.forEach(function(item) { + emit(item, 1); + }); + } else if (typeof field != 'undefined') { + emit(field, 1); + } else { + emit(null, 1); + } + } + """ % dict(field=field) + reduce_func = """ + function(key, values) { + var total = 0; + var valuesSize = values.length; + for (var i=0; i < valuesSize; i++) { + total += parseInt(values[i], 10); + } + return total; + } + """ + values = self.map_reduce(map_func, reduce_func, 'inline') + frequencies = {} + for f in values: + key = f.key + if isinstance(key, float): + if int(key) == key: + key = int(key) + frequencies[key] = int(f.value) + + if normalize: + count = sum(frequencies.values()) + frequencies = dict([(k, float(v) / count) + for k, v in frequencies.items()]) + + return frequencies + + def _item_frequencies_exec_js(self, field, normalize=False): + """Uses exec_js to execute""" + freq_func = """ + function(path) { + var path = path.split('.'); + + var total = 0.0; + db[collection].find(query).forEach(function(doc) { + var field = doc; + for (p in path) { + if (field) + field = field[path[p]]; + else + break; + } + if (field && field.constructor == Array) { + total += field.length; + } else { + total++; + } + }); + + var frequencies = {}; + var types = {}; + var inc = 1.0; + + db[collection].find(query).forEach(function(doc) { + field = doc; + for (p in path) { + if (field) + field = field[path[p]]; + else + break; + } + if (field && field.constructor == Array) { + field.forEach(function(item) { + frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]); + }); + } else { + var item = field; + types[item] = item; + frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]); + } + }); + return [total, frequencies, types]; + } + """ + total, data, types = self.exec_js(freq_func, field) + values = dict([(types.get(k), int(v)) for k, v in data.iteritems()]) + + if normalize: + values = dict([(k, float(v) / total) for k, v in values.items()]) + + frequencies = {} + for k, v in values.iteritems(): + if isinstance(k, float): + if int(k) == k: + k = int(k) + + frequencies[k] = v + + return frequencies + + def _fields_to_dbfields(self, fields): + """Translate fields paths to its db equivalents""" + ret = [] + for field in fields: + field = ".".join(f.db_field for f in + self._document._lookup_field(field.split('.'))) + ret.append(field) + return ret + + def _get_order_by(self, keys): + """Creates a list of order by fields + """ + key_list = [] + for key in keys: + if not key: + continue + direction = pymongo.ASCENDING + if key[0] == '-': + direction = pymongo.DESCENDING + if key[0] in ('-', '+'): + key = key[1:] + key = key.replace('__', '.') + try: + key = self._document._translate_field_name(key) + except: + pass + key_list.append((key, direction)) + + if self._cursor_obj: + self._cursor_obj.sort(key_list) + return key_list + + def _get_scalar(self, doc): + + def lookup(obj, name): + chunks = name.split('__') + for chunk in chunks: + obj = getattr(obj, chunk) + return obj + + data = [lookup(doc, n) for n in self._scalar] + if len(data) == 1: + return data[0] + + return tuple(data) + + def _get_as_pymongo(self, row): + # Extract which fields paths we should follow if .fields(...) was + # used. If not, handle all fields. + if not getattr(self, '__as_pymongo_fields', None): + self.__as_pymongo_fields = [] + + for field in self._loaded_fields.fields - set(['_cls']): + self.__as_pymongo_fields.append(field) + while '.' in field: + field, _ = field.rsplit('.', 1) + self.__as_pymongo_fields.append(field) + + all_fields = not self.__as_pymongo_fields + + def clean(data, path=None): + path = path or '' + + if isinstance(data, dict): + new_data = {} + for key, value in data.iteritems(): + new_path = '%s.%s' % (path, key) if path else key + + if all_fields: + include_field = True + elif self._loaded_fields.value == QueryFieldList.ONLY: + include_field = new_path in self.__as_pymongo_fields + else: + include_field = new_path not in self.__as_pymongo_fields + + if include_field: + new_data[key] = clean(value, path=new_path) + data = new_data + elif isinstance(data, list): + data = [clean(d, path=path) for d in data] + else: + if self._as_pymongo_coerce: + # If we need to coerce types, we need to determine the + # type of this field and use the corresponding + # .to_python(...) + from mongoengine.fields import EmbeddedDocumentField + obj = self._document + for chunk in path.split('.'): + obj = getattr(obj, chunk, None) + if obj is None: + break + elif isinstance(obj, EmbeddedDocumentField): + obj = obj.document_type + if obj and data is not None: + data = obj.to_python(data) + return data + return clean(row) + + def _sub_js_fields(self, code): + """When fields are specified with [~fieldname] syntax, where + *fieldname* is the Python name of a field, *fieldname* will be + substituted for the MongoDB name of the field (specified using the + :attr:`name` keyword argument in a field's constructor). + """ + def field_sub(match): + # Extract just the field name, and look up the field objects + field_name = match.group(1).split('.') + fields = self._document._lookup_field(field_name) + # Substitute the correct name for the field into the javascript + return u'["%s"]' % fields[-1].db_field + + def field_path_sub(match): + # Extract just the field name, and look up the field objects + field_name = match.group(1).split('.') + fields = self._document._lookup_field(field_name) + # Substitute the correct name for the field into the javascript + return ".".join([f.db_field for f in fields]) + + code = re.sub(u'\[\s*~([A-z_][A-z_0-9.]+?)\s*\]', field_sub, code) + code = re.sub(u'\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}', field_path_sub, + code) + return code + + # Deprecated + def ensure_index(self, **kwargs): + """Deprecated use :func:`Document.ensure_index`""" + msg = ("Doc.objects()._ensure_index() is deprecated. " + "Use Doc.ensure_index() instead.") + warnings.warn(msg, DeprecationWarning) + self._document.__class__.ensure_index(**kwargs) + return self + + def _ensure_indexes(self): + """Deprecated use :func:`~Document.ensure_indexes`""" + msg = ("Doc.objects()._ensure_indexes() is deprecated. " + "Use Doc.ensure_indexes() instead.") + warnings.warn(msg, DeprecationWarning) + self._document.__class__.ensure_indexes() \ No newline at end of file diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 690e3f03..9db98a7e 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1,137 +1,26 @@ -from __future__ import absolute_import +from mongoengine.errors import OperationError +from mongoengine.queryset.base import (BaseQuerySet, DO_NOTHING, NULLIFY, + CASCADE, DENY, PULL) -import copy -import itertools -import operator -import pprint -import re -import warnings - -from bson.code import Code -from bson import json_util -import pymongo -from pymongo.common import validate_read_preference - -from mongoengine import signals -from mongoengine.common import _import_class -from mongoengine.errors import (OperationError, NotUniqueError, - InvalidQueryError) - -from mongoengine.queryset import transform -from mongoengine.queryset.field_list import QueryFieldList -from mongoengine.queryset.visitor import Q, QNode - - -__all__ = ('QuerySet', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL') +__all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE', + 'DENY', 'PULL') # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 ITER_CHUNK_SIZE = 100 -# Delete rules -DO_NOTHING = 0 -NULLIFY = 1 -CASCADE = 2 -DENY = 3 -PULL = 4 -RE_TYPE = type(re.compile('')) +class QuerySet(BaseQuerySet): + """The default queryset, that builds queries and handles a set of results + returned from a query. - -class QuerySet(object): - """A set of results returned from a query. Wraps a MongoDB cursor, - providing :class:`~mongoengine.Document` objects as the results. + Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as + the results. """ - __dereference = False - _auto_dereference = True - def __init__(self, document, collection): - self._document = document - self._collection_obj = collection - self._mongo_query = None - self._query_obj = Q() - self._initial_query = {} - self._where_clause = None - self._loaded_fields = QueryFieldList() - self._ordering = [] - self._snapshot = False - self._timeout = True - self._class_check = True - self._slave_okay = False - self._read_preference = None - self._iter = False - self._scalar = [] - self._none = False - self._as_pymongo = False - self._as_pymongo_coerce = False - self._result_cache = [] - self._has_more = True - self._len = None - - # If inheritance is allowed, only return instances and instances of - # subclasses of the class being used - if document._meta.get('allow_inheritance') is True: - if len(self._document._subclasses) == 1: - self._initial_query = {"_cls": self._document._subclasses[0]} - else: - self._initial_query = {"_cls": {"$in": self._document._subclasses}} - self._loaded_fields = QueryFieldList(always_include=['_cls']) - self._cursor_obj = None - self._limit = None - self._skip = None - self._hint = -1 # Using -1 as None is a valid value for hint - - def __call__(self, q_obj=None, class_check=True, slave_okay=False, - read_preference=None, **query): - """Filter the selected documents by calling the - :class:`~mongoengine.queryset.QuerySet` with a query. - - :param q_obj: a :class:`~mongoengine.queryset.Q` object to be used in - the query; the :class:`~mongoengine.queryset.QuerySet` is filtered - multiple times with different :class:`~mongoengine.queryset.Q` - objects, only the last one will be used - :param class_check: If set to False bypass class name check when - querying collection - :param slave_okay: if True, allows this query to be run against a - replica secondary. - :params read_preference: if set, overrides connection-level - read_preference from `ReplicaSetConnection`. - :param query: Django-style query keyword arguments - """ - query = Q(**query) - if q_obj: - # make sure proper query object is passed - if not isinstance(q_obj, QNode): - msg = ("Not a query object: %s. " - "Did you intend to use key=value?" % q_obj) - raise InvalidQueryError(msg) - query &= q_obj - - if read_preference is None: - queryset = self.clone() - else: - # Use the clone provided when setting read_preference - queryset = self.read_preference(read_preference) - - queryset._query_obj &= query - queryset._mongo_query = None - queryset._cursor_obj = None - queryset._class_check = class_check - - return queryset - - def __len__(self): - """Since __len__ is called quite frequently (for example, as part of - list(qs) we populate the result cache and cache the length. - """ - if self._len is not None: - return self._len - if self._has_more: - # populate the cache - list(self._iter_results()) - - self._len = len(self._result_cache) - return self._len + _has_more = True + _len = None + _result_cache = None def __iter__(self): """Iteration utilises a results cache which iterates the cursor @@ -147,11 +36,39 @@ class QuerySet(object): # iterating over the cache. return iter(self._result_cache) + def __len__(self): + """Since __len__ is called quite frequently (for example, as part of + list(qs) we populate the result cache and cache the length. + """ + if self._len is not None: + return self._len + if self._has_more: + # populate the cache + list(self._iter_results()) + + self._len = len(self._result_cache) + return self._len + + def __repr__(self): + """Provides the string representation of the QuerySet + """ + if self._iter: + return '.. queryset mid-iteration ..' + + self._populate_cache() + data = self._result_cache[:REPR_OUTPUT_SIZE + 1] + if len(data) > REPR_OUTPUT_SIZE: + data[-1] = "...(remaining elements truncated)..." + return repr(data) + + def _iter_results(self): """A generator for iterating over the result cache. Also populates the cache if there are more possible results to yield. Raises StopIteration when there are no more results""" + if self._result_cache is None: + self._result_cache = [] pos = 0 while True: upper = len(self._result_cache) @@ -168,6 +85,8 @@ class QuerySet(object): Populates the result cache with ``ITER_CHUNK_SIZE`` more entries (until the cursor is exhausted). """ + if self._result_cache is None: + self._result_cache = [] if self._has_more: try: for i in xrange(ITER_CHUNK_SIZE): @@ -175,1369 +94,43 @@ class QuerySet(object): except StopIteration: self._has_more = False - def __getitem__(self, key): - """Support skip and limit using getitem and slicing syntax. - """ - queryset = self.clone() + def no_cache(self): + """Convert to a non_caching queryset""" + if self._result_cache is not None: + raise OperationError("QuerySet already cached") + return self.clone_into(QuerySetNoCache(self._document, self._collection)) - # Slice provided - if isinstance(key, slice): - try: - queryset._cursor_obj = queryset._cursor[key] - queryset._skip, queryset._limit = key.start, key.stop - if key.start and key.stop: - queryset._limit = key.stop - key.start - except IndexError, err: - # PyMongo raises an error if key.start == key.stop, catch it, - # bin it, kill it. - start = key.start or 0 - if start >= 0 and key.stop >= 0 and key.step is None: - if start == key.stop: - queryset.limit(0) - queryset._skip = key.start - queryset._limit = key.stop - start - return queryset - raise err - # Allow further QuerySet modifications to be performed - return queryset - # Integer index provided - elif isinstance(key, int): - if queryset._scalar: - return queryset._get_scalar( - queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference)) - if queryset._as_pymongo: - return queryset._get_as_pymongo(queryset._cursor.next()) - return queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference) - raise AttributeError + +class QuerySetNoCache(BaseQuerySet): + """A non caching QuerySet""" + + def cache(self): + """Convert to a caching queryset""" + return self.clone_into(QuerySet(self._document, self._collection)) def __repr__(self): """Provides the string representation of the QuerySet - """ + .. versionchanged:: 0.6.13 Now doesnt modify the cursor + """ if self._iter: return '.. queryset mid-iteration ..' - self._populate_cache() - data = self._result_cache[:REPR_OUTPUT_SIZE + 1] + data = [] + for i in xrange(REPR_OUTPUT_SIZE + 1): + try: + data.append(self.next()) + except StopIteration: + break if len(data) > REPR_OUTPUT_SIZE: data[-1] = "...(remaining elements truncated)..." + + self.rewind() return repr(data) - # Core functions - - def all(self): - """Returns all documents.""" - return self.__call__() - - def filter(self, *q_objs, **query): - """An alias of :meth:`~mongoengine.queryset.QuerySet.__call__` - """ - return self.__call__(*q_objs, **query) - - def get(self, *q_objs, **query): - """Retrieve the the matching object raising - :class:`~mongoengine.queryset.MultipleObjectsReturned` or - `DocumentName.MultipleObjectsReturned` exception if multiple results - and :class:`~mongoengine.queryset.DoesNotExist` or - `DocumentName.DoesNotExist` if no results are found. - - .. versionadded:: 0.3 - """ - queryset = self.clone() - queryset = queryset.limit(2) - queryset = queryset.filter(*q_objs, **query) - - try: - result = queryset.next() - except StopIteration: - msg = ("%s matching query does not exist." - % queryset._document._class_name) - raise queryset._document.DoesNotExist(msg) - try: - queryset.next() - except StopIteration: - return result - + def __iter__(self): + queryset = self + if queryset._iter: + queryset = self.clone() queryset.rewind() - message = u'%d items returned, instead of 1' % queryset.count() - raise queryset._document.MultipleObjectsReturned(message) - - def create(self, **kwargs): - """Create new object. Returns the saved object instance. - - .. versionadded:: 0.4 - """ - 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 accidently 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. - """ - queryset = self.clone() - try: - result = queryset[0] - except IndexError: - result = None - return result - - def insert(self, doc_or_docs, load_bulk=True, write_concern=None): - """bulk insert documents - - :param docs_or_doc: a document or list of documents to be inserted - :param load_bulk (optional): If True returns the list of document - instances - :param write_concern: Extra keyword arguments are passed down to - :meth:`~pymongo.collection.Collection.insert` - which will be used as options for the resultant - ``getLastError`` command. For example, - ``insert(..., {w: 2, fsync: True})`` will wait until at least - two servers have recorded the write and will force an fsync on - each server being written to. - - By default returns document instances, set ``load_bulk`` to False to - return just ``ObjectIds`` - - .. versionadded:: 0.5 - """ - Document = _import_class('Document') - - if write_concern is None: - write_concern = {} - - docs = doc_or_docs - return_one = False - if isinstance(docs, Document) or issubclass(docs.__class__, Document): - return_one = True - docs = [docs] - - raw = [] - for doc in docs: - if not isinstance(doc, self._document): - msg = ("Some documents inserted aren't instances of %s" - % str(self._document)) - raise OperationError(msg) - if doc.pk and not doc._created: - msg = "Some documents have ObjectIds use doc.update() instead" - raise OperationError(msg) - raw.append(doc.to_mongo()) - - signals.pre_bulk_insert.send(self._document, documents=docs) - try: - ids = self._collection.insert(raw, **write_concern) - except pymongo.errors.OperationFailure, err: - message = 'Could not save document (%s)' - if re.match('^E1100[01] duplicate key', unicode(err)): - # E11000 - duplicate key error index - # E11001 - duplicate key on update - message = u'Tried to save duplicate unique keys (%s)' - raise NotUniqueError(message % unicode(err)) - raise OperationError(message % unicode(err)) - - if not load_bulk: - signals.post_bulk_insert.send( - self._document, documents=docs, loaded=False) - return return_one and ids[0] or ids - - documents = self.in_bulk(ids) - results = [] - for obj_id in ids: - results.append(documents.get(obj_id)) - signals.post_bulk_insert.send( - self._document, documents=results, loaded=True) - return return_one and results[0] or results - - def count(self, with_limit_and_skip=True): - """Count the selected elements in the query. - - :param with_limit_and_skip (optional): take any :meth:`limit` or - :meth:`skip` that has been applied to this cursor into account when - getting the count - """ - if self._limit == 0: - return 0 - if with_limit_and_skip and self._len is not None: - return self._len - count = self._cursor.count(with_limit_and_skip=with_limit_and_skip) - if with_limit_and_skip: - self._len = count - return count - - def delete(self, write_concern=None, _from_doc_delete=False): - """Delete the documents matched by the query. - - :param write_concern: Extra keyword arguments are passed down which - will be used as options for the resultant - ``getLastError`` command. For example, - ``save(..., write_concern={w: 2, fsync: True}, ...)`` will - wait until at least two servers have recorded the write and - will force an fsync on the primary server. - :param _from_doc_delete: True when called from document delete therefore - signals will have been triggered so don't loop. - """ - queryset = self.clone() - doc = queryset._document - - if write_concern is None: - write_concern = {} - - # Handle deletes where skips or limits have been applied or - # there is an untriggered delete signal - has_delete_signal = signals.signals_available and ( - signals.pre_delete.has_receivers_for(self._document) or - signals.post_delete.has_receivers_for(self._document)) - - call_document_delete = (queryset._skip or queryset._limit or - has_delete_signal) and not _from_doc_delete - - if call_document_delete: - for doc in queryset: - doc.delete(write_concern=write_concern) - return - - delete_rules = doc._meta.get('delete_rules') or {} - # Check for DENY rules before actually deleting/nullifying any other - # references - for rule_entry in delete_rules: - document_cls, field_name = rule_entry - rule = doc._meta['delete_rules'][rule_entry] - if rule == DENY and document_cls.objects( - **{field_name + '__in': self}).count() > 0: - msg = ("Could not delete document (%s.%s refers to it)" - % (document_cls.__name__, field_name)) - raise OperationError(msg) - - for rule_entry in delete_rules: - document_cls, field_name = rule_entry - rule = doc._meta['delete_rules'][rule_entry] - if rule == CASCADE: - ref_q = document_cls.objects(**{field_name + '__in': self}) - ref_q_count = ref_q.count() - if (doc != document_cls and ref_q_count > 0 - or (doc == document_cls and ref_q_count > 0)): - ref_q.delete(write_concern=write_concern) - elif rule == NULLIFY: - document_cls.objects(**{field_name + '__in': self}).update( - write_concern=write_concern, **{'unset__%s' % field_name: 1}) - elif rule == PULL: - document_cls.objects(**{field_name + '__in': self}).update( - write_concern=write_concern, - **{'pull_all__%s' % field_name: self}) - - queryset._collection.remove(queryset._query, write_concern=write_concern) - - def update(self, upsert=False, multi=True, write_concern=None, - full_result=False, **update): - """Perform an atomic update on the fields matched by the query. - - :param upsert: Any existing document with that "_id" is overwritten. - :param multi: Update multiple documents. - :param write_concern: Extra keyword arguments are passed down which - will be used as options for the resultant - ``getLastError`` command. For example, - ``save(..., write_concern={w: 2, fsync: True}, ...)`` will - wait until at least two servers have recorded the write and - will force an fsync on the primary server. - :param full_result: Return the full result rather than just the number - updated. - :param update: Django-style update keyword arguments - - .. versionadded:: 0.2 - """ - if not update and not upsert: - raise OperationError("No update parameters, would remove data") - - if write_concern is None: - write_concern = {} - - queryset = self.clone() - query = queryset._query - update = transform.update(queryset._document, **update) - - # If doing an atomic upsert on an inheritable class - # then ensure we add _cls to the update operation - if upsert and '_cls' in query: - if '$set' in update: - update["$set"]["_cls"] = queryset._document._class_name - else: - update["$set"] = {"_cls": queryset._document._class_name} - try: - result = queryset._collection.update(query, update, multi=multi, - upsert=upsert, **write_concern) - if full_result: - return result - elif result: - return result['n'] - except pymongo.errors.OperationFailure, err: - if unicode(err) == u'multi not coded yet': - message = u'update() method requires MongoDB 1.1.3+' - raise OperationError(message) - raise OperationError(u'Update failed (%s)' % unicode(err)) - - def update_one(self, upsert=False, write_concern=None, **update): - """Perform an atomic update on first field matched by the query. - - :param upsert: Any existing document with that "_id" is overwritten. - :param write_concern: Extra keyword arguments are passed down which - will be used as options for the resultant - ``getLastError`` command. For example, - ``save(..., write_concern={w: 2, fsync: True}, ...)`` will - wait until at least two servers have recorded the write and - will force an fsync on the primary server. - :param update: Django-style update keyword arguments - - .. versionadded:: 0.2 - """ - return self.update( - upsert=upsert, multi=False, write_concern=write_concern, **update) - - def with_id(self, object_id): - """Retrieve the object matching the id provided. Uses `object_id` only - and raises InvalidQueryError if a filter has been applied. Returns - `None` if no document exists with that id. - - :param object_id: the value for the id of the document to look up - - .. versionchanged:: 0.6 Raises InvalidQueryError if filter has been set - """ - queryset = self.clone() - if not queryset._query_obj.empty: - msg = "Cannot use a filter whilst using `with_id`" - raise InvalidQueryError(msg) - return queryset.filter(pk=object_id).first() - - def in_bulk(self, object_ids): - """Retrieve a set of documents by their ids. - - :param object_ids: a list or tuple of ``ObjectId``\ s - :rtype: dict of ObjectIds as keys and collection-specific - Document subclasses as values. - - .. versionadded:: 0.3 - """ - doc_map = {} - - docs = self._collection.find({'_id': {'$in': object_ids}}, - **self._cursor_args) - if self._scalar: - for doc in docs: - doc_map[doc['_id']] = self._get_scalar( - self._document._from_son(doc)) - elif self._as_pymongo: - for doc in docs: - doc_map[doc['_id']] = self._get_as_pymongo(doc) - else: - for doc in docs: - doc_map[doc['_id']] = self._document._from_son(doc) - - return doc_map - - def none(self): - """Helper that just returns a list""" - queryset = self.clone() - queryset._none = True return queryset - - def no_sub_classes(self): - """ - Only return instances of this document and not any inherited documents - """ - if self._document._meta.get('allow_inheritance') is True: - self._initial_query = {"_cls": self._document._class_name} - - return self - - def clone(self): - """Creates a copy of the current - :class:`~mongoengine.queryset.QuerySet` - - .. versionadded:: 0.5 - """ - c = self.__class__(self._document, self._collection_obj) - - copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj', - '_where_clause', '_loaded_fields', '_ordering', '_snapshot', - '_timeout', '_class_check', '_slave_okay', '_read_preference', - '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', - '_limit', '_skip', '_hint', '_auto_dereference') - - for prop in copy_props: - val = getattr(self, prop) - setattr(c, prop, copy.copy(val)) - - if self._cursor_obj: - c._cursor_obj = self._cursor_obj.clone() - - return c - - def select_related(self, max_depth=1): - """Handles dereferencing of :class:`~bson.dbref.DBRef` objects or - :class:`~bson.object_id.ObjectId` a maximum depth in order to cut down - the number queries to mongodb. - - .. versionadded:: 0.5 - """ - # Make select related work the same for querysets - max_depth += 1 - queryset = self.clone() - return queryset._dereference(queryset, max_depth=max_depth) - - def limit(self, n): - """Limit the number of returned documents to `n`. This may also be - achieved using array-slicing syntax (e.g. ``User.objects[:5]``). - - :param n: the maximum number of objects to return - """ - queryset = self.clone() - if n == 0: - queryset._cursor.limit(1) - else: - queryset._cursor.limit(n) - queryset._limit = n - # Return self to allow chaining - return queryset - - def skip(self, n): - """Skip `n` documents before returning the results. This may also be - achieved using array-slicing syntax (e.g. ``User.objects[5:]``). - - :param n: the number of objects to skip before returning results - """ - queryset = self.clone() - queryset._cursor.skip(n) - queryset._skip = n - return queryset - - def hint(self, index=None): - """Added 'hint' support, telling Mongo the proper index to use for the - query. - - Judicious use of hints can greatly improve query performance. When - doing a query on multiple fields (at least one of which is indexed) - pass the indexed field as a hint to the query. - - Hinting will not do anything if the corresponding index does not exist. - The last hint applied to this cursor takes precedence over all others. - - .. versionadded:: 0.5 - """ - queryset = self.clone() - queryset._cursor.hint(index) - queryset._hint = index - return queryset - - def distinct(self, field): - """Return a list of distinct values for a given field. - - :param field: the field to select distinct values from - - .. note:: This is a command and won't take ordering or limit into - account. - - .. versionadded:: 0.4 - .. versionchanged:: 0.5 - Fixed handling references - .. versionchanged:: 0.6 - Improved db_field refrence handling - """ - queryset = self.clone() - try: - field = self._fields_to_dbfields([field]).pop() - finally: - return self._dereference(queryset._cursor.distinct(field), 1, - name=field, instance=self._document) - - def only(self, *fields): - """Load only a subset of this document's fields. :: - - post = BlogPost.objects(...).only("title", "author.name") - - .. note :: `only()` is chainable and will perform a union :: - So with the following it will fetch both: `title` and `author.name`:: - - post = BlogPost.objects.only("title").only("author.name") - - :func:`~mongoengine.queryset.QuerySet.all_fields` will reset any - field filters. - - :param fields: fields to include - - .. versionadded:: 0.3 - .. versionchanged:: 0.5 - Added subfield support - """ - fields = dict([(f, QueryFieldList.ONLY) for f in fields]) - return self.fields(True, **fields) - - def exclude(self, *fields): - """Opposite to .only(), exclude some document's fields. :: - - post = BlogPost.objects(...).exclude("comments") - - .. note :: `exclude()` is chainable and will perform a union :: - So with the following it will exclude both: `title` and `author.name`:: - - post = BlogPost.objects.exclude("title").exclude("author.name") - - :func:`~mongoengine.queryset.QuerySet.all_fields` will reset any - field filters. - - :param fields: fields to exclude - - .. versionadded:: 0.5 - """ - fields = dict([(f, QueryFieldList.EXCLUDE) for f in fields]) - return self.fields(**fields) - - def fields(self, _only_called=False, **kwargs): - """Manipulate how you load this document's fields. Used by `.only()` - and `.exclude()` to manipulate which fields to retrieve. Fields also - allows for a greater level of control for example: - - Retrieving a Subrange of Array Elements: - - You can use the $slice operator to retrieve a subrange of elements in - an array. For example to get the first 5 comments:: - - post = BlogPost.objects(...).fields(slice__comments=5) - - :param kwargs: A dictionary identifying what to include - - .. versionadded:: 0.5 - """ - - # Check for an operator and transform to mongo-style if there is - operators = ["slice"] - 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} - key = '.'.join(parts) - cleaned_fields.append((key, value)) - - fields = sorted(cleaned_fields, key=operator.itemgetter(1)) - queryset = self.clone() - for value, group in itertools.groupby(fields, lambda x: x[1]): - fields = [field for field, value in group] - fields = queryset._fields_to_dbfields(fields) - queryset._loaded_fields += QueryFieldList(fields, value=value, _only_called=_only_called) - - return queryset - - def all_fields(self): - """Include all fields. Reset all previously calls of .only() or - .exclude(). :: - - post = BlogPost.objects.exclude("comments").all_fields() - - .. versionadded:: 0.5 - """ - queryset = self.clone() - queryset._loaded_fields = QueryFieldList( - always_include=queryset._loaded_fields.always_include) - return queryset - - def order_by(self, *keys): - """Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The - order may be specified by prepending each of the keys by a + or a -. - Ascending order is assumed. - - :param keys: fields to order the query results by; keys may be - prefixed with **+** or **-** to determine the ordering direction - """ - queryset = self.clone() - queryset._ordering = queryset._get_order_by(keys) - return queryset - - def explain(self, format=False): - """Return an explain plan record for the - :class:`~mongoengine.queryset.QuerySet`\ 's cursor. - - :param format: format the plan before returning it - """ - plan = self._cursor.explain() - if format: - plan = pprint.pformat(plan) - return plan - - def snapshot(self, enabled): - """Enable or disable snapshot mode when querying. - - :param enabled: whether or not snapshot mode is enabled - - ..versionchanged:: 0.5 - made chainable - """ - queryset = self.clone() - queryset._snapshot = enabled - return queryset - - def timeout(self, enabled): - """Enable or disable the default mongod timeout when querying. - - :param enabled: whether or not the timeout is used - - ..versionchanged:: 0.5 - made chainable - """ - queryset = self.clone() - queryset._timeout = enabled - return queryset - - def slave_okay(self, enabled): - """Enable or disable the slave_okay when querying. - - :param enabled: whether or not the slave_okay is enabled - """ - queryset = self.clone() - queryset._slave_okay = enabled - return queryset - - def read_preference(self, read_preference): - """Change the read_preference when querying. - - :param read_preference: override ReplicaSetConnection-level - preference. - """ - validate_read_preference('read_preference', read_preference) - queryset = self.clone() - queryset._read_preference = read_preference - return queryset - - def scalar(self, *fields): - """Instead of returning Document instances, return either a specific - value or a tuple of values in order. - - Can be used along with - :func:`~mongoengine.queryset.QuerySet.no_dereference` to turn off - dereferencing. - - .. note:: This effects all results and can be unset by calling - ``scalar`` without arguments. Calls ``only`` automatically. - - :param fields: One or more fields to return instead of a Document. - """ - queryset = self.clone() - queryset._scalar = list(fields) - - if fields: - queryset = queryset.only(*fields) - else: - queryset = queryset.all_fields() - - return queryset - - def values_list(self, *fields): - """An alias for scalar""" - return self.scalar(*fields) - - def as_pymongo(self, coerce_types=False): - """Instead of returning Document instances, return raw values from - pymongo. - - :param coerce_type: Field types (if applicable) would be use to - coerce types. - """ - queryset = self.clone() - queryset._as_pymongo = True - queryset._as_pymongo_coerce = coerce_types - return queryset - - # JSON Helpers - - def to_json(self): - """Converts a queryset to JSON""" - return json_util.dumps(self.as_pymongo()) - - def from_json(self, json_data): - """Converts json data to unsaved objects""" - son_data = json_util.loads(json_data) - return [self._document._from_son(data) for data in son_data] - - # JS functionality - - def map_reduce(self, map_f, reduce_f, output, finalize_f=None, limit=None, - scope=None): - """Perform a map/reduce query using the current query spec - and ordering. While ``map_reduce`` respects ``QuerySet`` chaining, - it must be the last call made, as it does not return a maleable - ``QuerySet``. - - See the :meth:`~mongoengine.tests.QuerySetTest.test_map_reduce` - and :meth:`~mongoengine.tests.QuerySetTest.test_map_advanced` - tests in ``tests.queryset.QuerySetTest`` for usage examples. - - :param map_f: map function, as :class:`~bson.code.Code` or string - :param reduce_f: reduce function, as - :class:`~bson.code.Code` or string - :param output: output collection name, if set to 'inline' will try to - use :class:`~pymongo.collection.Collection.inline_map_reduce` - This can also be a dictionary containing output options - see: http://docs.mongodb.org/manual/reference/commands/#mapReduce - :param finalize_f: finalize function, an optional function that - performs any post-reduction processing. - :param scope: values to insert into map/reduce global scope. Optional. - :param limit: number of objects from current query to provide - to map/reduce method - - Returns an iterator yielding - :class:`~mongoengine.document.MapReduceDocument`. - - .. note:: - - Map/Reduce changed in server version **>= 1.7.4**. The PyMongo - :meth:`~pymongo.collection.Collection.map_reduce` helper requires - PyMongo version **>= 1.11**. - - .. versionchanged:: 0.5 - - removed ``keep_temp`` keyword argument, which was only relevant - for MongoDB server versions older than 1.7.4 - - .. versionadded:: 0.3 - """ - queryset = self.clone() - - MapReduceDocument = _import_class('MapReduceDocument') - - if not hasattr(self._collection, "map_reduce"): - raise NotImplementedError("Requires MongoDB >= 1.7.1") - - map_f_scope = {} - if isinstance(map_f, Code): - map_f_scope = map_f.scope - map_f = unicode(map_f) - map_f = Code(queryset._sub_js_fields(map_f), map_f_scope) - - reduce_f_scope = {} - if isinstance(reduce_f, Code): - reduce_f_scope = reduce_f.scope - reduce_f = unicode(reduce_f) - reduce_f_code = queryset._sub_js_fields(reduce_f) - reduce_f = Code(reduce_f_code, reduce_f_scope) - - mr_args = {'query': queryset._query} - - if finalize_f: - finalize_f_scope = {} - if isinstance(finalize_f, Code): - finalize_f_scope = finalize_f.scope - finalize_f = unicode(finalize_f) - finalize_f_code = queryset._sub_js_fields(finalize_f) - finalize_f = Code(finalize_f_code, finalize_f_scope) - mr_args['finalize'] = finalize_f - - if scope: - mr_args['scope'] = scope - - if limit: - mr_args['limit'] = limit - - if output == 'inline' and not queryset._ordering: - map_reduce_function = 'inline_map_reduce' - else: - map_reduce_function = 'map_reduce' - mr_args['out'] = output - - results = getattr(queryset._collection, map_reduce_function)( - map_f, reduce_f, **mr_args) - - if map_reduce_function == 'map_reduce': - results = results.find() - - if queryset._ordering: - results = results.sort(queryset._ordering) - - for doc in results: - yield MapReduceDocument(queryset._document, queryset._collection, - doc['_id'], doc['value']) - - def exec_js(self, code, *fields, **options): - """Execute a Javascript function on the server. A list of fields may be - provided, which will be translated to their correct names and supplied - as the arguments to the function. A few extra variables are added to - the function's scope: ``collection``, which is the name of the - collection in use; ``query``, which is an object representing the - current query; and ``options``, which is an object containing any - options specified as keyword arguments. - - As fields in MongoEngine may use different names in the database (set - using the :attr:`db_field` keyword argument to a :class:`Field` - constructor), a mechanism exists for replacing MongoEngine field names - with the database field names in Javascript code. When accessing a - field, use square-bracket notation, and prefix the MongoEngine field - name with a tilde (~). - - :param code: a string of Javascript code to execute - :param fields: fields that you will be using in your function, which - will be passed in to your function as arguments - :param options: options that you want available to the function - (accessed in Javascript through the ``options`` object) - """ - queryset = self.clone() - - code = queryset._sub_js_fields(code) - - fields = [queryset._document._translate_field_name(f) for f in fields] - collection = queryset._document._get_collection_name() - - scope = { - 'collection': collection, - 'options': options or {}, - } - - query = queryset._query - if queryset._where_clause: - query['$where'] = queryset._where_clause - - scope['query'] = query - code = Code(code, scope=scope) - - db = queryset._document._get_db() - return db.eval(code, *fields) - - def where(self, where_clause): - """Filter ``QuerySet`` results with a ``$where`` clause (a Javascript - expression). Performs automatic field name substitution like - :meth:`mongoengine.queryset.Queryset.exec_js`. - - .. note:: When using this mode of query, the database will call your - function, or evaluate your predicate clause, for each object - in the collection. - - .. versionadded:: 0.5 - """ - queryset = self.clone() - where_clause = queryset._sub_js_fields(where_clause) - queryset._where_clause = where_clause - return queryset - - def 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 - - .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work - with sharding. - """ - map_func = Code(""" - function() { - function deepFind(obj, path) { - var paths = path.split('.') - , current = obj - , i; - - for (i = 0; i < paths.length; ++i) { - if (current[paths[i]] == undefined) { - return undefined; - } else { - current = current[paths[i]]; - } - } - return current; - } - - emit(1, deepFind(this, field) || 0); - } - """, scope={'field': field}) - - reduce_func = Code(""" - function(key, values) { - var sum = 0; - for (var i in values) { - sum += values[i]; - } - return sum; - } - """) - - for result in self.map_reduce(map_func, reduce_func, output='inline'): - return result.value - else: - return 0 - - def 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 - - .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work - with sharding. - """ - map_func = Code(""" - function() { - function deepFind(obj, path) { - var paths = path.split('.') - , current = obj - , i; - - for (i = 0; i < paths.length; ++i) { - if (current[paths[i]] == undefined) { - return undefined; - } else { - current = current[paths[i]]; - } - } - return current; - } - - val = deepFind(this, field) - if (val !== undefined) - emit(1, {t: val || 0, c: 1}); - } - """, scope={'field': field}) - - reduce_func = Code(""" - function(key, values) { - var out = {t: 0, c: 0}; - for (var i in values) { - var value = values[i]; - out.t += value.t; - out.c += value.c; - } - return out; - } - """) - - finalize_func = Code(""" - function(key, value) { - return value.t / value.c; - } - """) - - for result in self.map_reduce(map_func, reduce_func, - finalize_f=finalize_func, output='inline'): - return result.value - else: - 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. - This is useful for generating tag clouds, or searching documents. - - .. note:: - - Can only do direct simple mappings and cannot map across - :class:`~mongoengine.fields.ReferenceField` or - :class:`~mongoengine.fields.GenericReferenceField` for more complex - counting a manual map reduce call would is required. - - If the field is a :class:`~mongoengine.fields.ListField`, the items within - each list will be counted individually. - - :param field: the field to use - :param normalize: normalize the results so they add to 1.0 - :param map_reduce: Use map_reduce over exec_js - - .. versionchanged:: 0.5 defaults to map_reduce and can handle embedded - document lookups - """ - if map_reduce: - return self._item_frequencies_map_reduce(field, - normalize=normalize) - return self._item_frequencies_exec_js(field, normalize=normalize) - - # Iterator helpers - - def next(self): - """Wrap the result in a :class:`~mongoengine.Document` object. - """ - if self._limit == 0 or self._none: - raise StopIteration - - raw_doc = self._cursor.next() - if self._as_pymongo: - return self._get_as_pymongo(raw_doc) - doc = self._document._from_son(raw_doc, - _auto_dereference=self._auto_dereference) - if self._scalar: - return self._get_scalar(doc) - - return doc - - def rewind(self): - """Rewind the cursor to its unevaluated state. - - .. versionadded:: 0.3 - """ - self._iter = False - self._cursor.rewind() - - # Properties - - @property - def _collection(self): - """Property that returns the collection object. This allows us to - perform operations only if the collection is accessed. - """ - return self._collection_obj - - @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 - else: - cursor_args['slave_okay'] = self._slave_okay - if self._loaded_fields: - cursor_args['fields'] = self._loaded_fields.as_dict() - return cursor_args - - @property - def _cursor(self): - if self._cursor_obj is None: - - self._cursor_obj = self._collection.find(self._query, - **self._cursor_args) - # Apply where clauses to cursor - if self._where_clause: - where_clause = self._sub_js_fields(self._where_clause) - self._cursor_obj.where(where_clause) - - if self._ordering: - # Apply query ordering - self._cursor_obj.sort(self._ordering) - elif self._document._meta['ordering']: - # Otherwise, apply the ordering from the document model - order = self._get_order_by(self._document._meta['ordering']) - self._cursor_obj.sort(order) - - if self._limit is not None: - self._cursor_obj.limit(self._limit) - - if self._skip is not None: - self._cursor_obj.skip(self._skip) - - if self._hint != -1: - self._cursor_obj.hint(self._hint) - - return self._cursor_obj - - def __deepcopy__(self, memo): - """Essential for chained queries with ReferenceFields involved""" - return self.clone() - - @property - def _query(self): - if self._mongo_query is None: - self._mongo_query = self._query_obj.to_query(self._document) - if self._class_check: - self._mongo_query.update(self._initial_query) - return self._mongo_query - - @property - def _dereference(self): - if not self.__dereference: - self.__dereference = _import_class('DeReference')() - return self.__dereference - - def no_dereference(self): - """Turn off any dereferencing for the results of this queryset. - """ - queryset = self.clone() - queryset._auto_dereference = False - return queryset - - # Helper Functions - - def _item_frequencies_map_reduce(self, field, normalize=False): - map_func = """ - function() { - var path = '{{~%(field)s}}'.split('.'); - var field = this; - - for (p in path) { - if (typeof field != 'undefined') - field = field[path[p]]; - else - break; - } - if (field && field.constructor == Array) { - field.forEach(function(item) { - emit(item, 1); - }); - } else if (typeof field != 'undefined') { - emit(field, 1); - } else { - emit(null, 1); - } - } - """ % dict(field=field) - reduce_func = """ - function(key, values) { - var total = 0; - var valuesSize = values.length; - for (var i=0; i < valuesSize; i++) { - total += parseInt(values[i], 10); - } - return total; - } - """ - values = self.map_reduce(map_func, reduce_func, 'inline') - frequencies = {} - for f in values: - key = f.key - if isinstance(key, float): - if int(key) == key: - key = int(key) - frequencies[key] = int(f.value) - - if normalize: - count = sum(frequencies.values()) - frequencies = dict([(k, float(v) / count) - for k, v in frequencies.items()]) - - return frequencies - - def _item_frequencies_exec_js(self, field, normalize=False): - """Uses exec_js to execute""" - freq_func = """ - function(path) { - var path = path.split('.'); - - var total = 0.0; - db[collection].find(query).forEach(function(doc) { - var field = doc; - for (p in path) { - if (field) - field = field[path[p]]; - else - break; - } - if (field && field.constructor == Array) { - total += field.length; - } else { - total++; - } - }); - - var frequencies = {}; - var types = {}; - var inc = 1.0; - - db[collection].find(query).forEach(function(doc) { - field = doc; - for (p in path) { - if (field) - field = field[path[p]]; - else - break; - } - if (field && field.constructor == Array) { - field.forEach(function(item) { - frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]); - }); - } else { - var item = field; - types[item] = item; - frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]); - } - }); - return [total, frequencies, types]; - } - """ - total, data, types = self.exec_js(freq_func, field) - values = dict([(types.get(k), int(v)) for k, v in data.iteritems()]) - - if normalize: - values = dict([(k, float(v) / total) for k, v in values.items()]) - - frequencies = {} - for k, v in values.iteritems(): - if isinstance(k, float): - if int(k) == k: - k = int(k) - - frequencies[k] = v - - return frequencies - - def _fields_to_dbfields(self, fields): - """Translate fields paths to its db equivalents""" - ret = [] - for field in fields: - field = ".".join(f.db_field for f in - self._document._lookup_field(field.split('.'))) - ret.append(field) - return ret - - def _get_order_by(self, keys): - """Creates a list of order by fields - """ - key_list = [] - for key in keys: - if not key: - continue - direction = pymongo.ASCENDING - if key[0] == '-': - direction = pymongo.DESCENDING - if key[0] in ('-', '+'): - key = key[1:] - key = key.replace('__', '.') - try: - key = self._document._translate_field_name(key) - except: - pass - key_list.append((key, direction)) - - if self._cursor_obj: - self._cursor_obj.sort(key_list) - return key_list - - def _get_scalar(self, doc): - - def lookup(obj, name): - chunks = name.split('__') - for chunk in chunks: - obj = getattr(obj, chunk) - return obj - - data = [lookup(doc, n) for n in self._scalar] - if len(data) == 1: - return data[0] - - return tuple(data) - - def _get_as_pymongo(self, row): - # Extract which fields paths we should follow if .fields(...) was - # used. If not, handle all fields. - if not getattr(self, '__as_pymongo_fields', None): - self.__as_pymongo_fields = [] - - for field in self._loaded_fields.fields - set(['_cls']): - self.__as_pymongo_fields.append(field) - while '.' in field: - field, _ = field.rsplit('.', 1) - self.__as_pymongo_fields.append(field) - - all_fields = not self.__as_pymongo_fields - - def clean(data, path=None): - path = path or '' - - if isinstance(data, dict): - new_data = {} - for key, value in data.iteritems(): - new_path = '%s.%s' % (path, key) if path else key - - if all_fields: - include_field = True - elif self._loaded_fields.value == QueryFieldList.ONLY: - include_field = new_path in self.__as_pymongo_fields - else: - include_field = new_path not in self.__as_pymongo_fields - - if include_field: - new_data[key] = clean(value, path=new_path) - data = new_data - elif isinstance(data, list): - data = [clean(d, path=path) for d in data] - else: - if self._as_pymongo_coerce: - # If we need to coerce types, we need to determine the - # type of this field and use the corresponding - # .to_python(...) - from mongoengine.fields import EmbeddedDocumentField - obj = self._document - for chunk in path.split('.'): - obj = getattr(obj, chunk, None) - if obj is None: - break - elif isinstance(obj, EmbeddedDocumentField): - obj = obj.document_type - if obj and data is not None: - data = obj.to_python(data) - return data - return clean(row) - - def _sub_js_fields(self, code): - """When fields are specified with [~fieldname] syntax, where - *fieldname* is the Python name of a field, *fieldname* will be - substituted for the MongoDB name of the field (specified using the - :attr:`name` keyword argument in a field's constructor). - """ - def field_sub(match): - # Extract just the field name, and look up the field objects - field_name = match.group(1).split('.') - fields = self._document._lookup_field(field_name) - # Substitute the correct name for the field into the javascript - return u'["%s"]' % fields[-1].db_field - - def field_path_sub(match): - # Extract just the field name, and look up the field objects - field_name = match.group(1).split('.') - fields = self._document._lookup_field(field_name) - # Substitute the correct name for the field into the javascript - return ".".join([f.db_field for f in fields]) - - code = re.sub(u'\[\s*~([A-z_][A-z_0-9.]+?)\s*\]', field_sub, code) - code = re.sub(u'\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}', field_path_sub, - code) - return code - - # Deprecated - def ensure_index(self, **kwargs): - """Deprecated use :func:`Document.ensure_index`""" - msg = ("Doc.objects()._ensure_index() is deprecated. " - "Use Doc.ensure_index() instead.") - warnings.warn(msg, DeprecationWarning) - self._document.__class__.ensure_index(**kwargs) - return self - - def _ensure_indexes(self): - """Deprecated use :func:`~Document.ensure_indexes`""" - msg = ("Doc.objects()._ensure_indexes() is deprecated. " - "Use Doc.ensure_indexes() instead.") - warnings.warn(msg, DeprecationWarning) - self._document.__class__.ensure_indexes() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 9495a25e..6e3eb9bf 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3254,7 +3254,7 @@ class QuerySetTest(unittest.TestCase): User(name="Barack Obama", age=51, price=Decimal('2.22')).save() results = User.objects.only('id', 'name').as_pymongo() - self.assertEqual(results[0].keys(), ['_id', 'name']) + self.assertEqual(sorted(results[0].keys()), sorted(['_id', 'name'])) users = User.objects.only('name', 'price').as_pymongo() results = list(users) @@ -3365,6 +3365,34 @@ class QuerySetTest(unittest.TestCase): self.assertEqual("%s" % users, "[]") self.assertEqual(1, len(users._result_cache)) + def test_no_cache(self): + """Ensure you can add meta data to file""" + + class Noddy(Document): + fields = DictField() + + Noddy.drop_collection() + for i in xrange(100): + noddy = Noddy() + for j in range(20): + noddy.fields["key"+str(j)] = "value "+str(j) + noddy.save() + + docs = Noddy.objects.no_cache() + + counter = len([1 for i in docs]) + self.assertEquals(counter, 100) + + self.assertEquals(len(list(docs)), 100) + self.assertRaises(TypeError, lambda: len(docs)) + + with query_counter() as q: + self.assertEqual(q, 0) + list(docs) + self.assertEqual(q, 1) + list(docs) + self.assertEqual(q, 2) + def test_nested_queryset_iterator(self): # Try iterating the same queryset twice, nested. names = ['Alice', 'Bob', 'Chuck', 'David', 'Eric', 'Francis', 'George'] From fb0dd2c1ca6eb4a1a8fef29cce2a39497231d9e3 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 19:54:30 +0000 Subject: [PATCH 027/222] Updated changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d875040b..76df2308 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog Changes in 0.8.3 ================ - Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) -- Fixed sum and average mapreduce dot notation support (#375, #376) +- Fixed sum and average mapreduce dot notation support (#375, #376, #393) - Fixed as_pymongo to return the id (#386) - Document.select_related() now respects `db_alias` (#377) - Reload uses shard_key if applicable (#384) From e155e1fa8621c158b619bcbb1c5b9601f5364634 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 20:10:01 +0000 Subject: [PATCH 028/222] Add a default for previously pickled versions --- 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 04b0c050..0eb63d5f 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -160,7 +160,7 @@ class BaseDocument(object): '_fields_ordered', '_dynamic_fields'): if k in data: setattr(self, k, data[k]) - for k in data.get('_dynamic_fields').keys(): + for k in data.get('_dynamic_fields', SON()).keys(): setattr(self, k, data["_data"].get(k)) def __iter__(self): From d9f538170b73f53f43f3f97ae3f6b66d7c40bb90 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 21:19:11 +0000 Subject: [PATCH 029/222] Added get_proxy_object helper to filefields (#391) --- docs/changelog.rst | 1 + mongoengine/fields.py | 18 ++++++++---------- tests/fields/file_tests.py | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 76df2308..e0f47fe6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Added get_proxy_object helper to filefields (#391) - Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) - Fixed sum and average mapreduce dot notation support (#375, #376, #393) - Fixed as_pymongo to return the id (#386) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 9d3a668f..47554e03 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1190,7 +1190,7 @@ class FileField(BaseField): # Check if a file already exists for this model grid_file = instance._data.get(self.name) if not isinstance(grid_file, self.proxy_class): - grid_file = self.get_proxy_obj(key=key, instance=instance) + grid_file = self.get_proxy_obj(key=self.name, instance=instance) instance._data[self.name] = grid_file if not grid_file.key: @@ -1218,16 +1218,16 @@ class FileField(BaseField): instance._data[key] = value instance._mark_as_changed(key) - + def get_proxy_obj(self, key, instance, db_alias=None, collection_name=None): if db_alias is None: db_alias = self.db_alias if collection_name is None: collection_name = self.collection_name - - return self.proxy_class(key=key, instance=instance, - db_alias=db_alias, - collection_name=collection_name) + + return self.proxy_class(key=key, instance=instance, + db_alias=db_alias, + collection_name=collection_name) def to_mongo(self, value): # Store the GridFS file id in MongoDB @@ -1261,10 +1261,8 @@ class ImageGridFsProxy(GridFSProxy): applying field properties (size, thumbnail_size) """ field = self.instance._fields[self.key] - # if the field from the instance has an attribute field - # we use that one and hope for the best. Usually only container - # fields have a field attribute. - if hasattr(field, 'field'): + # Handle nested fields + if hasattr(field, 'field') and isinstance(field.field, FileField): field = field.field try: diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index dfef9eed..d0445008 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -455,5 +455,31 @@ class FileTest(unittest.TestCase): self.assertEqual(1, TestImage.objects(Q(image1=grid_id) or Q(image2=grid_id)).count()) + def test_complex_field_filefield(self): + """Ensure you can add meta data to file""" + + class Animal(Document): + genus = StringField() + family = StringField() + photos = ListField(FileField()) + + Animal.drop_collection() + marmot = Animal(genus='Marmota', family='Sciuridae') + + marmot_photo = open(TEST_IMAGE_PATH, 'rb') # Retrieve a photo from disk + + photos_field = marmot._fields['photos'].field + new_proxy = photos_field.get_proxy_obj('photos', marmot) + new_proxy.put(marmot_photo, content_type='image/jpeg', foo='bar') + marmot_photo.close() + + marmot.photos.append(new_proxy) + marmot.save() + + marmot = Animal.objects.get() + self.assertEqual(marmot.photos[0].content_type, 'image/jpeg') + self.assertEqual(marmot.photos[0].foo, 'bar') + self.assertEqual(marmot.photos[0].get().length, 8313) + if __name__ == '__main__': unittest.main() From f48a0b7b7d8cee37c7519bab162123a5493e17f1 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 10 Jul 2013 21:30:29 +0000 Subject: [PATCH 030/222] Trying to fix travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2bb58638..8c4d5e1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,8 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install django==$DJANGO --use-mirrors ; true; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install python-dateutils==1.5 --no-allow-external ; true; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then pip install python-dateutils --no-allow-external ; true; fi - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi - python setup.py install From 6c599ef50678bbef18a49bedddcfe3ea87705c86 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 11 Jul 2013 07:15:34 +0000 Subject: [PATCH 031/222] Fix edge case where _dynamic_keys stored as None (#387, #401) --- mongoengine/base/document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 0eb63d5f..cbce4ffe 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -160,7 +160,8 @@ class BaseDocument(object): '_fields_ordered', '_dynamic_fields'): if k in data: setattr(self, k, data[k]) - for k in data.get('_dynamic_fields', SON()).keys(): + dynamic_fields = data.get('_dynamic_fields') or SON() + for k in dynamic_fields.keys(): setattr(self, k, data["_data"].get(k)) def __iter__(self): From d593f7e04b6bf26b576b597b2a68dfd72b400269 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 11 Jul 2013 08:11:00 +0000 Subject: [PATCH 032/222] Fixed EmbeddedDocuments with `id` also storing `_id` (#402) --- mongoengine/base/document.py | 6 ++++-- tests/document/instance.py | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index cbce4ffe..536fc2f4 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -262,8 +262,10 @@ class BaseDocument(object): data[field.db_field] = value # If "_id" has not been set, then try and set it - if data["_id"] is None: - data["_id"] = self._data.get("id", None) + Document = _import_class("Document") + if isinstance(self, Document): + if data["_id"] is None: + data["_id"] = self._data.get("id", None) if data['_id'] is None: data.pop('_id') diff --git a/tests/document/instance.py b/tests/document/instance.py index e85c9d86..a61c4396 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -444,6 +444,13 @@ class InstanceTest(unittest.TestCase): 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): + id = StringField(required=True) + + sub_doc = SubDoc(id="abc") + self.assertEqual(sub_doc.to_mongo().keys(), ['id']) + def test_embedded_document(self): """Ensure that embedded documents are set up correctly. """ From 6c2c33cac8010d8658810073ba3c21843f24cb9a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 11 Jul 2013 08:12:27 +0000 Subject: [PATCH 033/222] Add Jatin- to Authors, changelog update --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 70720f09..b8143a0c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -171,3 +171,4 @@ that much better: * Michael Bartnett (https://github.com/michaelbartnett) * Alon Horev (https://github.com/alonho) * Kelvin Hammond (https://github.com/kelvinhammond) + * Jatin- (https://github.com/jatin-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e0f47fe6..6ca52b26 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.3 ================ +- Fixed EmbeddedDocuments with `id` also storing `_id` (#402) - Added get_proxy_object helper to filefields (#391) - Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) - Fixed sum and average mapreduce dot notation support (#375, #376, #393) From 73026047e9e8be9581a61f561b8cb14cb9613fdf Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 11 Jul 2013 09:29:06 +0000 Subject: [PATCH 034/222] Trying to fix dateutil --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c4d5e1c..092b985a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,11 +14,11 @@ env: - PYMONGO=3.2 DJANGO=1.5.1 - PYMONGO=3.3 DJANGO=1.5.1 install: + - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then sudo apt-get install python-dateutil; true; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then sudo apt-get install python3-dateutil; true; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install django==$DJANGO --use-mirrors ; true; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install python-dateutils==1.5 --no-allow-external ; true; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then pip install python-dateutils --no-allow-external ; true; fi - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi - python setup.py install From 1aa2b86df31822ec527460db16422e2177e81eee Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 11 Jul 2013 09:38:59 +0000 Subject: [PATCH 035/222] travis install python-dateutil direct --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 092b985a..c7e8ea3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,13 +14,12 @@ env: - PYMONGO=3.2 DJANGO=1.5.1 - PYMONGO=3.3 DJANGO=1.5.1 install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then sudo apt-get install python-dateutil; true; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then sudo apt-get install python3-dateutil; true; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install django==$DJANGO --use-mirrors ; true; fi - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi + - pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - python setup.py install script: - python setup.py test From 48ef176e281bba8bd973a1caa530774540d5ce61 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 12 Jul 2013 08:41:56 +0000 Subject: [PATCH 036/222] 0.8.3 is a go --- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 5bd12019..bfa35fb5 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 2) +VERSION = (0, 8, 3) def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 4eaba4db..512c621b 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.2 +Version: 0.8.3 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From dc5512e4039f81b7773f177eb6824e63af2d513f Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 12 Jul 2013 09:01:11 +0000 Subject: [PATCH 037/222] Upgrade warning for 0.8.3 --- docs/upgrade.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index b8864b0d..0051a626 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -3,7 +3,7 @@ Upgrading ######### -0.8.2 to 0.8.2 +0.8.2 to 0.8.3 ************** Minor change that may impact users: From 35f2781518e7d8d642b54010b9ec6f94ba3e44c9 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 12 Jul 2013 09:11:27 +0000 Subject: [PATCH 038/222] Update changelog --- docs/_themes/nature/static/nature.css_t | 72 +++++++++++++------------ docs/changelog.rst | 5 ++ 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t index 03b0379d..337760b5 100644 --- a/docs/_themes/nature/static/nature.css_t +++ b/docs/_themes/nature/static/nature.css_t @@ -2,11 +2,15 @@ * Sphinx stylesheet -- default theme * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - + @import url("basic.css"); - + +#changelog p.first {margin-bottom: 0 !important;} +#changelog p {margin-top: 0 !important; + margin-bottom: 0 !important;} + /* -- page layout ----------------------------------------------------------- */ - + body { font-family: Arial, sans-serif; font-size: 100%; @@ -28,18 +32,18 @@ div.bodywrapper { hr{ border: 1px solid #B1B4B6; } - + div.document { background-color: #eee; } - + div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; font-size: 0.8em; } - + div.footer { color: #555; width: 100%; @@ -47,12 +51,12 @@ div.footer { text-align: center; font-size: 75%; } - + div.footer a { color: #444; text-decoration: underline; } - + div.related { background-color: #6BA81E; line-height: 32px; @@ -60,11 +64,11 @@ div.related { text-shadow: 0px 1px 0 #444; font-size: 0.80em; } - + div.related a { color: #E2F3CC; } - + div.sphinxsidebar { font-size: 0.75em; line-height: 1.5em; @@ -73,7 +77,7 @@ div.sphinxsidebar { div.sphinxsidebarwrapper{ padding: 20px 0; } - + div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: Arial, sans-serif; @@ -89,30 +93,30 @@ div.sphinxsidebar h4 { div.sphinxsidebar h4{ font-size: 1.1em; } - + div.sphinxsidebar h3 a { color: #444; } - - + + div.sphinxsidebar p { color: #888; padding: 5px 20px; } - + div.sphinxsidebar p.topless { } - + div.sphinxsidebar ul { margin: 10px 20px; padding: 0; color: #000; } - + div.sphinxsidebar a { color: #444; } - + div.sphinxsidebar input { border: 1px solid #ccc; font-family: sans-serif; @@ -122,19 +126,19 @@ div.sphinxsidebar input { div.sphinxsidebar input[type=text]{ margin-left: 20px; } - + /* -- body styles ----------------------------------------------------------- */ - + a { color: #005B81; text-decoration: none; } - + a:hover { color: #E32E00; text-decoration: underline; } - + div.body h1, div.body h2, div.body h3, @@ -149,30 +153,30 @@ div.body h6 { padding: 5px 0 5px 10px; text-shadow: 0px 1px 0 white } - + div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } div.body h2 { font-size: 150%; background-color: #C8D5E3; } div.body h3 { font-size: 120%; background-color: #D8DEE3; } div.body h4 { font-size: 110%; background-color: #D8DEE3; } div.body h5 { font-size: 100%; background-color: #D8DEE3; } div.body h6 { font-size: 100%; background-color: #D8DEE3; } - + a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; } - + a.headerlink:hover { background-color: #c60f0f; color: white; } - + div.body p, div.body dd, div.body li { line-height: 1.5em; } - + div.admonition p.admonition-title + p { display: inline; } @@ -185,29 +189,29 @@ div.note { background-color: #eee; border: 1px solid #ccc; } - + div.seealso { background-color: #ffc; border: 1px solid #ff6; } - + div.topic { background-color: #eee; } - + div.warning { background-color: #ffe4e4; border: 1px solid #f66; } - + p.admonition-title { display: inline; } - + p.admonition-title:after { content: ":"; } - + pre { padding: 10px; background-color: White; @@ -219,7 +223,7 @@ pre { -webkit-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 1px 1px 1px #d8d8d8; } - + tt { background-color: #ecf0f3; color: #222; diff --git a/docs/changelog.rst b/docs/changelog.rst index 6ca52b26..ee92d477 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,9 @@ Changes in 0.8.3 - Document.select_related() now respects `db_alias` (#377) - Reload uses shard_key if applicable (#384) - Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) + + **Potential breaking change:** http://docs.mongoengine.org/en/latest/upgrade.html#to-0-8-3 + - Fixed pickling dynamic documents `_dynamic_fields` (#387) - Fixed ListField setslice and delslice dirty tracking (#390) - Added Django 1.5 PY3 support (#392) @@ -20,6 +23,8 @@ Changes in 0.8.3 - Fixed queryset.get() respecting no_dereference (#373) - Added full_result kwarg to update (#380) + + Changes in 0.8.2 ================ - Added compare_indexes helper (#361) From bcf83ec76190524a5508f17d462b9b360584c07e Mon Sep 17 00:00:00 2001 From: bool-dev Date: Thu, 18 Jul 2013 09:17:28 +0530 Subject: [PATCH 039/222] Corrected spelling mistakes, some grammar, and UUID/DecimalField error in upgrade.rst --- docs/upgrade.rst | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 0051a626..a1fccea2 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -16,8 +16,8 @@ fields. Previously they were stored alphabetically. ********** There have been numerous backwards breaking changes in 0.8. The reasons for -these are ensure that MongoEngine has sane defaults going forward and -performs the best it can out the box. Where possible there have been +these are to ensure that MongoEngine has sane defaults going forward and that it +performs the best it can out of the box. Where possible there have been FutureWarnings to help get you ready for the change, but that hasn't been possible for the whole of the release. @@ -71,7 +71,7 @@ inherited classes like so: :: Document Definition ------------------- -The default for inheritance has changed - its now off by default and +The default for inheritance has changed - it is now off by default and :attr:`_cls` will not be stored automatically with the class. So if you extend your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocuments` you will need to declare :attr:`allow_inheritance` in the meta data like so: :: @@ -81,7 +81,7 @@ you will need to declare :attr:`allow_inheritance` in the meta data like so: :: meta = {'allow_inheritance': True} -Previously, if you had data the database that wasn't defined in the Document +Previously, if you had data in the database that wasn't defined in the Document definition, it would set it as an attribute on the document. This is no longer the case and the data is set only in the ``document._data`` dictionary: :: @@ -102,8 +102,8 @@ the case and the data is set only in the ``document._data`` dictionary: :: AttributeError: 'Animal' object has no attribute 'size' The Document class has introduced a reserved function `clean()`, which will be -called before saving the document. If your document class happen to have a method -with the same name, please try rename it. +called before saving the document. If your document class happens to have a method +with the same name, please try to rename it. def clean(self): pass @@ -111,7 +111,7 @@ with the same name, please try rename it. ReferenceField -------------- -ReferenceFields now store ObjectId's by default - this is more efficient than +ReferenceFields now store ObjectIds by default - this is more efficient than DBRefs as we already know what Document types they reference:: # Old code @@ -157,7 +157,7 @@ UUIDFields now default to storing binary values:: class Animal(Document): uuid = UUIDField(binary=False) -To migrate all the uuid's you need to touch each object and mark it as dirty +To migrate all the uuids you need to touch each object and mark it as dirty eg:: # Doc definition @@ -175,7 +175,7 @@ eg:: DecimalField ------------ -DecimalField now store floats - previous it was storing strings and that +DecimalFields now store floats - previously it was storing strings and that made it impossible to do comparisons when querying correctly.:: # Old code @@ -186,7 +186,7 @@ made it impossible to do comparisons when querying correctly.:: class Person(Document): balance = DecimalField(force_string=True) -To migrate all the uuid's you need to touch each object and mark it as dirty +To migrate all the DecimalFields you need to touch each object and mark it as dirty eg:: # Doc definition @@ -198,7 +198,7 @@ eg:: p._mark_as_changed('balance') p.save() -.. note:: DecimalField's have also been improved with the addition of precision +.. note:: DecimalFields have also been improved with the addition of precision and rounding. See :class:`~mongoengine.fields.DecimalField` for more information. `An example test migration for DecimalFields is available on github @@ -207,7 +207,7 @@ eg:: Cascading Saves --------------- To improve performance document saves will no longer automatically cascade. -Any changes to a Documents references will either have to be saved manually or +Any changes to a Document's references will either have to be saved manually or you will have to explicitly tell it to cascade on save:: # At the class level: @@ -249,7 +249,7 @@ update your code like so: :: # Update example a) assign queryset after a change: mammals = Animal.objects(type="mammal") - carnivores = mammals.filter(order="Carnivora") # Reassign the new queryset so fitler can be applied + carnivores = mammals.filter(order="Carnivora") # Reassign the new queryset so filter can be applied [m for m in carnivores] # This will return all carnivores # Update example b) chain the queryset: @@ -276,7 +276,7 @@ queryset you should upgrade to use count:: .only() now inline with .exclude() ---------------------------------- -The behaviour of `.only()` was highly ambious, now it works in the mirror fashion +The behaviour of `.only()` was highly ambiguous, now it works in mirror fashion to `.exclude()`. Chaining `.only()` calls will increase the fields required:: # Old code @@ -440,7 +440,7 @@ main areas of changed are: choices in fields, map_reduce and collection names. Choice options: =============== -Are now expected to be an iterable of tuples, with the first element in each +Are now expected to be an iterable of tuples, with the first element in each tuple being the actual value to be stored. The second element is the human-readable name for the option. @@ -462,8 +462,8 @@ such the following have been changed: Default collection naming ========================= -Previously it was just lowercase, its now much more pythonic and readable as -its lowercase and underscores, previously :: +Previously it was just lowercase, it's now much more pythonic and readable as +it's lowercase and underscores, previously :: class MyAceDocument(Document): pass @@ -530,5 +530,5 @@ Alternatively, you can rename your collections eg :: mongodb 1.8 > 2.0 + =================== -Its been reported that indexes may need to be recreated to the newer version of indexes. +It's been reported that indexes may need to be recreated to the newer version of indexes. To do this drop indexes and call ``ensure_indexes`` on each model. From 80b3df89537a50cc9c6f91163306185a0816c386 Mon Sep 17 00:00:00 2001 From: Thom Knowles Date: Mon, 22 Jul 2013 20:07:57 -0400 Subject: [PATCH 040/222] dereference instance not thread-safe --- AUTHORS | 1 + mongoengine/base/fields.py | 16 +++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index b8143a0c..69a87a21 100644 --- a/AUTHORS +++ b/AUTHORS @@ -172,3 +172,4 @@ that much better: * Alon Horev (https://github.com/alonho) * Kelvin Hammond (https://github.com/kelvinhammond) * Jatin- (https://github.com/jatin-) + * Thom Knowles (https://github.com/fleat) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index eda9b3c9..c6abd02b 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -186,7 +186,6 @@ class ComplexBaseField(BaseField): """ field = None - __dereference = False def __get__(self, instance, owner): """Descriptor to automatically dereference references. @@ -201,9 +200,11 @@ class ComplexBaseField(BaseField): (self.field is None or isinstance(self.field, (GenericReferenceField, ReferenceField)))) + _dereference = _import_class("DeReference")() + self._auto_dereference = instance._fields[self.name]._auto_dereference - if not self.__dereference and instance._initialised and dereference: - instance._data[self.name] = self._dereference( + if instance._initialised and dereference: + instance._data[self.name] = _dereference( instance._data.get(self.name), max_depth=1, instance=instance, name=self.name ) @@ -222,7 +223,7 @@ class ComplexBaseField(BaseField): if (self._auto_dereference and instance._initialised and isinstance(value, (BaseList, BaseDict)) and not value._dereferenced): - value = self._dereference( + value = _dereference( value, max_depth=1, instance=instance, name=self.name ) value._dereferenced = True @@ -382,13 +383,6 @@ class ComplexBaseField(BaseField): owner_document = property(_get_owner_document, _set_owner_document) - @property - def _dereference(self,): - if not self.__dereference: - DeReference = _import_class("DeReference") - self.__dereference = DeReference() # Cached - return self.__dereference - class ObjectIdField(BaseField): """A field wrapper around MongoDB's ObjectIds. From d92ed04538e7dcf0e94f87e9c3272c0f8a3c1ee0 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 23 Jul 2013 08:13:52 +0000 Subject: [PATCH 041/222] Docs update #406 --- mongoengine/queryset/queryset.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 9db98a7e..9cfb1b6f 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -95,7 +95,10 @@ class QuerySet(BaseQuerySet): self._has_more = False def no_cache(self): - """Convert to a non_caching queryset""" + """Convert to a non_caching queryset + + .. versionadded:: 0.8.3 Convert to non caching queryset + """ if self._result_cache is not None: raise OperationError("QuerySet already cached") return self.clone_into(QuerySetNoCache(self._document, self._collection)) @@ -105,7 +108,10 @@ class QuerySetNoCache(BaseQuerySet): """A non caching QuerySet""" def cache(self): - """Convert to a caching queryset""" + """Convert to a caching queryset + + .. versionadded:: 0.8.3 Convert to caching queryset + """ return self.clone_into(QuerySet(self._document, self._collection)) def __repr__(self): From a458d5a176afc645e35ef02f3103c597e728e849 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 23 Jul 2013 08:16:06 +0000 Subject: [PATCH 042/222] Docs update #406 --- docs/guide/querying.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 5fd03601..127e4798 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -17,8 +17,8 @@ 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` to - return a non-caching queryset. + desired behavour you can call :class:`~mongoengine.QuerySet.no_cache` + (version **0.8.3+**) to return a non-caching queryset. Filtering queries ================= From dae9e662a57ed6c4e73a1aaa517b6d17a7bb9fcc Mon Sep 17 00:00:00 2001 From: Paul Uithol Date: Thu, 25 Jul 2013 14:30:20 +0200 Subject: [PATCH 043/222] Create test case for failing saves (wrong delta) with dbref=False --- tests/document/delta.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/document/delta.py b/tests/document/delta.py index 3656d9e3..a5302f1a 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -313,17 +313,17 @@ class DeltaTest(unittest.TestCase): self.circular_reference_deltas_2(DynamicDocument, Document) self.circular_reference_deltas_2(DynamicDocument, DynamicDocument) - def circular_reference_deltas_2(self, DocClass1, DocClass2): + def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True): class Person(DocClass1): name = StringField() - owns = ListField(ReferenceField('Organization')) - employer = ReferenceField('Organization') + owns = ListField(ReferenceField('Organization', dbref=dbref)) + employer = ReferenceField('Organization', dbref=dbref) class Organization(DocClass2): name = StringField() - owner = ReferenceField('Person') - employees = ListField(ReferenceField('Person')) + owner = ReferenceField('Person', dbref=dbref) + employees = ListField(ReferenceField('Person', dbref=dbref)) Person.drop_collection() Organization.drop_collection() @@ -355,6 +355,8 @@ class DeltaTest(unittest.TestCase): self.assertEqual(o.owner, p) self.assertEqual(e.employer, o) + return person, organization, employee + def test_delta_db_field(self): self.delta_db_field(Document) self.delta_db_field(DynamicDocument) @@ -686,6 +688,19 @@ class DeltaTest(unittest.TestCase): self.assertEqual(doc._get_changed_fields(), ['list_field']) self.assertEqual(doc._delta(), ({}, {'list_field': 1})) + def test_delta_with_dbref_true(self): + person, organization, employee = self.circular_reference_deltas_2(Document, Document, True) + employee.name = 'test' + changed = organization._get_changed_fields() + delta = organization._delta() + organization.save() + + def test_delta_with_dbref_false(self): + person, organization, employee = self.circular_reference_deltas_2(Document, Document, False) + employee.name = 'test' + changed = organization._get_changed_fields() + delta = organization._delta() + organization.save() if __name__ == '__main__': unittest.main() From 2ad5ffbda215f71f91261fa1cac3ebfaa978f904 Mon Sep 17 00:00:00 2001 From: Paul Uithol Date: Thu, 25 Jul 2013 14:51:09 +0200 Subject: [PATCH 044/222] Add asserts to `test_delta_with_dbref_*`, instead of relying on exceptions --- tests/document/delta.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/document/delta.py b/tests/document/delta.py index a5302f1a..fae65c94 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -691,15 +691,25 @@ class DeltaTest(unittest.TestCase): def test_delta_with_dbref_true(self): person, organization, employee = self.circular_reference_deltas_2(Document, Document, True) employee.name = 'test' - changed = organization._get_changed_fields() - delta = organization._delta() + + self.assertEqual(organization._get_changed_fields(), ['employees.0.name']) + + updates, removals = organization._delta() + self.assertEqual({}, removals) + self.assertIn('employees.0', updates) + organization.save() def test_delta_with_dbref_false(self): person, organization, employee = self.circular_reference_deltas_2(Document, Document, False) employee.name = 'test' - changed = organization._get_changed_fields() - delta = organization._delta() + + self.assertEqual(organization._get_changed_fields(), ['employees.0.name']) + + updates, removals = organization._delta() + self.assertEqual({}, removals) + self.assertIn('employees.0', updates) + organization.save() if __name__ == '__main__': From e27439be6adf4326177e7ff1530047c22a8e2831 Mon Sep 17 00:00:00 2001 From: Paul Uithol Date: Thu, 25 Jul 2013 14:52:03 +0200 Subject: [PATCH 045/222] Fix `BaseDocument._delta` when working with plain ObjectIds instead of DBRefs --- mongoengine/base/document.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 536fc2f4..258b3f25 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -4,7 +4,7 @@ import numbers from functools import partial import pymongo -from bson import json_util +from bson import json_util, ObjectId from bson.dbref import DBRef from bson.son import SON @@ -454,7 +454,7 @@ class BaseDocument(object): d = doc new_path = [] for p in parts: - if isinstance(d, DBRef): + if isinstance(d, (ObjectId, DBRef)): break elif isinstance(d, list) and p.isdigit(): d = d[int(p)] From d143e50238c304c6aca9ce9142c59707c24e4572 Mon Sep 17 00:00:00 2001 From: Paul Uithol Date: Thu, 25 Jul 2013 15:34:58 +0200 Subject: [PATCH 046/222] Replace `assertIn` with an `assertTrue`; apparently missing in Python 2.6 --- tests/document/delta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/document/delta.py b/tests/document/delta.py index fae65c94..c6efc028 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -696,7 +696,7 @@ class DeltaTest(unittest.TestCase): updates, removals = organization._delta() self.assertEqual({}, removals) - self.assertIn('employees.0', updates) + self.assertTrue('employees.0' in updates) organization.save() @@ -708,7 +708,7 @@ class DeltaTest(unittest.TestCase): updates, removals = organization._delta() self.assertEqual({}, removals) - self.assertIn('employees.0', updates) + self.assertTrue('employees.0' in updates) organization.save() From 67f43b2aad761e7d04908742c14441282f20d0db Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Jul 2013 15:29:48 +0000 Subject: [PATCH 047/222] Allow args and kwargs to be passed through to_json (#420) --- docs/changelog.rst | 4 ++++ mongoengine/base/document.py | 4 ++-- mongoengine/queryset/base.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ee92d477..a53d6df4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +Changes in 0.8.4 +================ +- Allow args and kwargs to be passed through to_json (#420) + Changes in 0.8.3 ================ - Fixed EmbeddedDocuments with `id` also storing `_id` (#402) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 536fc2f4..e8232a0e 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -321,9 +321,9 @@ class BaseDocument(object): message = "ValidationError (%s:%s) " % (self._class_name, pk) raise ValidationError(message, errors=errors) - def to_json(self): + def to_json(self, *args, **kwargs): """Converts a document to JSON""" - return json_util.dumps(self.to_mongo()) + return json_util.dumps(self.to_mongo(), *args, **kwargs) @classmethod def from_json(cls, json_data): diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index d3bb4c4b..e88feb30 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -827,9 +827,9 @@ class BaseQuerySet(object): # JSON Helpers - def to_json(self): + def to_json(self, *args, **kwargs): """Converts a queryset to JSON""" - return json_util.dumps(self.as_pymongo()) + return json_util.dumps(self.as_pymongo(), *args, **kwargs) def from_json(self, json_data): """Converts json data to unsaved objects""" From 7a97d42338b49aa4253c3d590390a68810ff98cd Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Jul 2013 15:38:08 +0000 Subject: [PATCH 048/222] to_json test updates #420 --- tests/document/json_serialisation.py | 4 ++++ tests/queryset/queryset.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/document/json_serialisation.py b/tests/document/json_serialisation.py index dbc09d83..2b5d9a0c 100644 --- a/tests/document/json_serialisation.py +++ b/tests/document/json_serialisation.py @@ -31,6 +31,10 @@ class TestJson(unittest.TestCase): doc = Doc(string="Hi", embedded_field=Embedded(string="Hi")) + doc_json = doc.to_json(sort_keys=True, separators=(',', ':')) + expected_json = """{"embedded_field":{"string":"Hi"},"string":"Hi"}""" + self.assertEqual(doc_json, expected_json) + self.assertEqual(doc, Doc.from_json(doc.to_json())) def test_json_complex(self): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c56b31eb..0ec41fac 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3327,7 +3327,7 @@ class QuerySetTest(unittest.TestCase): Doc(string="Bye", embedded_field=Embedded(string="Bye")).save() Doc().save() - json_data = Doc.objects.to_json() + json_data = Doc.objects.to_json(sort_keys=True, separators=(',', ':')) doc_objects = list(Doc.objects) self.assertEqual(doc_objects, Doc.objects.from_json(json_data)) From 93a2adb3e6b8d4cac791659e0f3154df8a752904 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Jul 2013 15:43:54 +0000 Subject: [PATCH 049/222] Updating changelog and authors #417 --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index b8143a0c..e9f6ad92 100644 --- a/AUTHORS +++ b/AUTHORS @@ -172,3 +172,4 @@ that much better: * Alon Horev (https://github.com/alonho) * Kelvin Hammond (https://github.com/kelvinhammond) * Jatin- (https://github.com/jatin-) + * Paul Uithol (https://github.com/PaulUithol) diff --git a/docs/changelog.rst b/docs/changelog.rst index a53d6df4..b9c74f89 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed _delta including referenced fields when dbref=False (#417) - Allow args and kwargs to be passed through to_json (#420) Changes in 0.8.3 From 1e4d48d371e2920dd3397bb20b2f6f1456ed1566 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 29 Jul 2013 17:22:24 +0000 Subject: [PATCH 050/222] Don't follow references in _get_changed_fields (#422, #417) A better fix so we dont follow down a references rabbit hole. --- docs/changelog.rst | 2 +- mongoengine/base/document.py | 30 ++++++++++++++++++------------ tests/document/delta.py | 30 ++++++++++++++++-------------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b9c74f89..9112b2b8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog Changes in 0.8.4 ================ -- Fixed _delta including referenced fields when dbref=False (#417) +- Don't follow references in _get_changed_fields (#422, #417) - Allow args and kwargs to be passed through to_json (#420) Changes in 0.8.3 diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index f1c1d55f..80111f70 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -395,6 +395,7 @@ class BaseDocument(object): """ EmbeddedDocument = _import_class("EmbeddedDocument") DynamicEmbeddedDocument = _import_class("DynamicEmbeddedDocument") + ReferenceField = _import_class("ReferenceField") _changed_fields = [] _changed_fields += getattr(self, '_changed_fields', []) @@ -405,31 +406,36 @@ class BaseDocument(object): inspected.add(self.id) for field_name in self._fields_ordered: - db_field_name = self._db_field_map.get(field_name, field_name) key = '%s.' % db_field_name - field = self._data.get(field_name, None) - if hasattr(field, 'id'): - if field.id in inspected: - continue - inspected.add(field.id) + data = self._data.get(field_name, None) + field = self._fields.get(field_name) - if (isinstance(field, (EmbeddedDocument, DynamicEmbeddedDocument)) + if hasattr(data, 'id'): + if data.id in inspected: + continue + inspected.add(data.id) + if isinstance(field, ReferenceField): + continue + elif (isinstance(data, (EmbeddedDocument, DynamicEmbeddedDocument)) and db_field_name not in _changed_fields): # Find all embedded fields that have been changed - changed = field._get_changed_fields(inspected) + changed = data._get_changed_fields(inspected) _changed_fields += ["%s%s" % (key, k) for k in changed if k] - elif (isinstance(field, (list, tuple, dict)) and + elif (isinstance(data, (list, tuple, dict)) and db_field_name not in _changed_fields): # Loop list / dict fields as they contain documents # Determine the iterator to use - if not hasattr(field, 'items'): - iterator = enumerate(field) + if not hasattr(data, 'items'): + iterator = enumerate(data) else: - iterator = field.iteritems() + iterator = data.iteritems() for index, value in iterator: if not hasattr(value, '_get_changed_fields'): continue + if (hasattr(field, 'field') and + isinstance(field.field, ReferenceField)): + continue list_key = "%s%s." % (key, index) changed = value._get_changed_fields(inspected) _changed_fields += ["%s%s" % (list_key, k) diff --git a/tests/document/delta.py b/tests/document/delta.py index c6efc028..b4749f38 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -328,14 +328,9 @@ class DeltaTest(unittest.TestCase): Person.drop_collection() Organization.drop_collection() - person = Person(name="owner") - person.save() - - employee = Person(name="employee") - employee.save() - - organization = Organization(name="company") - organization.save() + person = Person(name="owner").save() + employee = Person(name="employee").save() + organization = Organization(name="company").save() person.owns.append(organization) organization.owner = person @@ -692,25 +687,32 @@ class DeltaTest(unittest.TestCase): person, organization, employee = self.circular_reference_deltas_2(Document, Document, True) employee.name = 'test' - self.assertEqual(organization._get_changed_fields(), ['employees.0.name']) + self.assertEqual(organization._get_changed_fields(), []) updates, removals = organization._delta() self.assertEqual({}, removals) - self.assertTrue('employees.0' in updates) + self.assertEqual({}, updates) - organization.save() + organization.employees.append(person) + updates, removals = organization._delta() + self.assertEqual({}, removals) + self.assertTrue('employees' in updates) def test_delta_with_dbref_false(self): person, organization, employee = self.circular_reference_deltas_2(Document, Document, False) employee.name = 'test' - self.assertEqual(organization._get_changed_fields(), ['employees.0.name']) + self.assertEqual(organization._get_changed_fields(), []) updates, removals = organization._delta() self.assertEqual({}, removals) - self.assertTrue('employees.0' in updates) + self.assertEqual({}, updates) + + organization.employees.append(person) + updates, removals = organization._delta() + self.assertEqual({}, removals) + self.assertTrue('employees' in updates) - organization.save() if __name__ == '__main__': unittest.main() From 6efd6faa3f5a467012fa4ee128889104ca0ba6f7 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 10:30:16 +0000 Subject: [PATCH 051/222] Fixed QuerySetNoCache.count() caching (#410) --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 10 ++-------- mongoengine/queryset/queryset.py | 15 +++++++++++++++ tests/queryset/queryset.py | 21 +++++++++++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9112b2b8..e2f07bc0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed QuerySetNoCache.count() caching (#410) - Don't follow references in _get_changed_fields (#422, #417) - Allow args and kwargs to be passed through to_json (#420) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index e88feb30..a6ba49b2 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -60,7 +60,6 @@ class BaseQuerySet(object): self._none = False self._as_pymongo = False self._as_pymongo_coerce = False - self._len = None # If inheritance is allowed, only return instances and instances of # subclasses of the class being used @@ -331,14 +330,9 @@ class BaseQuerySet(object): :meth:`skip` that has been applied to this cursor into account when getting the count """ - if self._limit == 0: + if self._limit == 0 and with_limit_and_skip: return 0 - if with_limit_and_skip and self._len is not None: - return self._len - count = self._cursor.count(with_limit_and_skip=with_limit_and_skip) - if with_limit_and_skip: - self._len = count - return count + return self._cursor.count(with_limit_and_skip=with_limit_and_skip) def delete(self, write_concern=None, _from_doc_delete=False): """Delete the documents matched by the query. diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 9cfb1b6f..1437e76b 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -94,6 +94,21 @@ class QuerySet(BaseQuerySet): except StopIteration: self._has_more = False + def count(self, with_limit_and_skip=True): + """Count the selected elements in the query. + + :param with_limit_and_skip (optional): take any :meth:`limit` or + :meth:`skip` that has been applied to this cursor into account when + getting the count + """ + if with_limit_and_skip is False: + return super(QuerySet, self).count(with_limit_and_skip) + + if self._len is None: + self._len = super(QuerySet, self).count(with_limit_and_skip) + + return self._len + def no_cache(self): """Convert to a non_caching queryset diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 0ec41fac..9c04c0be 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3483,6 +3483,27 @@ class QuerySetTest(unittest.TestCase): people.count() # count is cached self.assertEqual(q, 1) + def test_no_cached_queryset(self): + class Person(Document): + name = StringField() + + Person.drop_collection() + for i in xrange(100): + Person(name="No: %s" % i).save() + + with query_counter() as q: + self.assertEqual(q, 0) + people = Person.objects.no_cache() + + [x for x in people] + self.assertEqual(q, 1) + + list(people) + self.assertEqual(q, 2) + + people.count() + self.assertEqual(q, 3) + def test_cache_not_cloned(self): class User(Document): From e98c5e10bc0eefacc11aa6955db9635fabc549de Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 10:49:08 +0000 Subject: [PATCH 052/222] Fixed dereference threading issue in ComplexField.__get__ (#412) --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e2f07bc0..7cb1456f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed dereference threading issue in ComplexField.__get__ (#412) - Fixed QuerySetNoCache.count() caching (#410) - Don't follow references in _get_changed_fields (#422, #417) - Allow args and kwargs to be passed through to_json (#420) From dc310b99f94ddf365369340c074eb2a35d68c685 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 10:54:04 +0000 Subject: [PATCH 053/222] Updated docs about TTL indexes and signals (#413) --- docs/guide/defining-documents.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index a50450e3..bc78a666 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -558,6 +558,11 @@ documentation for more information. A common usecase might be session data:: ] } +.. warning:: TTL indexes happen on the MongoDB server and not in the application + code, therefore no signals will be fired on document deletion. + If you need signals to be fired on deletion, then you must handle the + deletion of Documents in your application code. + Comparing Indexes ----------------- From 0c437879961008281018e572369a61d58da909d6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 11:43:52 +0000 Subject: [PATCH 054/222] Fixed indexing - turn off _cls (#414) --- docs/changelog.rst | 1 + docs/guide/defining-documents.rst | 31 +++++++++++++++++++++++++++++++ mongoengine/base/document.py | 6 ++++-- mongoengine/document.py | 2 ++ tests/document/indexes.py | 19 +++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7cb1456f..3569132d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed indexing - turn off _cls (#414) - Fixed dereference threading issue in ComplexField.__get__ (#412) - Fixed QuerySetNoCache.count() caching (#410) - Don't follow references in _get_changed_fields (#422, #417) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index bc78a666..407fbdac 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -442,6 +442,8 @@ The following example shows a :class:`Log` document that will be limited to ip_address = StringField() meta = {'max_documents': 1000, 'max_size': 2000000} +.. defining-indexes_ + Indexes ======= @@ -485,6 +487,35 @@ If a dictionary is passed then the following options are available: Inheritance adds extra fields indices see: :ref:`document-inheritance`. +Global index default options +---------------------------- + +There are a few top level defaults for all indexes that can be set:: + + class Page(Document): + title = StringField() + rating = StringField() + meta = { + 'index_options': {}, + 'index_background': True, + 'index_drop_dups': True, + 'index_cls': False + } + + +:attr:`index_options` (Optional) + Set any default index options - see the `full options list `_ + +:attr:`index_background` (Optional) + Set the default value for if an index should be indexed in the background + +: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. + + Compound Indexes and Indexing sub documents ------------------------------------------- diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 80111f70..b9c07cf5 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -629,8 +629,10 @@ class BaseDocument(object): # Check to see if we need to include _cls allow_inheritance = cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) - include_cls = allow_inheritance and not spec.get('sparse', False) - + include_cls = (allow_inheritance and not spec.get('sparse', False) and + spec.get('cls', True)) + if "cls" in spec: + spec.pop('cls') for key in spec['fields']: # If inherited spec continue if isinstance(key, (list, tuple)): diff --git a/mongoengine/document.py b/mongoengine/document.py index e331aa1f..2f3a92a5 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -536,6 +536,8 @@ class Document(BaseDocument): def ensure_indexes(cls): """Checks the document meta data and ensures all the indexes exist. + Global defaults can be set in the meta - see :doc:`guide/defining-documents` + .. note:: You can disable automatic index creation by setting `auto_create_index` to False in the documents meta data """ diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 04d56324..ccf84636 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -156,6 +156,25 @@ class IndexesTest(unittest.TestCase): self.assertEqual([{'fields': [('_cls', 1), ('title', 1)]}], A._meta['index_specs']) + def test_index_no_cls(self): + """Ensure index specs are inhertited correctly""" + + class A(Document): + title = StringField() + meta = { + 'indexes': [ + {'fields': ('title',), 'cls': False}, + ], + 'allow_inheritance': True, + 'index_cls': False + } + + self.assertEqual([('title', 1)], A._meta['index_specs'][0]['fields']) + A._get_collection().drop_indexes() + A.ensure_indexes() + info = A._get_collection().index_information() + self.assertEqual(len(info.keys()), 2) + def test_build_index_spec_is_not_destructive(self): class MyDoc(Document): From 5e70e1bcb28c60520964e683b60ed1c60ef0f429 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 13:17:38 +0000 Subject: [PATCH 055/222] Update transform to handle docs erroneously passed to unset (#416) --- docs/changelog.rst | 1 + mongoengine/queryset/transform.py | 4 +++- tests/queryset/transform.py | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3569132d..8199e03e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Update transform to handle docs erroneously passed to unset (#416) - Fixed indexing - turn off _cls (#414) - Fixed dereference threading issue in ComplexField.__get__ (#412) - Fixed QuerySetNoCache.count() caching (#410) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 352774ff..e0a7d3c6 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -203,11 +203,13 @@ def update(_doc_cls=None, **update): value = field.prepare_query_value(op, value) elif op in ('pushAll', 'pullAll'): value = [field.prepare_query_value(op, v) for v in value] - elif op == 'addToSet': + elif op in ('addToSet', 'setOnInsert'): if isinstance(value, (list, tuple, set)): value = [field.prepare_query_value(op, v) for v in value] elif field.required or value is not None: value = field.prepare_query_value(op, value) + elif op == "unset": + value = 1 if match: match = '$' + match diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 7886965b..d2e8b784 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -31,6 +31,31 @@ class TransformTest(unittest.TestCase): self.assertEqual(transform.query(name__exists=True), {'name': {'$exists': True}}) + def test_transform_update(self): + class DicDoc(Document): + dictField = DictField() + + class Doc(Document): + pass + + DicDoc.drop_collection() + Doc.drop_collection() + + doc = Doc().save() + dic_doc = DicDoc().save() + + for k, v in (("set", "$set"), ("set_on_insert", "$setOnInsert"), ("push", "$push")): + update = transform.update(DicDoc, **{"%s__dictField__test" % k: doc}) + self.assertTrue(isinstance(update[v]["dictField.test"], dict)) + + # Update special cases + update = transform.update(DicDoc, unset__dictField__test=doc) + self.assertEqual(update["$unset"]["dictField.test"], 1) + + update = transform.update(DicDoc, pull__dictField__test=doc) + self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict)) + + def test_query_field_name(self): """Ensure that the correct field name is used when querying. """ From a57d9a9303a8487cbc4417dceb5c61012f9a88b9 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 13:28:05 +0000 Subject: [PATCH 056/222] Added regression test (#418) --- tests/queryset/field_list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index 2bdfce1f..f981c12a 100644 --- a/tests/queryset/field_list.py +++ b/tests/queryset/field_list.py @@ -162,6 +162,10 @@ class OnlyExcludeAllTest(unittest.TestCase): self.assertEqual(obj.name, person.name) self.assertEqual(obj.age, person.age) + obj = Person.objects.only(*('id', 'name',)).get() + self.assertEqual(obj.name, person.name) + self.assertEqual(obj.age, None) + # Check polymorphism still works class Employee(self.Person): salary = IntField(db_field='wage') From b4777f7f4f1e2ee5d7cc8e1102421d87540a69f5 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 30 Jul 2013 15:04:52 +0000 Subject: [PATCH 057/222] Fix test --- tests/queryset/field_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index f981c12a..a18e167e 100644 --- a/tests/queryset/field_list.py +++ b/tests/queryset/field_list.py @@ -162,7 +162,7 @@ class OnlyExcludeAllTest(unittest.TestCase): self.assertEqual(obj.name, person.name) self.assertEqual(obj.age, person.age) - obj = Person.objects.only(*('id', 'name',)).get() + obj = self.Person.objects.only(*('id', 'name',)).get() self.assertEqual(obj.name, person.name) self.assertEqual(obj.age, None) From c17f94422fd6ac388dbdf1bc60139525bfcbd018 Mon Sep 17 00:00:00 2001 From: Nicolas Cortot Date: Tue, 30 Jul 2013 20:43:21 +0200 Subject: [PATCH 058/222] Add get_user_document and improve mongo_auth module * Added a get_user_document() methot to access the actual Document class used for authentication. * Clarified the docstring on MongoUser to prevent its use when the user Document class should be used. * Removed the masking of exceptions when loading the user document class. --- mongoengine/django/mongo_auth/models.py | 42 ++++++++++++++++++------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py index 3529d8e1..71797185 100644 --- a/mongoengine/django/mongo_auth/models.py +++ b/mongoengine/django/mongo_auth/models.py @@ -6,10 +6,29 @@ 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(self): + """Get the user docuemnt class user for authentcation. + + 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. @@ -44,7 +63,7 @@ class MongoUserManager(UserManager): def contribute_to_class(self, model, name): super(MongoUserManager, self).contribute_to_class(model, name) self.dj_model = self.model - self.model = self._get_user_document() + self.model = get_user_document() self.dj_model.USERNAME_FIELD = self.model.USERNAME_FIELD username = models.CharField(_('username'), max_length=30, unique=True) @@ -55,16 +74,6 @@ class MongoUserManager(UserManager): field = models.CharField(_(name), max_length=30) field.contribute_to_class(self.dj_model, name) - def _get_user_document(self): - try: - name = MONGOENGINE_USER_DOCUMENT - dot = name.rindex('.') - module = import_module(name[:dot]) - return getattr(module, name[dot + 1:]) - except ImportError: - raise ImproperlyConfigured("Error importing %s, please check " - "settings.MONGOENGINE_USER_DOCUMENT" - % name) def get(self, *args, **kwargs): try: @@ -85,5 +94,14 @@ class MongoUserManager(UserManager): class MongoUser(models.Model): - objects = MongoUserManager() + """"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() From a69db231cc7696bb78795caeef2e4d2b3c034148 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 31 Jul 2013 11:26:23 +1000 Subject: [PATCH 059/222] Pretty-print GridFSProxy objects --- mongoengine/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 47554e03..39a6caa1 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1082,6 +1082,10 @@ class GridFSProxy(object): def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.grid_id) + + def __unicode__(self): + name = getattr(self.get(), 'filename', self.grid_id) if self.get() else '(no file)' + return '<%s: %s>' % (self.__class__.__name__, name) def __eq__(self, other): if isinstance(other, GridFSProxy): From d8ffa843a9bfbe7c6b645bbab4f0fcd61dd6af43 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 31 Jul 2013 09:29:41 +0000 Subject: [PATCH 060/222] Added str representation of GridFSProxy (#424) --- docs/changelog.rst | 1 + mongoengine/fields.py | 4 ++-- tests/fields/file_tests.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8199e03e..49d79fe0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Added str representation of GridFSProxy (#424) - Update transform to handle docs erroneously passed to unset (#416) - Fixed indexing - turn off _cls (#414) - Fixed dereference threading issue in ComplexField.__get__ (#412) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 39a6caa1..826e1258 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1082,8 +1082,8 @@ class GridFSProxy(object): def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.grid_id) - - def __unicode__(self): + + def __str__(self): name = getattr(self.get(), 'filename', self.grid_id) if self.get() else '(no file)' return '<%s: %s>' % (self.__class__.__name__, name) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index d0445008..ba601dea 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -53,11 +53,12 @@ class FileTest(unittest.TestCase): content_type = 'text/plain' putfile = PutFile() - putfile.the_file.put(text, content_type=content_type) + putfile.the_file.put(text, content_type=content_type, filename="hello") putfile.save() result = PutFile.objects.first() self.assertTrue(putfile == result) + self.assertEqual("%s" % result.the_file, "") self.assertEqual(result.the_file.read(), text) self.assertEqual(result.the_file.content_type, content_type) result.the_file.delete() # Remove file from GridFS From 7431b1f123a84adcc3e7ee9ca86ef18b52d01c5d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 31 Jul 2013 09:31:04 +0000 Subject: [PATCH 061/222] Updated AUTHORS (#424) --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b779da22..938c5c0a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -174,3 +174,4 @@ that much better: * Jatin- (https://github.com/jatin-) * Paul Uithol (https://github.com/PaulUithol) * Thom Knowles (https://github.com/fleat) + * Paul (https://github.com/squamous) \ No newline at end of file From 719bb53c3a01a50c325696f87aaa3fb08256b22f Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 31 Jul 2013 09:44:15 +0000 Subject: [PATCH 062/222] Updated changelog (#423) --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 49d79fe0..d93406db 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Added get_user_document and improve mongo_auth module (#423) - Added str representation of GridFSProxy (#424) - Update transform to handle docs erroneously passed to unset (#416) - Fixed indexing - turn off _cls (#414) From b3f462a39d1f43a7bbc2b887757f340d8fea16cf Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Thu, 1 Aug 2013 03:51:10 +0200 Subject: [PATCH 063/222] updated docs for django shortcuts get_object_or_404 and get_list_or_404 --- docs/django.rst | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/django.rst b/docs/django.rst index da151888..62d4dd41 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -45,7 +45,7 @@ The :mod:`~mongoengine.django.auth` module also contains a 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 @@ -74,7 +74,7 @@ An additional ``MONGOENGINE_USER_DOCUMENT`` setting enables you to replace the 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. @@ -128,7 +128,7 @@ appended to the filename until the generated filename doesn't exist. The >>> fs.listdir() ([], [u'hello.txt']) -All files will be saved and retrieved in GridFS via the :class::`FileDocument` +All files will be saved and retrieved in GridFS via the :class:`FileDocument` document, allowing easy access to the files without the GridFSStorage backend.:: @@ -137,3 +137,36 @@ backend.:: [] .. 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. + From a448c9aebf573d13c7876fc5380e2519aabc302d Mon Sep 17 00:00:00 2001 From: devoto13 Date: Thu, 1 Aug 2013 17:54:41 +0300 Subject: [PATCH 064/222] removed duplicated method --- docs/guide/querying.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 127e4798..f50985b5 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -497,7 +497,6 @@ that you may use with these methods: * ``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 -* ``pop`` -- remove the last item from a list * ``push`` -- append a value to a list * ``push_all`` -- append several values to a list * ``pop`` -- remove the first or last element of a list From b98b06ff79f54318b8329822bcf081cb8953a1da Mon Sep 17 00:00:00 2001 From: Nicolas Cortot Date: Sun, 4 Aug 2013 11:01:09 +0200 Subject: [PATCH 065/222] Fix an error in get_user_document --- mongoengine/django/mongo_auth/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py index 71797185..35960c9e 100644 --- a/mongoengine/django/mongo_auth/models.py +++ b/mongoengine/django/mongo_auth/models.py @@ -15,7 +15,7 @@ MONGOENGINE_USER_DOCUMENT = getattr( settings, 'MONGOENGINE_USER_DOCUMENT', 'mongoengine.django.auth.User') -def get_user_document(self): +def get_user_document(): """Get the user docuemnt class user for authentcation. This is the class defined in settings.MONGOENGINE_USER_DOCUMENT, which From 40b0a15b350cba12ebe37e595352d10c7628a2d3 Mon Sep 17 00:00:00 2001 From: Nicolas Cortot Date: Sun, 4 Aug 2013 11:03:34 +0200 Subject: [PATCH 066/222] Fixing typos --- mongoengine/django/mongo_auth/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py index 35960c9e..d4947a23 100644 --- a/mongoengine/django/mongo_auth/models.py +++ b/mongoengine/django/mongo_auth/models.py @@ -16,7 +16,7 @@ MONGOENGINE_USER_DOCUMENT = getattr( def get_user_document(): - """Get the user docuemnt class user for authentcation. + """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`. From a0d255369aef155fbb0cdd139542cbf5719a4046 Mon Sep 17 00:00:00 2001 From: Nicolas Cortot Date: Sun, 4 Aug 2013 11:08:11 +0200 Subject: [PATCH 067/222] Add a test case for get_user_document --- tests/test_django.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 63e3245a..8fe0da39 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -22,7 +22,11 @@ try: 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 + from mongoengine.django.mongo_auth.models import ( + MongoUser, + MongoUserManager, + get_user_document, + ) DJ15 = True except Exception: DJ15 = False @@ -270,9 +274,12 @@ class MongoAuthTest(unittest.TestCase): User.drop_collection() super(MongoAuthTest, self).setUp() - def test_user_model(self): + 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)) From 5bcc4546783e8ee7200ad7669ed14a40ea67376e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 7 Aug 2013 09:07:57 +0000 Subject: [PATCH 068/222] Handle dynamic fieldnames that look like digits (#434) --- docs/changelog.rst | 1 + mongoengine/base/document.py | 2 +- tests/queryset/queryset.py | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d93406db..b877d4d1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Handle dynamic fieldnames that look like digits (#434) - Added get_user_document and improve mongo_auth module (#423) - Added str representation of GridFSProxy (#424) - Update transform to handle docs erroneously passed to unset (#416) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index b9c07cf5..cea2f09b 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -762,7 +762,7 @@ class BaseDocument(object): for field_name in parts: # Handle ListField indexing: - if field_name.isdigit(): + if field_name.isdigit() and hasattr(field, 'field'): new_field = field.field fields.append(field_name) continue diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 9c04c0be..75708cab 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3299,6 +3299,13 @@ class QuerySetTest(unittest.TestCase): Test.objects(test='foo').update_one(upsert=True, set__test='foo') self.assertTrue('_cls' in Test._collection.find_one()) + def test_update_upsert_looks_like_a_digit(self): + class MyDoc(DynamicDocument): + pass + MyDoc.drop_collection() + self.assertEqual(1, MyDoc.objects.update_one(upsert=True, inc__47=1)) + self.assertEqual(MyDoc.objects.get()['47'], 1) + def test_read_preference(self): class Bar(Document): pass From f30208f3453dd7999987f07cf383e6fd9fb546ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Cort=C3=A8s?= Date: Mon, 12 Aug 2013 19:12:53 +0200 Subject: [PATCH 069/222] Fix the ._get_db() attribute after a Document.switch_db() Without this patch, I've got: ``` myobj._get_db() > ``` I need to `myobj._get_db()()` to get the database. I felt this like a bug. regards, --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 2f3a92a5..1bbd7b73 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -400,7 +400,7 @@ class Document(BaseDocument): """ with switch_db(self.__class__, db_alias) as cls: collection = cls._get_collection() - db = cls._get_db + db = cls._get_db() self._get_collection = lambda: collection self._get_db = lambda: db self._collection = collection From 70b320633ff801ad0885d395e7f6caa64587dbb1 Mon Sep 17 00:00:00 2001 From: crazyzubr Date: Thu, 15 Aug 2013 19:32:13 +0800 Subject: [PATCH 070/222] permit the establishment of a field with the name of size or other Example: # model class Example(Document): size = ReferenceField(Size, verbose_name='Size') # query examples = Example.objects(size=instance_size) # caused an error """ File ".../mongoengine/queryset/transform.py", line 50, in query if parts[-1] == 'not': IndexError: list index out of range """ --- mongoengine/queryset/transform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index e0a7d3c6..d82f33db 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -43,11 +43,11 @@ def query(_doc_cls=None, _field_operation=False, **query): parts = [part for part in parts if not part.isdigit()] # Check for an operator and transform to mongo-style if there is op = None - if parts[-1] in MATCH_OPERATORS: + if len(parts) > 1 and parts[-1] in MATCH_OPERATORS: op = parts.pop() negate = False - if parts[-1] == 'not': + if len(parts) > 1 and parts[-1] == 'not': parts.pop() negate = True From d07a9d2ef8b63fea934ef658678823b4d6073338 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 08:30:20 +0000 Subject: [PATCH 071/222] Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449) --- docs/changelog.rst | 1 + mongoengine/fields.py | 13 ++++++++++++- tests/fields/fields.py | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b877d4d1..fa27d1aa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449) - Handle dynamic fieldnames that look like digits (#434) - Added get_user_document and improve mongo_auth module (#423) - Added str representation of GridFSProxy (#424) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 826e1258..c1fc1a76 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -624,7 +624,9 @@ class DynamicField(BaseField): cls = value.__class__ val = value.to_mongo() # If we its a document thats not inherited add _cls - if (isinstance(value, (Document, EmbeddedDocument))): + if (isinstance(value, Document)): + val = {"_ref": value.to_dbref(), "_cls": cls.__name__} + if (isinstance(value, EmbeddedDocument)): val['_cls'] = cls.__name__ return val @@ -645,6 +647,15 @@ class DynamicField(BaseField): value = [v for k, v in sorted(data.iteritems(), key=itemgetter(0))] return value + def to_python(self, value): + if isinstance(value, dict) and '_cls' in value: + doc_cls = get_document(value['_cls']) + if '_ref' in value: + value = doc_cls._get_db().dereference(value['_ref']) + return doc_cls._from_son(value) + + return super(DynamicField, self).to_python(value) + def lookup_member(self, member_name): return member_name diff --git a/tests/fields/fields.py b/tests/fields/fields.py index b3d8d52e..8791781c 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2506,5 +2506,46 @@ class FieldTest(unittest.TestCase): self.assertTrue(tuple(x.items[0]) in tuples) self.assertTrue(x.items[0] in tuples) + def test_dynamic_fields_class(self): + + class Doc2(Document): + field_1 = StringField(db_field='f') + + class Doc(Document): + my_id = IntField(required=True, unique=True, primary_key=True) + embed_me = DynamicField(db_field='e') + field_x = StringField(db_field='x') + + Doc.drop_collection() + Doc2.drop_collection() + + doc2 = Doc2(field_1="hello") + doc = Doc(my_id=1, embed_me=doc2, field_x="x") + self.assertRaises(OperationError, doc.save) + + doc2.save() + doc.save() + + doc = Doc.objects.get() + self.assertEqual(doc.embed_me.field_1, "hello") + + def test_dynamic_fields_embedded_class(self): + + class Embed(EmbeddedDocument): + field_1 = StringField(db_field='f') + + class Doc(Document): + my_id = IntField(required=True, unique=True, primary_key=True) + embed_me = DynamicField(db_field='e') + field_x = StringField(db_field='x') + + Doc.drop_collection() + + Doc(my_id=1, embed_me=Embed(field_1="hello"), field_x="x").save() + + doc = Doc.objects.get() + self.assertEqual(doc.embed_me.field_1, "hello") + + if __name__ == '__main__': unittest.main() From ee7666ddea20b7a261c7dec021bbe595f015a970 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 08:31:56 +0000 Subject: [PATCH 072/222] Update AUTHORS and Changelog (#441) --- AUTHORS | 3 ++- docs/changelog.rst | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 938c5c0a..2a2eb363 100644 --- a/AUTHORS +++ b/AUTHORS @@ -174,4 +174,5 @@ that much better: * Jatin- (https://github.com/jatin-) * Paul Uithol (https://github.com/PaulUithol) * Thom Knowles (https://github.com/fleat) - * Paul (https://github.com/squamous) \ No newline at end of file + * Paul (https://github.com/squamous) + * Olivier Cortès (https://github.com/Karmak23) \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index fa27d1aa..b0275627 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed ._get_db() attribute after a Document.switch_db() (#441) - Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449) - Handle dynamic fieldnames that look like digits (#434) - Added get_user_document and improve mongo_auth module (#423) From 67baf465f4ebd1024d4730dc206900bb1bd3a1a2 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 09:14:58 +0000 Subject: [PATCH 073/222] Fixed slice when using inheritance causing fields to be excluded (#437) --- docs/changelog.rst | 1 + mongoengine/queryset/field_list.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b0275627..26a07167 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed slice when using inheritance causing fields to be excluded (#437) - Fixed ._get_db() attribute after a Document.switch_db() (#441) - Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449) - Handle dynamic fieldnames that look like digits (#434) diff --git a/mongoengine/queryset/field_list.py b/mongoengine/queryset/field_list.py index 73d3cc24..140a71e0 100644 --- a/mongoengine/queryset/field_list.py +++ b/mongoengine/queryset/field_list.py @@ -55,7 +55,8 @@ class QueryFieldList(object): if self.always_include: if self.value is self.ONLY and self.fields: - self.fields = self.fields.union(self.always_include) + if sorted(self.slice.keys()) != sorted(self.fields): + self.fields = self.fields.union(self.always_include) else: self.fields -= self.always_include From 49f5b4fa5cf9a86ce548123478bcdec94a0698c6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 09:45:00 +0000 Subject: [PATCH 074/222] Fix Queryset docs (#448) --- docs/apireference.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/apireference.rst b/docs/apireference.rst index 774d3b89..9057de55 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -44,17 +44,21 @@ Context Managers Querying ======== -.. autoclass:: mongoengine.queryset.QuerySet - :members: +.. automodule:: mongoengine.queryset + :synopsis: Queryset level operations - .. automethod:: mongoengine.queryset.QuerySet.__call__ + .. autoclass:: mongoengine.queryset.QuerySet + :members: + :inherited-members: -.. autoclass:: mongoengine.queryset.QuerySetNoCache - :members: + .. automethod:: QuerySet.__call__ - .. automethod:: mongoengine.queryset.QuerySetNoCache.__call__ + .. autoclass:: mongoengine.queryset.QuerySetNoCache + :members: -.. autofunction:: mongoengine.queryset.queryset_manager + .. automethod:: mongoengine.queryset.QuerySetNoCache.__call__ + + .. autofunction:: mongoengine.queryset.queryset_manager Fields ====== From 2cd722d751438141d1bcfff5824f49496f2ffddd Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 10:20:05 +0000 Subject: [PATCH 075/222] Updated setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f6b3c1bf..6bd778bd 100644 --- a/setup.py +++ b/setup.py @@ -51,13 +51,13 @@ CLASSIFIERS = [ extra_opts = {} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'django>=1.5.1'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2>=2.6', 'django>=1.5.1'] extra_opts['packages'] = find_packages(exclude=('tests',)) if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'].append("tests") extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} else: - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'PIL', 'jinja2==2.6', 'python-dateutil'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'PIL', 'jinja2>=2.6', 'python-dateutil'] extra_opts['packages'] = find_packages(exclude=('tests',)) setup(name='mongoengine', From 661398d8914bda0821091b7734c75b7b74c2566f Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 10:22:06 +0000 Subject: [PATCH 076/222] Fixed dereference issue with embedded listfield referencefields (#439) --- docs/changelog.rst | 1 + mongoengine/dereference.py | 5 +++-- tests/test_dereference.py | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26a07167..74e2e503 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed dereference issue with embedded listfield referencefields (#439) - Fixed slice when using inheritance causing fields to be excluded (#437) - Fixed ._get_db() attribute after a Document.switch_db() (#441) - Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index e5e8886b..ceda403e 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -4,7 +4,7 @@ from base import (BaseDict, BaseList, TopLevelDocumentMetaclass, get_document) from fields import (ReferenceField, ListField, DictField, MapField) from connection import get_db from queryset import QuerySet -from document import Document +from document import Document, EmbeddedDocument class DeReference(object): @@ -33,7 +33,8 @@ class DeReference(object): self.max_depth = max_depth doc_type = None - if instance and isinstance(instance, (Document, TopLevelDocumentMetaclass)): + if instance and isinstance(instance, (Document, EmbeddedDocument, + TopLevelDocumentMetaclass)): doc_type = instance._fields.get(name) if hasattr(doc_type, 'field'): doc_type = doc_type.field diff --git a/tests/test_dereference.py b/tests/test_dereference.py index db9868a0..6f2664a3 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -1171,6 +1171,30 @@ class FieldTest(unittest.TestCase): self.assertEqual(2, len([brand for bg in brand_groups for brand in bg.brands])) + def test_dereferencing_embedded_listfield_referencefield(self): + class Tag(Document): + meta = {'collection': 'tags'} + name = StringField() + + class Post(EmbeddedDocument): + body = StringField() + tags = ListField(ReferenceField("Tag", dbref=True)) + + class Page(Document): + meta = {'collection': 'pages'} + tags = ListField(ReferenceField("Tag", dbref=True)) + posts = ListField(EmbeddedDocumentField(Post)) + + Tag.drop_collection() + Page.drop_collection() + + tag = Tag(name='test').save() + post = Post(body='test body', tags=[tag]) + Page(tags=[tag], posts=[post]).save() + + page = Page.objects.first() + self.assertEqual(page.tags[0], page.posts[0].tags[0]) + if __name__ == '__main__': unittest.main() From 29c887f30b0f7db13d30c920d29d2b4f2f490047 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 12:21:20 +0000 Subject: [PATCH 077/222] Updated field filter logic - can now exclude subclass fields (#443) --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 31 ++++++++++++++++++++++++++----- tests/queryset/field_list.py | 23 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 74e2e503..489f2ff5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed can now exclude subclass fields (#443) - Fixed dereference issue with embedded listfield referencefields (#439) - Fixed slice when using inheritance causing fields to be excluded (#437) - Fixed ._get_db() attribute after a Document.switch_db() (#441) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index a6ba49b2..7af9daad 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -14,8 +14,9 @@ from pymongo.common import validate_read_preference from mongoengine import signals from mongoengine.common import _import_class +from mongoengine.base.common import get_document from mongoengine.errors import (OperationError, NotUniqueError, - InvalidQueryError) + InvalidQueryError, LookUpError) from mongoengine.queryset import transform from mongoengine.queryset.field_list import QueryFieldList @@ -1333,13 +1334,33 @@ class BaseQuerySet(object): return frequencies - def _fields_to_dbfields(self, fields): + def _fields_to_dbfields(self, fields, subdoc=False): """Translate fields paths to its db equivalents""" ret = [] + subclasses = [] + document = self._document + if document._meta['allow_inheritance']: + subclasses = [get_document(x) + for x in document._subclasses][1:] for field in fields: - field = ".".join(f.db_field for f in - self._document._lookup_field(field.split('.'))) - ret.append(field) + try: + field = ".".join(f.db_field for f in + document._lookup_field(field.split('.'))) + ret.append(field) + except LookUpError, e: + found = False + for subdoc in subclasses: + try: + subfield = ".".join(f.db_field for f in + subdoc._lookup_field(field.split('.'))) + ret.append(subfield) + found = True + break + except LookUpError, e: + pass + + if not found: + raise e return ret def _get_order_by(self, keys): diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index a18e167e..7d66d263 100644 --- a/tests/queryset/field_list.py +++ b/tests/queryset/field_list.py @@ -399,5 +399,28 @@ class OnlyExcludeAllTest(unittest.TestCase): numbers = Numbers.objects.fields(embedded__n={"$slice": [-5, 10]}).get() self.assertEqual(numbers.embedded.n, [-5, -4, -3, -2, -1]) + + def test_exclude_from_subclasses_docs(self): + + class Base(Document): + username = StringField() + + meta = {'allow_inheritance': True} + + class Anon(Base): + anon = BooleanField() + + class User(Base): + password = StringField() + wibble = StringField() + + Base.drop_collection() + User(username="mongodb", password="secret").save() + + user = Base.objects().exclude("password", "wibble").first() + self.assertEqual(user.password, None) + + self.assertRaises(LookUpError, Base.objects.exclude, "made_up") + if __name__ == '__main__': unittest.main() From a707598042fb0312eb01c15b57b4796dd63ad9a1 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 13:13:17 +0000 Subject: [PATCH 078/222] Allow fields to be named the same as query operators (#445) --- AUTHORS | 3 ++- docs/changelog.rst | 3 ++- tests/queryset/queryset.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2a2eb363..a5b73c7b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -175,4 +175,5 @@ that much better: * Paul Uithol (https://github.com/PaulUithol) * Thom Knowles (https://github.com/fleat) * Paul (https://github.com/squamous) - * Olivier Cortès (https://github.com/Karmak23) \ No newline at end of file + * Olivier Cortès (https://github.com/Karmak23) + * crazyzubr (https://github.com/crazyzubr) \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 489f2ff5..27754293 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,8 @@ Changelog Changes in 0.8.4 ================ -- Fixed can now exclude subclass fields (#443) +- Allow fields to be named the same as query operators (#445) +- Updated field filter logic - can now exclude subclass fields (#443) - Fixed dereference issue with embedded listfield referencefields (#439) - Fixed slice when using inheritance causing fields to be excluded (#437) - Fixed ._get_db() attribute after a Document.switch_db() (#441) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 75708cab..7f641351 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3691,6 +3691,23 @@ class QuerySetTest(unittest.TestCase): '_cls': 'Animal.Cat' }) + def test_can_have_field_same_name_as_query_operator(self): + + class Size(Document): + name = StringField() + + class Example(Document): + size = ReferenceField(Size) + + Size.drop_collection() + Example.drop_collection() + + instance_size = Size(name="Large").save() + Example(size=instance_size).save() + + self.assertEqual(Example.objects(size=instance_size).count(), 1) + self.assertEqual(Example.objects(size__in=[instance_size]).count(), 1) + if __name__ == '__main__': unittest.main() From 0dd01bda016e44aca102d4998bf7c1a0a89739e9 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 15:54:42 +0000 Subject: [PATCH 079/222] Fixed "$pull" semantics for nested ListFields (#447) --- AUTHORS | 3 +- docs/changelog.rst | 1 + mongoengine/common.py | 5 ++- mongoengine/fields.py | 4 ++ mongoengine/queryset/transform.py | 25 ++++++++++- tests/queryset/queryset.py | 71 ++++++++++++++++++++++++++++--- 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/AUTHORS b/AUTHORS index a5b73c7b..452ba370 100644 --- a/AUTHORS +++ b/AUTHORS @@ -176,4 +176,5 @@ that much better: * Thom Knowles (https://github.com/fleat) * Paul (https://github.com/squamous) * Olivier Cortès (https://github.com/Karmak23) - * crazyzubr (https://github.com/crazyzubr) \ No newline at end of file + * crazyzubr (https://github.com/crazyzubr) + * FrankSomething (https://github.com/FrankSomething) \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 27754293..6a0258cc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Fixed "$pull" semantics for nested ListFields (#447) - Allow fields to be named the same as query operators (#445) - Updated field filter logic - can now exclude subclass fields (#443) - Fixed dereference issue with embedded listfield referencefields (#439) diff --git a/mongoengine/common.py b/mongoengine/common.py index 20d51387..6303231e 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -23,8 +23,9 @@ def _import_class(cls_name): field_classes = ('DictField', 'DynamicField', 'EmbeddedDocumentField', 'FileField', 'GenericReferenceField', 'GenericEmbeddedDocumentField', 'GeoPointField', - 'PointField', 'LineStringField', 'PolygonField', - 'ReferenceField', 'StringField', 'ComplexBaseField') + 'PointField', 'LineStringField', 'ListField', + 'PolygonField', 'ReferenceField', 'StringField', + 'ComplexBaseField') queryset_classes = ('OperationError',) deref_classes = ('DeReference',) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index c1fc1a76..419f2ef7 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -780,6 +780,10 @@ class DictField(ComplexBaseField): if op in match_operators and isinstance(value, basestring): return StringField().prepare_query_value(op, value) + + if hasattr(self.field, 'field'): + return self.field.prepare_query_value(op, value) + return super(DictField, self).prepare_query_value(op, value) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index d82f33db..2ee7e386 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -182,6 +182,7 @@ def update(_doc_cls=None, **update): parts = [] cleaned_fields = [] + appended_sub_field = False for field in fields: append_field = True if isinstance(field, basestring): @@ -193,10 +194,17 @@ def update(_doc_cls=None, **update): else: parts.append(field.db_field) if append_field: + appended_sub_field = False cleaned_fields.append(field) + if hasattr(field, 'field'): + cleaned_fields.append(field.field) + appended_sub_field = True # Convert value to proper value - field = cleaned_fields[-1] + if appended_sub_field: + field = cleaned_fields[-2] + else: + field = cleaned_fields[-1] if op in (None, 'set', 'push', 'pull'): if field.required or value is not None: @@ -223,11 +231,24 @@ def update(_doc_cls=None, **update): if 'pull' in op and '.' in key: # Dot operators don't work on pull operations - # it uses nested dict syntax + # unless they point to a list field + # Otherwise it uses nested dict syntax if op == 'pullAll': raise InvalidQueryError("pullAll operations only support " "a single field depth") + # Look for the last list field and use dot notation until there + field_classes = [c.__class__ for c in cleaned_fields] + field_classes.reverse() + ListField = _import_class('ListField') + if ListField in field_classes: + # Join all fields via dot notation to the last ListField + # Then process as normal + last_listField = len(cleaned_fields) - field_classes.index(ListField) + key = ".".join(parts[:last_listField]) + parts = parts[last_listField:] + parts.insert(0, key) + parts.reverse() for key in parts: value = {key: value} diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7f641351..b4bcf2a7 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1497,9 +1497,6 @@ class QuerySetTest(unittest.TestCase): def test_pull_nested(self): - class User(Document): - name = StringField() - class Collaborator(EmbeddedDocument): user = StringField() @@ -1514,8 +1511,7 @@ class QuerySetTest(unittest.TestCase): Site.drop_collection() c = Collaborator(user='Esteban') - s = Site(name="test", collaborators=[c]) - s.save() + s = Site(name="test", collaborators=[c]).save() Site.objects(id=s.id).update_one(pull__collaborators__user='Esteban') self.assertEqual(Site.objects.first().collaborators, []) @@ -1525,6 +1521,71 @@ class QuerySetTest(unittest.TestCase): self.assertRaises(InvalidQueryError, pull_all) + def test_pull_from_nested_embedded(self): + + class User(EmbeddedDocument): + name = StringField() + + def __unicode__(self): + return '%s' % self.name + + class Collaborator(EmbeddedDocument): + helpful = ListField(EmbeddedDocumentField(User)) + unhelpful = ListField(EmbeddedDocumentField(User)) + + class Site(Document): + name = StringField(max_length=75, unique=True, required=True) + collaborators = EmbeddedDocumentField(Collaborator) + + + Site.drop_collection() + + c = User(name='Esteban') + f = User(name='Frank') + s = Site(name="test", collaborators=Collaborator(helpful=[c], unhelpful=[f])).save() + + Site.objects(id=s.id).update_one(pull__collaborators__helpful=c) + self.assertEqual(Site.objects.first().collaborators['helpful'], []) + + Site.objects(id=s.id).update_one(pull__collaborators__unhelpful={'name': 'Frank'}) + self.assertEqual(Site.objects.first().collaborators['unhelpful'], []) + + def pull_all(): + Site.objects(id=s.id).update_one(pull_all__collaborators__helpful__name=['Ross']) + + self.assertRaises(InvalidQueryError, pull_all) + + def test_pull_from_nested_mapfield(self): + + class Collaborator(EmbeddedDocument): + user = StringField() + + def __unicode__(self): + return '%s' % self.user + + class Site(Document): + name = StringField(max_length=75, unique=True, required=True) + collaborators = MapField(ListField(EmbeddedDocumentField(Collaborator))) + + + Site.drop_collection() + + c = Collaborator(user='Esteban') + f = Collaborator(user='Frank') + s = Site(name="test", collaborators={'helpful':[c],'unhelpful':[f]}) + s.save() + + Site.objects(id=s.id).update_one(pull__collaborators__helpful__user='Esteban') + self.assertEqual(Site.objects.first().collaborators['helpful'], []) + + Site.objects(id=s.id).update_one(pull__collaborators__unhelpful={'user':'Frank'}) + self.assertEqual(Site.objects.first().collaborators['unhelpful'], []) + + def pull_all(): + Site.objects(id=s.id).update_one(pull_all__collaborators__helpful__user=['Ross']) + + self.assertRaises(InvalidQueryError, pull_all) + def test_update_one_pop_generic_reference(self): class BlogTag(Document): From a0ef649dd8777c874927686ef02b533590420390 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 18:31:33 +0000 Subject: [PATCH 080/222] Update travis.yml --- .travis.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7e8ea3c..609d8987 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,10 @@ env: install: - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install django==$DJANGO --use-mirrors ; true; fi - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi - pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b + - pip install django==$DJANGO --use-mirrors - python setup.py install script: - python setup.py test diff --git a/setup.py b/setup.py index 6bd778bd..f5498f19 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ CLASSIFIERS = [ extra_opts = {} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2>=2.6', 'django>=1.5.1'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'django>=1.5.1'] extra_opts['packages'] = find_packages(exclude=('tests',)) if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'].append("tests") From 200e52bab50906761fa09b8a34f0cac5ddec335e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 18:44:12 +0000 Subject: [PATCH 081/222] Added documentation about abstract meta Refs #438 --- docs/guide/defining-documents.rst | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 407fbdac..ba1af333 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -689,7 +689,6 @@ document.:: .. note:: From 0.8 onwards you must declare :attr:`allow_inheritance` defaults to False, meaning you must set it to True to use inheritance. - Working with existing data -------------------------- As MongoEngine no longer defaults to needing :attr:`_cls` you can quickly and @@ -709,3 +708,25 @@ defining all possible field types. If you use :class:`~mongoengine.Document` and the database contains data that isn't defined then that data will be stored in the `document._data` dictionary. + +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`. +This won't turn on :ref:`document-inheritance` but will allow you to keep your +code DRY:: + + class BaseDocument(Document): + meta = { + 'abstract': True, + } + def check_permissions(self): + ... + + class User(BaseDocument): + ... + +Now the User class will have access to the inherited `check_permissions` method +and won't store any of the extra `_cls` information. From fffd0e899019c33e08cb53bdf7ce9492884a4e3e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 20 Aug 2013 18:54:14 +0000 Subject: [PATCH 082/222] Fixed error raise --- mongoengine/queryset/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7af9daad..b4dad0c5 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1347,7 +1347,7 @@ class BaseQuerySet(object): field = ".".join(f.db_field for f in document._lookup_field(field.split('.'))) ret.append(field) - except LookUpError, e: + except LookUpError, err: found = False for subdoc in subclasses: try: @@ -1360,7 +1360,7 @@ class BaseQuerySet(object): pass if not found: - raise e + raise err return ret def _get_order_by(self, keys): From f57569f553ab2ffd1947db0a4c014e5026cd0f0d Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Wed, 21 Aug 2013 13:52:24 +0400 Subject: [PATCH 083/222] Remove database name necessity in uri connection schema --- docs/guide/connecting.rst | 7 +++++-- mongoengine/connection.py | 5 +---- tests/test_connection.py | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index 854e2c30..f681aada 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -23,12 +23,15 @@ arguments should be provided:: connect('project1', username='webapp', password='pwd123') -Uri style connections are also supported as long as you include the database -name - just supply the uri as the :attr:`host` to +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` + ReplicaSets =========== diff --git a/mongoengine/connection.py b/mongoengine/connection.py index abab269f..4275da53 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -55,12 +55,9 @@ def register_connection(alias, name, host='localhost', port=27017, # Handle uri style connections if "://" in host: uri_dict = uri_parser.parse_uri(host) - if uri_dict.get('database') is None: - raise ConnectionError("If using URI style connection include "\ - "database name in string") conn_settings.update({ 'host': host, - 'name': uri_dict.get('database'), + 'name': uri_dict.get('database') or name, 'username': uri_dict.get('username'), 'password': uri_dict.get('password'), 'read_preference': read_preference, diff --git a/tests/test_connection.py b/tests/test_connection.py index d27a66db..62d795c8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -59,6 +59,32 @@ class ConnectionTest(unittest.TestCase): c.admin.system.users.remove({}) 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 + """ + c = connect(db='mongoenginetest', alias='admin') + c.admin.system.users.remove({}) + c.mongoenginetest.system.users.remove({}) + + c.admin.add_user("admin", "password") + c.admin.authenticate("admin", "password") + c.mongoenginetest.add_user("username", "password") + + self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') + + connect("mongoenginetest", host='mongodb://localhost/') + + conn = get_connection() + self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + + db = get_db() + self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertEqual(db.name, 'mongoenginetest') + + c.admin.system.users.remove({}) + c.mongoenginetest.system.users.remove({}) + def test_register_connection(self): """Ensure that connections with different aliases may be registered. """ From f4db0da58581dc4956add7278c625e9fd37a4f7c Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 23 Aug 2013 09:03:51 +0000 Subject: [PATCH 084/222] Update changelog add LK4D4 to authors (#452) --- AUTHORS | 3 ++- docs/changelog.rst | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 452ba370..f043207b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -177,4 +177,5 @@ that much better: * Paul (https://github.com/squamous) * Olivier Cortès (https://github.com/Karmak23) * crazyzubr (https://github.com/crazyzubr) - * FrankSomething (https://github.com/FrankSomething) \ No newline at end of file + * FrankSomething (https://github.com/FrankSomething) + * Alexandr Morozov (https://github.com/LK4D4) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6a0258cc..926fb8a8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.4 ================ +- Remove database name necessity in uri connection schema (#452) - Fixed "$pull" semantics for nested ListFields (#447) - Allow fields to be named the same as query operators (#445) - Updated field filter logic - can now exclude subclass fields (#443) From 23843ec86e4bdced0996de4a68d01bbf61e86a31 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 23 Aug 2013 09:06:57 +0000 Subject: [PATCH 085/222] Updated travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 609d8987..26b502c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ install: - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi - pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - - pip install django==$DJANGO --use-mirrors + - pip install django==$DJANGO -U --use-mirrors - python setup.py install script: - python setup.py test From 6738a9433b86fa2e7b4fef6bf124d5c166f5652b Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 23 Aug 2013 09:36:33 +0000 Subject: [PATCH 086/222] Updated travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 26b502c2..4395107e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ install: - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi - pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - - pip install django==$DJANGO -U --use-mirrors - python setup.py install script: - python setup.py test From 86c8929d774483ed992557210b7a83a7bc7579cf Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 23 Aug 2013 10:03:10 +0000 Subject: [PATCH 087/222] 0.8.4 is a go --- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index bfa35fb5..2b68b3cc 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 3) +VERSION = (0, 8, 4) def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 512c621b..b9c45efa 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.3 +Version: 0.8.4 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From bcbe740598747c97d1911ecad8c2865887363df8 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 23 Aug 2013 13:41:15 +0000 Subject: [PATCH 088/222] Updated setup.py --- setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index f5498f19..85707d00 100644 --- a/setup.py +++ b/setup.py @@ -48,17 +48,15 @@ CLASSIFIERS = [ 'Topic :: Software Development :: Libraries :: Python Modules', ] -extra_opts = {} +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', 'django>=1.5.1'] - extra_opts['packages'] = find_packages(exclude=('tests',)) if "test" in sys.argv or "nosetests" in sys.argv: - extra_opts['packages'].append("tests") + 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', 'PIL', 'jinja2>=2.6', 'python-dateutil'] - extra_opts['packages'] = find_packages(exclude=('tests',)) setup(name='mongoengine', version=VERSION, From 168ecd67b0009bd64a74c5b188d6cce816caaeac Mon Sep 17 00:00:00 2001 From: "bool.dev" Date: Fri, 6 Sep 2013 22:05:31 +0530 Subject: [PATCH 089/222] Fixes #458, DecimalField now ignores incorrect values until validate is called, just like FloatField. --- mongoengine/fields.py | 5 ++++- tests/fields/fields.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 419f2ef7..cf027258 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -304,7 +304,10 @@ class DecimalField(BaseField): return value # Convert to string for python 2.6 before casting to Decimal - value = decimal.Decimal("%s" % value) + try: + value = decimal.Decimal("%s" % value) + except decimal.InvalidOperation: + return value return value.quantize(self.precision, rounding=self.rounding) def to_mongo(self, value): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 8791781c..f119bd59 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -384,6 +384,9 @@ class FieldTest(unittest.TestCase): person.height = 4.0 self.assertRaises(ValidationError, person.validate) + person_2 = Person(height='something invalid') + self.assertRaises(ValidationError, person_2.validate) + def test_decimal_validation(self): """Ensure that invalid values cannot be assigned to decimal fields. """ @@ -405,6 +408,11 @@ class FieldTest(unittest.TestCase): self.assertRaises(ValidationError, person.validate) person.height = Decimal('4.0') self.assertRaises(ValidationError, person.validate) + person.height = 'something invalid' + self.assertRaises(ValidationError, person.validate) + + person_2 = Person(height='something invalid') + self.assertRaises(ValidationError, person_2.validate) Person.drop_collection() From 2f9964e46efc54908cf8ab20dcd6fafcbe42c227 Mon Sep 17 00:00:00 2001 From: Christopher Adams Date: Tue, 17 Sep 2013 00:02:36 -0400 Subject: [PATCH 090/222] Expanded example of using ListFields for one-to-many relationships. - Added an example of how to use the atomic update operations to pull and push to a ListField containing ReferenceFields. --- docs/guide/defining-documents.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 3ee77961..3ef18e32 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -287,6 +287,12 @@ instance of the object to the query:: # Find all pages that both Bob and John have authored Page.objects(authors__all=[bob, john]) + # Remove Bob from the authors for a page. + Page.objects(id='...').update_one(pull__authors=bob) + + # Add John to the authors for a page. + Page.objects(id='...').update_one(push__authors=john) + Dealing with deletion of referred documents ''''''''''''''''''''''''''''''''''''''''''' From 89785da1c5f0f3166fc6cf5cf03dcc6e4136bf6d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 16 Sep 2013 23:50:13 -0700 Subject: [PATCH 091/222] fix validation for a nested DictField --- mongoengine/fields.py | 19 +++++++++++++++++-- tests/fields/fields.py | 6 ++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 419f2ef7..2635b817 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -735,6 +735,21 @@ class SortedListField(ListField): reverse=self._order_reverse) return sorted(value, reverse=self._order_reverse) +def key_not_string(d): + """ Helper function to recursively determine if any key in a dictionary is + not a string. + """ + for k, v in d.items(): + if not isinstance(k, basestring) or (isinstance(v, dict) and key_not_string(v)): + return True + +def key_has_dot_or_dollar(d): + """ Helper function to recursively determine if any key in a dictionary + contains a dot or a dollar sign. + """ + for k, v in d.items(): + if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)): + return True class DictField(ComplexBaseField): """A dictionary field that wraps a standard Python dictionary. This is @@ -761,11 +776,11 @@ class DictField(ComplexBaseField): if not isinstance(value, dict): self.error('Only dictionaries may be used in a DictField') - if any(k for k in value.keys() if not isinstance(k, basestring)): + if key_not_string(value): msg = ("Invalid dictionary key - documents must " "have only string keys") self.error(msg) - if any(('.' in k or '$' in k) for k in value.keys()): + if key_has_dot_or_dollar(value): self.error('Invalid dictionary key name - keys may not contain "."' ' or "$" characters') super(DictField, self).validate(value) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 8791781c..a9db8619 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1109,9 +1109,15 @@ class FieldTest(unittest.TestCase): post.info = {'$title': 'test'} self.assertRaises(ValidationError, post.validate) + post.info = {'nested': {'$title': 'test'}} + self.assertRaises(ValidationError, post.validate) + post.info = {'the.title': 'test'} self.assertRaises(ValidationError, post.validate) + post.info = {'nested': {'the.title': 'test'}} + self.assertRaises(ValidationError, post.validate) + post.info = {1: 'test'} self.assertRaises(ValidationError, post.validate) From 654cca82a9ee44527b22418c4f751e98efd9427a Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Wed, 18 Sep 2013 11:38:38 -0600 Subject: [PATCH 092/222] Fixes AttributeError when using storage.exists() on a non-existing file. --- mongoengine/django/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/django/storage.py b/mongoengine/django/storage.py index 341455cd..9df6f9e8 100644 --- a/mongoengine/django/storage.py +++ b/mongoengine/django/storage.py @@ -76,7 +76,7 @@ class GridFSStorage(Storage): """Find the documents in the store with the given name """ docs = self.document.objects - doc = [d for d in docs if getattr(d, self.field).name == name] + 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: From 66d9182e5056a626d36128e3cc910e7395c58a51 Mon Sep 17 00:00:00 2001 From: swistakm Date: Fri, 20 Sep 2013 12:43:26 +0200 Subject: [PATCH 093/222] fix broken external docs link --- 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 b4dad0c5..ad75eccb 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -850,7 +850,7 @@ class BaseQuerySet(object): :param output: output collection name, if set to 'inline' will try to use :class:`~pymongo.collection.Collection.inline_map_reduce` This can also be a dictionary containing output options - see: http://docs.mongodb.org/manual/reference/commands/#mapReduce + see: http://docs.mongodb.org/manual/reference/command/mapReduce/#dbcmd.mapReduce :param finalize_f: finalize function, an optional function that performs any post-reduction processing. :param scope: values to insert into map/reduce global scope. Optional. From c8a33b83f103b9974305f4bbf7f454d7562397e7 Mon Sep 17 00:00:00 2001 From: mishudark Date: Sun, 29 Sep 2013 20:25:26 +0200 Subject: [PATCH 094/222] fix(session): session serializer to json --- mongoengine/django/sessions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mongoengine/django/sessions.py b/mongoengine/django/sessions.py index 7e4e182f..3528dcca 100644 --- a/mongoengine/django/sessions.py +++ b/mongoengine/django/sessions.py @@ -55,6 +55,12 @@ 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, From 7c254c6136ef827f44bb9ced2261e14485c1c2e6 Mon Sep 17 00:00:00 2001 From: Joe Friedl Date: Fri, 4 Oct 2013 14:18:36 -0400 Subject: [PATCH 095/222] Use defaults when host and port are passed as None --- mongoengine/connection.py | 13 ++++++------- tests/test_connection.py | 8 ++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 4275da53..7cc626f4 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -18,7 +18,7 @@ _connections = {} _dbs = {} -def register_connection(alias, name, host='localhost', port=27017, +def register_connection(alias, name, host=None, port=None, is_slave=False, read_preference=False, slaves=None, username=None, password=None, **kwargs): """Add a connection. @@ -43,8 +43,8 @@ def register_connection(alias, name, host='localhost', port=27017, conn_settings = { 'name': name, - 'host': host, - 'port': port, + 'host': host or 'localhost', + 'port': port or 27017, 'is_slave': is_slave, 'slaves': slaves or [], 'username': username, @@ -53,16 +53,15 @@ def register_connection(alias, name, host='localhost', port=27017, } # Handle uri style connections - if "://" in host: - uri_dict = uri_parser.parse_uri(host) + if "://" in conn_settings['host']: + uri_dict = uri_parser.parse_uri(conn_settings['host']) conn_settings.update({ - 'host': host, 'name': uri_dict.get('database') or name, 'username': uri_dict.get('username'), 'password': uri_dict.get('password'), 'read_preference': read_preference, }) - if "replicaSet" in host: + if "replicaSet" in conn_settings['host']: conn_settings['replicaSet'] = True conn_settings.update(kwargs) diff --git a/tests/test_connection.py b/tests/test_connection.py index 62d795c8..96135bc5 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -98,6 +98,14 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'mongoenginetest2') + def test_register_connection_defaults(self): + """Ensure that defaults are used when the host and port are None. + """ + register_connection('testdb', 'mongoenginetest', host=None, port=None) + + conn = get_connection('testdb') + self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + def test_connection_kwargs(self): """Ensure that connection kwargs get passed to pymongo. """ From a2dd8cb6b9b85d10eed3fc98fb23d1a185d7efd1 Mon Sep 17 00:00:00 2001 From: Dan Ward Date: Sat, 5 Oct 2013 20:39:41 +0100 Subject: [PATCH 096/222] Added app_label Meta attribute to MongoUser class --- mongoengine/django/mongo_auth/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py index d4947a23..c391fc9d 100644 --- a/mongoengine/django/mongo_auth/models.py +++ b/mongoengine/django/mongo_auth/models.py @@ -105,3 +105,6 @@ class MongoUser(models.Model): """ objects = MongoUserManager() + + class Meta: + app_label = 'mongo_auth' From eb975d7e1300e94be8181fde454f653c9b9c9d5c Mon Sep 17 00:00:00 2001 From: Dan Ward Date: Sat, 5 Oct 2013 20:46:51 +0100 Subject: [PATCH 097/222] Added entry to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index f043207b..146aa6d4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -179,3 +179,4 @@ that much better: * crazyzubr (https://github.com/crazyzubr) * FrankSomething (https://github.com/FrankSomething) * Alexandr Morozov (https://github.com/LK4D4) + * Daniel Ward (https://github.com/danielward) From ebf7056f4a04119d46114b91b2805bf1434a1f20 Mon Sep 17 00:00:00 2001 From: Mitar Date: Sun, 6 Oct 2013 02:55:02 -0700 Subject: [PATCH 098/222] Test for testing invalid dict field value. --- tests/fields/fields.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 3d272181..77cbda32 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2560,6 +2560,20 @@ class FieldTest(unittest.TestCase): doc = Doc.objects.get() self.assertEqual(doc.embed_me.field_1, "hello") + def test_invalid_dict_value(self): + class DictFieldTest(Document): + dictionary = DictField(required=True) + + DictFieldTest.drop_collection() + + test = DictFieldTest(dictionary=None) + test.dictionary # Just access to test getter + self.assertRaises(ValidationError, test.validate) + + test = DictFieldTest(dictionary=False) + test.dictionary # Just access to test getter + self.assertRaises(ValidationError, test.validate) + if __name__ == '__main__': unittest.main() From fc4b247f4f01072a5895bbc2cf520a847a2e3d39 Mon Sep 17 00:00:00 2001 From: rfkrocktk Date: Mon, 14 Oct 2013 13:13:34 -0700 Subject: [PATCH 099/222] Patch for #491 Fix for issue #491, a bug in the permissions system. --- mongoengine/django/auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py index cff4b743..33a48eb6 100644 --- a/mongoengine/django/auth.py +++ b/mongoengine/django/auth.py @@ -117,7 +117,7 @@ class Permission(Document): class Meta: verbose_name = _('permission') - verbose_name_plural = _('permissions') + verbose_name_plural = _('permissions')U # unique_together = (('content_type', 'codename'),) # ordering = ('content_type__app_label', 'content_type__model', 'codename') @@ -229,6 +229,9 @@ class User(Document): 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'] From 1978dc80eb65025290d81e79a46222bc4ea54696 Mon Sep 17 00:00:00 2001 From: Loic Raucy Date: Thu, 17 Oct 2013 16:26:19 +0200 Subject: [PATCH 100/222] Added regression test for bug with DictField and numeric keys. When a DictField has numeric fields, BaseDocument._lookup_field doesn't work correclty. --- tests/queryset/queryset.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b4bcf2a7..73178cca 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3367,6 +3367,17 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(1, MyDoc.objects.update_one(upsert=True, inc__47=1)) self.assertEqual(MyDoc.objects.get()['47'], 1) + def test_dictfield_key_looks_like_a_digit(self): + """Only should work with DictField even if they have numeric keys.""" + + class MyDoc(Document): + test = DictField() + + MyDoc.drop_collection() + doc = MyDoc(test={'47': 1}) + doc.save() + self.assertEqual(MyDoc.objects.only('test__47').get().test['47'], 1) + def test_read_preference(self): class Bar(Document): pass From 9ed138f896baf7ecef2f941da92de3ab6e831c91 Mon Sep 17 00:00:00 2001 From: Loic Raucy Date: Thu, 17 Oct 2013 16:32:47 +0200 Subject: [PATCH 101/222] Fixed bug with numeric valuedict keys and BaseDocument._lookup_field(). --- mongoengine/base/document.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index cea2f09b..86838226 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -755,6 +755,9 @@ class BaseDocument(object): """Lookup a field based on its attribute and return a list containing the field's parents and the field. """ + + from mongoengine.fields import ListField + if not isinstance(parts, (list, tuple)): parts = [parts] fields = [] @@ -762,7 +765,7 @@ class BaseDocument(object): for field_name in parts: # Handle ListField indexing: - if field_name.isdigit() and hasattr(field, 'field'): + if field_name.isdigit() and isinstance(field, ListField): new_field = field.field fields.append(field_name) continue From 27b846717f521c3c96885be9bac4ba2bbb8c9af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9s=20Angulo?= Date: Thu, 17 Oct 2013 16:37:31 -0500 Subject: [PATCH 102/222] Fixed bug for count method when _none is True If my queryset have elements example: qs.all().count() => 10 q = qs.all().none() the count in queryset "q" must be 0 q.count() => 0 --- mongoengine/queryset/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index ad75eccb..2a93e649 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -331,7 +331,7 @@ class BaseQuerySet(object): :meth:`skip` that has been applied to this cursor into account when getting the count """ - if self._limit == 0 and with_limit_and_skip: + if self._limit == 0 and with_limit_and_skip or self._none: return 0 return self._cursor.count(with_limit_and_skip=with_limit_and_skip) @@ -1491,4 +1491,4 @@ class BaseQuerySet(object): msg = ("Doc.objects()._ensure_indexes() is deprecated. " "Use Doc.ensure_indexes() instead.") warnings.warn(msg, DeprecationWarning) - self._document.__class__.ensure_indexes() \ No newline at end of file + self._document.__class__.ensure_indexes() From f2c25b47448a6742fd08d7ce68c47dc85d1ce120 Mon Sep 17 00:00:00 2001 From: Dmytro Popovych Date: Sun, 20 Oct 2013 20:08:00 +0300 Subject: [PATCH 103/222] Error in GenericReferenceField serialization was fixed --- mongoengine/fields.py | 5 ++++- tests/fields/fields.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 036a00c2..c69d14e6 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1022,7 +1022,10 @@ class GenericReferenceField(BaseField): id_ = id_field.to_mongo(id_) collection = document._get_collection_name() ref = DBRef(collection, id_) - return {'_cls': document._class_name, '_ref': ref} + return SON(( + ('_cls', document._class_name), + ('_ref', ref) + )) def prepare_query_value(self, op, value): if value is None: diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 3d272181..52817479 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1904,6 +1904,37 @@ class FieldTest(unittest.TestCase): Post.drop_collection() User.drop_collection() + def test_generic_reference_list_item_modification(self): + """Ensure that modifications of related documents (through generic reference) don't influence on querying + """ + class Post(Document): + title = StringField() + + class User(Document): + username = StringField() + bookmarks = ListField(GenericReferenceField()) + + Post.drop_collection() + User.drop_collection() + + post_1 = Post(title="Behind the Scenes of the Pavement Reunion") + post_1.save() + + user = User(bookmarks=[post_1]) + user.save() + + post_1.title = "Title was modified" + user.username = "New username" + user.save() + + user = User.objects(bookmarks__all=[post_1]).first() + + self.assertIsNotNone(user) + self.assertEqual(user.bookmarks[0], post_1) + + Post.drop_collection() + User.drop_collection() + def test_binary_fields(self): """Ensure that binary fields can be stored and retrieved. """ From 11d4f6499aa85646a5ba85f524e92681498e2a40 Mon Sep 17 00:00:00 2001 From: Dmytro Popovych Date: Sun, 20 Oct 2013 20:21:57 +0300 Subject: [PATCH 104/222] Python 2.5-2.6 compatibility fix --- 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 52817479..3fc218fa 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1929,7 +1929,7 @@ class FieldTest(unittest.TestCase): user = User.objects(bookmarks__all=[post_1]).first() - self.assertIsNotNone(user) + self.assertNotEqual(user, None) self.assertEqual(user.bookmarks[0], post_1) Post.drop_collection() From c18f8c92e73fd1d35bb387ee0b4c7e196c6ef8df Mon Sep 17 00:00:00 2001 From: j0hnsmith Date: Mon, 11 Nov 2013 15:13:41 +0000 Subject: [PATCH 105/222] added BSONSerializer --- docs/django.rst | 5 +++++ mongoengine/django/sessions.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/django.rst b/docs/django.rst index 62d4dd41..5d2423f2 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -90,10 +90,15 @@ session backend, ensure that your settings module has 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 diff --git a/mongoengine/django/sessions.py b/mongoengine/django/sessions.py index 7e4e182f..ac7c0356 100644 --- a/mongoengine/django/sessions.py +++ b/mongoengine/django/sessions.py @@ -1,3 +1,4 @@ +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 @@ -103,3 +104,15 @@ class SessionStore(SessionBase): 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')) + From dfe8987aaa0bf25af391fdf92d96bc8b12b8027f Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 09:09:16 +0000 Subject: [PATCH 106/222] Updated geo tests --- tests/fields/geo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/fields/geo.py b/tests/fields/geo.py index 31ded26a..be29a0b2 100644 --- a/tests/fields/geo.py +++ b/tests/fields/geo.py @@ -75,6 +75,12 @@ class GeoFieldTest(unittest.TestCase): self._test_for_expected_error(Location, coord, expected) Location(loc=[1, 2]).validate() + Location(loc={ + "type": "Point", + "coordinates": [ + 81.4471435546875, + 23.61432859499169 + ]}).validate() def test_linestring_validation(self): class Location(Document): From 7c938712f238bd8384703a2242efadc23a65cdc3 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 09:11:08 +0000 Subject: [PATCH 107/222] Updates to docs ref: #454, #480 --- AUTHORS | 1 + docs/changelog.rst | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/AUTHORS b/AUTHORS index f043207b..316a7441 100644 --- a/AUTHORS +++ b/AUTHORS @@ -179,3 +179,4 @@ that much better: * crazyzubr (https://github.com/crazyzubr) * FrankSomething (https://github.com/FrankSomething) * Alexandr Morozov (https://github.com/LK4D4) + * mishudark (https://github.com/mishudark) diff --git a/docs/changelog.rst b/docs/changelog.rst index 926fb8a8..95272ba3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +Changes in 0.8.5 +================ +- Fixed Django 1.6 sessions (#454, #480) + Changes in 0.8.4 ================ - Remove database name necessity in uri connection schema (#452) From 48a5679087215ffbdab59ce74cd8a8a74cf84e89 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 09:13:37 +0000 Subject: [PATCH 108/222] Added Django 1.6 to travis Refs: #480, #454 --- .travis.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4395107e..5739909b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,16 @@ python: - "3.2" - "3.3" env: - - PYMONGO=dev DJANGO=1.5.1 - - PYMONGO=dev DJANGO=1.4.2 - - PYMONGO=2.5 DJANGO=1.5.1 - - PYMONGO=2.5 DJANGO=1.4.2 - - PYMONGO=3.2 DJANGO=1.5.1 - - PYMONGO=3.3 DJANGO=1.5.1 + - PYMONGO=dev DJANGO=1.6 + - PYMONGO=dev DJANGO=1.5.5 + - PYMONGO=dev DJANGO=1.4.10 + - PYMONGO=2.5 DJANGO=1.6 + - PYMONGO=2.5 DJANGO=1.5.5 + - PYMONGO=2.5 DJANGO=1.4.10 + - PYMONGO=3.2 DJANGO=1.6 + - PYMONGO=3.2 DJANGO=1.5.5 + - PYMONGO=3.3 DJANGO=1.6 + - PYMONGO=3.3 DJANGO=1.5.5 install: - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi From c28d9135d908521c1e3434650a1202e77e88503d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 09:48:53 +0000 Subject: [PATCH 109/222] Fixed distinct casting issue with ListField of EmbeddedDocuments (#470) --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 11 +++++++++-- tests/queryset/queryset.py | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 95272ba3..5c3b02d0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Fixed distinct casting issue with ListField of EmbeddedDocuments (#470) - Fixed Django 1.6 sessions (#454, #480) Changes in 0.8.4 diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index ad75eccb..c7fe276c 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -621,8 +621,15 @@ class BaseQuerySet(object): try: field = self._fields_to_dbfields([field]).pop() finally: - return self._dereference(queryset._cursor.distinct(field), 1, - name=field, instance=self._document) + 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", None) + instance = getattr(doc_field, "document_type", False) + if instance: + distinct = [instance(**doc) for doc in distinct] + return distinct def only(self, *fields): """Load only a subset of this document's fields. :: diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b4bcf2a7..4b2ec6b4 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2519,6 +2519,27 @@ class QuerySetTest(unittest.TestCase): Product.drop_collection() + def test_distinct_ListField_EmbeddedDocumentField(self): + + class Author(EmbeddedDocument): + name = StringField() + + class Book(Document): + title = StringField() + authors = ListField(EmbeddedDocumentField(Author)) + + Book.drop_collection() + + mark_twain = Author(name="Mark Twain") + john_tolkien = Author(name="John Ronald Reuel Tolkien") + + 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() + authors = Book.objects.distinct("authors") + + self.assertEquals(authors, [mark_twain, john_tolkien]) + def test_custom_manager(self): """Ensure that custom QuerySetManager instances work as expected. """ From d79a5ec3d6b2e326714daeaee8e60faf4c7a81f4 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 10:12:02 +0000 Subject: [PATCH 110/222] Updated Authors and changelog (#483) --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 316a7441..633a0438 100644 --- a/AUTHORS +++ b/AUTHORS @@ -180,3 +180,4 @@ that much better: * FrankSomething (https://github.com/FrankSomething) * Alexandr Morozov (https://github.com/LK4D4) * mishudark (https://github.com/mishudark) + * Joe Friedl (https://github.com/grampajoe) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c3b02d0..35a02f85 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Use defaults when host and port are passed as None (#483) - Fixed distinct casting issue with ListField of EmbeddedDocuments (#470) - Fixed Django 1.6 sessions (#454, #480) From 6465726008b0bfa4333f76a351a2986713b05712 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 10:33:36 +0000 Subject: [PATCH 111/222] Fix handling invalid dict field value (#485) --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 334b1a24..6820368c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Fix testing invalid dict field value (#485) - Added app_label to MongoUser (#484) - Use defaults when host and port are passed as None (#483) - Fixed distinct casting issue with ListField of EmbeddedDocuments (#470) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index c6abd02b..752dc631 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -203,7 +203,7 @@ class ComplexBaseField(BaseField): _dereference = _import_class("DeReference")() self._auto_dereference = instance._fields[self.name]._auto_dereference - if instance._initialised and dereference: + if instance._initialised and dereference and instance._data.get(self.name): instance._data[self.name] = _dereference( instance._data.get(self.name), max_depth=1, instance=instance, name=self.name From 6db59a9c314827050b1e8a6ff746c5940b3c22da Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 11:41:54 +0000 Subject: [PATCH 112/222] Fix setting Geo Location fields (#488) --- docs/changelog.rst | 3 ++- mongoengine/common.py | 2 +- mongoengine/fields.py | 3 ++- mongoengine/queryset/transform.py | 4 +++ tests/queryset/geo.py | 42 +++++++++++++++++++++++++++++++ tests/queryset/transform.py | 29 +++++++++++++++++++++ 6 files changed, 80 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6820368c..427d2106 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,8 @@ Changelog Changes in 0.8.5 ================ -- Fix testing invalid dict field value (#485) +- Fix setting Geo Location fields (#488) +- Fix handling invalid dict field value (#485) - Added app_label to MongoUser (#484) - Use defaults when host and port are passed as None (#483) - Fixed distinct casting issue with ListField of EmbeddedDocuments (#470) diff --git a/mongoengine/common.py b/mongoengine/common.py index 6303231e..daa194b9 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -25,7 +25,7 @@ def _import_class(cls_name): 'GenericEmbeddedDocumentField', 'GeoPointField', 'PointField', 'LineStringField', 'ListField', 'PolygonField', 'ReferenceField', 'StringField', - 'ComplexBaseField') + 'ComplexBaseField', 'GeoJsonBaseField') queryset_classes = ('OperationError',) deref_classes = ('DeReference',) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 036a00c2..2a270cc6 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -42,7 +42,8 @@ __all__ = ['StringField', 'URLField', 'EmailField', 'IntField', 'LongField', 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', - 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField'] + 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', + 'GeoJsonBaseField'] RECURSIVE_REFERENCE_CONSTANT = 'self' diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 2ee7e386..e31a8b7d 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -206,6 +206,10 @@ def update(_doc_cls=None, **update): else: field = cleaned_fields[-1] + GeoJsonBaseField = _import_class("GeoJsonBaseField") + if isinstance(field, GeoJsonBaseField): + value = field.to_mongo(value) + if op in (None, 'set', 'push', 'pull'): if field.required or value is not None: value = field.prepare_query_value(op, value) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index f5648961..62e1f668 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -414,5 +414,47 @@ class GeoQueriesTest(unittest.TestCase): roads = Road.objects.filter(poly__geo_intersects={"$geometry": polygon}).count() self.assertEqual(1, roads) + def test_2dsphere_point_sets_correctly(self): + class Location(Document): + loc = PointField() + + Location.drop_collection() + + Location(loc=[1,2]).save() + loc = Location.objects.as_pymongo()[0] + self.assertEquals(loc["loc"], {"type": "Point", "coordinates": [1, 2]}) + + Location.objects.update(set__loc=[2,1]) + loc = Location.objects.as_pymongo()[0] + self.assertEquals(loc["loc"], {"type": "Point", "coordinates": [2, 1]}) + + def test_2dsphere_linestring_sets_correctly(self): + class Location(Document): + line = LineStringField() + + Location.drop_collection() + + Location(line=[[1, 2], [2, 2]]).save() + loc = Location.objects.as_pymongo()[0] + self.assertEquals(loc["line"], {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}) + + Location.objects.update(set__line=[[2, 1], [1, 2]]) + loc = Location.objects.as_pymongo()[0] + self.assertEquals(loc["line"], {"type": "LineString", "coordinates": [[2, 1], [1, 2]]}) + + def test_geojson_PolygonField(self): + class Location(Document): + poly = PolygonField() + + Location.drop_collection() + + Location(poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]).save() + loc = Location.objects.as_pymongo()[0] + self.assertEquals(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) + + Location.objects.update(set__poly=[[[40, 4], [40, 6], [41, 6], [40, 4]]]) + loc = Location.objects.as_pymongo()[0] + self.assertEquals(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]}) + if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index d2e8b784..2ca67222 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -167,6 +167,35 @@ class TransformTest(unittest.TestCase): {'attachments.views.extracted': 'no'}]} self.assertEqual(expected, raw_query) + def test_geojson_PointField(self): + class Location(Document): + loc = PointField() + + update = transform.update(Location, set__loc=[1, 2]) + self.assertEquals(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) + + update = transform.update(Location, set__loc={"type": "Point", "coordinates": [1,2]}) + self.assertEquals(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) + + def test_geojson_LineStringField(self): + class Location(Document): + line = LineStringField() + + update = transform.update(Location, set__line=[[1, 2], [2, 2]]) + self.assertEquals(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}}) + + update = transform.update(Location, set__line={"type": "LineString", "coordinates": [[1, 2], [2, 2]]}) + self.assertEquals(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}}) + + def test_geojson_PolygonField(self): + class Location(Document): + poly = PolygonField() + + update = transform.update(Location, set__poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) + self.assertEquals(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}}) + + update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) + self.assertEquals(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}}) if __name__ == '__main__': unittest.main() From 499e11f73030e6b53f1df259d30089f980dc4bdd Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 11:42:30 +0000 Subject: [PATCH 113/222] Updated AUTHORS (#488) --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 195f739d..1126166e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -182,3 +182,4 @@ that much better: * mishudark (https://github.com/mishudark) * Joe Friedl (https://github.com/grampajoe) * Daniel Ward (https://github.com/danielward) + * Aniket Deshpande (https://github.com/anicake) From 855efe7fe81389796dc8e491e687272915cc59a9 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 11:48:14 +0000 Subject: [PATCH 114/222] Added user_permissions to Django User object (#491, #492) --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/django/auth.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1126166e..ce015e68 100644 --- a/AUTHORS +++ b/AUTHORS @@ -183,3 +183,4 @@ that much better: * Joe Friedl (https://github.com/grampajoe) * Daniel Ward (https://github.com/danielward) * Aniket Deshpande (https://github.com/anicake) + * rfkrocktk (https://github.com/rfkrocktk) \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 427d2106..406fe82c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Added user_permissions to Django User object (#491, #492) - Fix setting Geo Location fields (#488) - Fix handling invalid dict field value (#485) - Added app_label to MongoUser (#484) diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py index 33a48eb6..f0f5ea1b 100644 --- a/mongoengine/django/auth.py +++ b/mongoengine/django/auth.py @@ -117,7 +117,7 @@ class Permission(Document): class Meta: verbose_name = _('permission') - verbose_name_plural = _('permissions')U + verbose_name_plural = _('permissions') # unique_together = (('content_type', 'codename'),) # ordering = ('content_type__app_label', 'content_type__model', 'codename') @@ -229,7 +229,7 @@ class User(Document): 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.')) From 06caabf333ed01297f1fbc136af039245a3542c7 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 11:53:24 +0000 Subject: [PATCH 115/222] Import fix --- 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 86838226..9bf77302 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -756,7 +756,7 @@ class BaseDocument(object): the field's parents and the field. """ - from mongoengine.fields import ListField + ListField = _import_class("ListField") if not isinstance(parts, (list, tuple)): parts = [parts] From 60ceeb0ddd40d34d2871541ba7cd81ef1b31334b Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 11:55:03 +0000 Subject: [PATCH 116/222] Update change log and Authors (#496) --- AUTHORS | 2 +- docs/changelog.rst | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index ce015e68..a00acd15 100644 --- a/AUTHORS +++ b/AUTHORS @@ -134,7 +134,7 @@ that much better: * Paul Swartz * Sundar Raman * Benoit Louy - * lraucy + * Loic Raucy (https://github.com/lraucy) * hellysmile * Jaepil Jeong * Daniil Sharou diff --git a/docs/changelog.rst b/docs/changelog.rst index 406fe82c..c21dc6b8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,9 @@ Changelog Changes in 0.8.5 ================ +- Fixed bug with .only() and DictField with digit keys (#496) - Added user_permissions to Django User object (#491, #492) -- Fix setting Geo Location fields (#488) +- Fix updating Geo Location fields (#488) - Fix handling invalid dict field value (#485) - Added app_label to MongoUser (#484) - Use defaults when host and port are passed as None (#483) From 6925344807093d6e8c94c7404e7487a219f5f8bd Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 12:01:14 +0000 Subject: [PATCH 117/222] Test updates --- tests/fields/file_tests.py | 2 +- tests/queryset/geo.py | 12 ++++++------ tests/queryset/queryset.py | 6 +++--- tests/queryset/transform.py | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index ba601dea..902b1512 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -279,7 +279,7 @@ class FileTest(unittest.TestCase): t.image.put(f) self.fail("Should have raised an invalidation error") except ValidationError, e: - self.assertEquals("%s" % e, "Invalid image: cannot identify image file") + self.assertEqual("%s" % e, "Invalid image: cannot identify image file") t = TestImage() t.image.put(open(TEST_IMAGE_PATH, 'rb')) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 62e1f668..65ab519a 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -422,11 +422,11 @@ class GeoQueriesTest(unittest.TestCase): Location(loc=[1,2]).save() loc = Location.objects.as_pymongo()[0] - self.assertEquals(loc["loc"], {"type": "Point", "coordinates": [1, 2]}) + self.assertEqual(loc["loc"], {"type": "Point", "coordinates": [1, 2]}) Location.objects.update(set__loc=[2,1]) loc = Location.objects.as_pymongo()[0] - self.assertEquals(loc["loc"], {"type": "Point", "coordinates": [2, 1]}) + self.assertEqual(loc["loc"], {"type": "Point", "coordinates": [2, 1]}) def test_2dsphere_linestring_sets_correctly(self): class Location(Document): @@ -436,11 +436,11 @@ class GeoQueriesTest(unittest.TestCase): Location(line=[[1, 2], [2, 2]]).save() loc = Location.objects.as_pymongo()[0] - self.assertEquals(loc["line"], {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}) + self.assertEqual(loc["line"], {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}) Location.objects.update(set__line=[[2, 1], [1, 2]]) loc = Location.objects.as_pymongo()[0] - self.assertEquals(loc["line"], {"type": "LineString", "coordinates": [[2, 1], [1, 2]]}) + self.assertEqual(loc["line"], {"type": "LineString", "coordinates": [[2, 1], [1, 2]]}) def test_geojson_PolygonField(self): class Location(Document): @@ -450,11 +450,11 @@ class GeoQueriesTest(unittest.TestCase): Location(poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]).save() loc = Location.objects.as_pymongo()[0] - self.assertEquals(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) + self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) Location.objects.update(set__poly=[[[40, 4], [40, 6], [41, 6], [40, 4]]]) loc = Location.objects.as_pymongo()[0] - self.assertEquals(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]}) + self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]}) if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d09f19cd..de39ee10 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2538,7 +2538,7 @@ class QuerySetTest(unittest.TestCase): book = Book(title="The Stories", authors=[mark_twain, john_tolkien]).save() authors = Book.objects.distinct("authors") - self.assertEquals(authors, [mark_twain, john_tolkien]) + self.assertEqual(authors, [mark_twain, john_tolkien]) def test_custom_manager(self): """Ensure that custom QuerySetManager instances work as expected. @@ -3641,9 +3641,9 @@ class QuerySetTest(unittest.TestCase): docs = Noddy.objects.no_cache() counter = len([1 for i in docs]) - self.assertEquals(counter, 100) + self.assertEqual(counter, 100) - self.assertEquals(len(list(docs)), 100) + self.assertEqual(len(list(docs)), 100) self.assertRaises(TypeError, lambda: len(docs)) with query_counter() as q: diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 2ca67222..48e01919 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -172,30 +172,30 @@ class TransformTest(unittest.TestCase): loc = PointField() update = transform.update(Location, set__loc=[1, 2]) - self.assertEquals(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) + self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) update = transform.update(Location, set__loc={"type": "Point", "coordinates": [1,2]}) - self.assertEquals(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) + self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) def test_geojson_LineStringField(self): class Location(Document): line = LineStringField() update = transform.update(Location, set__line=[[1, 2], [2, 2]]) - self.assertEquals(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}}) + self.assertEqual(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}}) update = transform.update(Location, set__line={"type": "LineString", "coordinates": [[1, 2], [2, 2]]}) - self.assertEquals(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}}) + self.assertEqual(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}}) def test_geojson_PolygonField(self): class Location(Document): poly = PolygonField() update = transform.update(Location, set__poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) - self.assertEquals(update, {'$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]]]}}}) update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) - self.assertEquals(update, {'$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]]]}}}) if __name__ == '__main__': unittest.main() From bcbbbe40463eca15ad34f721430482bd8f16f9a3 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 12:04:30 +0000 Subject: [PATCH 118/222] Added test, updated AUTHORS and changelog (#498) --- AUTHORS | 3 ++- docs/changelog.rst | 1 + tests/queryset/queryset.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index a00acd15..f9be6765 100644 --- a/AUTHORS +++ b/AUTHORS @@ -183,4 +183,5 @@ that much better: * Joe Friedl (https://github.com/grampajoe) * Daniel Ward (https://github.com/danielward) * Aniket Deshpande (https://github.com/anicake) - * rfkrocktk (https://github.com/rfkrocktk) \ No newline at end of file + * rfkrocktk (https://github.com/rfkrocktk) + * Gustavo Andrés Angulo (https://github.com/woakas) diff --git a/docs/changelog.rst b/docs/changelog.rst index c21dc6b8..596b71b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Fixed count and none bug (#498) - Fixed bug with .only() and DictField with digit keys (#496) - Added user_permissions to Django User object (#491, #492) - Fix updating Geo Location fields (#488) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index de39ee10..7ff2965d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2882,6 +2882,19 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(10, Post.objects.limit(5).skip(5).count(with_limit_and_skip=False)) + def test_count_and_none(self): + """Test count works with None()""" + + class MyDoc(Document): + pass + + MyDoc.drop_collection() + for i in xrange(0, 10): + MyDoc().save() + + self.assertEqual(MyDoc.objects.count(), 10) + self.assertEqual(MyDoc.objects.none().count(), 0) + def test_call_after_limits_set(self): """Ensure that re-filtering after slicing works """ From bb56f92213cdb784ccc5afc859aeca65d13705af Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 12:07:33 +0000 Subject: [PATCH 119/222] Updated changelog and AUTHORS (#499) --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index f9be6765..0d64a565 100644 --- a/AUTHORS +++ b/AUTHORS @@ -185,3 +185,4 @@ that much better: * Aniket Deshpande (https://github.com/anicake) * rfkrocktk (https://github.com/rfkrocktk) * Gustavo Andrés Angulo (https://github.com/woakas) + * Dmytro Popovych (https://github.com/drudim) diff --git a/docs/changelog.rst b/docs/changelog.rst index 596b71b0..76157220 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Fixed GenericReferenceField serialization order (#499) - Fixed count and none bug (#498) - Fixed bug with .only() and DictField with digit keys (#496) - Added user_permissions to Django User object (#491, #492) From a2a698ab0eb15205c9afde8712d1e4d1dd4c0a15 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 12:46:18 +0000 Subject: [PATCH 120/222] Fixed EmbeddedDocument with ReferenceField equality issue (#502) --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/document.py | 2 +- tests/document/instance.py | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 0d64a565..163ee8fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -186,3 +186,4 @@ that much better: * rfkrocktk (https://github.com/rfkrocktk) * Gustavo Andrés Angulo (https://github.com/woakas) * Dmytro Popovych (https://github.com/drudim) + * Tom (https://github.com/tomprimozic) \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 76157220..fa01df85 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Fixed EmbeddedDocument with ReferenceField equality issue (#502) - Fixed GenericReferenceField serialization order (#499) - Fixed count and none bug (#498) - Fixed bug with .only() and DictField with digit keys (#496) diff --git a/mongoengine/document.py b/mongoengine/document.py index 1bbd7b73..59bea359 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -66,7 +66,7 @@ class EmbeddedDocument(BaseDocument): def __eq__(self, other): if isinstance(other, self.__class__): - return self._data == other._data + return self.to_mongo() == other.to_mongo() return False def __ne__(self, other): diff --git a/tests/document/instance.py b/tests/document/instance.py index a61c4396..06b884f5 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2392,6 +2392,24 @@ class InstanceTest(unittest.TestCase): system = System.objects.first() self.assertEqual("UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) + def test_embedded_document_equality(self): + + class Test(Document): + field = StringField(required=True) + + class Embedded(EmbeddedDocument): + ref = ReferenceField(Test) + + Test.drop_collection() + test = Test(field='123').save() # has id + + e = Embedded(ref=test) + f1 = Embedded._from_son(e.to_mongo()) + f2 = Embedded._from_son(e.to_mongo()) + + self.assertEqual(f1, f2) + f1.ref # Dereferences lazily + self.assertEqual(f1, f2) if __name__ == '__main__': unittest.main() From b024dd913d986e1a6c2cc24de68e30fdbddd2639 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 13:09:11 +0000 Subject: [PATCH 121/222] EmbeddedDocument._instance is now set when settng the attribute (#506) --- docs/changelog.rst | 1 + mongoengine/base/fields.py | 11 +++++------ tests/document/instance.py | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fa01df85..4eb82d21 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- EmbeddedDocument._instance is now set when settng the attribute (#506) - Fixed EmbeddedDocument with ReferenceField equality issue (#502) - Fixed GenericReferenceField serialization order (#499) - Fixed count and none bug (#498) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 752dc631..04c559e2 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -89,12 +89,7 @@ class BaseField(object): return self # Get value from document instance if available - value = instance._data.get(self.name) - - EmbeddedDocument = _import_class('EmbeddedDocument') - if isinstance(value, EmbeddedDocument) and value._instance is None: - value._instance = weakref.proxy(instance) - return value + return instance._data.get(self.name) def __set__(self, instance, value): """Descriptor for assigning a value to a field in a document. @@ -116,6 +111,10 @@ class BaseField(object): # Values cant be compared eg: naive and tz datetimes # So mark it as changed instance._mark_as_changed(self.name) + + EmbeddedDocument = _import_class('EmbeddedDocument') + if isinstance(value, EmbeddedDocument) and value._instance is None: + value._instance = weakref.proxy(instance) instance._data[self.name] = value def error(self, message="", errors=None, field_name=None): diff --git a/tests/document/instance.py b/tests/document/instance.py index 06b884f5..982b1e6c 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -490,6 +490,26 @@ class InstanceTest(unittest.TestCase): doc = Doc.objects.get() self.assertEqual(doc, doc.embedded_field[0]._instance) + def test_instance_is_set_on_setattr(self): + + class Email(EmbeddedDocument): + email = EmailField() + def clean(self): + print "instance:" + print self._instance + + class Account(Document): + email = EmbeddedDocumentField(Email) + + Account.drop_collection() + acc = Account() + acc.email = Email(email='test@example.com') + self.assertTrue(hasattr(acc._data["email"], "_instance")) + acc.save() + + acc1 = Account.objects.first() + self.assertTrue(hasattr(acc1._data["email"], "_instance")) + def test_document_clean(self): class TestDocument(Document): status = StringField() From 453024c58d501ff874a6cc04f621bb69e036dcb1 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 13:11:56 +0000 Subject: [PATCH 122/222] Django 1.6 session fix (#509) --- AUTHORS | 3 ++- docs/changelog.rst | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 163ee8fe..21b0ae1b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -186,4 +186,5 @@ that much better: * rfkrocktk (https://github.com/rfkrocktk) * Gustavo Andrés Angulo (https://github.com/woakas) * Dmytro Popovych (https://github.com/drudim) - * Tom (https://github.com/tomprimozic) \ No newline at end of file + * Tom (https://github.com/tomprimozic) + * j0hnsmith (https://github.com/j0hnsmith) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4eb82d21..43404d1a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Django 1.6 session fix (#509) - EmbeddedDocument._instance is now set when settng the attribute (#506) - Fixed EmbeddedDocument with ReferenceField equality issue (#502) - Fixed GenericReferenceField serialization order (#499) From c074f4d9259c6e853b7b3d636ebc4ab042f3e2a2 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 13:19:52 +0000 Subject: [PATCH 123/222] Django 1.6 login fix (#522) --- docs/changelog.rst | 1 + mongoengine/django/mongo_auth/models.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 43404d1a..6b950318 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Django 1.6 login fix (#522) - Django 1.6 session fix (#509) - EmbeddedDocument._instance is now set when settng the attribute (#506) - Fixed EmbeddedDocument with ReferenceField equality issue (#502) diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py index c391fc9d..60965e6d 100644 --- a/mongoengine/django/mongo_auth/models.py +++ b/mongoengine/django/mongo_auth/models.py @@ -1,4 +1,5 @@ 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 @@ -108,3 +109,7 @@ class MongoUser(models.Model): 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) From d868cfdeb08ec2de046f0efac34ae735a4745b6f Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 29 Nov 2013 16:24:32 +0000 Subject: [PATCH 124/222] Fix multi level nested fields getting marked as changed (#523) --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/base/document.py | 84 ++++++++++++++++++++---------------- tests/document/delta.py | 21 +++++++++ 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/AUTHORS b/AUTHORS index 21b0ae1b..d6994d50 100644 --- a/AUTHORS +++ b/AUTHORS @@ -188,3 +188,4 @@ that much better: * Dmytro Popovych (https://github.com/drudim) * Tom (https://github.com/tomprimozic) * j0hnsmith (https://github.com/j0hnsmith) + * Damien Churchill (https://github.com/damoxc) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6b950318..774b1dbc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.5 ================ +- Fix multi level nested fields getting marked as changed (#523) - Django 1.6 login fix (#522) - Django 1.6 session fix (#509) - EmbeddedDocument._instance is now set when settng the attribute (#506) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 9bf77302..e425cb84 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -375,20 +375,41 @@ class BaseDocument(object): self._changed_fields.append(key) def _clear_changed_fields(self): + """Using get_changed_fields iterate and remove any fields that are + marked as changed""" + for changed in self._get_changed_fields(): + parts = changed.split(".") + data = self + for part in parts: + if isinstance(data, list): + try: + data = data[int(part)] + except IndexError: + data = None + elif isinstance(data, dict): + data = data.get(part, None) + else: + data = getattr(data, part, None) + if hasattr(data, "_changed_fields"): + data._changed_fields = [] self._changed_fields = [] - EmbeddedDocumentField = _import_class("EmbeddedDocumentField") - for field_name, field in self._fields.iteritems(): - if (isinstance(field, ComplexBaseField) and - isinstance(field.field, EmbeddedDocumentField)): - field_value = getattr(self, field_name, None) - if field_value: - for idx in (field_value if isinstance(field_value, dict) - else xrange(len(field_value))): - field_value[idx]._clear_changed_fields() - elif isinstance(field, EmbeddedDocumentField): - field_value = getattr(self, field_name, None) - if field_value: - field_value._clear_changed_fields() + + def _nestable_types_changed_fields(self, changed_fields, key, data, inspected): + # Loop list / dict fields as they contain documents + # Determine the iterator to use + if not hasattr(data, 'items'): + iterator = enumerate(data) + else: + iterator = data.iteritems() + + for index, value in iterator: + list_key = "%s%s." % (key, index) + if hasattr(value, '_get_changed_fields'): + changed = value._get_changed_fields(inspected) + changed_fields += ["%s%s" % (list_key, k) + for k in changed if k] + elif isinstance(value, (list, tuple, dict)): + self._nestable_types_changed_fields(changed_fields, list_key, value, inspected) def _get_changed_fields(self, inspected=None): """Returns a list of all fields that have explicitly been changed. @@ -396,13 +417,12 @@ class BaseDocument(object): EmbeddedDocument = _import_class("EmbeddedDocument") DynamicEmbeddedDocument = _import_class("DynamicEmbeddedDocument") ReferenceField = _import_class("ReferenceField") - _changed_fields = [] - _changed_fields += getattr(self, '_changed_fields', []) - + changed_fields = [] + changed_fields += getattr(self, '_changed_fields', []) inspected = inspected or set() - if hasattr(self, 'id'): + if hasattr(self, 'id') and not isinstance(self.id, dict): if self.id in inspected: - return _changed_fields + return changed_fields inspected.add(self.id) for field_name in self._fields_ordered: @@ -418,29 +438,17 @@ class BaseDocument(object): if isinstance(field, ReferenceField): continue elif (isinstance(data, (EmbeddedDocument, DynamicEmbeddedDocument)) - and db_field_name not in _changed_fields): + and db_field_name not in changed_fields): # 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] + changed_fields += ["%s%s" % (key, k) for k in changed if k] elif (isinstance(data, (list, tuple, dict)) and - db_field_name not in _changed_fields): - # Loop list / dict fields as they contain documents - # Determine the iterator to use - if not hasattr(data, 'items'): - iterator = enumerate(data) - else: - iterator = data.iteritems() - for index, value in iterator: - if not hasattr(value, '_get_changed_fields'): - continue - if (hasattr(field, 'field') and - isinstance(field.field, ReferenceField)): - continue - list_key = "%s%s." % (key, index) - changed = value._get_changed_fields(inspected) - _changed_fields += ["%s%s" % (list_key, k) - for k in changed if k] - return _changed_fields + db_field_name not in changed_fields): + if (hasattr(field, 'field') and + isinstance(field.field, ReferenceField)): + continue + self._nestable_types_changed_fields(changed_fields, key, data, inspected) + return changed_fields def _delta(self): """Returns the delta (set, unset) of the changes for a document. diff --git a/tests/document/delta.py b/tests/document/delta.py index b4749f38..b0f5f01a 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -713,6 +713,27 @@ class DeltaTest(unittest.TestCase): self.assertEqual({}, removals) self.assertTrue('employees' in updates) + def test_nested_nested_fields_mark_as_changed(self): + class EmbeddedDoc(EmbeddedDocument): + name = StringField() + + class MyDoc(Document): + subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc))) + name = StringField() + + MyDoc.drop_collection() + + mydoc = MyDoc(name='testcase1', subs={'a': {'b': EmbeddedDoc(name='foo')}}).save() + + mydoc = MyDoc.objects.first() + subdoc = mydoc.subs['a']['b'] + subdoc.name = 'bar' + + self.assertEqual(["name"], subdoc._get_changed_fields()) + self.assertEqual(["subs.a.b.name"], mydoc._get_changed_fields()) + + mydoc._clear_changed_fields() + self.assertEqual([], mydoc._get_changed_fields()) if __name__ == '__main__': unittest.main() From 16dcf78cab97aff74aa8ec201c3865587c741613 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 2 Dec 2013 14:14:46 +0000 Subject: [PATCH 125/222] Ensure id is Hashable --- mongoengine/base/document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index e425cb84..f5eae8ff 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1,6 +1,7 @@ import copy import operator import numbers +from collections import Hashable from functools import partial import pymongo @@ -420,7 +421,7 @@ class BaseDocument(object): changed_fields = [] changed_fields += getattr(self, '_changed_fields', []) inspected = inspected or set() - if hasattr(self, 'id') and not isinstance(self.id, dict): + if hasattr(self, 'id') and isinstance(self.id, Hashable): if self.id in inspected: return changed_fields inspected.add(self.id) From d1b30f47924b983549ece75372067749517d3b1b Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 4 Dec 2013 10:00:12 +0000 Subject: [PATCH 126/222] Fix auth to use `get_user_document` #527 --- mongoengine/django/auth.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py index f0f5ea1b..3d0adb04 100644 --- a/mongoengine/django/auth.py +++ b/mongoengine/django/auth.py @@ -8,6 +8,11 @@ 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 +from .mongo_auth import get_user_document + +REDIRECT_FIELD_NAME = 'next' + try: from django.contrib.auth.hashers import check_password, make_password except ImportError: @@ -33,10 +38,6 @@ except ImportError: hash = get_hexdigest(algo, salt, raw_password) return '%s$%s$%s' % (algo, salt, hash) -from .utils import datetime_now - -REDIRECT_FIELD_NAME = 'next' - class ContentType(Document): name = StringField(max_length=100) @@ -383,7 +384,7 @@ class MongoEngineBackend(object): supports_inactive_user = False def authenticate(self, username=None, password=None): - user = User.objects(username=username).first() + user = get_user_document().objects(username=username).first() if user: if password and user.check_password(password): backend = auth.get_backends()[0] @@ -392,7 +393,7 @@ class MongoEngineBackend(object): return None def get_user(self, user_id): - return User.objects.with_id(user_id) + return get_user_document().objects.with_id(user_id) def get_user(userid): From 7a73a92074a3f9cb33a65718909ed346b74c0816 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 4 Dec 2013 10:01:16 +0000 Subject: [PATCH 127/222] Update changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 774b1dbc..4265b7e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog Changes in 0.8.5 ================ - Fix multi level nested fields getting marked as changed (#523) -- Django 1.6 login fix (#522) +- Django 1.6 login fix (#522) (#527) - Django 1.6 session fix (#509) - EmbeddedDocument._instance is now set when settng the attribute (#506) - Fixed EmbeddedDocument with ReferenceField equality issue (#502) From 57ac38ddcaf5441f8173a273fbbaa187dd89c404 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 4 Dec 2013 10:02:05 +0000 Subject: [PATCH 128/222] Version bump 0.8.5 --- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 2b68b3cc..cbc81d12 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 4) +VERSION = (0, 8, 5) def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index b9c45efa..5b871a53 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.4 +Version: 0.8.5 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From d1b86fdef540a1b7e45a014d74701bbefb131afe Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 4 Dec 2013 13:31:54 +0000 Subject: [PATCH 129/222] Fix for import --- mongoengine/__init__.py | 2 +- mongoengine/django/auth.py | 2 +- python-mongoengine.spec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index cbc81d12..1537c444 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 5) +VERSION = (0, 8, 6) def get_version(): diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py index 3d0adb04..89ea003c 100644 --- a/mongoengine/django/auth.py +++ b/mongoengine/django/auth.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import AnonymousUser from django.utils.translation import ugettext_lazy as _ from .utils import datetime_now -from .mongo_auth import get_user_document +from .mongo_auth.models import get_user_document REDIRECT_FIELD_NAME = 'next' diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 5b871a53..07b169dc 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.5 +Version: 0.8.6 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From 6a02ac7e809a5dbc68542e9c76ab4ad508ddd2ef Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 4 Dec 2013 13:32:56 +0000 Subject: [PATCH 130/222] Fix django auth import (#531) --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4265b7e8..3d8c3307 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +Changes in 0.8.6 +================ +- Fix django auth import (#531) + Changes in 0.8.5 ================ - Fix multi level nested fields getting marked as changed (#523) From f7302f710bd2d7b1cb930e0e68848417a6d3e615 Mon Sep 17 00:00:00 2001 From: Matthew Owen Date: Thu, 5 Dec 2013 09:50:12 -0800 Subject: [PATCH 131/222] Reject email addresses where hostname ends with '.' --- mongoengine/fields.py | 2 +- tests/fields/fields.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0ac6f0e4..82642cda 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -153,7 +153,7 @@ class EmailField(StringField): EMAIL_REGEX = re.compile( r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string - r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE # domain + r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?\.)+[A-Z]{2,6}$', re.IGNORECASE # domain ) def validate(self, value): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 9ce6ae87..c108f37e 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2499,6 +2499,9 @@ class FieldTest(unittest.TestCase): user = User(email="ross@example.com") self.assertTrue(user.validate() is None) + user = User(email="ross@example.co.uk") + self.assertTrue(user.validate() is None) + user = User(email=("Kofq@rhom0e4klgauOhpbpNdogawnyIKvQS0wk2mjqrgGQ5S" "ucictfqpdkK9iS1zeFw8sg7s7cwAF7suIfUfeyueLpfosjn3" "aJIazqqWkm7.net")) @@ -2507,6 +2510,9 @@ class FieldTest(unittest.TestCase): user = User(email='me@localhost') self.assertRaises(ValidationError, user.validate) + user = User(email="ross@example.com.") + self.assertRaises(ValidationError, user.validate) + def test_email_field_honors_regex(self): class User(Document): email = EmailField(regex=r'\w+@example.com') @@ -2594,13 +2600,13 @@ class FieldTest(unittest.TestCase): def test_invalid_dict_value(self): class DictFieldTest(Document): dictionary = DictField(required=True) - + DictFieldTest.drop_collection() test = DictFieldTest(dictionary=None) test.dictionary # Just access to test getter self.assertRaises(ValidationError, test.validate) - + test = DictFieldTest(dictionary=False) test.dictionary # Just access to test getter self.assertRaises(ValidationError, test.validate) From d2941a91108666ca35e99c124cc1bacaf49b5790 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 13 Dec 2013 09:22:41 +0000 Subject: [PATCH 132/222] Updated doc examples to open using read binary mode --- docs/guide/gridfs.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/gridfs.rst b/docs/guide/gridfs.rst index d81bb922..596585de 100644 --- a/docs/guide/gridfs.rst +++ b/docs/guide/gridfs.rst @@ -20,7 +20,7 @@ a document is created to store details about animals, including a photo:: marmot = Animal(genus='Marmota', family='Sciuridae') - marmot_photo = open('marmot.jpg', 'r') + marmot_photo = open('marmot.jpg', 'rb') marmot.photo.put(marmot_photo, content_type = 'image/jpeg') marmot.save() @@ -70,5 +70,5 @@ Replacing files Files can be replaced with the :func:`replace` method. This works just like the :func:`put` method so even metadata can (and should) be replaced:: - another_marmot = open('another_marmot.png', 'r') + another_marmot = open('another_marmot.png', 'rb') marmot.photo.replace(another_marmot, content_type='image/png') From 516591fe883f8691d63ff2094ab75a722cd6acc6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 13 Dec 2013 09:46:45 +0000 Subject: [PATCH 133/222] Docs update and canonical url fix --- docs/_themes/nature/static/nature.css_t | 233 ---------- docs/_themes/nature/static/pygments.css | 54 --- docs/_themes/nature/theme.conf | 4 - docs/_themes/sphinx_rtd_theme/__init__.py | 17 + .../_themes/sphinx_rtd_theme/breadcrumbs.html | 15 + docs/_themes/sphinx_rtd_theme/footer.html | 30 ++ docs/_themes/sphinx_rtd_theme/layout.html | 142 +++++++ docs/_themes/sphinx_rtd_theme/layout_old.html | 205 +++++++++ docs/_themes/sphinx_rtd_theme/search.html | 50 +++ docs/_themes/sphinx_rtd_theme/searchbox.html | 5 + .../static/css/badge_only.css | 1 + .../sphinx_rtd_theme/static/css/theme.css | 1 + .../sphinx_rtd_theme/static/favicon.ico | Bin 0 -> 6261 bytes .../static/font/fontawesome_webfont.eot | Bin 0 -> 37405 bytes .../static/font/fontawesome_webfont.svg | 399 ++++++++++++++++++ .../static/font/fontawesome_webfont.ttf | Bin 0 -> 79076 bytes .../static/font/fontawesome_webfont.woff | Bin 0 -> 43572 bytes .../sphinx_rtd_theme/static/js/theme.js | 16 + docs/_themes/sphinx_rtd_theme/theme.conf | 8 + docs/_themes/sphinx_rtd_theme/versions.html | 37 ++ docs/conf.py | 7 +- 21 files changed, 931 insertions(+), 293 deletions(-) delete mode 100644 docs/_themes/nature/static/nature.css_t delete mode 100644 docs/_themes/nature/static/pygments.css delete mode 100644 docs/_themes/nature/theme.conf create mode 100755 docs/_themes/sphinx_rtd_theme/__init__.py create mode 100755 docs/_themes/sphinx_rtd_theme/breadcrumbs.html create mode 100755 docs/_themes/sphinx_rtd_theme/footer.html create mode 100755 docs/_themes/sphinx_rtd_theme/layout.html create mode 100755 docs/_themes/sphinx_rtd_theme/layout_old.html create mode 100755 docs/_themes/sphinx_rtd_theme/search.html create mode 100755 docs/_themes/sphinx_rtd_theme/searchbox.html create mode 100755 docs/_themes/sphinx_rtd_theme/static/css/badge_only.css create mode 100755 docs/_themes/sphinx_rtd_theme/static/css/theme.css create mode 100644 docs/_themes/sphinx_rtd_theme/static/favicon.ico create mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.eot create mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.svg create mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.ttf create mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.woff create mode 100755 docs/_themes/sphinx_rtd_theme/static/js/theme.js create mode 100755 docs/_themes/sphinx_rtd_theme/theme.conf create mode 100755 docs/_themes/sphinx_rtd_theme/versions.html diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t deleted file mode 100644 index 337760b5..00000000 --- a/docs/_themes/nature/static/nature.css_t +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -#changelog p.first {margin-bottom: 0 !important;} -#changelog p {margin-top: 0 !important; - margin-bottom: 0 !important;} - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: Arial, sans-serif; - font-size: 100%; - background-color: #111; - color: #555; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -hr{ - border: 1px solid #B1B4B6; -} - -div.document { - background-color: #eee; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; - font-size: 0.8em; -} - -div.footer { - color: #555; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #444; - text-decoration: underline; -} - -div.related { - background-color: #6BA81E; - line-height: 32px; - color: #fff; - text-shadow: 0px 1px 0 #444; - font-size: 0.80em; -} - -div.related a { - color: #E2F3CC; -} - -div.sphinxsidebar { - font-size: 0.75em; - line-height: 1.5em; -} - -div.sphinxsidebarwrapper{ - padding: 20px 0; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: Arial, sans-serif; - color: #222; - font-size: 1.2em; - font-weight: normal; - margin: 0; - padding: 5px 10px; - background-color: #ddd; - text-shadow: 1px 1px 0 white -} - -div.sphinxsidebar h4{ - font-size: 1.1em; -} - -div.sphinxsidebar h3 a { - color: #444; -} - - -div.sphinxsidebar p { - color: #888; - padding: 5px 20px; -} - -div.sphinxsidebar p.topless { -} - -div.sphinxsidebar ul { - margin: 10px 20px; - padding: 0; - color: #000; -} - -div.sphinxsidebar a { - color: #444; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar input[type=text]{ - margin-left: 20px; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #005B81; - text-decoration: none; -} - -a:hover { - color: #E32E00; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: Arial, sans-serif; - background-color: #BED4EB; - font-weight: normal; - color: #212224; - margin: 30px 0px 10px 0px; - padding: 5px 0 5px 10px; - text-shadow: 0px 1px 0 white -} - -div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 150%; background-color: #C8D5E3; } -div.body h3 { font-size: 120%; background-color: #D8DEE3; } -div.body h4 { font-size: 110%; background-color: #D8DEE3; } -div.body h5 { font-size: 100%; background-color: #D8DEE3; } -div.body h6 { font-size: 100%; background-color: #D8DEE3; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - line-height: 1.5em; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight{ - background-color: white; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 10px; - background-color: White; - color: #222; - line-height: 1.2em; - border: 1px solid #C6C9CB; - font-size: 1.2em; - margin: 1.5em 0 1.5em 0; - -webkit-box-shadow: 1px 1px 1px #d8d8d8; - -moz-box-shadow: 1px 1px 1px #d8d8d8; -} - -tt { - background-color: #ecf0f3; - color: #222; - padding: 1px 2px; - font-size: 1.2em; - font-family: monospace; -} diff --git a/docs/_themes/nature/static/pygments.css b/docs/_themes/nature/static/pygments.css deleted file mode 100644 index 652b7612..00000000 --- a/docs/_themes/nature/static/pygments.css +++ /dev/null @@ -1,54 +0,0 @@ -.c { color: #999988; font-style: italic } /* Comment */ -.k { font-weight: bold } /* Keyword */ -.o { font-weight: bold } /* Operator */ -.cm { color: #999988; font-style: italic } /* Comment.Multiline */ -.cp { color: #999999; font-weight: bold } /* Comment.preproc */ -.c1 { color: #999988; font-style: italic } /* Comment.Single */ -.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.ge { font-style: italic } /* Generic.Emph */ -.gr { color: #aa0000 } /* Generic.Error */ -.gh { color: #999999 } /* Generic.Heading */ -.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.go { color: #111 } /* Generic.Output */ -.gp { color: #555555 } /* Generic.Prompt */ -.gs { font-weight: bold } /* Generic.Strong */ -.gu { color: #aaaaaa } /* Generic.Subheading */ -.gt { color: #aa0000 } /* Generic.Traceback */ -.kc { font-weight: bold } /* Keyword.Constant */ -.kd { font-weight: bold } /* Keyword.Declaration */ -.kp { font-weight: bold } /* Keyword.Pseudo */ -.kr { font-weight: bold } /* Keyword.Reserved */ -.kt { color: #445588; font-weight: bold } /* Keyword.Type */ -.m { color: #009999 } /* Literal.Number */ -.s { color: #bb8844 } /* Literal.String */ -.na { color: #008080 } /* Name.Attribute */ -.nb { color: #999999 } /* Name.Builtin */ -.nc { color: #445588; font-weight: bold } /* Name.Class */ -.no { color: #ff99ff } /* Name.Constant */ -.ni { color: #800080 } /* Name.Entity */ -.ne { color: #990000; font-weight: bold } /* Name.Exception */ -.nf { color: #990000; font-weight: bold } /* Name.Function */ -.nn { color: #555555 } /* Name.Namespace */ -.nt { color: #000080 } /* Name.Tag */ -.nv { color: purple } /* Name.Variable */ -.ow { font-weight: bold } /* Operator.Word */ -.mf { color: #009999 } /* Literal.Number.Float */ -.mh { color: #009999 } /* Literal.Number.Hex */ -.mi { color: #009999 } /* Literal.Number.Integer */ -.mo { color: #009999 } /* Literal.Number.Oct */ -.sb { color: #bb8844 } /* Literal.String.Backtick */ -.sc { color: #bb8844 } /* Literal.String.Char */ -.sd { color: #bb8844 } /* Literal.String.Doc */ -.s2 { color: #bb8844 } /* Literal.String.Double */ -.se { color: #bb8844 } /* Literal.String.Escape */ -.sh { color: #bb8844 } /* Literal.String.Heredoc */ -.si { color: #bb8844 } /* Literal.String.Interpol */ -.sx { color: #bb8844 } /* Literal.String.Other */ -.sr { color: #808000 } /* Literal.String.Regex */ -.s1 { color: #bb8844 } /* Literal.String.Single */ -.ss { color: #bb8844 } /* Literal.String.Symbol */ -.bp { color: #999999 } /* Name.Builtin.Pseudo */ -.vc { color: #ff99ff } /* Name.Variable.Class */ -.vg { color: #ff99ff } /* Name.Variable.Global */ -.vi { color: #ff99ff } /* Name.Variable.Instance */ -.il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_themes/nature/theme.conf b/docs/_themes/nature/theme.conf deleted file mode 100644 index 1cc40044..00000000 --- a/docs/_themes/nature/theme.conf +++ /dev/null @@ -1,4 +0,0 @@ -[theme] -inherit = basic -stylesheet = nature.css -pygments_style = tango diff --git a/docs/_themes/sphinx_rtd_theme/__init__.py b/docs/_themes/sphinx_rtd_theme/__init__.py new file mode 100755 index 00000000..1440863d --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/__init__.py @@ -0,0 +1,17 @@ +"""Sphinx ReadTheDocs theme. + +From https://github.com/ryan-roemer/sphinx-bootstrap-theme. + +""" +import os + +VERSION = (0, 1, 5) + +__version__ = ".".join(str(v) for v in VERSION) +__version_full__ = __version__ + + +def get_html_theme_path(): + """Return list of HTML theme paths.""" + cur_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + return cur_dir diff --git a/docs/_themes/sphinx_rtd_theme/breadcrumbs.html b/docs/_themes/sphinx_rtd_theme/breadcrumbs.html new file mode 100755 index 00000000..3e4f359c --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/breadcrumbs.html @@ -0,0 +1,15 @@ + +
+ diff --git a/docs/_themes/sphinx_rtd_theme/footer.html b/docs/_themes/sphinx_rtd_theme/footer.html new file mode 100755 index 00000000..e42d753f --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/footer.html @@ -0,0 +1,30 @@ +
+ {% if next or prev %} + + {% endif %} + +
+ +

+ {%- if show_copyright %} + {%- if hasdoc('copyright') %} + {% trans path=pathto('copyright'), copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} + {%- else %} + {% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} + {%- endif %} + {%- endif %} + + {%- if last_updated %} + {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} + {%- endif %} +

+ + {% trans %}Sphinx theme provided by Read the Docs{% endtrans %} +
diff --git a/docs/_themes/sphinx_rtd_theme/layout.html b/docs/_themes/sphinx_rtd_theme/layout.html new file mode 100755 index 00000000..febe8eb0 --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/layout.html @@ -0,0 +1,142 @@ +{# TEMPLATE VAR SETTINGS #} +{%- set url_root = pathto('', 1) %} +{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} +{%- if not embedded and docstitle %} + {%- set titlesuffix = " — "|safe + docstitle|e %} +{%- else %} + {%- set titlesuffix = "" %} +{%- endif %} + + + + + + + + {% block htmltitle %} + {{ title|striptags|e }}{{ titlesuffix }} + {% endblock %} + + {# FAVICON #} + {% if favicon %} + + {% endif %} + {# CANONICAL #} + {%- if theme_canonical_url %} + + {%- endif %} + + {# CSS #} + + + {# JS #} + {% if not embedded %} + + + {%- for scriptfile in script_files %} + + {%- endfor %} + + {% if use_opensearch %} + + {% endif %} + + {% endif %} + + {# RTD hosts these file themselves, so just load on non RTD builds #} + {% if not READTHEDOCS %} + + + {% endif %} + + {% for cssfile in css_files %} + + {% endfor %} + + {%- block linktags %} + {%- if hasdoc('about') %} + + {%- endif %} + {%- if hasdoc('genindex') %} + + {%- endif %} + {%- if hasdoc('search') %} + + {%- endif %} + {%- if hasdoc('copyright') %} + + {%- endif %} + + {%- if parents %} + + {%- endif %} + {%- if next %} + + {%- endif %} + {%- if prev %} + + {%- endif %} + {%- endblock %} + {%- block extrahead %} {% endblock %} + + + + + + + +
+ + {# SIDE NAV, TOGGLES ON MOBILE #} + + +
+ + {# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #} + + + + {# PAGE CONTENT #} +
+
+ {% include "breadcrumbs.html" %} + {% block body %}{% endblock %} + {% include "footer.html" %} +
+
+ +
+ +
+ {% include "versions.html" %} + + diff --git a/docs/_themes/sphinx_rtd_theme/layout_old.html b/docs/_themes/sphinx_rtd_theme/layout_old.html new file mode 100755 index 00000000..deb8df2a --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/layout_old.html @@ -0,0 +1,205 @@ +{# + basic/layout.html + ~~~~~~~~~~~~~~~~~ + + Master layout template for Sphinx themes. + + :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +#} +{%- block doctype -%} + +{%- endblock %} +{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} +{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} +{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and + (sidebars != []) %} +{%- set url_root = pathto('', 1) %} +{# XXX necessary? #} +{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} +{%- if not embedded and docstitle %} + {%- set titlesuffix = " — "|safe + docstitle|e %} +{%- else %} + {%- set titlesuffix = "" %} +{%- endif %} + +{%- macro relbar() %} + +{%- endmacro %} + +{%- macro sidebar() %} + {%- if render_sidebar %} +
+
+ {%- block sidebarlogo %} + {%- if logo %} + + {%- endif %} + {%- endblock %} + {%- if sidebars != None %} + {#- new style sidebar: explicitly include/exclude templates #} + {%- for sidebartemplate in sidebars %} + {%- include sidebartemplate %} + {%- endfor %} + {%- else %} + {#- old style sidebars: using blocks -- should be deprecated #} + {%- block sidebartoc %} + {%- include "localtoc.html" %} + {%- endblock %} + {%- block sidebarrel %} + {%- include "relations.html" %} + {%- endblock %} + {%- block sidebarsourcelink %} + {%- include "sourcelink.html" %} + {%- endblock %} + {%- if customsidebar %} + {%- include customsidebar %} + {%- endif %} + {%- block sidebarsearch %} + {%- include "searchbox.html" %} + {%- endblock %} + {%- endif %} +
+
+ {%- endif %} +{%- endmacro %} + +{%- macro script() %} + + {%- for scriptfile in script_files %} + + {%- endfor %} +{%- endmacro %} + +{%- macro css() %} + + + {%- for cssfile in css_files %} + + {%- endfor %} +{%- endmacro %} + + + + + {{ metatags }} + {%- block htmltitle %} + {{ title|striptags|e }}{{ titlesuffix }} + {%- endblock %} + {{ css() }} + {%- if not embedded %} + {{ script() }} + {%- if use_opensearch %} + + {%- endif %} + {%- if favicon %} + + {%- endif %} + {%- endif %} +{%- block linktags %} + {%- if hasdoc('about') %} + + {%- endif %} + {%- if hasdoc('genindex') %} + + {%- endif %} + {%- if hasdoc('search') %} + + {%- endif %} + {%- if hasdoc('copyright') %} + + {%- endif %} + + {%- if parents %} + + {%- endif %} + {%- if next %} + + {%- endif %} + {%- if prev %} + + {%- endif %} +{%- endblock %} +{%- block extrahead %} {% endblock %} + + +{%- block header %}{% endblock %} + +{%- block relbar1 %}{{ relbar() }}{% endblock %} + +{%- block content %} + {%- block sidebar1 %} {# possible location for sidebar #} {% endblock %} + +
+ {%- block document %} +
+ {%- if render_sidebar %} +
+ {%- endif %} +
+ {% block body %} {% endblock %} +
+ {%- if render_sidebar %} +
+ {%- endif %} +
+ {%- endblock %} + + {%- block sidebar2 %}{{ sidebar() }}{% endblock %} +
+
+{%- endblock %} + +{%- block relbar2 %}{{ relbar() }}{% endblock %} + +{%- block footer %} + +

asdf asdf asdf asdf 22

+{%- endblock %} + + + diff --git a/docs/_themes/sphinx_rtd_theme/search.html b/docs/_themes/sphinx_rtd_theme/search.html new file mode 100755 index 00000000..d8bbe690 --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/search.html @@ -0,0 +1,50 @@ +{# + basic/search.html + ~~~~~~~~~~~~~~~~~ + + Template for the search page. + + :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +#} +{%- extends "layout.html" %} +{% set title = _('Search') %} +{% set script_files = script_files + ['_static/searchtools.js'] %} +{% block extrahead %} + + {# this is used when loading the search index using $.ajax fails, + such as on Chrome for documents on localhost #} + + {{ super() }} +{% endblock %} +{% block body %} + + + {% if search_performed %} +

{{ _('Search Results') }}

+ {% if not search_results %} +

{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}

+ {% endif %} + {% endif %} +
+ {% if search_results %} +
    + {% for href, caption, context in search_results %} +
  • + {{ caption }} +

    {{ context|e }}

    +
  • + {% endfor %} +
+ {% endif %} +
+{% endblock %} diff --git a/docs/_themes/sphinx_rtd_theme/searchbox.html b/docs/_themes/sphinx_rtd_theme/searchbox.html new file mode 100755 index 00000000..f62545ea --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/searchbox.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/docs/_themes/sphinx_rtd_theme/static/css/badge_only.css b/docs/_themes/sphinx_rtd_theme/static/css/badge_only.css new file mode 100755 index 00000000..7fccc414 --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/static/css/badge_only.css @@ -0,0 +1 @@ +.font-smooth,.icon:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:fontawesome-webfont;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#fontawesome-webfont") format("svg")}.icon:before{display:inline-block;font-family:fontawesome-webfont;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .icon{display:inline-block;text-decoration:inherit}li .icon{display:inline-block}li .icon-large:before,li .icon-large:before{width:1.875em}ul.icons{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.icons li .icon{width:0.8em}ul.icons li .icon-large:before,ul.icons li .icon-large:before{vertical-align:baseline}.icon-book:before{content:"\f02d"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}} diff --git a/docs/_themes/sphinx_rtd_theme/static/css/theme.css b/docs/_themes/sphinx_rtd_theme/static/css/theme.css new file mode 100755 index 00000000..a37f8d8c --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/static/css/theme.css @@ -0,0 +1 @@ +*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:20px 0;padding:0}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:0.2em 0;background:#ccc;color:#000;padding:0.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.font-smooth,.icon:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-tag-input-group .wy-tag .wy-tag-remove:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-tag-input-group,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:fontawesome-webfont;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#fontawesome-webfont") format("svg")}.icon:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-tag-input-group .wy-tag .wy-tag-remove:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before{display:inline-block;font-family:fontawesome-webfont;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .icon,a .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success a .wy-input-context,a .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger a .wy-input-context,a .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning a .wy-input-context,a .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info a .wy-input-context,a .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag a .wy-tag-remove,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink{display:inline-block;text-decoration:inherit}.icon-large:before{vertical-align:-10%;font-size:1.33333em}.btn .icon,.btn .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success .btn .wy-input-context,.btn .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .btn .wy-input-context,.btn .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .btn .wy-input-context,.btn .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info .btn .wy-input-context,.btn .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag .btn .wy-tag-remove,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.nav .icon,.nav .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success .nav .wy-input-context,.nav .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .nav .wy-input-context,.nav .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .nav .wy-input-context,.nav .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info .nav .wy-input-context,.nav .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag .nav .wy-tag-remove,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink{display:inline}.btn .icon.icon-large,.btn .wy-inline-validate.wy-inline-validate-success .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-success .btn .icon-large.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-danger .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .btn .icon-large.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-warning .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .btn .icon-large.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-info .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-info .btn .icon-large.wy-input-context,.btn .wy-tag-input-group .wy-tag .icon-large.wy-tag-remove,.wy-tag-input-group .wy-tag .btn .icon-large.wy-tag-remove,.btn .rst-content .icon-large.admonition-title,.rst-content .btn .icon-large.admonition-title,.btn .rst-content h1 .icon-large.headerlink,.rst-content h1 .btn .icon-large.headerlink,.btn .rst-content h2 .icon-large.headerlink,.rst-content h2 .btn .icon-large.headerlink,.btn .rst-content h3 .icon-large.headerlink,.rst-content h3 .btn .icon-large.headerlink,.btn .rst-content h4 .icon-large.headerlink,.rst-content h4 .btn .icon-large.headerlink,.btn .rst-content h5 .icon-large.headerlink,.rst-content h5 .btn .icon-large.headerlink,.btn .rst-content h6 .icon-large.headerlink,.rst-content h6 .btn .icon-large.headerlink,.btn .rst-content dl dt .icon-large.headerlink,.rst-content dl dt .btn .icon-large.headerlink,.nav .icon.icon-large,.nav .wy-inline-validate.wy-inline-validate-success .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-success .nav .icon-large.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-danger .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .nav .icon-large.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-warning .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .nav .icon-large.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-info .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-info .nav .icon-large.wy-input-context,.nav .wy-tag-input-group .wy-tag .icon-large.wy-tag-remove,.wy-tag-input-group .wy-tag .nav .icon-large.wy-tag-remove,.nav .rst-content .icon-large.admonition-title,.rst-content .nav .icon-large.admonition-title,.nav .rst-content h1 .icon-large.headerlink,.rst-content h1 .nav .icon-large.headerlink,.nav .rst-content h2 .icon-large.headerlink,.rst-content h2 .nav .icon-large.headerlink,.nav .rst-content h3 .icon-large.headerlink,.rst-content h3 .nav .icon-large.headerlink,.nav .rst-content h4 .icon-large.headerlink,.rst-content h4 .nav .icon-large.headerlink,.nav .rst-content h5 .icon-large.headerlink,.rst-content h5 .nav .icon-large.headerlink,.nav .rst-content h6 .icon-large.headerlink,.rst-content h6 .nav .icon-large.headerlink,.nav .rst-content dl dt .icon-large.headerlink,.rst-content dl dt .nav .icon-large.headerlink{line-height:0.9em}.btn .icon.icon-spin,.btn .wy-inline-validate.wy-inline-validate-success .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-success .btn .icon-spin.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-danger .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .btn .icon-spin.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-warning .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .btn .icon-spin.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-info .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-info .btn .icon-spin.wy-input-context,.btn .wy-tag-input-group .wy-tag .icon-spin.wy-tag-remove,.wy-tag-input-group .wy-tag .btn .icon-spin.wy-tag-remove,.btn .rst-content .icon-spin.admonition-title,.rst-content .btn .icon-spin.admonition-title,.btn .rst-content h1 .icon-spin.headerlink,.rst-content h1 .btn .icon-spin.headerlink,.btn .rst-content h2 .icon-spin.headerlink,.rst-content h2 .btn .icon-spin.headerlink,.btn .rst-content h3 .icon-spin.headerlink,.rst-content h3 .btn .icon-spin.headerlink,.btn .rst-content h4 .icon-spin.headerlink,.rst-content h4 .btn .icon-spin.headerlink,.btn .rst-content h5 .icon-spin.headerlink,.rst-content h5 .btn .icon-spin.headerlink,.btn .rst-content h6 .icon-spin.headerlink,.rst-content h6 .btn .icon-spin.headerlink,.btn .rst-content dl dt .icon-spin.headerlink,.rst-content dl dt .btn .icon-spin.headerlink,.nav .icon.icon-spin,.nav .wy-inline-validate.wy-inline-validate-success .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-success .nav .icon-spin.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-danger .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .nav .icon-spin.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-warning .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .nav .icon-spin.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-info .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-info .nav .icon-spin.wy-input-context,.nav .wy-tag-input-group .wy-tag .icon-spin.wy-tag-remove,.wy-tag-input-group .wy-tag .nav .icon-spin.wy-tag-remove,.nav .rst-content .icon-spin.admonition-title,.rst-content .nav .icon-spin.admonition-title,.nav .rst-content h1 .icon-spin.headerlink,.rst-content h1 .nav .icon-spin.headerlink,.nav .rst-content h2 .icon-spin.headerlink,.rst-content h2 .nav .icon-spin.headerlink,.nav .rst-content h3 .icon-spin.headerlink,.rst-content h3 .nav .icon-spin.headerlink,.nav .rst-content h4 .icon-spin.headerlink,.rst-content h4 .nav .icon-spin.headerlink,.nav .rst-content h5 .icon-spin.headerlink,.rst-content h5 .nav .icon-spin.headerlink,.nav .rst-content h6 .icon-spin.headerlink,.rst-content h6 .nav .icon-spin.headerlink,.nav .rst-content dl dt .icon-spin.headerlink,.rst-content dl dt .nav .icon-spin.headerlink{display:inline-block}.btn.icon:before,.wy-inline-validate.wy-inline-validate-success .btn.wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .btn.wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .btn.wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .btn.wy-input-context:before,.wy-tag-input-group .wy-tag .btn.wy-tag-remove:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before{opacity:0.5;-webkit-transition:opacity 0.05s ease-in;-moz-transition:opacity 0.05s ease-in;transition:opacity 0.05s ease-in}.btn.icon:hover:before,.wy-inline-validate.wy-inline-validate-success .btn.wy-input-context:hover:before,.wy-inline-validate.wy-inline-validate-danger .btn.wy-input-context:hover:before,.wy-inline-validate.wy-inline-validate-warning .btn.wy-input-context:hover:before,.wy-inline-validate.wy-inline-validate-info .btn.wy-input-context:hover:before,.wy-tag-input-group .wy-tag .btn.wy-tag-remove:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before{opacity:1}.btn-mini .icon:before,.btn-mini .wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .btn-mini .wy-input-context:before,.btn-mini .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .btn-mini .wy-input-context:before,.btn-mini .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .btn-mini .wy-input-context:before,.btn-mini .wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .btn-mini .wy-input-context:before,.btn-mini .wy-tag-input-group .wy-tag .wy-tag-remove:before,.wy-tag-input-group .wy-tag .btn-mini .wy-tag-remove:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before{font-size:14px;vertical-align:-15%}li .icon,li .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success li .wy-input-context,li .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger li .wy-input-context,li .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning li .wy-input-context,li .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info li .wy-input-context,li .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag li .wy-tag-remove,li .rst-content .admonition-title,.rst-content li .admonition-title,li .rst-content h1 .headerlink,.rst-content h1 li .headerlink,li .rst-content h2 .headerlink,.rst-content h2 li .headerlink,li .rst-content h3 .headerlink,.rst-content h3 li .headerlink,li .rst-content h4 .headerlink,.rst-content h4 li .headerlink,li .rst-content h5 .headerlink,.rst-content h5 li .headerlink,li .rst-content h6 .headerlink,.rst-content h6 li .headerlink,li .rst-content dl dt .headerlink,.rst-content dl dt li .headerlink{display:inline-block}li .icon-large:before,li .icon-large:before{width:1.875em}ul.icons{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.icons li .icon,ul.icons li .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success ul.icons li .wy-input-context,ul.icons li .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger ul.icons li .wy-input-context,ul.icons li .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning ul.icons li .wy-input-context,ul.icons li .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info ul.icons li .wy-input-context,ul.icons li .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag ul.icons li .wy-tag-remove,ul.icons li .rst-content .admonition-title,.rst-content ul.icons li .admonition-title,ul.icons li .rst-content h1 .headerlink,.rst-content h1 ul.icons li .headerlink,ul.icons li .rst-content h2 .headerlink,.rst-content h2 ul.icons li .headerlink,ul.icons li .rst-content h3 .headerlink,.rst-content h3 ul.icons li .headerlink,ul.icons li .rst-content h4 .headerlink,.rst-content h4 ul.icons li .headerlink,ul.icons li .rst-content h5 .headerlink,.rst-content h5 ul.icons li .headerlink,ul.icons li .rst-content h6 .headerlink,.rst-content h6 ul.icons li .headerlink,ul.icons li .rst-content dl dt .headerlink,.rst-content dl dt ul.icons li .headerlink{width:0.8em}ul.icons li .icon-large:before,ul.icons li .icon-large:before{vertical-align:baseline}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope-alt:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before,.wy-tag-input-group .wy-tag .wy-tag-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-power-off:before,.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-gear:before,.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file-alt:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-rotate-right:before,.icon-repeat:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-mail-forward:before,.icon-share-alt:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-gears:before,.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up-alt:before{content:"\f087"}.icon-thumbs-down-alt:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-unchecked:before,.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paperclip:before,.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-rotate-left:before,.icon-undo:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-text-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-mail-reply:before,.icon-reply:before{content:"\f112"}.icon-github-alt:before{content:"\f113"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"}.icon-expand-alt:before{content:"\f116"}.icon-collapse-alt:before{content:"\f117"}.icon-smile:before{content:"\f118"}.icon-frown:before{content:"\f119"}.icon-meh:before{content:"\f11a"}.icon-gamepad:before{content:"\f11b"}.icon-keyboard:before{content:"\f11c"}.icon-flag-alt:before{content:"\f11d"}.icon-flag-checkered:before{content:"\f11e"}.icon-terminal:before{content:"\f120"}.icon-code:before{content:"\f121"}.icon-reply-all:before{content:"\f122"}.icon-mail-reply-all:before{content:"\f122"}.icon-star-half-full:before,.icon-star-half-empty:before{content:"\f123"}.icon-location-arrow:before{content:"\f124"}.icon-crop:before{content:"\f125"}.icon-code-fork:before{content:"\f126"}.icon-unlink:before{content:"\f127"}.icon-question:before{content:"\f128"}.icon-info:before{content:"\f129"}.icon-exclamation:before{content:"\f12a"}.icon-superscript:before{content:"\f12b"}.icon-subscript:before{content:"\f12c"}.icon-eraser:before{content:"\f12d"}.icon-puzzle-piece:before{content:"\f12e"}.icon-microphone:before{content:"\f130"}.icon-microphone-off:before{content:"\f131"}.icon-shield:before{content:"\f132"}.icon-calendar-empty:before{content:"\f133"}.icon-fire-extinguisher:before{content:"\f134"}.icon-rocket:before{content:"\f135"}.icon-maxcdn:before{content:"\f136"}.icon-chevron-sign-left:before{content:"\f137"}.icon-chevron-sign-right:before{content:"\f138"}.icon-chevron-sign-up:before{content:"\f139"}.icon-chevron-sign-down:before{content:"\f13a"}.icon-html5:before{content:"\f13b"}.icon-css3:before{content:"\f13c"}.icon-anchor:before{content:"\f13d"}.icon-unlock-alt:before{content:"\f13e"}.icon-bullseye:before{content:"\f140"}.icon-ellipsis-horizontal:before{content:"\f141"}.icon-ellipsis-vertical:before{content:"\f142"}.icon-rss-sign:before{content:"\f143"}.icon-play-sign:before{content:"\f144"}.icon-ticket:before{content:"\f145"}.icon-minus-sign-alt:before{content:"\f146"}.icon-check-minus:before{content:"\f147"}.icon-level-up:before{content:"\f148"}.icon-level-down:before{content:"\f149"}.icon-check-sign:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:"\f14a"}.icon-edit-sign:before{content:"\f14b"}.icon-external-link-sign:before{content:"\f14c"}.icon-share-sign:before{content:"\f14d"}.icon-compass:before{content:"\f14e"}.icon-collapse:before{content:"\f150"}.icon-collapse-top:before{content:"\f151"}.icon-expand:before{content:"\f152"}.icon-euro:before,.icon-eur:before{content:"\f153"}.icon-gbp:before{content:"\f154"}.icon-dollar:before,.icon-usd:before{content:"\f155"}.icon-rupee:before,.icon-inr:before{content:"\f156"}.icon-yen:before,.icon-jpy:before{content:"\f157"}.icon-renminbi:before,.icon-cny:before{content:"\f158"}.icon-won:before,.icon-krw:before{content:"\f159"}.icon-bitcoin:before,.icon-btc:before{content:"\f15a"}.icon-file:before{content:"\f15b"}.icon-file-text:before{content:"\f15c"}.icon-sort-by-alphabet:before{content:"\f15d"}.icon-sort-by-alphabet-alt:before{content:"\f15e"}.icon-sort-by-attributes:before{content:"\f160"}.icon-sort-by-attributes-alt:before{content:"\f161"}.icon-sort-by-order:before{content:"\f162"}.icon-sort-by-order-alt:before{content:"\f163"}.icon-thumbs-up:before{content:"\f164"}.icon-thumbs-down:before{content:"\f165"}.icon-youtube-sign:before{content:"\f166"}.icon-youtube:before{content:"\f167"}.icon-xing:before{content:"\f168"}.icon-xing-sign:before{content:"\f169"}.icon-youtube-play:before{content:"\f16a"}.icon-dropbox:before{content:"\f16b"}.icon-stackexchange:before{content:"\f16c"}.icon-instagram:before{content:"\f16d"}.icon-flickr:before{content:"\f16e"}.icon-adn:before{content:"\f170"}.icon-bitbucket:before{content:"\f171"}.icon-bitbucket-sign:before{content:"\f172"}.icon-tumblr:before{content:"\f173"}.icon-tumblr-sign:before{content:"\f174"}.icon-long-arrow-down:before{content:"\f175"}.icon-long-arrow-up:before{content:"\f176"}.icon-long-arrow-left:before{content:"\f177"}.icon-long-arrow-right:before{content:"\f178"}.icon-apple:before{content:"\f179"}.icon-windows:before{content:"\f17a"}.icon-android:before{content:"\f17b"}.icon-linux:before{content:"\f17c"}.icon-dribbble:before{content:"\f17d"}.icon-skype:before{content:"\f17e"}.icon-foursquare:before{content:"\f180"}.icon-trello:before{content:"\f181"}.icon-female:before{content:"\f182"}.icon-male:before{content:"\f183"}.icon-gittip:before{content:"\f184"}.icon-sun:before{content:"\f185"}.icon-moon:before{content:"\f186"}.icon-archive:before{content:"\f187"}.icon-bug:before{content:"\f188"}.icon-vk:before{content:"\f189"}.icon-weibo:before{content:"\f18a"}.icon-renren:before{content:"\f18b"}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso{padding:12px;line-height:24px;margin-bottom:24px}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:transparent;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso{background:#f3f6f6}.wy-alert.wy-alert-neutral strong,.rst-content .wy-alert-neutral.note strong,.rst-content .wy-alert-neutral.attention strong,.rst-content .wy-alert-neutral.caution strong,.rst-content .wy-alert-neutral.danger strong,.rst-content .wy-alert-neutral.error strong,.rst-content .wy-alert-neutral.hint strong,.rst-content .wy-alert-neutral.important strong,.rst-content .wy-alert-neutral.tip strong,.rst-content .wy-alert-neutral.warning strong,.rst-content .wy-alert-neutral.seealso strong{color:#404040}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a{color:#2980b9}.wy-tray-container{position:fixed;top:-50px;left:0;width:100%;-webkit-transition:top 0.2s ease-in;-moz-transition:top 0.2s ease-in;transition:top 0.2s ease-in}.wy-tray-container.on{top:0}.wy-tray-container li{display:none;width:100%;background:#343131;padding:12px 24px;color:#fff;margin-bottom:6px;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1),0px -1px 2px -1px rgba(255,255,255,0.5) inset}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.btn{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:100%;padding:6px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);border-bottom:solid 3px rgba(0,0,0,0.1);background-color:#27ae60;text-decoration:none;font-weight:500;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset;-webkit-transition:all 0.1s linear;-moz-transition:all 0.1s linear;transition:all 0.1s linear;outline-none:false}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;color:#fff;outline:0}.btn:active{border-top:solid 3px rgba(0,0,0,0.1);border-bottom:solid 1px rgba(0,0,0,0.1);box-shadow:0px 1px 2px -1px rgba(0,0,0,0.5) inset}.btn[disabled]{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-danger{background-color:#e74c3c !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#e67e22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#343131}.btn-invert:hover{background-color:#413d3d !important}.btn-link{background-color:transparent !important;color:#2980b9;border-color:transparent}.btn-link:hover{background-color:transparent !important;color:#409ad5;border-color:transparent}.btn-link:active{background-color:transparent !important;border-color:transparent;border-top:solid 1px transparent;border-bottom:solid 3px transparent}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown:hover .wy-dropdown-menu{display:block}.wy-dropdown .caret:after{font-family:fontawesome-webfont;content:"\f0d7";font-size:70%}.wy-dropdown-menu{position:absolute;top:100%;left:0;display:none;float:left;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:0.5em 1em 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:0.5em}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 0.3125em 0;color:#999;font-size:90%}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button{-webkit-appearance:button;cursor:pointer;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}input[type="datetime-local"]{padding:0.34375em 0.625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:0.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#2980b9}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#f3f6f6;color:#cad2d3}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#e9322d}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%}select,textarea{padding:0.5em 0.625em;display:inline-block;border:1px solid #ccc;font-size:0.8em;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fff;color:#cad2d3;border-color:transparent}.wy-checkbox,.wy-radio{margin:0.5em 0;color:#404040 !important;display:block}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{padding:6px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:0.5em 0.625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.wy-control-group{margin-bottom:24px;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 2px #e74c3c}.wy-control-group.wy-control-group-error textarea{border:solid 2px #e74c3c}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:0.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#ccc;font-size:70%;margin-top:0.3125em;font-style:italic}.wy-tag-input-group{padding:4px 4px 0px 4px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}.wy-tag-input-group .wy-tag{display:inline-block;background-color:rgba(0,0,0,0.1);padding:0.5em 0.625em;border-radius:2px;position:relative;margin-bottom:4px}.wy-tag-input-group .wy-tag .wy-tag-remove{color:#ccc;margin-left:5px}.wy-tag-input-group .wy-tag .wy-tag-remove:hover{color:#e74c3c}.wy-tag-input-group label{margin-left:5px;display:inline-block;margin-bottom:0}.wy-tag-input-group input{border:none;font-size:100%;margin-bottom:4px;box-shadow:none}.wy-form-upload{border:solid 1px #ccc;border-bottom:solid 3px #ccc;background-color:#fff;padding:24px;display:inline-block;text-align:center;cursor:pointer;color:#404040;-webkit-transition:border-color 0.1s ease-in;-moz-transition:border-color 0.1s ease-in;transition:border-color 0.1s ease-in;*zoom:1}.wy-form-upload:before,.wy-form-upload:after{display:table;content:""}.wy-form-upload:after{clear:both}@media screen and (max-width: 480px){.wy-form-upload{width:100%}}.wy-form-upload .image-drop{display:none}.wy-form-upload .image-desktop{display:none}.wy-form-upload .image-loading{display:none}.wy-form-upload .wy-form-upload-icon{display:block;font-size:32px;color:#b3b3b3}.wy-form-upload .image-drop .wy-form-upload-icon{color:#27ae60}.wy-form-upload p{font-size:90%}.wy-form-upload .wy-form-upload-image{float:left;margin-right:24px}@media screen and (max-width: 480px){.wy-form-upload .wy-form-upload-image{width:100%;margin-bottom:24px}}.wy-form-upload img{max-width:125px;max-height:125px;opacity:0.9;-webkit-transition:opacity 0.1s ease-in;-moz-transition:opacity 0.1s ease-in;transition:opacity 0.1s ease-in}.wy-form-upload .wy-form-upload-content{float:left}@media screen and (max-width: 480px){.wy-form-upload .wy-form-upload-content{width:100%}}.wy-form-upload:hover{border-color:#b3b3b3;color:#404040}.wy-form-upload:hover .image-desktop{display:block}.wy-form-upload:hover .image-drag{display:none}.wy-form-upload:hover img{opacity:1}.wy-form-upload:active{border-top:solid 3px #ccc;border-bottom:solid 1px #ccc}.wy-form-upload.wy-form-upload-big{width:100%;text-align:center;padding:72px}.wy-form-upload.wy-form-upload-big .wy-form-upload-content{float:none}.wy-form-upload.wy-form-upload-file p{margin-bottom:0}.wy-form-upload.wy-form-upload-file .wy-form-upload-icon{display:inline-block;font-size:inherit}.wy-form-upload.wy-form-upload-drop{background-color:#ddf7e8}.wy-form-upload.wy-form-upload-drop .image-drop{display:block}.wy-form-upload.wy-form-upload-drop .image-desktop{display:none}.wy-form-upload.wy-form-upload-drop .image-drag{display:none}.wy-form-upload.wy-form-upload-loading .image-drag{display:none}.wy-form-upload.wy-form-upload-loading .image-desktop{display:none}.wy-form-upload.wy-form-upload-loading .image-loading{display:block}.wy-form-upload.wy-form-upload-loading .wy-input-prefix{display:none}.wy-form-upload.wy-form-upload-loading p{margin-bottom:0}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}.wy-form-gallery-manage{margin-left:-12px;margin-right:-12px}.wy-form-gallery-manage li{float:left;padding:12px;width:20%;cursor:pointer}@media screen and (max-width: 768px){.wy-form-gallery-manage li{width:25%}}@media screen and (max-width: 480px){.wy-form-gallery-manage li{width:50%}}.wy-form-gallery-manage li:active{cursor:move}.wy-form-gallery-manage li>a{padding:12px;background-color:#fff;border:solid 1px #e1e4e5;border-bottom:solid 3px #e1e4e5;display:inline-block;-webkit-transition:all 0.1s ease-in;-moz-transition:all 0.1s ease-in;transition:all 0.1s ease-in}.wy-form-gallery-manage li>a:active{border:solid 1px #ccc;border-top:solid 3px #ccc}.wy-form-gallery-manage img{width:100%;-webkit-transition:all 0.05s ease-in;-moz-transition:all 0.05s ease-in;transition:all 0.05s ease-in}li.wy-form-gallery-edit{position:relative;color:#fff;padding:24px;width:100%;display:block;background-color:#343131;border-radius:4px}li.wy-form-gallery-edit .arrow{position:absolute;display:block;top:-50px;left:50%;margin-left:-25px;z-index:500;height:0;width:0;border-color:transparent;border-style:solid;border-width:25px;border-bottom-color:#343131}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:0.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0.3em;display:block}.wy-form label{margin-bottom:0.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:0.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-controls{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:0.2em 0 0.8em}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-grid-one-col{*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;max-width:1066px;margin-top:1.618em}.wy-grid-one-col:before,.wy-grid-one-col:after{display:table;content:""}.wy-grid-one-col:after{clear:both}.wy-grid-one-col section{display:block;float:left;margin-right:2.35765%;width:100%;background:#fcfcfc;padding:1.618em;margin-right:0}.wy-grid-one-col section:last-child{margin-right:0}.wy-grid-index-card{*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;max-width:460px;margin-top:1.618em;background:#fcfcfc;padding:1.618em}.wy-grid-index-card:before,.wy-grid-index-card:after{display:table;content:""}.wy-grid-index-card:after{clear:both}.wy-grid-index-card header,.wy-grid-index-card section,.wy-grid-index-card aside{display:block;float:left;margin-right:2.35765%;width:100%}.wy-grid-index-card header:last-child,.wy-grid-index-card section:last-child,.wy-grid-index-card aside:last-child{margin-right:0}.wy-grid-index-card.twocol{max-width:768px}.wy-grid-index-card.twocol section{display:block;float:left;margin-right:2.35765%;width:48.82117%}.wy-grid-index-card.twocol section:last-child{margin-right:0}.wy-grid-index-card.twocol aside{display:block;float:left;margin-right:2.35765%;width:48.82117%}.wy-grid-index-card.twocol aside:last-child{margin-right:0}.wy-grid-search-filter{*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;margin-bottom:24px}.wy-grid-search-filter:before,.wy-grid-search-filter:after{display:table;content:""}.wy-grid-search-filter:after{clear:both}.wy-grid-search-filter .wy-grid-search-filter-input{display:block;float:left;margin-right:2.35765%;width:74.41059%}.wy-grid-search-filter .wy-grid-search-filter-input:last-child{margin-right:0}.wy-grid-search-filter .wy-grid-search-filter-btn{display:block;float:left;margin-right:2.35765%;width:23.23176%}.wy-grid-search-filter .wy-grid-search-filter-btn:last-child{margin-right:0}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px;margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}a{color:#2980b9;text-decoration:none}a:hover{color:#3091d1}.link-danger{color:#e74c3c}.link-danger:hover{color:#d62c1a}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}small{font-size:80%}code,.rst-content tt{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:"Incosolata","Consolata","Monaco",monospace;color:#e74c3c;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.full-width{width:100%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li{list-style:square}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li{list-style:decimal;margin-left:24px}.wy-type-large{font-size:120%}.wy-type-normal{font-size:100%}.wy-type-small{font-size:100%}.wy-type-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980b9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27ae60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#e74c3c !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}.codeblock-example{border:1px solid #e1e4e5;border-bottom:none;padding:24px;padding-top:48px;font-weight:500;background:#fff;position:relative}.codeblock-example:after{content:"Example";position:absolute;top:0px;left:0px;background:#9b59b6;color:#fff;padding:6px 12px}.codeblock-example.prettyprint-example-only{border:1px solid #e1e4e5;margin-bottom:24px}.codeblock,.rst-content .literal-block,div[class^='highlight']{border:1px solid #e1e4e5;padding:0px;overflow-x:auto;background:#fff;margin:1px 0 24px 0}.codeblock div[class^='highlight'],.rst-content .literal-block div[class^='highlight'],div[class^='highlight'] div[class^='highlight']{border:none;background:none;margin:0}div[class^='highlight'] td.code{width:100%}.linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:"Incosolata","Consolata","Monaco",monospace;font-size:12px;line-height:1.5;color:#d9d9d9}div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;font-family:"Incosolata","Consolata","Monaco",monospace;font-size:12px;line-height:1.5;display:block;overflow:auto;color:#404040}pre.literal-block{@extends .codeblock;}@media print{.codeblock,.rst-content .literal-block,div[class^='highlight'],div[class^='highlight'] pre{white-space:pre-wrap}}.hll{background-color:#ffc;margin:0 -12px;padding:0 12px;display:block}.c{color:#998;font-style:italic}.err{color:#a61717;background-color:#e3d2d2}.k{font-weight:bold}.o{font-weight:bold}.cm{color:#998;font-style:italic}.cp{color:#999;font-weight:bold}.c1{color:#998;font-style:italic}.cs{color:#999;font-weight:bold;font-style:italic}.gd{color:#000;background-color:#fdd}.gd .x{color:#000;background-color:#faa}.ge{font-style:italic}.gr{color:#a00}.gh{color:#999}.gi{color:#000;background-color:#dfd}.gi .x{color:#000;background-color:#afa}.go{color:#888}.gp{color:#555}.gs{font-weight:bold}.gu{color:purple;font-weight:bold}.gt{color:#a00}.kc{font-weight:bold}.kd{font-weight:bold}.kn{font-weight:bold}.kp{font-weight:bold}.kr{font-weight:bold}.kt{color:#458;font-weight:bold}.m{color:#099}.s{color:#d14}.n{color:#333}.na{color:teal}.nb{color:#0086b3}.nc{color:#458;font-weight:bold}.no{color:teal}.ni{color:purple}.ne{color:#900;font-weight:bold}.nf{color:#900;font-weight:bold}.nn{color:#555}.nt{color:navy}.nv{color:teal}.ow{font-weight:bold}.w{color:#bbb}.mf{color:#099}.mh{color:#099}.mi{color:#099}.mo{color:#099}.sb{color:#d14}.sc{color:#d14}.sd{color:#d14}.s2{color:#d14}.se{color:#d14}.sh{color:#d14}.si{color:#d14}.sx{color:#d14}.sr{color:#009926}.s1{color:#d14}.ss{color:#990073}.bp{color:#999}.vc{color:teal}.vg{color:teal}.vi{color:teal}.il{color:#099}.gc{color:#999;background-color:#eaf2f5}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical header{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;color:#2980b9;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:0.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:0.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.tocktree-l2.current>a{background:#c9c9c9}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical .local-toc li ul{display:block}.wy-menu-vertical li ul li a{margin-bottom:0;color:#b3b3b3;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:0.4045em 1.618em;display:block;position:relative;font-size:90%;color:#b3b3b3}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-side-nav-search{z-index:200;background-color:#2980b9;text-align:center;padding:0.809em;display:block;color:#fcfcfc;margin-bottom:0.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto 0.809em auto;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:0.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all 0.2s ease-in;-moz-transition:all 0.2s ease-in;transition:all 0.2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:left repeat-y #fcfcfc;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);background-size:300px 1px}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:absolute;top:0;left:0;width:300px;overflow:hidden;min-height:100%;background:#343131;z-index:200}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:0.4045em 0.809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:#999}footer p{margin-bottom:12px}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1400px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag .rst-versions .rst-current-version .wy-tag-remove,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink{color:#fcfcfc}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}}.rst-content img{max-width:100%;height:auto !important}.rst-content .section>img{margin-bottom:24px}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .line-block{margin-left:24px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto;display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink{display:none;visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after{visibility:visible;content:"\f0c1";font-family:fontawesome-webfont;display:inline-block}.rst-content h1:hover .headerlink,.rst-content h2:hover .headerlink,.rst-content h3:hover .headerlink,.rst-content h4:hover .headerlink,.rst-content h5:hover .headerlink,.rst-content h6:hover .headerlink,.rst-content dl dt:hover .headerlink{display:inline-block}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:super;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:#999}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left;padding-left:0}.rst-content tt{color:#000}.rst-content tt big,.rst-content tt em{font-size:100% !important;line-height:normal}.rst-content tt .xref,a .rst-content tt{font-weight:bold}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:inline-block;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:gray}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040} diff --git a/docs/_themes/sphinx_rtd_theme/static/favicon.ico b/docs/_themes/sphinx_rtd_theme/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6970cfde25f4158dcde472becc0e56d7eb26d10c GIT binary patch literal 6261 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 literal 0 HcmV?d00001 diff --git a/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.eot b/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..0662cb96bfb78cb2603df4bc9995314bd6806312 GIT binary patch literal 37405 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�-A&iY%khEsGVMaq&$BJA^QAs8x>7-g_ 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 k3oQ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.ttf b/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..d3659246915cacb0c9204271f1f9fc5f77049eac GIT binary patch literal 79076 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 literal 0 HcmV?d00001 diff --git a/docs/_themes/sphinx_rtd_theme/static/js/theme.js b/docs/_themes/sphinx_rtd_theme/static/js/theme.js new file mode 100755 index 00000000..58e514c0 --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/static/js/theme.js @@ -0,0 +1,16 @@ +$( document ).ready(function() { + // Shift nav in mobile when clicking the menu. + $("[data-toggle='wy-nav-top']").click(function() { + $("[data-toggle='wy-nav-shift']").toggleClass("shift"); + $("[data-toggle='rst-versions']").toggleClass("shift"); + }); + // Close menu when you click a link. + $(".wy-menu-vertical .current ul li a").click(function() { + $("[data-toggle='wy-nav-shift']").removeClass("shift"); + $("[data-toggle='rst-versions']").toggleClass("shift"); + }); + $("[data-toggle='rst-current-version']").click(function() { + $("[data-toggle='rst-versions']").toggleClass("shift-up"); + }); + $("table.docutils:not(.field-list").wrap("

"); +}); diff --git a/docs/_themes/sphinx_rtd_theme/theme.conf b/docs/_themes/sphinx_rtd_theme/theme.conf new file mode 100755 index 00000000..173ca698 --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/theme.conf @@ -0,0 +1,8 @@ +[theme] +inherit = basic +stylesheet = css/theme.css + +[options] +typekit_id = hiw1hhg +analytics_id = +canonical_url = \ No newline at end of file diff --git a/docs/_themes/sphinx_rtd_theme/versions.html b/docs/_themes/sphinx_rtd_theme/versions.html new file mode 100755 index 00000000..93319be8 --- /dev/null +++ b/docs/_themes/sphinx_rtd_theme/versions.html @@ -0,0 +1,37 @@ +{% if READTHEDOCS %} +{# Add rst-badge after rst-versions for small badge style. #} +
+ + Read the Docs + v: {{ current_version }} + + +
+
+
Versions
+ {% for slug, url in versions %} +
{{ slug }}
+ {% endfor %} +
+
+
Downloads
+ {% for type, url in downloads %} +
{{ type }}
+ {% endfor %} +
+
+
On Read the Docs
+
+ Project Home +
+
+ Builds +
+
+
+ Free document hosting provided by Read the Docs. + +
+
+{% endif %} + diff --git a/docs/conf.py b/docs/conf.py index 40c1f430..cddd35db 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -92,7 +92,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -116,7 +116,7 @@ html_theme_path = ['_themes'] # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +html_favicon = "favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -200,3 +200,6 @@ latex_documents = [ autoclass_content = 'both' +html_theme_options = dict( + canonical_url='http://docs.mongoengine.org/en/latest/' +) From c5cc4b78672ce1f70379a1b6677f574f3605d686 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jan 2014 16:57:49 +0000 Subject: [PATCH 134/222] Updated docs --- docs/apireference.rst | 1 + docs/guide/connecting.rst | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/apireference.rst b/docs/apireference.rst index 9057de55..6c42d40a 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -38,6 +38,7 @@ Context Managers ================ .. autoclass:: mongoengine.context_managers.switch_db +.. autoclass:: mongoengine.context_managers.switch_collection .. autoclass:: mongoengine.context_managers.no_dereference .. autoclass:: mongoengine.context_managers.query_counter diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index f681aada..dd89938b 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -100,3 +100,18 @@ access to the same User document across databases:: .. 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:: + + from mongoengine.context_managers import switch_db + + class Group(Document): + name = StringField() + + Group(name="test").save() # Saves in the default db + + with switch_collection(Group, 'group2000') as Group: + Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection From 1b62dd5c405cb47f8dcbaa724ce2d9f9cc73e180 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 14 Jan 2014 10:01:39 +0000 Subject: [PATCH 135/222] Fix duplicate error check --- mongoengine/document.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 59bea359..b47490bd 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -280,7 +280,9 @@ class Document(BaseDocument): kwargs.update(cascade_kwargs) kwargs['_refs'] = _refs self.cascade_save(**kwargs) - + except pymongo.errors.DuplicateKeyError, err: + message = u'Tried to save duplicate unique keys (%s)' + raise NotUniqueError(message % unicode(err)) except pymongo.errors.OperationFailure, err: message = 'Could not save document (%s)' if re.match('^E1100[01] duplicate key', unicode(err)): From 708dbac70e89a216e6017dca6b5fa5c03e329409 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 14 Jan 2014 10:09:11 +0000 Subject: [PATCH 136/222] Depending on pymongo it might raise a TypeError or ValueError --- 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 ccf84636..cf6122a3 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -491,7 +491,7 @@ class IndexesTest(unittest.TestCase): def invalid_index_2(): return BlogPost.objects.hint(('tags', 1)) - self.assertRaises(TypeError, invalid_index_2) + self.assertRaises(Exception, invalid_index_2) def test_unique(self): """Ensure that uniqueness constraints are applied to fields. From 7914cd47ca732edf53e5473d5c0f3c532d5c1eaf Mon Sep 17 00:00:00 2001 From: David Wilemski Date: Fri, 17 Jan 2014 20:36:02 -0500 Subject: [PATCH 137/222] Fix typo in upgrade docs Removed extra closing parenthesis --- docs/upgrade.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index a1fccea2..fadda7c6 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -270,7 +270,7 @@ queryset you should upgrade to use count:: len(Animal.objects(type="mammal")) # New code - Animal.objects(type="mammal").count()) + Animal.objects(type="mammal").count() .only() now inline with .exclude() From 63ebb6998e8eaa258e518372d79a28fabcc30153 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Wed, 22 Jan 2014 22:47:48 +0200 Subject: [PATCH 138/222] geo_within docs - fix broken code samples No such thing as "geo_with" --- docs/guide/querying.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index f50985b5..32cbb94e 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -92,8 +92,8 @@ were added in 0.8 for: :class:`~mongoengine.fields.PointField`, * ``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_with=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) - loc.objects(point__geo_with={"type": "Polygon", + 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:: From 0277062693c7e07353f78bae113d557dd48cf1ca Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 24 Jan 2014 13:17:52 +0000 Subject: [PATCH 139/222] Stop ensure_indexes running on a secondaries (#555) --- docs/changelog.rst | 4 ++++ mongoengine/document.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3d8c3307..0a0d91dd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +Changes in 0.8.7 +================ +- Stop ensure_indexes running on a secondaries (#555) + Changes in 0.8.6 ================ - Fix django auth import (#531) diff --git a/mongoengine/document.py b/mongoengine/document.py index b47490bd..9a260279 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -549,6 +549,8 @@ class Document(BaseDocument): index_cls = cls._meta.get('index_cls', True) collection = cls._get_collection() + if collection.read_preference > 1: + return # determine if an index which we are creating includes # _cls as its first field; if so, we can avoid creating From 0e5a0661e1eb1df6f0d36046e2319f2a61b6ece7 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 24 Jan 2014 13:50:09 +0000 Subject: [PATCH 140/222] Fixed possible issue not catching duplicate key errors --- mongoengine/queryset/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index d57021fe..c2ad027e 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -302,8 +302,11 @@ class BaseQuerySet(object): signals.pre_bulk_insert.send(self._document, documents=docs) try: ids = self._collection.insert(raw, **write_concern) + except pymongo.errors.DuplicateKeyError, err: + message = 'Could not save document (%s)'; + raise NotUniqueError(message % unicode(err)) except pymongo.errors.OperationFailure, err: - message = 'Could not save document (%s)' + message = 'Could not save document (%s)'; if re.match('^E1100[01] duplicate key', unicode(err)): # E11000 - duplicate key error index # E11001 - duplicate key on update From bc08bea2849ec36a83d5f9c6181ed3bfe0e2d394 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 24 Jan 2014 13:54:16 +0000 Subject: [PATCH 141/222] Fix circular import issue with django auth (#531) (#545) --- docs/changelog.rst | 1 + mongoengine/django/auth.py | 12 +++++++++--- tests/test_django.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0a0d91dd..ec3a5f86 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.8.7 ================ - Stop ensure_indexes running on a secondaries (#555) +- Fix circular import issue with django auth (#531) (#545) Changes in 0.8.6 ================ diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py index 89ea003c..0a309c4c 100644 --- a/mongoengine/django/auth.py +++ b/mongoengine/django/auth.py @@ -9,7 +9,6 @@ from django.contrib.auth.models import AnonymousUser from django.utils.translation import ugettext_lazy as _ from .utils import datetime_now -from .mongo_auth.models import get_user_document REDIRECT_FIELD_NAME = 'next' @@ -382,9 +381,10 @@ class MongoEngineBackend(object): supports_object_permissions = False supports_anonymous_user = False supports_inactive_user = False + _user_doc = False def authenticate(self, username=None, password=None): - user = get_user_document().objects(username=username).first() + user = self.user_document.objects(username=username).first() if user: if password and user.check_password(password): backend = auth.get_backends()[0] @@ -393,8 +393,14 @@ class MongoEngineBackend(object): return None def get_user(self, user_id): - return get_user_document().objects.with_id(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 diff --git a/tests/test_django.py b/tests/test_django.py index 46568ac8..56a61640 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -16,6 +16,7 @@ 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: From 6d5e7d9e81f35f8b8f02063e6b2c80adaea30c4e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 24 Jan 2014 14:10:55 +0000 Subject: [PATCH 142/222] Calling reload on deleted / nonexistant documents raises DoesNotExist (#538) --- docs/changelog.rst | 1 + docs/upgrade.rst | 6 ++++++ mongoengine/document.py | 9 ++++++--- tests/document/instance.py | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ec3a5f86..51134238 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.8.7 ================ +- Calling reload on deleted / nonexistant documents raises DoesNotExist (#538) - Stop ensure_indexes running on a secondaries (#555) - Fix circular import issue with django auth (#531) (#545) diff --git a/docs/upgrade.rst b/docs/upgrade.rst index fadda7c6..b55fe312 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -2,6 +2,12 @@ Upgrading ######### +0.8.7 +***** + +Calling reload on deleted / nonexistant documents now raises a DoesNotExist +exception. + 0.8.2 to 0.8.3 ************** diff --git a/mongoengine/document.py b/mongoengine/document.py index 9a260279..114778eb 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -12,6 +12,7 @@ from mongoengine.common import _import_class from mongoengine.base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, BaseDict, BaseList, ALLOW_INHERITANCE, get_document) +from mongoengine.errors import ValidationError from mongoengine.queryset import OperationError, NotUniqueError, QuerySet from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME from mongoengine.context_managers import switch_db, switch_collection @@ -452,14 +453,16 @@ class Document(BaseDocument): .. versionadded:: 0.1.2 .. versionchanged:: 0.6 Now chainable """ + if not self.pk: + raise self.DoesNotExist("Document does not exist") obj = self._qs.read_preference(ReadPreference.PRIMARY).filter( - **self._object_key).limit(1).select_related(max_depth=max_depth) + **self._object_key).limit(1).select_related(max_depth=max_depth) + if obj: obj = obj[0] else: - msg = "Reloaded document has been deleted" - raise OperationError(msg) + raise self.DoesNotExist("Document does not exist") for field in self._fields_ordered: setattr(self, field, self._reload(field, obj[field])) self._changed_fields = obj._changed_fields diff --git a/tests/document/instance.py b/tests/document/instance.py index 982b1e6c..07db85a0 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -409,6 +409,27 @@ class InstanceTest(unittest.TestCase): self.assertEqual(len(doc.embedded_field.list_field), 4) self.assertEqual(len(doc.embedded_field.dict_field), 2) + def test_reload_doesnt_exist(self): + class Foo(Document): + pass + + f = Foo() + try: + f.reload() + except Foo.DoesNotExist: + pass + except Exception as ex: + self.assertFalse("Threw wrong exception") + + f.save() + f.delete() + try: + f.reload() + except Foo.DoesNotExist: + pass + except Exception as ex: + self.assertFalse("Threw wrong exception") + def test_dictionary_access(self): """Ensure that dictionary-style field access works properly. """ From 0da694b8457ea820627fcdc96472211d7bcda541 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 24 Jan 2014 16:23:52 +0000 Subject: [PATCH 143/222] 0.8.7 --- README.rst | 2 +- mongoengine/__init__.py | 2 +- python-mongoengine.spec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index ea4b5050..8df9c3a0 100644 --- a/README.rst +++ b/README.rst @@ -92,4 +92,4 @@ Community Contributing ============ -We welcome contributions! see the`Contribution guidelines `_ +We welcome contributions! see the `Contribution guidelines `_ diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 1537c444..ac33ad2b 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -15,7 +15,7 @@ import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) -VERSION = (0, 8, 6) +VERSION = (0, 8, 7) def get_version(): diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 07b169dc..eddb488d 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.8.6 +Version: 0.8.7 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB From 0d4afad342b6412d322c3ef20c61c7b51e9698ef Mon Sep 17 00:00:00 2001 From: Andrei Zbikowski Date: Fri, 24 Jan 2014 16:54:29 -0600 Subject: [PATCH 144/222] Fixes issue with recursive embedded document errors --- 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 f5eae8ff..c625f8b8 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -317,7 +317,7 @@ class BaseDocument(object): pk = "None" if hasattr(self, 'pk'): pk = self.pk - elif self._instance: + elif self._instance and hasattr(self._instance, 'pk'): pk = self._instance.pk message = "ValidationError (%s:%s) " % (self._class_name, pk) raise ValidationError(message, errors=errors) From b085993901e57797bdf8944e86a8a2611ac1e823 Mon Sep 17 00:00:00 2001 From: "Brian J. Dowling" Date: Mon, 27 Jan 2014 23:05:29 +0000 Subject: [PATCH 145/222] Allow dynamic dictionary-style field access Allows the doc[key] syntax to work for dynamicembeddeddocument fields Fixes #559 --- 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 f5eae8ff..c5ef087c 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -182,7 +182,7 @@ class BaseDocument(object): """Dictionary-style field access, set a field's value. """ # Ensure that the field exists before settings its value - if name not in self._fields: + if not self._dynamic and name not in self._fields: raise KeyError(name) return setattr(self, name, value) From d4b36496407fbf49c461342289f1a6e2c148ca32 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 28 Jan 2014 09:25:59 +0000 Subject: [PATCH 146/222] Added coveralls.io badge https://coveralls.io/r/MongoEngine/mongoengine --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 8df9c3a0..cc4524ae 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,9 @@ 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 + :target: https://coveralls.io/r/MongoEngine/mongoengine?branch=master About ===== From 9b2080d036859c4456fc56c3085a14e791853035 Mon Sep 17 00:00:00 2001 From: "Brian J. Dowling" Date: Tue, 28 Jan 2014 22:10:26 -0500 Subject: [PATCH 147/222] Added a test for allowing dynamic dictionary-style field access Closes #559 --- tests/document/dynamic.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/document/dynamic.py b/tests/document/dynamic.py index 6263e68c..bf69cb27 100644 --- a/tests/document/dynamic.py +++ b/tests/document/dynamic.py @@ -292,6 +292,44 @@ class DynamicTest(unittest.TestCase): person.save() self.assertEqual(Person.objects.first().age, 35) + def test_dynamic_and_embedded_dict_access(self): + """Ensure embedded dynamic documents work with dict[] style access""" + + class Address(EmbeddedDocument): + city = StringField() + + class Person(DynamicDocument): + name = StringField() + + Person.drop_collection() + + Person(name="Ross", address=Address(city="London")).save() + + person = Person.objects.first() + person.attrval = "This works" + + person["phone"] = "555-1212" # but this should too + + # Same thing two levels deep + person["address"]["city"] = "Lundenne" + person.save() + + self.assertEqual(Person.objects.first().address.city, "Lundenne") + + self.assertEqual(Person.objects.first().phone, "555-1212") + + person = Person.objects.first() + person.address = Address(city="Londinium") + person.save() + + self.assertEqual(Person.objects.first().address.city, "Londinium") + + + person = Person.objects.first() + person["age"] = 35 + person.save() + self.assertEqual(Person.objects.first().age, 35) + if __name__ == '__main__': unittest.main() From db1e69813bc0b31957e7589723a2f80cda74454b Mon Sep 17 00:00:00 2001 From: Frank Battaglia Date: Thu, 31 Oct 2013 20:43:07 -0400 Subject: [PATCH 148/222] add atomic conditions to save Conflicts: mongoengine/document.py --- mongoengine/document.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 114778eb..f870a078 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -13,7 +13,8 @@ from mongoengine.base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, BaseDict, BaseList, ALLOW_INHERITANCE, get_document) from mongoengine.errors import ValidationError -from mongoengine.queryset import OperationError, NotUniqueError, QuerySet +from mongoengine.queryset import (OperationError, NotUniqueError, + QuerySet, transform) from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME from mongoengine.context_managers import switch_db, switch_collection @@ -180,7 +181,7 @@ class Document(BaseDocument): def save(self, force_insert=False, validate=True, clean=True, write_concern=None, cascade=None, cascade_kwargs=None, - _refs=None, **kwargs): + _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 created. @@ -203,7 +204,8 @@ class Document(BaseDocument): :param cascade_kwargs: (optional) kwargs dictionary to be passed throw 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) .. versionchanged:: 0.5 In existing documents it only saves changed fields using set / unset. Saves are cascaded and any @@ -217,6 +219,9 @@ class Document(BaseDocument): meta['cascade'] = True. Also you can pass different kwargs to the cascade save using cascade_kwargs which overwrites the existing kwargs with custom values. + .. versionchanged:: 0.8.5 + Optional save_condition that only overwrites existing documents + if the condition is satisfied in the current db record. """ signals.pre_save.send(self.__class__, document=self) @@ -230,7 +235,8 @@ class Document(BaseDocument): created = ('_id' not in doc or self._created or force_insert) - signals.pre_save_post_validation.send(self.__class__, document=self, created=created) + signals.pre_save_post_validation.send(self.__class__, document=self, + created=created) try: collection = self._get_collection() @@ -243,7 +249,12 @@ class Document(BaseDocument): object_id = doc['_id'] updates, removals = self._delta() # Need to add shard key to query, or you get an error - select_dict = {'_id': object_id} + if save_condition is not None: + select_dict = transform.query(self.__class__, + **save_condition) + else: + select_dict = {} + select_dict['_id'] = object_id shard_key = self.__class__._meta.get('shard_key', tuple()) for k in shard_key: actual_key = self._db_field_map.get(k, k) @@ -263,10 +274,12 @@ class Document(BaseDocument): if removals: update_query["$unset"] = removals if updates or removals: + upsert = save_condition is None last_error = collection.update(select_dict, update_query, - upsert=True, **write_concern) + upsert=upsert, **write_concern) created = is_new_object(last_error) + if cascade is None: cascade = self._meta.get('cascade', False) or cascade_kwargs is not None From 673a966541e8b2083616497378f54c021f5b0c08 Mon Sep 17 00:00:00 2001 From: Frank Battaglia Date: Tue, 12 Nov 2013 15:22:27 -0500 Subject: [PATCH 149/222] add tests for save_condition kwarg in document.save() --- tests/document/instance.py | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/document/instance.py b/tests/document/instance.py index 07db85a0..4747c0ed 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -820,6 +820,68 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) + def test_save_atomicity_condition(self): + + class Widget(Document): + toggle = BooleanField(default=False) + count = IntField(default=0) + save_id = UUIDField() + + def flip(widget): + widget.toggle = not widget.toggle + widget.count += 1 + + def UUID(i): + return uuid.UUID(int=i) + + Widget.drop_collection() + + w1 = Widget(toggle=False, save_id=UUID(1)) + + # ignore save_condition on new record creation + w1.save(save_condition={'save_id':UUID(42)}) + w1.reload() + self.assertFalse(w1.toggle) + self.assertEqual(w1.save_id, UUID(1)) + self.assertEqual(w1.count, 0) + + # mismatch in save_condition prevents save + flip(w1) + self.assertTrue(w1.toggle) + self.assertEqual(w1.count, 1) + w1.save(save_condition={'save_id':UUID(42)}) + w1.reload() + self.assertFalse(w1.toggle) + self.assertEqual(w1.count, 0) + + # matched save_condition allows save + flip(w1) + self.assertTrue(w1.toggle) + self.assertEqual(w1.count, 1) + w1.save(save_condition={'save_id':UUID(1)}) + w1.reload() + self.assertTrue(w1.toggle) + self.assertEqual(w1.count, 1) + + # save_condition can be used to ensure atomic read & updates + # i.e., prevent interleaved reads and writes from separate contexts + w2 = Widget.objects.get() + self.assertEqual(w1, w2) + old_id = w1.save_id + + flip(w1) + 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.reload() + self.assertFalse(w1.toggle) + self.assertEqual(w1.count, 2) + + def test_update(self): """Ensure that an existing document is updated instead of be overwritten.""" From 0a2dbbc58b5ae0df92e5465c1a43d46b71d0adb4 Mon Sep 17 00:00:00 2001 From: Frank Battaglia Date: Tue, 12 Nov 2013 16:01:56 -0500 Subject: [PATCH 150/222] add tests for mongo query operators --- tests/document/instance.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 4747c0ed..df6b4fcb 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -870,6 +870,7 @@ class InstanceTest(unittest.TestCase): old_id = w1.save_id flip(w1) + w1.save_id = UUID(2) w1.save(save_condition={'save_id':old_id}) w1.reload() self.assertFalse(w1.toggle) @@ -878,8 +879,20 @@ class InstanceTest(unittest.TestCase): flip(w2) w2.save(save_condition={'save_id':old_id}) w2.reload() - self.assertFalse(w1.toggle) - self.assertEqual(w1.count, 2) + 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.reload() + self.assertTrue(w1.toggle) + self.assertEqual(w1.count, 3) + flip(w1) + w1.save(save_condition={'count__gte':w1.count}) + w1.reload() + self.assertTrue(w1.toggle) + self.assertEqual(w1.count, 3) def test_update(self): From 86363986fc0521262676811835469efdb1530ddb Mon Sep 17 00:00:00 2001 From: Frank Battaglia Date: Tue, 12 Nov 2013 16:37:20 -0500 Subject: [PATCH 151/222] whitespace --- tests/document/instance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index df6b4fcb..acb26c6d 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -894,7 +894,6 @@ class InstanceTest(unittest.TestCase): self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) - def test_update(self): """Ensure that an existing document is updated instead of be overwritten.""" From 9d125c9e797643fbe776114407da321560ef7ca2 Mon Sep 17 00:00:00 2001 From: Frank Battaglia Date: Sun, 23 Feb 2014 19:37:42 -0500 Subject: [PATCH 152/222] inherit parent Document type _auto_id_field value --- mongoengine/base/metaclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index ff5afddf..4b2e8b9b 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -359,7 +359,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): new_class.id = field # Set primary key if not defined by the document - new_class._auto_id_field = False + new_class._auto_id_field = getattr(parent_doc_cls, + '_auto_id_field', False) if not new_class._meta.get('id_field'): new_class._auto_id_field = True new_class._meta['id_field'] = 'id' From 295ef3dc1deb1a805f8b19e3e859a381f825f119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Tue, 25 Feb 2014 15:36:30 -0300 Subject: [PATCH 153/222] db_alias support and fixes for custom map/reduce output --- mongoengine/queryset/base.py | 33 ++++++++- tests/queryset/queryset.py | 136 ++++++++++++++++++++++++++++++++++- 2 files changed, 167 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c2ad027e..4bd7128e 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -7,12 +7,14 @@ import pprint import re import warnings +from bson import SON from bson.code import Code from bson import json_util import pymongo from pymongo.common import validate_read_preference from mongoengine import signals +from mongoengine.connection import get_db from mongoengine.common import _import_class from mongoengine.base.common import get_document from mongoengine.errors import (OperationError, NotUniqueError, @@ -923,7 +925,36 @@ class BaseQuerySet(object): map_reduce_function = 'inline_map_reduce' else: map_reduce_function = 'map_reduce' - mr_args['out'] = output + + if isinstance(output, basestring): + mr_args['out'] = output + + elif isinstance(output, dict): + ordered_output = [] + + for part in ('replace', 'merge', 'reduce'): + value = output.get(part) + if value: + ordered_output.append((part, value)) + break + + else: + raise OperationError("actionData not specified for output") + + db_alias = output.get('db_alias') + remaing_args = ['db', 'sharded', 'nonAtomic'] + + if db_alias: + ordered_output.append(('db', get_db(db_alias).name)) + del remaing_args[0] + + + for part in remaing_args: + value = output.get(part) + if value: + ordered_output.append((part, value)) + + mr_args['out'] = SON(ordered_output) results = getattr(queryset._collection, map_reduce_function)( map_f, reduce_f, **mr_args) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7ff2965d..2fcd466c 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -14,7 +14,7 @@ from pymongo.read_preferences import ReadPreference from bson import ObjectId from mongoengine import * -from mongoengine.connection import get_connection +from mongoengine.connection import get_connection, get_db from mongoengine.python_support import PY3 from mongoengine.context_managers import query_counter from mongoengine.queryset import (QuerySet, QuerySetManager, @@ -1925,6 +1925,140 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + def test_map_reduce_custom_output(self): + """ + Test map/reduce custom output + """ + register_connection('test2', 'mongoenginetest2') + + class Family(Document): + id = IntField( + primary_key=True) + log = StringField() + + class Person(Document): + id = IntField( + primary_key=True) + name = StringField() + age = IntField() + family = ReferenceField(Family) + + Family.drop_collection() + Person.drop_collection() + + # creating first family + f1 = Family(id=1, log="Trav 02 de Julho") + f1.save() + + # persons of first family + Person(id=1, family=f1, name=u"Wilson Jr", age=21).save() + Person(id=2, family=f1, name=u"Wilson Father", age=45).save() + Person(id=3, family=f1, name=u"Eliana Costa", age=40).save() + Person(id=4, family=f1, name=u"Tayza Mariana", age=17).save() + + # creating second family + f2 = Family(id=2, log="Av prof frasc brunno") + f2.save() + + #persons of second family + Person(id=5, family=f2, name="Isabella Luanna", age=16).save() + Person(id=6, family=f2, name="Sandra Mara", age=36).save() + Person(id=7, family=f2, name="Igor Gabriel", age=10).save() + + # creating third family + f3 = Family(id=3, log="Av brazil") + f3.save() + + #persons of thrird family + Person(id=8, family=f3, name="Arthur WA", age=30).save() + Person(id=9, family=f3, name="Paula Leonel", age=25).save() + + # executing join map/reduce + map_person = """ + function () { + emit(this.family, { + totalAge: this.age, + persons: [{ + name: this.name, + age: this.age + }]}); + } + """ + + map_family = """ + function () { + emit(this._id, { + totalAge: 0, + persons: [] + }); + } + """ + + reduce_f = """ + function (key, values) { + var family = {persons: [], totalAge: 0}; + + values.forEach(function(value) { + if (value.persons) { + value.persons.forEach(function (person) { + family.persons.push(person); + family.totalAge += person.age; + }); + } + }); + + return family; + } + """ + cursor = Family.objects.map_reduce( + map_f=map_family, + reduce_f=reduce_f, + output={'replace': 'family_map', 'db_alias': 'test2'}) + + # start a map/reduce + cursor.next() + + results = Person.objects.map_reduce( + map_f=map_person, + reduce_f=reduce_f, + output={'reduce': 'family_map', 'db_alias': 'test2'}) + + results = list(results) + collection = get_db('test2').family_map + + self.assertEqual( + collection.find_one({'_id': 1}), { + '_id': 1, + 'value': { + 'persons': [ + {'age': 21, 'name': u'Wilson Jr'}, + {'age': 45, 'name': u'Wilson Father'}, + {'age': 40, 'name': u'Eliana Costa'}, + {'age': 17, 'name': u'Tayza Mariana'}], + 'totalAge': 123} + }) + + self.assertEqual( + collection.find_one({'_id': 2}), { + '_id': 2, + 'value': { + 'persons': [ + {'age': 16, 'name': u'Isabella Luanna'}, + {'age': 36, 'name': u'Sandra Mara'}, + {'age': 10, 'name': u'Igor Gabriel'}], + 'totalAge': 62} + }) + + self.assertEqual( + collection.find_one({'_id': 3}), { + '_id': 3, + 'value': { + 'persons': [ + {'age': 30, 'name': u'Arthur WA'}, + {'age': 25, 'name': u'Paula Leonel'}], + 'totalAge': 55} + }) + def test_map_reduce_finalize(self): """Ensure that map, reduce, and finalize run and introduce "scope" by simulating "hotness" ranking with Reddit algorithm. From ef55e6d476a89838103f70e420b6535d8186ac99 Mon Sep 17 00:00:00 2001 From: Vlad Zloteanu Date: Sat, 1 Mar 2014 17:51:59 +0100 Subject: [PATCH 154/222] fixes MongoEngine/mongoengine#589 --- mongoengine/document.py | 2 +- tests/test_signals.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 114778eb..3aaede27 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -296,9 +296,9 @@ class Document(BaseDocument): if id_field not in self._meta.get('shard_key', []): self[id_field] = self._fields[id_field].to_python(object_id) + signals.post_save.send(self.__class__, document=self, created=created) self._clear_changed_fields() self._created = False - signals.post_save.send(self.__class__, document=self, created=created) return self def cascade_save(self, *args, **kwargs): diff --git a/tests/test_signals.py b/tests/test_signals.py index 50e5e6b8..3d0cbb3e 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -54,7 +54,9 @@ class SignalTests(unittest.TestCase): @classmethod def post_save(cls, sender, document, **kwargs): + dirty_keys = document._delta()[0].keys() + document._delta()[1].keys() signal_output.append('post_save signal, %s' % document) + signal_output.append('post_save dirty keys, %s' % dirty_keys) if 'created' in kwargs: if kwargs['created']: signal_output.append('Is created') @@ -203,6 +205,7 @@ class SignalTests(unittest.TestCase): "pre_save_post_validation signal, Bill Shakespeare", "Is created", "post_save signal, Bill Shakespeare", + "post_save dirty keys, ['name']", "Is created" ]) @@ -213,6 +216,7 @@ class SignalTests(unittest.TestCase): "pre_save_post_validation signal, William Shakespeare", "Is updated", "post_save signal, William Shakespeare", + "post_save dirty keys, ['name']", "Is updated" ]) From 19314e7e06c3b4b393b0017868182c689c75f9f9 Mon Sep 17 00:00:00 2001 From: Kirill Pavlov Date: Mon, 3 Mar 2014 13:09:26 +0800 Subject: [PATCH 155/222] fix docstring for DictField --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 82642cda..b0e51a24 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -760,7 +760,7 @@ class DictField(ComplexBaseField): similar to an embedded document, but the structure is not defined. .. note:: - Required means it cannot be empty - as the default for ListFields is [] + Required means it cannot be empty - as the default for DictFields is {} .. versionadded:: 0.3 .. versionchanged:: 0.5 - Can now handle complex / varying types of data From c82f4f0d45e6e70bf8410d28415602ddea6794e3 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Fri, 7 Mar 2014 13:29:29 -0800 Subject: [PATCH 156/222] clarifying the 'push' atomic update docs the first time I read this I was all like... "no duh it will remove either the first or the last, but which does it do???" --- docs/guide/querying.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 32cbb94e..7168b5e8 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -499,11 +499,13 @@ that you may use with these methods: * ``dec`` -- decrement a value by a given amount * ``push`` -- append a value to a list * ``push_all`` -- append several values to a list -* ``pop`` -- remove the first or last element of a list +* ``pop`` -- remove the first or last element of a list `depending on the value`_ * ``pull`` -- remove a value from a list * ``pull_all`` -- remove several values from a list * ``add_to_set`` -- add value to a list only if its not in the list already +.. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/ + The syntax for atomic updates is similar to the querying syntax, but the modifier comes before the field, not after it:: From d27a1103fac41fd3eb4231a89fc35044641b317a Mon Sep 17 00:00:00 2001 From: Damien Churchill Date: Wed, 12 Mar 2014 17:19:49 +0000 Subject: [PATCH 157/222] workaround a dateutil bug In the latest released version of dateutil, there's a bug whereby a TypeError can be raised whilst parsing a date. This is because it calls a method which it expects to return 2 arguments, however it can return 1 depending upon the input, which results in a TypeError: ArgType not iterable exception. Since this is equivalent to a failed parse anyway, we can treat it the same as a ValueError. --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 82642cda..85a1a743 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -391,7 +391,7 @@ class DateTimeField(BaseField): if dateutil: try: return dateutil.parser.parse(value) - except ValueError: + except (TypeError, ValueError): return None # split usecs, because they are not recognized by strptime. From 5d9ec0b20854eac476f974b495a1d444c7134bf8 Mon Sep 17 00:00:00 2001 From: Nicolas Despres Date: Mon, 17 Mar 2014 17:19:17 +0100 Subject: [PATCH 158/222] Save is called on the document not the file field. --- docs/guide/gridfs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/gridfs.rst b/docs/guide/gridfs.rst index 596585de..68e7a6d2 100644 --- a/docs/guide/gridfs.rst +++ b/docs/guide/gridfs.rst @@ -46,7 +46,7 @@ slightly different manner. First, a new file must be created by calling the marmot.photo.write('some_more_image_data') marmot.photo.close() - marmot.photo.save() + marmot.save() Deletion -------- From c1f88a4e14f319b8a849dad44849d6f6884c8e75 Mon Sep 17 00:00:00 2001 From: Falcon Dai Date: Mon, 17 Mar 2014 22:29:53 -0500 Subject: [PATCH 159/222] minor change to geo-related docs --- docs/guide/defining-documents.rst | 2 ++ mongoengine/fields.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 5d8b628a..07bce3bb 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -531,6 +531,8 @@ field name to the index definition. Sometimes its more efficient to index parts of Embedded / dictionary fields, in this case use 'dot' notation to identify the value to index eg: `rank.title` +.. _geospatial-indexes: + Geospatial indexes ------------------ diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 82642cda..48db6dfd 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1613,7 +1613,12 @@ class UUIDField(BaseField): class GeoPointField(BaseField): - """A list storing a latitude and longitude. + """A list storing a longitude and latitude coordinate. + + .. note:: this represents a generic point in a 2D plane and a legacy way of + representing a geo point. It admits 2d indexes but not "2dsphere" indexes + in MongoDB > 2.4 which are more natural for modeling geospatial points. + See :ref:`geospatial-indexes` .. versionadded:: 0.4 """ @@ -1635,7 +1640,7 @@ class GeoPointField(BaseField): class PointField(GeoJsonBaseField): - """A geo json field storing a latitude and longitude. + """A GeoJSON field storing a longitude and latitude coordinate. The data is represented as: @@ -1654,7 +1659,7 @@ class PointField(GeoJsonBaseField): class LineStringField(GeoJsonBaseField): - """A geo json field storing a line of latitude and longitude coordinates. + """A GeoJSON field storing a line of longitude and latitude coordinates. The data is represented as: @@ -1672,7 +1677,7 @@ class LineStringField(GeoJsonBaseField): class PolygonField(GeoJsonBaseField): - """A geo json field storing a polygon of latitude and longitude coordinates. + """A GeoJSON field storing a polygon of longitude and latitude coordinates. The data is represented as: From 4d7b988018c2d150401fb96db36da1ebdd6a2c4d Mon Sep 17 00:00:00 2001 From: poly Date: Tue, 1 Apr 2014 19:52:21 +0800 Subject: [PATCH 160/222] Fixed uncorrectly split a query key, when it ends with "_" --- 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 e31a8b7d..27e41ad2 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -38,7 +38,7 @@ def query(_doc_cls=None, _field_operation=False, **query): mongo_query.update(value) continue - parts = key.split('__') + parts = key.rsplit('__') indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()] parts = [part for part in parts if not part.isdigit()] # Check for an operator and transform to mongo-style if there is From 803caddbd40ab2ea6cbee8e6b65a5921277fcafc Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Wed, 9 Apr 2014 14:25:53 +0400 Subject: [PATCH 161/222] Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError --- mongoengine/queryset/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c2ad027e..c34c1479 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -443,6 +443,8 @@ class BaseQuerySet(object): return result elif result: return result['n'] + except pymongo.errors.DuplicateKeyError, err: + raise NotUniqueError(u'Update failed (%s)' % unicode(err)) except pymongo.errors.OperationFailure, err: if unicode(err) == u'multi not coded yet': message = u'update() method requires MongoDB 1.1.3+' From b45a601ad2a208b209af5e8f45893aaf16ebd59f Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Tue, 15 Apr 2014 19:32:42 +0400 Subject: [PATCH 162/222] Test raising NotUniqueError by Document.update() --- tests/document/instance.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 07db85a0..c57102da 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -15,7 +15,7 @@ from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, from mongoengine import * from mongoengine.errors import (NotRegistered, InvalidDocumentError, - InvalidQueryError) + InvalidQueryError, NotUniqueError) from mongoengine.queryset import NULLIFY, Q from mongoengine.connection import get_db from mongoengine.base import get_document @@ -990,6 +990,16 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidQueryError, update_no_op_raises) + def test_update_unique_field(self): + class Doc(Document): + name = StringField(unique=True) + + doc1 = Doc(name="first").save() + doc2 = Doc(name="second").save() + + self.assertRaises(NotUniqueError, lambda: + doc2.update(set__name=doc1.name)) + def test_embedded_update(self): """ Test update on `EmbeddedDocumentField` fields From 12809ebc741dc04677a4e3b0f7c3ed5c23563d7f Mon Sep 17 00:00:00 2001 From: Jatin Chopra Date: Tue, 6 May 2014 00:25:55 -0700 Subject: [PATCH 163/222] Updated Jatin's name and github name --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index d6994d50..5fa0cb3d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -171,7 +171,7 @@ that much better: * Michael Bartnett (https://github.com/michaelbartnett) * Alon Horev (https://github.com/alonho) * Kelvin Hammond (https://github.com/kelvinhammond) - * Jatin- (https://github.com/jatin-) + * Jatin Chopra (https://github.com/jatin) * Paul Uithol (https://github.com/PaulUithol) * Thom Knowles (https://github.com/fleat) * Paul (https://github.com/squamous) From babbc8bcd64893a328f34d1cde17dd7125b6dd2f Mon Sep 17 00:00:00 2001 From: Ronald van Rij Date: Mon, 5 May 2014 16:39:55 +0200 Subject: [PATCH 164/222] When using autogenerated document ids in a sharded collection, do set that id back into the Document --- mongoengine/document.py | 2 +- tests/document/instance.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 114778eb..e09503c8 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -293,7 +293,7 @@ class Document(BaseDocument): raise NotUniqueError(message % unicode(err)) raise OperationError(message % unicode(err)) id_field = self._meta['id_field'] - if id_field not in self._meta.get('shard_key', []): + if created or id_field not in self._meta.get('shard_key', []): self[id_field] = self._fields[id_field].to_python(object_id) self._clear_changed_fields() diff --git a/tests/document/instance.py b/tests/document/instance.py index 07db85a0..14cc0074 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2281,6 +2281,8 @@ class InstanceTest(unittest.TestCase): log.machine = "Localhost" log.save() + self.assertIsNotNone(log.id) + log.log = "Saving" log.save() @@ -2304,6 +2306,8 @@ class InstanceTest(unittest.TestCase): log.machine = "Localhost" log.save() + self.assertIsNotNone(log.id) + log.log = "Saving" log.save() From 9544b7d9689bb2ef3672aa230b3829400be16414 Mon Sep 17 00:00:00 2001 From: Ronald van Rij Date: Fri, 9 May 2014 14:33:18 +0200 Subject: [PATCH 165/222] Fixed unit test which used assertIsNotNone --- tests/document/instance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 14cc0074..7db452d9 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2281,7 +2281,7 @@ class InstanceTest(unittest.TestCase): log.machine = "Localhost" log.save() - self.assertIsNotNone(log.id) + self.assertTrue(log.id is not None) log.log = "Saving" log.save() @@ -2306,7 +2306,7 @@ class InstanceTest(unittest.TestCase): log.machine = "Localhost" log.save() - self.assertIsNotNone(log.id) + self.assertTrue(log.id is not None) log.log = "Saving" log.save() From abcacc82f359d37922dc6c19c6b860b4c633e8e0 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 21 May 2014 22:21:46 -0700 Subject: [PATCH 166/222] dont use a system collection --- tests/document/instance.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 07db85a0..6c4da162 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2411,7 +2411,7 @@ class InstanceTest(unittest.TestCase): for parameter_name, parameter in self.parameters.iteritems(): parameter.expand() - class System(Document): + class NodesSystem(Document): name = StringField(required=True) nodes = MapField(ReferenceField(Node, dbref=False)) @@ -2419,18 +2419,18 @@ class InstanceTest(unittest.TestCase): for node_name, node in self.nodes.iteritems(): node.expand() node.save(*args, **kwargs) - super(System, self).save(*args, **kwargs) + super(NodesSystem, self).save(*args, **kwargs) - System.drop_collection() + NodesSystem.drop_collection() Node.drop_collection() - system = System(name="system") + system = NodesSystem(name="system") system.nodes["node"] = Node() system.save() system.nodes["node"].parameters["param"] = Parameter() system.save() - system = System.objects.first() + system = NodesSystem.objects.first() self.assertEqual("UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) def test_embedded_document_equality(self): From 3faf3c84be55f709d828ea7837db988c56b0239e Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Tue, 27 May 2014 16:33:38 -0300 Subject: [PATCH 167/222] Avoid to open all documents from cursors in an if stmt Using a cursos in an if statement: cursor = Collection.objects if cursor: (...) Will open all documents, because there are not an __nonzero__ method. This change check only one document (if present) and returns True or False. --- mongoengine/queryset/base.py | 9 +++++++++ tests/queryset/queryset.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c2ad027e..823bc164 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -154,6 +154,15 @@ class BaseQuerySet(object): def __iter__(self): raise NotImplementedError + def __nonzero__(self): + """ Avoid to open all records in an if stmt """ + + for value in self: + self.rewind() + return True + + return False + # Core functions def all(self): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7ff2965d..f274e0ee 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3814,6 +3814,29 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Example.objects(size=instance_size).count(), 1) self.assertEqual(Example.objects(size__in=[instance_size]).count(), 1) + def test_cursor_in_an_if_stmt(self): + + class Test(Document): + test_field = StringField() + + Test.drop_collection() + queryset = Test.objects + + if queryset: + raise AssertionError('Empty cursor returns True') + + test = Test() + test.test_field = 'test' + test.save() + + queryset = Test.objects + if not test: + raise AssertionError('There is data, but cursor returned False') + + queryset.next() + if queryset: + raise AssertionError('There is no data left in cursor') + if __name__ == '__main__': unittest.main() From 9ba657797ed64d50a876c95bc2a243bed3037e19 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 08:33:22 -0300 Subject: [PATCH 168/222] Authors updated according guideline --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d6994d50..5d6a45fa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -189,3 +189,4 @@ that much better: * Tom (https://github.com/tomprimozic) * j0hnsmith (https://github.com/j0hnsmith) * Damien Churchill (https://github.com/damoxc) + * Jonathan Simon Prates (https://github.com/jonathansp) \ No newline at end of file From 47f0de9836a7e340047c6639dee9ade29b9755cb Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 08:36:57 -0300 Subject: [PATCH 169/222] Py3 fix --- mongoengine/queryset/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 823bc164..022b7c1f 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -161,7 +161,12 @@ class BaseQuerySet(object): self.rewind() return True - return False + return False + + def __bool__(self): + """ Same behaviour in Py3 """ + + return self.__nonzero__(): # Core functions From edcdfeb0573f253227a196b3080344e849f48109 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 09:03:12 -0300 Subject: [PATCH 170/222] Fix syntax error --- 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 022b7c1f..0dfff7cc 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -166,7 +166,7 @@ class BaseQuerySet(object): def __bool__(self): """ Same behaviour in Py3 """ - return self.__nonzero__(): + return self.__nonzero__() # Core functions From dfdecef8e72fdd0379cdec4452f3175f0826b892 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 09:40:22 -0300 Subject: [PATCH 171/222] Fix py2 and py3 --- mongoengine/queryset/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 0dfff7cc..09fb5bf6 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -154,7 +154,7 @@ class BaseQuerySet(object): def __iter__(self): raise NotImplementedError - def __nonzero__(self): + def _bool(self): """ Avoid to open all records in an if stmt """ for value in self: @@ -163,10 +163,15 @@ class BaseQuerySet(object): return False + def __nonzero__(self): + """ Same behaviour in Py2 """ + + return self._bool() + def __bool__(self): """ Same behaviour in Py3 """ - return self.__nonzero__() + return self._bool() # Core functions From ee0c7fd8bfa08ec885f379b2aa6dd72f48d8d7c5 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 13:21:00 -0300 Subject: [PATCH 172/222] Change for loop to self.first() --- mongoengine/queryset/base.py | 6 +----- tests/queryset/queryset.py | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 09fb5bf6..099831fe 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -157,11 +157,7 @@ class BaseQuerySet(object): def _bool(self): """ Avoid to open all records in an if stmt """ - for value in self: - self.rewind() - return True - - return False + return False if self.first() is None else True def __nonzero__(self): """ Same behaviour in Py2 """ diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index f274e0ee..f6adaf39 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3834,8 +3834,8 @@ class QuerySetTest(unittest.TestCase): raise AssertionError('There is data, but cursor returned False') queryset.next() - if queryset: - raise AssertionError('There is no data left in cursor') + if not queryset: + raise AssertionError('There is data, cursor must return True') if __name__ == '__main__': From 30964f65e465f7e29960cd49caf29d5c9d4ac756 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 17:06:15 -0300 Subject: [PATCH 173/222] Remove orderby in if stmt --- mongoengine/queryset/base.py | 8 +++++- tests/queryset/queryset.py | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 099831fe..fe1285c6 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -157,7 +157,13 @@ class BaseQuerySet(object): def _bool(self): """ Avoid to open all records in an if stmt """ - return False if self.first() is None else True + queryset = self.clone() + queryset._ordering = [] + try: + queryset[0] + return True + except IndexError: + return False def __nonzero__(self): """ Same behaviour in Py2 """ diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index f6adaf39..65f7255b 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3837,6 +3837,59 @@ class QuerySetTest(unittest.TestCase): if not queryset: raise AssertionError('There is data, cursor must return True') + def test_bool_performance(self): + + class Person(Document): + name = StringField() + + Person.drop_collection() + for i in xrange(100): + Person(name="No: %s" % i).save() + + with query_counter() as q: + if Person.objects: + pass + + self.assertEqual(q, 1) + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertEqual(op['nreturned'], 1) + + + def test_bool_with_ordering(self): + + class Person(Document): + name = StringField() + + Person.drop_collection() + Person(name="Test").save() + + qs = Person.objects.order_by('name') + + with query_counter() as q: + + if qs: + pass + + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertFalse('$orderby' in op['query'], + 'BaseQuerySet cannot use orderby in if stmt') + + with query_counter() as p: + + for x in qs: + pass + + op = p.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + + self.assertTrue('$orderby' in op['query'], + 'BaseQuerySet cannot remove orderby in for loop') + if __name__ == '__main__': unittest.main() From 39735594bd935f3003d85009c3dc14a8d3d71f23 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 17:15:48 -0300 Subject: [PATCH 174/222] Removed blank line --- tests/queryset/queryset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 65f7255b..c5fea003 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3886,7 +3886,6 @@ class QuerySetTest(unittest.TestCase): op = p.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertTrue('$orderby' in op['query'], 'BaseQuerySet cannot remove orderby in for loop') From c87801f0a911bd02f4b4ae910751c8b04717fd02 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Wed, 28 May 2014 17:26:28 -0300 Subject: [PATCH 175/222] Using first() from cloned queryset --- mongoengine/queryset/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index fe1285c6..85d2dc3f 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -159,11 +159,7 @@ class BaseQuerySet(object): queryset = self.clone() queryset._ordering = [] - try: - queryset[0] - return True - except IndexError: - return False + return False if queryset.first() is None else True def __nonzero__(self): """ Same behaviour in Py2 """ From c744104a18829f7f34d491a580914b89ecf3c620 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Thu, 29 May 2014 10:53:20 -0300 Subject: [PATCH 176/222] Added test with meta --- tests/queryset/queryset.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c5fea003..bad0d1e5 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3889,6 +3889,33 @@ class QuerySetTest(unittest.TestCase): self.assertTrue('$orderby' in op['query'], 'BaseQuerySet cannot remove orderby in for loop') + def test_bool_with_ordering_from_meta_dict(self): + + class Person(Document): + name = StringField() + meta = { + 'ordering': ['name'] + } + + Person.drop_collection() + + Person(name="B").save() + Person(name="C").save() + Person(name="A").save() + + with query_counter() as q: + + if Person.objects: + pass + + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertTrue('$orderby' in op['query'], + 'BaseQuerySet cannot remove orderby from meta in boolen test') + + self.assertEqual(Person.objects.first().name, 'A') + if __name__ == '__main__': unittest.main() From 819ff2a90286030740e2d6d886c2f14e29d0ccc2 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Thu, 29 May 2014 14:36:30 -0300 Subject: [PATCH 177/222] Renamed to has_data() --- AUTHORS | 3 ++- mongoengine/queryset/base.py | 12 ++++++------ tests/queryset/queryset.py | 8 ++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5d6a45fa..7b466d6b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -189,4 +189,5 @@ that much better: * Tom (https://github.com/tomprimozic) * j0hnsmith (https://github.com/j0hnsmith) * Damien Churchill (https://github.com/damoxc) - * Jonathan Simon Prates (https://github.com/jonathansp) \ No newline at end of file + * Jonathan Simon Prates (https://github.com/jonathansp) + * Thiago Papageorgiou (https://github.com/tmpapageorgiou) \ No newline at end of file diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 85d2dc3f..ef8cd2a7 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -154,22 +154,22 @@ class BaseQuerySet(object): def __iter__(self): raise NotImplementedError - def _bool(self): - """ Avoid to open all records in an if stmt """ + def has_data(self): + """ Retrieves whether cursor has any data. """ queryset = self.clone() queryset._ordering = [] return False if queryset.first() is None else True def __nonzero__(self): - """ Same behaviour in Py2 """ + """ Avoid to open all records in an if stmt in Py2. """ - return self._bool() + return self.has_data() def __bool__(self): - """ Same behaviour in Py3 """ + """ Avoid to open all records in an if stmt in Py3. """ - return self._bool() + return self.has_data() # Core functions diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index bad0d1e5..fe932367 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3831,11 +3831,15 @@ class QuerySetTest(unittest.TestCase): queryset = Test.objects if not test: - raise AssertionError('There is data, but cursor returned False') + raise AssertionError('Cursor has data and returned False') queryset.next() if not queryset: - raise AssertionError('There is data, cursor must return True') + raise AssertionError('Cursor has data and it must returns False,' + ' even in the last item.') + + self.assertTrue(queryset.has_data(), 'Cursor has data and ' + 'returned False') def test_bool_performance(self): From 85187239b629772ba276aaee54eb678c64ad8207 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Thu, 29 May 2014 15:21:24 -0300 Subject: [PATCH 178/222] Fix tests msg --- 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 fe932367..f68468ff 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3835,7 +3835,7 @@ class QuerySetTest(unittest.TestCase): queryset.next() if not queryset: - raise AssertionError('Cursor has data and it must returns False,' + raise AssertionError('Cursor has data and it must returns True,' ' even in the last item.') self.assertTrue(queryset.has_data(), 'Cursor has data and ' From 1eacc6fbff0bbe14a18d62032fe0b616194c8d26 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Fri, 30 May 2014 15:08:03 -0700 Subject: [PATCH 179/222] clear ordering via empty order_by --- mongoengine/queryset/base.py | 7 ++-- tests/queryset/queryset.py | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c2ad027e..4f451667 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -50,7 +50,7 @@ class BaseQuerySet(object): self._initial_query = {} self._where_clause = None self._loaded_fields = QueryFieldList() - self._ordering = [] + self._ordering = None self._snapshot = False self._timeout = True self._class_check = True @@ -1189,8 +1189,9 @@ class BaseQuerySet(object): if self._ordering: # Apply query ordering self._cursor_obj.sort(self._ordering) - elif self._document._meta['ordering']: - # Otherwise, apply the ordering from the document model + elif self._ordering is None and self._document._meta['ordering']: + # Otherwise, apply the ordering from the document model, unless + # it's been explicitly cleared via order_by with no arguments order = self._get_order_by(self._document._meta['ordering']) self._cursor_obj.sort(order) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7ff2965d..d706eda8 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1040,6 +1040,76 @@ class QuerySetTest(unittest.TestCase): expected = [blog_post_1, blog_post_2, blog_post_3] self.assertSequence(qs, expected) + def test_clear_ordering(self): + """ Make sure one can clear the query set ordering by applying a + consecutive order_by() + """ + + class Person(Document): + name = StringField() + + Person.drop_collection() + Person(name="A").save() + Person(name="B").save() + + qs = Person.objects.order_by('-name') + + # Make sure we can clear a previously specified ordering + with query_counter() as q: + lst = list(qs.order_by()) + + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertTrue('$orderby' not in op['query']) + self.assertEqual(lst[0].name, 'A') + + # Make sure previously specified ordering is preserved during + # consecutive calls to the same query set + with query_counter() as q: + lst = list(qs) + + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertTrue('$orderby' in op['query']) + self.assertEqual(lst[0].name, 'B') + + def test_clear_default_ordering(self): + + class Person(Document): + name = StringField() + meta = { + 'ordering': ['-name'] + } + + Person.drop_collection() + Person(name="A").save() + Person(name="B").save() + + qs = Person.objects + + # Make sure clearing default ordering works + with query_counter() as q: + lst = list(qs.order_by()) + + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertTrue('$orderby' not in op['query']) + self.assertEqual(lst[0].name, 'A') + + # Make sure default ordering is preserved during consecutive calls + # to the same query set + with query_counter() as q: + lst = list(qs) + + op = q.db.system.profile.find({"ns": + {"$ne": "%s.system.indexes" % q.db.name}})[0] + + self.assertTrue('$orderby' in op['query']) + self.assertEqual(lst[0].name, 'B') + def test_find_embedded(self): """Ensure that an embedded document is properly returned from a query. """ From 9835b382dab4ed3ceef277a2948dde103542303c Mon Sep 17 00:00:00 2001 From: Sagiv Malihi Date: Thu, 3 Apr 2014 12:38:33 +0300 Subject: [PATCH 180/222] added __slots__ to BaseDocument and Document changed the _data field to static key-value mapping instead of hash table This implements #624 --- mongoengine/base/datastructures.py | 97 ++++++++++++++++++++++++++ mongoengine/base/document.py | 46 +++++++++---- mongoengine/document.py | 7 +- tests/test_datastructures.py | 107 +++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 tests/test_datastructures.py diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 4652fb56..32a66018 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -1,4 +1,6 @@ import weakref +import functools +import itertools from mongoengine.common import _import_class __all__ = ("BaseDict", "BaseList") @@ -156,3 +158,98 @@ class BaseList(list): def _mark_as_changed(self): if hasattr(self._instance, '_mark_as_changed'): self._instance._mark_as_changed(self._name) + + +class StrictDict(object): + __slots__ = () + _special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create']) + _classes = {} + def __init__(self, **kwargs): + 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: + delattr(self, key) + 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() + + @classmethod + def create(cls, allowed_keys): + allowed_keys_tuple = tuple(('_reserved_' + k if k in cls._special_fields else k) for k in allowed_keys) + allowed_keys = frozenset(allowed_keys_tuple) + if allowed_keys not in cls._classes: + class SpecificStrictDict(cls): + __slots__ = allowed_keys_tuple + cls._classes[allowed_keys] = SpecificStrictDict + return cls._classes[allowed_keys] + + +class SemiStrictDict(StrictDict): + __slots__ = ('_extras') + _classes = {} + def __getattr__(self, attr): + try: + super(SemiStrictDict, self).__getattr__(attr) + except AttributeError: + try: + return self.__getattribute__('_extras')[attr] + except KeyError as e: + raise AttributeError(e) + def __setattr__(self, attr, value): + try: + super(SemiStrictDict, self).__setattr__(attr, value) + except AttributeError: + try: + self._extras[attr] = value + except AttributeError: + self._extras = {attr: value} + + def __delattr__(self, attr): + try: + super(SemiStrictDict, self).__delattr__(attr) + except AttributeError: + try: + del self._extras[attr] + except KeyError as e: + raise AttributeError(e) + + def __iter__(self): + try: + extras_iter = iter(self.__getattribute__('_extras')) + except AttributeError: + extras_iter = () + return itertools.chain(super(SemiStrictDict, self).__iter__(), extras_iter) + diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index cea2f09b..01809aa9 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -16,20 +16,20 @@ from mongoengine.python_support import (PY3, UNICODE_KWARGS, txt_type, to_str_keys_recursive) from mongoengine.base.common import get_document, ALLOW_INHERITANCE -from mongoengine.base.datastructures import BaseDict, BaseList +from mongoengine.base.datastructures import BaseDict, BaseList, StrictDict, SemiStrictDict from mongoengine.base.fields import ComplexBaseField __all__ = ('BaseDocument', 'NON_FIELD_ERRORS') NON_FIELD_ERRORS = '__all__' - class BaseDocument(object): + __slots__ = ('_changed_fields', '_initialised', '_created', '_data', + '_dynamic_fields', '_auto_id_field', '_db_field_map', '_cls', '__weakref__') _dynamic = False - _created = True _dynamic_lock = True - _initialised = False + STRICT = False def __init__(self, *args, **values): """ @@ -38,6 +38,8 @@ class BaseDocument(object): :param __auto_convert: Try and will cast python objects to Object types :param values: A dictionary of values for the document """ + self._initialised = False + self._created = True if args: # Combine positional arguments with named arguments. # We only want named arguments. @@ -52,8 +54,12 @@ class BaseDocument(object): values[name] = value __auto_convert = values.pop("__auto_convert", True) signals.pre_init.send(self.__class__, document=self, values=values) - - self._data = {} + + if self.STRICT and not self._dynamic: + self._data = StrictDict.create(allowed_keys=self._fields.keys())() + else: + self._data = SemiStrictDict.create(allowed_keys=self._fields.keys())() + self._dynamic_fields = SON() # Assign default values to instance @@ -129,17 +135,25 @@ class BaseDocument(object): self._data[name] = value if hasattr(self, '_changed_fields'): self._mark_as_changed(name) + try: + self__created = self._created + except AttributeError: + self__created = True - if (self._is_document and not self._created and + if (self._is_document and not self__created and name in self._meta.get('shard_key', tuple()) and self._data.get(name) != value): OperationError = _import_class('OperationError') msg = "Shard Keys are immutable. Tried to update %s" % name raise OperationError(msg) + try: + self__initialised = self._initialised + except AttributeError: + 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']): + if (self._is_document and self__initialised + and self__created and name == self._meta['id_field']): super(BaseDocument, self).__setattr__('_created', False) super(BaseDocument, self).__setattr__(name, value) @@ -157,9 +171,11 @@ class BaseDocument(object): if isinstance(data["_data"], SON): data["_data"] = self.__class__._from_son(data["_data"])._data for k in ('_changed_fields', '_initialised', '_created', '_data', - '_fields_ordered', '_dynamic_fields'): + '_dynamic_fields'): if k in data: setattr(self, k, data[k]) + if '_fields_ordered' in data: + setattr(type(self), '_fields_ordered', data['_fields_ordered']) dynamic_fields = data.get('_dynamic_fields') or SON() for k in dynamic_fields.keys(): setattr(self, k, data["_data"].get(k)) @@ -576,7 +592,9 @@ class BaseDocument(object): msg = ("Invalid data to create a `%s` instance.\n%s" % (cls._class_name, errors)) raise InvalidDocumentError(msg) - + + if cls.STRICT: + data = dict((k, v) for k,v in data.iteritems() if k in cls._fields) obj = cls(__auto_convert=False, **data) obj._changed_fields = changed_fields obj._created = False @@ -813,7 +831,11 @@ class BaseDocument(object): """Dynamically set the display value for a field with choices""" for attr_name, field in self._fields.items(): if field.choices: - setattr(self, + if self._dynamic: + obj = self + else: + obj = type(self) + setattr(obj, 'get_%s_display' % attr_name, partial(self.__get_field_display, field=field)) diff --git a/mongoengine/document.py b/mongoengine/document.py index 1bbd7b73..98e1d2a3 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -52,16 +52,17 @@ class EmbeddedDocument(BaseDocument): `_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta` dictionary. """ + + __slots__ = ('_instance') # The __metaclass__ attribute is removed by 2to3 when running with Python3 # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = DocumentMetaclass __metaclass__ = DocumentMetaclass - _instance = None - def __init__(self, *args, **kwargs): super(EmbeddedDocument, self).__init__(*args, **kwargs) + self._instance = None self._changed_fields = [] def __eq__(self, other): @@ -124,6 +125,8 @@ class Document(BaseDocument): my_metaclass = TopLevelDocumentMetaclass __metaclass__ = TopLevelDocumentMetaclass + __slots__ = ('__objects' ) + def pk(): """Primary key alias """ diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py new file mode 100644 index 00000000..c761a41e --- /dev/null +++ b/tests/test_datastructures.py @@ -0,0 +1,107 @@ +import unittest +from mongoengine.base.datastructures import StrictDict, SemiStrictDict + +class TestStrictDict(unittest.TestCase): + def strict_dict_class(self, *args, **kwargs): + return StrictDict.create(*args, **kwargs) + def setUp(self): + self.dtype = self.strict_dict_class(("a", "b", "c")) + def test_init(self): + d = self.dtype(a=1, b=1, c=1) + self.assertEqual((d.a, d.b, d.c), (1, 1, 1)) + + def test_init_fails_on_nonexisting_attrs(self): + self.assertRaises(AttributeError, lambda: self.dtype(a=1, b=2, d=3)) + + def test_eq(self): + d = self.dtype(a=1, b=1, c=1) + dd = self.dtype(a=1, b=1, c=1) + e = self.dtype(a=1, b=1, c=3) + f = self.dtype(a=1, b=1) + g = self.strict_dict_class(("a", "b", "c", "d"))(a=1, b=1, c=1, d=1) + h = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=1) + i = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=2) + + self.assertEqual(d, dd) + self.assertNotEqual(d, e) + self.assertNotEqual(d, f) + self.assertNotEqual(d, g) + self.assertNotEqual(f, d) + self.assertEqual(d, h) + self.assertNotEqual(d, i) + + def test_setattr_getattr(self): + d = self.dtype() + d.a = 1 + self.assertEqual(d.a, 1) + self.assertRaises(AttributeError, lambda: d.b) + + def test_setattr_raises_on_nonexisting_attr(self): + d = self.dtype() + def _f(): + d.x=1 + self.assertRaises(AttributeError, _f) + + def test_setattr_getattr_special(self): + d = self.strict_dict_class(["items"]) + d.items = 1 + self.assertEqual(d.items, 1) + + def test_get(self): + d = self.dtype(a=1) + self.assertEqual(d.get('a'), 1) + self.assertEqual(d.get('b', 'bla'), 'bla') + + def test_items(self): + d = self.dtype(a=1) + self.assertEqual(d.items(), [('a', 1)]) + d = self.dtype(a=1, b=2) + self.assertEqual(d.items(), [('a', 1), ('b', 2)]) + + def test_mappings_protocol(self): + d = self.dtype(a=1, b=2) + assert dict(d) == {'a': 1, 'b': 2} + assert dict(**d) == {'a': 1, 'b': 2} + + +class TestSemiSrictDict(TestStrictDict): + def strict_dict_class(self, *args, **kwargs): + return SemiStrictDict.create(*args, **kwargs) + + def test_init_fails_on_nonexisting_attrs(self): + # disable irrelevant test + pass + + def test_setattr_raises_on_nonexisting_attr(self): + # disable irrelevant test + pass + + def test_setattr_getattr_nonexisting_attr_succeeds(self): + d = self.dtype() + d.x = 1 + self.assertEqual(d.x, 1) + + def test_init_succeeds_with_nonexisting_attrs(self): + d = self.dtype(a=1, b=1, c=1, x=2) + self.assertEqual((d.a, d.b, d.c, d.x), (1, 1, 1, 2)) + + def test_iter_with_nonexisting_attrs(self): + d = self.dtype(a=1, b=1, c=1, x=2) + self.assertEqual(list(d), ['a', 'b', 'c', 'x']) + + def test_iteritems_with_nonexisting_attrs(self): + d = self.dtype(a=1, b=1, c=1, x=2) + self.assertEqual(list(d.iteritems()), [('a', 1), ('b', 1), ('c', 1), ('x', 2)]) + + def tets_cmp_with_strict_dicts(self): + d = self.dtype(a=1, b=1, c=1) + dd = StrictDict.create(("a", "b", "c"))(a=1, b=1, c=1) + self.assertEqual(d, dd) + + def test_cmp_with_strict_dict_with_nonexisting_attrs(self): + d = self.dtype(a=1, b=1, c=1, x=2) + dd = StrictDict.create(("a", "b", "c", "x"))(a=1, b=1, c=1, x=2) + self.assertEqual(d, dd) + +if __name__ == '__main__': + unittest.main() From 7bb2fe128a4b8c8b08f4d800f15229e2814bfac8 Mon Sep 17 00:00:00 2001 From: Jonathan Prates Date: Thu, 12 Jun 2014 11:08:41 -0300 Subject: [PATCH 181/222] Added PR #657 --- mongoengine/queryset/base.py | 11 +++++------ tests/queryset/queryset.py | 9 ++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 94a6e4b5..cb48f6ca 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -154,22 +154,21 @@ class BaseQuerySet(object): def __iter__(self): raise NotImplementedError - def has_data(self): + def _has_data(self): """ Retrieves whether cursor has any data. """ - queryset = self.clone() - queryset._ordering = [] + queryset = self.order_by() return False if queryset.first() is None else True def __nonzero__(self): """ Avoid to open all records in an if stmt in Py2. """ - return self.has_data() + return self._has_data() def __bool__(self): """ Avoid to open all records in an if stmt in Py3. """ - return self.has_data() + return self._has_data() # Core functions @@ -1410,7 +1409,7 @@ class BaseQuerySet(object): pass key_list.append((key, direction)) - if self._cursor_obj: + if self._cursor_obj and key_list: self._cursor_obj.sort(key_list) return key_list diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 38499df4..a2438e21 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3908,9 +3908,6 @@ class QuerySetTest(unittest.TestCase): raise AssertionError('Cursor has data and it must returns True,' ' even in the last item.') - self.assertTrue(queryset.has_data(), 'Cursor has data and ' - 'returned False') - def test_bool_performance(self): class Person(Document): @@ -3985,10 +3982,12 @@ class QuerySetTest(unittest.TestCase): op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertTrue('$orderby' in op['query'], - 'BaseQuerySet cannot remove orderby from meta in boolen test') + self.assertFalse('$orderby' in op['query'], + 'BaseQuerySet must remove orderby from meta in boolen test') self.assertEqual(Person.objects.first().name, 'A') + self.assertTrue(Person.objects._has_data(), + 'Cursor has data and returned False') if __name__ == '__main__': From 03559a3cc4b0ee3a5509c8eb2f1f54bf342284b4 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 24 Jun 2014 19:20:15 +0300 Subject: [PATCH 182/222] Added Python 3.4 to the build process. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5739909b..7ba22694 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" env: - PYMONGO=dev DJANGO=1.6 - PYMONGO=dev DJANGO=1.5.5 From bb461b009f6aa0c64c6fb2cf845a4714ee324908 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 24 Jun 2014 19:39:27 +0300 Subject: [PATCH 183/222] Travis build improvements. The latest patch version of each Django minor version is used. The build now installs existing pymongo versions. The build now actually tests against the specified Django version. Replaced PIL with Pillow. Added PyPy and Python 3.4 to the build. Rebase Log: Installing Pillow instead of PIL for testing since it's recommended and it supports PyPy. Excluding Django versions that do not work with Python 3. Improved formatting of .travis.yml. Specifying Pillow 2.0.0 and above since it's the first version that is supported in Python 3. PIL should not be installed alongside Pillow. Also, I installed some libraries that both PIL and Pillow depend on. It seems I have to be explicit on all envvars in order to exclude Django 1.4 from the build matrix. The build is now installing pymongo versions that actually exist. openjpeg has a different name on Ubuntu 12.04. Restoring libz hack. Also installing all Pillow requirements just in case. Fixed the build matrix. Acting according to @BanzaiMan's advice in travis-ci/travis-ci/#1492. --- .travis.yml | 58 +++++++++++++++++++++++++++++++++++++++++------------ setup.py | 4 ++-- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5739909b..d00dde3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,22 +6,54 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" + - "pypy" env: - - PYMONGO=dev DJANGO=1.6 - - PYMONGO=dev DJANGO=1.5.5 - - PYMONGO=dev DJANGO=1.4.10 - - PYMONGO=2.5 DJANGO=1.6 - - PYMONGO=2.5 DJANGO=1.5.5 - - PYMONGO=2.5 DJANGO=1.4.10 - - PYMONGO=3.2 DJANGO=1.6 - - PYMONGO=3.2 DJANGO=1.5.5 - - PYMONGO=3.3 DJANGO=1.6 - - PYMONGO=3.3 DJANGO=1.5.5 + - PYMONGO=dev DJANGO=1.6.5 + - PYMONGO=dev DJANGO=1.5.8 + - PYMONGO=dev DJANGO=1.4.13 + - PYMONGO=2.5.2 DJANGO=1.6.5 + - PYMONGO=2.5.2 DJANGO=1.5.8 + - PYMONGO=2.5.2 DJANGO=1.4.13 + - PYMONGO=2.6.3 DJANGO=1.6.5 + - PYMONGO=2.6.3 DJANGO=1.5.8 + - PYMONGO=2.6.3 DJANGO=1.4.13 + - PYMONGO=2.7.1 DJANGO=1.6.5 + - PYMONGO=2.7.1 DJANGO=1.5.8 + - PYMONGO=2.7.1 DJANGO=1.4.13 + +matrix: + exclude: + - python: "3.2" + env: PYMONGO=dev DJANGO=1.4.13 + - python: "3.2" + env: PYMONGO=2.5.2 DJANGO=1.4.13 + - python: "3.2" + env: PYMONGO=2.6.3 DJANGO=1.4.13 + - python: "3.2" + env: PYMONGO=2.7.1 DJANGO=1.4.13 + - python: "3.3" + env: PYMONGO=dev DJANGO=1.4.13 + - python: "3.3" + env: PYMONGO=2.5.2 DJANGO=1.4.13 + - python: "3.3" + env: PYMONGO=2.6.3 DJANGO=1.4.13 + - python: "3.3" + env: PYMONGO=2.7.1 DJANGO=1.4.13 + - python: "3.4" + env: PYMONGO=dev DJANGO=1.4.13 + - python: "3.4" + env: PYMONGO=2.5.2 DJANGO=1.4.13 + - python: "3.4" + env: PYMONGO=2.6.3 DJANGO=1.4.13 + - python: "3.4" + env: PYMONGO=2.7.1 DJANGO=1.4.13 install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi + - 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 + - cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/ - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi + - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO; true; fi + - pip install Django==$DJANGO - pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - python setup.py install script: diff --git a/setup.py b/setup.py index 85707d00..40c27ebe 100644 --- a/setup.py +++ b/setup.py @@ -51,12 +51,12 @@ CLASSIFIERS = [ 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', 'django>=1.5.1'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'Pillow>=2.0.0', 'django>=1.5.1'] 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', 'PIL', 'jinja2>=2.6', 'python-dateutil'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'Pillow>=2.0.0', 'jinja2>=2.6', 'python-dateutil'] setup(name='mongoengine', version=VERSION, From 8e852bce02b14a0149c0e189642631edc2de31fe Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 10:58:00 +0300 Subject: [PATCH 184/222] Pillow provides a more descriptive error message, therefor the build failure. --- tests/fields/file_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 902b1512..7ae53e8a 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -279,7 +279,7 @@ class FileTest(unittest.TestCase): t.image.put(f) self.fail("Should have raised an invalidation error") except ValidationError, e: - self.assertEqual("%s" % e, "Invalid image: cannot identify image file") + self.assertEqual("%s" % e, "Invalid image: cannot identify image file %s" % f) t = TestImage() t.image.put(open(TEST_IMAGE_PATH, 'rb')) From adbbc656d41dbc7ecd33ae933c164efe3fd89316 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 11:12:40 +0300 Subject: [PATCH 185/222] Removing zlib hack since only PIL needs it. The build should pass without it. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d00dde3a..1265ba70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,6 @@ matrix: env: PYMONGO=2.7.1 DJANGO=1.4.13 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 - - cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/ - if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi - if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO; true; fi - pip install Django==$DJANGO From 8adf1cdd0212e18d26220d7e5997c3534c92a017 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 11:18:35 +0300 Subject: [PATCH 186/222] Fast finish the build if there are failures since we have a very large build matrix and each build takes a very long time. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1265ba70..4079855d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ env: - PYMONGO=2.7.1 DJANGO=1.4.13 matrix: + fast_finish: true exclude: - python: "3.2" env: PYMONGO=dev DJANGO=1.4.13 From fc3eda55c72e99f0b80bb37bf917099780c2f79e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 11:32:41 +0300 Subject: [PATCH 187/222] Added a note about optional dependencies to the README file. --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cc4524ae..8c3ee26e 100644 --- a/README.rst +++ b/README.rst @@ -29,9 +29,18 @@ setup.py install``. Dependencies ============ -- pymongo 2.5+ +- pymongo>=2.5 - sphinx (optional - for documentation generation) +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) +- 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 + Examples ======== Some simple examples of what MongoEngine code looks like:: From fe2ef4e61c4bd44e5156542728b7755f30ef8148 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 11:39:08 +0300 Subject: [PATCH 188/222] Made the benchmark script compatitable with Python 3 and ensured it runs on every build. --- .travis.yml | 1 + benchmark.py | 41 +++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4079855d..fff35ca9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,7 @@ install: - python setup.py install script: - python setup.py test + - python benchmark.py notifications: irc: "irc.freenode.org#mongoengine" branches: diff --git a/benchmark.py b/benchmark.py index 16b2fd47..cb5e19d8 100644 --- a/benchmark.py +++ b/benchmark.py @@ -113,6 +113,7 @@ def main(): 4.68946313858 ---------------------------------------------------------------------------------------------------- """ + print("Benchmarking...") setup = """ from pymongo import MongoClient @@ -138,10 +139,10 @@ myNoddys = noddy.find() [n for n in myNoddys] # iterate """ - print "-" * 100 + print("-" * 100) print """Creating 10000 dictionaries - Pymongo""" t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) stmt = """ from pymongo import MongoClient @@ -161,10 +162,10 @@ myNoddys = noddy.find() [n for n in myNoddys] # iterate """ - print "-" * 100 + print("-" * 100) print """Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""" t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) setup = """ from pymongo import MongoClient @@ -190,10 +191,10 @@ myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ - print "-" * 100 - print """Creating 10000 dictionaries - MongoEngine""" + print("-" * 100) + print("""Creating 10000 dictionaries - MongoEngine""") t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) stmt = """ for i in xrange(10000): @@ -208,10 +209,10 @@ myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ - print "-" * 100 + print("-" * 100) print """Creating 10000 dictionaries without continual assign - MongoEngine""" t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) stmt = """ for i in xrange(10000): @@ -224,10 +225,10 @@ myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ - print "-" * 100 + print("-" * 100) print """Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""" t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) stmt = """ for i in xrange(10000): @@ -240,10 +241,10 @@ myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ - print "-" * 100 + print("-" * 100) print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""" t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) stmt = """ for i in xrange(10000): @@ -256,10 +257,10 @@ myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ - print "-" * 100 - print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""" + print("-" * 100) + print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""") t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) stmt = """ for i in xrange(10000): @@ -272,11 +273,11 @@ myNoddys = Noddy.objects() [n for n in myNoddys] # iterate """ - print "-" * 100 - print """Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""" + print("-" * 100) + print("""Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""") t = timeit.Timer(stmt=stmt, setup=setup) - print t.timeit(1) + print(t.timeit(1)) if __name__ == "__main__": - main() \ No newline at end of file + main() From f44c8f120532f407b5eea6fed480183c8aa51758 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 13:11:32 +0300 Subject: [PATCH 189/222] Skipping a test that does not work on PyPy due to a PyPy bug/feature. --- tests/queryset/queryset.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7ff2965d..33f7dd91 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3586,7 +3586,13 @@ class QuerySetTest(unittest.TestCase): [x for x in people] self.assertEqual(100, len(people._result_cache)) - self.assertEqual(None, people._len) + + import platform + + if platform.python_implementation() != "PyPy": + # PyPy evaluates __len__ when iterating with list comprehensions while CPython does not. + # This may be a bug in PyPy (PyPy/#1802) but it does not affect the behavior of MongoEngine. + self.assertEqual(None, people._len) self.assertEqual(q, 1) list(people) From 1914032e35ef917ea95efc192b8280858ff78c82 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 14:20:54 +0300 Subject: [PATCH 190/222] Missed some of the print statements in the benchmarks script. --- benchmark.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark.py b/benchmark.py index cb5e19d8..5613eaf8 100644 --- a/benchmark.py +++ b/benchmark.py @@ -210,7 +210,7 @@ myNoddys = Noddy.objects() """ print("-" * 100) - print """Creating 10000 dictionaries without continual assign - MongoEngine""" + print("""Creating 10000 dictionaries without continual assign - MongoEngine""") t = timeit.Timer(stmt=stmt, setup=setup) print(t.timeit(1)) @@ -242,7 +242,7 @@ myNoddys = Noddy.objects() """ print("-" * 100) - print """Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""" + print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""") t = timeit.Timer(stmt=stmt, setup=setup) print(t.timeit(1)) From 7f7745071af87957cdc5705b409b96ed49a1e481 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 15:47:54 +0300 Subject: [PATCH 191/222] Found more print statements that were not turned into function calls. --- benchmark.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmark.py b/benchmark.py index 5613eaf8..d33e58db 100644 --- a/benchmark.py +++ b/benchmark.py @@ -140,7 +140,7 @@ myNoddys = noddy.find() """ print("-" * 100) - print """Creating 10000 dictionaries - Pymongo""" + print("""Creating 10000 dictionaries - Pymongo""") t = timeit.Timer(stmt=stmt, setup=setup) print(t.timeit(1)) @@ -163,7 +163,7 @@ myNoddys = noddy.find() """ print("-" * 100) - print """Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""" + print("""Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""") t = timeit.Timer(stmt=stmt, setup=setup) print(t.timeit(1)) @@ -226,7 +226,7 @@ myNoddys = Noddy.objects() """ print("-" * 100) - print """Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""" + print("""Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""") t = timeit.Timer(stmt=stmt, setup=setup) print(t.timeit(1)) From 29309dac9a926077962fc0778e5b2fbaf1d29cc2 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 16:53:24 +0300 Subject: [PATCH 192/222] Mongo clients with the same settings should be shared since they manage a connection pool. Also, I removed old code that was supposed to support Pymongo<2.1 which we don't support anymore. --- mongoengine/connection.py | 33 ++++++++++++++++++--------------- tests/test_connection.py | 11 +++++++++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 7cc626f4..d3efac62 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -93,20 +93,11 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): raise ConnectionError(msg) conn_settings = _connection_settings[alias].copy() - if hasattr(pymongo, 'version_tuple'): # Support for 2.1+ - conn_settings.pop('name', None) - conn_settings.pop('slaves', None) - conn_settings.pop('is_slave', None) - conn_settings.pop('username', None) - conn_settings.pop('password', None) - else: - # Get all the slave connections - if 'slaves' in conn_settings: - slaves = [] - for slave_alias in conn_settings['slaves']: - slaves.append(get_connection(slave_alias)) - conn_settings['slaves'] = slaves - conn_settings.pop('read_preference', None) + conn_settings.pop('name', None) + conn_settings.pop('slaves', None) + conn_settings.pop('is_slave', None) + conn_settings.pop('username', None) + conn_settings.pop('password', None) connection_class = MongoClient if 'replicaSet' in conn_settings: @@ -119,7 +110,19 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): connection_class = MongoReplicaSetClient try: - _connections[alias] = connection_class(**conn_settings) + connection = None + connection_settings_iterator = ((alias, settings.copy()) for alias, settings in _connection_settings.iteritems()) + for alias, connection_settings in connection_settings_iterator: + connection_settings.pop('name', None) + connection_settings.pop('slaves', None) + connection_settings.pop('is_slave', None) + connection_settings.pop('username', None) + connection_settings.pop('password', None) + if conn_settings == connection_settings and _connections.get(alias, None): + connection = _connections[alias] + break + + _connections[alias] = connection if connection else connection_class(**conn_settings) except Exception, e: raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e)) return _connections[alias] diff --git a/tests/test_connection.py b/tests/test_connection.py index 96135bc5..bf615ceb 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -34,6 +34,17 @@ class ConnectionTest(unittest.TestCase): conn = get_connection('testdb') self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + def test_sharing_connections(self): + """Ensure that connections are shared when the connection settings are exactly the same + """ + connect('mongoenginetest', alias='testdb1') + + expected_connection = get_connection('testdb1') + + connect('mongoenginetest', alias='testdb2') + actual_connection = get_connection('testdb2') + self.assertIs(expected_connection, actual_connection) + def test_connect_uri(self): """Ensure that the connect() method works properly with uri's """ From b8d568761e0e3130ce37d5675f8b218b11b88e29 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 17:24:52 +0300 Subject: [PATCH 193/222] Getting rid of xrange since it's not in Python 3 and does not affect the benchmark. --- benchmark.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/benchmark.py b/benchmark.py index d33e58db..53ecf32c 100644 --- a/benchmark.py +++ b/benchmark.py @@ -15,7 +15,7 @@ def cprofile_main(): class Noddy(Document): fields = DictField() - for i in xrange(1): + for i in range(1): noddy = Noddy() for j in range(20): noddy.fields["key" + str(j)] = "value " + str(j) @@ -128,7 +128,7 @@ connection = MongoClient() db = connection.timeit_test noddy = db.noddy -for i in xrange(10000): +for i in range(10000): example = {'fields': {}} for j in range(20): example['fields']["key"+str(j)] = "value "+str(j) @@ -151,7 +151,7 @@ connection = MongoClient() db = connection.timeit_test noddy = db.noddy -for i in xrange(10000): +for i in range(10000): example = {'fields': {}} for j in range(20): example['fields']["key"+str(j)] = "value "+str(j) @@ -181,7 +181,7 @@ class Noddy(Document): """ stmt = """ -for i in xrange(10000): +for i in range(10000): noddy = Noddy() for j in range(20): noddy.fields["key"+str(j)] = "value "+str(j) @@ -197,7 +197,7 @@ myNoddys = Noddy.objects() print(t.timeit(1)) stmt = """ -for i in xrange(10000): +for i in range(10000): noddy = Noddy() fields = {} for j in range(20): @@ -215,7 +215,7 @@ myNoddys = Noddy.objects() print(t.timeit(1)) stmt = """ -for i in xrange(10000): +for i in range(10000): noddy = Noddy() for j in range(20): noddy.fields["key"+str(j)] = "value "+str(j) @@ -231,7 +231,7 @@ myNoddys = Noddy.objects() print(t.timeit(1)) stmt = """ -for i in xrange(10000): +for i in range(10000): noddy = Noddy() for j in range(20): noddy.fields["key"+str(j)] = "value "+str(j) @@ -247,7 +247,7 @@ myNoddys = Noddy.objects() print(t.timeit(1)) stmt = """ -for i in xrange(10000): +for i in range(10000): noddy = Noddy() for j in range(20): noddy.fields["key"+str(j)] = "value "+str(j) @@ -263,7 +263,7 @@ myNoddys = Noddy.objects() print(t.timeit(1)) stmt = """ -for i in xrange(10000): +for i in range(10000): noddy = Noddy() for j in range(20): noddy.fields["key"+str(j)] = "value "+str(j) From 5ae588833b7ba1498a95e2efda4f788d4297997b Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 Jun 2014 18:22:39 +0300 Subject: [PATCH 194/222] Allowed to switch databases for a specific query. --- mongoengine/context_managers.py | 7 ------- mongoengine/queryset/base.py | 24 ++++++++++++++++++++---- mongoengine/queryset/queryset.py | 7 +++++++ tests/queryset/queryset.py | 16 ++++++++++++++++ 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index 13ed1009..cc860066 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -1,6 +1,5 @@ from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db -from mongoengine.queryset import QuerySet __all__ = ("switch_db", "switch_collection", "no_dereference", @@ -162,12 +161,6 @@ class no_sub_classes(object): return self.cls -class QuerySetNoDeRef(QuerySet): - """Special no_dereference QuerySet""" - def __dereference(items, max_depth=1, instance=None, name=None): - return items - - class query_counter(object): """ Query_counter context manager to get the number of queries. """ diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index c2ad027e..a8d204b3 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -13,11 +13,11 @@ import pymongo from pymongo.common import validate_read_preference from mongoengine import signals +from mongoengine.context_managers import switch_db from mongoengine.common import _import_class from mongoengine.base.common import get_document from mongoengine.errors import (OperationError, NotUniqueError, InvalidQueryError, LookUpError) - from mongoengine.queryset import transform from mongoengine.queryset.field_list import QueryFieldList from mongoengine.queryset.visitor import Q, QNode @@ -389,7 +389,7 @@ class BaseQuerySet(object): ref_q = document_cls.objects(**{field_name + '__in': self}) ref_q_count = ref_q.count() if (doc != document_cls and ref_q_count > 0 - or (doc == document_cls and ref_q_count > 0)): + or (doc == document_cls and ref_q_count > 0)): ref_q.delete(write_concern=write_concern) elif rule == NULLIFY: document_cls.objects(**{field_name + '__in': self}).update( @@ -522,6 +522,19 @@ class BaseQuerySet(object): return self + def using(self, alias): + """This method is for controlling which database the QuerySet will be evaluated against if you are using more than one database. + + :param alias: The database alias + + .. versionadded:: 0.8 + """ + + with switch_db(self._document, alias) as cls: + collection = cls._get_collection() + + return self.clone_into(self.__class__(self._document, collection)) + def clone(self): """Creates a copy of the current :class:`~mongoengine.queryset.QuerySet` @@ -926,7 +939,7 @@ class BaseQuerySet(object): mr_args['out'] = output results = getattr(queryset._collection, map_reduce_function)( - map_f, reduce_f, **mr_args) + map_f, reduce_f, **mr_args) if map_reduce_function == 'map_reduce': results = results.find() @@ -1362,7 +1375,7 @@ class BaseQuerySet(object): for subdoc in subclasses: try: subfield = ".".join(f.db_field for f in - subdoc._lookup_field(field.split('.'))) + subdoc._lookup_field(field.split('.'))) ret.append(subfield) found = True break @@ -1450,6 +1463,7 @@ class BaseQuerySet(object): # type of this field and use the corresponding # .to_python(...) from mongoengine.fields import EmbeddedDocumentField + obj = self._document for chunk in path.split('.'): obj = getattr(obj, chunk, None) @@ -1460,6 +1474,7 @@ class BaseQuerySet(object): if obj and data is not None: data = obj.to_python(data) return data + return clean(row) def _sub_js_fields(self, code): @@ -1468,6 +1483,7 @@ class BaseQuerySet(object): substituted for the MongoDB name of the field (specified using the :attr:`name` keyword argument in a field's constructor). """ + def field_sub(match): # Extract just the field name, and look up the field objects field_name = match.group(1).split('.') diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 1437e76b..cebfcc50 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -155,3 +155,10 @@ class QuerySetNoCache(BaseQuerySet): queryset = self.clone() queryset.rewind() return queryset + + +class QuerySetNoDeRef(QuerySet): + """Special no_dereference QuerySet""" + + def __dereference(items, max_depth=1, instance=None, name=None): + return items \ No newline at end of file diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7ff2965d..25ede817 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -29,6 +29,7 @@ class QuerySetTest(unittest.TestCase): def setUp(self): connect(db='mongoenginetest') + connect(db='mongoenginetest2', alias='test2') class PersonMeta(EmbeddedDocument): weight = IntField() @@ -2957,6 +2958,21 @@ class QuerySetTest(unittest.TestCase): Number.drop_collection() + def test_using(self): + """Ensure that switching databases for a queryset is possible + """ + class Number2(Document): + n = IntField() + + Number2.drop_collection() + + for i in xrange(1, 10): + t = Number2(n=i) + t.switch_db('test2') + t.save() + + self.assertEqual(len(Number2.objects.using('test2')), 9) + def test_unset_reference(self): class Comment(Document): text = StringField() From 67a65a2aa9ecd77de372511aa54541100923d3d3 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 26 Jun 2014 11:17:57 +0300 Subject: [PATCH 195/222] Installing unittest2 on Python 2.6. --- setup.py | 3 +++ tests/test_connection.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85707d00..f8f0d6da 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,9 @@ if sys.version_info[0] == 3: else: extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'PIL', 'jinja2>=2.6', 'python-dateutil'] + if sys.version_info[0] == 2 and sys.version_info[1] == 6: + extra_opts['tests_require'].append('unittest2') + setup(name='mongoengine', version=VERSION, author='Harry Marr', diff --git a/tests/test_connection.py b/tests/test_connection.py index bf615ceb..a5b1b089 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,6 +1,11 @@ import sys sys.path[0:0] = [""] -import unittest + +try: + import unittest2 as unittest +except ImportError: + import unittest + import datetime import pymongo From cae91ce0c564089b418d0fd508d80c7f923df949 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 26 Jun 2014 12:31:07 +0300 Subject: [PATCH 196/222] Convert codebase to Python 3 using 2to3 before running benchmarks. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fff35ca9..6281e6b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,7 @@ install: - python setup.py install script: - python setup.py test + - 2to3 . -w - python benchmark.py notifications: irc: "irc.freenode.org#mongoengine" From 0e7878b406bc23b46140edd54912e4491595fac4 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 26 Jun 2014 12:41:26 +0300 Subject: [PATCH 197/222] Only run 2to3 on Python 3.x. Makes sense no? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6281e6b4..5f10339b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,7 +58,7 @@ install: - python setup.py install script: - python setup.py test - - 2to3 . -w + - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then 2to3 . -w; fi; - python benchmark.py notifications: irc: "irc.freenode.org#mongoengine" From b02a31d4b920981f6b7330de23e78518f334cc0d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 14:44:44 +0100 Subject: [PATCH 198/222] Updated .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5739909b..c7397fd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,7 @@ python: - "3.3" env: - PYMONGO=dev DJANGO=1.6 - - PYMONGO=dev DJANGO=1.5.5 - - PYMONGO=dev DJANGO=1.4.10 + - PYMONGO=dev DJANGO=1.5.8 - PYMONGO=2.5 DJANGO=1.6 - PYMONGO=2.5 DJANGO=1.5.5 - PYMONGO=2.5 DJANGO=1.4.10 @@ -31,3 +30,4 @@ notifications: branches: only: - master + - "0.8" From dd51589f67fd02f3049aadea5268a79cd3799092 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 16:02:40 +0100 Subject: [PATCH 199/222] Updates --- setup.py | 2 +- tests/document/instance.py | 4 ++-- tests/queryset/queryset.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 85707d00..25075275 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup(name='mongoengine', long_description=LONG_DESCRIPTION, platforms=['any'], classifiers=CLASSIFIERS, - install_requires=['pymongo>=2.5'], + install_requires=['pymongo>=2.7'], test_suite='nose.collector', **extra_opts ) diff --git a/tests/document/instance.py b/tests/document/instance.py index afb27e0d..6f04ac1d 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -57,7 +57,7 @@ class InstanceTest(unittest.TestCase): date = DateTimeField(default=datetime.now) meta = { 'max_documents': 10, - 'max_size': 90000, + 'max_size': 4096, } Log.drop_collection() @@ -75,7 +75,7 @@ class InstanceTest(unittest.TestCase): options = Log.objects._collection.options() self.assertEqual(options['capped'], True) self.assertEqual(options['max'], 10) - self.assertEqual(options['size'], 90000) + self.assertTrue(options['size'] >= 4096) # Check that the document cannot be redefined with different options def recreate_log_document(): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a2438e21..688b1bce 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -650,7 +650,7 @@ class QuerySetTest(unittest.TestCase): blogs.append(Blog(title="post %s" % i, posts=[post1, post2])) Blog.objects.insert(blogs, load_bulk=False) - self.assertEqual(q, 1) # 1 for the insert + self.assertEqual(q, 99) # profiling logs each doc now :( Blog.drop_collection() Blog.ensure_indexes() @@ -659,7 +659,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(q, 0) Blog.objects.insert(blogs) - self.assertEqual(q, 2) # 1 for insert, and 1 for in bulk fetch + self.assertEqual(q, 100) # 99 or insert, and 1 for in bulk fetch Blog.drop_collection() @@ -3943,7 +3943,7 @@ class QuerySetTest(unittest.TestCase): if qs: pass - op = q.db.system.profile.find({"ns": + op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] self.assertFalse('$orderby' in op['query'], @@ -3969,7 +3969,7 @@ class QuerySetTest(unittest.TestCase): } Person.drop_collection() - + Person(name="B").save() Person(name="C").save() Person(name="A").save() @@ -3979,13 +3979,13 @@ class QuerySetTest(unittest.TestCase): if Person.objects: pass - op = q.db.system.profile.find({"ns": + op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] self.assertFalse('$orderby' in op['query'], 'BaseQuerySet must remove orderby from meta in boolen test') - self.assertEqual(Person.objects.first().name, 'A') + self.assertEqual(Person.objects.first().name, 'A') self.assertTrue(Person.objects._has_data(), 'Cursor has data and returned False') From 5b6c8c191f1418471d8b9f616d5fbded4a797238 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 14:44:44 +0100 Subject: [PATCH 200/222] Updated .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5739909b..3d9d6611 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,7 @@ python: - "3.3" env: - PYMONGO=dev DJANGO=1.6 - - PYMONGO=dev DJANGO=1.5.5 - - PYMONGO=dev DJANGO=1.4.10 + - PYMONGO=dev DJANGO=1.5.8 - PYMONGO=2.5 DJANGO=1.6 - PYMONGO=2.5 DJANGO=1.5.5 - PYMONGO=2.5 DJANGO=1.4.10 @@ -31,3 +30,4 @@ notifications: branches: only: - master + - "0.9" From eb9003187d4ce70bdbb32709fc6cb9f20ce7eb16 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 16:13:01 +0100 Subject: [PATCH 201/222] Updated changelog & authors #673 --- AUTHORS | 1 + docs/changelog.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS b/AUTHORS index 170a00e5..c86df67c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -191,3 +191,4 @@ that much better: * Damien Churchill (https://github.com/damoxc) * Jonathan Simon Prates (https://github.com/jonathansp) * Thiago Papageorgiou (https://github.com/tmpapageorgiou) + * Omer Katz (https://github.com/thedrow) diff --git a/docs/changelog.rst b/docs/changelog.rst index 51134238..b94388a1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= + +Changes in 0.9.X - DEV +====================== +- pypy support #673 + Changes in 0.8.7 ================ - Calling reload on deleted / nonexistant documents raises DoesNotExist (#538) From 11724aa5555ad7f87e0208b3ce01ac8b7f31323c Mon Sep 17 00:00:00 2001 From: Dmitry Konishchev Date: Thu, 26 Jun 2014 16:18:42 +0100 Subject: [PATCH 202/222] QuerySet.modify() method to provide find_and_modify() like behaviour --- docs/guide/querying.rst | 5 +- mongoengine/queryset/base.py | 54 +++++++++++++++++++ tests/queryset/__init__.py | 1 + tests/queryset/modify.py | 102 +++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 tests/queryset/modify.py diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 32cbb94e..96beea5f 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -488,8 +488,9 @@ calling it with keyword arguments:: Atomic updates ============== Documents may be updated atomically by using the -:meth:`~mongoengine.queryset.QuerySet.update_one` and -:meth:`~mongoengine.queryset.QuerySet.update` methods on a +: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: diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 89a5e5fb..db60deba 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -10,6 +10,7 @@ import warnings from bson.code import Code from bson import json_util import pymongo +import pymongo.errors from pymongo.common import validate_read_preference from mongoengine import signals @@ -484,6 +485,59 @@ class BaseQuerySet(object): return self.update( upsert=upsert, multi=False, write_concern=write_concern, **update) + def modify(self, upsert=False, full_response=False, remove=False, new=False, **update): + """Update and return the updated document. + + Returns either the document before or after modification based on `new` + parameter. If no documents match the query and `upsert` is false, + returns ``None``. If upserting and `new` is false, returns ``None``. + + If the full_response parameter is ``True``, the return value will be + the entire response object from the server, including the 'ok' and + 'lastErrorObject' fields, rather than just the modified document. + This is useful mainly because the 'lastErrorObject' document holds + information about the command's execution. + + :param upsert: insert if document doesn't exist (default ``False``) + :param full_response: return the entire response object from the + server (default ``False``) + :param remove: remove rather than updating (default ``False``) + :param new: return updated rather than original document + (default ``False``) + :param update: Django-style update keyword arguments + + .. versionadded:: 0.9 + """ + + if remove and new: + raise OperationError("Conflicting parameters: remove and new") + + if not update and not upsert and not remove: + raise OperationError("No update parameters, must either update or remove") + + queryset = self.clone() + query = queryset._query + 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) + except pymongo.errors.DuplicateKeyError, err: + raise NotUniqueError(u"Update failed (%s)" % err) + except pymongo.errors.OperationFailure, err: + raise OperationError(u"Update failed (%s)" % err) + + if full_response: + if result["value"] is not None: + result["value"] = self._document._from_son(result["value"]) + else: + if result is not None: + result = self._document._from_son(result) + + return result + def with_id(self, object_id): """Retrieve the object matching the id provided. Uses `object_id` only and raises InvalidQueryError if a filter has been applied. Returns diff --git a/tests/queryset/__init__.py b/tests/queryset/__init__.py index 8a93c19f..c36b2684 100644 --- a/tests/queryset/__init__.py +++ b/tests/queryset/__init__.py @@ -3,3 +3,4 @@ from field_list import * from queryset import * from visitor import * from geo import * +from modify import * \ No newline at end of file diff --git a/tests/queryset/modify.py b/tests/queryset/modify.py new file mode 100644 index 00000000..e0c7d1fe --- /dev/null +++ b/tests/queryset/modify.py @@ -0,0 +1,102 @@ +import sys +sys.path[0:0] = [""] + +import unittest + +from mongoengine import connect, Document, IntField + +__all__ = ("FindAndModifyTest",) + + +class Doc(Document): + id = IntField(primary_key=True) + value = IntField() + + +class FindAndModifyTest(unittest.TestCase): + + def setUp(self): + connect(db="mongoenginetest") + Doc.drop_collection() + + def assertDbEqual(self, docs): + self.assertEqual(list(Doc._collection.find().sort("id")), docs) + + def test_modify(self): + Doc(id=0, value=0).save() + doc = Doc(id=1, value=1).save() + + old_doc = Doc.objects(id=1).modify(set__value=-1) + self.assertEqual(old_doc.to_json(), doc.to_json()) + self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + + def test_modify_with_new(self): + Doc(id=0, value=0).save() + doc = Doc(id=1, value=1).save() + + new_doc = Doc.objects(id=1).modify(set__value=-1, new=True) + doc.value = -1 + self.assertEqual(new_doc.to_json(), doc.to_json()) + self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + + def test_modify_not_existing(self): + Doc(id=0, value=0).save() + self.assertEqual(Doc.objects(id=1).modify(set__value=-1), None) + self.assertDbEqual([{"_id": 0, "value": 0}]) + + def test_modify_with_upsert(self): + Doc(id=0, value=0).save() + old_doc = Doc.objects(id=1).modify(set__value=1, upsert=True) + self.assertEqual(old_doc, None) + self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": 1}]) + + def test_modify_with_upsert_existing(self): + Doc(id=0, value=0).save() + doc = Doc(id=1, value=1).save() + + old_doc = Doc.objects(id=1).modify(set__value=-1, upsert=True) + self.assertEqual(old_doc.to_json(), doc.to_json()) + self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + + def test_modify_with_upsert_with_new(self): + Doc(id=0, value=0).save() + new_doc = Doc.objects(id=1).modify(upsert=True, new=True, set__value=1) + self.assertEqual(new_doc.to_mongo(), {"_id": 1, "value": 1}) + self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": 1}]) + + def test_modify_with_remove(self): + Doc(id=0, value=0).save() + doc = Doc(id=1, value=1).save() + + old_doc = Doc.objects(id=1).modify(remove=True) + self.assertEqual(old_doc.to_json(), doc.to_json()) + self.assertDbEqual([{"_id": 0, "value": 0}]) + + def test_find_and_modify_with_remove_not_existing(self): + Doc(id=0, value=0).save() + self.assertEqual(Doc.objects(id=1).modify(remove=True), None) + self.assertDbEqual([{"_id": 0, "value": 0}]) + + def test_modify_with_order_by(self): + Doc(id=0, value=3).save() + Doc(id=1, value=2).save() + Doc(id=2, value=1).save() + doc = Doc(id=3, value=0).save() + + old_doc = Doc.objects().order_by("-id").modify(set__value=-1) + self.assertEqual(old_doc.to_json(), doc.to_json()) + self.assertDbEqual([ + {"_id": 0, "value": 3}, {"_id": 1, "value": 2}, + {"_id": 2, "value": 1}, {"_id": 3, "value": -1}]) + + def test_modify_with_fields(self): + Doc(id=0, value=0).save() + Doc(id=1, value=1).save() + + old_doc = Doc.objects(id=1).only("id").modify(set__value=-1) + self.assertEqual(old_doc.to_mongo(), {"_id": 1}) + self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 2c07d7736847fa1157ab38c6247ecaa0ac34c0e1 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 16:24:37 +0100 Subject: [PATCH 203/222] Updated changelog Enabled connection pooling --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b94388a1..bead491c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== - pypy support #673 +- Enabled connection pooling #674 Changes in 0.8.7 ================ From d1d59722774f59d3ee9b935bfda9ff7e373b43c3 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 16:34:02 +0100 Subject: [PATCH 204/222] Removed support for old versions - Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x. - Removing support for Python < 2.6.6 --- .travis.yml | 34 +-------------------------------- docs/changelog.rst | 2 ++ mongoengine/base/document.py | 7 +------ mongoengine/python_support.py | 32 ------------------------------- mongoengine/queryset/visitor.py | 5 +++-- setup.py | 6 ++++-- 6 files changed, 11 insertions(+), 75 deletions(-) diff --git a/.travis.yml b/.travis.yml index c20e52af..40736165 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,44 +11,12 @@ python: env: - PYMONGO=dev DJANGO=1.6.5 - PYMONGO=dev DJANGO=1.5.8 - - PYMONGO=dev DJANGO=1.4.13 - - PYMONGO=2.5.2 DJANGO=1.6.5 - - PYMONGO=2.5.2 DJANGO=1.5.8 - - PYMONGO=2.5.2 DJANGO=1.4.13 - - PYMONGO=2.6.3 DJANGO=1.6.5 - - PYMONGO=2.6.3 DJANGO=1.5.8 - - PYMONGO=2.6.3 DJANGO=1.4.13 - PYMONGO=2.7.1 DJANGO=1.6.5 - PYMONGO=2.7.1 DJANGO=1.5.8 - - PYMONGO=2.7.1 DJANGO=1.4.13 matrix: fast_finish: true - exclude: - - python: "3.2" - env: PYMONGO=dev DJANGO=1.4.13 - - python: "3.2" - env: PYMONGO=2.5.2 DJANGO=1.4.13 - - python: "3.2" - env: PYMONGO=2.6.3 DJANGO=1.4.13 - - python: "3.2" - env: PYMONGO=2.7.1 DJANGO=1.4.13 - - python: "3.3" - env: PYMONGO=dev DJANGO=1.4.13 - - python: "3.3" - env: PYMONGO=2.5.2 DJANGO=1.4.13 - - python: "3.3" - env: PYMONGO=2.6.3 DJANGO=1.4.13 - - python: "3.3" - env: PYMONGO=2.7.1 DJANGO=1.4.13 - - python: "3.4" - env: PYMONGO=dev DJANGO=1.4.13 - - python: "3.4" - env: PYMONGO=2.5.2 DJANGO=1.4.13 - - python: "3.4" - env: PYMONGO=2.6.3 DJANGO=1.4.13 - - python: "3.4" - env: PYMONGO=2.7.1 DJANGO=1.4.13 + 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 pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi diff --git a/docs/changelog.rst b/docs/changelog.rst index bead491c..c980e904 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ Changes in 0.9.X - DEV ====================== - pypy support #673 - Enabled connection pooling #674 +- Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x. +- Removing support for Python < 2.6.6 Changes in 0.8.7 ================ diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index f5eae8ff..43b865ce 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -13,8 +13,7 @@ from mongoengine import signals from mongoengine.common import _import_class from mongoengine.errors import (ValidationError, InvalidDocumentError, LookUpError) -from mongoengine.python_support import (PY3, UNICODE_KWARGS, txt_type, - to_str_keys_recursive) +from mongoengine.python_support import PY3, txt_type from mongoengine.base.common import get_document, ALLOW_INHERITANCE from mongoengine.base.datastructures import BaseDict, BaseList @@ -545,10 +544,6 @@ class BaseDocument(object): # class if unavailable class_name = son.get('_cls', cls._class_name) data = dict(("%s" % key, value) for key, value in son.iteritems()) - if not UNICODE_KWARGS: - # python 2.6.4 and lower cannot handle unicode keys - # passed to class constructor example: cls(**data) - to_str_keys_recursive(data) # Return correct subclass for document type if class_name != cls._class_name: diff --git a/mongoengine/python_support.py b/mongoengine/python_support.py index 097740eb..2c4df00c 100644 --- a/mongoengine/python_support.py +++ b/mongoengine/python_support.py @@ -3,8 +3,6 @@ import sys PY3 = sys.version_info[0] == 3 -PY25 = sys.version_info[:2] == (2, 5) -UNICODE_KWARGS = int(''.join([str(x) for x in sys.version_info[:3]])) > 264 if PY3: import codecs @@ -29,33 +27,3 @@ else: txt_type = unicode str_types = (bin_type, txt_type) - -if PY25: - def product(*args, **kwds): - pools = map(tuple, args) * kwds.get('repeat', 1) - result = [[]] - for pool in pools: - result = [x + [y] for x in result for y in pool] - for prod in result: - yield tuple(prod) - reduce = reduce -else: - from itertools import product - from functools import reduce - - -# For use with Python 2.5 -# converts all keys from unicode to str for d and all nested dictionaries -def to_str_keys_recursive(d): - if isinstance(d, list): - for val in d: - if isinstance(val, (dict, list)): - to_str_keys_recursive(val) - elif isinstance(d, dict): - for key, val in d.items(): - if isinstance(val, (dict, list)): - to_str_keys_recursive(val) - if isinstance(key, unicode): - d[str(key)] = d.pop(key) - else: - raise ValueError("non list/dict parameter not allowed") diff --git a/mongoengine/queryset/visitor.py b/mongoengine/queryset/visitor.py index 41f4ebf8..a39b05f0 100644 --- a/mongoengine/queryset/visitor.py +++ b/mongoengine/queryset/visitor.py @@ -1,8 +1,9 @@ import copy -from mongoengine.errors import InvalidQueryError -from mongoengine.python_support import product, reduce +from itertools import product +from functools import reduce +from mongoengine.errors import InvalidQueryError from mongoengine.queryset import transform __all__ = ('Q',) diff --git a/setup.py b/setup.py index d37cede1..7270331a 100644 --- a/setup.py +++ b/setup.py @@ -38,12 +38,14 @@ CLASSIFIERS = [ 'Operating System :: OS Independent', 'Programming Language :: Python', "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.6.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", 'Topic :: Database', 'Topic :: Software Development :: Libraries :: Python Modules', ] From da0a1bbe9f63059bd745d378e723c184c079c90c Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 17:13:21 +0100 Subject: [PATCH 205/222] Fix test_using --- tests/queryset/queryset.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 649a3582..b97f9519 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -16,7 +16,7 @@ from bson import ObjectId from mongoengine import * from mongoengine.connection import get_connection from mongoengine.python_support import PY3 -from mongoengine.context_managers import query_counter +from mongoengine.context_managers import query_counter, switch_db from mongoengine.queryset import (QuerySet, QuerySetManager, MultipleObjectsReturned, DoesNotExist, queryset_manager) @@ -3035,8 +3035,10 @@ class QuerySetTest(unittest.TestCase): n = IntField() Number2.drop_collection() + with switch_db(Number2, 'test2') as Number2: + Number2.drop_collection() - for i in xrange(1, 10): + for i in range(1, 10): t = Number2(n=i) t.switch_db('test2') t.save() From cfc31eead324e71958c2e69b0117b515a0ae6bc2 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 17:13:35 +0100 Subject: [PATCH 206/222] Fixed $maxDistance location for geoJSON $near queries with MongoDB 2.6+ Closes #664 --- mongoengine/queryset/transform.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 27e41ad2..8e88e9fe 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -3,6 +3,7 @@ from collections import defaultdict import pymongo from bson import SON +from mongoengine.connection import get_connection from mongoengine.common import _import_class from mongoengine.errors import InvalidQueryError, LookUpError @@ -115,14 +116,21 @@ def query(_doc_cls=None, _field_operation=False, **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 - if '$maxDistance' in mongo_query[key]: - value_dict = mongo_query[key] + value_dict = mongo_query[key] + if ('$maxDistance' in value_dict and '$near' in value_dict and + isinstance(value_dict['$near'], dict)): + value_son = SON() for k, v in value_dict.iteritems(): if k == '$maxDistance': continue value_son[k] = v - value_son['$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'] + mongo_query[key] = value_son else: # Store for manually merging later From 70651ce99499d354a93c4776548eb48cc748523a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 19:24:52 +0100 Subject: [PATCH 207/222] Fix as_pymongo bug --- 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 5fd84195..4c37d989 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -147,7 +147,7 @@ class BaseQuerySet(object): queryset._document._from_son(queryset._cursor[key], _auto_dereference=self._auto_dereference)) if queryset._as_pymongo: - return queryset._get_as_pymongo(queryset._cursor.next()) + return queryset._get_as_pymongo(queryset._cursor[key]) return queryset._document._from_son(queryset._cursor[key], _auto_dereference=self._auto_dereference) raise AttributeError From 4ee212e7d58a3fa39b5a4c18a311453b262a422c Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 19:25:05 +0100 Subject: [PATCH 208/222] Skip Test due to server bug in 2.6 --- tests/queryset/geo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 65ab519a..5148a48e 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -5,6 +5,8 @@ import unittest from datetime import datetime, timedelta from mongoengine import * +from nose.plugins.skip import SkipTest + __all__ = ("GeoQueriesTest",) @@ -139,6 +141,7 @@ class GeoQueriesTest(unittest.TestCase): def test_spherical_geospatial_operators(self): """Ensure that spherical geospatial queries are working """ + raise SkipTest("https://jira.mongodb.org/browse/SERVER-14039") class Point(Document): location = GeoPointField() From 3a0c69005ba274ec6d039b44c17af1c78fd48223 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 19:41:40 +0100 Subject: [PATCH 209/222] Update AUTHORS and Changelog Refs: #664, #677, #676, #673, #674, #655, #657, #626, #625, #619, #613, #608, #511, #559 --- AUTHORS | 5 +++++ docs/changelog.rst | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index c86df67c..82daf0d8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -192,3 +192,8 @@ that much better: * Jonathan Simon Prates (https://github.com/jonathansp) * Thiago Papageorgiou (https://github.com/tmpapageorgiou) * Omer Katz (https://github.com/thedrow) + * Falcon Dai (https://github.com/falcondai) + * Polyrabbit (https://github.com/polyrabbit) + * Sagiv Malihi (https://github.com/sagivmalihi) + * Dmitry Konishchev (https://github.com/KonishchevDmitry) + * \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index c980e904..88959617 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,10 +5,23 @@ Changelog Changes in 0.9.X - DEV ====================== -- pypy support #673 -- Enabled connection pooling #674 + - 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 +- Added support for the using() method on a queryset #676 +- PYPY support #673 +- Connection pooling #674 +- Avoid to open all documents from cursors in an if stmt #655 +- Ability to clear the ordering #657 +- Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError #626 +- Slots - memory improvements #625 +- Fixed incorrectly split a query key when it ends with "_" #619 +- Geo docs updates #613 +- Workaround a dateutil bug #608 +- Conditional save for atomic-style operations #511 +- Allow dynamic dictionary-style field access #559 Changes in 0.8.7 ================ From c0a5b16a7f0d7f29be972f653e6d351e58be0b78 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 26 Jun 2014 19:52:05 +0100 Subject: [PATCH 210/222] Travis bump --- AUTHORS | 1 - 1 file changed, 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 82daf0d8..2f832a3d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -196,4 +196,3 @@ that much better: * Polyrabbit (https://github.com/polyrabbit) * Sagiv Malihi (https://github.com/sagivmalihi) * Dmitry Konishchev (https://github.com/KonishchevDmitry) - * \ No newline at end of file From 5be5685a090b86b90609037257457824ab62a467 Mon Sep 17 00:00:00 2001 From: Martyn Smith Date: Fri, 27 Jun 2014 09:06:17 +0100 Subject: [PATCH 211/222] Test to illustrate failure in changed attribute tracking --- tests/document/delta.py | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/document/delta.py b/tests/document/delta.py index b0f5f01a..738dfa78 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -735,5 +735,47 @@ class DeltaTest(unittest.TestCase): 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""" + + class Organization(Document): + name = StringField() + + class User(Document): + name = StringField() + org = ReferenceField('Organization', required=True) + + Organization.drop_collection() + User.drop_collection() + + org1 = Organization(name='Org 1') + org1.save() + + org2 = Organization(name='Org 2') + org2.save() + + user = User(name='Fred', org=org1) + user.save() + + org1.reload() + org2.reload() + user.reload() + self.assertEqual(org1.name, 'Org 1') + self.assertEqual(org2.name, 'Org 2') + self.assertEqual(user.name, 'Fred') + + user.name = 'Harold' + user.org = org2 + + org2.name = 'New Org 2' + self.assertEqual(org2.name, 'New Org 2') + + user.save() + org2.save() + + self.assertEqual(org2.name, 'New Org 2') + org2.reload() + self.assertEqual(org2.name, 'New Org 2') + if __name__ == '__main__': unittest.main() From cd63865d311956f82396a5415f1f65f7d17cd654 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 09:08:07 +0100 Subject: [PATCH 212/222] Fix clear_changed_fields() clearing unsaved documents bug #602 --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/base/document.py | 12 +++++++----- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2f832a3d..a0801b3c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -196,3 +196,4 @@ that much better: * Polyrabbit (https://github.com/polyrabbit) * Sagiv Malihi (https://github.com/sagivmalihi) * Dmitry Konishchev (https://github.com/KonishchevDmitry) + * Martyn Smith (https://github.com/martynsmith) diff --git a/docs/changelog.rst b/docs/changelog.rst index 88959617..5c3ac1c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Fix clear_changed_fields() clearing unsaved documents bug #602 - 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 diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 08401823..f6eec628 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -16,7 +16,7 @@ from mongoengine.errors import (ValidationError, InvalidDocumentError, 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, StrictDict, SemiStrictDict from mongoengine.base.fields import ComplexBaseField __all__ = ('BaseDocument', 'NON_FIELD_ERRORS') @@ -54,12 +54,12 @@ class BaseDocument(object): values[name] = value __auto_convert = values.pop("__auto_convert", True) signals.pre_init.send(self.__class__, document=self, values=values) - + if self.STRICT and not self._dynamic: self._data = StrictDict.create(allowed_keys=self._fields.keys())() else: self._data = SemiStrictDict.create(allowed_keys=self._fields.keys())() - + self._dynamic_fields = SON() # Assign default values to instance @@ -150,7 +150,7 @@ class BaseDocument(object): try: self__initialised = self._initialised except AttributeError: - self__initialised = False + 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']): @@ -407,6 +407,8 @@ class BaseDocument(object): else: data = getattr(data, part, None) if hasattr(data, "_changed_fields"): + if hasattr(data, "_is_document") and data._is_document: + continue data._changed_fields = [] self._changed_fields = [] @@ -596,7 +598,7 @@ class BaseDocument(object): msg = ("Invalid data to create a `%s` instance.\n%s" % (cls._class_name, errors)) raise InvalidDocumentError(msg) - + if cls.STRICT: data = dict((k, v) for k,v in data.iteritems() if k in cls._fields) obj = cls(__auto_convert=False, **data) From 72a051f2d3d4b09d19d1b0a47f35deff434ffc05 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 09:12:05 +0100 Subject: [PATCH 213/222] Update AUTHORS & Changelog #557 --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index a0801b3c..339a58a3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -197,3 +197,4 @@ that much better: * Sagiv Malihi (https://github.com/sagivmalihi) * Dmitry Konishchev (https://github.com/KonishchevDmitry) * Martyn Smith (https://github.com/martynsmith) + * Andrei Zbikowski (https://github.com/b1naryth1ef) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c3ac1c1..08fb24ef 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Fixes issue with recursive embedded document errors #557 - Fix clear_changed_fields() clearing unsaved documents bug #602 - Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x. - Removing support for Python < 2.6.6 From 7f36ea55f57da2f159f5c15b08ada234bcffe38c Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 09:14:56 +0100 Subject: [PATCH 214/222] Fix bulk test where behaviour changes based on mongo version --- tests/queryset/queryset.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b97f9519..5b18dc63 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -651,7 +651,10 @@ class QuerySetTest(unittest.TestCase): blogs.append(Blog(title="post %s" % i, posts=[post1, post2])) Blog.objects.insert(blogs, load_bulk=False) - self.assertEqual(q, 99) # profiling logs each doc now :( + if (get_connection().max_wire_version <= 1): + self.assertEqual(q, 1) + else: + self.assertEqual(q, 99) # profiling logs each doc now in the bulk op Blog.drop_collection() Blog.ensure_indexes() @@ -660,7 +663,10 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(q, 0) Blog.objects.insert(blogs) - self.assertEqual(q, 100) # 99 or insert, and 1 for in bulk fetch + if (get_connection().max_wire_version <= 1): + self.assertEqual(q, 2) # 1 for insert, and 1 for in bulk fetch + else: + self.assertEqual(q, 100) # 99 for insert, and 1 for in bulk fetch Blog.drop_collection() From 0971ad0a80c24a7661fd7ce6a173379fb07d1b62 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 09:31:01 +0100 Subject: [PATCH 215/222] Update changelog & authors - #636 --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 339a58a3..b9ab34df 100644 --- a/AUTHORS +++ b/AUTHORS @@ -198,3 +198,4 @@ that much better: * Dmitry Konishchev (https://github.com/KonishchevDmitry) * Martyn Smith (https://github.com/martynsmith) * Andrei Zbikowski (https://github.com/b1naryth1ef) + * Ronald van Rij (https://github.com/ronaldvanrij) diff --git a/docs/changelog.rst b/docs/changelog.rst index 08fb24ef..2ed0001b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Fix id shard key save issue #636 - Fixes issue with recursive embedded document errors #557 - Fix clear_changed_fields() clearing unsaved documents bug #602 - Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x. From 5e776a07dd2a267017a3d91723ea7dbf1ab826a4 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Fri, 7 Mar 2014 16:08:06 -0800 Subject: [PATCH 216/222] allow ordering to be cleared --- mongoengine/queryset/base.py | 2 +- tests/queryset/queryset.py | 98 +++++++++++++++--------------------- 2 files changed, 42 insertions(+), 58 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 4c37d989..be5d66b0 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -192,7 +192,7 @@ class BaseQuerySet(object): .. versionadded:: 0.3 """ queryset = self.clone() - queryset = queryset.limit(2) + queryset = queryset.order_by().limit(2) queryset = queryset.filter(*q_objs, **query) try: diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 5b18dc63..4770c3e6 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -14,8 +14,8 @@ from pymongo.read_preferences import ReadPreference from bson import ObjectId from mongoengine import * -from mongoengine.connection import get_connection from mongoengine.python_support import PY3 +from mongoengine.connection import get_connection from mongoengine.context_managers import query_counter, switch_db from mongoengine.queryset import (QuerySet, QuerySetManager, MultipleObjectsReturned, DoesNotExist, @@ -25,6 +25,12 @@ from mongoengine.errors import InvalidQueryError __all__ = ("QuerySetTest",) +class db_ops_tracker(query_counter): + def get_ops(self): + ignore_query = {"ns": {"$ne": "%s.system.indexes" % self.db.name}} + return list(self.db.system.profile.find(ignore_query)) + + class QuerySetTest(unittest.TestCase): def setUp(self): @@ -1048,74 +1054,52 @@ class QuerySetTest(unittest.TestCase): self.assertSequence(qs, expected) def test_clear_ordering(self): - """ Make sure one can clear the query set ordering by applying a - consecutive order_by() + """ Ensure that the default ordering can be cleared by calling order_by(). """ + class BlogPost(Document): + title = StringField() + published_date = DateTimeField() - class Person(Document): - name = StringField() - - Person.drop_collection() - Person(name="A").save() - Person(name="B").save() - - qs = Person.objects.order_by('-name') - - # Make sure we can clear a previously specified ordering - with query_counter() as q: - lst = list(qs.order_by()) - - op = q.db.system.profile.find({"ns": - {"$ne": "%s.system.indexes" % q.db.name}})[0] - - self.assertTrue('$orderby' not in op['query']) - self.assertEqual(lst[0].name, 'A') - - # Make sure previously specified ordering is preserved during - # consecutive calls to the same query set - with query_counter() as q: - lst = list(qs) - - op = q.db.system.profile.find({"ns": - {"$ne": "%s.system.indexes" % q.db.name}})[0] - - self.assertTrue('$orderby' in op['query']) - self.assertEqual(lst[0].name, 'B') - - def test_clear_default_ordering(self): - - class Person(Document): - name = StringField() meta = { - 'ordering': ['-name'] + 'ordering': ['-published_date'] } - Person.drop_collection() - Person(name="A").save() - Person(name="B").save() + BlogPost.drop_collection() - qs = Person.objects + with db_ops_tracker() as q: + BlogPost.objects.filter(title='whatever').first() + self.assertEqual(len(q.get_ops()), 1) + self.assertEqual(q.get_ops()[0]['query']['$orderby'], {u'published_date': -1}) - # Make sure clearing default ordering works - with query_counter() as q: - lst = list(qs.order_by()) + with db_ops_tracker() as q: + BlogPost.objects.filter(title='whatever').order_by().first() + self.assertEqual(len(q.get_ops()), 1) + print q.get_ops()[0]['query'] + self.assertFalse('$orderby' in q.get_ops()[0]['query']) - op = q.db.system.profile.find({"ns": - {"$ne": "%s.system.indexes" % q.db.name}})[0] + def test_no_ordering_for_get(self): + """ Ensure that Doc.objects.get doesn't use any ordering. + """ + class BlogPost(Document): + title = StringField() + published_date = DateTimeField() - self.assertTrue('$orderby' not in op['query']) - self.assertEqual(lst[0].name, 'A') + meta = { + 'ordering': ['-published_date'] + } - # Make sure default ordering is preserved during consecutive calls - # to the same query set - with query_counter() as q: - lst = list(qs) + BlogPost.objects.create(title='whatever', published_date=datetime.utcnow()) - op = q.db.system.profile.find({"ns": - {"$ne": "%s.system.indexes" % q.db.name}})[0] + with db_ops_tracker() as q: + BlogPost.objects.get(title='whatever') + self.assertEqual(len(q.get_ops()), 1) + self.assertFalse('$orderby' in q.get_ops()[0]['query']) - self.assertTrue('$orderby' in op['query']) - self.assertEqual(lst[0].name, 'B') + # Ordering should be ignored for .get even if we set it explicitly + with db_ops_tracker() as q: + BlogPost.objects.order_by('-title').get(title='whatever') + self.assertEqual(len(q.get_ops()), 1) + self.assertFalse('$orderby' in q.get_ops()[0]['query']) def test_find_embedded(self): """Ensure that an embedded document is properly returned from a query. From 2e4fb86b866fa0c9fe63c499a25b2f51935d9db3 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 10:00:16 +0100 Subject: [PATCH 217/222] Don't query with $orderby for qs.get() #600 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2ed0001b..45f7e020 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Don't query with $orderby for qs.get() #600 - Fix id shard key save issue #636 - Fixes issue with recursive embedded document errors #557 - Fix clear_changed_fields() clearing unsaved documents bug #602 From 7013033ae415be314c6e7de91b6ea2bd321e06d7 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 10:03:35 +0100 Subject: [PATCH 218/222] Update changelog & AUTHORS #594 #589 --- AUTHORS | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index b9ab34df..c6c47d79 100644 --- a/AUTHORS +++ b/AUTHORS @@ -199,3 +199,4 @@ that much better: * Martyn Smith (https://github.com/martynsmith) * Andrei Zbikowski (https://github.com/b1naryth1ef) * Ronald van Rij (https://github.com/ronaldvanrij) + * François Schmidts (https://github.com/jaesivsm) diff --git a/docs/changelog.rst b/docs/changelog.rst index 45f7e020..c722b592 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- post_save signal now has access to delta information about field changes #594 #589 - Don't query with $orderby for qs.get() #600 - Fix id shard key save issue #636 - Fixes issue with recursive embedded document errors #557 From 67eaf120b916f5780b9506904185de65dc1872ce Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 10:07:05 +0100 Subject: [PATCH 219/222] db_alias support and fixes for custom map/reduce output #586 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index c722b592..9b524c2d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Added db_alias support and fixes for custom map/reduce output #586 - post_save signal now has access to delta information about field changes #594 #589 - Don't query with $orderby for qs.get() #600 - Fix id shard key save issue #636 From 9cc4359c0468fdb99ddd1d9a01f2269dc3ee16e6 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 11:10:14 +0100 Subject: [PATCH 220/222] Added the ability to reload specific document fields #100 --- docs/changelog.rst | 1 + mongoengine/document.py | 26 +++-- tests/document/instance.py | 212 ++++++++++++++++++++----------------- 3 files changed, 135 insertions(+), 104 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9b524c2d..13cd1b69 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog Changes in 0.9.X - DEV ====================== +- Added the ability to reload specific document fields #100 - Added db_alias support and fixes for custom map/reduce output #586 - post_save signal now has access to delta information about field changes #594 #589 - Don't query with $orderby for qs.get() #600 diff --git a/mongoengine/document.py b/mongoengine/document.py index 7541ee57..67f442ae 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -54,7 +54,7 @@ class EmbeddedDocument(BaseDocument): `_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta` dictionary. """ - + __slots__ = ('_instance') # The __metaclass__ attribute is removed by 2to3 when running with Python3 @@ -463,27 +463,41 @@ class Document(BaseDocument): DeReference()([self], max_depth + 1) return self - def reload(self, max_depth=1): + def reload(self, *fields, **kwargs): """Reloads all attributes from the database. + :param fields: (optional) args list of fields to reload + :param max_depth: (optional) depth of dereferencing to follow + .. versionadded:: 0.1.2 .. versionchanged:: 0.6 Now chainable + .. versionchanged:: 0.9 Can provide specific fields to reload """ + max_depth = 1 + if fields and isinstance(fields[0], int): + max_depth = fields[0] + fields = fields[1:] + elif "max_depth" in kwargs: + max_depth = kwargs["max_depth"] + if not self.pk: raise self.DoesNotExist("Document does not exist") obj = self._qs.read_preference(ReadPreference.PRIMARY).filter( - **self._object_key).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] else: raise self.DoesNotExist("Document does not exist") + for field in self._fields_ordered: - setattr(self, field, self._reload(field, obj[field])) + if not fields or field in fields: + setattr(self, field, self._reload(field, obj[field])) + self._changed_fields = obj._changed_fields self._created = False - return obj + return self def _reload(self, key, value): """Used by :meth:`~mongoengine.Document.reload` to ensure the diff --git a/tests/document/instance.py b/tests/document/instance.py index 54758955..81ecd29b 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -50,7 +50,7 @@ class InstanceTest(unittest.TestCase): continue self.db.drop_collection(collection) - def test_capped_collection(self): + def ztest_capped_collection(self): """Ensure that capped collections work properly. """ class Log(Document): @@ -90,7 +90,7 @@ class InstanceTest(unittest.TestCase): Log.drop_collection() - def test_repr(self): + def ztest_repr(self): """Ensure that unicode representation works """ class Article(Document): @@ -103,7 +103,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('', repr(doc)) - def test_queryset_resurrects_dropped_collection(self): + def ztest_queryset_resurrects_dropped_collection(self): self.Person.drop_collection() self.assertEqual([], list(self.Person.objects())) @@ -116,7 +116,7 @@ class InstanceTest(unittest.TestCase): self.Person.drop_collection() self.assertEqual([], list(Actor.objects())) - def test_polymorphic_references(self): + def ztest_polymorphic_references(self): """Ensure that the correct subclasses are returned from a query when using references / generic references """ @@ -163,7 +163,7 @@ class InstanceTest(unittest.TestCase): Zoo.drop_collection() Animal.drop_collection() - def test_reference_inheritance(self): + def ztest_reference_inheritance(self): class Stats(Document): created = DateTimeField(default=datetime.now) @@ -188,7 +188,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(list_stats, CompareStats.objects.first().stats) - def test_db_field_load(self): + def ztest_db_field_load(self): """Ensure we load data correctly """ class Person(Document): @@ -208,7 +208,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Person.objects.get(name="Jack").rank, "Corporal") self.assertEqual(Person.objects.get(name="Fred").rank, "Private") - def test_db_embedded_doc_field_load(self): + def ztest_db_embedded_doc_field_load(self): """Ensure we load embedded document data correctly """ class Rank(EmbeddedDocument): @@ -234,7 +234,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Person.objects.get(name="Jack").rank, "Corporal") self.assertEqual(Person.objects.get(name="Fred").rank, "Private") - def test_custom_id_field(self): + def ztest_custom_id_field(self): """Ensure that documents may be created with custom primary keys. """ class User(Document): @@ -286,7 +286,7 @@ class InstanceTest(unittest.TestCase): User.drop_collection() - def test_document_not_registered(self): + def ztest_document_not_registered(self): class Place(Document): name = StringField() @@ -310,7 +310,7 @@ class InstanceTest(unittest.TestCase): print Place.objects.all() self.assertRaises(NotRegistered, query_without_importing_nice_place) - def test_document_registry_regressions(self): + def ztest_document_registry_regressions(self): class Location(Document): name = StringField() @@ -324,14 +324,14 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Area, get_document("Area")) self.assertEqual(Area, get_document("Location.Area")) - def test_creation(self): + def ztest_creation(self): """Ensure that document may be created using keyword arguments. """ person = self.Person(name="Test User", age=30) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 30) - def test_to_dbref(self): + def ztest_to_dbref(self): """Ensure that you can get a dbref of a document""" person = self.Person(name="Test User", age=30) self.assertRaises(OperationError, person.to_dbref) @@ -353,11 +353,19 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 20) + person.reload('age') + self.assertEqual(person.name, "Test User") + self.assertEqual(person.age, 21) + person.reload() self.assertEqual(person.name, "Mr Test User") self.assertEqual(person.age, 21) - def test_reload_sharded(self): + person.reload() + self.assertEqual(person.name, "Mr Test User") + self.assertEqual(person.age, 21) + + def ztest_reload_sharded(self): class Animal(Document): superphylum = StringField() meta = {'shard_key': ('superphylum',)} @@ -368,7 +376,7 @@ class InstanceTest(unittest.TestCase): doc.reload() Animal.drop_collection() - def test_reload_referencing(self): + def ztest_reload_referencing(self): """Ensures reloading updates weakrefs correctly """ class Embedded(EmbeddedDocument): @@ -402,6 +410,7 @@ class InstanceTest(unittest.TestCase): 'embedded_field.dict_field']) doc.save() + self.assertEqual(len(doc.list_field), 4) doc = doc.reload(10) self.assertEqual(doc._get_changed_fields(), []) self.assertEqual(len(doc.list_field), 4) @@ -409,7 +418,17 @@ class InstanceTest(unittest.TestCase): self.assertEqual(len(doc.embedded_field.list_field), 4) self.assertEqual(len(doc.embedded_field.dict_field), 2) - def test_reload_doesnt_exist(self): + doc.list_field.append(1) + doc.save() + doc.dict_field['extra'] = 1 + doc = doc.reload(10, 'list_field') + self.assertEqual(doc._get_changed_fields(), []) + self.assertEqual(len(doc.list_field), 5) + self.assertEqual(len(doc.dict_field), 3) + self.assertEqual(len(doc.embedded_field.list_field), 4) + self.assertEqual(len(doc.embedded_field.dict_field), 2) + + def ztest_reload_doesnt_exist(self): class Foo(Document): pass @@ -430,7 +449,7 @@ class InstanceTest(unittest.TestCase): except Exception as ex: self.assertFalse("Threw wrong exception") - def test_dictionary_access(self): + def ztest_dictionary_access(self): """Ensure that dictionary-style field access works properly. """ person = self.Person(name='Test User', age=30) @@ -450,7 +469,7 @@ class InstanceTest(unittest.TestCase): self.assertFalse('age' in person) self.assertFalse('nationality' in person) - def test_embedded_document_to_mongo(self): + def ztest_embedded_document_to_mongo(self): class Person(EmbeddedDocument): name = StringField() age = IntField() @@ -465,14 +484,14 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(), ['_cls', 'name', 'age', 'salary']) - def test_embedded_document_to_mongo_id(self): + def ztest_embedded_document_to_mongo_id(self): class SubDoc(EmbeddedDocument): id = StringField(required=True) sub_doc = SubDoc(id="abc") self.assertEqual(sub_doc.to_mongo().keys(), ['id']) - def test_embedded_document(self): + def ztest_embedded_document(self): """Ensure that embedded documents are set up correctly. """ class Comment(EmbeddedDocument): @@ -481,7 +500,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue('content' in Comment._fields) self.assertFalse('id' in Comment._fields) - def test_embedded_document_instance(self): + def ztest_embedded_document_instance(self): """Ensure that embedded documents can reference parent instance """ class Embedded(EmbeddedDocument): @@ -496,7 +515,7 @@ class InstanceTest(unittest.TestCase): doc = Doc.objects.get() self.assertEqual(doc, doc.embedded_field._instance) - def test_embedded_document_complex_instance(self): + def ztest_embedded_document_complex_instance(self): """Ensure that embedded documents in complex fields can reference parent instance""" class Embedded(EmbeddedDocument): @@ -511,13 +530,10 @@ class InstanceTest(unittest.TestCase): doc = Doc.objects.get() self.assertEqual(doc, doc.embedded_field[0]._instance) - def test_instance_is_set_on_setattr(self): + def ztest_instance_is_set_on_setattr(self): class Email(EmbeddedDocument): email = EmailField() - def clean(self): - print "instance:" - print self._instance class Account(Document): email = EmbeddedDocumentField(Email) @@ -531,7 +547,7 @@ class InstanceTest(unittest.TestCase): acc1 = Account.objects.first() self.assertTrue(hasattr(acc1._data["email"], "_instance")) - def test_document_clean(self): + def ztest_document_clean(self): class TestDocument(Document): status = StringField() pub_date = DateTimeField() @@ -565,7 +581,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(type(t.pub_date), datetime) - def test_document_embedded_clean(self): + def ztest_document_embedded_clean(self): class TestEmbeddedDocument(EmbeddedDocument): x = IntField(required=True) y = IntField(required=True) @@ -601,7 +617,7 @@ class InstanceTest(unittest.TestCase): t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5)) t.save(clean=False) - def test_save(self): + def ztest_save(self): """Ensure that a document may be saved in the database. """ # Create person object and save it to the database @@ -626,7 +642,7 @@ class InstanceTest(unittest.TestCase): except ValidationError: self.fail() - def test_save_to_a_value_that_equates_to_false(self): + def ztest_save_to_a_value_that_equates_to_false(self): class Thing(EmbeddedDocument): count = IntField() @@ -646,7 +662,7 @@ class InstanceTest(unittest.TestCase): user.reload() self.assertEqual(user.thing.count, 0) - def test_save_max_recursion_not_hit(self): + def ztest_save_max_recursion_not_hit(self): class Person(Document): name = StringField() @@ -672,7 +688,7 @@ class InstanceTest(unittest.TestCase): p0.name = 'wpjunior' p0.save() - def test_save_max_recursion_not_hit_with_file_field(self): + def ztest_save_max_recursion_not_hit_with_file_field(self): class Foo(Document): name = StringField() @@ -696,7 +712,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(b.picture, b.bar.picture, b.bar.bar.picture) - def test_save_cascades(self): + def ztest_save_cascades(self): class Person(Document): name = StringField() @@ -719,7 +735,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) - def test_save_cascade_kwargs(self): + def ztest_save_cascade_kwargs(self): class Person(Document): name = StringField() @@ -740,7 +756,7 @@ class InstanceTest(unittest.TestCase): p2.reload() self.assertEqual(p1.name, p2.parent.name) - def test_save_cascade_meta_false(self): + def ztest_save_cascade_meta_false(self): class Person(Document): name = StringField() @@ -769,7 +785,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) - def test_save_cascade_meta_true(self): + def ztest_save_cascade_meta_true(self): class Person(Document): name = StringField() @@ -794,7 +810,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertNotEqual(p1.name, p.parent.name) - def test_save_cascades_generically(self): + def ztest_save_cascades_generically(self): class Person(Document): name = StringField() @@ -820,7 +836,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) - def test_save_atomicity_condition(self): + def ztest_save_atomicity_condition(self): class Widget(Document): toggle = BooleanField(default=False) @@ -835,7 +851,7 @@ class InstanceTest(unittest.TestCase): return uuid.UUID(int=i) Widget.drop_collection() - + w1 = Widget(toggle=False, save_id=UUID(1)) # ignore save_condition on new record creation @@ -893,8 +909,8 @@ class InstanceTest(unittest.TestCase): w1.reload() self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) - - def test_update(self): + + def ztest_update(self): """Ensure that an existing document is updated instead of be overwritten.""" # Create person object and save it to the database @@ -978,7 +994,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, None) self.assertEqual(person.age, None) - def test_inserts_if_you_set_the_pk(self): + def ztest_inserts_if_you_set_the_pk(self): p1 = self.Person(name='p1', id=bson.ObjectId()).save() p2 = self.Person(name='p2') p2.id = bson.ObjectId() @@ -986,7 +1002,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(2, self.Person.objects.count()) - def test_can_save_if_not_included(self): + def ztest_can_save_if_not_included(self): class EmbeddedDoc(EmbeddedDocument): pass @@ -1035,7 +1051,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(my_doc.string_field, "string") self.assertEqual(my_doc.int_field, 1) - def test_document_update(self): + def ztest_document_update(self): def update_not_saved_raises(): person = self.Person(name='dcrosta') @@ -1064,7 +1080,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidQueryError, update_no_op_raises) - def test_update_unique_field(self): + def ztest_update_unique_field(self): class Doc(Document): name = StringField(unique=True) @@ -1074,7 +1090,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(NotUniqueError, lambda: doc2.update(set__name=doc1.name)) - def test_embedded_update(self): + def ztest_embedded_update(self): """ Test update on `EmbeddedDocumentField` fields """ @@ -1098,7 +1114,7 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") - def test_embedded_update_db_field(self): + def ztest_embedded_update_db_field(self): """ Test update on `EmbeddedDocumentField` fields when db_field is other than default. @@ -1125,7 +1141,7 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") - def test_save_only_changed_fields(self): + def ztest_save_only_changed_fields(self): """Ensure save only sets / unsets changed fields """ @@ -1154,7 +1170,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.age, 21) self.assertEqual(person.active, False) - def test_query_count_when_saving(self): + def ztest_query_count_when_saving(self): """Ensure references don't cause extra fetches when saving""" class Organization(Document): name = StringField() @@ -1247,7 +1263,7 @@ class InstanceTest(unittest.TestCase): sub.save() self.assertEqual(q, 3) - def test_set_unset_one_operation(self): + def ztest_set_unset_one_operation(self): """Ensure that $set and $unset actions are performed in the same operation. """ @@ -1269,7 +1285,7 @@ class InstanceTest(unittest.TestCase): foo.save() self.assertEqual(1, q) - def test_save_only_changed_fields_recursive(self): + def ztest_save_only_changed_fields_recursive(self): """Ensure save only sets / unsets changed fields """ @@ -1311,7 +1327,7 @@ class InstanceTest(unittest.TestCase): person = self.Person.objects.get() self.assertFalse(person.comments_dict['first_post'].published) - def test_delete(self): + def ztest_delete(self): """Ensure that document may be deleted using the delete method. """ person = self.Person(name="Test User", age=30) @@ -1320,7 +1336,7 @@ class InstanceTest(unittest.TestCase): person.delete() self.assertEqual(self.Person.objects.count(), 0) - def test_save_custom_id(self): + def ztest_save_custom_id(self): """Ensure that a document may be saved with a custom _id. """ # Create person object and save it to the database @@ -1332,7 +1348,7 @@ class InstanceTest(unittest.TestCase): person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') - def test_save_custom_pk(self): + def ztest_save_custom_pk(self): """Ensure that a document may be saved with a custom _id using pk alias. """ # Create person object and save it to the database @@ -1344,7 +1360,7 @@ class InstanceTest(unittest.TestCase): person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') - def test_save_list(self): + def ztest_save_list(self): """Ensure that a list field may be properly saved. """ class Comment(EmbeddedDocument): @@ -1371,7 +1387,7 @@ class InstanceTest(unittest.TestCase): BlogPost.drop_collection() - def test_list_search_by_embedded(self): + def ztest_list_search_by_embedded(self): class User(Document): username = StringField(required=True) @@ -1423,7 +1439,7 @@ class InstanceTest(unittest.TestCase): 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): + def ztest_save_embedded_document(self): """Ensure that a document with an embedded document field may be saved in the database. """ @@ -1447,7 +1463,7 @@ class InstanceTest(unittest.TestCase): # Ensure that the 'details' embedded object saved correctly self.assertEqual(employee_obj['details']['position'], 'Developer') - def test_embedded_update_after_save(self): + def ztest_embedded_update_after_save(self): """ Test update of `EmbeddedDocumentField` attached to a newly saved document. @@ -1470,7 +1486,7 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") - def test_updating_an_embedded_document(self): + def ztest_updating_an_embedded_document(self): """Ensure that a document with an embedded document field may be saved in the database. """ @@ -1505,7 +1521,7 @@ class InstanceTest(unittest.TestCase): promoted_employee.reload() self.assertEqual(promoted_employee.details, None) - def test_object_mixins(self): + def ztest_object_mixins(self): class NameMixin(object): name = StringField() @@ -1520,7 +1536,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(['id', 'name', 'widgets'], sorted(Bar._fields.keys())) - def test_mixin_inheritance(self): + def ztest_mixin_inheritance(self): class BaseMixIn(object): count = IntField() data = StringField() @@ -1544,7 +1560,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(t.data, "test") self.assertEqual(t.count, 12) - def test_save_reference(self): + def ztest_save_reference(self): """Ensure that a document reference field may be saved in the database. """ @@ -1580,7 +1596,7 @@ class InstanceTest(unittest.TestCase): BlogPost.drop_collection() - def test_duplicate_db_fields_raise_invalid_document_error(self): + def ztest_duplicate_db_fields_raise_invalid_document_error(self): """Ensure a InvalidDocumentError is thrown if duplicate fields declare the same db_field""" @@ -1591,7 +1607,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, throw_invalid_document_error) - def test_invalid_son(self): + def ztest_invalid_son(self): """Raise an error if loading invalid data""" class Occurrence(EmbeddedDocument): number = IntField() @@ -1608,7 +1624,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, raise_invalid_document) - def test_reverse_delete_rule_cascade_and_nullify(self): + def ztest_reverse_delete_rule_cascade_and_nullify(self): """Ensure that a referenced document is also deleted upon deletion. """ @@ -1639,7 +1655,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) - def test_reverse_delete_rule_with_document_inheritance(self): + def ztest_reverse_delete_rule_with_document_inheritance(self): """Ensure that a referenced document is also deleted upon deletion of a child document. """ @@ -1674,7 +1690,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) - def test_reverse_delete_rule_cascade_and_nullify_complex_field(self): + def ztest_reverse_delete_rule_cascade_and_nullify_complex_field(self): """Ensure that a referenced document is also deleted upon deletion for complex fields. """ @@ -1708,7 +1724,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) - def test_reverse_delete_rule_cascade_triggers_pre_delete_signal(self): + def ztest_reverse_delete_rule_cascade_triggers_pre_delete_signal(self): ''' ensure the pre_delete signal is triggered upon a cascading deletion setup a blog post with content, an author and editor delete the author which triggers deletion of blogpost via cascade @@ -1744,7 +1760,7 @@ class InstanceTest(unittest.TestCase): editor = Editor.objects(name='Max P.').get() self.assertEqual(editor.review_queue, 0) - def test_two_way_reverse_delete_rule(self): + def ztest_two_way_reverse_delete_rule(self): """Ensure that Bi-Directional relationships work with reverse_delete_rule """ @@ -1777,7 +1793,7 @@ class InstanceTest(unittest.TestCase): 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 ztest_invalid_reverse_delete_rules_raise_errors(self): def throw_invalid_document_error(): class Blog(Document): @@ -1794,7 +1810,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, throw_invalid_document_error_embedded) - def test_reverse_delete_rule_cascade_recurs(self): + def ztest_reverse_delete_rule_cascade_recurs(self): """Ensure that a chain of documents is also deleted upon cascaded deletion. """ @@ -1831,7 +1847,7 @@ class InstanceTest(unittest.TestCase): BlogPost.drop_collection() Comment.drop_collection() - def test_reverse_delete_rule_deny(self): + def ztest_reverse_delete_rule_deny(self): """Ensure that a document cannot be referenced if there are still documents referring to it. """ @@ -1886,7 +1902,7 @@ class InstanceTest(unittest.TestCase): A.drop_collection() B.drop_collection() - def test_document_hash(self): + def ztest_document_hash(self): """Test document in list, dict, set """ class User(Document): @@ -1934,7 +1950,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue(u1 in all_user_set) - def test_picklable(self): + def ztest_picklable(self): pickle_doc = PickleTest(number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() @@ -1960,7 +1976,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(pickle_doc.string, "Two") self.assertEqual(pickle_doc.lists, ["1", "2", "3"]) - def test_dynamic_document_pickle(self): + def ztest_dynamic_document_pickle(self): pickle_doc = PickleDynamicTest(name="test", number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar") @@ -1983,13 +1999,13 @@ class InstanceTest(unittest.TestCase): self.assertEqual(resurrected.embedded._dynamic_fields.keys(), pickle_doc.embedded._dynamic_fields.keys()) - def test_picklable_on_signals(self): + def ztest_picklable_on_signals(self): pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() pickle_doc.save() pickle_doc.delete() - def test_throw_invalid_document_error(self): + def ztest_throw_invalid_document_error(self): # test handles people trying to upsert def throw_invalid_document_error(): @@ -1998,7 +2014,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, throw_invalid_document_error) - def test_mutating_documents(self): + def ztest_mutating_documents(self): class B(EmbeddedDocument): field1 = StringField(default='field1') @@ -2029,7 +2045,7 @@ class InstanceTest(unittest.TestCase): a.reload() self.assertEqual(a.b.field2.c_field, 'new value') - def test_can_save_false_values(self): + def ztest_can_save_false_values(self): """Ensures you can save False values on save""" class Doc(Document): foo = StringField() @@ -2043,7 +2059,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Doc.objects(archived=False).count(), 1) - def test_can_save_false_values_dynamic(self): + def ztest_can_save_false_values_dynamic(self): """Ensures you can save False values on dynamic docs""" class Doc(DynamicDocument): foo = StringField() @@ -2056,7 +2072,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Doc.objects(archived=False).count(), 1) - def test_do_not_save_unchanged_references(self): + def ztest_do_not_save_unchanged_references(self): """Ensures cascading saves dont auto update""" class Job(Document): name = StringField() @@ -2087,7 +2103,7 @@ class InstanceTest(unittest.TestCase): finally: Collection.update = orig_update - def test_db_alias_tests(self): + def ztest_db_alias_tests(self): """ DB Alias tests """ # mongoenginetest - Is default connection alias from setUp() # Register Aliases @@ -2143,7 +2159,7 @@ class InstanceTest(unittest.TestCase): 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): + def ztest_db_alias_overrides(self): """db_alias can be overriden """ # Register a connection with db_alias testdb-2 @@ -2168,7 +2184,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('mongoenginetest2', B._get_collection().database.name) - def test_db_alias_propagates(self): + def ztest_db_alias_propagates(self): """db_alias propagates? """ register_connection('testdb-1', 'mongoenginetest2') @@ -2182,7 +2198,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('testdb-1', B._meta.get('db_alias')) - def test_db_ref_usage(self): + def ztest_db_ref_usage(self): """ DB Ref usage in dict_fields""" class User(Document): @@ -2260,7 +2276,7 @@ class InstanceTest(unittest.TestCase): })]), "1,2") - def test_switch_db_instance(self): + def ztest_switch_db_instance(self): register_connection('testdb-1', 'mongoenginetest2') class Group(Document): @@ -2310,7 +2326,7 @@ class InstanceTest(unittest.TestCase): group = Group.objects.first() self.assertEqual("hello - default", group.name) - def test_no_overwritting_no_data_loss(self): + def ztest_no_overwritting_no_data_loss(self): class User(Document): username = StringField(primary_key=True) @@ -2334,7 +2350,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual("Bar", user._data["foo"]) self.assertEqual([1, 2, 3], user._data["data"]) - def test_spaces_in_keys(self): + def ztest_spaces_in_keys(self): class Embedded(DynamicEmbeddedDocument): pass @@ -2350,7 +2366,7 @@ class InstanceTest(unittest.TestCase): one = Doc.objects.filter(**{'hello world': 1}).count() self.assertEqual(1, one) - def test_shard_key(self): + def ztest_shard_key(self): class LogEntry(Document): machine = StringField() log = StringField() @@ -2375,7 +2391,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(OperationError, change_shard_key) - def test_shard_key_primary(self): + def ztest_shard_key_primary(self): class LogEntry(Document): machine = StringField(primary_key=True) log = StringField() @@ -2400,7 +2416,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(OperationError, change_shard_key) - def test_kwargs_simple(self): + def ztest_kwargs_simple(self): class Embedded(EmbeddedDocument): name = StringField() @@ -2416,7 +2432,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(classic_doc, dict_doc) self.assertEqual(classic_doc._data, dict_doc._data) - def test_kwargs_complex(self): + def ztest_kwargs_complex(self): class Embedded(EmbeddedDocument): name = StringField() @@ -2435,21 +2451,21 @@ class InstanceTest(unittest.TestCase): self.assertEqual(classic_doc, dict_doc) self.assertEqual(classic_doc._data, dict_doc._data) - def test_positional_creation(self): + def ztest_positional_creation(self): """Ensure that document may be created using positional arguments. """ person = self.Person("Test User", 42) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) - def test_mixed_creation(self): + def ztest_mixed_creation(self): """Ensure that document may be created using mixed arguments. """ person = self.Person("Test User", age=42) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) - def test_mixed_creation_dynamic(self): + def ztest_mixed_creation_dynamic(self): """Ensure that document may be created using mixed arguments. """ class Person(DynamicDocument): @@ -2459,7 +2475,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) - def test_bad_mixed_creation(self): + def ztest_bad_mixed_creation(self): """Ensure that document gives correct error when duplicating arguments """ def construct_bad_instance(): @@ -2467,7 +2483,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(TypeError, construct_bad_instance) - def test_data_contains_id_field(self): + def ztest_data_contains_id_field(self): """Ensure that asking for _data returns 'id' """ class Person(Document): @@ -2480,7 +2496,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue('id' in person._data.keys()) self.assertEqual(person._data.get('id'), person.id) - def test_complex_nesting_document_and_embedded_document(self): + def ztest_complex_nesting_document_and_embedded_document(self): class Macro(EmbeddedDocument): value = DynamicField(default="UNDEFINED") @@ -2521,7 +2537,7 @@ class InstanceTest(unittest.TestCase): system = NodesSystem.objects.first() self.assertEqual("UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) - def test_embedded_document_equality(self): + def ztest_embedded_document_equality(self): class Test(Document): field = StringField(required=True) From de9ba12779c6e1b79ec0719463466f17c405d387 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 11:16:23 +0100 Subject: [PATCH 221/222] Turn on tests --- tests/document/instance.py | 186 ++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 81ecd29b..951871b6 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -50,7 +50,7 @@ class InstanceTest(unittest.TestCase): continue self.db.drop_collection(collection) - def ztest_capped_collection(self): + def test_capped_collection(self): """Ensure that capped collections work properly. """ class Log(Document): @@ -90,7 +90,7 @@ class InstanceTest(unittest.TestCase): Log.drop_collection() - def ztest_repr(self): + def test_repr(self): """Ensure that unicode representation works """ class Article(Document): @@ -103,7 +103,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('', repr(doc)) - def ztest_queryset_resurrects_dropped_collection(self): + def test_queryset_resurrects_dropped_collection(self): self.Person.drop_collection() self.assertEqual([], list(self.Person.objects())) @@ -116,7 +116,7 @@ class InstanceTest(unittest.TestCase): self.Person.drop_collection() self.assertEqual([], list(Actor.objects())) - def ztest_polymorphic_references(self): + def test_polymorphic_references(self): """Ensure that the correct subclasses are returned from a query when using references / generic references """ @@ -163,7 +163,7 @@ class InstanceTest(unittest.TestCase): Zoo.drop_collection() Animal.drop_collection() - def ztest_reference_inheritance(self): + def test_reference_inheritance(self): class Stats(Document): created = DateTimeField(default=datetime.now) @@ -188,7 +188,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(list_stats, CompareStats.objects.first().stats) - def ztest_db_field_load(self): + def test_db_field_load(self): """Ensure we load data correctly """ class Person(Document): @@ -208,7 +208,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Person.objects.get(name="Jack").rank, "Corporal") self.assertEqual(Person.objects.get(name="Fred").rank, "Private") - def ztest_db_embedded_doc_field_load(self): + def test_db_embedded_doc_field_load(self): """Ensure we load embedded document data correctly """ class Rank(EmbeddedDocument): @@ -234,7 +234,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Person.objects.get(name="Jack").rank, "Corporal") self.assertEqual(Person.objects.get(name="Fred").rank, "Private") - def ztest_custom_id_field(self): + def test_custom_id_field(self): """Ensure that documents may be created with custom primary keys. """ class User(Document): @@ -286,7 +286,7 @@ class InstanceTest(unittest.TestCase): User.drop_collection() - def ztest_document_not_registered(self): + def test_document_not_registered(self): class Place(Document): name = StringField() @@ -310,7 +310,7 @@ class InstanceTest(unittest.TestCase): print Place.objects.all() self.assertRaises(NotRegistered, query_without_importing_nice_place) - def ztest_document_registry_regressions(self): + def test_document_registry_regressions(self): class Location(Document): name = StringField() @@ -324,14 +324,14 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Area, get_document("Area")) self.assertEqual(Area, get_document("Location.Area")) - def ztest_creation(self): + def test_creation(self): """Ensure that document may be created using keyword arguments. """ person = self.Person(name="Test User", age=30) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 30) - def ztest_to_dbref(self): + def test_to_dbref(self): """Ensure that you can get a dbref of a document""" person = self.Person(name="Test User", age=30) self.assertRaises(OperationError, person.to_dbref) @@ -365,7 +365,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, "Mr Test User") self.assertEqual(person.age, 21) - def ztest_reload_sharded(self): + def test_reload_sharded(self): class Animal(Document): superphylum = StringField() meta = {'shard_key': ('superphylum',)} @@ -376,7 +376,7 @@ class InstanceTest(unittest.TestCase): doc.reload() Animal.drop_collection() - def ztest_reload_referencing(self): + def test_reload_referencing(self): """Ensures reloading updates weakrefs correctly """ class Embedded(EmbeddedDocument): @@ -428,7 +428,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(len(doc.embedded_field.list_field), 4) self.assertEqual(len(doc.embedded_field.dict_field), 2) - def ztest_reload_doesnt_exist(self): + def test_reload_doesnt_exist(self): class Foo(Document): pass @@ -449,7 +449,7 @@ class InstanceTest(unittest.TestCase): except Exception as ex: self.assertFalse("Threw wrong exception") - def ztest_dictionary_access(self): + def test_dictionary_access(self): """Ensure that dictionary-style field access works properly. """ person = self.Person(name='Test User', age=30) @@ -469,7 +469,7 @@ class InstanceTest(unittest.TestCase): self.assertFalse('age' in person) self.assertFalse('nationality' in person) - def ztest_embedded_document_to_mongo(self): + def test_embedded_document_to_mongo(self): class Person(EmbeddedDocument): name = StringField() age = IntField() @@ -484,14 +484,14 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(), ['_cls', 'name', 'age', 'salary']) - def ztest_embedded_document_to_mongo_id(self): + def test_embedded_document_to_mongo_id(self): class SubDoc(EmbeddedDocument): id = StringField(required=True) sub_doc = SubDoc(id="abc") self.assertEqual(sub_doc.to_mongo().keys(), ['id']) - def ztest_embedded_document(self): + def test_embedded_document(self): """Ensure that embedded documents are set up correctly. """ class Comment(EmbeddedDocument): @@ -500,7 +500,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue('content' in Comment._fields) self.assertFalse('id' in Comment._fields) - def ztest_embedded_document_instance(self): + def test_embedded_document_instance(self): """Ensure that embedded documents can reference parent instance """ class Embedded(EmbeddedDocument): @@ -515,7 +515,7 @@ class InstanceTest(unittest.TestCase): doc = Doc.objects.get() self.assertEqual(doc, doc.embedded_field._instance) - def ztest_embedded_document_complex_instance(self): + def test_embedded_document_complex_instance(self): """Ensure that embedded documents in complex fields can reference parent instance""" class Embedded(EmbeddedDocument): @@ -530,7 +530,7 @@ class InstanceTest(unittest.TestCase): doc = Doc.objects.get() self.assertEqual(doc, doc.embedded_field[0]._instance) - def ztest_instance_is_set_on_setattr(self): + def test_instance_is_set_on_setattr(self): class Email(EmbeddedDocument): email = EmailField() @@ -547,7 +547,7 @@ class InstanceTest(unittest.TestCase): acc1 = Account.objects.first() self.assertTrue(hasattr(acc1._data["email"], "_instance")) - def ztest_document_clean(self): + def test_document_clean(self): class TestDocument(Document): status = StringField() pub_date = DateTimeField() @@ -581,7 +581,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(type(t.pub_date), datetime) - def ztest_document_embedded_clean(self): + def test_document_embedded_clean(self): class TestEmbeddedDocument(EmbeddedDocument): x = IntField(required=True) y = IntField(required=True) @@ -617,7 +617,7 @@ class InstanceTest(unittest.TestCase): t = TestDocument(doc=TestEmbeddedDocument(x=15, y=35, z=5)) t.save(clean=False) - def ztest_save(self): + def test_save(self): """Ensure that a document may be saved in the database. """ # Create person object and save it to the database @@ -642,7 +642,7 @@ class InstanceTest(unittest.TestCase): except ValidationError: self.fail() - def ztest_save_to_a_value_that_equates_to_false(self): + def test_save_to_a_value_that_equates_to_false(self): class Thing(EmbeddedDocument): count = IntField() @@ -662,7 +662,7 @@ class InstanceTest(unittest.TestCase): user.reload() self.assertEqual(user.thing.count, 0) - def ztest_save_max_recursion_not_hit(self): + def test_save_max_recursion_not_hit(self): class Person(Document): name = StringField() @@ -688,7 +688,7 @@ class InstanceTest(unittest.TestCase): p0.name = 'wpjunior' p0.save() - def ztest_save_max_recursion_not_hit_with_file_field(self): + def test_save_max_recursion_not_hit_with_file_field(self): class Foo(Document): name = StringField() @@ -712,7 +712,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(b.picture, b.bar.picture, b.bar.bar.picture) - def ztest_save_cascades(self): + def test_save_cascades(self): class Person(Document): name = StringField() @@ -735,7 +735,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) - def ztest_save_cascade_kwargs(self): + def test_save_cascade_kwargs(self): class Person(Document): name = StringField() @@ -756,7 +756,7 @@ class InstanceTest(unittest.TestCase): p2.reload() self.assertEqual(p1.name, p2.parent.name) - def ztest_save_cascade_meta_false(self): + def test_save_cascade_meta_false(self): class Person(Document): name = StringField() @@ -785,7 +785,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) - def ztest_save_cascade_meta_true(self): + def test_save_cascade_meta_true(self): class Person(Document): name = StringField() @@ -810,7 +810,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertNotEqual(p1.name, p.parent.name) - def ztest_save_cascades_generically(self): + def test_save_cascades_generically(self): class Person(Document): name = StringField() @@ -836,7 +836,7 @@ class InstanceTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) - def ztest_save_atomicity_condition(self): + def test_save_atomicity_condition(self): class Widget(Document): toggle = BooleanField(default=False) @@ -910,7 +910,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue(w1.toggle) self.assertEqual(w1.count, 3) - def ztest_update(self): + def test_update(self): """Ensure that an existing document is updated instead of be overwritten.""" # Create person object and save it to the database @@ -994,7 +994,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, None) self.assertEqual(person.age, None) - def ztest_inserts_if_you_set_the_pk(self): + def test_inserts_if_you_set_the_pk(self): p1 = self.Person(name='p1', id=bson.ObjectId()).save() p2 = self.Person(name='p2') p2.id = bson.ObjectId() @@ -1002,7 +1002,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(2, self.Person.objects.count()) - def ztest_can_save_if_not_included(self): + def test_can_save_if_not_included(self): class EmbeddedDoc(EmbeddedDocument): pass @@ -1051,7 +1051,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(my_doc.string_field, "string") self.assertEqual(my_doc.int_field, 1) - def ztest_document_update(self): + def test_document_update(self): def update_not_saved_raises(): person = self.Person(name='dcrosta') @@ -1080,7 +1080,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidQueryError, update_no_op_raises) - def ztest_update_unique_field(self): + def test_update_unique_field(self): class Doc(Document): name = StringField(unique=True) @@ -1090,7 +1090,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(NotUniqueError, lambda: doc2.update(set__name=doc1.name)) - def ztest_embedded_update(self): + def test_embedded_update(self): """ Test update on `EmbeddedDocumentField` fields """ @@ -1114,7 +1114,7 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") - def ztest_embedded_update_db_field(self): + def test_embedded_update_db_field(self): """ Test update on `EmbeddedDocumentField` fields when db_field is other than default. @@ -1141,7 +1141,7 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") - def ztest_save_only_changed_fields(self): + def test_save_only_changed_fields(self): """Ensure save only sets / unsets changed fields """ @@ -1170,7 +1170,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.age, 21) self.assertEqual(person.active, False) - def ztest_query_count_when_saving(self): + def test_query_count_when_saving(self): """Ensure references don't cause extra fetches when saving""" class Organization(Document): name = StringField() @@ -1263,7 +1263,7 @@ class InstanceTest(unittest.TestCase): sub.save() self.assertEqual(q, 3) - def ztest_set_unset_one_operation(self): + def test_set_unset_one_operation(self): """Ensure that $set and $unset actions are performed in the same operation. """ @@ -1285,7 +1285,7 @@ class InstanceTest(unittest.TestCase): foo.save() self.assertEqual(1, q) - def ztest_save_only_changed_fields_recursive(self): + def test_save_only_changed_fields_recursive(self): """Ensure save only sets / unsets changed fields """ @@ -1327,7 +1327,7 @@ class InstanceTest(unittest.TestCase): person = self.Person.objects.get() self.assertFalse(person.comments_dict['first_post'].published) - def ztest_delete(self): + def test_delete(self): """Ensure that document may be deleted using the delete method. """ person = self.Person(name="Test User", age=30) @@ -1336,7 +1336,7 @@ class InstanceTest(unittest.TestCase): person.delete() self.assertEqual(self.Person.objects.count(), 0) - def ztest_save_custom_id(self): + def test_save_custom_id(self): """Ensure that a document may be saved with a custom _id. """ # Create person object and save it to the database @@ -1348,7 +1348,7 @@ class InstanceTest(unittest.TestCase): person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') - def ztest_save_custom_pk(self): + def test_save_custom_pk(self): """Ensure that a document may be saved with a custom _id using pk alias. """ # Create person object and save it to the database @@ -1360,7 +1360,7 @@ class InstanceTest(unittest.TestCase): person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') - def ztest_save_list(self): + def test_save_list(self): """Ensure that a list field may be properly saved. """ class Comment(EmbeddedDocument): @@ -1387,7 +1387,7 @@ class InstanceTest(unittest.TestCase): BlogPost.drop_collection() - def ztest_list_search_by_embedded(self): + def test_list_search_by_embedded(self): class User(Document): username = StringField(required=True) @@ -1439,7 +1439,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual([p1, p2, p4], list(Page.objects.filter(comments__user=u2))) self.assertEqual([p1, p3], list(Page.objects.filter(comments__user=u3))) - def ztest_save_embedded_document(self): + def test_save_embedded_document(self): """Ensure that a document with an embedded document field may be saved in the database. """ @@ -1463,7 +1463,7 @@ class InstanceTest(unittest.TestCase): # Ensure that the 'details' embedded object saved correctly self.assertEqual(employee_obj['details']['position'], 'Developer') - def ztest_embedded_update_after_save(self): + def test_embedded_update_after_save(self): """ Test update of `EmbeddedDocumentField` attached to a newly saved document. @@ -1486,7 +1486,7 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") - def ztest_updating_an_embedded_document(self): + def test_updating_an_embedded_document(self): """Ensure that a document with an embedded document field may be saved in the database. """ @@ -1521,7 +1521,7 @@ class InstanceTest(unittest.TestCase): promoted_employee.reload() self.assertEqual(promoted_employee.details, None) - def ztest_object_mixins(self): + def test_object_mixins(self): class NameMixin(object): name = StringField() @@ -1536,7 +1536,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(['id', 'name', 'widgets'], sorted(Bar._fields.keys())) - def ztest_mixin_inheritance(self): + def test_mixin_inheritance(self): class BaseMixIn(object): count = IntField() data = StringField() @@ -1560,7 +1560,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(t.data, "test") self.assertEqual(t.count, 12) - def ztest_save_reference(self): + def test_save_reference(self): """Ensure that a document reference field may be saved in the database. """ @@ -1596,7 +1596,7 @@ class InstanceTest(unittest.TestCase): BlogPost.drop_collection() - def ztest_duplicate_db_fields_raise_invalid_document_error(self): + def test_duplicate_db_fields_raise_invalid_document_error(self): """Ensure a InvalidDocumentError is thrown if duplicate fields declare the same db_field""" @@ -1607,7 +1607,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, throw_invalid_document_error) - def ztest_invalid_son(self): + def test_invalid_son(self): """Raise an error if loading invalid data""" class Occurrence(EmbeddedDocument): number = IntField() @@ -1624,7 +1624,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, raise_invalid_document) - def ztest_reverse_delete_rule_cascade_and_nullify(self): + def test_reverse_delete_rule_cascade_and_nullify(self): """Ensure that a referenced document is also deleted upon deletion. """ @@ -1655,7 +1655,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) - def ztest_reverse_delete_rule_with_document_inheritance(self): + def test_reverse_delete_rule_with_document_inheritance(self): """Ensure that a referenced document is also deleted upon deletion of a child document. """ @@ -1690,7 +1690,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) - def ztest_reverse_delete_rule_cascade_and_nullify_complex_field(self): + def test_reverse_delete_rule_cascade_and_nullify_complex_field(self): """Ensure that a referenced document is also deleted upon deletion for complex fields. """ @@ -1724,7 +1724,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) - def ztest_reverse_delete_rule_cascade_triggers_pre_delete_signal(self): + def test_reverse_delete_rule_cascade_triggers_pre_delete_signal(self): ''' ensure the pre_delete signal is triggered upon a cascading deletion setup a blog post with content, an author and editor delete the author which triggers deletion of blogpost via cascade @@ -1760,7 +1760,7 @@ class InstanceTest(unittest.TestCase): editor = Editor.objects(name='Max P.').get() self.assertEqual(editor.review_queue, 0) - def ztest_two_way_reverse_delete_rule(self): + def test_two_way_reverse_delete_rule(self): """Ensure that Bi-Directional relationships work with reverse_delete_rule """ @@ -1793,7 +1793,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Bar.objects.count(), 1) # No effect on the BlogPost self.assertEqual(Bar.objects.get().foo, None) - def ztest_invalid_reverse_delete_rules_raise_errors(self): + def test_invalid_reverse_delete_rules_raise_errors(self): def throw_invalid_document_error(): class Blog(Document): @@ -1810,7 +1810,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, throw_invalid_document_error_embedded) - def ztest_reverse_delete_rule_cascade_recurs(self): + def test_reverse_delete_rule_cascade_recurs(self): """Ensure that a chain of documents is also deleted upon cascaded deletion. """ @@ -1847,7 +1847,7 @@ class InstanceTest(unittest.TestCase): BlogPost.drop_collection() Comment.drop_collection() - def ztest_reverse_delete_rule_deny(self): + def test_reverse_delete_rule_deny(self): """Ensure that a document cannot be referenced if there are still documents referring to it. """ @@ -1902,7 +1902,7 @@ class InstanceTest(unittest.TestCase): A.drop_collection() B.drop_collection() - def ztest_document_hash(self): + def test_document_hash(self): """Test document in list, dict, set """ class User(Document): @@ -1950,7 +1950,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue(u1 in all_user_set) - def ztest_picklable(self): + def test_picklable(self): pickle_doc = PickleTest(number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() @@ -1976,7 +1976,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(pickle_doc.string, "Two") self.assertEqual(pickle_doc.lists, ["1", "2", "3"]) - def ztest_dynamic_document_pickle(self): + def test_dynamic_document_pickle(self): pickle_doc = PickleDynamicTest(name="test", number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar") @@ -1999,13 +1999,13 @@ class InstanceTest(unittest.TestCase): self.assertEqual(resurrected.embedded._dynamic_fields.keys(), pickle_doc.embedded._dynamic_fields.keys()) - def ztest_picklable_on_signals(self): + def test_picklable_on_signals(self): pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() pickle_doc.save() pickle_doc.delete() - def ztest_throw_invalid_document_error(self): + def test_throw_invalid_document_error(self): # test handles people trying to upsert def throw_invalid_document_error(): @@ -2014,7 +2014,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(InvalidDocumentError, throw_invalid_document_error) - def ztest_mutating_documents(self): + def test_mutating_documents(self): class B(EmbeddedDocument): field1 = StringField(default='field1') @@ -2045,7 +2045,7 @@ class InstanceTest(unittest.TestCase): a.reload() self.assertEqual(a.b.field2.c_field, 'new value') - def ztest_can_save_false_values(self): + def test_can_save_false_values(self): """Ensures you can save False values on save""" class Doc(Document): foo = StringField() @@ -2059,7 +2059,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Doc.objects(archived=False).count(), 1) - def ztest_can_save_false_values_dynamic(self): + def test_can_save_false_values_dynamic(self): """Ensures you can save False values on dynamic docs""" class Doc(DynamicDocument): foo = StringField() @@ -2072,7 +2072,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Doc.objects(archived=False).count(), 1) - def ztest_do_not_save_unchanged_references(self): + def test_do_not_save_unchanged_references(self): """Ensures cascading saves dont auto update""" class Job(Document): name = StringField() @@ -2103,7 +2103,7 @@ class InstanceTest(unittest.TestCase): finally: Collection.update = orig_update - def ztest_db_alias_tests(self): + def test_db_alias_tests(self): """ DB Alias tests """ # mongoenginetest - Is default connection alias from setUp() # Register Aliases @@ -2159,7 +2159,7 @@ class InstanceTest(unittest.TestCase): 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 ztest_db_alias_overrides(self): + def test_db_alias_overrides(self): """db_alias can be overriden """ # Register a connection with db_alias testdb-2 @@ -2184,7 +2184,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('mongoenginetest2', B._get_collection().database.name) - def ztest_db_alias_propagates(self): + def test_db_alias_propagates(self): """db_alias propagates? """ register_connection('testdb-1', 'mongoenginetest2') @@ -2198,7 +2198,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('testdb-1', B._meta.get('db_alias')) - def ztest_db_ref_usage(self): + def test_db_ref_usage(self): """ DB Ref usage in dict_fields""" class User(Document): @@ -2276,7 +2276,7 @@ class InstanceTest(unittest.TestCase): })]), "1,2") - def ztest_switch_db_instance(self): + def test_switch_db_instance(self): register_connection('testdb-1', 'mongoenginetest2') class Group(Document): @@ -2326,7 +2326,7 @@ class InstanceTest(unittest.TestCase): group = Group.objects.first() self.assertEqual("hello - default", group.name) - def ztest_no_overwritting_no_data_loss(self): + def test_no_overwritting_no_data_loss(self): class User(Document): username = StringField(primary_key=True) @@ -2350,7 +2350,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual("Bar", user._data["foo"]) self.assertEqual([1, 2, 3], user._data["data"]) - def ztest_spaces_in_keys(self): + def test_spaces_in_keys(self): class Embedded(DynamicEmbeddedDocument): pass @@ -2366,7 +2366,7 @@ class InstanceTest(unittest.TestCase): one = Doc.objects.filter(**{'hello world': 1}).count() self.assertEqual(1, one) - def ztest_shard_key(self): + def test_shard_key(self): class LogEntry(Document): machine = StringField() log = StringField() @@ -2391,7 +2391,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(OperationError, change_shard_key) - def ztest_shard_key_primary(self): + def test_shard_key_primary(self): class LogEntry(Document): machine = StringField(primary_key=True) log = StringField() @@ -2416,7 +2416,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(OperationError, change_shard_key) - def ztest_kwargs_simple(self): + def test_kwargs_simple(self): class Embedded(EmbeddedDocument): name = StringField() @@ -2432,7 +2432,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(classic_doc, dict_doc) self.assertEqual(classic_doc._data, dict_doc._data) - def ztest_kwargs_complex(self): + def test_kwargs_complex(self): class Embedded(EmbeddedDocument): name = StringField() @@ -2451,21 +2451,21 @@ class InstanceTest(unittest.TestCase): self.assertEqual(classic_doc, dict_doc) self.assertEqual(classic_doc._data, dict_doc._data) - def ztest_positional_creation(self): + def test_positional_creation(self): """Ensure that document may be created using positional arguments. """ person = self.Person("Test User", 42) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) - def ztest_mixed_creation(self): + def test_mixed_creation(self): """Ensure that document may be created using mixed arguments. """ person = self.Person("Test User", age=42) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) - def ztest_mixed_creation_dynamic(self): + def test_mixed_creation_dynamic(self): """Ensure that document may be created using mixed arguments. """ class Person(DynamicDocument): @@ -2475,7 +2475,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) - def ztest_bad_mixed_creation(self): + def test_bad_mixed_creation(self): """Ensure that document gives correct error when duplicating arguments """ def construct_bad_instance(): @@ -2483,7 +2483,7 @@ class InstanceTest(unittest.TestCase): self.assertRaises(TypeError, construct_bad_instance) - def ztest_data_contains_id_field(self): + def test_data_contains_id_field(self): """Ensure that asking for _data returns 'id' """ class Person(Document): @@ -2496,7 +2496,7 @@ class InstanceTest(unittest.TestCase): self.assertTrue('id' in person._data.keys()) self.assertEqual(person._data.get('id'), person.id) - def ztest_complex_nesting_document_and_embedded_document(self): + def test_complex_nesting_document_and_embedded_document(self): class Macro(EmbeddedDocument): value = DynamicField(default="UNDEFINED") @@ -2537,7 +2537,7 @@ class InstanceTest(unittest.TestCase): system = NodesSystem.objects.first() self.assertEqual("UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) - def ztest_embedded_document_equality(self): + def test_embedded_document_equality(self): class Test(Document): field = StringField(required=True) From 89b9b60e0c218654091b34e6139f1c44f1368b08 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 27 Jun 2014 11:27:10 +0100 Subject: [PATCH 222/222] Geo SON tweaks --- mongoengine/queryset/transform.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 8e88e9fe..d72d97a3 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -117,19 +117,24 @@ 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 and - isinstance(value_dict['$near'], dict)): - + if ('$maxDistance' in value_dict and '$near' in value_dict): value_son = SON() - 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'] + 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'] else: - value_son['$near'] = SON(value_son['$near']) - value_son['$near']['$maxDistance'] = value_dict['$maxDistance'] + for k, v in value_dict.iteritems(): + if k == '$maxDistance': + continue + value_son[k] = v + value_son['$maxDistance'] = value_dict['$maxDistance'] mongo_query[key] = value_son else: