Skip to content

Commit 9541d7a

Browse files
Fixed #251 -- Added OR support to queries, via the new 'complex' DB API 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
1 parent 837435a commit 9541d7a

File tree

5 files changed

+182
-8
lines changed

5 files changed

+182
-8
lines changed

django/contrib/admin/views/main.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,17 @@ def get_lookup_params(self):
216216
break
217217
lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,)
218218
if lookup_opts.admin.search_fields and query:
219-
or_queries = []
219+
complex_queries = []
220220
for bit in query.split():
221-
or_query = []
221+
or_queries = []
222222
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)
227226
if opts.one_to_one_field:
228227
lookup_params.update(opts.one_to_one_field.rel.limit_choices_to)
229228
self.lookup_params = lookup_params
230229

231-
232230
def change_list(request, app_label, module_name):
233231
try:
234232
cl = ChangeList(request, app_label, module_name)

django/core/meta/__init__.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,81 @@ def get_method_name_part(self):
282282
rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name)
283283
return rel_obj_name
284284

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+
285360
class Options:
286361
def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='',
287362
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):
13901465
continue
13911466
if kwarg_value is None:
13921467
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
13931475
if kwarg == '_or':
13941476
for val in kwarg_value:
13951477
tables2, join_where2, where2, params2, table_count = _parse_lookup(val, opts, table_count)

docs/db-api.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,42 @@ If you pass an invalid keyword argument, the function will raise ``TypeError``.
219219

220220
.. _`Keyword Arguments`: http://docs.python.org/tut/node6.html#SECTION006720000000000000000
221221

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+
222258
Ordering
223259
========
224260

tests/testapp/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
__all__ = ['basic', 'repr', 'custom_methods', 'many_to_one', 'many_to_many',
22
'ordering', 'lookup', 'get_latest', 'm2m_intermediary', 'one_to_one',
33
'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']

tests/testapp/models/or_lookups.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)