diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 089cc5381a..11a7b9ba47 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -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) diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 8b760dced7..39ba4bc434 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -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) diff --git a/docs/db-api.txt b/docs/db-api.txt index fe17bd5921..3cee4d6c6e 100644 --- a/docs/db-api.txt +++ b/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 ======== diff --git a/tests/testapp/models/__init__.py b/tests/testapp/models/__init__.py index c0e39fae7d..a8aeada597 100644 --- a/tests/testapp/models/__init__.py +++ b/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'] diff --git a/tests/testapp/models/or_lookups.py b/tests/testapp/models/or_lookups.py new file mode 100644 index 0000000000..0bf554e408 --- /dev/null +++ b/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] + +"""