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:
parent
837435a08a
commit
9541d7a7c7
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
========
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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]
|
||||
|
||||
"""
|
Loading…
Reference in New Issue