Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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.
  • Loading branch information...
commit 2d309a7043e3625cfeeadbc252322e5599dfffc0 1 parent b06f6c1
@bbenko bbenko authored
View
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>
View
31 django/contrib/admin/options.py
@@ -1,4 +1,5 @@
import copy
+import operator
from functools import update_wrapper, partial
from django import forms
@@ -9,7 +10,7 @@
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 @@ def get_prepopulated_fields(self, request, obj=None):
"""
return self.prepopulated_fields
+ def get_search_results(self, request, queryset, search_term):
@loic Collaborator
loic added a note

Does this method really need to be on BaseModelAdmin or should it be ModelAdmin?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # 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
View
32 django/contrib/admin/views/main.py
@@ -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 @@ def get_ordering_field_columns(self):
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 @@ def get_queryset(self, request):
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
-
- 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
+ # Apply search results
+ qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query)
- if use_distinct:
+ # Remove duplicates from results, if neccesary
+ if filters_use_distinct | search_use_distinct:
return qs.distinct()
else:
return qs
View
36 docs/ref/contrib/admin/index.txt
@@ -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
View
17 tests/admin_views/admin.py
@@ -24,7 +24,7 @@
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 @@ def colored_name(self, obj):
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 @@ class ChoiceList(admin.ModelAdmin):
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)
View
6 tests/admin_views/models.py
@@ -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
View
16 tests/admin_views/tests.py
@@ -46,7 +46,7 @@
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 @@ def test_beginning_matches(self):
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):
Please sign in to comment.
Something went wrong with that request. Please try again.