diff --git a/.travis.yml b/.travis.yml index 25fe7c4..265da0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,24 @@ cache: - $HOME/.cache/pip matrix: fast_finish: true +env: + matrix: + - WORKER=python + - WORKER=flake8 services: - postgresql - mysql install: - pip install tox + - pip install flake8 before_script: - - mysql -e 'create database test_project' - - psql -c 'create database test_project;' -U postgres -script: tox + - | + if [[ $WORKER == python ]]; then + mysql -e 'create database test_project'; + psql -c 'create database test_project;' -U postgres; + fi +script: + - if [[ $WORKER == python ]]; then tox; fi + - if [[ $WORKER == flake8 ]]; then flake8 --jobs=2 . ; fi notifications: email: false diff --git a/MANIFEST.in b/MANIFEST.in index 3612c94..539cfd4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include src/watson/templates/watson/*.html -include src/watson/locale/*/LC_MESSAGES/django.* +include watson/templates/watson/*.html +include watson/locale/*/LC_MESSAGES/django.* include LICENSE include README.markdown \ No newline at end of file diff --git a/setup.py b/setup.py index d12975e..601f487 100644 --- a/setup.py +++ b/setup.py @@ -3,15 +3,15 @@ from distutils.core import setup from watson import __version__ setup( - name = "django-watson", - version = '.'.join(str(x) for x in __version__), - description = "Full-text multi-table search application for Django. Easy to install and use, with good performance.", - long_description = open(os.path.join(os.path.dirname(__file__), "README.markdown")).read(), - author = "Dave Hall", - author_email = "dave@etianen.com", - url = "http://github.com/etianen/django-watson", - zip_safe = False, - packages = [ + name="django-watson", + version='.'.join(str(x) for x in __version__), + description="Full-text multi-table search application for Django. Easy to install and use, with good performance.", + long_description=open(os.path.join(os.path.dirname(__file__), "README.markdown")).read(), + author="Dave Hall", + author_email="dave@etianen.com", + url="http://github.com/etianen/django-watson", + zip_safe=False, + packages=[ "watson", "watson.management", "watson.management.commands", @@ -19,10 +19,7 @@ setup( "watson.south_migrations", "watson.templatetags", ], - package_dir = { - "": "src", - }, - package_data = { + package_data={ "watson": [ "locale/*/LC_MESSAGES/django.*", "templates/watson/*.html", diff --git a/src/watson/__init__.py b/src/watson/__init__.py deleted file mode 100644 index bd3ef74..0000000 --- a/src/watson/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Multi-table search application for Django, using native database search engines. - -Developed by Dave Hall. - - -""" diff --git a/src/tests/__init__.py b/tests/__init__.py similarity index 100% rename from src/tests/__init__.py rename to tests/__init__.py diff --git a/src/tests/runtests.py b/tests/runtests.py similarity index 95% rename from src/tests/runtests.py rename to tests/runtests.py index de8ab0e..ab0d196 100755 --- a/src/tests/runtests.py +++ b/tests/runtests.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -import sys, os, os.path +import os +import sys from optparse import OptionParser AVAILABLE_DATABASES = { @@ -97,9 +98,9 @@ def main(): 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], 'APP_DIRS': True, - }], - + }], ) + # Run Django setup (1.7+). import django try: @@ -110,9 +111,9 @@ def main(): from django.test.utils import get_runner TestRunner = get_runner(settings) test_runner = TestRunner( - verbosity = int(options.verbosity), - interactive = options.interactive, - failfast = options.failfast, + verbosity=int(options.verbosity), + interactive=options.interactive, + failfast=options.failfast, ) # Run the tests. failures = test_runner.run_tests(["test_watson"]) diff --git a/src/tests/test_watson/__init__.py b/tests/test_watson/__init__.py similarity index 100% rename from src/tests/test_watson/__init__.py rename to tests/test_watson/__init__.py diff --git a/src/tests/test_watson/admin.py b/tests/test_watson/admin.py similarity index 100% rename from src/tests/test_watson/admin.py rename to tests/test_watson/admin.py diff --git a/src/tests/test_watson/models.py b/tests/test_watson/models.py similarity index 71% rename from src/tests/test_watson/models.py rename to tests/test_watson/models.py index 2241c17..1b88778 100644 --- a/src/tests/test_watson/models.py +++ b/tests/test_watson/models.py @@ -4,47 +4,46 @@ from django.utils.encoding import force_text, python_2_unicode_compatible @python_2_unicode_compatible class TestModelBase(models.Model): - title = models.CharField( - max_length = 200, + max_length=200, ) - + content = models.TextField( - blank = True, + blank=True, ) - + description = models.TextField( - blank = True, + blank=True, ) - + is_published = models.BooleanField( - default = True, + default=True, ) - + def __str__(self): return force_text(self.title) class Meta: abstract = True - + class WatsonTestModel1(TestModelBase): pass -str_pk_gen = 0; +str_pk_gen = 0 + def get_str_pk(): global str_pk_gen - str_pk_gen += 1; + str_pk_gen += 1 return str(str_pk_gen) - + class WatsonTestModel2(TestModelBase): - id = models.CharField( - primary_key = True, - max_length = 100, - default = get_str_pk + primary_key=True, + max_length=100, + default=get_str_pk ) diff --git a/src/tests/test_watson/tests.py b/tests/test_watson/tests.py similarity index 90% rename from src/tests/test_watson/tests.py rename to tests/test_watson/tests.py index bc2d58b..82121a8 100644 --- a/src/tests/test_watson/tests.py +++ b/tests/test_watson/tests.py @@ -29,7 +29,7 @@ from watson.models import SearchEntry from watson.backends import escape_query from test_watson.models import WatsonTestModel1, WatsonTestModel2 -from test_watson import admin # Force early registration of all admin models. +from test_watson import admin # Force early registration of all admin models. # noQA class RegistrationTest(TestCase): @@ -105,28 +105,32 @@ class SearchTestBase(TestCase): # Register the test models. watson.register(self.model1) watson.register(self.model2, exclude=("id",)) - complex_registration_search_engine.register(WatsonTestModel1, exclude=("content", "description",), store=("is_published",)) - complex_registration_search_engine.register(WatsonTestModel2, fields=("title",)) + complex_registration_search_engine.register( + WatsonTestModel1, exclude=("content", "description",), store=("is_published",) + ) + complex_registration_search_engine.register( + WatsonTestModel2, fields=("title",) + ) # Create some test models. self.test11 = WatsonTestModel1.objects.create( - title = "title model1 instance11", - content = "content model1 instance11", - description = "description model1 instance11", + title="title model1 instance11", + content="content model1 instance11", + description="description model1 instance11", ) self.test12 = WatsonTestModel1.objects.create( - title = "title model1 instance12", - content = "content model1 instance12", - description = "description model1 instance12", + title="title model1 instance12", + content="content model1 instance12", + description="description model1 instance12", ) self.test21 = WatsonTestModel2.objects.create( - title = "title model2 instance21", - content = "content model2 instance21", - description = "description model2 instance21", + title="title model2 instance21", + content="content model2 instance21", + description="description model2 instance21", ) self.test22 = WatsonTestModel2.objects.create( - title = "title model2 instance22", - content = "content model2 instance22", - description = "description model2 instance22", + title="title model2 instance22", + content="content model2 instance22", + description="description model2 instance22", ) def tearDown(self): @@ -366,8 +370,10 @@ class SearchTest(SearchTestBase): self.assertEqual(watson.search("abcd@efgh").count(), 1) x.delete() - - @skipUnless(watson.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) @@ -411,67 +417,83 @@ class SearchTest(SearchTestBase): def testLimitedModelQuerySet(self): # Test a search that should get all models. - self.assertEqual(watson.search("TITLE", models=(WatsonTestModel1.objects.filter(title__icontains="TITLE"), WatsonTestModel2.objects.filter(title__icontains="TITLE"),)).count(), 4) + self.assertEqual(watson.search( + "TITLE", + models=( + WatsonTestModel1.objects.filter(title__icontains="TITLE"), + WatsonTestModel2.objects.filter(title__icontains="TITLE"), + ) + ).count(), 4) # Test a search that should get two models. - self.assertEqual(watson.search("MODEL1", models=(WatsonTestModel1.objects.filter( - title__icontains = "MODEL1", - description__icontains = "MODEL1", - ),)).count(), 2) + self.assertEqual( + watson.search( + "MODEL1", + models=(WatsonTestModel1.objects.filter( + title__icontains="MODEL1", + description__icontains="MODEL1", + ),) + ).count(), 2) self.assertEqual(watson.search("MODEL2", models=(WatsonTestModel2.objects.filter( - title__icontains = "MODEL2", - description__icontains = "MODEL2", + title__icontains="MODEL2", + description__icontains="MODEL2", ),)).count(), 2) # Test a search that should get one model. self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel1.objects.filter( - title__icontains = "MODEL1", + title__icontains="MODEL1", ),)).count(), 1) self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel2.objects.filter( - title__icontains = "MODEL2", + title__icontains="MODEL2", ),)).count(), 1) # Test a search that should get no models. self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel1.objects.filter( - title__icontains = "MODEL2", + title__icontains="MODEL2", ),)).count(), 0) self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel2.objects.filter( - title__icontains = "MODEL1", + 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) + self.assertEqual( + watson.search( + "TITLE", + exclude=( + WatsonTestModel1.objects.filter(title__icontains="FOOO"), + WatsonTestModel2.objects.filter(title__icontains="FOOO"),) + ).count(), 4) # Test a search that should get two models. self.assertEqual(watson.search("MODEL1", exclude=(WatsonTestModel1.objects.filter( - title__icontains = "INSTANCE21", - description__icontains = "INSTANCE22", + title__icontains="INSTANCE21", + description__icontains="INSTANCE22", ),)).count(), 2) self.assertEqual(watson.search("MODEL2", exclude=(WatsonTestModel2.objects.filter( - title__icontains = "INSTANCE11", - description__icontains = "INSTANCE12", + title__icontains="INSTANCE11", + description__icontains="INSTANCE12", ),)).count(), 2) # Test a search that should get one model. self.assertEqual(watson.search("INSTANCE11", exclude=(WatsonTestModel1.objects.filter( - title__icontains = "MODEL2", + title__icontains="MODEL2", ),)).count(), 1) self.assertEqual(watson.search("INSTANCE21", exclude=(WatsonTestModel2.objects.filter( - title__icontains = "MODEL1", + title__icontains="MODEL1", ),)).count(), 1) # Test a search that should get no models. self.assertEqual(watson.search("INSTANCE11", exclude=(WatsonTestModel1.objects.filter( - title__icontains = "MODEL1", + title__icontains="MODEL1", ),)).count(), 0) self.assertEqual(watson.search("INSTANCE21", exclude=(WatsonTestModel2.objects.filter( - title__icontains = "MODEL2", + 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( "INSTANCE11", - models = ( + models=( WatsonTestModel1.objects.filter(title__icontains="INSTANCE11"), WatsonTestModel2.objects.filter(title__icontains="TITLE"), ), - exclude = ( + exclude=( WatsonTestModel1.objects.filter(title__icontains="MODEL2"), WatsonTestModel2.objects.filter(title__icontains="MODEL1"), ) @@ -500,7 +522,10 @@ class LiveFilterSearchTest(SearchTest): 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) + self.assertEqual(watson.search( + "tItle Content Description", + models=(WatsonTestModel2, WatsonTestModel1._base_manager.all(),) + ).count(), 4) class RankingTest(SearchTestBase): @@ -522,7 +547,10 @@ class RankingTest(SearchTestBase): 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) + self.assertRaises( + AttributeError, + lambda: watson.filter(WatsonTestModel1, "TITLE", ranking=False)[0].watson_rank + ) @skipUnless(watson.get_backend().supports_ranking, "search backend does not support ranking") def testRankingWithSearch(self): @@ -545,7 +573,10 @@ class ComplexRegistrationTest(SearchTestBase): 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"]) + 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) @@ -562,13 +593,13 @@ class ComplexRegistrationTest(SearchTestBase): class AdminIntegrationTest(SearchTestBase): - + def setUp(self): super(AdminIntegrationTest, self).setUp() self.user = User( - username = "foo", - is_staff = True, - is_superuser = True, + username="foo", + is_staff=True, + is_superuser=True, ) self.user.set_password("bar") self.user.save() @@ -577,8 +608,8 @@ class AdminIntegrationTest(SearchTestBase): def testAdminIntegration(self): # Log the user in. self.client.login( - username = "foo", - password = "bar", + username="foo", + password="bar", ) # Test a search with no query. response = self.client.get("/admin/test_watson/watsontestmodel1/") diff --git a/src/tests/test_watson/urls.py b/tests/test_watson/urls.py similarity index 97% rename from src/tests/test_watson/urls.py rename to tests/test_watson/urls.py index 42b01c0..ad09f1f 100644 --- a/src/tests/test_watson/urls.py +++ b/tests/test_watson/urls.py @@ -5,7 +5,7 @@ from django.contrib import admin urlpatterns = [ url("^simple/", include("watson.urls")), - + url("^custom/", include("watson.urls"), kwargs={ "query_param": "fooo", "empty_query_redirect": "/simple/", @@ -15,6 +15,6 @@ urlpatterns = [ }, "paginate_by": 10, }), - + url("^admin/", include(admin.site.urls)), ] diff --git a/src/tests/urls.py b/tests/urls.py similarity index 100% rename from src/tests/urls.py rename to tests/urls.py diff --git a/tox.ini b/tox.ini index 1221429..a1855c7 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,10 @@ deps = postgres: psycopg2 mysql: mysqlclient commands = - sqlite: coverage run src/tests/runtests.py - postgres: coverage run src/tests/runtests.py -d psql - mysql: coverage run src/tests/runtests.py -d mysql + sqlite: coverage run tests/runtests.py + postgres: coverage run tests/runtests.py -d psql + mysql: coverage run tests/runtests.py -d mysql + +[flake8] +max-line-length=120 +exclude=build,venv,migrations,south_migrations,.tox diff --git a/src/watson/admin.py b/watson/admin.py similarity index 93% rename from src/watson/admin.py rename to watson/admin.py index fee50ee..7cdc1c7 100644 --- a/src/watson/admin.py +++ b/watson/admin.py @@ -68,9 +68,9 @@ class SearchAdmin(admin.ModelAdmin): if not self.search_engine.is_registered(self.model) and self.search_fields: self.search_engine.register( self.model, - fields = self.search_fields, - adapter_cls = self.search_adapter_cls, - get_live_queryset = lambda self_: None, # Ensure complete queryset is used in admin. + fields=self.search_fields, + adapter_cls=self.search_adapter_cls, + get_live_queryset=lambda self_: None, # Ensure complete queryset is used in admin. ) def get_changelist(self, request, **kwargs): diff --git a/src/watson/backends.py b/watson/backends.py similarity index 93% rename from src/watson/backends.py rename to watson/backends.py index 8ec1c4a..0900119 100644 --- a/src/watson/backends.py +++ b/watson/backends.py @@ -118,7 +118,7 @@ class RegexSearchMixin(six.with_metaclass(abc.ABCMeta)): """, """ ({db_table}.{content_type_id} = %s) """] - word_kwargs= { + word_kwargs = { "db_table": db_table, "model_db_table": model_db_table, "engine_slug": connection.ops.quote_name("engine_slug"), @@ -147,9 +147,13 @@ class RegexSearchMixin(six.with_metaclass(abc.ABCMeta)): # Add in all words. for word in search_text.split(): regex = regex_from_word(word) - word_query.append(""" - ({db_table}.{title} {iregex_operator} OR {db_table}.{description} {iregex_operator} OR {db_table}.{content} {iregex_operator}) - """) + word_query.append( + """ + ({db_table}.{title} {iregex_operator} + OR {db_table}.{description} {iregex_operator} + OR {db_table}.{content} {iregex_operator}) + """ + ) word_args.extend((regex, regex, regex)) # Compile the query. full_word_query = " AND ".join(word_query).format(**word_kwargs) @@ -354,25 +358,36 @@ class MySQLSearchBackend(SearchBackend): def is_installed(self): """Checks whether django-watson is installed.""" cursor = connection.cursor() - cursor.execute("SHOW INDEX FROM watson_searchentry WHERE Key_name = 'watson_searchentry_fulltext'"); + cursor.execute("SHOW INDEX FROM watson_searchentry WHERE Key_name = 'watson_searchentry_fulltext'") return bool(cursor.fetchall()) def do_install(self): """Executes the MySQL specific SQL code to install django-watson.""" cursor = connection.cursor() # Drop all foreign keys on the watson_searchentry table. - cursor.execute("SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'watson_searchentry' AND CONSTRAINT_TYPE = 'FOREIGN KEY'") + cursor.execute( + "SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS " + "WHERE CONSTRAINT_SCHEMA = DATABASE() " + "AND TABLE_NAME = 'watson_searchentry' " + "AND CONSTRAINT_TYPE = 'FOREIGN KEY'" + ) for constraint_name, in cursor.fetchall(): - cursor.execute("ALTER TABLE watson_searchentry DROP FOREIGN KEY {constraint_name}".format( - constraint_name=constraint_name, - )) + cursor.execute( + "ALTER TABLE watson_searchentry DROP FOREIGN KEY {constraint_name}".format( + constraint_name=constraint_name, + ) + ) # Change the storage engine to MyISAM. cursor.execute("ALTER TABLE watson_searchentry ENGINE = MyISAM") # Add the full text indexes. - cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_fulltext ON watson_searchentry (title, description, content)") - cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_title ON watson_searchentry (title)") - cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_description ON watson_searchentry (description)") - cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_content ON watson_searchentry (content)") + cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_fulltext " + "ON watson_searchentry (title, description, content)") + cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_title " + "ON watson_searchentry (title)") + cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_description " + "ON watson_searchentry (description)") + cursor.execute("CREATE FULLTEXT INDEX watson_searchentry_content " + "ON watson_searchentry (content)") def do_uninstall(self): """Executes the SQL needed to uninstall django-watson.""" @@ -427,7 +442,8 @@ class MySQLSearchBackend(SearchBackend): tables=("watson_searchentry",), where=( "watson_searchentry.engine_slug = %s", - "MATCH (watson_searchentry.title, watson_searchentry.description, watson_searchentry.content) AGAINST (%s IN BOOLEAN MODE)", + "MATCH (watson_searchentry.title, watson_searchentry.description, watson_searchentry.content) " + "AGAINST (%s IN BOOLEAN MODE)", "watson_searchentry.{ref_name} = {table_name}.{pk_name}".format( ref_name=ref_name, table_name=connection.ops.quote_name(model._meta.db_table), diff --git a/src/watson/management/__init__.py b/watson/management/__init__.py similarity index 100% rename from src/watson/management/__init__.py rename to watson/management/__init__.py diff --git a/src/watson/management/commands/__init__.py b/watson/management/commands/__init__.py similarity index 100% rename from src/watson/management/commands/__init__.py rename to watson/management/commands/__init__.py diff --git a/src/watson/management/commands/buildwatson.py b/watson/management/commands/buildwatson.py similarity index 71% rename from src/watson/management/commands/buildwatson.py rename to watson/management/commands/buildwatson.py index de257c1..59703d2 100644 --- a/src/watson/management/commands/buildwatson.py +++ b/watson/management/commands/buildwatson.py @@ -2,8 +2,6 @@ from __future__ import unicode_literals, print_function -from optparse import make_option - from django.core.management.base import BaseCommand, CommandError from django.apps import apps from django.contrib import admin @@ -21,56 +19,68 @@ from watson.models import SearchEntry # Sets up registration for django-watson's admin integration. admin.autodiscover() + def get_engine(engine_slug_): - '''returns search engine with a given name''' + """returns search engine with a given name""" try: return [x[1] for x in SearchEngine.get_created_engines() if x[0] == engine_slug_][0] except IndexError: raise CommandError("Search Engine \"%s\" is not registered!" % force_text(engine_slug_)) + def rebuild_index_for_model(model_, engine_slug_, verbosity_): - '''rebuilds index for a model''' + """rebuilds index for a model""" search_engine_ = get_engine(engine_slug_) local_refreshed_model_count = [0] # HACK: Allows assignment to outer scope. + def iter_search_entries(): for obj in model_._default_manager.all().iterator(): for search_entry in search_engine_._update_obj_index_iter(obj): yield search_entry local_refreshed_model_count[0] += 1 if verbosity_ >= 3: - print("Refreshed search entry for {model} {obj} in {engine_slug!r} search engine.".format( - model = force_text(model_._meta.verbose_name), - obj = force_text(obj), - engine_slug = force_text(engine_slug_), - )) + print( + "Refreshed search entry for {model} {obj} " + "in {engine_slug!r} search engine.".format( + model=force_text(model_._meta.verbose_name), + obj=force_text(obj), + engine_slug=force_text(engine_slug_), + ) + ) if verbosity_ == 2: - print("Refreshed {local_refreshed_model_count} {model} search entry(s) in {engine_slug!r} search engine.".format( - model = force_text(model_._meta.verbose_name), - local_refreshed_model_count = local_refreshed_model_count[0], - engine_slug = force_text(engine_slug_), - )) + print( + "Refreshed {local_refreshed_model_count} {model} search entry(s) " + "in {engine_slug!r} search engine.".format( + model=force_text(model_._meta.verbose_name), + local_refreshed_model_count=local_refreshed_model_count[0], + engine_slug=force_text(engine_slug_), + ) + ) _bulk_save_search_entries(iter_search_entries()) return local_refreshed_model_count[0] + class Command(BaseCommand): args = "[[--engine=search_engine] ... ]" - help = "Rebuilds the database indices needed by django-watson. You can (re-)build index for selected models by specifying them" + help = "Rebuilds the database indices needed by django-watson. " \ + "You can (re-)build index for selected models by specifying them" def add_arguments(self, parser): parser.add_argument("apps", nargs="*", action="store", default=[]) - parser.add_argument('--engine', + parser.add_argument( + '--engine', action="store", help='Search engine models are registered with' ) - + @transaction.atomic() def handle(self, *args, **options): """Runs the management command.""" activate(settings.LANGUAGE_CODE) verbosity = int(options.get("verbosity", 1)) - + # see if we're asked to use a specific search engine if options.get('engine'): engine_slug = options['engine'] @@ -78,13 +88,13 @@ class Command(BaseCommand): else: engine_slug = "default" engine_selected = False - + # work-around for legacy optparser hack in BaseCommand. In Django=1.10 the # args are collected in options['apps'], but in earlier versions they are # kept in args. - if len(options['apps']): + if len(options['apps']): args = options['apps'] - + # get the search engine we'll be checking registered models for, may be "default" search_engine = get_engine(engine_slug) models = [] @@ -101,7 +111,10 @@ class Command(BaseCommand): else: model = None if model is None or not search_engine.is_registered(model): - raise CommandError("Model \"%s\" is not registered with django-watson search engine \"%s\"!" % (force_text(model_name), force_text(engine_slug))) + raise CommandError( + "Model \"%s\" is not registered with django-watson search engine \"%s\"!" + % (force_text(model_name), force_text(engine_slug)) + ) models.append(model) refreshed_model_count = 0 @@ -128,24 +141,31 @@ class Command(BaseCommand): for model in registered_models: refreshed_model_count += rebuild_index_for_model(model, engine_slug, verbosity) - # Clean out any search entries that exist for stale content types. Only do it during full rebuild + # Clean out any search entries that exist for stale content types. + # Only do it during full rebuild valid_content_types = [ContentType.objects.get_for_model(model) for model in registered_models] stale_entries = SearchEntry.objects.filter( - engine_slug = engine_slug, + engine_slug=engine_slug, ).exclude( - content_type__in = valid_content_types + content_type__in=valid_content_types ) stale_entry_count = stale_entries.count() if stale_entry_count > 0: stale_entries.delete() if verbosity >= 1: - print("Deleted {stale_entry_count} stale search entry(s) in {engine_slug!r} search engine.".format( - stale_entry_count = stale_entry_count, - engine_slug = force_text(engine_slug), - )) + print( + "Deleted {stale_entry_count} stale search entry(s) " + "in {engine_slug!r} search engine.".format( + stale_entry_count=stale_entry_count, + engine_slug=force_text(engine_slug), + ) + ) if verbosity == 1: - print("Refreshed {refreshed_model_count} search entry(s) in {engine_slug!r} search engine.".format( - refreshed_model_count = refreshed_model_count, - engine_slug = force_text(engine_slug), - )) + print( + "Refreshed {refreshed_model_count} search entry(s) " + "in {engine_slug!r} search engine.".format( + refreshed_model_count=refreshed_model_count, + engine_slug=force_text(engine_slug), + ) + ) diff --git a/src/watson/management/commands/installwatson.py b/watson/management/commands/installwatson.py similarity index 100% rename from src/watson/management/commands/installwatson.py rename to watson/management/commands/installwatson.py diff --git a/src/watson/management/commands/listwatson.py b/watson/management/commands/listwatson.py similarity index 99% rename from src/watson/management/commands/listwatson.py rename to watson/management/commands/listwatson.py index 2bc0939..9b47707 100644 --- a/src/watson/management/commands/listwatson.py +++ b/watson/management/commands/listwatson.py @@ -3,6 +3,7 @@ from django.core.management.base import BaseCommand from watson import search as watson + class Command(BaseCommand): help = "List all registed models by django-watson." @@ -12,5 +13,3 @@ class Command(BaseCommand): 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__) - - diff --git a/src/watson/management/commands/uninstallwatson.py b/watson/management/commands/uninstallwatson.py similarity index 100% rename from src/watson/management/commands/uninstallwatson.py rename to watson/management/commands/uninstallwatson.py diff --git a/src/watson/middleware.py b/watson/middleware.py similarity index 100% rename from src/watson/middleware.py rename to watson/middleware.py diff --git a/src/watson/migrations/0001_initial.py b/watson/migrations/0001_initial.py similarity index 100% rename from src/watson/migrations/0001_initial.py rename to watson/migrations/0001_initial.py diff --git a/src/watson/migrations/__init__.py b/watson/migrations/__init__.py similarity index 100% rename from src/watson/migrations/__init__.py rename to watson/migrations/__init__.py diff --git a/src/watson/models.py b/watson/models.py similarity index 89% rename from src/watson/models.py rename to watson/models.py index 108993c..fb3d382 100644 --- a/src/watson/models.py +++ b/watson/models.py @@ -2,8 +2,6 @@ from __future__ import unicode_literals -import json - from django.db import models from django.contrib.contenttypes.models import ContentType from django.utils.functional import cached_property @@ -13,6 +11,7 @@ try: except ImportError: from django.contrib.contenttypes.generic import GenericForeignKey + def has_int_pk(model): """Tests whether the given model has an integer primary key.""" pk = model._meta.pk @@ -34,9 +33,9 @@ class SearchEntry(models.Model): """An entry in the search index.""" engine_slug = models.CharField( - max_length = 200, - db_index = True, - default = "default", + max_length=200, + db_index=True, + default="default", ) content_type = models.ForeignKey( @@ -46,28 +45,28 @@ class SearchEntry(models.Model): object_id = models.TextField() object_id_int = models.IntegerField( - blank = True, - null = True, - db_index = True, + blank=True, + null=True, + db_index=True, ) object = GenericForeignKey() title = models.CharField( - max_length = 1000, + max_length=1000, ) description = models.TextField( - blank = True, + blank=True, ) content = models.TextField( - blank = True, + blank=True, ) url = models.CharField( - max_length = 1000, - blank = True, + max_length=1000, + blank=True, ) meta_encoded = models.TextField() diff --git a/src/watson/search.py b/watson/search.py similarity index 87% rename from src/watson/search.py rename to watson/search.py index de56b33..a040584 100644 --- a/src/watson/search.py +++ b/watson/search.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals -import sys, json +import json +import sys from itertools import chain, islice from threading import local from functools import wraps @@ -63,11 +64,14 @@ class SearchAdapter(object): try: value = getattr(self, prefix) except AttributeError: - raise SearchAdapterError("Could not find a property called {name!r} on either {obj!r} or {search_adapter!r}".format( - name = prefix, - obj = obj, - search_adapter = self, - )) + raise SearchAdapterError( + "Could not find a property called {name!r}" + " on either {obj!r} or {search_adapter!r}".format( + name=prefix, + obj=obj, + search_adapter=self, + ) + ) else: # Run the attribute on the search adapter, if it's callable. if not isinstance(value, (QuerySet, models.Manager)): @@ -97,7 +101,8 @@ class SearchAdapter(object): def get_title(self, obj): """ - Returns the title of this search result. This is given high priority in search result ranking. + Returns the title of this search result. + This is given high priority in search result ranking. You can access the title of the search entry as `entry.title` in your search results. @@ -107,11 +112,12 @@ class SearchAdapter(object): def get_description(self, obj): """ - Returns the description of this search result. This is given medium priority in search result ranking. + Returns the description of this search result. + This is given medium priority in search result ranking. - You can access the description of the search entry as `entry.description` in your search results. Since - this should contains a short description of the search entry, it's excellent for providing a summary - in your search results. + You can access the description of the search entry as `entry.description` + in your search results. Since this should contains a short description of the search entry, + it's excellent for providing a summary in your search results. The default implementation returns `""`. """ @@ -119,15 +125,20 @@ class SearchAdapter(object): def get_content(self, obj): """ - Returns the content of this search result. This is given low priority in search result ranking. + Returns the content of this search result. + This is given low priority in search result ranking. - You can access the content of the search entry as `entry.content` in your search results, although - this field generally contains a big mess of search data so is less suitable for frontend display. + You can access the content of the search entry as `entry.content` in your search results, + although this field generally contains a big mess of search data so is less suitable + for frontend display. The default implementation returns all the registered fields in your model joined together. """ # Get the field names to look up. - field_names = self.fields or (field.name for field in self.model._meta.fields if isinstance(field, (models.CharField, models.TextField))) + field_names = self.fields or ( + field.name for field in self.model._meta.fields if + isinstance(field, (models.CharField, models.TextField)) + ) # Exclude named fields. field_names = (field_name for field_name in field_names if field_name not in self.exclude) # Create the text. @@ -244,7 +255,11 @@ class SearchContextManager(local): # Save all the models. tasks, is_invalid = self._stack.pop() if not is_invalid: - _bulk_save_search_entries(list(chain.from_iterable(engine._update_obj_index_iter(obj) for engine, obj in tasks))) + _bulk_save_search_entries( + list(chain.from_iterable(engine._update_obj_index_iter(obj) + for engine, obj in tasks) + ) + ) # Context management. @@ -345,9 +360,11 @@ class SearchEngine(object): """Initializes the search engine.""" # Check the slug is unique for this project. if engine_slug in SearchEngine._created_engines: - raise SearchEngineError("A search engine has already been created with the slug {engine_slug!r}".format( - engine_slug = engine_slug, - )) + raise SearchEngineError( + "A search engine has already been created with the slug {engine_slug!r}".format( + engine_slug=engine_slug, + ) + ) # Initialize thie engine. self._registered_models = {} self._engine_slug = engine_slug @@ -374,13 +391,16 @@ class SearchEngine(object): field_overrides["get_live_queryset"] = lambda self_: live_queryset.all() # Check for existing registration. if self.is_registered(model): - raise RegistrationError("{model!r} is already registered with this search engine".format( - model = model, - )) + raise RegistrationError( + "{model!r} is already registered with this search engine".format( + model=model, + )) # Perform any customization. if field_overrides: # Conversion to str is needed because Python 2 doesn't accept unicode for class name - adapter_cls = type(str("Custom") + adapter_cls.__name__, (adapter_cls,), field_overrides) + adapter_cls = type( + str("Custom") + adapter_cls.__name__, (adapter_cls,), field_overrides + ) # Perform the registration. adapter_obj = adapter_cls(model) self._registered_models[model] = adapter_obj @@ -401,7 +421,7 @@ class SearchEngine(object): # Check for registration. if not self.is_registered(model): raise RegistrationError("{model!r} is not registered with this search engine".format( - model = model, + model=model, )) # Perform the unregistration. del self._registered_models[model] @@ -418,7 +438,7 @@ class SearchEngine(object): if self.is_registered(model): return self._registered_models[model] raise RegistrationError("{model!r} is not registered with this search engine".format( - model = model, + model=model, )) def _get_entries_for_obj(self, obj): @@ -430,20 +450,20 @@ class SearchEngine(object): object_id = force_text(obj.pk) # Get the basic list of search entries. search_entries = SearchEntry.objects.filter( - content_type = content_type, - engine_slug = self._engine_slug, + content_type=content_type, + engine_slug=self._engine_slug, ) if has_int_pk(model): # Do a fast indexed lookup. object_id_int = int(obj.pk) search_entries = search_entries.filter( - object_id_int = object_id_int, + object_id_int=object_id_int, ) else: # Alas, have to do a slow unindexed lookup. object_id_int = None search_entries = search_entries.filter( - object_id = object_id, + object_id=object_id, ) return object_id_int, search_entries @@ -514,31 +534,35 @@ class SearchEngine(object): queryset = sub_queryset.values_list("pk", flat=True) if has_int_pk(model): filter &= Q( - object_id_int__in = queryset, + object_id_int__in=queryset, ) else: live_ids = list(queryset) if live_ids: filter &= Q( - object_id__in = live_ids, + object_id__in=live_ids, ) else: - # HACK: There is a bug in Django (https://code.djangoproject.com/ticket/15145) that messes up __in queries when the iterable is empty. - # This bit of nonsense ensures that this aspect of the query will be impossible to fulfill. + # HACK: There is a bug in Django + # (https://code.djangoproject.com/ticket/15145) + # that messes up __in queries when the iterable is empty. + # This bit of nonsense ensures that this aspect of the query + # will be impossible to fulfill. filter &= Q( - content_type = ContentType.objects.get_for_model(model).id + 1, + content_type=ContentType.objects.get_for_model(model).id + 1, ) # Add the model to the filter. content_type = ContentType.objects.get_for_model(model) filter &= Q( - content_type = content_type, + content_type=content_type, ) # Combine with the other filters. filters |= filter return filters def _get_included_models(self, models): - """Returns an iterable of models and querysets that should be included in the search query.""" + """Returns an iterable of models and querysets that should be included + in the search query.""" for model in models or self.get_registered_models(): if isinstance(model, QuerySet): yield model @@ -559,7 +583,7 @@ class SearchEngine(object): return SearchEntry.objects.none() # Get the initial queryset. queryset = SearchEntry.objects.filter( - engine_slug = self._engine_slug, + engine_slug=self._engine_slug, ) # Process the allowed models. queryset = queryset.filter( @@ -620,10 +644,11 @@ def get_backend(backend_name=None): try: backend_cls = getattr(backend_module, backend_cls_name) except AttributeError: - raise ImproperlyConfigured("Could not find a class named {backend_module_name!r} in {backend_cls_name!r}".format( - backend_module_name = backend_module_name, - backend_cls_name = backend_cls_name, - )) + raise ImproperlyConfigured( + "Could not find a class named {backend_module_name!r} in {backend_cls_name!r}".format( + backend_module_name=backend_module_name, + backend_cls_name=backend_cls_name, + )) # Initialize the backend. backend = backend_cls() _backends_cache[backend_name] = backend diff --git a/src/watson/south_migrations/0001_initial.py b/watson/south_migrations/0001_initial.py similarity index 100% rename from src/watson/south_migrations/0001_initial.py rename to watson/south_migrations/0001_initial.py diff --git a/src/watson/south_migrations/0002_installwatson.py b/watson/south_migrations/0002_installwatson.py similarity index 100% rename from src/watson/south_migrations/0002_installwatson.py rename to watson/south_migrations/0002_installwatson.py diff --git a/src/watson/south_migrations/__init__.py b/watson/south_migrations/__init__.py similarity index 100% rename from src/watson/south_migrations/__init__.py rename to watson/south_migrations/__init__.py diff --git a/src/watson/templates/watson/includes/search_result_item.html b/watson/templates/watson/includes/search_result_item.html similarity index 100% rename from src/watson/templates/watson/includes/search_result_item.html rename to watson/templates/watson/includes/search_result_item.html diff --git a/src/watson/templates/watson/includes/search_results.html b/watson/templates/watson/includes/search_results.html similarity index 100% rename from src/watson/templates/watson/includes/search_results.html rename to watson/templates/watson/includes/search_results.html diff --git a/src/watson/templates/watson/search_results.html b/watson/templates/watson/search_results.html similarity index 100% rename from src/watson/templates/watson/search_results.html rename to watson/templates/watson/search_results.html diff --git a/src/watson/templatetags/__init__.py b/watson/templatetags/__init__.py similarity index 100% rename from src/watson/templatetags/__init__.py rename to watson/templatetags/__init__.py diff --git a/src/watson/templatetags/watson.py b/watson/templatetags/watson.py similarity index 100% rename from src/watson/templatetags/watson.py rename to watson/templatetags/watson.py diff --git a/src/watson/urls.py b/watson/urls.py similarity index 98% rename from src/watson/urls.py rename to watson/urls.py index de2355f..0481726 100644 --- a/src/watson/urls.py +++ b/watson/urls.py @@ -9,7 +9,6 @@ from watson.views import search, search_json urlpatterns = [ url("^$", search, name="search"), - url("^json/$", search_json, name="search_json"), ] diff --git a/src/watson/views.py b/watson/views.py similarity index 100% rename from src/watson/views.py rename to watson/views.py