Added flake8, fixed syntax, flattened package structure

This commit is contained in:
Rustem Sayargaliev 2016-11-07 21:02:12 +01:00
parent 03c929c571
commit fb1a3991f1
36 changed files with 301 additions and 208 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -1,7 +0,0 @@
"""
Multi-table search application for Django, using native database search engines.
Developed by Dave Hall.
<http://www.etianen.com/>
"""

View File

@ -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"])

View File

@ -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
)

View File

@ -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/")

View File

@ -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)),
]

10
tox.ini
View File

@ -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

View File

@ -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):

View File

@ -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),

View File

@ -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] <app.model|model> <app.model|model> ... ]"
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),
)
)

View File

@ -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__)

View File

@ -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()

View File

@ -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

View File

@ -9,7 +9,6 @@ from watson.views import search, search_json
urlpatterns = [
url("^$", search, name="search"),
url("^json/$", search_json, name="search_json"),
]