diff --git a/.travis.yml b/.travis.yml index a4fc89042..11c9233c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,25 +32,26 @@ before_install: # See https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html#deb-repo - wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - - > - if [[ $VERSION_ES == '>=2.0.0,<3.0.0' ]]; - then - echo "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-2.x.list - sudo apt-get update - sudo apt-get -qy --allow-downgrades install elasticsearch=2.4.6 - elif [[ $VERSION_ES == '>=5.0.0,<6.0.0' ]]; - then - echo "deb https://artifacts.elastic.co/packages/5.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-5.x.list - sudo apt-get update -qy - sudo apt-get -y --allow-downgrades install elasticsearch=5.6.10 - else - echo "deb http://packages.elastic.co/elasticsearch/1.7/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-1.7.list - sudo apt-get update -qy - sudo apt-get -qy --allow-downgrades install elasticsearch=1.7.6 - fi + if [[ $VERSION_ES == '>=2.0.0,<3.0.0' ]]; + then + echo "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-2.x.list + sudo apt-get update + sudo apt-get -qy --allow-downgrades install elasticsearch=2.4.6 + elif [[ $VERSION_ES == '>=5.0.0,<6.0.0' ]]; + then + echo "deb https://artifacts.elastic.co/packages/5.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-5.x.list + sudo apt-get update -qy + sudo apt-get -y --allow-downgrades install elasticsearch=5.6.10 + else + echo "deb http://packages.elastic.co/elasticsearch/1.7/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-1.7.list + sudo apt-get update -qy + sudo apt-get -qy --allow-downgrades install elasticsearch=1.7.6 + fi - sudo service elasticsearch restart install: - pip install --upgrade setuptools + - pip install codecov coverage - pip install requests "Django${DJANGO_VERSION}" "elasticsearch${VERSION_ES}" - python setup.py clean build install @@ -59,16 +60,22 @@ before_script: script: - python test_haystack/solr_tests/server/wait-for-solr - - python setup.py test + - coverage run setup.py test + +after_success: + - codecov env: matrix: - DJANGO_VERSION=">=1.11,<2.0" VERSION_ES=">=1.0.0,<2.0.0" - DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=1.0.0,<2.0.0" + - DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=1.0.0,<2.0.0" - DJANGO_VERSION=">=1.11,<2.0" VERSION_ES=">=2.0.0,<3.0.0" - DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=2.0.0,<3.0.0" + - DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=2.0.0,<3.0.0" - DJANGO_VERSION=">=1.11,<2.0" VERSION_ES=">=5.0.0,<6.0.0" - DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=5.0.0,<6.0.0" + - DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=5.0.0,<6.0.0" matrix: allow_failures: - python: 'pypy' @@ -79,7 +86,31 @@ matrix: env: DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=2.0.0,<3.0.0" - python: 2.7 env: DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=1.0.0,<2.0.0" + - python: 2.7 + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=5.0.0,<6.0.0" + - python: 2.7 + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=2.0.0,<3.0.0" + - python: 2.7 + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=1.0.0,<2.0.0" + - python: 3.4 + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=5.0.0,<6.0.0" + - python: 3.4 + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=2.0.0,<3.0.0" + - python: 3.4 + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=1.0.0,<2.0.0" + - python: pypy + env: DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=5.0.0,<6.0.0" + - python: pypy + env: DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=2.0.0,<3.0.0" + - python: pypy + env: DJANGO_VERSION=">=2.0,<2.1" VERSION_ES=">=1.0.0,<2.0.0" + - python: pypy + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=5.0.0,<6.0.0" + - python: pypy + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=2.0.0,<3.0.0" + - python: pypy + env: DJANGO_VERSION=">=2.1,<2.2" VERSION_ES=">=1.0.0,<2.0.0" notifications: - irc: "irc.freenode.org#haystack" + irc: 'irc.freenode.org#haystack' email: false diff --git a/docs/backend_support.rst b/docs/backend_support.rst index 9ba1a265f..7936d9841 100644 --- a/docs/backend_support.rst +++ b/docs/backend_support.rst @@ -50,7 +50,7 @@ ElasticSearch * Stored (non-indexed) fields * Highlighting * Spatial search -* Requires: `elasticsearch-py `_ 1.x or 2.x. ElasticSearch 5.X is currently unsupported: see `#1383 `_. +* Requires: `elasticsearch-py `_ 1.x, 2.x, or 5.X. Whoosh ------ diff --git a/docs/changelog.rst b/docs/changelog.rst index c200c82bf..00a749710 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -93,6 +93,8 @@ Changelog Add max-retries argument to rebuild_index managment command. This is useful for debug at development time + Add Django 2.1 compatibility. [Tim Graham] + v2.8.1 (2018-03-16) ------------------- @@ -4112,5 +4114,3 @@ v1.1 (2010-11-23) [Daniel Lindsley] - Initial commit. Basic IndexSite implementation complete. Needs tests. [Daniel Lindsley] - - diff --git a/docs/installing_search_engines.rst b/docs/installing_search_engines.rst index 3dc73ef37..f08bc6e0d 100644 --- a/docs/installing_search_engines.rst +++ b/docs/installing_search_engines.rst @@ -17,9 +17,11 @@ Solr 4.x+ with a little effort. Installation is relatively simple: For Solr 6.X:: curl -LO https://archive.apache.org/dist/lucene/solr/x.Y.0/solr-X.Y.0.tgz + mkdir solr tar -C solr -xf solr-X.Y.0.tgz --strip-components=1 cd solr - ./bin/solr create -c tester -n basic_config + ./bin/solr start # start solr + ./bin/solr create -c tester -n basic_config # create core named 'tester' By default this will create a core with a managed schema. This setup is dynamic but not useful for haystack, and we'll need to configure solr to use a static diff --git a/docs/searchqueryset_api.rst b/docs/searchqueryset_api.rst index ea8e5bbde..98eb77e84 100644 --- a/docs/searchqueryset_api.rst +++ b/docs/searchqueryset_api.rst @@ -252,6 +252,14 @@ instead of normal keyword arguments:: result = sqs[0] result.highlighted['other_field'][0] # u'Two computer scientists walk into a bar. The bartender says "Foo!".' +Elasticsearch accepts keyword arguments:: + + # Use the ``pre_tag`` and ``post_tag`` keywords and pass the desired tags as lists. + sqs = SearchQuerySet().filter(content='foo').highlight( + pre_tags=[''], post_tags=['']) + result_example = " ".join(sqs[0].highlighted) + # u'Two foo computer scientists walk into a bar. The bartender says "Foo!"' + ``models`` ~~~~~~~~~~ diff --git a/docs/spatial.rst b/docs/spatial.rst index de54af6a9..4e2906d58 100644 --- a/docs/spatial.rst +++ b/docs/spatial.rst @@ -65,8 +65,7 @@ Geospatial Assumptions ---------- Haystack prefers to work with ``Point`` objects, which are located in -``django.contrib.gis.geos.Point`` but conviently importable out of -``haystack.utils.geo.Point``. +``django.contrib.gis.geos.Point``. ``Point`` objects use **LONGITUDE, LATITUDE** for their construction, regardless if you use the parameters to instantiate them or WKT_/``GEOSGeometry``. @@ -76,7 +75,7 @@ if you use the parameters to instantiate them or WKT_/``GEOSGeometry``. Examples:: # Using positional arguments. - from haystack.utils.geo import Point + from django.contrib.gis.geos import Point pnt = Point(-95.23592948913574, 38.97127105172941) # Using WKT. @@ -92,8 +91,7 @@ with GeoDjango's use. ------------ Haystack also uses the ``D`` (or ``Distance``) objects from GeoDjango, -implemented in ``django.contrib.gis.measure.Distance`` but conveniently -importable out of ``haystack.utils.geo.D`` (or ``haystack.utils.geo.Distance``). +implemented in ``django.contrib.gis.measure.Distance``. ``Distance`` objects accept a very flexible set of measurements during instantiaton and can convert amongst them freely. This is important, because @@ -102,7 +100,7 @@ whatever units you want. Examples:: - from haystack.utils.geo import D + from django.contrib.gis.measure import D # Start at 5 miles. imperial_d = D(mi=5) @@ -223,7 +221,7 @@ point. It is faster but slighty sloppier than its counterpart. Examples:: from haystack.query import SearchQuerySet - from haystack.utils.geo import Point + from django.contrib.gis.geos import Point downtown_bottom_left = Point(-95.23947, 38.9637903) downtown_top_right = Point(-95.23362278938293, 38.973081081164715) @@ -263,7 +261,7 @@ calculations on your part. Examples:: from haystack.query import SearchQuerySet - from haystack.utils.geo import Point, D + from django.contrib.gis.geos import Point, D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) # Within a two miles. @@ -306,7 +304,7 @@ include these calculated distances on results. Examples:: from haystack.query import SearchQuerySet - from haystack.utils.geo import Point, D + from django.contrib.gis.geos import Point, D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) @@ -324,7 +322,7 @@ key, well-cached hotspots in town but want distances from the user's current position:: from haystack.query import SearchQuerySet - from haystack.utils.geo import Point, D + from django.contrib.gis.geos import Point, D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) user_loc = Point(-95.23455619812012, 38.97240128290697) @@ -365,7 +363,7 @@ distance information on the results & nothing to sort by. Examples:: from haystack.query import SearchQuerySet - from haystack.utils.geo import Point, D + from django.contrib.gis.geos import Point, D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) downtown_bottom_left = Point(-95.23947, 38.9637903) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 0cd97bb70..a8133d7d7 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -112,7 +112,7 @@ the following: Solr ~~~~ -Example:: +Example (Solr 4.X):: HAYSTACK_CONNECTIONS = { 'default': { @@ -123,6 +123,17 @@ Example:: }, } +Example (Solr 6.X):: + + HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'haystack.backends.solr_backend.SolrEngine', + 'URL': 'http://127.0.0.1:8983/solr/tester', # Assuming you created a core named 'tester' as described in installing search engines. + 'ADMIN_URL': 'http://127.0.0.1:8983/solr/admin/cores' + # ...or for multicore... + # 'URL': 'http://127.0.0.1:8983/solr/mysite', + }, + } Elasticsearch ~~~~~~~~~~~~~ @@ -147,6 +158,7 @@ Example (ElasticSearch 2.x):: }, } + Whoosh ~~~~~~ diff --git a/haystack/__init__.py b/haystack/__init__.py index c76274ed4..d9f5f8025 100644 --- a/haystack/__init__.py +++ b/haystack/__init__.py @@ -38,7 +38,8 @@ ) if hasattr(settings, "HAYSTACK_INCLUDE_SPELLING"): raise ImproperlyConfigured( - "The HAYSTACK_INCLUDE_SPELLING setting is now a per-backend setting & belongs in HAYSTACK_CONNECTIONS." + "The HAYSTACK_INCLUDE_SPELLING setting is now a per-backend setting" + " & belongs in HAYSTACK_CONNECTIONS." ) diff --git a/haystack/admin.py b/haystack/admin.py index 2814dedc3..cfbe13092 100644 --- a/haystack/admin.py +++ b/haystack/admin.py @@ -15,28 +15,13 @@ from haystack.utils import get_model_ct_tuple -def list_max_show_all(changelist): - """ - Returns the maximum amount of results a changelist can have for the - "Show all" link to be displayed in a manner compatible with both Django - 1.4 and 1.3. See Django ticket #15997 for details. - """ - try: - # This import is available in Django 1.3 and below - from django.contrib.admin.views.main import MAX_SHOW_ALL_ALLOWED - - return MAX_SHOW_ALL_ALLOWED - except ImportError: - return changelist.list_max_show_all - - class SearchChangeList(ChangeList): def __init__(self, **kwargs): self.haystack_connection = kwargs.pop("haystack_connection", "default") super(SearchChangeList, self).__init__(**kwargs) def get_results(self, request): - if not SEARCH_VAR in request.GET: + if SEARCH_VAR not in request.GET: return super(SearchChangeList, self).get_results(request) # Note that pagination is 0-based, not 1-based. @@ -54,7 +39,7 @@ def get_results(self, request): SearchQuerySet(self.haystack_connection).models(self.model).all().count() ) - can_show_all = result_count <= list_max_show_all(self) + can_show_all = result_count <= self.list_max_show_all multi_page = result_count > self.list_per_page # Get the list of objects to display on this page. @@ -83,7 +68,7 @@ def changelist_view(self, request, extra_context=None): if not self.has_change_permission(request, None): raise PermissionDenied - if not SEARCH_VAR in request.GET: + if SEARCH_VAR not in request.GET: # Do the usual song and dance. return super(SearchModelAdminMixin, self).changelist_view( request, extra_context @@ -91,12 +76,13 @@ def changelist_view(self, request, extra_context=None): # Do a search of just this model and populate a Changelist with the # returned bits. - if ( - not self.model - in connections[self.haystack_connection] + indexed_models = ( + connections[self.haystack_connection] .get_unified_index() .get_indexed_models() - ): + ) + + if self.model not in indexed_models: # Oops. That model isn't being indexed. Return the usual # behavior instead. return super(SearchModelAdminMixin, self).changelist_view( @@ -119,15 +105,13 @@ def changelist_view(self, request, extra_context=None): "list_select_related": self.list_select_related, "list_per_page": self.list_per_page, "list_editable": self.list_editable, + "list_max_show_all": self.list_max_show_all, "model_admin": self, } - - # Django 1.4 compatibility. - if hasattr(self, "list_max_show_all"): - kwargs["list_max_show_all"] = self.list_max_show_all - + if hasattr(self, 'get_sortable_by'): # Django 2.1+ + kwargs["sortable_by"] = self.get_sortable_by(request) changelist = SearchChangeList(**kwargs) - formset = changelist.formset = None + changelist.formset = None media = self.media # Build the action form and populate it with available actions. @@ -160,8 +144,7 @@ def changelist_view(self, request, extra_context=None): "cl": changelist, "media": media, "has_add_permission": self.has_add_permission(request), - # More Django 1.4 compatibility - "root_path": getattr(self.admin_site, "root_path", None), + "opts": changelist.opts, "app_label": self.model._meta.app_label, "action_form": action_form, "actions_on_top": self.actions_on_top, diff --git a/haystack/backends/__init__.py b/haystack/backends/__init__.py index 87355b531..e60e7bd1a 100644 --- a/haystack/backends/__init__.py +++ b/haystack/backends/__init__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals import copy +import six from copy import deepcopy from time import time from django.conf import settings from django.db.models import Q from django.db.models.base import ModelBase -from django.utils import six from django.utils import tree from django.utils.encoding import force_text diff --git a/haystack/backends/elasticsearch_backend.py b/haystack/backends/elasticsearch_backend.py index bcd6796b7..8dca5d177 100644 --- a/haystack/backends/elasticsearch_backend.py +++ b/haystack/backends/elasticsearch_backend.py @@ -2,13 +2,13 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import six import re import warnings from datetime import datetime, timedelta from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils import six import haystack from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query @@ -123,13 +123,13 @@ def __init__(self, connection_alias, **connection_options): connection_alias, **connection_options ) - if not "URL" in connection_options: + if "URL" not in connection_options: raise ImproperlyConfigured( "You must specify a 'URL' in your settings for connection '%s'." % connection_alias ) - if not "INDEX_NAME" in connection_options: + if "INDEX_NAME" not in connection_options: raise ImproperlyConfigured( "You must specify a 'INDEX_NAME' in your settings for connection '%s'." % connection_alias @@ -735,7 +735,7 @@ def from_timestamp(tm): additional_fields["_point_of_origin"] = distance_point if geo_sort and raw_result.get("sort"): - from haystack.utils.geo import Distance + from django.contrib.gis.measure import Distance additional_fields["_distance"] = Distance( km=float(raw_result["sort"][0]) diff --git a/haystack/backends/simple_backend.py b/haystack/backends/simple_backend.py index bfd3f30b9..d4748f4cb 100644 --- a/haystack/backends/simple_backend.py +++ b/haystack/backends/simple_backend.py @@ -5,11 +5,10 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import six from warnings import warn -from django.conf import settings from django.db.models import Q -from django.utils import six from haystack import connections from haystack.backends import ( @@ -23,26 +22,6 @@ from haystack.models import SearchResult from haystack.utils import get_model_ct_tuple -if settings.DEBUG: - import logging - - class NullHandler(logging.Handler): - def emit(self, record): - pass - - ch = logging.StreamHandler() - ch.setLevel(logging.WARNING) - ch.setFormatter( - logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ) - - logger = logging.getLogger("haystack.simple_backend") - logger.setLevel(logging.WARNING) - logger.addHandler(NullHandler()) - logger.addHandler(ch) -else: - logger = None - class SimpleSearchBackend(BaseSearchBackend): def update(self, indexer, iterable, commit=True): diff --git a/haystack/backends/solr_backend.py b/haystack/backends/solr_backend.py index 347fd514e..bc6068e94 100644 --- a/haystack/backends/solr_backend.py +++ b/haystack/backends/solr_backend.py @@ -2,11 +2,11 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import six import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils import six import haystack from haystack.backends import ( @@ -561,7 +561,7 @@ def _process_results( additional_fields["_point_of_origin"] = distance_point if raw_result.get("__dist__"): - from haystack.utils.geo import Distance + from django.contrib.gis.measure import Distance additional_fields["_distance"] = Distance( km=float(raw_result["__dist__"]) diff --git a/haystack/backends/whoosh_backend.py b/haystack/backends/whoosh_backend.py index b435a5167..0c379482e 100644 --- a/haystack/backends/whoosh_backend.py +++ b/haystack/backends/whoosh_backend.py @@ -6,12 +6,12 @@ import os import re import shutil +import six import threading import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils import six from django.utils.datetime_safe import datetime from django.utils.encoding import force_text @@ -22,7 +22,13 @@ EmptyResults, log_query, ) -from haystack.constants import DJANGO_CT, DJANGO_ID, ID +from haystack.constants import ( + DJANGO_CT, + DJANGO_ID, + FUZZY_WHOOSH_MAX_EDITS, + FUZZY_WHOOSH_MIN_PREFIX, + ID, +) from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument from haystack.inputs import Clean, Exact, PythonData, Raw from haystack.models import SearchResult @@ -59,7 +65,7 @@ from whoosh.filedb.filestore import FileStorage, RamStorage from whoosh.highlight import highlight as whoosh_highlight from whoosh.highlight import ContextFragmenter, HtmlFormatter -from whoosh.qparser import QueryParser +from whoosh.qparser import QueryParser, FuzzyTermPlugin from whoosh.searching import ResultsPage from whoosh.writing import AsyncWriter @@ -162,6 +168,7 @@ def setup(self): connections[self.connection_alias].get_unified_index().all_searchfields() ) self.parser = QueryParser(self.content_field_name, schema=self.schema) + self.parser.add_plugins([FuzzyTermPlugin]) if new_index is True: self.index = self.storage.create_index(self.schema) @@ -959,7 +966,7 @@ def build_query_fragment(self, field, filter_type, value): "gte": "[%s to]", "lt": "{to %s}", "lte": "[to %s]", - "fuzzy": "%s~", + "fuzzy": "%s~{}/%d".format(FUZZY_WHOOSH_MAX_EDITS), } if value.post_process is False: @@ -987,10 +994,23 @@ def build_query_fragment(self, field, filter_type, value): possible_values = [prepared_value] for possible_value in possible_values: - terms.append( - filter_types[filter_type] - % self.backend._from_python(possible_value) + possible_value_str = self.backend._from_python( + possible_value ) + if filter_type == "fuzzy": + terms.append( + filter_types[filter_type] % ( + possible_value_str, + min( + FUZZY_WHOOSH_MIN_PREFIX, + len(possible_value_str) + ) + ) + ) + else: + terms.append( + filter_types[filter_type] % possible_value_str + ) if len(terms) == 1: query_frag = terms[0] diff --git a/haystack/constants.py b/haystack/constants.py index 88f6751c9..24ad98d64 100644 --- a/haystack/constants.py +++ b/haystack/constants.py @@ -20,6 +20,10 @@ FUZZY_MIN_SIM = getattr(settings, "HAYSTACK_FUZZY_MIN_SIM", 0.5) FUZZY_MAX_EXPANSIONS = getattr(settings, "HAYSTACK_FUZZY_MAX_EXPANSIONS", 50) +# Default values on whoosh +FUZZY_WHOOSH_MIN_PREFIX = getattr(settings, 'HAYSTACK_FUZZY_WHOOSH_MIN_PREFIX', 3) +FUZZY_WHOOSH_MAX_EDITS = getattr(settings, 'HAYSTACK_FUZZY_WHOOSH_MAX_EDITS', 2) + # Valid expression extensions. VALID_FILTERS = set( [ @@ -46,6 +50,7 @@ # Number of SearchResults to load at a time. ITERATOR_LOAD_PER_QUERY = getattr(settings, "HAYSTACK_ITERATOR_LOAD_PER_QUERY", 10) + # A marker class in the hierarchy to indicate that it handles search data. class Indexable(object): haystack_use_for_indexing = True diff --git a/haystack/fields.py b/haystack/fields.py index 4f3626031..042b755eb 100644 --- a/haystack/fields.py +++ b/haystack/fields.py @@ -2,10 +2,11 @@ from __future__ import absolute_import, division, print_function, unicode_literals import re +import six from inspect import ismethod from django.template import loader -from django.utils import datetime_safe, six +from django.utils import datetime_safe from haystack.exceptions import SearchFieldError from haystack.utils import get_model_ct_tuple @@ -259,7 +260,8 @@ def prepare(self, obj): return "%s,%s" % (pnt_lat, pnt_lng) def convert(self, value): - from haystack.utils.geo import ensure_point, Point + from django.contrib.gis.geos import Point + from haystack.utils.geo import ensure_point if value is None: return None diff --git a/haystack/generic_views.py b/haystack/generic_views.py index a5a12db00..74cdfc9c9 100644 --- a/haystack/generic_views.py +++ b/haystack/generic_views.py @@ -44,7 +44,6 @@ class SearchMixin(MultipleObjectMixin, FormMixin): template_name = "search/search.html" load_all = True form_class = ModelSearchForm - queryset = SearchQuerySet() context_object_name = None paginate_by = RESULTS_PER_PAGE paginate_orphans = 0 @@ -54,6 +53,11 @@ class SearchMixin(MultipleObjectMixin, FormMixin): search_field = "q" object_list = None + def get_queryset(self): + if self.queryset is None: + self.queryset = SearchQuerySet() + return self.queryset + def get_form_kwargs(self): """ Returns the keyword arguments for instantiating the form. diff --git a/haystack/indexes.py b/haystack/indexes.py index c71995f60..f0c4d6320 100644 --- a/haystack/indexes.py +++ b/haystack/indexes.py @@ -5,14 +5,32 @@ import copy import threading import warnings +from six import with_metaclass from django.core.exceptions import ImproperlyConfigured from django.utils.encoding import force_text -from django.utils.six import with_metaclass from haystack import connection_router, connections -from haystack.constants import DEFAULT_ALIAS, DJANGO_CT, DJANGO_ID, ID, Indexable -from haystack.fields import * +from haystack.constants import Indexable # NOQA — exposed as a public export +from haystack.constants import DEFAULT_ALIAS, DJANGO_CT, DJANGO_ID, ID +from haystack.fields import ( # NOQA — exposed as a public export + BooleanField, + CharField, + DateField, + DateTimeField, + DecimalField, + EdgeNgramField, + FacetCharField, + FacetDateTimeField, + FacetIntegerField, + FloatField, + IntegerField, + LocationField, + MultiValueField, + NgramField, + SearchField, + SearchFieldError, +) from haystack.manager import SearchIndexManager from haystack.utils import get_facet_field_name, get_identifier, get_model_ct @@ -41,7 +59,7 @@ def __new__(cls, name, bases, attrs): for field_name, obj in attrs.items(): # Only need to check the FacetFields. if hasattr(obj, "facet_for"): - if not obj.facet_for in facet_fields: + if obj.facet_for not in facet_fields: facet_fields[obj.facet_for] = [] facet_fields[obj.facet_for].append(field_name) @@ -56,10 +74,10 @@ def __new__(cls, name, bases, attrs): # Only check non-faceted fields for the following info. if not hasattr(field, "facet_for"): - if field.faceted == True: + if field.faceted: # If no other field is claiming this field as # ``facet_for``, create a shadow ``FacetField``. - if not field_name in facet_fields: + if field_name not in facet_fields: shadow_facet_name = get_facet_field_name(field_name) shadow_facet_field = field.facet_class(facet_for=field_name) shadow_facet_field.set_instance_name(shadow_facet_name) @@ -68,7 +86,7 @@ def __new__(cls, name, bases, attrs): attrs["fields"].update(built_fields) # Assigning default 'objects' query manager if it does not already exist - if not "objects" in attrs: + if "objects" not in attrs: try: attrs["objects"] = SearchIndexManager(attrs["Meta"].index_label) except (KeyError, AttributeError): @@ -179,7 +197,8 @@ def build_queryset(self, using=None, start_date=None, end_date=None): if hasattr(self, "get_queryset"): warnings.warn( - "'SearchIndex.get_queryset' was deprecated in Haystack v2. Please rename the method 'index_queryset'." + "'SearchIndex.get_queryset' was deprecated in Haystack v2." + " Please rename the method 'index_queryset'." ) index_qs = self.get_queryset() else: diff --git a/haystack/inputs.py b/haystack/inputs.py index f1b0a7a65..12a0d19c3 100644 --- a/haystack/inputs.py +++ b/haystack/inputs.py @@ -4,8 +4,9 @@ import re import warnings +from six import python_2_unicode_compatible -from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.encoding import force_text @python_2_unicode_compatible @@ -117,7 +118,7 @@ def prepare(self, query_obj): for rough_token in self.exact_match_re.split(query_string): if not rough_token: continue - elif not rough_token in exacts: + elif rough_token not in exacts: # We have something that's not an exact match but may have more # than on word in it. tokens.extend(rough_token.split(" ")) diff --git a/haystack/management/commands/clear_index.py b/haystack/management/commands/clear_index.py index f2639f330..aa216e3a5 100644 --- a/haystack/management/commands/clear_index.py +++ b/haystack/management/commands/clear_index.py @@ -2,8 +2,9 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import six + from django.core.management.base import BaseCommand -from django.utils import six from haystack import connections diff --git a/haystack/models.py b/haystack/models.py index 06e72fd3b..fee0925ed 100644 --- a/haystack/models.py +++ b/haystack/models.py @@ -4,8 +4,9 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import six + from django.core.exceptions import ObjectDoesNotExist -from django.utils import six from django.utils.encoding import force_text from django.utils.text import capfirst @@ -123,7 +124,7 @@ def _set_model(self, obj): model = property(_get_model, _set_model) def _get_distance(self): - from haystack.utils.geo import Distance + from django.contrib.gis.measure import Distance if self._distance is None: # We didn't get it from the backend & we haven't tried calculating diff --git a/haystack/panels.py b/haystack/panels.py index 08fff1a33..ee6fb8f79 100644 --- a/haystack/panels.py +++ b/haystack/panels.py @@ -3,10 +3,10 @@ from __future__ import absolute_import, division, print_function, unicode_literals import datetime +import six from debug_toolbar.panels import DebugPanel from django.template.loader import render_to_string -from django.utils import six from django.utils.translation import ugettext_lazy as _ from haystack import connections diff --git a/haystack/query.py b/haystack/query.py index 390b099d6..f66055723 100644 --- a/haystack/query.py +++ b/haystack/query.py @@ -3,10 +3,9 @@ from __future__ import absolute_import, division, print_function, unicode_literals import operator +import six import warnings -from django.utils import six - from haystack import connection_router, connections from haystack.backends import SQ from haystack.constants import DEFAULT_OPERATOR, ITERATOR_LOAD_PER_QUERY diff --git a/haystack/templatetags/highlight.py b/haystack/templatetags/highlight.py index 2853b83ae..2fd45f794 100644 --- a/haystack/templatetags/highlight.py +++ b/haystack/templatetags/highlight.py @@ -2,10 +2,11 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import six + from django import template from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils import six from haystack.utils import importlib diff --git a/haystack/templatetags/more_like_this.py b/haystack/templatetags/more_like_this.py index da0c3306f..8f69cd3cb 100644 --- a/haystack/templatetags/more_like_this.py +++ b/haystack/templatetags/more_like_this.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import logging + from django import template from haystack.query import SearchQuerySet @@ -17,7 +19,7 @@ def __init__(self, model, varname, for_types=None, limit=None): self.for_types = for_types self.limit = limit - if not self.limit is None: + if self.limit is not None: self.limit = int(self.limit) def render(self, context): @@ -25,7 +27,7 @@ def render(self, context): model_instance = self.model.resolve(context) sqs = SearchQuerySet() - if not self.for_types is None: + if self.for_types is not None: intermediate = template.Variable(self.for_types) for_types = intermediate.resolve(context).split(",") search_models = [] @@ -40,12 +42,14 @@ def render(self, context): sqs = sqs.more_like_this(model_instance) - if not self.limit is None: + if self.limit is not None: sqs = sqs[: self.limit] context[self.varname] = sqs - except: - pass + except Exception as exc: + logging.warning( + "Unhandled exception rendering %r: %s", self, exc, exc_info=True + ) return "" diff --git a/haystack/utils/__init__.py b/haystack/utils/__init__.py index 17b10123c..e5f11b4a3 100644 --- a/haystack/utils/__init__.py +++ b/haystack/utils/__init__.py @@ -4,9 +4,9 @@ import importlib import re +import six from django.conf import settings -from django.utils import six from haystack.constants import ID, DJANGO_CT, DJANGO_ID from haystack.utils.highlighting import Highlighter diff --git a/haystack/utils/geo.py b/haystack/utils/geo.py index 60dac7a52..a25c9dad9 100644 --- a/haystack/utils/geo.py +++ b/haystack/utils/geo.py @@ -2,9 +2,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from django.contrib.gis.geos import Point -from django.contrib.gis.measure import D, Distance - from haystack.constants import WGS_84_SRID from haystack.exceptions import SpatialError @@ -58,7 +55,7 @@ def ensure_distance(dist): try: # Since we mostly only care about the ``.km`` attribute, make sure # it's there. - km = dist.km + dist.km except AttributeError: raise SpatialError("'%s' does not appear to be a 'Distance' object." % dist) diff --git a/haystack/utils/highlighting.py b/haystack/utils/highlighting.py index 014ac89e0..d57658267 100644 --- a/haystack/utils/highlighting.py +++ b/haystack/utils/highlighting.py @@ -42,7 +42,7 @@ def find_highlightable_words(self): lower_text_block = self.text_block.lower() for word in self.query_words: - if not word in word_positions: + if word not in word_positions: word_positions[word] = [] start_offset = 0 diff --git a/haystack/utils/loading.py b/haystack/utils/loading.py index 985dfecc6..662a6eda6 100644 --- a/haystack/utils/loading.py +++ b/haystack/utils/loading.py @@ -4,13 +4,13 @@ import copy import inspect +import six import threading import warnings from collections import OrderedDict from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils import six from django.utils.module_loading import module_has_submodule from haystack import constants diff --git a/haystack/views.py b/haystack/views.py index 27c66ff9d..cdde85746 100644 --- a/haystack/views.py +++ b/haystack/views.py @@ -37,7 +37,7 @@ def __init__( if form_class is None: self.form_class = ModelSearchForm - if not results_per_page is None: + if results_per_page is not None: self.results_per_page = results_per_page if template: diff --git a/setup.cfg b/setup.cfg index f12ea5257..8cfac863d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,7 @@ exclude=docs [flake8] line_length=88 exclude=docs +ignore = E203, E501, W503 [isort] line_length=88 diff --git a/setup.py b/setup.py index 3d4d360ae..887956047 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ "Framework :: Django", "Framework :: Django :: 1.11", "Framework :: Django :: 2.0", + "Framework :: Django :: 2.1", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", diff --git a/test_haystack/elasticsearch2_tests/test_backend.py b/test_haystack/elasticsearch2_tests/test_backend.py index 9e7333d32..6c644fa79 100644 --- a/test_haystack/elasticsearch2_tests/test_backend.py +++ b/test_haystack/elasticsearch2_tests/test_backend.py @@ -19,7 +19,6 @@ from haystack.models import SearchResult from haystack.query import SQ, RelatedSearchQuerySet, SearchQuerySet from haystack.utils import log as logging -from haystack.utils.geo import Point from haystack.utils.loading import UnifiedIndex from ..core.models import AFourthMockModel, AnotherMockModel, ASixthMockModel, MockModel @@ -582,6 +581,7 @@ def test_search(self): settings.HAYSTACK_LIMIT_TO_REGISTERED_MODELS = old_limit_to_registered_models def test_spatial_search_parameters(self): + from django.contrib.gis.geos import Point p1 = Point(1.23, 4.56) kwargs = self.sb.build_search_kwargs( "*:*", diff --git a/test_haystack/elasticsearch2_tests/test_query.py b/test_haystack/elasticsearch2_tests/test_query.py index 06a844628..d10b7917e 100644 --- a/test_haystack/elasticsearch2_tests/test_query.py +++ b/test_haystack/elasticsearch2_tests/test_query.py @@ -4,13 +4,13 @@ import datetime import elasticsearch +from django.contrib.gis.measure import D from django.test import TestCase from haystack import connections from haystack.inputs import Exact from haystack.models import SearchResult from haystack.query import SQ, SearchQuerySet -from haystack.utils.geo import D, Point from ..core.models import AnotherMockModel, MockModel @@ -139,7 +139,7 @@ def test_clean(self): self.assertEqual(self.sq.clean("hello AND world"), "hello and world") self.assertEqual( self.sq.clean( - 'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / world' + r'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / world' ), 'hello and or not to \\+ \\- \\&& \\|| \\! \\( \\) \\{ \\} \\[ \\] \\^ \\" \\~ \\* \\? \\: \\\\ \\/ world', ) @@ -197,6 +197,8 @@ def test_build_query_with_dwithin_range(self): """ Test build_search_kwargs with dwithin range for Elasticsearch versions < 1.0.0 """ + from django.contrib.gis.geos import Point + search_kwargs = self.backend.build_search_kwargs( "where", dwithin={ @@ -227,6 +229,8 @@ def test_build_query_with_dwithin_range(self): """ Test build_search_kwargs with dwithin range for Elasticsearch versions >= 1.0.0 """ + from django.contrib.gis.geos import Point + search_kwargs = self.backend.build_search_kwargs( "where", dwithin={ diff --git a/test_haystack/elasticsearch5_tests/test_backend.py b/test_haystack/elasticsearch5_tests/test_backend.py index 9b9e3eadb..cc94c0b03 100644 --- a/test_haystack/elasticsearch5_tests/test_backend.py +++ b/test_haystack/elasticsearch5_tests/test_backend.py @@ -19,7 +19,6 @@ from haystack.models import SearchResult from haystack.query import SQ, RelatedSearchQuerySet, SearchQuerySet from haystack.utils import log as logging -from haystack.utils.geo import Point from haystack.utils.loading import UnifiedIndex from ..core.models import AFourthMockModel, AnotherMockModel, ASixthMockModel, MockModel @@ -582,6 +581,8 @@ def test_search(self): settings.HAYSTACK_LIMIT_TO_REGISTERED_MODELS = old_limit_to_registered_models def test_spatial_search_parameters(self): + from django.contrib.gis.geos import Point + p1 = Point(1.23, 4.56) kwargs = self.sb.build_search_kwargs( "*:*", diff --git a/test_haystack/elasticsearch5_tests/test_query.py b/test_haystack/elasticsearch5_tests/test_query.py index cbddc2d8d..564a2fd15 100644 --- a/test_haystack/elasticsearch5_tests/test_query.py +++ b/test_haystack/elasticsearch5_tests/test_query.py @@ -3,14 +3,13 @@ import datetime -import elasticsearch +from django.contrib.gis.measure import D from django.test import TestCase from haystack import connections from haystack.inputs import Exact from haystack.models import SearchResult from haystack.query import SQ, SearchQuerySet -from haystack.utils.geo import D, Point from ..core.models import AnotherMockModel, MockModel @@ -139,7 +138,7 @@ def test_clean(self): self.assertEqual(self.sq.clean("hello AND world"), "hello and world") self.assertEqual( self.sq.clean( - 'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / world' + r'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / world' ), 'hello and or not to \\+ \\- \\&& \\|| \\! \\( \\) \\{ \\} \\[ \\] \\^ \\" \\~ \\* \\? \\: \\\\ \\/ world', ) @@ -183,6 +182,8 @@ def test_narrow_sq(self): self.assertEqual(sqs.query.narrow_queries.pop(), "foo:(moof)") def test_build_query_with_dwithin_range(self): + from django.contrib.gis.geos import Point + backend = connections["elasticsearch"].get_backend() search_kwargs = backend.build_search_kwargs( "where", diff --git a/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py b/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py index 7c9b9b715..2f72d081e 100644 --- a/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py +++ b/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py @@ -20,7 +20,6 @@ from haystack.models import SearchResult from haystack.query import SQ, RelatedSearchQuerySet, SearchQuerySet from haystack.utils import log as logging -from haystack.utils.geo import Point from haystack.utils.loading import UnifiedIndex from ..core.models import AFourthMockModel, AnotherMockModel, ASixthMockModel, MockModel @@ -622,6 +621,8 @@ def test_search(self): settings.HAYSTACK_LIMIT_TO_REGISTERED_MODELS = old_limit_to_registered_models def test_spatial_search_parameters(self): + from django.contrib.gis.geos import Point + p1 = Point(1.23, 4.56) kwargs = self.sb.build_search_kwargs( "*:*", diff --git a/test_haystack/elasticsearch_tests/test_elasticsearch_query.py b/test_haystack/elasticsearch_tests/test_elasticsearch_query.py index d51c6ab07..56f32346d 100644 --- a/test_haystack/elasticsearch_tests/test_elasticsearch_query.py +++ b/test_haystack/elasticsearch_tests/test_elasticsearch_query.py @@ -5,13 +5,13 @@ import datetime import elasticsearch +from django.contrib.gis.measure import D from django.test import TestCase from haystack import connections from haystack.inputs import Exact from haystack.models import SearchResult from haystack.query import SQ, SearchQuerySet -from haystack.utils.geo import D, Point from ..core.models import AnotherMockModel, MockModel @@ -152,7 +152,7 @@ def test_clean(self): self.assertEqual(self.sq.clean("hello AND world"), "hello and world") self.assertEqual( self.sq.clean( - 'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / world' + r'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / world' ), 'hello and or not to \\+ \\- \\&& \\|| \\! \\( \\) \\{ \\} \\[ \\] \\^ \\" \\~ \\* \\? \\: \\\\ \\/ world', ) @@ -219,6 +219,8 @@ def test_build_query_with_dwithin_range(self): """ Test build_search_kwargs with dwithin range for Elasticsearch versions < 1.0.0 """ + from django.contrib.gis.geos import Point + search_kwargs = self.backend.build_search_kwargs( "where", dwithin={ @@ -249,6 +251,8 @@ def test_build_query_with_dwithin_range(self): """ Test build_search_kwargs with dwithin range for Elasticsearch versions >= 1.0.0 """ + from django.contrib.gis.geos import Point + search_kwargs = self.backend.build_search_kwargs( "where", dwithin={ diff --git a/test_haystack/solr_tests/server/start-solr-test-server.sh b/test_haystack/solr_tests/server/start-solr-test-server.sh index 3c9340730..9bd57ea5f 100755 --- a/test_haystack/solr_tests/server/start-solr-test-server.sh +++ b/test_haystack/solr_tests/server/start-solr-test-server.sh @@ -2,7 +2,7 @@ set -e -SOLR_VERSION=6.5.0 +SOLR_VERSION=6.6.4 SOLR_DIR=solr @@ -28,12 +28,17 @@ if [ ! -f ${SOLR_ARCHIVE} ]; then curl -Lo $SOLR_ARCHIVE ${SOLR_DOWNLOAD_URL} || (echo "Unable to download ${SOLR_DOWNLOAD_URL}"; exit 2) fi -echo "Extracting Solr ${SOLR_ARCHIVE} to `pwd`/${SOLR_DIR}" +echo "Extracting Solr ${SOLR_ARCHIVE} to ${TEST_ROOT}/${SOLR_DIR}" rm -rf ${SOLR_DIR} mkdir ${SOLR_DIR} FULL_SOLR_DIR=$(readlink -f ./${SOLR_DIR}) tar -C ${SOLR_DIR} -xf ${SOLR_ARCHIVE} --strip-components=1 +# These tuning options will break on Java 10 and for testing we don't care about +# production server optimizations: +export GC_LOG_OPTS="" +export GC_TUNE="" + export SOLR_LOGS_DIR="${FULL_SOLR_DIR}/logs" install -d ${SOLR_LOGS_DIR} diff --git a/test_haystack/solr_tests/test_solr_backend.py b/test_haystack/solr_tests/test_solr_backend.py index e1a88353f..6f2e1de02 100644 --- a/test_haystack/solr_tests/test_solr_backend.py +++ b/test_haystack/solr_tests/test_solr_backend.py @@ -19,7 +19,6 @@ from haystack.inputs import AltParser, AutoQuery, Raw from haystack.models import SearchResult from haystack.query import SQ, RelatedSearchQuerySet, SearchQuerySet -from haystack.utils.geo import Point from haystack.utils.loading import UnifiedIndex from ..core.models import AFourthMockModel, AnotherMockModel, ASixthMockModel, MockModel @@ -561,6 +560,8 @@ def test_spelling(self): ) def test_spatial_search_parameters(self): + from django.contrib.gis.geos import Point + p1 = Point(1.23, 4.56) kwargs = self.sb.build_search_kwargs( "*:*", diff --git a/test_haystack/spatial/models.py b/test_haystack/spatial/models.py index ed1166257..756536e2e 100644 --- a/test_haystack/spatial/models.py +++ b/test_haystack/spatial/models.py @@ -29,7 +29,7 @@ class Meta: def get_location(self): # Nothing special about this Point, but ensure that's we don't have to worry # about import paths. - from haystack.utils.geo import Point + from django.contrib.gis.geos import Point pnt = Point(self.longitude, self.latitude) return pnt diff --git a/test_haystack/spatial/test_spatial.py b/test_haystack/spatial/test_spatial.py index a33c009ad..750e7e1c5 100644 --- a/test_haystack/spatial/test_spatial.py +++ b/test_haystack/spatial/test_spatial.py @@ -2,15 +2,13 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from django.contrib.gis.geos import GEOSGeometry +from django.contrib.gis.measure import D from django.test import TestCase from haystack import connections from haystack.exceptions import SpatialError from haystack.query import SearchQuerySet from haystack.utils.geo import ( - D, - Point, ensure_distance, ensure_geometry, ensure_point, @@ -23,6 +21,8 @@ class SpatialUtilitiesTestCase(TestCase): def test_ensure_geometry(self): + from django.contrib.gis.geos import GEOSGeometry, Point + self.assertRaises( SpatialError, ensure_geometry, [38.97127105172941, -95.23592948913574] ) @@ -31,6 +31,8 @@ def test_ensure_geometry(self): ensure_geometry(Point(-95.23592948913574, 38.97127105172941)) def test_ensure_point(self): + from django.contrib.gis.geos import GEOSGeometry, Point + self.assertRaises( SpatialError, ensure_point, [38.97127105172941, -95.23592948913574] ) @@ -42,6 +44,8 @@ def test_ensure_point(self): ensure_point(Point(-95.23592948913574, 38.97127105172941)) def test_ensure_wgs84(self): + from django.contrib.gis.geos import GEOSGeometry, Point + self.assertRaises( SpatialError, ensure_wgs84, @@ -71,6 +75,8 @@ def test_ensure_distance(self): ensure_distance(D(mi=5)) def test_generate_bounding_box(self): + from django.contrib.gis.geos import Point + downtown_bottom_left = Point(-95.23947, 38.9637903) downtown_top_right = Point(-95.23362278938293, 38.973081081164715) ((min_lat, min_lng), (max_lat, max_lng)) = generate_bounding_box( @@ -82,6 +88,8 @@ def test_generate_bounding_box(self): self.assertEqual(max_lng, -95.23362278938293) def test_generate_bounding_box_crossing_line_date(self): + from django.contrib.gis.geos import Point + downtown_bottom_left = Point(95.23947, 38.9637903) downtown_top_right = Point(-95.23362278938293, 38.973081081164715) ((south, west), (north, east)) = generate_bounding_box( @@ -98,6 +106,8 @@ class SpatialSolrTestCase(TestCase): using = "solr" def setUp(self): + from django.contrib.gis.geos import Point + super(SpatialSolrTestCase, self).setUp() self.ui = connections[self.using].get_unified_index() self.checkindex = self.ui.get_index(Checkin) diff --git a/test_haystack/test_managers.py b/test_haystack/test_managers.py index 0fcfa8dbe..257de66d3 100644 --- a/test_haystack/test_managers.py +++ b/test_haystack/test_managers.py @@ -4,6 +4,7 @@ import datetime +from django.contrib.gis.measure import D from django.test import TestCase from test_haystack.core.models import MockModel @@ -16,7 +17,6 @@ ValuesListSearchQuerySet, ValuesSearchQuerySet, ) -from haystack.utils.geo import D, Point from .mocks import CharPKMockSearchBackend from .test_views import BasicAnotherMockModelSearchIndex, BasicMockModelSearchIndex @@ -85,6 +85,8 @@ def test_order_by(self): self.assertTrue("foo" in sqs.query.order_by) def test_order_by_distance(self): + from django.contrib.gis.geos import Point + p = Point(1.23, 4.56) sqs = self.search_index.objects.distance("location", p).order_by("distance") self.assertTrue(isinstance(sqs, SearchQuerySet)) @@ -114,6 +116,8 @@ def test_facets(self): self.assertEqual(len(sqs.query.facets), 1) def test_within(self): + from django.contrib.gis.geos import Point + # This is a meaningless query but we're just confirming that the manager updates the parameters here: p1 = Point(-90, -90) p2 = Point(90, 90) @@ -128,6 +132,8 @@ def test_within(self): ) def test_dwithin(self): + from django.contrib.gis.geos import Point + p = Point(0, 0) distance = D(mi=500) sqs = self.search_index.objects.dwithin("location", p, distance) @@ -141,6 +147,8 @@ def test_dwithin(self): ) def test_distance(self): + from django.contrib.gis.geos import Point + p = Point(0, 0) sqs = self.search_index.objects.distance("location", p) self.assertTrue(isinstance(sqs, SearchQuerySet)) diff --git a/test_haystack/whoosh_tests/test_whoosh_query.py b/test_haystack/whoosh_tests/test_whoosh_query.py index 2d928d2fa..9813ac458 100644 --- a/test_haystack/whoosh_tests/test_whoosh_query.py +++ b/test_haystack/whoosh_tests/test_whoosh_query.py @@ -115,7 +115,7 @@ def test_build_query_wildcard_filter_types(self): def test_build_query_fuzzy_filter_types(self): self.sq.add_filter(SQ(content="why")) self.sq.add_filter(SQ(title__fuzzy="haystack")) - self.assertEqual(self.sq.build_query(), "((why) AND title:(haystack~))") + self.assertEqual(self.sq.build_query(), "((why) AND title:(haystack~2/3))") def test_build_query_with_contains(self): self.sq.add_filter(SQ(content="circular")) diff --git a/tox.ini b/tox.ini index e3fb3d5d1..7539c6cc2 100644 --- a/tox.ini +++ b/tox.ini @@ -5,23 +5,30 @@ envlist = docs, py34-django2.0-es1.x, py35-django1.11-es1.x, py35-django2.0-es1.x, + py35-django2.1-es1.x, pypy-django1.11-es1.x, py27-django1.11-es2.x, py34-django1.11-es2.x, py34-django2.0-es2.x, py35-django1.11-es2.x, py35-django2.0-es2.x, + py35-django2.1-es2.x, py36-django1.11-es2.x, py36-django2.0-es2.x, + py36-django2.1-es2.x, pypy-django1.11-es2.x, py27-django1.11-es5.x, py36-django1.11-es5.x, py36-django2.0-es5.x, + py36-django2.1-es5.x, pypy-django1.11-es5.x, [base] deps = requests +[django2.1] +deps = Django>=2.1,<2.2 + [django2.0] deps = Django>=2.0,<2.1 @@ -92,6 +99,14 @@ deps = {[django2.0]deps} {[base]deps} +[testenv:py35-django2.1-es1.x] +basepython = python3.5 +setenv = VERSION_ES=>=1.0.0,<2.0.0 +deps = + {[es1.x]deps} + {[django2.1]deps} + {[base]deps} + [testenv:pypy-django1.11-es2.x] setenv = VERSION_ES=>=2.0.0,<3.0.0 deps = @@ -139,6 +154,14 @@ deps = {[django2.0]deps} {[base]deps} +[testenv:py35-django2.1-es2.x] +basepython = python3.5 +setenv = VERSION_ES=>=2.0.0,<3.0.0 +deps = + {[es2.x]deps} + {[django2.1]deps} + {[base]deps} + [testenv:py36-django1.11-es2.x] basepython = python3.6 setenv = VERSION_ES=>=2.0.0,<3.0.0 @@ -155,6 +178,14 @@ deps = {[django2.0]deps} {[base]deps} +[testenv:py36-django2.1-es2.x] +basepython = python3.6 +setenv = VERSION_ES=>=2.0.0,<3.0.0 +deps = + {[es2.x]deps} + {[django2.1]deps} + {[base]deps} + [testenv:pypy-django1.11-es5.x] setenv = VERSION_ES=>=5.0.0,<6.0.0 deps = @@ -186,6 +217,14 @@ deps = {[django2.0]deps} {[base]deps} +[testenv:py36-django2.1-es5.x] +basepython = python3.6 +setenv = VERSION_ES=>=5.0.0,<6.0.0 +deps = + {[es5.x]deps} + {[django2.1]deps} + {[base]deps} + [testenv:docs] changedir = docs deps =