From f7610fa7c9eccb96559490787a3f73e74d8b9812 Mon Sep 17 00:00:00 2001 From: Don Naegely Date: Thu, 20 Nov 2014 15:02:01 -0500 Subject: [PATCH] Add fallback search for case where Haystack isn't installed Signed-off-by: Don Naegely --- avocado/managers.py | 67 ++++++++-- tests/cases/search/fixtures/search.json | 158 ++++++++++++------------ tests/cases/search/tests.py | 51 ++++++++ 3 files changed, 188 insertions(+), 88 deletions(-) diff --git a/avocado/managers.py b/avocado/managers.py index aa6d078..6d00a4e 100644 --- a/avocado/managers.py +++ b/avocado/managers.py @@ -5,7 +5,7 @@ from django.conf import settings as djsettings from django.core.exceptions import ImproperlyConfigured from django.db.models.manager import ManagerDescriptor -from avocado.conf import OPTIONAL_DEPS, requires_dep, settings +from avocado.conf import OPTIONAL_DEPS, dep_supported, settings from avocado.core.managers import PublishedManager, PublishedQuerySet @@ -13,9 +13,7 @@ class DataSearchMixin(models.Manager): - @requires_dep('haystack') - def search(self, content, queryset=None, max_results=None, partial=False, - using=None): + def _haystack_search(self, content, queryset, max_results, partial, using): from haystack.query import RelatedSearchQuerySet from haystack.inputs import AutoQuery # Limit to the model bound to this manager, e.g. DataConcept. @@ -29,11 +27,11 @@ def search(self, content, queryset=None, max_results=None, partial=False, sqs = sqs.using(using) if partial: - # Autocomplete only works with N-gram fields + # Autocomplete only works with N-gram fields. sqs = sqs.autocomplete(text_auto=content) else: - # Automatically handles advanced search syntax for negations and - # quoted strings + # Automatically handles advanced search syntax for negations + # and quoted strings. sqs = sqs.filter(text=AutoQuery(content)) if queryset is not None: @@ -44,6 +42,57 @@ def search(self, content, queryset=None, max_results=None, partial=False, return sqs + def _basic_search(self, content, queryset): + """Provides the most basic search possible for the Data* models. + + Individual managers must override this method to actually implement + the basic search across the pertinent fields for the specific Data* + model. + """ + raise NotImplemented('Subclasses must define this method.') + + def search(self, content, queryset=None, max_results=None, partial=False, + using=None): + if dep_supported('haystack'): + return self._haystack_search( + content, queryset, max_results, partial, using) + + return self._basic_search(content, queryset) + + +class DataFieldSearchMixin(DataSearchMixin): + def _basic_search(self, content, queryset): + if queryset is None: + queryset = self.model.objects.all() + + q = Q(name__icontains=content) | \ + Q(description__icontains=content) | \ + Q(keywords__icontains=content) | \ + Q(model_name__icontains=content) | \ + Q(category__name__icontains=content) | \ + Q(category__description__icontains=content) | \ + Q(category__keywords__icontains=content) + + return queryset.filter(q) + + +class DataConceptSearchMixin(DataSearchMixin): + def _basic_search(self, content, queryset): + if queryset is None: + queryset = self.model.objects.all() + + q = Q(name__icontains=content) | \ + Q(description__icontains=content) | \ + Q(keywords__icontains=content) | \ + Q(fields__name__icontains=content) | \ + Q(fields__description__icontains=content) | \ + Q(fields__keywords__icontains=content) | \ + Q(category__name__icontains=content) | \ + Q(category__description__icontains=content) | \ + Q(category__keywords__icontains=content) + + return queryset.filter(q) + class DataFieldQuerySet(PublishedQuerySet): def published(self, user=None, perm='avocado.view_datafield'): @@ -130,7 +179,7 @@ def __get__(self, instance, type=None): return super(DataFieldManagerDescriptor, self).__get__(instance, type) -class DataFieldManager(PublishedManager, DataSearchMixin): +class DataFieldManager(PublishedManager, DataFieldSearchMixin): "Manager for the `DataField` model." def contribute_to_class(self, model, name): @@ -165,7 +214,7 @@ def get_by_natural_key(self, app_name, model_name=None, field_name=None): return queryset.get(**dict(zip(keys, values))) -class DataConceptManager(PublishedManager, DataSearchMixin): +class DataConceptManager(PublishedManager, DataConceptSearchMixin): "Manager for the `DataConcept` model." def get_query_set(self): return DataConceptQuerySet(self.model, using=self._db) diff --git a/tests/cases/search/fixtures/search.json b/tests/cases/search/fixtures/search.json index 676de49..1ec16db 100644 --- a/tests/cases/search/fixtures/search.json +++ b/tests/cases/search/fixtures/search.json @@ -1,137 +1,137 @@ [ { - "pk": 1, - "model": "search.office", + "pk": 1, + "model": "search.office", "fields": { "location": "3535 Market" } - }, + }, { - "pk": 1, - "model": "search.title", + "pk": 1, + "model": "search.title", "fields": { - "salary": 15000, - "name": "Programmer", + "salary": 15000, + "name": "Programmer", "boss": false } - }, + }, { - "pk": 2, - "model": "search.title", + "pk": 2, + "model": "search.title", "fields": { - "salary": 20000, - "name": "Analyst", + "salary": 20000, + "name": "Analyst", "boss": false } - }, + }, { - "pk": 3, - "model": "search.title", + "pk": 3, + "model": "search.title", "fields": { - "salary": 15000, - "name": "QA", + "salary": 15000, + "name": "QA", "boss": false } - }, + }, { - "pk": 4, - "model": "search.title", + "pk": 4, + "model": "search.title", "fields": { - "salary": 200000, - "name": "CEO", + "salary": 200000, + "name": "CEO", "boss": true } - }, + }, { - "pk": 5, - "model": "search.title", + "pk": 5, + "model": "search.title", "fields": { - "salary": 15000, - "name": "IT", + "salary": 15000, + "name": "IT", "boss": false } - }, + }, { - "pk": 6, - "model": "search.title", + "pk": 6, + "model": "search.title", "fields": { - "salary": 10000, - "name": "Guard", + "salary": 10000, + "name": "Guard", "boss": false } - }, + }, { - "pk": 7, - "model": "search.title", + "pk": 7, + "model": "search.title", "fields": { - "salary": 100000, - "name": "Lawyer", + "salary": 100000, + "name": "Lawyer", "boss": false } - }, + }, { - "pk": 1, - "model": "search.employee", + "pk": 1, + "model": "search.employee", "fields": { - "first_name": "Eric", - "last_name": "Smith", - "is_manager": true, - "office": 1, + "first_name": "Eric", + "last_name": "Smith", + "is_manager": true, + "office": 1, "title": 1 } - }, + }, { - "pk": 2, - "model": "search.employee", + "pk": 2, + "model": "search.employee", "fields": { - "first_name": "Erin", - "last_name": "Jones", - "is_manager": false, - "office": 1, + "first_name": "Erin", + "last_name": "Jones", + "is_manager": false, + "office": 1, "title": 2 } - }, + }, { - "pk": 3, - "model": "search.employee", + "pk": 3, + "model": "search.employee", "fields": { - "first_name": "Erick", - "last_name": "Smith", - "is_manager": false, - "office": 1, + "first_name": "Erick", + "last_name": "Smith", + "is_manager": false, + "office": 1, "title": 1 } - }, + }, { - "pk": 4, - "model": "search.employee", + "pk": 4, + "model": "search.employee", "fields": { - "first_name": "Aaron", - "last_name": "Harris", - "is_manager": false, - "office": 1, + "first_name": "Aaron", + "last_name": "Harris", + "is_manager": false, + "office": 1, "title": 2 } - }, + }, { - "pk": 5, - "model": "search.employee", + "pk": 5, + "model": "search.employee", "fields": { - "first_name": "Zac", - "last_name": "Cook", - "is_manager": false, - "office": 1, + "first_name": "Zac", + "last_name": "Cook", + "is_manager": false, + "office": 1, "title": 1 } - }, + }, { - "pk": 6, - "model": "search.employee", + "pk": 6, + "model": "search.employee", "fields": { - "first_name": "Mel", - "last_name": "Brooks", - "is_manager": false, - "office": 1, + "first_name": "Mel", + "last_name": "Brooks", + "is_manager": false, + "office": 1, "title": 2 } } diff --git a/tests/cases/search/tests.py b/tests/cases/search/tests.py index 63d6b69..06c2cd3 100644 --- a/tests/cases/search/tests.py +++ b/tests/cases/search/tests.py @@ -2,6 +2,7 @@ from django.core import management from haystack.query import RelatedSearchQuerySet from avocado.models import DataField, DataConcept, DataCategory +from avocado.conf import OPTIONAL_DEPS class SearchTest(TestCase): @@ -24,6 +25,56 @@ def setUp(self): verbosity=0) +class NoHaystackSearchTest(TestCase): + fixtures = ['search.json'] + + def setUp(self): + management.call_command('avocado', 'init', 'search', quiet=True) + + # Create categories for test purposes. + category = DataCategory(name='Avocado', published=True) + category.save() + + DataField.objects.update(category=category) + DataConcept.objects.update(category=category) + + self._haystack = OPTIONAL_DEPS['haystack'] + OPTIONAL_DEPS['haystack'] = None + + def test_field_search(self): + # This basic search doesn't really hit the data itself, just the + # metadata for the field so this normally successful search term + # should return 0 results. + self.assertEqual(len(DataField.objects.search('Erick')), 0) + + # Now search using terms that appear in the metadata and we should + # get results back now. This covers both field and field.category + # metadata. + self.assertEqual(len(DataField.objects.search('avoc')), 12) + self.assertEqual(len(DataField.objects.search('employee')), 3) + + def test_concept_search(self): + # This basic search doesn't really hit the data itself, just the + # metadata for the concept so this normally successful search term + # should return 0 results. + self.assertEqual(len(DataField.objects.search('Erick')), 0) + + # Now search using terms that appear in the metadata and we should + # get results back now. This covers both concept and concept.field + # metadata. + self.assertEqual(len(DataConcept.objects.search('Boss')), 1) + self.assertEqual(len(DataConcept.objects.search('address')), 0) + + # Add a description to the location field to make sure that concept + # search hits the field level. + location = DataField.objects\ + .get_by_natural_key('search.office.location') + location.description = 'Address' + + def tearDown(self): + OPTIONAL_DEPS['haystack'] = self._haystack + + class ExcludedIndexSearchTest(SearchTest): def test_field_search(self): search = DataField.objects.search