Repository: django-haystack/django-haystack

Issue:
App server won't start if GDAL library is not setup
* [x] Tested with the latest Haystack release
* [x] Tested with the current Haystack master branch

## Expected behaviour
Setting up GDAL library should be optional, if someone doesn't want to use LocationField

## Actual behaviour

django.contrib.gis.geos.Point import is global in fields.py which depends on GDAL library and it throws an exception: ImproperlyConfigured: Could not find the GDAL library

In the version Haystack 2.8.1 this is a local import in the convert function of the LocationField, which solves the issue.

## Steps to reproduce the behaviour

1. Make sure that GDAL library is not setup for your app, runserver will fail with the above mentioned exception.

## Configuration

* Operating system version:  macOS 10.13.4
* Search engine version: Elasticsearch 5.5.3
* Python version: 2.7.13
* Django version: 1.11.13
* Haystack version: Latest Master from Commit - bbcd038a96a4572bafa7f47bdf9eb0d345a2445b


Discussion:
Same problem here. Current master branch, macOS 10.13.6, Elasticsearch 5.6, Python 3.6, Django 2.0.7.
I'm also experiencing the same problem. For now I've opted to fork it and remove the LocationField functionality that relies on this, and then install my Fork. Which I realize is a hacky solution, but I don't need the LocationField.

Resolving patch:
diff --git a/haystack/fields.py b/haystack/fields.py
--- a/haystack/fields.py
+++ b/haystack/fields.py
@@ -4,7 +4,6 @@
 import re
 from inspect import ismethod
 
-from django.contrib.gis.geos import Point
 from django.template import loader
 from django.utils import datetime_safe, six
 
@@ -260,6 +259,7 @@ def prepare(self, obj):
         return "%s,%s" % (pnt_lat, pnt_lng)
 
     def convert(self, value):
+        from django.contrib.gis.geos import Point
         from haystack.utils.geo import ensure_point
 
         if value is None:


Test patch:
diff --git a/test_haystack/elasticsearch2_tests/test_backend.py b/test_haystack/elasticsearch2_tests/test_backend.py
--- a/test_haystack/elasticsearch2_tests/test_backend.py
+++ b/test_haystack/elasticsearch2_tests/test_backend.py
@@ -10,7 +10,6 @@
 import elasticsearch
 from django.apps import apps
 from django.conf import settings
-from django.contrib.gis.geos import Point
 from django.test import TestCase
 from django.test.utils import override_settings
 
@@ -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
--- a/test_haystack/elasticsearch2_tests/test_query.py
+++ b/test_haystack/elasticsearch2_tests/test_query.py
@@ -4,7 +4,6 @@
 import datetime
 
 import elasticsearch
-from django.contrib.gis.geos import Point
 from django.contrib.gis.measure import D
 from django.test import TestCase
 
@@ -140,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',
         )
@@ -198,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={
@@ -228,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
--- a/test_haystack/elasticsearch5_tests/test_backend.py
+++ b/test_haystack/elasticsearch5_tests/test_backend.py
@@ -10,7 +10,6 @@
 import elasticsearch
 from django.apps import apps
 from django.conf import settings
-from django.contrib.gis.geos import Point
 from django.test import TestCase
 from django.test.utils import override_settings
 
@@ -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
--- a/test_haystack/elasticsearch5_tests/test_query.py
+++ b/test_haystack/elasticsearch5_tests/test_query.py
@@ -3,8 +3,6 @@
 
 import datetime
 
-import elasticsearch
-from django.contrib.gis.geos import Point
 from django.contrib.gis.measure import D
 from django.test import TestCase
 
@@ -140,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',
         )
@@ -184,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
--- a/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py
+++ b/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py
@@ -11,7 +11,6 @@
 import elasticsearch
 from django.apps import apps
 from django.conf import settings
-from django.contrib.gis.geos import Point
 from django.test import TestCase
 from django.test.utils import override_settings
 
@@ -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
--- a/test_haystack/elasticsearch_tests/test_elasticsearch_query.py
+++ b/test_haystack/elasticsearch_tests/test_elasticsearch_query.py
@@ -5,7 +5,6 @@
 import datetime
 
 import elasticsearch
-from django.contrib.gis.geos import Point
 from django.contrib.gis.measure import D
 from django.test import TestCase
 
@@ -153,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',
         )
@@ -220,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={
@@ -250,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/test_solr_backend.py b/test_haystack/solr_tests/test_solr_backend.py
--- a/test_haystack/solr_tests/test_solr_backend.py
+++ b/test_haystack/solr_tests/test_solr_backend.py
@@ -9,7 +9,6 @@
 
 import pysolr
 from django.conf import settings
-from django.contrib.gis.geos import Point
 from django.test import TestCase
 from django.test.utils import override_settings
 from mock import patch
@@ -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/test_spatial.py b/test_haystack/spatial/test_spatial.py
--- a/test_haystack/spatial/test_spatial.py
+++ b/test_haystack/spatial/test_spatial.py
@@ -2,7 +2,6 @@
 
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-from django.contrib.gis.geos import GEOSGeometry, Point
 from django.contrib.gis.measure import D
 from django.test import TestCase
 
@@ -22,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]
         )
@@ -30,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]
         )
@@ -41,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,
@@ -70,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(
@@ -81,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(
@@ -97,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
--- a/test_haystack/test_managers.py
+++ b/test_haystack/test_managers.py
@@ -4,7 +4,6 @@
 
 import datetime
 
-from django.contrib.gis.geos import Point
 from django.contrib.gis.measure import D
 from django.test import TestCase
 from test_haystack.core.models import MockModel
@@ -86,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))
@@ -115,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)
@@ -129,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)
@@ -142,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))