Merge branch 'regex-query-shortcuts'
This commit is contained in:
commit
5e2c5fa97b
@ -68,6 +68,16 @@ Available operators are as follows:
|
|||||||
* ``size`` -- the size of the array is
|
* ``size`` -- the size of the array is
|
||||||
* ``exists`` -- value for field exists
|
* ``exists`` -- value for field exists
|
||||||
|
|
||||||
|
The following operators are available as shortcuts to querying with regular
|
||||||
|
expressions:
|
||||||
|
|
||||||
|
* ``contains`` -- string field contains value
|
||||||
|
* ``icontains`` -- string field contains value (case insensitive)
|
||||||
|
* ``startswith`` -- string field starts with value
|
||||||
|
* ``istartswith`` -- string field starts with value (case insensitive)
|
||||||
|
* ``endswith`` -- string field ends with value
|
||||||
|
* ``iendswith`` -- string field ends with value (case insensitive)
|
||||||
|
|
||||||
Limiting and skipping results
|
Limiting and skipping results
|
||||||
=============================
|
=============================
|
||||||
Just as with traditional ORMs, you may limit the number of results returned, or
|
Just as with traditional ORMs, you may limit the number of results returned, or
|
||||||
@ -232,6 +242,109 @@ calling it with keyword arguments::
|
|||||||
natively supported by MongoDB -- they are compiled to Javascript and sent
|
natively supported by MongoDB -- they are compiled to Javascript and sent
|
||||||
to the server for execution.
|
to the server for execution.
|
||||||
|
|
||||||
|
Server-side javascript execution
|
||||||
|
================================
|
||||||
|
Javascript functions may be written and sent to the server for execution. The
|
||||||
|
result of this is the return value of the Javascript function. This
|
||||||
|
functionality is accessed through the
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.exec_js` method on
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet` objects. Pass in a string containing a
|
||||||
|
Javascript function as the first argument.
|
||||||
|
|
||||||
|
The remaining positional arguments are names of fields that will be passed into
|
||||||
|
you Javascript function as its arguments. This allows functions to be written
|
||||||
|
that may be executed on any field in a collection (e.g. the
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.sum` method, which accepts the name of
|
||||||
|
the field to sum over as its argument). Note that field names passed in in this
|
||||||
|
manner are automatically translated to the names used on the database (set
|
||||||
|
using the :attr:`name` keyword argument to a field constructor).
|
||||||
|
|
||||||
|
Keyword arguments to :meth:`~mongoengine.queryset.QuerySet.exec_js` are
|
||||||
|
combined into an object called :attr:`options`, which is available in the
|
||||||
|
Javascript function. This may be used for defining specific parameters for your
|
||||||
|
function.
|
||||||
|
|
||||||
|
Some variables are made available in the scope of the Javascript function:
|
||||||
|
|
||||||
|
* ``collection`` -- the name of the collection that corresponds to the
|
||||||
|
:class:`~mongoengine.Document` class that is being used; this should be
|
||||||
|
used to get the :class:`Collection` object from :attr:`db` in Javascript
|
||||||
|
code
|
||||||
|
* ``query`` -- the query that has been generated by the
|
||||||
|
:class:`~mongoengine.queryset.QuerySet` object; this may be passed into
|
||||||
|
the :meth:`find` method on a :class:`Collection` object in the Javascript
|
||||||
|
function
|
||||||
|
* ``options`` -- an object containing the keyword arguments passed into
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.exec_js`
|
||||||
|
|
||||||
|
The following example demonstrates the intended usage of
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.exec_js` by defining a function that sums
|
||||||
|
over a field on a document (this functionality is already available throught
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.sum` but is shown here for sake of
|
||||||
|
example)::
|
||||||
|
|
||||||
|
def sum_field(document, field_name, include_negatives=True):
|
||||||
|
code = """
|
||||||
|
function(sumField) {
|
||||||
|
var total = 0.0;
|
||||||
|
db[collection].find(query).forEach(function(doc) {
|
||||||
|
var val = doc[sumField];
|
||||||
|
if (val >= 0.0 || options.includeNegatives) {
|
||||||
|
total += val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
options = {'includeNegatives': include_negatives}
|
||||||
|
return document.objects.exec_js(code, field_name, **options)
|
||||||
|
|
||||||
|
As fields in MongoEngine may use different names in the database (set using the
|
||||||
|
:attr:`name` 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 on a collection object, use
|
||||||
|
square-bracket notation, and prefix the MongoEngine field name with a tilde.
|
||||||
|
The field name that follows the tilde will be translated to the name used in
|
||||||
|
the database. Note that when referring to fields on embedded documents,
|
||||||
|
the name of the :class:`~mongoengine.EmbeddedDocumentField`, followed by a dot,
|
||||||
|
should be used before the name of the field on the embedded document. The
|
||||||
|
following example shows how the substitutions are made::
|
||||||
|
|
||||||
|
class Comment(EmbeddedDocument):
|
||||||
|
content = StringField(name='body')
|
||||||
|
|
||||||
|
class BlogPost(Document):
|
||||||
|
title = StringField(name='doctitle')
|
||||||
|
comments = ListField(EmbeddedDocumentField(Comment), name='cs')
|
||||||
|
|
||||||
|
# Returns a list of dictionaries. Each dictionary contains a value named
|
||||||
|
# "document", which corresponds to the "title" field on a BlogPost, and
|
||||||
|
# "comment", which corresponds to an individual comment. The substitutions
|
||||||
|
# made are shown in the comments.
|
||||||
|
BlogPost.objects.exec_js("""
|
||||||
|
function() {
|
||||||
|
var comments = [];
|
||||||
|
db[collection].find(query).forEach(function(doc) {
|
||||||
|
// doc[~comments] -> doc["cs"]
|
||||||
|
var docComments = doc[~comments];
|
||||||
|
|
||||||
|
for (var i = 0; i < docComments.length; i++) {
|
||||||
|
// doc[~comments][i] -> doc["cs"][i]
|
||||||
|
var comment = doc[~comments][i];
|
||||||
|
|
||||||
|
comments.push({
|
||||||
|
// doc[~title] -> doc["doctitle"]
|
||||||
|
'document': doc[~title],
|
||||||
|
|
||||||
|
// comment[~comments.content] -> comment["body"]
|
||||||
|
'comment': comment[~comments.content]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
.. _guide-atomic-updates:
|
.. _guide-atomic-updates:
|
||||||
|
|
||||||
Atomic updates
|
Atomic updates
|
||||||
|
@ -39,8 +39,25 @@ class StringField(BaseField):
|
|||||||
def lookup_member(self, member_name):
|
def lookup_member(self, member_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def prepare_query_value(self, op, value):
|
||||||
|
if not isinstance(op, basestring):
|
||||||
|
return value
|
||||||
|
|
||||||
class URLField(BaseField):
|
if op.lstrip('i') in ('startswith', 'endswith', 'contains'):
|
||||||
|
flags = 0
|
||||||
|
if op.startswith('i'):
|
||||||
|
flags = re.IGNORECASE
|
||||||
|
op = op.lstrip('i')
|
||||||
|
|
||||||
|
regex = r'%s'
|
||||||
|
if op == 'startswith':
|
||||||
|
regex = r'^%s'
|
||||||
|
elif op == 'endswith':
|
||||||
|
regex = r'%s$'
|
||||||
|
value = re.compile(regex % value, flags)
|
||||||
|
return value
|
||||||
|
|
||||||
|
class URLField(StringField):
|
||||||
"""A field that validates input as a URL.
|
"""A field that validates input as a URL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from connection import _get_db
|
from connection import _get_db
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
|
import re
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
|
||||||
@ -27,6 +28,8 @@ class OperationError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
RE_TYPE = type(re.compile(''))
|
||||||
|
|
||||||
class Q(object):
|
class Q(object):
|
||||||
|
|
||||||
OR = '||'
|
OR = '||'
|
||||||
@ -46,6 +49,8 @@ class Q(object):
|
|||||||
'return this.%(field)s.indexOf(a) != -1 })'),
|
'return this.%(field)s.indexOf(a) != -1 })'),
|
||||||
'size': 'this.%(field)s.length == %(value)s',
|
'size': 'this.%(field)s.length == %(value)s',
|
||||||
'exists': 'this.%(field)s != null',
|
'exists': 'this.%(field)s != null',
|
||||||
|
'regex_eq': '%(value)s.test(this.%(field)s)',
|
||||||
|
'regex_ne': '!%(value)s.test(this.%(field)s)',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **query):
|
def __init__(self, **query):
|
||||||
@ -90,24 +95,41 @@ class Q(object):
|
|||||||
for j, (op, value) in enumerate(value.items()):
|
for j, (op, value) in enumerate(value.items()):
|
||||||
# Create a custom variable name for this operator
|
# Create a custom variable name for this operator
|
||||||
op_value_name = '%so%s' % (value_name, j)
|
op_value_name = '%so%s' % (value_name, j)
|
||||||
|
# Construct the JS that uses this op
|
||||||
|
value, operation_js = self._build_op_js(op, key, value,
|
||||||
|
op_value_name)
|
||||||
# Update the js scope with the value for this op
|
# Update the js scope with the value for this op
|
||||||
js_scope[op_value_name] = value
|
js_scope[op_value_name] = value
|
||||||
# Construct the JS that uses this op
|
|
||||||
operation_js = Q.OPERATORS[op.strip('$')] % {
|
|
||||||
'field': key,
|
|
||||||
'value': op_value_name
|
|
||||||
}
|
|
||||||
js.append(operation_js)
|
js.append(operation_js)
|
||||||
else:
|
else:
|
||||||
js_scope[value_name] = value
|
|
||||||
# Construct the JS for this field
|
# Construct the JS for this field
|
||||||
field_js = Q.OPERATORS[op.strip('$')] % {
|
value, field_js = self._build_op_js(op, key, value, value_name)
|
||||||
'field': key,
|
js_scope[value_name] = value
|
||||||
'value': value_name
|
|
||||||
}
|
|
||||||
js.append(field_js)
|
js.append(field_js)
|
||||||
return ' && '.join(js)
|
return ' && '.join(js)
|
||||||
|
|
||||||
|
def _build_op_js(self, op, key, value, value_name):
|
||||||
|
"""Substitute the values in to the correct chunk of Javascript.
|
||||||
|
"""
|
||||||
|
if isinstance(value, RE_TYPE):
|
||||||
|
# Regexes are handled specially
|
||||||
|
if op.strip('$') == 'ne':
|
||||||
|
op_js = Q.OPERATORS['regex_ne']
|
||||||
|
else:
|
||||||
|
op_js = Q.OPERATORS['regex_eq']
|
||||||
|
else:
|
||||||
|
op_js = Q.OPERATORS[op.strip('$')]
|
||||||
|
|
||||||
|
# Comparing two ObjectIds in Javascript doesn't work..
|
||||||
|
if isinstance(value, pymongo.objectid.ObjectId):
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
# Perform the substitution
|
||||||
|
operation_js = op_js % {
|
||||||
|
'field': key,
|
||||||
|
'value': value_name
|
||||||
|
}
|
||||||
|
return value, operation_js
|
||||||
|
|
||||||
class QuerySet(object):
|
class QuerySet(object):
|
||||||
"""A set of results returned from a query. Wraps a MongoDB cursor,
|
"""A set of results returned from a query. Wraps a MongoDB cursor,
|
||||||
@ -274,13 +296,15 @@ class QuerySet(object):
|
|||||||
"""
|
"""
|
||||||
operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
||||||
'all', 'size', 'exists']
|
'all', 'size', 'exists']
|
||||||
|
match_operators = ['contains', 'icontains', 'startswith',
|
||||||
|
'istartswith', 'endswith', 'iendswith']
|
||||||
|
|
||||||
mongo_query = {}
|
mongo_query = {}
|
||||||
for key, value in query.items():
|
for key, value in query.items():
|
||||||
parts = key.split('__')
|
parts = key.split('__')
|
||||||
# Check for an operator and transform to mongo-style if there is
|
# Check for an operator and transform to mongo-style if there is
|
||||||
op = None
|
op = None
|
||||||
if parts[-1] in operators:
|
if parts[-1] in operators + match_operators:
|
||||||
op = parts.pop()
|
op = parts.pop()
|
||||||
|
|
||||||
if _doc_cls:
|
if _doc_cls:
|
||||||
@ -290,13 +314,15 @@ class QuerySet(object):
|
|||||||
|
|
||||||
# Convert value to proper value
|
# Convert value to proper value
|
||||||
field = fields[-1]
|
field = fields[-1]
|
||||||
if op in (None, 'ne', 'gt', 'gte', 'lt', 'lte'):
|
singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte']
|
||||||
|
singular_ops += match_operators
|
||||||
|
if op in singular_ops:
|
||||||
value = field.prepare_query_value(op, value)
|
value = field.prepare_query_value(op, value)
|
||||||
elif op in ('in', 'nin', 'all'):
|
elif op in ('in', 'nin', 'all'):
|
||||||
# 'in', 'nin' and 'all' require a list of values
|
# 'in', 'nin' and 'all' require a list of values
|
||||||
value = [field.prepare_query_value(op, v) for v in value]
|
value = [field.prepare_query_value(op, v) for v in value]
|
||||||
|
|
||||||
if op:
|
if op and op not in match_operators:
|
||||||
value = {'$' + op: value}
|
value = {'$' + op: value}
|
||||||
|
|
||||||
key = '.'.join(parts)
|
key = '.'.join(parts)
|
||||||
@ -372,7 +398,7 @@ class QuerySet(object):
|
|||||||
def in_bulk(self, object_ids):
|
def in_bulk(self, object_ids):
|
||||||
"""Retrieve a set of documents by their ids.
|
"""Retrieve a set of documents by their ids.
|
||||||
|
|
||||||
:param object_ids: a list or tuple of ``ObjectId``s
|
:param object_ids: a list or tuple of ``ObjectId``\ s
|
||||||
:rtype: dict of ObjectIds as keys and collection-specific
|
:rtype: dict of ObjectIds as keys and collection-specific
|
||||||
Document subclasses as values.
|
Document subclasses as values.
|
||||||
"""
|
"""
|
||||||
@ -454,7 +480,7 @@ class QuerySet(object):
|
|||||||
|
|
||||||
post = BlogPost.objects(...).only("title")
|
post = BlogPost.objects(...).only("title")
|
||||||
|
|
||||||
:param *fields: fields to include
|
:param fields: fields to include
|
||||||
"""
|
"""
|
||||||
self._loaded_fields = []
|
self._loaded_fields = []
|
||||||
for field in fields:
|
for field in fields:
|
||||||
@ -604,6 +630,21 @@ class QuerySet(object):
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
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 = QuerySet._lookup_field(self._document, field_name)
|
||||||
|
# Substitute the correct name for the field into the javascript
|
||||||
|
return '["%s"]' % fields[-1].name
|
||||||
|
|
||||||
|
return re.sub('\[\s*~([A-z_][A-z_0-9.]+?)\s*\]', field_sub, code)
|
||||||
|
|
||||||
def exec_js(self, code, *fields, **options):
|
def exec_js(self, code, *fields, **options):
|
||||||
"""Execute a Javascript function on the server. A list of fields may be
|
"""Execute a Javascript function on the server. A list of fields may be
|
||||||
provided, which will be translated to their correct names and supplied
|
provided, which will be translated to their correct names and supplied
|
||||||
@ -613,12 +654,21 @@ class QuerySet(object):
|
|||||||
current query; and ``options``, which is an object containing any
|
current query; and ``options``, which is an object containing any
|
||||||
options specified as keyword arguments.
|
options specified as keyword arguments.
|
||||||
|
|
||||||
|
As fields in MongoEngine may use different names in the database (set
|
||||||
|
using the :attr:`name` 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 code: a string of Javascript code to execute
|
||||||
:param fields: fields that you will be using in your function, which
|
:param fields: fields that you will be using in your function, which
|
||||||
will be passed in to your function as arguments
|
will be passed in to your function as arguments
|
||||||
:param options: options that you want available to the function
|
:param options: options that you want available to the function
|
||||||
(accessed in Javascript through the ``options`` object)
|
(accessed in Javascript through the ``options`` object)
|
||||||
"""
|
"""
|
||||||
|
code = self._sub_js_fields(code)
|
||||||
|
|
||||||
fields = [QuerySet._translate_field_name(self._document, f)
|
fields = [QuerySet._translate_field_name(self._document, f)
|
||||||
for f in fields]
|
for f in fields]
|
||||||
collection = self._document._meta['collection']
|
collection = self._document._meta['collection']
|
||||||
|
@ -186,6 +186,59 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
person = self.Person.objects.get(age=50)
|
person = self.Person.objects.get(age=50)
|
||||||
self.assertEqual(person.name, "User C")
|
self.assertEqual(person.name, "User C")
|
||||||
|
|
||||||
|
def test_regex_query_shortcuts(self):
|
||||||
|
"""Ensure that contains, startswith, endswith, etc work.
|
||||||
|
"""
|
||||||
|
person = self.Person(name='Guido van Rossum')
|
||||||
|
person.save()
|
||||||
|
|
||||||
|
# Test contains
|
||||||
|
obj = self.Person.objects(name__contains='van').first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(name__contains='Van').first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
obj = self.Person.objects(Q(name__contains='van')).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__contains='Van')).first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
|
||||||
|
# Test icontains
|
||||||
|
obj = self.Person.objects(name__icontains='Van').first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__icontains='Van')).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
|
||||||
|
# Test startswith
|
||||||
|
obj = self.Person.objects(name__startswith='Guido').first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(name__startswith='guido').first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
obj = self.Person.objects(Q(name__startswith='Guido')).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__startswith='guido')).first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
|
||||||
|
# Test istartswith
|
||||||
|
obj = self.Person.objects(name__istartswith='guido').first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__istartswith='guido')).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
|
||||||
|
# Test endswith
|
||||||
|
obj = self.Person.objects(name__endswith='Rossum').first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(name__endswith='rossuM').first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
obj = self.Person.objects(Q(name__endswith='Rossum')).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__endswith='rossuM')).first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
|
||||||
|
# Test iendswith
|
||||||
|
obj = self.Person.objects(name__iendswith='rossuM').first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__iendswith='rossuM')).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
|
||||||
def test_filter_chaining(self):
|
def test_filter_chaining(self):
|
||||||
"""Ensure filters can be chained together.
|
"""Ensure filters can be chained together.
|
||||||
@ -356,6 +409,11 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
post6 = BlogPost(published=False)
|
post6 = BlogPost(published=False)
|
||||||
post6.save()
|
post6.save()
|
||||||
|
|
||||||
|
# Check ObjectId lookup works
|
||||||
|
obj = BlogPost.objects(id=post1.id).first()
|
||||||
|
self.assertEqual(obj, post1)
|
||||||
|
|
||||||
|
# Check Q object combination
|
||||||
date = datetime(2010, 1, 10)
|
date = datetime(2010, 1, 10)
|
||||||
q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True))
|
q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True))
|
||||||
posts = [post.id for post in q]
|
posts = [post.id for post in q]
|
||||||
@ -376,6 +434,26 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(self.Person.objects(Q(age__in=[20]))), 2)
|
self.assertEqual(len(self.Person.objects(Q(age__in=[20]))), 2)
|
||||||
self.assertEqual(len(self.Person.objects(Q(age__in=[20, 30]))), 3)
|
self.assertEqual(len(self.Person.objects(Q(age__in=[20, 30]))), 3)
|
||||||
|
|
||||||
|
def test_q_regex(self):
|
||||||
|
"""Ensure that Q objects can be queried using regexes.
|
||||||
|
"""
|
||||||
|
person = self.Person(name='Guido van Rossum')
|
||||||
|
person.save()
|
||||||
|
|
||||||
|
import re
|
||||||
|
obj = self.Person.objects(Q(name=re.compile('^Gui'))).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name=re.compile('^gui'))).first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
|
||||||
|
obj = self.Person.objects(Q(name=re.compile('^gui', re.I))).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
|
||||||
|
obj = self.Person.objects(Q(name__ne=re.compile('^bob'))).first()
|
||||||
|
self.assertEqual(obj, person)
|
||||||
|
obj = self.Person.objects(Q(name__ne=re.compile('^Gui'))).first()
|
||||||
|
self.assertEqual(obj, None)
|
||||||
|
|
||||||
def test_exec_js_query(self):
|
def test_exec_js_query(self):
|
||||||
"""Ensure that queries are properly formed for use in exec_js.
|
"""Ensure that queries are properly formed for use in exec_js.
|
||||||
"""
|
"""
|
||||||
@ -420,6 +498,58 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
|
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
def test_exec_js_field_sub(self):
|
||||||
|
"""Ensure that field substitutions occur properly in exec_js functions.
|
||||||
|
"""
|
||||||
|
class Comment(EmbeddedDocument):
|
||||||
|
content = StringField(name='body')
|
||||||
|
|
||||||
|
class BlogPost(Document):
|
||||||
|
name = StringField(name='doc-name')
|
||||||
|
comments = ListField(EmbeddedDocumentField(Comment), name='cmnts')
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
comments1 = [Comment(content='cool'), Comment(content='yay')]
|
||||||
|
post1 = BlogPost(name='post1', comments=comments1)
|
||||||
|
post1.save()
|
||||||
|
|
||||||
|
comments2 = [Comment(content='nice stuff')]
|
||||||
|
post2 = BlogPost(name='post2', comments=comments2)
|
||||||
|
post2.save()
|
||||||
|
|
||||||
|
code = """
|
||||||
|
function getComments() {
|
||||||
|
var comments = [];
|
||||||
|
db[collection].find(query).forEach(function(doc) {
|
||||||
|
var docComments = doc[~comments];
|
||||||
|
for (var i = 0; i < docComments.length; i++) {
|
||||||
|
comments.push({
|
||||||
|
'document': doc[~name],
|
||||||
|
'comment': doc[~comments][i][~comments.content]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
sub_code = BlogPost.objects._sub_js_fields(code)
|
||||||
|
code_chunks = ['doc["cmnts"];', 'doc["doc-name"],',
|
||||||
|
'doc["cmnts"][i]["body"]']
|
||||||
|
for chunk in code_chunks:
|
||||||
|
self.assertTrue(chunk in sub_code)
|
||||||
|
|
||||||
|
results = BlogPost.objects.exec_js(code)
|
||||||
|
expected_results = [
|
||||||
|
{u'comment': u'cool', u'document': u'post1'},
|
||||||
|
{u'comment': u'yay', u'document': u'post1'},
|
||||||
|
{u'comment': u'nice stuff', u'document': u'post2'},
|
||||||
|
]
|
||||||
|
self.assertEqual(results, expected_results)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
"""Ensure that documents are properly deleted from the database.
|
"""Ensure that documents are properly deleted from the database.
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user