mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Merge pull request #1166 from bbenko/ticket_15961
Fixed #15961 -- Added get_search_results to ModelAdmin
This commit is contained in:
		
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -99,6 +99,7 @@ answer newbie questions, and generally made Django that much better: | |||||||
|     Brian Beck <http://blog.brianbeck.com/> |     Brian Beck <http://blog.brianbeck.com/> | ||||||
|     Shannon -jj Behrens <http://jjinux.blogspot.com/> |     Shannon -jj Behrens <http://jjinux.blogspot.com/> | ||||||
|     Esdras Beleza <linux@esdrasbeleza.com> |     Esdras Beleza <linux@esdrasbeleza.com> | ||||||
|  |     Božidar Benko <bbenko@gmail.com> | ||||||
|     Chris Bennett <chrisrbennett@yahoo.com> |     Chris Bennett <chrisrbennett@yahoo.com> | ||||||
|     Danilo Bargen |     Danilo Bargen | ||||||
|     Shai Berger <shai@platonix.com> |     Shai Berger <shai@platonix.com> | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import copy | import copy | ||||||
|  | import operator | ||||||
| from functools import update_wrapper, partial | from functools import update_wrapper, partial | ||||||
|  |  | ||||||
| from django import forms | from django import forms | ||||||
| @@ -9,7 +10,7 @@ from django.forms.models import (modelform_factory, modelformset_factory, | |||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.contrib.admin import widgets, helpers | from django.contrib.admin import widgets, helpers | ||||||
| from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, | from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, | ||||||
|     model_format_dict, NestedObjects) |     model_format_dict, NestedObjects, lookup_needs_distinct) | ||||||
| from django.contrib.admin import validation | from django.contrib.admin import validation | ||||||
| from django.contrib.admin.templatetags.admin_static import static | from django.contrib.admin.templatetags.admin_static import static | ||||||
| from django.contrib import messages | from django.contrib import messages | ||||||
| @@ -255,6 +256,34 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): | |||||||
|         """ |         """ | ||||||
|         return self.prepopulated_fields |         return self.prepopulated_fields | ||||||
|  |  | ||||||
|  |     def get_search_results(self, request, queryset, search_term): | ||||||
|  |         # Apply keyword searches. | ||||||
|  |         def construct_search(field_name): | ||||||
|  |             if field_name.startswith('^'): | ||||||
|  |                 return "%s__istartswith" % field_name[1:] | ||||||
|  |             elif field_name.startswith('='): | ||||||
|  |                 return "%s__iexact" % field_name[1:] | ||||||
|  |             elif field_name.startswith('@'): | ||||||
|  |                 return "%s__search" % field_name[1:] | ||||||
|  |             else: | ||||||
|  |                 return "%s__icontains" % field_name | ||||||
|  |  | ||||||
|  |         use_distinct = False | ||||||
|  |         if self.search_fields and search_term: | ||||||
|  |             orm_lookups = [construct_search(str(search_field)) | ||||||
|  |                            for search_field in self.search_fields] | ||||||
|  |             for bit in search_term.split(): | ||||||
|  |                 or_queries = [models.Q(**{orm_lookup: bit}) | ||||||
|  |                               for orm_lookup in orm_lookups] | ||||||
|  |                 queryset = queryset.filter(reduce(operator.or_, or_queries)) | ||||||
|  |             if not use_distinct: | ||||||
|  |                 for search_spec in orm_lookups: | ||||||
|  |                     if lookup_needs_distinct(self.opts, search_spec): | ||||||
|  |                         use_distinct = True | ||||||
|  |                         break | ||||||
|  |  | ||||||
|  |         return queryset, use_distinct | ||||||
|  |  | ||||||
|     def get_queryset(self, request): |     def get_queryset(self, request): | ||||||
|         """ |         """ | ||||||
|         Returns a QuerySet of all model instances that can be edited by the |         Returns a QuerySet of all model instances that can be edited by the | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| import operator |  | ||||||
| import sys | import sys | ||||||
| import warnings | import warnings | ||||||
| from functools import reduce |  | ||||||
|  |  | ||||||
| from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured | from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured | ||||||
| from django.core.paginator import InvalidPage | from django.core.paginator import InvalidPage | ||||||
| @@ -331,7 +329,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): | |||||||
|     def get_queryset(self, request): |     def get_queryset(self, request): | ||||||
|         # First, we collect all the declared list filters. |         # First, we collect all the declared list filters. | ||||||
|         (self.filter_specs, self.has_filters, remaining_lookup_params, |         (self.filter_specs, self.has_filters, remaining_lookup_params, | ||||||
|          use_distinct) = self.get_filters(request) |          filters_use_distinct) = self.get_filters(request) | ||||||
|  |  | ||||||
|         # Then, we let every list filter modify the queryset to its liking. |         # Then, we let every list filter modify the queryset to its liking. | ||||||
|         qs = self.root_queryset |         qs = self.root_queryset | ||||||
| @@ -378,31 +376,11 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): | |||||||
|         ordering = self.get_ordering(request, qs) |         ordering = self.get_ordering(request, qs) | ||||||
|         qs = qs.order_by(*ordering) |         qs = qs.order_by(*ordering) | ||||||
|  |  | ||||||
|         # Apply keyword searches. |         # Apply search results | ||||||
|         def construct_search(field_name): |         qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query) | ||||||
|             if field_name.startswith('^'): |  | ||||||
|                 return "%s__istartswith" % field_name[1:] |  | ||||||
|             elif field_name.startswith('='): |  | ||||||
|                 return "%s__iexact" % field_name[1:] |  | ||||||
|             elif field_name.startswith('@'): |  | ||||||
|                 return "%s__search" % field_name[1:] |  | ||||||
|             else: |  | ||||||
|                 return "%s__icontains" % field_name |  | ||||||
|  |  | ||||||
|         if self.search_fields and self.query: |         # Remove duplicates from results, if neccesary | ||||||
|             orm_lookups = [construct_search(str(search_field)) |         if filters_use_distinct | search_use_distinct: | ||||||
|                            for search_field in self.search_fields] |  | ||||||
|             for bit in self.query.split(): |  | ||||||
|                 or_queries = [models.Q(**{orm_lookup: bit}) |  | ||||||
|                               for orm_lookup in orm_lookups] |  | ||||||
|                 qs = qs.filter(reduce(operator.or_, or_queries)) |  | ||||||
|             if not use_distinct: |  | ||||||
|                 for search_spec in orm_lookups: |  | ||||||
|                     if lookup_needs_distinct(self.lookup_opts, search_spec): |  | ||||||
|                         use_distinct = True |  | ||||||
|                         break |  | ||||||
|  |  | ||||||
|         if use_distinct: |  | ||||||
|             return qs.distinct() |             return qs.distinct() | ||||||
|         else: |         else: | ||||||
|             return qs |             return qs | ||||||
|   | |||||||
| @@ -1005,6 +1005,9 @@ subclass:: | |||||||
|         Performs a full-text match. This is like the default search method but |         Performs a full-text match. This is like the default search method but | ||||||
|         uses an index. Currently this is only available for MySQL. |         uses an index. Currently this is only available for MySQL. | ||||||
|  |  | ||||||
|  |     If you need to customize search you can use :meth:`ModelAdmin.get_search_results` to provide additional or alternate | ||||||
|  |     search behaviour. | ||||||
|  |  | ||||||
| Custom template options | Custom template options | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
| @@ -1102,6 +1105,39 @@ templates used by the :class:`ModelAdmin` views: | |||||||
|                 else: |                 else: | ||||||
|                     return ['name'] |                     return ['name'] | ||||||
|  |  | ||||||
|  | .. method:: ModelAdmin.get_search_results(self, request, queryset, search_term) | ||||||
|  |  | ||||||
|  |     .. versionadded:: 1.6 | ||||||
|  |  | ||||||
|  |     The ``get_search_results`` method modifies the list of objects displayed in | ||||||
|  |     to those that match the provided search term. It accepts the request, a | ||||||
|  |     queryset that applies the current filters, and the user-provided search term. | ||||||
|  |     It returns a tuple containing a queryset modified to implement the search, and | ||||||
|  |     a boolean indicating if the results may contain duplicates. | ||||||
|  |  | ||||||
|  |     The default implementation searches the fields named in :attr:`ModelAdmin.search_fields`. | ||||||
|  |  | ||||||
|  |     This method may be overridden with your own custom search method. For | ||||||
|  |     example, you might wish to search by an integer field, or use an external | ||||||
|  |     tool such as Solr or Haystack. You must establish if the queryset changes | ||||||
|  |     implemented by your search method may introduce duplicates into the results, | ||||||
|  |     and return ``True`` in the second element of the return value. | ||||||
|  |  | ||||||
|  |     For example, to enable search by integer field, you could use:: | ||||||
|  |  | ||||||
|  |         class PersonAdmin(admin.ModelAdmin): | ||||||
|  |             list_display = ('name', 'age') | ||||||
|  |             search_fields = ('name',) | ||||||
|  |  | ||||||
|  |             def get_search_results(self, request, queryset, search_term): | ||||||
|  |                 queryset, use_distinct = super(PersonAdmin, self).get_search_results(request, queryset, search_term) | ||||||
|  |                 try: | ||||||
|  |                     search_term_as_int = int(search_term) | ||||||
|  |                     queryset |= self.model.objects.filter(age=search_term_as_int) | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 return queryset, use_distinct | ||||||
|  |  | ||||||
| .. method:: ModelAdmin.save_related(self, request, form, formsets, change) | .. method:: ModelAdmin.save_related(self, request, form, formsets, change) | ||||||
|  |  | ||||||
|     The ``save_related`` method is given the ``HttpRequest``, the parent |     The ``save_related`` method is given the ``HttpRequest``, the parent | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, | |||||||
|     Gadget, Villain, SuperVillain, Plot, PlotDetails, CyclicOne, CyclicTwo, |     Gadget, Villain, SuperVillain, Plot, PlotDetails, CyclicOne, CyclicTwo, | ||||||
|     WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper, |     WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper, | ||||||
|     CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping, |     CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping, | ||||||
|     Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, |     Album, Question, Answer, ComplexSortedPerson, PluggableSearchPerson, PrePopulatedPostLargeSlug, | ||||||
|     AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, |     AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, | ||||||
|     AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, |     AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, | ||||||
|     RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice, |     RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice, | ||||||
| @@ -530,6 +530,20 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin): | |||||||
|     colored_name.admin_order_field = 'name' |     colored_name.admin_order_field = 'name' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PluggableSearchPersonAdmin(admin.ModelAdmin): | ||||||
|  |     list_display = ('name', 'age') | ||||||
|  |     search_fields = ('name',) | ||||||
|  |  | ||||||
|  |     def get_search_results(self, request, queryset, search_term): | ||||||
|  |         queryset, use_distinct = super(PluggableSearchPersonAdmin, self).get_search_results(request, queryset, search_term) | ||||||
|  |         try: | ||||||
|  |             search_term_as_int = int(search_term) | ||||||
|  |             queryset |= self.model.objects.filter(age=search_term_as_int) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |         return queryset, use_distinct | ||||||
|  |  | ||||||
|  |  | ||||||
| class AlbumAdmin(admin.ModelAdmin): | class AlbumAdmin(admin.ModelAdmin): | ||||||
|     list_filter = ['title'] |     list_filter = ['title'] | ||||||
|  |  | ||||||
| @@ -733,6 +747,7 @@ site.register(Question) | |||||||
| site.register(Answer) | site.register(Answer) | ||||||
| site.register(PrePopulatedPost, PrePopulatedPostAdmin) | site.register(PrePopulatedPost, PrePopulatedPostAdmin) | ||||||
| site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) | site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) | ||||||
|  | site.register(PluggableSearchPerson, PluggableSearchPersonAdmin) | ||||||
| site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin) | site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin) | ||||||
| site.register(AdminOrderedField, AdminOrderedFieldAdmin) | site.register(AdminOrderedField, AdminOrderedFieldAdmin) | ||||||
| site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) | site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) | ||||||
|   | |||||||
| @@ -591,6 +591,12 @@ class ComplexSortedPerson(models.Model): | |||||||
|     age = models.PositiveIntegerField() |     age = models.PositiveIntegerField() | ||||||
|     is_employee = models.NullBooleanField() |     is_employee = models.NullBooleanField() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PluggableSearchPerson(models.Model): | ||||||
|  |     name = models.CharField(max_length=100) | ||||||
|  |     age = models.PositiveIntegerField() | ||||||
|  |  | ||||||
|  |  | ||||||
| class PrePopulatedPostLargeSlug(models.Model): | class PrePopulatedPostLargeSlug(models.Model): | ||||||
|     """ |     """ | ||||||
|     Regression test for #15938: a large max_length for the slugfield must not |     Regression test for #15938: a large max_length for the slugfield must not | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, | |||||||
|     DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter, |     DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter, | ||||||
|     Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, |     Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, | ||||||
|     FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, |     FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, | ||||||
|     OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, |     OtherStory, ComplexSortedPerson, PluggableSearchPerson, Parent, Child, AdminOrderedField, | ||||||
|     AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, |     AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, | ||||||
|     Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, |     Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, | ||||||
|     Simple, UndeletableObject, Choice, ShortMessage, Telegram) |     Simple, UndeletableObject, Choice, ShortMessage, Telegram) | ||||||
| @@ -2202,6 +2202,20 @@ class AdminSearchTest(TestCase): | |||||||
|         self.assertContains(response, "\n0 persons\n") |         self.assertContains(response, "\n0 persons\n") | ||||||
|         self.assertNotContains(response, "Guido") |         self.assertNotContains(response, "Guido") | ||||||
|  |  | ||||||
|  |     def test_pluggable_search(self): | ||||||
|  |         p1 = PluggableSearchPerson.objects.create(name="Bob", age=10) | ||||||
|  |         p2 = PluggableSearchPerson.objects.create(name="Amy", age=20) | ||||||
|  |  | ||||||
|  |         response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=Bob') | ||||||
|  |         # confirm the search returned one object | ||||||
|  |         self.assertContains(response, "\n1 pluggable search person\n") | ||||||
|  |         self.assertContains(response, "Bob") | ||||||
|  |  | ||||||
|  |         response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=20') | ||||||
|  |         # confirm the search returned one object | ||||||
|  |         self.assertContains(response, "\n1 pluggable search person\n") | ||||||
|  |         self.assertContains(response, "Amy") | ||||||
|  |  | ||||||
|  |  | ||||||
| @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) | @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) | ||||||
| class AdminInheritedInlinesTest(TestCase): | class AdminInheritedInlinesTest(TestCase): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user