diff --git a/MANIFEST.in b/MANIFEST.in index 1592c66c..6ceae640 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include README.rst include LICENSE recursive-include docs * -prune docs/_build/* +prune docs/_build recursive-include tests * recursive-exclude * *.pyc *.swp diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 84ee6acf..3b700322 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -142,8 +142,8 @@ A :class:`~mongoengine.queryset.Q` object represents part of a query, and can be initialised using the same keyword-argument syntax you use to query documents. To build a complex query, you may combine :class:`~mongoengine.queryset.Q` objects using the ``&`` (and) and ``|`` (or) -operators. To use :class:`~mongoengine.queryset.Q` objects, pass them in -as positional arguments to :attr:`Document.objects` when you filter it by +operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the +first positional argument to :attr:`Document.objects` when you filter it by calling it with keyword arguments:: # Get published posts diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 06c82cf9..bb0090ea 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -111,7 +111,7 @@ class QuerySet(object): self._collection_obj = collection self._accessed_collection = False self._query = {} - self._where_clauses = [] + self._where_clause = None # If inheritance is allowed, only return instances and instances of # subclasses of the class being used @@ -165,16 +165,18 @@ class QuerySet(object): return index_list - def __call__(self, *q_objs, **query): + def __call__(self, q_obj=None, **query): """Filter the selected documents by calling the :class:`~mongoengine.queryset.QuerySet` with a query. - :param q_objs: :class:`~mongoengine.queryset.Q` objects to be used in - the 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 query: Django-style query keyword arguments """ - for q in q_objs: - self._where_clauses.append(q.as_js(self._document)) + if q_obj: + self._where_clause = q_obj.as_js(self._document) query = QuerySet._transform_query(_doc_cls=self._document, **query) self._query.update(query) return self @@ -209,11 +211,11 @@ class QuerySet(object): @property def _cursor(self): - if not self._cursor_obj: + if self._cursor_obj is None: self._cursor_obj = self._collection.find(self._query) # Apply where clauses to cursor - for js in self._where_clauses: - self._cursor_obj.where(js) + if self._where_clause: + self._cursor_obj.where(self._where_clause) # apply default ordering if self._document._meta['ordering']: @@ -516,11 +518,17 @@ class QuerySet(object): fields = [QuerySet._translate_field_name(self._document, f) for f in fields] collection = self._document._meta['collection'] + scope = { 'collection': collection, - 'query': self._query, 'options': options or {}, } + + query = self._query + if self._where_clause: + query['$where'] = self._where_clause + + scope['query'] = query code = pymongo.code.Code(code, scope=scope) db = _get_db() diff --git a/tests/document.py b/tests/document.py index 317472ef..0c0b220b 100644 --- a/tests/document.py +++ b/tests/document.py @@ -240,7 +240,7 @@ class DocumentTest(unittest.TestCase): info = BlogPost.objects._collection.index_information() # _id, types, '-date', 'tags', ('cat', 'date') - self.assertEqual(len(info), 5) + self.assertEqual(len(info), 5) # Indexes are lazy so use list() to perform query list(BlogPost.objects) diff --git a/tests/queryset.py b/tests/queryset.py index 93dc0747..10507e00 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -264,6 +264,50 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + def test_exec_js_query(self): + """Ensure that queries are properly formed for use in exec_js. + """ + class BlogPost(Document): + hits = IntField() + published = BooleanField() + + BlogPost.drop_collection() + + post1 = BlogPost(hits=1, published=False) + post1.save() + + post2 = BlogPost(hits=1, published=True) + post2.save() + + post3 = BlogPost(hits=1, published=True) + post3.save() + + js_func = """ + function(hitsField) { + var count = 0; + db[collection].find(query).forEach(function(doc) { + count += doc[hitsField]; + }); + return count; + } + """ + + # Ensure that normal queries work + c = BlogPost.objects(published=True).exec_js(js_func, 'hits') + self.assertEqual(c, 2) + + c = BlogPost.objects(published=False).exec_js(js_func, 'hits') + self.assertEqual(c, 1) + + # Ensure that Q object queries work + c = BlogPost.objects(Q(published=True)).exec_js(js_func, 'hits') + self.assertEqual(c, 2) + + c = BlogPost.objects(Q(published=False)).exec_js(js_func, 'hits') + self.assertEqual(c, 1) + + BlogPost.drop_collection() + def test_delete(self): """Ensure that documents are properly deleted from the database. """