mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		| @@ -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'] | ||||
|   | ||||
							
								
								
									
										57
									
								
								tests/testapp/models/or_lookups.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								tests/testapp/models/or_lookups.py
									
									
									
									
									
										Normal 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] | ||||
|  | ||||
| """ | ||||
		Reference in New Issue
	
	Block a user