Skip to content
This repository has been archived by the owner on Jan 18, 2020. It is now read-only.

Commit

Permalink
Merge pull request #271 from cbmi/issue-268
Browse files Browse the repository at this point in the history
Add fallback search for case where Haystack isn't installed
  • Loading branch information
bruth committed Nov 25, 2014
2 parents 37ea25c + f7610fa commit bed75bf
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 88 deletions.
67 changes: 58 additions & 9 deletions avocado/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@
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


logger = logging.getLogger(__name__)


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.
Expand All @@ -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:
Expand All @@ -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'):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
158 changes: 79 additions & 79 deletions tests/cases/search/fixtures/search.json
Original file line number Diff line number Diff line change
@@ -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
}
}
Expand Down
51 changes: 51 additions & 0 deletions tests/cases/search/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down

0 comments on commit bed75bf

Please sign in to comment.