Fixed #15961 -- Modified ModelAdmin to allow for custom search methods.
This adds a get_search_results method that users can override to provide custom search strategies. Thanks to Daniele Procida for help with the docs.
This commit is contained in:
parent
b06f6c1618
commit
2d309a7043
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):
|
||||
|
|
Loading…
Reference in New Issue