Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #251 -- Added OR support to queries, via the new 'complex' DB A…

…PI keyword argument. Updated docs and added unit tests. Also removed old, undocumented '_or' parameter. Thanks, Hugo.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@1508 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 9541d7a7c70df6be8c8673789437c717b884e039 1 parent 837435a
Adrian Holovaty authored
12  django/contrib/admin/views/main.py
@@ -216,19 +216,17 @@ def get_lookup_params(self):
216 216
                         break
217 217
         lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,)
218 218
         if lookup_opts.admin.search_fields and query:
219  
-            or_queries = []
  219
+            complex_queries = []
220 220
             for bit in query.split():
221  
-                or_query = []
  221
+                or_queries = []
222 222
                 for field_name in lookup_opts.admin.search_fields:
223  
-                    or_query.append(('%s__icontains' % field_name, bit))
224  
-                or_queries.append(or_query)
225  
-            lookup_params['_or'] = or_queries
226  
-
  223
+                    or_queries.append(meta.Q(**{'%s__icontains' % field_name: bit}))
  224
+                complex_queries.append(reduce(operator.or_, or_queries))
  225
+            lookup_params['complex'] = reduce(operator.and_, complex_queries)
227 226
         if opts.one_to_one_field:
228 227
             lookup_params.update(opts.one_to_one_field.rel.limit_choices_to)
229 228
         self.lookup_params = lookup_params
230 229
 
231  
-
232 230
 def change_list(request, app_label, module_name):
233 231
     try:
234 232
         cl = ChangeList(request, app_label, module_name)
82  django/core/meta/__init__.py
@@ -282,6 +282,81 @@ def get_method_name_part(self):
282 282
             rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name)
283 283
         return rel_obj_name
284 284
 
  285
+class QBase:
  286
+    "Base class for QAnd and QOr"
  287
+    def __init__(self, *args):
  288
+        self.args = args
  289
+
  290
+    def __repr__(self):
  291
+        return '(%s)' % self.operator.join([repr(el) for el in self.args])
  292
+
  293
+    def get_sql(self, opts, table_count):
  294
+        tables, join_where, where, params = [], [], [], []
  295
+        for val in self.args:
  296
+            tables2, join_where2, where2, params2, table_count = val.get_sql(opts, table_count)
  297
+            tables.extend(tables2)
  298
+            join_where.extend(join_where2)
  299
+            where.extend(where2)
  300
+            params.extend(params2)
  301
+        return tables, join_where, ['(%s)' % self.operator.join(where)], params, table_count
  302
+
  303
+class QAnd(QBase):
  304
+    "Encapsulates a combined query that uses 'AND'."
  305
+    operator = ' AND '
  306
+    def __or__(self, other):
  307
+        if isinstance(other, (QAnd, QOr, Q)):
  308
+            return QOr(self, other)
  309
+        else:
  310
+            raise TypeError, other
  311
+
  312
+    def __and__(self, other):
  313
+        if isinstance(other, QAnd):
  314
+            return QAnd(*(self.args+other.args))
  315
+        elif isinstance(other, (Q, QOr)):
  316
+            return QAnd(*(self.args+(other,)))
  317
+        else:
  318
+            raise TypeError, other
  319
+
  320
+class QOr(QBase):
  321
+    "Encapsulates a combined query that uses 'OR'."
  322
+    operator = ' OR '
  323
+    def __and__(self, other):
  324
+        if isinstance(other, (QAnd, QOr, Q)):
  325
+            return QAnd(self, other)
  326
+        else:
  327
+            raise TypeError, other
  328
+
  329
+    def __or__(self, other):
  330
+        if isinstance(other, QOr):
  331
+            return QOr(*(self.args+other.args))
  332
+        elif isinstance(other, (Q, QAnd)):
  333
+            return QOr(*(self.args+(other,)))
  334
+        else:
  335
+            raise TypeError, other
  336
+
  337
+class Q:
  338
+    "Encapsulates queries for the 'complex' parameter to Django API functions."
  339
+    def __init__(self, **kwargs):
  340
+        self.kwargs = kwargs
  341
+
  342
+    def __repr__(self):
  343
+        return 'Q%r' % self.kwargs
  344
+
  345
+    def __and__(self, other):
  346
+        if isinstance(other, (Q, QAnd, QOr)):
  347
+            return QAnd(self, other)
  348
+        else:
  349
+            raise TypeError, other
  350
+
  351
+    def __or__(self, other):
  352
+        if isinstance(other, (Q, QAnd, QOr)):
  353
+            return QOr(self, other)
  354
+        else:
  355
+            raise TypeError, other
  356
+
  357
+    def get_sql(self, opts, table_count):
  358
+        return _parse_lookup(self.kwargs.items(), opts, table_count)
  359
+
285 360
 class Options:
286 361
     def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='',
287 362
         fields=None, ordering=None, unique_together=None, admin=None, has_related_links=False,
@@ -1390,6 +1465,13 @@ def _parse_lookup(kwarg_items, opts, table_count=0):
1390 1465
             continue
1391 1466
         if kwarg_value is None:
1392 1467
             continue
  1468
+        if kwarg == 'complex':
  1469
+            tables2, join_where2, where2, params2, table_count = kwarg_value.get_sql(opts, table_count)
  1470
+            tables.extend(tables2)
  1471
+            join_where.extend(join_where2)
  1472
+            where.extend(where2)
  1473
+            params.extend(params2)
  1474
+            continue
1393 1475
         if kwarg == '_or':
1394 1476
             for val in kwarg_value:
1395 1477
                 tables2, join_where2, where2, params2, table_count = _parse_lookup(val, opts, table_count)
36  docs/db-api.txt
@@ -219,6 +219,42 @@ If you pass an invalid keyword argument, the function will raise ``TypeError``.
219 219
 
220 220
 .. _`Keyword Arguments`: http://docs.python.org/tut/node6.html#SECTION006720000000000000000
221 221
 
  222
+OR lookups
  223
+----------
  224
+
  225
+**New in Django development version.**
  226
+
  227
+By default, multiple lookups are "AND"ed together. If you'd like to use ``OR``
  228
+statements in your queries, use the ``complex`` lookup type.
  229
+
  230
+``complex`` takes an expression of clauses, each of which is an instance of
  231
+``django.core.meta.Q``. ``Q`` takes an arbitrary number of keyword arguments in
  232
+the standard Django lookup format. And you can use Python's "and" (``&``) and
  233
+"or" (``|``) operators to combine ``Q`` instances. For example::
  234
+
  235
+    from django.core.meta import Q
  236
+    polls.get_object(complex=(Q(question__startswith='Who') | Q(question__startswith='What')))
  237
+
  238
+The ``|`` symbol signifies an "OR", so this (roughly) translates into::
  239
+
  240
+    SELECT * FROM polls
  241
+    WHERE question LIKE 'Who%' OR question LIKE 'What%';
  242
+
  243
+You can use ``&`` and ``|`` operators together, and use parenthetical grouping.
  244
+Example::
  245
+
  246
+    polls.get_object(complex=(Q(question__startswith='Who') & (Q(pub_date__exact=date(2005, 5, 2)) | pub_date__exact=date(2005, 5, 6)))
  247
+
  248
+This roughly translates into::
  249
+
  250
+    SELECT * FROM polls
  251
+    WHERE question LIKE 'Who%'
  252
+        AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06');
  253
+
  254
+See the `OR lookups examples page`_ for more examples.
  255
+
  256
+.. _OR lookups examples page: http://www.djangoproject.com/documentation/models/or_lookups/
  257
+
222 258
 Ordering
223 259
 ========
224 260
 
3  tests/testapp/models/__init__.py
... ...
@@ -1,4 +1,5 @@
1 1
 __all__ = ['basic', 'repr', 'custom_methods', 'many_to_one', 'many_to_many',
2 2
            'ordering', 'lookup', 'get_latest', 'm2m_intermediary', 'one_to_one',
3 3
            'm2o_recursive', 'm2o_recursive2', 'save_delete_hooks', 'custom_pk',
4  
-           'subclassing', 'many_to_one_null', 'custom_columns', 'reserved_names']
  4
+           'subclassing', 'many_to_one_null', 'custom_columns', 'reserved_names',
  5
+           'or_lookups']
57  tests/testapp/models/or_lookups.py
... ...
@@ -0,0 +1,57 @@
  1
+"""
  2
+19. OR lookups
  3
+
  4
+To perform an OR lookup, or a lookup that combines ANDs and ORs, use the
  5
+``complex`` keyword argument, and pass it an expression of clauses using the
  6
+variable ``django.core.meta.Q``.
  7
+"""
  8
+
  9
+from django.core import meta
  10
+
  11
+class Article(meta.Model):
  12
+    headline = meta.CharField(maxlength=50)
  13
+    pub_date = meta.DateTimeField()
  14
+    class META:
  15
+       ordering = ('pub_date',)
  16
+
  17
+    def __repr__(self):
  18
+        return self.headline
  19
+
  20
+API_TESTS = """
  21
+>>> from datetime import datetime
  22
+>>> from django.core.meta import Q
  23
+
  24
+>>> a1 = articles.Article(headline='Hello', pub_date=datetime(2005, 11, 27))
  25
+>>> a1.save()
  26
+
  27
+>>> a2 = articles.Article(headline='Goodbye', pub_date=datetime(2005, 11, 28))
  28
+>>> a2.save()
  29
+
  30
+>>> a3 = articles.Article(headline='Hello and goodbye', pub_date=datetime(2005, 11, 29))
  31
+>>> a3.save()
  32
+
  33
+>>> articles.get_list(complex=(Q(headline__startswith='Hello') | Q(headline__startswith='Goodbye')))
  34
+[Hello, Goodbye, Hello and goodbye]
  35
+
  36
+>>> articles.get_list(complex=(Q(headline__startswith='Hello') & Q(headline__startswith='Goodbye')))
  37
+[]
  38
+
  39
+>>> articles.get_list(complex=(Q(headline__startswith='Hello') & Q(headline__contains='bye')))
  40
+[Hello and goodbye]
  41
+
  42
+>>> articles.get_list(headline__startswith='Hello', complex=Q(headline__contains='bye'))
  43
+[Hello and goodbye]
  44
+
  45
+>>> articles.get_list(complex=(Q(headline__contains='Hello') | Q(headline__contains='bye')))
  46
+[Hello, Goodbye, Hello and goodbye]
  47
+
  48
+>>> articles.get_list(complex=(Q(headline__iexact='Hello') | Q(headline__contains='ood')))
  49
+[Hello, Goodbye, Hello and goodbye]
  50
+
  51
+>>> articles.get_list(complex=(Q(pk=1) | Q(pk=2)))
  52
+[Hello, Goodbye]
  53
+
  54
+>>> articles.get_list(complex=(Q(pk=1) | Q(pk=2) | Q(pk=3)))
  55
+[Hello, Goodbye, Hello and goodbye]
  56
+
  57
+"""

0 notes on commit 9541d7a

Please sign in to comment.
Something went wrong with that request. Please try again.