Skip to content

Commit

Permalink
Improved the way selected_facets are handled.
Browse files Browse the repository at this point in the history
* ``selected_facets`` may be provided multiple times.
* Facet values are quoted to avoid backend confusion (i.e. `author:Joe Blow` is seen by Solr as `author:Joe AND Blow` rather than the expected `author:"Joe Blow"`)
  • Loading branch information
acdha authored and toastdriven committed May 2, 2011
1 parent 4df88b2 commit 7edac19
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 10 deletions.
7 changes: 5 additions & 2 deletions docs/views_and_forms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ The functional view provides an example of how Haystack can be used in more
traditional settings or as an example of how to write a more complex custom
view. It is also thread-safe.

``SearchView(template=None, load_all=True, form_class=ModelSearchForm, searchqueryset=None, context_class=RequestContext, results_per_page=None)``
``SearchView(template=None, load_all=True, form_class=None, searchqueryset=None, context_class=RequestContext, results_per_page=None)``
--------------------------------------------------------------------------------------------------------------------------------------------------

The ``SearchView`` is designed to be easy/flexible enough to override common
Expand Down Expand Up @@ -158,7 +158,7 @@ URLconf should look something like::
url(r'^$', SearchView(
template='my/special/path/john_search.html',
searchqueryset=sqs,
form_class=ModelSearchForm
form_class=SearchForm
), name='haystack_search'),
)
Expand All @@ -180,6 +180,9 @@ URLconf should look something like::
``search_view_factory`` function, which returns thread-safe instances of
``SearchView``.

By default, if you don't specify a ``form_class``, the view will use the
``haystack.forms.ModelSearchForm`` form.

Beyond this customizations, you can create your own ``SearchView`` and
extend/override the following methods to change the functionality.

Expand Down
16 changes: 13 additions & 3 deletions haystack/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,23 @@ def search(self):


class FacetedSearchForm(SearchForm):
selected_facets = forms.CharField(required=False, widget=forms.HiddenInput)
def __init__(self, *args, **kwargs):
self.selected_facets = kwargs.pop("selected_facets", [])
super(FacetedSearchForm, self).__init__(*args, **kwargs)

def search(self):
sqs = super(FacetedSearchForm, self).search()

if hasattr(self, 'cleaned_data') and self.cleaned_data['selected_facets']:
sqs = sqs.narrow(self.cleaned_data['selected_facets'])
# We need to process each facet to ensure that the field name and the
# value are quoted correctly and separately:
for facet in self.selected_facets:
if ":" not in facet:
continue

field, value = facet.split(":", 1)

if value:
sqs = sqs.narrow(u'%s:"%s"' % (field, sqs.query.clean(value)))

return sqs

Expand Down
24 changes: 22 additions & 2 deletions haystack/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.http import Http404
from django.shortcuts import render_to_response
from django.template import RequestContext
from haystack.forms import ModelSearchForm
from haystack.forms import ModelSearchForm, FacetedSearchForm
from haystack.query import EmptySearchQuerySet


Expand All @@ -20,12 +20,15 @@ class SearchView(object):
form = None
results_per_page = RESULTS_PER_PAGE

def __init__(self, template=None, load_all=True, form_class=ModelSearchForm, searchqueryset=None, context_class=RequestContext, results_per_page=None):
def __init__(self, template=None, load_all=True, form_class=None, searchqueryset=None, context_class=RequestContext, results_per_page=None):
self.load_all = load_all
self.form_class = form_class
self.context_class = context_class
self.searchqueryset = searchqueryset

if form_class is None:
self.form_class = ModelSearchForm

if not results_per_page is None:
self.results_per_page = results_per_page

Expand Down Expand Up @@ -139,6 +142,23 @@ def search_view(request):
class FacetedSearchView(SearchView):
__name__ = 'FacetedSearchView'

def __init__(self, *args, **kwargs):
# Needed to switch out the default form class.
if kwargs.get('form_class') is None:
kwargs['form_class'] = FacetedSearchForm

super(FacetedSearchView, self).__init__(*args, **kwargs)

def build_form(self, form_kwargs=None):
if form_kwargs is None:
form_kwargs = {}

# This way the form can always receive a list containing zero or more
# facet expressions:
form_kwargs['selected_facets'] = self.request.GET.getlist("selected_facets")

return super(FacetedSearchView, self).build_form(form_kwargs)

def extra_context(self):
extra = super(FacetedSearchView, self).extra_context()
extra['facets'] = self.results.facet_counts()
Expand Down
59 changes: 58 additions & 1 deletion tests/core/tests/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.test import TestCase
from haystack.forms import SearchForm, ModelSearchForm, model_choices
from haystack.forms import SearchForm, ModelSearchForm, model_choices, FacetedSearchForm
import haystack
from haystack.sites import SearchSite
from haystack.query import SearchQuerySet, EmptySearchQuerySet
Expand Down Expand Up @@ -73,3 +73,60 @@ def test_model_choices(self):
mis.register(AnotherMockModel)
self.assertEqual(len(model_choices(site=mis)), 2)
self.assertEqual([option[1] for option in model_choices(site=mis)], [u'Another mock models', u'Mock models'])


class FacetedSearchFormTestCase(TestCase):
def setUp(self):
super(FacetedSearchFormTestCase, self).setUp()
mock_index_site = SearchSite()
mock_index_site.register(MockModel)
mock_index_site.register(AnotherMockModel)

# Stow.
self.old_site = haystack.site
haystack.site = mock_index_site

self.sqs = SearchQuerySet(query=DummySearchQuery(backend=DummySearchBackend()), site=mock_index_site)

def tearDown(self):
haystack.site = self.old_site
super(FacetedSearchFormTestCase, self).tearDown()

def test_init_with_selected_facets(self):
sf = FacetedSearchForm({}, searchqueryset=self.sqs)
self.assertEqual(sf.errors, {})
self.assertEqual(sf.is_valid(), True)
self.assertEqual(sf.selected_facets, [])

sf = FacetedSearchForm({}, selected_facets=[], searchqueryset=self.sqs)
self.assertEqual(sf.errors, {})
self.assertEqual(sf.is_valid(), True)
self.assertEqual(sf.selected_facets, [])

sf = FacetedSearchForm({}, selected_facets=['author:daniel'], searchqueryset=self.sqs)
self.assertEqual(sf.errors, {})
self.assertEqual(sf.is_valid(), True)
self.assertEqual(sf.selected_facets, ['author:daniel'])

sf = FacetedSearchForm({}, selected_facets=['author:daniel', 'author:chris'], searchqueryset=self.sqs)
self.assertEqual(sf.errors, {})
self.assertEqual(sf.is_valid(), True)
self.assertEqual(sf.selected_facets, ['author:daniel', 'author:chris'])

def test_search(self):
sf = FacetedSearchForm({'q': 'test'}, selected_facets=[], searchqueryset=self.sqs)
sqs = sf.search()
self.assertEqual(sqs.query.narrow_queries, set())

# Test the "skip no-colon" bits.
sf = FacetedSearchForm({'q': 'test'}, selected_facets=['authordaniel'], searchqueryset=self.sqs)
sqs = sf.search()
self.assertEqual(sqs.query.narrow_queries, set())

sf = FacetedSearchForm({'q': 'test'}, selected_facets=['author:daniel'], searchqueryset=self.sqs)
sqs = sf.search()
self.assertEqual(sqs.query.narrow_queries, set([u'author:"daniel"']))

sf = FacetedSearchForm({'q': 'test'}, selected_facets=['author:daniel', 'author:chris'], searchqueryset=self.sqs)
sqs = sf.search()
self.assertEqual(sqs.query.narrow_queries, set([u'author:"daniel"', u'author:"chris"']))
25 changes: 23 additions & 2 deletions tests/core/tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from django.core.urlresolvers import reverse
from django.conf import settings
from django import forms
from django.http import HttpRequest
from django.http import HttpRequest, QueryDict
from django.test import TestCase
import haystack
from haystack.forms import model_choices, SearchForm, ModelSearchForm
from haystack.forms import model_choices, SearchForm, ModelSearchForm, FacetedSearchForm
from haystack.query import EmptySearchQuerySet
from haystack.sites import SearchSite
from haystack.views import SearchView, FacetedSearchView, search_view_factory
Expand Down Expand Up @@ -144,8 +144,29 @@ def test_search_no_query(self):
def test_empty_results(self):
fsv = FacetedSearchView()
fsv.request = HttpRequest()
fsv.request.GET = QueryDict('')
fsv.form = fsv.build_form()
self.assert_(isinstance(fsv.get_results(), EmptySearchQuerySet))

def test_default_form(self):
fsv = FacetedSearchView()
fsv.request = HttpRequest()
fsv.request.GET = QueryDict('')
fsv.form = fsv.build_form()
self.assert_(isinstance(fsv.form, FacetedSearchForm))

def test_list_selected_facets(self):
fsv = FacetedSearchView()
fsv.request = HttpRequest()
fsv.request.GET = QueryDict('')
fsv.form = fsv.build_form()
self.assertEqual(fsv.form.selected_facets, [])

fsv = FacetedSearchView()
fsv.request = HttpRequest()
fsv.request.GET = QueryDict('selected_facets=author:daniel&selected_facets=author:chris')
fsv.form = fsv.build_form()
self.assertEqual(fsv.form.selected_facets, [u'author:daniel', u'author:chris'])


class BasicSearchViewTestCase(TestCase):
Expand Down

0 comments on commit 7edac19

Please sign in to comment.