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

Added generic class based search views #1130

Merged
merged 10 commits into from Jan 14, 2015
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -36,6 +36,7 @@ env:
- TOX_ENV=py33-django1.7
- TOX_ENV=py34-django1.5
- TOX_ENV=py34-django1.6
- TOX_ENV=py34-django1.7
- TOX_ENV=pypy-django1.5
- TOX_ENV=pypy-django1.6
- TOX_ENV=pypy-django1.7
Expand Down
126 changes: 126 additions & 0 deletions haystack/generic_views.py
@@ -0,0 +1,126 @@
from __future__ import unicode_literals

from django.conf import settings
from django.core.paginator import Paginator
from django.views.generic import FormView
from django.views.generic.edit import FormMixin
from django.views.generic.list import MultipleObjectMixin

from .forms import FacetedSearchForm
from .forms import ModelSearchForm
from .query import SearchQuerySet


RESULTS_PER_PAGE = getattr(settings, 'HAYSTACK_SEARCH_RESULTS_PER_PAGE', 20)


class SearchMixin(MultipleObjectMixin, FormMixin):
"""
A mixin that allows adding in Haystacks search functionality into
another view class.

This mixin exhibits similar end functionality as the base Haystack search
view, but with some important distinctions oriented around greater
compatibility with Django's built-in class based views and mixins.

Normal flow:

self.request = request

self.form = self.build_form()
self.query = self.get_query()
self.results = self.get_results()

return self.create_response()

This mixin should:

1. Make the form
2. Get the queryset
3. Return the paginated queryset

"""
template_name = 'search/search.html'
load_all = True
form_class = ModelSearchForm
queryset = SearchQuerySet()
context_object_name = None
paginate_by = RESULTS_PER_PAGE
paginate_orphans = 0
paginator_class = Paginator
page_kwarg = 'page'
form_name = 'form'
search_field = 'q'
object_list = None

def get_form_kwargs(self):
"""
Returns the keyword arguments for instantiating the form.
"""
kwargs = {'initial': self.get_initial()}
if self.request.method == 'GET':
kwargs.update({
'data': self.request.GET,
})
kwargs.update({'searchqueryset': self.get_queryset()})
return kwargs

def form_invalid(self, form):
context = self.get_context_data(**{
self.form_name: form,
'object_list': self.get_queryset()
})
return self.render_to_response(context)

def form_valid(self, form):
self.queryset = form.search()
context = self.get_context_data(**{
self.form_name: form,
'query': form.cleaned_data.get(self.search_field),
'object_list': self.queryset
})
return self.render_to_response(context)


class FacetedSearchMixin(SearchMixin):
"""
A mixin that allows adding in a Haystack search functionality with search
faceting.
"""
form_class = FacetedSearchForm

def get_form_kwargs(self):
kwargs = super(SearchMixin, self).get_form_kwargs()
kwargs.update({
'selected_facets': self.request.GET.getlist("selected_facets")
})
return kwargs

def get_context_data(self, **kwargs):
context = super(FacetedSearchMixin, self).get_context_data(**kwargs)
context.update({'facets': self.results.facet_counts()})
return context


class SearchView(SearchMixin, FormView):
"""A view class for searching a Haystack managed search index"""

def get(self, request, *args, **kwargs):
"""
Handles GET requests and instantiates a blank version of the form.
"""
form_class = self.get_form_class()
form = self.get_form(form_class)

if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)


class FacetedSearchView(FacetedSearchMixin, SearchView):
"""
A view class for searching a Haystack managed search index with
facets
"""
pass
10 changes: 8 additions & 2 deletions haystack/models.py
Expand Up @@ -95,7 +95,13 @@ def _set_object(self, obj):

def _get_model(self):
if self._model is None:
self._model = models.get_model(self.app_label, self.model_name)
try:
self._model = models.get_model(self.app_label, self.model_name)
except LookupError:
# this changed in change 1.7 to throw an error instead of
# returning None when the model isn't found. So catch the
# lookup error and keep self._model == None.
pass
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_model raises LookupError instead of returning None when no model is found.

https://docs.djangoproject.com/en/1.7/releases/1.7/#introspecting-applications

This is also the first part of the fix to #1069.


return self._model

Expand Down Expand Up @@ -127,7 +133,7 @@ def _get_distance(self):
if location_field is None:
return None

lf_lng, lf_lat = location_field.get_coords()
lf_lng, lf_lat = location_field.get_coords()
self._distance = Distance(km=geopy_distance.distance((po_lat, po_lng), (lf_lat, lf_lng)).km)

# We've either already calculated it or the backend returned it, so
Expand Down
10 changes: 7 additions & 3 deletions test_haystack/elasticsearch_tests/test_elasticsearch_backend.py
Expand Up @@ -201,7 +201,11 @@ def get_model(self):
class TestSettings(TestCase):
def test_kwargs_are_passed_on(self):
from haystack.backends.elasticsearch_backend import ElasticsearchSearchBackend
backend = ElasticsearchSearchBackend('alias', **{'URL': {}, 'INDEX_NAME': 'testing', 'KWARGS': {'max_retries': 42}})
backend = ElasticsearchSearchBackend('alias', **{
'URL': settings.HAYSTACK_CONNECTIONS['elasticsearch']['URL'],
'INDEX_NAME': 'testing',
'KWARGS': {'max_retries': 42}
})

self.assertEqual(backend.conn.transport.max_retries, 42)

Expand Down Expand Up @@ -444,7 +448,7 @@ def test_build_schema(self):

(content_field_name, mapping) = self.sb.build_schema(old_ui.all_searchfields())
self.assertEqual(content_field_name, 'text')
self.assertEqual(len(mapping), 4+2) # +2 management fields
self.assertEqual(len(mapping), 4 + 2) # +2 management fields
self.assertEqual(mapping, {
'django_id': {'index': 'not_analyzed', 'type': 'string', 'include_in_all': False},
'django_ct': {'index': 'not_analyzed', 'type': 'string', 'include_in_all': False},
Expand All @@ -458,7 +462,7 @@ def test_build_schema(self):
ui.build(indexes=[ElasticsearchComplexFacetsMockSearchIndex()])
(content_field_name, mapping) = self.sb.build_schema(ui.all_searchfields())
self.assertEqual(content_field_name, 'text')
self.assertEqual(len(mapping), 15+2) # +2 management fields
self.assertEqual(len(mapping), 15 + 2) # +2 management fields
self.assertEqual(mapping, {
'django_id': {'index': 'not_analyzed', 'type': 'string', 'include_in_all': False},
'django_ct': {'index': 'not_analyzed', 'type': 'string', 'include_in_all': False},
Expand Down
2 changes: 1 addition & 1 deletion test_haystack/solr_tests/test_templatetags.py
Expand Up @@ -56,6 +56,6 @@ def test_more_like_this_for_model(self, mock_sqs):
call().models().more_like_this().__getitem__(slice(None, 5))],
any_order=True)

if django.get_version() == '1.7':
if django.VERSION >= (1, 7, 0):
# FIXME: https://github.com/toastdriven/django-haystack/issues/1069
test_more_like_this_for_model = unittest.expectedFailure(test_more_like_this_for_model)
2 changes: 1 addition & 1 deletion test_haystack/test_models.py
Expand Up @@ -143,7 +143,7 @@ def test_missing_object(self):
self.assertEqual(awol2.stored, None)
self.assertEqual(len(CaptureHandler.logs_seen), 12)

if django.get_version() == '1.7':
if django.VERSION >= (1, 7, 0):
# FIXME: https://github.com/toastdriven/django-haystack/issues/1069
test_missing_object = unittest.expectedFailure(test_missing_object)

Expand Down