Added support for the $not operator
This commit is contained in:
		| @@ -265,6 +265,124 @@ class Q(QNode): | |||||||
|         return not bool(self.query) |         return not bool(self.query) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OldQ(object): | ||||||
|  |  | ||||||
|  |     OR = '||' | ||||||
|  |     AND = '&&' | ||||||
|  |     OPERATORS = { | ||||||
|  |         'eq': ('((this.%(field)s instanceof Array) && ' | ||||||
|  |                '  this.%(field)s.indexOf(%(value)s) != -1) ||' | ||||||
|  |                ' this.%(field)s == %(value)s'), | ||||||
|  |         'ne': 'this.%(field)s != %(value)s', | ||||||
|  |         'gt': 'this.%(field)s > %(value)s', | ||||||
|  |         'gte': 'this.%(field)s >= %(value)s', | ||||||
|  |         'lt': 'this.%(field)s < %(value)s', | ||||||
|  |         'lte': 'this.%(field)s <= %(value)s', | ||||||
|  |         'lte': 'this.%(field)s <= %(value)s', | ||||||
|  |         'in': '%(value)s.indexOf(this.%(field)s) != -1', | ||||||
|  |         'nin': '%(value)s.indexOf(this.%(field)s) == -1', | ||||||
|  |         'mod': '%(field)s %% %(value)s', | ||||||
|  |         'all': ('%(value)s.every(function(a){' | ||||||
|  |                 'return this.%(field)s.indexOf(a) != -1 })'), | ||||||
|  |         'size': 'this.%(field)s.length == %(value)s', | ||||||
|  |         '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): | ||||||
|  |         self.query = [query] | ||||||
|  |  | ||||||
|  |     def _combine(self, other, op): | ||||||
|  |         obj = Q() | ||||||
|  |         if not other.query[0]: | ||||||
|  |             return self | ||||||
|  |         if self.query[0]: | ||||||
|  |             obj.query = (['('] + copy.deepcopy(self.query) + [op] + | ||||||
|  |                          copy.deepcopy(other.query) + [')']) | ||||||
|  |         else: | ||||||
|  |             obj.query = copy.deepcopy(other.query) | ||||||
|  |         return obj | ||||||
|  |  | ||||||
|  |     def __or__(self, other): | ||||||
|  |         return self._combine(other, self.OR) | ||||||
|  |  | ||||||
|  |     def __and__(self, other): | ||||||
|  |         return self._combine(other, self.AND) | ||||||
|  |  | ||||||
|  |     def as_js(self, document): | ||||||
|  |         js = [] | ||||||
|  |         js_scope = {} | ||||||
|  |         for i, item in enumerate(self.query): | ||||||
|  |             if isinstance(item, dict): | ||||||
|  |                 item_query = QuerySet._transform_query(document, **item) | ||||||
|  |                 # item_query will values will either be a value or a dict | ||||||
|  |                 js.append(self._item_query_as_js(item_query, js_scope, i)) | ||||||
|  |             else: | ||||||
|  |                 js.append(item) | ||||||
|  |         return pymongo.code.Code(' '.join(js), js_scope) | ||||||
|  |  | ||||||
|  |     def _item_query_as_js(self, item_query, js_scope, item_num): | ||||||
|  |         # item_query will be in one of the following forms | ||||||
|  |         #    {'age': 25, 'name': 'Test'} | ||||||
|  |         #    {'age': {'$lt': 25}, 'name': {'$in': ['Test', 'Example']} | ||||||
|  |         #    {'age': {'$lt': 25, '$gt': 18}} | ||||||
|  |         js = [] | ||||||
|  |         for i, (key, value) in enumerate(item_query.items()): | ||||||
|  |             op = 'eq' | ||||||
|  |             # Construct a variable name for the value in the JS | ||||||
|  |             value_name = 'i%sf%s' % (item_num, i) | ||||||
|  |             if isinstance(value, dict): | ||||||
|  |                 # Multiple operators for this field | ||||||
|  |                 for j, (op, value) in enumerate(value.items()): | ||||||
|  |                     # Create a custom variable name for this operator | ||||||
|  |                     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 | ||||||
|  |                     js_scope[op_value_name] = value | ||||||
|  |                     js.append(operation_js) | ||||||
|  |             else: | ||||||
|  |                 # Construct the JS for this field | ||||||
|  |                 value, field_js = self._build_op_js(op, key, value, value_name) | ||||||
|  |                 js_scope[value_name] = value | ||||||
|  |                 js.append(field_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 = unicode(value) | ||||||
|  |  | ||||||
|  |         # Handle DBRef | ||||||
|  |         if isinstance(value, pymongo.dbref.DBRef): | ||||||
|  |             op_js = '(this.%(field)s.$id == "%(id)s" &&'\ | ||||||
|  |                     ' this.%(field)s.$ref == "%(ref)s")' % { | ||||||
|  |                         'field': key, | ||||||
|  |                         'id': unicode(value.id), | ||||||
|  |                         'ref': unicode(value.collection) | ||||||
|  |                     } | ||||||
|  |             value = None | ||||||
|  |  | ||||||
|  |         # 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, | ||||||
|     providing :class:`~mongoengine.Document` objects as the results. |     providing :class:`~mongoengine.Document` objects as the results. | ||||||
| @@ -462,7 +580,7 @@ class QuerySet(object): | |||||||
|         """Transform a query from Django-style format to Mongo format. |         """Transform a query from Django-style format to Mongo format. | ||||||
|         """ |         """ | ||||||
|         operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', |         operators = ['ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod', | ||||||
|                      'all', 'size', 'exists'] |                      'all', 'size', 'exists', 'not'] | ||||||
|         geo_operators = ['within_distance', 'within_box', 'near'] |         geo_operators = ['within_distance', 'within_box', 'near'] | ||||||
|         match_operators = ['contains', 'icontains', 'startswith',  |         match_operators = ['contains', 'icontains', 'startswith',  | ||||||
|                            'istartswith', 'endswith', 'iendswith',  |                            'istartswith', 'endswith', 'iendswith',  | ||||||
| @@ -478,6 +596,11 @@ class QuerySet(object): | |||||||
|             if parts[-1] in operators + match_operators + geo_operators: |             if parts[-1] in operators + match_operators + geo_operators: | ||||||
|                 op = parts.pop() |                 op = parts.pop() | ||||||
|  |  | ||||||
|  |             negate = False | ||||||
|  |             if parts[-1] == 'not': | ||||||
|  |                 parts.pop() | ||||||
|  |                 negate = True | ||||||
|  |  | ||||||
|             if _doc_cls: |             if _doc_cls: | ||||||
|                 # Switch field names to proper names [set in Field(name='foo')] |                 # Switch field names to proper names [set in Field(name='foo')] | ||||||
|                 fields = QuerySet._lookup_field(_doc_cls, parts) |                 fields = QuerySet._lookup_field(_doc_cls, parts) | ||||||
| @@ -485,7 +608,7 @@ class QuerySet(object): | |||||||
|  |  | ||||||
|                 # Convert value to proper value |                 # Convert value to proper value | ||||||
|                 field = fields[-1] |                 field = fields[-1] | ||||||
|                 singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte'] |                 singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not'] | ||||||
|                 singular_ops += match_operators |                 singular_ops += match_operators | ||||||
|                 if op in singular_ops: |                 if op in singular_ops: | ||||||
|                     value = field.prepare_query_value(op, value) |                     value = field.prepare_query_value(op, value) | ||||||
| @@ -511,6 +634,9 @@ class QuerySet(object): | |||||||
|                 elif op not in match_operators: |                 elif op not in match_operators: | ||||||
|                     value = {'$' + op: value} |                     value = {'$' + op: value} | ||||||
|  |  | ||||||
|  |             if negate: | ||||||
|  |                 value = {'$not': value} | ||||||
|  |              | ||||||
|             for i, part in indices: |             for i, part in indices: | ||||||
|                 parts.insert(i, part) |                 parts.insert(i, part) | ||||||
|             key = '.'.join(parts) |             key = '.'.join(parts) | ||||||
|   | |||||||
| @@ -336,6 +336,18 @@ class QuerySetTest(unittest.TestCase): | |||||||
|         obj = self.Person.objects(Q(name__icontains='[.\'Geek')).first() |         obj = self.Person.objects(Q(name__icontains='[.\'Geek')).first() | ||||||
|         self.assertEqual(obj, person) |         self.assertEqual(obj, person) | ||||||
|  |  | ||||||
|  |     def test_not(self): | ||||||
|  |         """Ensure that the __not operator works as expected. | ||||||
|  |         """ | ||||||
|  |         alice = self.Person(name='Alice', age=25) | ||||||
|  |         alice.save() | ||||||
|  |  | ||||||
|  |         obj = self.Person.objects(name__iexact='alice').first() | ||||||
|  |         self.assertEqual(obj, alice) | ||||||
|  |  | ||||||
|  |         obj = self.Person.objects(name__not__iexact='alice').first() | ||||||
|  |         self.assertEqual(obj, None) | ||||||
|  |  | ||||||
|     def test_filter_chaining(self): |     def test_filter_chaining(self): | ||||||
|         """Ensure filters can be chained together. |         """Ensure filters can be chained together. | ||||||
|         """ |         """ | ||||||
| @@ -545,10 +557,10 @@ class QuerySetTest(unittest.TestCase): | |||||||
|         obj = self.Person.objects(Q(name=re.compile('^gui', re.I))).first() |         obj = self.Person.objects(Q(name=re.compile('^gui', re.I))).first() | ||||||
|         self.assertEqual(obj, person) |         self.assertEqual(obj, person) | ||||||
|  |  | ||||||
|         obj = self.Person.objects(Q(name__ne=re.compile('^bob'))).first() |         obj = self.Person.objects(Q(name__not=re.compile('^bob'))).first() | ||||||
|         self.assertEqual(obj, person) |         self.assertEqual(obj, person) | ||||||
|          |          | ||||||
|         obj = self.Person.objects(Q(name__ne=re.compile('^Gui'))).first() |         obj = self.Person.objects(Q(name__not=re.compile('^Gui'))).first() | ||||||
|         self.assertEqual(obj, None) |         self.assertEqual(obj, None) | ||||||
|  |  | ||||||
|     def test_q_lists(self): |     def test_q_lists(self): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user