From 28abf5f0ebc9d380f25dd278d7ef4642c4504545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Sep 2012 17:51:06 +0300 Subject: [PATCH] Fixed #16211 -- Added comparison and negation ops to F() expressions Work done by Walter Doekes and Trac alias knoeb. Reviewed by Simon Charette. --- django/db/backends/__init__.py | 3 + django/db/models/expressions.py | 37 +++++++++ django/utils/tree.py | 8 +- docs/releases/1.5.txt | 4 + docs/topics/db/queries.txt | 9 ++ tests/modeltests/expressions/models.py | 2 + tests/modeltests/expressions/tests.py | 109 ++++++++++++++++++++----- 7 files changed, 150 insertions(+), 22 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 02d2a16a46..4edde04f42 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -913,6 +913,9 @@ class BaseDatabaseOperations(object): can vary between backends (e.g., Oracle with %% and &) and between subexpression types (e.g., date expressions) """ + if connector == 'NOT': + assert len(sub_expressions) == 1 + return 'NOT (%s)' % sub_expressions[0] conn = ' %s ' % connector return conn.join(sub_expressions) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 639ef6ee10..972440b858 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -18,6 +18,17 @@ class ExpressionNode(tree.Node): AND = '&' OR = '|' + # Unary operator (needs special attention in combine_expression) + NOT = 'NOT' + + # Comparison operators + EQ = '=' + GE = '>=' + GT = '>' + LE = '<=' + LT = '<' + NE = '<>' + def __init__(self, children=None, connector=None, negated=False): if children is not None and len(children) > 1 and connector is None: raise TypeError('You have to specify a connector.') @@ -93,6 +104,32 @@ class ExpressionNode(tree.Node): def __ror__(self, other): return self._combine(other, self.OR, True) + def __invert__(self): + obj = ExpressionNode([self], connector=self.NOT, negated=True) + return obj + + def __eq__(self, other): + return self._combine(other, self.EQ, False) + + def __ge__(self, other): + return self._combine(other, self.GE, False) + + def __gt__(self, other): + return self._combine(other, self.GT, False) + + def __le__(self, other): + return self._combine(other, self.LE, False) + + def __lt__(self, other): + return self._combine(other, self.LT, False) + + def __ne__(self, other): + return self._combine(other, self.NE, False) + + def __bool__(self): + raise TypeError('Boolean operators should be avoided. Use bitwise operators.') + __nonzero__ = __bool__ + def prepare_database_save(self, unused): return self diff --git a/django/utils/tree.py b/django/utils/tree.py index 717181d2b9..6229493544 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -88,8 +88,12 @@ class Node(object): Otherwise, the whole tree is pushed down one level and a new root connector is created, connecting the existing tree and the new node. """ - if node in self.children and conn_type == self.connector: - return + # Using for loop with 'is' instead of 'if node in children' so node + # __eq__ method doesn't get called. The __eq__ method can be overriden + # by subtypes, for example the F-expression. + for child in self.children: + if node is child and conn_type == self.connector: + return if len(self.children) < 2: self.connector = conn_type if self.connector == conn_type: diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 367b4f8349..b371214994 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -176,6 +176,10 @@ Django 1.5 also includes several smaller improvements worth noting: :setting:`DEBUG` is `True` are sent to the console (unless you redefine the logger in your :setting:`LOGGING` setting). +* :ref:`F() expressions ` now support comparison operations + and inversion, expanding the types of expressions that can be passed to the + database. + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index dd160656c7..c724eabb8e 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -640,6 +640,15 @@ that were modified more than 3 days after they were published:: >>> from datetime import timedelta >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) +.. versionadded:: 1.5 + Comparisons and negation operators for ``F()`` expressions + +Django also supports the comparison operators ``==``, ``!=``, ``<=``, ``<``, +``>``, ``>=`` and the bitwise negation operator ``~`` (boolean ``not`` operator +will raise ``TypeError``):: + + >>> Entry.objects.filter(is_heavily_quoted=~(F('n_pingbacks') < 100)) + The pk lookup shortcut ---------------------- diff --git a/tests/modeltests/expressions/models.py b/tests/modeltests/expressions/models.py index f592a0eb13..15f0d24541 100644 --- a/tests/modeltests/expressions/models.py +++ b/tests/modeltests/expressions/models.py @@ -27,6 +27,8 @@ class Company(models.Model): Employee, related_name='company_point_of_contact_set', null=True) + is_large = models.BooleanField( + blank=True) def __str__(self): return self.name diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py index 99eb07e370..14419ec55b 100644 --- a/tests/modeltests/expressions/tests.py +++ b/tests/modeltests/expressions/tests.py @@ -11,22 +11,22 @@ from .models import Company, Employee class ExpressionsTests(TestCase): def test_filter(self): Company.objects.create( - name="Example Inc.", num_employees=2300, num_chairs=5, + name="Example Inc.", num_employees=2300, num_chairs=5, is_large=False, ceo=Employee.objects.create(firstname="Joe", lastname="Smith") ) Company.objects.create( - name="Foobar Ltd.", num_employees=3, num_chairs=4, + name="Foobar Ltd.", num_employees=3, num_chairs=4, is_large=False, ceo=Employee.objects.create(firstname="Frank", lastname="Meyer") ) Company.objects.create( - name="Test GmbH", num_employees=32, num_chairs=1, + name="Test GmbH", num_employees=32, num_chairs=1, is_large=False, ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") ) company_query = Company.objects.values( - "name", "num_employees", "num_chairs" + "name", "num_employees", "num_chairs", "is_large" ).order_by( - "name", "num_employees", "num_chairs" + "name", "num_employees", "num_chairs", "is_large" ) # We can filter for companies where the number of employees is greater @@ -37,11 +37,13 @@ class ExpressionsTests(TestCase): "num_chairs": 5, "name": "Example Inc.", "num_employees": 2300, + "is_large": False }, { "num_chairs": 1, "name": "Test GmbH", - "num_employees": 32 + "num_employees": 32, + "is_large": False }, ], lambda o: o @@ -55,17 +57,20 @@ class ExpressionsTests(TestCase): { "num_chairs": 2300, "name": "Example Inc.", - "num_employees": 2300 + "num_employees": 2300, + "is_large": False }, { "num_chairs": 3, "name": "Foobar Ltd.", - "num_employees": 3 + "num_employees": 3, + "is_large": False }, { "num_chairs": 32, "name": "Test GmbH", - "num_employees": 32 + "num_employees": 32, + "is_large": False } ], lambda o: o @@ -79,17 +84,20 @@ class ExpressionsTests(TestCase): { 'num_chairs': 2302, 'name': 'Example Inc.', - 'num_employees': 2300 + 'num_employees': 2300, + 'is_large': False }, { 'num_chairs': 5, 'name': 'Foobar Ltd.', - 'num_employees': 3 + 'num_employees': 3, + 'is_large': False }, { 'num_chairs': 34, 'name': 'Test GmbH', - 'num_employees': 32 + 'num_employees': 32, + 'is_large': False } ], lambda o: o, @@ -104,17 +112,20 @@ class ExpressionsTests(TestCase): { 'num_chairs': 6900, 'name': 'Example Inc.', - 'num_employees': 2300 + 'num_employees': 2300, + 'is_large': False }, { 'num_chairs': 9, 'name': 'Foobar Ltd.', - 'num_employees': 3 + 'num_employees': 3, + 'is_large': False }, { 'num_chairs': 96, 'name': 'Test GmbH', - 'num_employees': 32 + 'num_employees': 32, + 'is_large': False } ], lambda o: o, @@ -129,21 +140,80 @@ class ExpressionsTests(TestCase): { 'num_chairs': 5294600, 'name': 'Example Inc.', - 'num_employees': 2300 + 'num_employees': 2300, + 'is_large': False }, { 'num_chairs': 15, 'name': 'Foobar Ltd.', - 'num_employees': 3 + 'num_employees': 3, + 'is_large': False }, { 'num_chairs': 1088, 'name': 'Test GmbH', - 'num_employees': 32 + 'num_employees': 32, + 'is_large': False } ], lambda o: o, ) + # The comparison operators and the bitwise unary not can be used + # to assign to boolean fields + for expression in ( + # Check boundaries + ~(F('num_employees') < 33), + ~(F('num_employees') <= 32), + (F('num_employees') > 2299), + (F('num_employees') >= 2300), + (F('num_employees') == 2300), + ((F('num_employees') + 1 != 4) & (32 != F('num_employees'))), + # Inverted argument order works too + (2299 < F('num_employees')), + (2300 <= F('num_employees')) + ): + # Test update by F-expression + company_query.update( + is_large=expression + ) + # Compare results + self.assertQuerysetEqual( + company_query, [ + { + 'num_chairs': 5294600, + 'name': 'Example Inc.', + 'num_employees': 2300, + 'is_large': True + }, + { + 'num_chairs': 15, + 'name': 'Foobar Ltd.', + 'num_employees': 3, + 'is_large': False + }, + { + 'num_chairs': 1088, + 'name': 'Test GmbH', + 'num_employees': 32, + 'is_large': False + } + ], + lambda o: o, + ) + # Reset values + company_query.update( + is_large=False + ) + + # The python boolean operators should be avoided as they yield + # unexpected results + test_gmbh = Company.objects.get(name="Test GmbH") + with self.assertRaises(TypeError): + test_gmbh.is_large = not F('is_large') + with self.assertRaises(TypeError): + test_gmbh.is_large = F('is_large') and F('is_large') + with self.assertRaises(TypeError): + test_gmbh.is_large = F('is_large') or F('is_large') # The relation of a foreign key can become copied over to an other # foreign key. @@ -202,9 +272,8 @@ class ExpressionsTests(TestCase): test_gmbh.point_of_contact = None test_gmbh.save() self.assertTrue(test_gmbh.point_of_contact is None) - def test(): + with self.assertRaises(ValueError): test_gmbh.point_of_contact = F("ceo") - self.assertRaises(ValueError, test) test_gmbh.point_of_contact = test_gmbh.ceo test_gmbh.save()