mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +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/> | ||||
|     Shannon -jj Behrens <http://jjinux.blogspot.com/> | ||||
|     Esdras Beleza <linux@esdrasbeleza.com> | ||||
|     Božidar Benko <bbenko@gmail.com> | ||||
|     Chris Bennett <chrisrbennett@yahoo.com> | ||||
|     Danilo Bargen | ||||
|     Shai Berger <shai@platonix.com> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import copy | ||||
| import operator | ||||
| from functools import update_wrapper, partial | ||||
|  | ||||
| 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.admin import widgets, helpers | ||||
| 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.templatetags.admin_static import static | ||||
| from django.contrib import messages | ||||
| @@ -255,6 +256,34 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): | ||||
|         """ | ||||
|         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): | ||||
|         """ | ||||
|         Returns a QuerySet of all model instances that can be edited by the | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| import operator | ||||
| import sys | ||||
| import warnings | ||||
| from functools import reduce | ||||
|  | ||||
| from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured | ||||
| from django.core.paginator import InvalidPage | ||||
| @@ -331,7 +329,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): | ||||
|     def get_queryset(self, request): | ||||
|         # First, we collect all the declared list filters. | ||||
|         (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. | ||||
|         qs = self.root_queryset | ||||
| @@ -378,31 +376,11 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): | ||||
|         ordering = self.get_ordering(request, qs) | ||||
|         qs = qs.order_by(*ordering) | ||||
|  | ||||
|         # 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 | ||||
|         # Apply search results | ||||
|         qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query) | ||||
|  | ||||
|         if self.search_fields and self.query: | ||||
|             orm_lookups = [construct_search(str(search_field)) | ||||
|                            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: | ||||
|         # Remove duplicates from results, if neccesary | ||||
|         if filters_use_distinct | search_use_distinct: | ||||
|             return qs.distinct() | ||||
|         else: | ||||
|             return qs | ||||
|   | ||||
| @@ -1005,6 +1005,9 @@ subclass:: | ||||
|         Performs a full-text match. This is like the default search method but | ||||
|         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 | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| @@ -1102,6 +1105,39 @@ templates used by the :class:`ModelAdmin` views: | ||||
|                 else: | ||||
|                     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) | ||||
|  | ||||
|     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, | ||||
|     WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper, | ||||
|     CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping, | ||||
|     Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, | ||||
|     Album, Question, Answer, ComplexSortedPerson, PluggableSearchPerson, PrePopulatedPostLargeSlug, | ||||
|     AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, | ||||
|     AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, | ||||
|     RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice, | ||||
| @@ -530,6 +530,20 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin): | ||||
|     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): | ||||
|     list_filter = ['title'] | ||||
|  | ||||
| @@ -733,6 +747,7 @@ site.register(Question) | ||||
| site.register(Answer) | ||||
| site.register(PrePopulatedPost, PrePopulatedPostAdmin) | ||||
| site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) | ||||
| site.register(PluggableSearchPerson, PluggableSearchPersonAdmin) | ||||
| site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin) | ||||
| site.register(AdminOrderedField, AdminOrderedFieldAdmin) | ||||
| site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) | ||||
|   | ||||
| @@ -591,6 +591,12 @@ class ComplexSortedPerson(models.Model): | ||||
|     age = models.PositiveIntegerField() | ||||
|     is_employee = models.NullBooleanField() | ||||
|  | ||||
|  | ||||
| class PluggableSearchPerson(models.Model): | ||||
|     name = models.CharField(max_length=100) | ||||
|     age = models.PositiveIntegerField() | ||||
|  | ||||
|  | ||||
| class PrePopulatedPostLargeSlug(models.Model): | ||||
|     """ | ||||
|     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, | ||||
|     Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, | ||||
|     FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, | ||||
|     OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, | ||||
|     OtherStory, ComplexSortedPerson, PluggableSearchPerson, Parent, Child, AdminOrderedField, | ||||
|     AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, | ||||
|     Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, | ||||
|     Simple, UndeletableObject, Choice, ShortMessage, Telegram) | ||||
| @@ -2202,6 +2202,20 @@ class AdminSearchTest(TestCase): | ||||
|         self.assertContains(response, "\n0 persons\n") | ||||
|         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',)) | ||||
| class AdminInheritedInlinesTest(TestCase): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user