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
This commit is contained in:
Adrian Holovaty 2005-11-30 06:14:05 +00:00
parent 837435a08a
commit 9541d7a7c7
5 changed files with 182 additions and 8 deletions

View File

@ -216,19 +216,17 @@ class ChangeList(object):
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 File

@ -282,6 +282,81 @@ class RelatedObject(object):
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 File

@ -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 File

@ -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 File

@ -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]
"""