Merge branch 'django-1.9'

This commit is contained in:
Dave Hall 2015-12-03 10:28:55 +00:00
commit 5d1b2ed56b
13 changed files with 142 additions and 161 deletions

View file

@ -1,56 +1,43 @@
language: python
sudo: false
python:
- 2.6
- 2.7
- 3.2
- 3.3
- 3.4
- 3.5
cache:
directories:
- $HOME/.cache/pip
env:
- DJANGO=django==1.6.11
- DJANGO=django==1.6.11 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- DJANGO=django==1.6.11 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- DJANGO=django==1.7.7
- DJANGO=django==1.7.7 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- DJANGO=django==1.7.7 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- DJANGO=django==1.8.1
- DJANGO=django==1.8.1 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- DJANGO=django==1.8.1 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- DJANGO=django==1.7.11
- DJANGO=django==1.7.11 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- DJANGO=django==1.7.11 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- DJANGO=django==1.8.7
- DJANGO=django==1.8.7 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- DJANGO=django==1.8.7 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- DJANGO=django==1.9.0
- DJANGO=django==1.9.0 DB_ENGINE="django.db.backends.postgresql" DB_NAME="test_project" DB_USER="postgres"
- DJANGO=django==1.9.0 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
matrix:
exclude:
# Django >= 1.7 does not work with Python 2.6.
- python: 2.6
env: DJANGO=django==1.7.7
- python: 2.6
env: DJANGO=django==1.7.7 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- python: 2.6
env: DJANGO=django==1.7.7 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- python: 2.6
env: DJANGO=django==1.8.1
- python: 2.6
env: DJANGO=django==1.8.1 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- python: 2.6
env: DJANGO=django==1.8.1 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
# mysqlclient does not work with Python 2.6 or 3.2.
- python: 2.6
env: DJANGO=django==1.6.11 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- python: 3.2
env: DJANGO=django==1.6.11 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- python: 3.2
env: DJANGO=django==1.7.7 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- python: 3.2
env: DJANGO=django==1.8.1 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
- python: 3.5
env: DJANGO=django==1.7.11
- python: 3.5
env: DJANGO=django==1.7.11 DB_ENGINE="django.db.backends.postgresql_psycopg2" DB_NAME="test_project" DB_USER="postgres"
- python: 3.5
env: DJANGO=django==1.7.11 DB_ENGINE="django.db.backends.mysql" DB_NAME="test_project" DB_USER="travis"
fast_finish: true
services:
- postgresql
- mysql
install:
- travis_retry pip install $DJANGO
- if [[ "$DB_ENGINE" == "django.db.backends.postgresql" ]] ; then travis_retry pip install psycopg2; fi
- if [[ "$DB_ENGINE" == "django.db.backends.postgresql_psycopg2" ]] ; then travis_retry pip install psycopg2; fi
- if [[ "$DB_ENGINE" == "django.db.backends.mysql" ]] ; then travis_retry pip install mysqlclient ; fi
- python setup.py -q install
- travis_retry pip install -e .
before_script:
- if [[ "$DB_ENGINE" == "django.db.backends.mysql" ]] ; then mysql -e 'create database test_project'; fi
- if [[ "$DB_ENGINE" == "django.db.backends.postgresql" ]] ; then psql -c 'create database test_project;' -U postgres; fi
- if [[ "$DB_ENGINE" == "django.db.backends.postgresql_psycopg2" ]] ; then psql -c 'create database test_project;' -U postgres; fi
script: python src/tests/runtests.py
notifications:

View file

@ -1,14 +1,14 @@
from django.contrib import admin
import watson
from watson.admin import SearchAdmin
from test_watson.models import WatsonTestModel1
class WatsonTestModel1Admin(watson.SearchAdmin):
class WatsonTestModel1Admin(SearchAdmin):
search_fields = ("title", "description", "content",)
list_display = ("title",)
admin.site.register(WatsonTestModel1, WatsonTestModel1Admin)

View file

@ -22,8 +22,7 @@ from django.contrib.auth.models import User
from django import template
from django.utils.encoding import force_text
import watson
from watson.registration import RegistrationError, get_backend, SearchEngine
from watson import search as watson
from watson.models import SearchEntry
from test_watson.models import WatsonTestModel1, WatsonTestModel2
@ -31,30 +30,30 @@ from test_watson import admin # Force early registration of all admin models.
class RegistrationTest(TestCase):
def testRegistration(self):
# Register the model and test.
watson.register(WatsonTestModel1)
self.assertTrue(watson.is_registered(WatsonTestModel1))
self.assertRaises(RegistrationError, lambda: watson.register(WatsonTestModel1))
self.assertRaises(watson.RegistrationError, lambda: watson.register(WatsonTestModel1))
self.assertTrue(WatsonTestModel1 in watson.get_registered_models())
self.assertTrue(isinstance(watson.get_adapter(WatsonTestModel1), watson.SearchAdapter))
# Unregister the model and text.
watson.unregister(WatsonTestModel1)
self.assertFalse(watson.is_registered(WatsonTestModel1))
self.assertRaises(RegistrationError, lambda: watson.unregister(WatsonTestModel1))
self.assertRaises(watson.RegistrationError, lambda: watson.unregister(WatsonTestModel1))
self.assertTrue(WatsonTestModel1 not in watson.get_registered_models())
self.assertRaises(RegistrationError, lambda: isinstance(watson.get_adapter(WatsonTestModel1)))
self.assertRaises(watson.RegistrationError, lambda: isinstance(watson.get_adapter(WatsonTestModel1)))
complex_registration_search_engine = SearchEngine("restricted")
complex_registration_search_engine = watson.SearchEngine("restricted")
class InstallUninstallTestBase(TestCase):
@skipUnless(get_backend().requires_installation, "search backend does not require installation")
@skipUnless(watson.get_backend().requires_installation, "search backend does not require installation")
def testUninstallAndInstall(self):
backend = get_backend()
backend = watson.get_backend()
call_command("uninstallwatson", verbosity=0)
self.assertFalse(backend.is_installed())
call_command("installwatson", verbosity=0)
@ -64,7 +63,7 @@ class InstallUninstallTestBase(TestCase):
class SearchTestBase(TestCase):
model1 = WatsonTestModel1
model2 = WatsonTestModel2
def setUp(self):
@ -168,14 +167,14 @@ class InternalsTest(SearchTestBase):
# Delete a model and make sure that the search results match.
self.test11.delete()
self.assertEqual(watson.search("fooo").count(), 0)
def testSearchIndexUpdateDeferredByContext(self):
with watson.update_index():
self.test11.title = "fooo"
self.test11.save()
self.assertEqual(watson.search("fooo").count(), 0)
self.assertEqual(watson.search("fooo").count(), 1)
def testSearchIndexUpdateAbandonedOnError(self):
try:
with watson.update_index():
@ -186,14 +185,14 @@ class InternalsTest(SearchTestBase):
pass
# Test a search that should get not model.
self.assertEqual(watson.search("fooo").count(), 0)
def testSkipSearchIndexUpdate(self):
with watson.skip_index_update():
self.test11.title = "fooo"
self.test11.save()
# Test a search that should get not model.
self.assertEqual(watson.search("fooo").count(), 0)
def testNestedSkipInUpdateContext(self):
with watson.update_index():
self.test21.title = "baar"
@ -228,12 +227,12 @@ class InternalsTest(SearchTestBase):
call_command("buildwatson", verbosity=0)
# Make sure that we have four again (including duplicates).
self.assertEqual(search_entries.all().count(), 4)
def testEmptyFilterGivesAllResults(self):
for model in (WatsonTestModel1, WatsonTestModel2):
self.assertEqual(watson.filter(model, "").count(), 2)
self.assertEqual(watson.filter(model, " ").count(), 2)
def testFilter(self):
for model in (WatsonTestModel1, WatsonTestModel2):
# Test can find all.
@ -246,18 +245,18 @@ class InternalsTest(SearchTestBase):
obj = watson.filter(WatsonTestModel1.objects.filter(title__icontains="TITLE"), "INSTANCE12").get()
self.assertTrue(isinstance(obj, WatsonTestModel1))
self.assertEqual(obj.title, "title model1 instance12")
@skipUnless(get_backend().supports_prefix_matching, "Search backend does not support prefix matching.")
@skipUnless(watson.get_backend().supports_prefix_matching, "Search backend does not support prefix matching.")
def testPrefixFilter(self):
self.assertEqual(watson.filter(WatsonTestModel1, "INSTAN").count(), 2)
class SearchTest(SearchTestBase):
def emptySearchTextGivesNoResults(self):
self.assertEqual(watson.search("").count(), 0)
self.assertEqual(watson.search(" ").count(), 0)
self.assertEqual(watson.search(" ").count(), 0)
def testMultiTableSearch(self):
# Test a search that should get all models.
self.assertEqual(watson.search("TITLE").count(), 4)
@ -302,11 +301,11 @@ class SearchTest(SearchTestBase):
description = "description model1 instance13",
)
self.assertTrue(watson.search("'content").exists()) # Some database engines ignore leading apostrophes, some count them.
@skipUnless(get_backend().supports_prefix_matching, "Search backend does not support prefix matching.")
@skipUnless(watson.get_backend().supports_prefix_matching, "Search backend does not support prefix matching.")
def testMultiTablePrefixSearch(self):
self.assertEqual(watson.search("DESCR").count(), 4)
def testLimitedModelList(self):
# Test a search that should get all models.
self.assertEqual(watson.search("TITLE", models=(WatsonTestModel1, WatsonTestModel2)).count(), 4)
@ -325,7 +324,7 @@ class SearchTest(SearchTestBase):
self.assertEqual(watson.search("MODEL2", models=(WatsonTestModel1,)).count(), 0)
self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel1,)).count(), 0)
self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel2,)).count(), 0)
def testExcludedModelList(self):
# Test a search that should get all models.
self.assertEqual(watson.search("TITLE", exclude=()).count(), 4)
@ -371,7 +370,7 @@ class SearchTest(SearchTestBase):
self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel2.objects.filter(
title__icontains = "MODEL1",
),)).count(), 0)
def testExcludedModelQuerySet(self):
# Test a search that should get all models.
self.assertEqual(watson.search("TITLE", exclude=(WatsonTestModel1.objects.filter(title__icontains="FOOO"), WatsonTestModel2.objects.filter(title__icontains="FOOO"),)).count(), 4)
@ -398,7 +397,7 @@ class SearchTest(SearchTestBase):
self.assertEqual(watson.search("INSTANCE21", exclude=(WatsonTestModel2.objects.filter(
title__icontains = "MODEL2",
),)).count(), 0)
def testKitchenSink(self):
"""For sanity, let's just test everything together in one giant search of doom!"""
self.assertEqual(watson.search(
@ -412,14 +411,14 @@ class SearchTest(SearchTestBase):
WatsonTestModel2.objects.filter(title__icontains="MODEL1"),
)
).get().title, "title model1 instance11")
class LiveFilterSearchTest(SearchTest):
model1 = WatsonTestModel1.objects.filter(is_published=True)
model2 = WatsonTestModel2.objects.filter(is_published=True)
def testUnpublishedModelsNotFound(self):
# Make sure that there are four to find!
self.assertEqual(watson.search("tItle Content Description").count(), 4)
@ -430,15 +429,15 @@ class LiveFilterSearchTest(SearchTest):
self.test21.save()
# This should return 4, but two of them are unpublished.
self.assertEqual(watson.search("tItle Content Description").count(), 2)
def testCanOverridePublication(self):
# Unpublish two objects.
self.test11.is_published = False
self.test11.save()
# This should still return 4, since we're overriding the publication.
self.assertEqual(watson.search("tItle Content Description", models=(WatsonTestModel2, WatsonTestModel1._base_manager.all(),)).count(), 4)
class RankingTest(SearchTestBase):
def setUp(self):
@ -450,24 +449,24 @@ class RankingTest(SearchTestBase):
def testRankingParamPresentOnSearch(self):
self.assertGreater(watson.search("TITLE")[0].watson_rank, 0)
def testRankingParamPresentOnFilter(self):
self.assertGreater(watson.filter(WatsonTestModel1, "TITLE")[0].watson_rank, 0)
def testRankingParamAbsentOnSearch(self):
self.assertRaises(AttributeError, lambda: watson.search("TITLE", ranking=False)[0].watson_rank)
def testRankingParamAbsentOnFilter(self):
self.assertRaises(AttributeError, lambda: watson.filter(WatsonTestModel1, "TITLE", ranking=False)[0].watson_rank)
@skipUnless(get_backend().supports_ranking, "search backend does not support ranking")
@skipUnless(watson.get_backend().supports_ranking, "search backend does not support ranking")
def testRankingWithSearch(self):
self.assertEqual(
[entry.title for entry in watson.search("FOOO")],
["title model1 instance11 fooo baar fooo", "title model1 instance12"]
)
@skipUnless(get_backend().supports_ranking, "search backend does not support ranking")
@skipUnless(watson.get_backend().supports_ranking, "search backend does not support ranking")
def testRankingWithFilter(self):
self.assertEqual(
[entry.title for entry in watson.filter(WatsonTestModel1, "FOOO")],
@ -479,15 +478,15 @@ class ComplexRegistrationTest(SearchTestBase):
def testMetaStored(self):
self.assertEqual(complex_registration_search_engine.search("instance11")[0].meta["is_published"], True)
def testMetaNotStored(self):
self.assertRaises(KeyError, lambda: complex_registration_search_engine.search("instance21")[0].meta["is_published"])
def testFieldsExcludedOnSearch(self):
self.assertEqual(complex_registration_search_engine.search("TITLE").count(), 4)
self.assertEqual(complex_registration_search_engine.search("CONTENT").count(), 0)
self.assertEqual(complex_registration_search_engine.search("DESCRIPTION").count(), 0)
def testFieldsExcludedOnFilter(self):
self.assertEqual(complex_registration_search_engine.filter(WatsonTestModel1, "TITLE").count(), 2)
self.assertEqual(complex_registration_search_engine.filter(WatsonTestModel1, "CONTENT").count(), 0)
@ -500,7 +499,7 @@ class ComplexRegistrationTest(SearchTestBase):
class AdminIntegrationTest(SearchTestBase):
urls = "test_watson.urls"
def setUp(self):
super(AdminIntegrationTest, self).setUp()
self.user = User(
@ -510,7 +509,7 @@ class AdminIntegrationTest(SearchTestBase):
)
self.user.set_password("bar")
self.user.save()
@skipUnless("django.contrib.admin" in settings.INSTALLED_APPS, "Django admin site not installed")
def testAdminIntegration(self):
# Log the user in.
@ -531,17 +530,17 @@ class AdminIntegrationTest(SearchTestBase):
response = self.client.get("/admin/test_watson/watsontestmodel1/?q=instance11")
self.assertContains(response, "instance11")
self.assertNotContains(response, "instance12")
def tearDown(self):
super(AdminIntegrationTest, self).tearDown()
self.user.delete()
del self.user
class SiteSearchTest(SearchTestBase):
urls = "test_watson.urls"
def testSiteSearch(self):
# Test a search than should find everything.
response = self.client.get("/simple/?q=title")
@ -562,7 +561,7 @@ class SiteSearchTest(SearchTestBase):
self.assertNotContains(response, "instance12")
self.assertNotContains(response, "instance21")
self.assertNotContains(response, "instance22")
def testSiteSearchJSON(self):
# Test a search that should find everything.
response = self.client.get("/simple/json/?q=title")
@ -573,7 +572,7 @@ class SiteSearchTest(SearchTestBase):
self.assertTrue("title model1 instance12" in results)
self.assertTrue("title model2 instance21" in results)
self.assertTrue("title model2 instance22" in results)
def testSiteSearchCustom(self):
# Test a search than should find everything.
response = self.client.get("/custom/?fooo=title")
@ -605,7 +604,7 @@ class SiteSearchTest(SearchTestBase):
# Test a search that should find nothing.
response = self.client.get("/custom/?q=fooo")
self.assertRedirects(response, "/simple/")
def testSiteSearchCustomJSON(self):
# Test a search that should find everything.
response = self.client.get("/custom/json/?fooo=title&page=last")

View file

@ -5,26 +5,3 @@ Developed by Dave Hall.
<http://www.etianen.com/>
"""
from __future__ import unicode_literals
from watson.admin import SearchAdmin
from watson.registration import SearchAdapter, default_search_engine, search_context_manager
# The main search methods.
search = default_search_engine.search
filter = default_search_engine.filter
# Easy registration.
register = default_search_engine.register
unregister = default_search_engine.unregister
is_registered = default_search_engine.is_registered
get_registered_models = default_search_engine.get_registered_models
get_adapter = default_search_engine.get_adapter
# Easy context management.
update_index = search_context_manager.update_index
skip_index_update = search_context_manager.skip_index_update

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from watson.registration import SearchEngine, SearchAdapter
from watson.search import SearchEngine, SearchAdapter
admin_search_engine = SearchEngine("admin")

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals, print_function
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.db.models import get_model
from django.apps import apps
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
@ -14,7 +14,7 @@ from django.utils.translation import activate
from django.conf import settings
from watson.registration import SearchEngine, _bulk_save_search_entries
from watson.search import SearchEngine, _bulk_save_search_entries
from watson.models import SearchEntry
@ -83,7 +83,7 @@ class Command(BaseCommand):
models = []
for model_name in args:
try:
model = get_model(*model_name.split(".")) # app label, model name
model = apps.get_model(*model_name.split(".")) # app label, model name
except TypeError: # were we given only model name without app_name?
registered_models = search_engine.get_registered_models()
matching_models = [x for x in registered_models if x.__name__ == model_name]

View file

@ -4,13 +4,13 @@ from __future__ import unicode_literals
from django.core.management.base import NoArgsCommand
from watson.registration import get_backend
from watson.search import get_backend
class Command(NoArgsCommand):
help = "Creates the database indices needed by django-watson."
def handle_noargs(self, **options):
"""Runs the management command."""
verbosity = int(options.get("verbosity", 1))

View file

@ -1,16 +1,16 @@
"""Exposed the watson.get_registered_models() function as management command for debugging purpose. """
from django.core.management.base import NoArgsCommand
import watson
from watson import search as watson
class Command(NoArgsCommand):
help = "List all registed models by django-watson."
def handle_noargs(self, **options):
"""Runs the management command."""
self.stdout.write("The following models are registed for the django-watson search engine:\n")
for mdl in watson.get_registered_models():
self.stdout.write("- %s\n" % mdl.__name__)

View file

@ -4,13 +4,13 @@ from __future__ import unicode_literals
from django.core.management.base import NoArgsCommand
from watson.registration import get_backend
from watson.search import get_backend
class Command(NoArgsCommand):
help = "Destroys the database indices needed by django-watson."
def handle_noargs(self, **options):
"""Runs the management command."""
verbosity = int(options.get("verbosity", 1))

View file

@ -4,7 +4,7 @@ from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured
from watson.registration import search_context_manager
from watson.search import search_context_manager
WATSON_MIDDLEWARE_FLAG = "watson.search_context_middleware_active"

View file

@ -73,7 +73,7 @@ class SearchEntry(models.Model):
meta_encoded = models.TextField()
def _deserialize_meta(self):
from watson.registration import SearchEngine
from watson.search import SearchEngine
engine = SearchEngine._created_engines[self.engine_slug]
model = ContentType.objects.get_for_id(self.content_type_id).model_class()
adapter = engine.get_adapter(model)

View file

@ -628,3 +628,21 @@ def get_backend(backend_name=None):
backend = backend_cls()
_backends_cache[backend_name] = backend
return backend
# The main search methods.
search = default_search_engine.search
filter = default_search_engine.filter
# Easy registration.
register = default_search_engine.register
unregister = default_search_engine.unregister
is_registered = default_search_engine.is_registered
get_registered_models = default_search_engine.get_registered_models
get_adapter = default_search_engine.get_adapter
# Easy context management.
update_index = search_context_manager.update_index
skip_index_update = search_context_manager.skip_index_update

View file

@ -10,57 +10,57 @@ from django.utils import six
from django.views import generic
from django.views.generic.list import BaseListView
import watson
from watson import search as watson
class SearchMixin(object):
"""Base mixin for search views."""
context_object_name = "search_results"
query_param = "q"
def get_query_param(self):
"""Returns the query parameter to use in the request GET dictionary."""
return self.query_param
models = ()
def get_models(self):
"""Returns the models to use in the query."""
return self.models
return self.models
exclude = ()
def get_exclude(self):
"""Returns the models to exclude from the query."""
return self.exclude
def get_queryset(self):
"""Returns the initial queryset."""
return watson.search(self.query, models=self.get_models(), exclude=self.get_exclude())
def get_query(self, request):
"""Parses the query from the request."""
return request.GET.get(self.get_query_param(), "").strip()
empty_query_redirect = None
def get_empty_query_redirect(self):
"""Returns the URL to redirect an empty query to, or None."""
return self.empty_query_redirect
extra_context = {}
def get_extra_context(self):
"""
Returns any extra context variables.
Required for backwards compatibility with old function-based views.
"""
return self.extra_context
def get_context_data(self, **kwargs):
"""Generates context variables."""
context = super(SearchMixin, self).get_context_data(**kwargs)
@ -71,7 +71,7 @@ class SearchMixin(object):
value = value()
context[key] = value
return context
def get(self, request, *args, **kwargs):
"""Performs a GET request."""
self.query = self.get_query(request)
@ -83,16 +83,16 @@ class SearchMixin(object):
class SearchView(SearchMixin, generic.ListView):
"""View that performs a search and returns the search results."""
template_name = "watson/search_results.html"
class SearchApiView(SearchMixin, BaseListView):
"""A JSON-based search API."""
def render_to_response(self, context, **response_kwargs):
"""Renders the search results to the response."""
content = json.dumps({
@ -117,8 +117,8 @@ class SearchApiView(SearchMixin, BaseListView):
def search(request, **kwargs):
"""Renders a page of search results."""
return SearchView.as_view(**kwargs)(request)
def search_json(request, **kwargs):
"""Renders a JSON representation of matching search entries."""
return SearchApiView.as_view(**kwargs)(request)