Browse Source

Fixed #32548 -- Fixed crash when combining Q() objects with boolean expressions.

Jonathan Richards 4 years ago
parent
commit
00b0786de5
3 changed files with 22 additions and 16 deletions
  1. 4 8
      django/db/models/query_utils.py
  2. 4 0
      tests/expressions/tests.py
  3. 14 8
      tests/queries/test_q.py

+ 4 - 8
django/db/models/query_utils.py

@@ -84,14 +84,10 @@ class Q(tree.Node):
         path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
         if path.startswith('django.db.models.query_utils'):
             path = path.replace('django.db.models.query_utils', 'django.db.models')
-        args, kwargs = (), {}
-        if len(self.children) == 1 and not isinstance(self.children[0], Q):
-            child = self.children[0]
-            kwargs = {child[0]: child[1]}
-        else:
-            args = tuple(self.children)
-            if self.connector != self.default:
-                kwargs = {'_connector': self.connector}
+        args = tuple(self.children)
+        kwargs = {}
+        if self.connector != self.default:
+            kwargs['_connector'] = self.connector
         if self.negated:
             kwargs['_negated'] = True
         return path, args, kwargs

+ 4 - 0
tests/expressions/tests.py

@@ -833,6 +833,10 @@ class BasicExpressionsTests(TestCase):
             Q() & Exists(is_poc),
             Exists(is_poc) | Q(),
             Q() | Exists(is_poc),
+            Q(Exists(is_poc)) & Q(),
+            Q() & Q(Exists(is_poc)),
+            Q(Exists(is_poc)) | Q(),
+            Q() | Q(Exists(is_poc)),
         ]
         for conditions in tests:
             with self.subTest(conditions):

+ 14 - 8
tests/queries/test_q.py

@@ -1,6 +1,8 @@
-from django.db.models import F, Q
+from django.db.models import Exists, F, OuterRef, Q
 from django.test import SimpleTestCase
 
+from .models import Tag
+
 
 class QTests(SimpleTestCase):
     def test_combine_and_empty(self):
@@ -39,17 +41,14 @@ class QTests(SimpleTestCase):
         q = Q(price__gt=F('discounted_price'))
         path, args, kwargs = q.deconstruct()
         self.assertEqual(path, 'django.db.models.Q')
-        self.assertEqual(args, ())
-        self.assertEqual(kwargs, {'price__gt': F('discounted_price')})
+        self.assertEqual(args, (('price__gt', F('discounted_price')),))
+        self.assertEqual(kwargs, {})
 
     def test_deconstruct_negated(self):
         q = ~Q(price__gt=F('discounted_price'))
         path, args, kwargs = q.deconstruct()
-        self.assertEqual(args, ())
-        self.assertEqual(kwargs, {
-            'price__gt': F('discounted_price'),
-            '_negated': True,
-        })
+        self.assertEqual(args, (('price__gt', F('discounted_price')),))
+        self.assertEqual(kwargs, {'_negated': True})
 
     def test_deconstruct_or(self):
         q1 = Q(price__gt=F('discounted_price'))
@@ -88,6 +87,13 @@ class QTests(SimpleTestCase):
         self.assertEqual(args, (Q(price__gt=F('discounted_price')),))
         self.assertEqual(kwargs, {})
 
+    def test_deconstruct_boolean_expression(self):
+        tagged = Tag.objects.filter(category=OuterRef('pk'))
+        q = Q(Exists(tagged))
+        _, args, kwargs = q.deconstruct()
+        self.assertEqual(args, (Exists(tagged),))
+        self.assertEqual(kwargs, {})
+
     def test_reconstruct(self):
         q = Q(price__gt=F('discounted_price'))
         path, args, kwargs = q.deconstruct()