Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modeladmin search backend #5208

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 4 additions & 8 deletions docs/reference/contrib/modeladmin/indexview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,10 @@ for your model. For example:
**Expected value**: A list or tuple, where each item is the name of a model field
of type ``CharField``, ``TextField``, ``RichTextField`` or ``StreamField``.

Set ``search_fields`` to enable a search box at the top of the index page
for your model. You should add names of any fields on the model that should
be searched whenever somebody submits a search query using the search box.

Searching is all handled via Django's QuerySet API, rather than using Wagtail's
search backend. This means it will work for all models, whatever search backend
your project is using, and without any additional setup or configuration.
If your model is indexed using Wagtail's search backend, setting ``search_fields``
will determine which indexed fields are searched. Otherwise setting ``search_fields``
will allow searching via Django's QuerySet API, rather than using Wagtail's
search backend.

.. _modeladmin_ordering:

Expand Down Expand Up @@ -646,4 +643,3 @@ See the following part of the docs to find out more:

See the following part of the docs to find out more:
:ref:`modeladmin_overriding_views`

7 changes: 4 additions & 3 deletions wagtail/contrib/modeladmin/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .button import ButtonHelper, PageButtonHelper # NOQA
from .permission import PagePermissionHelper, PermissionHelper # NOQA
from .url import AdminURLHelper, PageAdminURLHelper # NOQA
from .button import ButtonHelper, PageButtonHelper # NOQA
from .permission import PagePermissionHelper, PermissionHelper # NOQA
from .search import DjangoORMSearchHandler, WagtailBackendSearchHandler # NOQA
from .url import AdminURLHelper, PageAdminURLHelper # NOQA
49 changes: 49 additions & 0 deletions wagtail/contrib/modeladmin/helpers/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import operator
from functools import reduce

from django.contrib.admin.utils import lookup_needs_distinct
from django.db.models import Q

from wagtail.search.backends import get_search_backend


class ModelAdminSearchHandler:
seb-b marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, queryset, search_fields):
self.queryset = queryset
self.search_fields = search_fields

def search(self, search_term):
"""
Returns a tuple containing a queryset to implement the search,
and a boolean indicating if the results may contain duplicates.
"""
raise NotImplementedError()

ababic marked this conversation as resolved.
Show resolved Hide resolved

class DjangoORMSearchHandler(ModelAdminSearchHandler):
def search(self, search_term):
if not search_term or self.search_fields:
return self.queryset
use_distinct = False
querset = self.queryset
orm_lookups = ['%s__icontains' % str(search_field)
for search_field in self.search_fields]
for bit in search_term.split():
or_queries = [Q(**{orm_lookup: bit})
for orm_lookup in orm_lookups]
querset = querset.filter(reduce(operator.or_, or_queries))
opts = querset.model._meta
for search_spec in orm_lookups:
# Check wether out results may have duplicates, then remove them
if lookup_needs_distinct(opts, search_spec):
use_distinct = True
break
return querset, use_distinct


class WagtailBackendSearchHandler(ModelAdminSearchHandler):
def search(self, search_term):
backend = get_search_backend()
if self.search_fields:
return backend.search(search_term, self.queryset, fields=self.search_fields), False
return backend.search(search_term, self.queryset), False
5 changes: 3 additions & 2 deletions wagtail/contrib/modeladmin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from wagtail.core.models import Page

from .helpers import (
AdminURLHelper, ButtonHelper, PageAdminURLHelper, PageButtonHelper, PagePermissionHelper,
PermissionHelper)
AdminURLHelper, ButtonHelper, DjangoORMSearchHandler, PageAdminURLHelper, PageButtonHelper,
PagePermissionHelper, PermissionHelper)
from .menus import GroupMenuItem, ModelAdminMenuItem, SubMenu
from .mixins import ThumbnailMixin # NOQA
from .views import ChooseParentView, CreateView, DeleteView, EditView, IndexView, InspectView
Expand Down Expand Up @@ -96,6 +96,7 @@ class ModelAdmin(WagtailRegisterable):
inspect_template_name = ''
delete_template_name = ''
choose_parent_template_name = ''
search_handler_class = DjangoORMSearchHandler
permission_helper_class = None
url_helper_class = None
button_helper_class = None
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load i18n static %}
{% if view.search_fields %}
{% if view.show_search %}
<form id="changelist-search" class="col search-form" action="{{ view.index_url }}" method="get">
<ul class="fields">
<li class="required">
Expand Down
6 changes: 3 additions & 3 deletions wagtail/contrib/modeladmin/tests/test_page_modeladmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ def test_filter(self):
self.assertEqual(eventpage.audience, 'public')

def test_search(self):
response = self.get(q='Someone')
response = self.get(q='moon')

self.assertEqual(response.status_code, 200)

# There are two eventpage's where the title contains 'Someone'
self.assertEqual(response.context['result_count'], 1)
# There are two eventpage's where the location is 'The moon'
self.assertEqual(response.context['result_count'], 2)

def test_ordering(self):
response = self.get(o='0.1')
Expand Down
9 changes: 8 additions & 1 deletion wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_filter(self):
for book in response.context['object_list']:
self.assertEqual(book.author_id, 1)

def test_search(self):
def test_search_indexed(self):
response = self.get(q='of')

self.assertEqual(response.status_code, 200)
Expand Down Expand Up @@ -109,6 +109,13 @@ def setUp(self):
def get(self, **params):
return self.client.get('/admin/modeladmintest/author/', params)

def test_search(self):
response = self.get(q='Roald Dahl')

self.assertEqual(response.status_code, 200)

self.assertEqual(response.context['result_count'], 2)

def test_col_extra_class_names(self):
response = self.get()
self.assertEqual(response.status_code, 200)
Expand Down
32 changes: 12 additions & 20 deletions wagtail/contrib/modeladmin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
from django.views.generic.edit import FormView

from wagtail.admin import messages
from wagtail.search.backends import get_search_backend
from wagtail.search.index import Indexed

from .forms import ParentChooserForm


try:
from django.db.models.sql.constants import QUERY_TERMS
except ImportError:
Expand Down Expand Up @@ -240,6 +241,7 @@ def dispatch(self, request, *args, **kwargs):
self.search_fields = self.model_admin.get_search_fields(request)
self.items_per_page = self.model_admin.list_per_page
self.select_related = self.model_admin.list_select_related
self.search_handler_class = self.model_admin.search_handler_class

# Get search parameters from the query string.
try:
Expand Down Expand Up @@ -270,25 +272,8 @@ def get_buttons_for_obj(self, obj):
obj, classnames_add=['button-small', 'button-secondary'])

def get_search_results(self, request, queryset, search_term):
"""
Returns a tuple containing a queryset to implement the search,
and a boolean indicating if the results may contain duplicates.
"""
use_distinct = False
if self.search_fields and search_term:
orm_lookups = ['%s__icontains' % 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
SearchHandler = self.search_handler_class(queryset, self.search_fields)
return SearchHandler.search(search_term)

def lookup_allowed(self, lookup, value):
# Check FKey lookups that are allowed, so that popups produced by
Expand Down Expand Up @@ -657,6 +642,13 @@ def get_context_data(self, **kwargs):
context.update(kwargs)
return super().get_context_data(**context)

@property
def show_search(self):
"""
Whether or not to show the search form on the index, used by the search_form tag
seb-b marked this conversation as resolved.
Show resolved Hide resolved
"""
return issubclass(self.model, Indexed) or self.search_fields

def get_template_names(self):
return self.model_admin.get_index_template()

Expand Down
4 changes: 4 additions & 0 deletions wagtail/tests/modeladmintest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class Book(models.Model, index.Indexed):
title = models.CharField(max_length=255)
cover_image = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, null=True, blank=True)

search_fields = [
index.SearchField('title'),
]

def __str__(self):
return self.title

Expand Down
1 change: 0 additions & 1 deletion wagtail/tests/modeladmintest/wagtail_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ class EventPageAdmin(ModelAdmin):
model = EventPage
list_display = ('title', 'date_from', 'audience')
list_filter = ('audience', )
search_fields = ('title', )
inspect_view_enabled = True
inspect_view_fields_exclude = ('feed_image', )

Expand Down