Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@adrianholovaty adrianholovaty authored
View
12 django/contrib/admin/views/main.py
@@ -216,19 +216,17 @@ def get_lookup_params(self):
break
lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,)
if lookup_opts.admin.search_fields and query:
- or_queries = []
+ complex_queries = []
for bit in query.split():
- or_query = []
+ or_queries = []
for field_name in lookup_opts.admin.search_fields:
- or_query.append(('%s__icontains' % field_name, bit))
- or_queries.append(or_query)
- lookup_params['_or'] = or_queries
-
+ or_queries.append(meta.Q(**{'%s__icontains' % field_name: bit}))
+ complex_queries.append(reduce(operator.or_, or_queries))
+ lookup_params['complex'] = reduce(operator.and_, complex_queries)
if opts.one_to_one_field:
lookup_params.update(opts.one_to_one_field.rel.limit_choices_to)
self.lookup_params = lookup_params
-
def change_list(request, app_label, module_name):
try:
cl = ChangeList(request, app_label, module_name)
View
82 django/core/meta/__init__.py
@@ -282,6 +282,81 @@ def get_method_name_part(self):
rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name)
return rel_obj_name
+class QBase:
+ "Base class for QAnd and QOr"
+ def __init__(self, *args):
+ self.args = args
+
+ def __repr__(self):
+ return '(%s)' % self.operator.join([repr(el) for el in self.args])
+
+ def get_sql(self, opts, table_count):
+ tables, join_where, where, params = [], [], [], []
+ for val in self.args:
+ tables2, join_where2, where2, params2, table_count = val.get_sql(opts, table_count)
+ tables.extend(tables2)
+ join_where.extend(join_where2)
+ where.extend(where2)
+ params.extend(params2)
+ return tables, join_where, ['(%s)' % self.operator.join(where)], params, table_count
+
+class QAnd(QBase):
+ "Encapsulates a combined query that uses 'AND'."
+ operator = ' AND '
+ def __or__(self, other):
+ if isinstance(other, (QAnd, QOr, Q)):
+ return QOr(self, other)
+ else:
+ raise TypeError, other
+
+ def __and__(self, other):
+ if isinstance(other, QAnd):
+ return QAnd(*(self.args+other.args))
+ elif isinstance(other, (Q, QOr)):
+ return QAnd(*(self.args+(other,)))
+ else:
+ raise TypeError, other
+
+class QOr(QBase):
+ "Encapsulates a combined query that uses 'OR'."
+ operator = ' OR '
+ def __and__(self, other):
+ if isinstance(other, (QAnd, QOr, Q)):
+ return QAnd(self, other)
+ else:
+ raise TypeError, other
+
+ def __or__(self, other):
+ if isinstance(other, QOr):
+ return QOr(*(self.args+other.args))
+ elif isinstance(other, (Q, QAnd)):
+ return QOr(*(self.args+(other,)))
+ else:
+ raise TypeError, other
+
+class Q:
+ "Encapsulates queries for the 'complex' parameter to Django API functions."
+ def __init__(self, **kwargs):
+ self.kwargs = kwargs
+
+ def __repr__(self):
+ return 'Q%r' % self.kwargs
+
+ def __and__(self, other):
+ if isinstance(other, (Q, QAnd, QOr)):
+ return QAnd(self, other)
+ else:
+ raise TypeError, other
+
+ def __or__(self, other):
+ if isinstance(other, (Q, QAnd, QOr)):
+ return QOr(self, other)
+ else:
+ raise TypeError, other
+
+ def get_sql(self, opts, table_count):
+ return _parse_lookup(self.kwargs.items(), opts, table_count)
+
class Options:
def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='',
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):
continue
if kwarg_value is None:
continue
+ if kwarg == 'complex':
+ tables2, join_where2, where2, params2, table_count = kwarg_value.get_sql(opts, table_count)
+ tables.extend(tables2)
+ join_where.extend(join_where2)
+ where.extend(where2)
+ params.extend(params2)
+ continue
if kwarg == '_or':
for val in kwarg_value:
tables2, join_where2, where2, params2, table_count = _parse_lookup(val, opts, table_count)
View
36 docs/db-api.txt
@@ -219,6 +219,42 @@ If you pass an invalid keyword argument, the function will raise ``TypeError``.
.. _`Keyword Arguments`: http://docs.python.org/tut/node6.html#SECTION006720000000000000000
+OR lookups
+----------
+
+**New in Django development version.**
+
+By default, multiple lookups are "AND"ed together. If you'd like to use ``OR``
+statements in your queries, use the ``complex`` lookup type.
+
+``complex`` takes an expression of clauses, each of which is an instance of
+``django.core.meta.Q``. ``Q`` takes an arbitrary number of keyword arguments in
+the standard Django lookup format. And you can use Python's "and" (``&``) and
+"or" (``|``) operators to combine ``Q`` instances. For example::
+
+ from django.core.meta import Q
+ polls.get_object(complex=(Q(question__startswith='Who') | Q(question__startswith='What')))
+
+The ``|`` symbol signifies an "OR", so this (roughly) translates into::
+
+ SELECT * FROM polls
+ WHERE question LIKE 'Who%' OR question LIKE 'What%';
+
+You can use ``&`` and ``|`` operators together, and use parenthetical grouping.
+Example::
+
+ polls.get_object(complex=(Q(question__startswith='Who') & (Q(pub_date__exact=date(2005, 5, 2)) | pub_date__exact=date(2005, 5, 6)))
+
+This roughly translates into::
+
+ SELECT * FROM polls
+ WHERE question LIKE 'Who%'
+ AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06');
+
+See the `OR lookups examples page`_ for more examples.
+
+.. _OR lookups examples page: http://www.djangoproject.com/documentation/models/or_lookups/
+
Ordering
========
View
3  tests/testapp/models/__init__.py
@@ -1,4 +1,5 @@
__all__ = ['basic', 'repr', 'custom_methods', 'many_to_one', 'many_to_many',
'ordering', 'lookup', 'get_latest', 'm2m_intermediary', 'one_to_one',
'm2o_recursive', 'm2o_recursive2', 'save_delete_hooks', 'custom_pk',
- 'subclassing', 'many_to_one_null', 'custom_columns', 'reserved_names']
+ 'subclassing', 'many_to_one_null', 'custom_columns', 'reserved_names',
+ 'or_lookups']
View
57 tests/testapp/models/or_lookups.py
@@ -0,0 +1,57 @@
+"""
+19. OR lookups
+
+To perform an OR lookup, or a lookup that combines ANDs and ORs, use the
+``complex`` keyword argument, and pass it an expression of clauses using the
+variable ``django.core.meta.Q``.
+"""
+
+from django.core import meta
+
+class Article(meta.Model):
+ headline = meta.CharField(maxlength=50)
+ pub_date = meta.DateTimeField()
+ class META:
+ ordering = ('pub_date',)
+
+ def __repr__(self):
+ return self.headline
+
+API_TESTS = """
+>>> from datetime import datetime
+>>> from django.core.meta import Q
+
+>>> a1 = articles.Article(headline='Hello', pub_date=datetime(2005, 11, 27))
+>>> a1.save()
+
+>>> a2 = articles.Article(headline='Goodbye', pub_date=datetime(2005, 11, 28))
+>>> a2.save()
+
+>>> a3 = articles.Article(headline='Hello and goodbye', pub_date=datetime(2005, 11, 29))
+>>> a3.save()
+
+>>> articles.get_list(complex=(Q(headline__startswith='Hello') | Q(headline__startswith='Goodbye')))
+[Hello, Goodbye, Hello and goodbye]
+
+>>> articles.get_list(complex=(Q(headline__startswith='Hello') & Q(headline__startswith='Goodbye')))
+[]
+
+>>> articles.get_list(complex=(Q(headline__startswith='Hello') & Q(headline__contains='bye')))
+[Hello and goodbye]
+
+>>> articles.get_list(headline__startswith='Hello', complex=Q(headline__contains='bye'))
+[Hello and goodbye]
+
+>>> articles.get_list(complex=(Q(headline__contains='Hello') | Q(headline__contains='bye')))
+[Hello, Goodbye, Hello and goodbye]
+
+>>> articles.get_list(complex=(Q(headline__iexact='Hello') | Q(headline__contains='ood')))
+[Hello, Goodbye, Hello and goodbye]
+
+>>> articles.get_list(complex=(Q(pk=1) | Q(pk=2)))
+[Hello, Goodbye]
+
+>>> articles.get_list(complex=(Q(pk=1) | Q(pk=2) | Q(pk=3)))
+[Hello, Goodbye, Hello and goodbye]
+
+"""
Please sign in to comment.
Something went wrong with that request. Please try again.