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 |                         break | ||||||
|         lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,) |         lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,) | ||||||
|         if lookup_opts.admin.search_fields and query: |         if lookup_opts.admin.search_fields and query: | ||||||
|             or_queries = [] |             complex_queries = [] | ||||||
|             for bit in query.split(): |             for bit in query.split(): | ||||||
|                 or_query = [] |                 or_queries = [] | ||||||
|                 for field_name in lookup_opts.admin.search_fields: |                 for field_name in lookup_opts.admin.search_fields: | ||||||
|                     or_query.append(('%s__icontains' % field_name, bit)) |                     or_queries.append(meta.Q(**{'%s__icontains' % field_name: bit})) | ||||||
|                 or_queries.append(or_query) |                 complex_queries.append(reduce(operator.or_, or_queries)) | ||||||
|             lookup_params['_or'] = or_queries |             lookup_params['complex'] = reduce(operator.and_, complex_queries) | ||||||
|  |  | ||||||
|         if opts.one_to_one_field: |         if opts.one_to_one_field: | ||||||
|             lookup_params.update(opts.one_to_one_field.rel.limit_choices_to) |             lookup_params.update(opts.one_to_one_field.rel.limit_choices_to) | ||||||
|         self.lookup_params = lookup_params |         self.lookup_params = lookup_params | ||||||
|  |  | ||||||
|  |  | ||||||
| def change_list(request, app_label, module_name): | def change_list(request, app_label, module_name): | ||||||
|     try: |     try: | ||||||
|         cl = ChangeList(request, app_label, module_name) |         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) |             rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name) | ||||||
|         return 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: | class Options: | ||||||
|     def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='', |     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, |         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 |             continue | ||||||
|         if kwarg_value is None: |         if kwarg_value is None: | ||||||
|             continue |             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': |         if kwarg == '_or': | ||||||
|             for val in kwarg_value: |             for val in kwarg_value: | ||||||
|                 tables2, join_where2, where2, params2, table_count = _parse_lookup(val, opts, table_count) |                 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 | .. _`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 | Ordering | ||||||
| ======== | ======== | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| __all__ = ['basic', 'repr', 'custom_methods', 'many_to_one', 'many_to_many', | __all__ = ['basic', 'repr', 'custom_methods', 'many_to_one', 'many_to_many', | ||||||
|            'ordering', 'lookup', 'get_latest', 'm2m_intermediary', 'one_to_one', |            'ordering', 'lookup', 'get_latest', 'm2m_intermediary', 'one_to_one', | ||||||
|            'm2o_recursive', 'm2o_recursive2', 'save_delete_hooks', 'custom_pk', |            '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