From f7a363ee1d2039824d95f35e54219e09c5af67b0 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 14 Jun 2016 16:18:33 +0200 Subject: [PATCH] Fixed #26753 -- Made GDAL a required dependency for contrib.gis Thanks Tim Graham for the review. --- django/contrib/gis/admin/options.py | 9 +- django/contrib/gis/db/backends/base/models.py | 143 +++++------------- django/contrib/gis/db/models/fields.py | 26 +--- django/contrib/gis/forms/widgets.py | 10 +- django/contrib/gis/gdal/__init__.py | 6 - django/contrib/gis/geos/geometry.py | 9 -- .../gis/management/commands/ogrinspect.py | 2 - django/contrib/gis/serializers/geojson.py | 8 +- docs/ref/contrib/gis/db-api.txt | 6 +- docs/ref/contrib/gis/geos.txt | 18 +-- docs/ref/contrib/gis/install/geolibs.txt | 14 +- docs/ref/contrib/gis/install/index.txt | 6 +- docs/ref/contrib/gis/serializers.txt | 4 - docs/ref/contrib/gis/tutorial.txt | 11 +- docs/releases/1.11.txt | 7 + tests/gis_tests/distapp/tests.py | 40 +++-- tests/gis_tests/geo3d/tests.py | 11 +- tests/gis_tests/geoadmin/models.py | 12 -- tests/gis_tests/geoadmin/tests.py | 24 +-- tests/gis_tests/geoapp/test_serializers.py | 11 +- tests/gis_tests/geogapp/tests.py | 2 - tests/gis_tests/geos_tests/test_geos.py | 24 --- .../gis_migrations/test_operations.py | 5 - tests/gis_tests/inspectapp/tests.py | 6 +- tests/gis_tests/layermap/tests.py | 3 - tests/gis_tests/rasterapp/models.py | 33 ++-- tests/gis_tests/rasterapp/test_rasterfield.py | 17 +-- tests/gis_tests/test_geoforms.py | 7 - tests/gis_tests/test_spatialrefsys.py | 11 +- 29 files changed, 117 insertions(+), 368 deletions(-) diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py index aaef037d57..5a3d2fa158 100644 --- a/django/contrib/gis/admin/options.py +++ b/django/contrib/gis/admin/options.py @@ -1,8 +1,7 @@ from django.contrib.admin import ModelAdmin from django.contrib.gis.admin.widgets import OpenLayersWidget from django.contrib.gis.db import models -from django.contrib.gis.gdal import HAS_GDAL, OGRGeomType -from django.core.exceptions import ImproperlyConfigured +from django.contrib.gis.gdal import OGRGeomType spherical_mercator_srid = 3857 @@ -59,12 +58,6 @@ class GeoModelAdmin(ModelAdmin): 3D editing). """ if isinstance(db_field, models.GeometryField) and db_field.dim < 3: - if not HAS_GDAL and db_field.srid != self.map_srid: - raise ImproperlyConfigured( - "Map SRID is %s and SRID of `%s` is %s. GDAL must be " - "installed to perform the transformation." - % (self.map_srid, db_field, db_field.srid) - ) # Setting the widget with the newly defined widget. kwargs['widget'] = self.get_map_widget(db_field) return db_field.formfield(**kwargs) diff --git a/django/contrib/gis/db/backends/base/models.py b/django/contrib/gis/db/backends/base/models.py index befb493919..8ae9f2b126 100644 --- a/django/contrib/gis/db/backends/base/models.py +++ b/django/contrib/gis/db/backends/base/models.py @@ -1,5 +1,3 @@ -import re - from django.contrib.gis import gdal from django.utils import six from django.utils.encoding import python_2_unicode_compatible @@ -11,47 +9,32 @@ class SpatialRefSysMixin(object): The SpatialRefSysMixin is a class used by the database-dependent SpatialRefSys objects to reduce redundant code. """ - # For pulling out the spheroid from the spatial reference string. This - # regular expression is used only if the user does not have GDAL installed. - # TODO: Flattening not used in all ellipsoids, could also be a minor axis, - # or 'b' parameter. - spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P.+)\",(?P\d+(\.\d+)?),(?P\d{3}\.\d+),') - - # For pulling out the units on platforms w/o GDAL installed. - # TODO: Figure out how to pull out angular units of projected coordinate system and - # fix for LOCAL_CS types. GDAL should be highly recommended for performing - # distance queries. - units_regex = re.compile(r'.+UNIT ?\["(?P[\w \.\'\(\)]+)", ?(?P[^ ,\]]+)', re.DOTALL) - @property def srs(self): """ - Returns a GDAL SpatialReference object, if GDAL is installed. + Returns a GDAL SpatialReference object. """ - if gdal.HAS_GDAL: - # TODO: Is caching really necessary here? Is complexity worth it? - if hasattr(self, '_srs'): - # Returning a clone of the cached SpatialReference object. - return self._srs.clone() - else: - # Attempting to cache a SpatialReference object. - - # Trying to get from WKT first. - try: - self._srs = gdal.SpatialReference(self.wkt) - return self.srs - except Exception as e: - msg = e - - try: - self._srs = gdal.SpatialReference(self.proj4text) - return self.srs - except Exception as e: - msg = e - - raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) + # TODO: Is caching really necessary here? Is complexity worth it? + if hasattr(self, '_srs'): + # Returning a clone of the cached SpatialReference object. + return self._srs.clone() else: - raise Exception('GDAL is not installed.') + # Attempting to cache a SpatialReference object. + + # Trying to get from WKT first. + try: + self._srs = gdal.SpatialReference(self.wkt) + return self.srs + except Exception as e: + msg = e + + try: + self._srs = gdal.SpatialReference(self.proj4text) + return self.srs + except Exception as e: + msg = e + + raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) @property def ellipsoid(self): @@ -59,14 +42,7 @@ class SpatialRefSysMixin(object): Returns a tuple of the ellipsoid parameters: (semimajor axis, semiminor axis, and inverse flattening). """ - if gdal.HAS_GDAL: - return self.srs.ellipsoid - else: - m = self.spheroid_regex.match(self.wkt) - if m: - return (float(m.group('major')), float(m.group('flattening'))) - else: - return None + return self.srs.ellipsoid @property def name(self): @@ -86,70 +62,37 @@ class SpatialRefSysMixin(object): @property def projected(self): "Is this Spatial Reference projected?" - if gdal.HAS_GDAL: - return self.srs.projected - else: - return self.wkt.startswith('PROJCS') + return self.srs.projected @property def local(self): "Is this Spatial Reference local?" - if gdal.HAS_GDAL: - return self.srs.local - else: - return self.wkt.startswith('LOCAL_CS') + return self.srs.local @property def geographic(self): "Is this Spatial Reference geographic?" - if gdal.HAS_GDAL: - return self.srs.geographic - else: - return self.wkt.startswith('GEOGCS') + return self.srs.geographic @property def linear_name(self): "Returns the linear units name." - if gdal.HAS_GDAL: - return self.srs.linear_name - elif self.geographic: - return None - else: - m = self.units_regex.match(self.wkt) - return m.group('unit_name') + return self.srs.linear_name @property def linear_units(self): "Returns the linear units." - if gdal.HAS_GDAL: - return self.srs.linear_units - elif self.geographic: - return None - else: - m = self.units_regex.match(self.wkt) - return m.group('unit') + return self.srs.linear_units @property def angular_name(self): "Returns the name of the angular units." - if gdal.HAS_GDAL: - return self.srs.angular_name - elif self.projected: - return None - else: - m = self.units_regex.match(self.wkt) - return m.group('unit_name') + return self.srs.angular_name @property def angular_units(self): "Returns the angular units." - if gdal.HAS_GDAL: - return self.srs.angular_units - elif self.projected: - return None - else: - m = self.units_regex.match(self.wkt) - return m.group('unit') + return self.srs.angular_units @property def units(self): @@ -167,11 +110,7 @@ class SpatialRefSysMixin(object): Return a tuple of (unit_value, unit_name) for the given WKT without using any of the database fields. """ - if gdal.HAS_GDAL: - return gdal.SpatialReference(wkt).units - else: - m = cls.units_regex.match(wkt) - return float(m.group('unit')), m.group('unit_name') + return gdal.SpatialReference(wkt).units @classmethod def get_spheroid(cls, wkt, string=True): @@ -179,17 +118,9 @@ class SpatialRefSysMixin(object): Class method used by GeometryField on initialization to retrieve the `SPHEROID[..]` parameters from the given WKT. """ - if gdal.HAS_GDAL: - srs = gdal.SpatialReference(wkt) - sphere_params = srs.ellipsoid - sphere_name = srs['spheroid'] - else: - m = cls.spheroid_regex.match(wkt) - if m: - sphere_params = (float(m.group('major')), float(m.group('flattening'))) - sphere_name = m.group('name') - else: - return None + srs = gdal.SpatialReference(wkt) + sphere_params = srs.ellipsoid + sphere_name = srs['spheroid'] if not string: return sphere_name, sphere_params @@ -203,10 +134,6 @@ class SpatialRefSysMixin(object): def __str__(self): """ - Returns the string representation. If GDAL is installed, - it will be 'pretty' OGC WKT. + Returns the string representation, a 'pretty' OGC WKT. """ - try: - return six.text_type(self.srs) - except Exception: - return six.text_type(self.wkt) + return six.text_type(self.srs) diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 1621fe4c48..74c2eebbf1 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -1,9 +1,8 @@ -from django.contrib.gis import forms +from django.contrib.gis import forms, gdal from django.contrib.gis.db.models.lookups import ( RasterBandTransform, gis_lookups, ) from django.contrib.gis.db.models.proxy import SpatialProxy -from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.gdal.error import GDALException from django.contrib.gis.geometry.backend import Geometry, GeometryException from django.core.exceptions import ImproperlyConfigured @@ -186,18 +185,16 @@ class BaseSpatialField(Field): """ Return a GDALRaster if conversion is successful, otherwise return None. """ - from django.contrib.gis.gdal import GDALRaster - - if isinstance(value, GDALRaster): + if isinstance(value, gdal.GDALRaster): return value elif is_candidate: try: - return GDALRaster(value) + return gdal.GDALRaster(value) except GDALException: pass elif isinstance(value, dict): try: - return GDALRaster(value) + return gdal.GDALRaster(value) except GDALException: raise ValueError("Couldn't create spatial object from lookup value '%s'." % value) @@ -228,10 +225,8 @@ class BaseSpatialField(Field): else: # Check if input is a candidate for conversion to raster or geometry. is_candidate = isinstance(obj, (bytes, six.string_types)) or hasattr(obj, '__geo_interface__') - # With GDAL installed, try to convert the input to raster. - raster = False - if HAS_GDAL: - raster = self.get_raster_prep_value(obj, is_candidate) + # Try to convert the input to raster. + raster = self.get_raster_prep_value(obj, is_candidate) if raster: obj = raster @@ -425,11 +420,6 @@ class RasterField(BaseSpatialField): geom_type = 'RASTER' geography = False - def __init__(self, *args, **kwargs): - if not HAS_GDAL: - raise ImproperlyConfigured('RasterField requires GDAL.') - super(RasterField, self).__init__(*args, **kwargs) - def _check_connection(self, connection): # Make sure raster fields are used only on backends with raster support. if not connection.features.gis_enabled or not connection.features.supports_raster: @@ -451,13 +441,11 @@ class RasterField(BaseSpatialField): def contribute_to_class(self, cls, name, **kwargs): super(RasterField, self).contribute_to_class(cls, name, **kwargs) - # Importing GDALRaster raises an exception on systems without gdal. - from django.contrib.gis.gdal import GDALRaster # Setup for lazy-instantiated Raster object. For large querysets, the # instantiation of all GDALRasters can potentially be expensive. This # delays the instantiation of the objects to the moment of evaluation # of the raster attribute. - setattr(cls, self.attname, SpatialProxy(GDALRaster, self)) + setattr(cls, self.attname, SpatialProxy(gdal.GDALRaster, self)) def get_transform(self, name): try: diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py index 01d2f7e53f..efa76a0b2d 100644 --- a/django/contrib/gis/forms/widgets.py +++ b/django/contrib/gis/forms/widgets.py @@ -91,6 +91,7 @@ class OSMWidget(BaseGeometryWidget): template_name = 'gis/openlayers-osm.html' default_lon = 5 default_lat = 47 + map_srid = 3857 class Media: js = ( @@ -104,12 +105,3 @@ class OSMWidget(BaseGeometryWidget): self.attrs[key] = getattr(self, key) if attrs: self.attrs.update(attrs) - - @property - def map_srid(self): - # Use the official spherical mercator projection SRID when GDAL is - # available; otherwise, fallback to 900913. - if gdal.HAS_GDAL: - return 3857 - else: - return 900913 diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py index 4d8688c6ac..86ce62a068 100644 --- a/django/contrib/gis/gdal/__init__.py +++ b/django/contrib/gis/gdal/__init__.py @@ -24,12 +24,6 @@ library name for the current OS. The default library path may be overridden by setting `GDAL_LIBRARY_PATH` in your settings with the path to the GDAL C library on your system. - - GDAL links to a large number of external libraries that consume RAM when - loaded. Thus, it may desirable to disable GDAL on systems with limited - RAM resources -- this may be accomplished by setting `GDAL_LIBRARY_PATH` - to a non-existent file location (e.g., `GDAL_LIBRARY_PATH='/null/path'`; - setting to None/False/'' will not work as a string must be given). """ from django.contrib.gis.gdal.envelope import Envelope from django.contrib.gis.gdal.error import ( # NOQA diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index c48e99cd5b..d0a9455659 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -64,8 +64,6 @@ class GEOSGeometry(GEOSBase, ListMixin): g = wkb_r().read(force_bytes(geo_input)) elif json_regex.match(geo_input): # Handling GeoJSON input. - if not gdal.HAS_GDAL: - raise ValueError('Initializing geometry from JSON input requires GDAL.') g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb) else: raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.') @@ -476,8 +474,6 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def ogr(self): "Returns the OGR Geometry for this Geometry." - if not gdal.HAS_GDAL: - raise GEOSException('GDAL required to convert to an OGRGeometry.') if self.srid: try: return gdal.OGRGeometry(self.wkb, self.srid) @@ -488,8 +484,6 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def srs(self): "Returns the OSR SpatialReference for SRID of this Geometry." - if not gdal.HAS_GDAL: - raise GEOSException('GDAL required to return a SpatialReference object.') if self.srid: try: return gdal.SpatialReference(self.srid) @@ -520,9 +514,6 @@ class GEOSGeometry(GEOSBase, ListMixin): else: return - if not gdal.HAS_GDAL: - raise GEOSException("GDAL library is not available to transform() geometry.") - if isinstance(ct, gdal.CoordTransform): # We don't care about SRID because CoordTransform presupposes # source SRS. diff --git a/django/contrib/gis/management/commands/ogrinspect.py b/django/contrib/gis/management/commands/ogrinspect.py index cb4ee96e4c..aa5e22ec7c 100644 --- a/django/contrib/gis/management/commands/ogrinspect.py +++ b/django/contrib/gis/management/commands/ogrinspect.py @@ -98,8 +98,6 @@ class Command(BaseCommand): def handle(self, *args, **options): data_source, model_name = options.pop('data_source'), options.pop('model_name') - if not gdal.HAS_GDAL: - raise CommandError('GDAL is required to inspect geospatial data sources.') # Getting the OGR DataSource from the string parameter. try: diff --git a/django/contrib/gis/serializers/geojson.py b/django/contrib/gis/serializers/geojson.py index 49c0b7ba52..dcdf153c36 100644 --- a/django/contrib/gis/serializers/geojson.py +++ b/django/contrib/gis/serializers/geojson.py @@ -1,9 +1,7 @@ from __future__ import unicode_literals from django.contrib.gis.gdal import HAS_GDAL -from django.core.serializers.base import ( - SerializationError, SerializerDoesNotExist, -) +from django.core.serializers.base import SerializerDoesNotExist from django.core.serializers.json import Serializer as JSONSerializer if HAS_GDAL: @@ -53,10 +51,6 @@ class Serializer(JSONSerializer): if self._geometry: if self._geometry.srid != self.srid: # If needed, transform the geometry in the srid of the global geojson srid - if not HAS_GDAL: - raise SerializationError( - 'Unable to convert geometry to SRID %s when GDAL is not installed.' % self.srid - ) if self._geometry.srid not in self._cts: srs = SpatialReference(self.srid) self._cts[self._geometry.srid] = CoordTransform(self._geometry.srs, srs) diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 3db8f426aa..07acf14b35 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -91,9 +91,9 @@ transform procedure:: Thus, geometry parameters may be passed in using the ``GEOSGeometry`` object, WKT (Well Known Text [#fnwkt]_), HEXEWKB (PostGIS specific -- a WKB geometry in -hexadecimal [#fnewkb]_), and GeoJSON [#fngeojson]_ (requires GDAL). Essentially, -if the input is not a ``GEOSGeometry`` object, the geometry field will attempt to -create a ``GEOSGeometry`` instance from the input. +hexadecimal [#fnewkb]_), and GeoJSON [#fngeojson]_. Essentially, if the input is +not a ``GEOSGeometry`` object, the geometry field will attempt to create a +``GEOSGeometry`` instance from the input. For more information creating :class:`~django.contrib.gis.geos.GEOSGeometry` objects, refer to the :ref:`GEOS tutorial `. diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index 11c1a8dcd3..811a9ba946 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -195,7 +195,7 @@ Format Input Type WKT / EWKT ``str`` or ``unicode`` HEX / HEXEWKB ``str`` or ``unicode`` WKB / EWKB ``buffer`` -GeoJSON (requires GDAL) ``str`` or ``unicode`` +GeoJSON ``str`` or ``unicode`` ======================= ====================== Properties @@ -345,10 +345,6 @@ another object. Returns an :class:`~django.contrib.gis.gdal.OGRGeometry` object corresponding to the GEOS geometry. - .. note:: - - Requires GDAL. - .. _wkb: .. attribute:: GEOSGeometry.wkb @@ -618,10 +614,6 @@ Other Properties & Methods Returns a :class:`~django.contrib.gis.gdal.SpatialReference` object corresponding to the SRID of the geometry or ``None``. - .. note:: - - Requires GDAL. - .. method:: GEOSGeometry.transform(ct, clone=False) Transforms the geometry according to the given coordinate transformation @@ -635,10 +627,10 @@ Other Properties & Methods .. note:: - Requires GDAL. Raises :class:`~django.contrib.gis.geos.GEOSException` if - GDAL is not available or if the geometry's SRID is ``None`` or less than - 0. It doesn't impose any constraints on the geometry's SRID if called - with a :class:`~django.contrib.gis.gdal.CoordTransform` object. + Raises :class:`~django.contrib.gis.geos.GEOSException` if GDAL is not + available or if the geometry's SRID is ``None`` or less than 0. It + doesn't impose any constraints on the geometry's SRID if called with a + :class:`~django.contrib.gis.gdal.CoordTransform` object. .. versionchanged:: 1.10 diff --git a/docs/ref/contrib/gis/install/geolibs.txt b/docs/ref/contrib/gis/install/geolibs.txt index d668b4cacf..8598fcaca2 100644 --- a/docs/ref/contrib/gis/install/geolibs.txt +++ b/docs/ref/contrib/gis/install/geolibs.txt @@ -10,7 +10,7 @@ Program Description Required ======================== ==================================== ================================ =================================== :doc:`GEOS <../geos>` Geometry Engine Open Source Yes 3.4, 3.3 `PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 4.9, 4.8, 4.7, 4.6, 4.5, 4.4 -:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes (SQLite only) 2.1, 2.0, 1.11, 1.10, 1.9, 1.8, 1.7 +:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes 2.1, 2.0, 1.11, 1.10, 1.9, 1.8, 1.7 :doc:`GeoIP <../geoip>` IP-based geolocation library No 1.4 `PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 2.2, 2.1 `SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 4.3, 4.2, 4.1, 4.0 @@ -19,6 +19,11 @@ Program Description Required Note that older or more recent versions of these libraries *may* also work totally fine with GeoDjango. Your mileage may vary. +.. versionchanged:: 1.11 + + In older versions, GDAL is required only for SQLite. Now it's required for + all databases. + .. Libs release dates: GEOS 3.3.0 2011-05-30 @@ -37,13 +42,6 @@ totally fine with GeoDjango. Your mileage may vary. Spatialite 4.2.0 2014-07-25 Spatialite 4.3.0 2015-09-07 -.. admonition:: Install GDAL - - While :ref:`gdalbuild` is technically not required, it is *recommended*. - Important features of GeoDjango (including the :doc:`../layermapping`, - geometry reprojection, and the geographic admin) depend on its - functionality. - .. note:: The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 10b0577a1a..2bdc78a6ba 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -58,9 +58,9 @@ supported versions, and any notes for each of the supported database backends: ================== ============================== ================== ========================================= Database Library Requirements Supported Versions Notes ================== ============================== ================== ========================================= -PostgreSQL GEOS, PROJ.4, PostGIS 9.3+ Requires PostGIS. -MySQL GEOS 5.5+ Not OGC-compliant; :ref:`limited functionality `. -Oracle GEOS 11.2+ XE not supported. +PostgreSQL GEOS, GDAL, PROJ.4, PostGIS 9.3+ Requires PostGIS. +MySQL GEOS, GDAL 5.5+ Not OGC-compliant; :ref:`limited functionality `. +Oracle GEOS, GDAL 11.2+ XE not supported. SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 4.0+, pysqlite2 2.5+ ================== ============================== ================== ========================================= diff --git a/docs/ref/contrib/gis/serializers.txt b/docs/ref/contrib/gis/serializers.txt index 14137d2e1a..ffd134936b 100644 --- a/docs/ref/contrib/gis/serializers.txt +++ b/docs/ref/contrib/gis/serializers.txt @@ -8,10 +8,6 @@ GeoDjango provides a specific serializer for the `GeoJSON`__ format. See :doc:`/topics/serialization` for more information on serialization. -The GDAL library is required if any of the serialized geometries need -coordinate transformations (that is if the geometry's spatial reference system -differs from the ``srid`` serializer option). - __ http://geojson.org/ The ``geojson`` serializer is not meant for round-tripping data, as it has no diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index 5d753a327e..f7fd175103 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -668,7 +668,7 @@ for popular geospatial formats:: MULTIPOLYGON (((12.4157980000000006 43.9579540000000009, 12.4505540000000003 43.9797209999999978, ... >>> sm.mpoly.wkb # WKB (as Python binary buffer) - >>> sm.mpoly.geojson # GeoJSON (requires GDAL) + >>> sm.mpoly.geojson # GeoJSON '{ "type": "MultiPolygon", "coordinates": [ [ [ [ 12.415798, 43.957954 ], [ 12.450554, 43.979721 ], ... This includes access to all of the advanced geometric operations provided by @@ -753,13 +753,8 @@ This provides more context (including street and thoroughfare details) than available with the :class:`~django.contrib.gis.admin.GeoModelAdmin` (which uses the `Vector Map Level 0`_ WMS dataset hosted at `OSGeo`_). -First, there are some important requirements: - -* :class:`~django.contrib.gis.admin.OSMGeoAdmin` requires that - :doc:`GDAL ` is installed. - -* The PROJ.4 datum shifting files must be installed (see the - :ref:`PROJ.4 installation instructions ` for more details). +The PROJ.4 datum shifting files must be installed (see the :ref:`PROJ.4 +installation instructions ` for more details). If you meet this requirement, then just substitute the ``OSMGeoAdmin`` option class in your ``admin.py`` file:: diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 709c4052f1..019b52f0f8 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -227,6 +227,13 @@ Validators Backwards incompatible changes in 1.11 ====================================== +:mod:`django.contrib.gis` +------------------------- + +* To simplify the codebase and because it's easier to install than when + ``contrib.gis`` was first released, :ref:`gdalbuild` is now a required + dependency for GeoDjango. In older versions, it's only required for SQLite. + Database backend API -------------------- diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py index ca057da9b8..71ab5265d6 100644 --- a/tests/gis_tests/distapp/tests.py +++ b/tests/gis_tests/distapp/tests.py @@ -1,16 +1,13 @@ from __future__ import unicode_literals -from unittest import skipUnless - from django.contrib.gis.db.models.functions import ( Area, Distance, Length, Perimeter, Transform, ) -from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import GEOSGeometry, LineString, Point from django.contrib.gis.measure import D # alias for Distance from django.db import connection from django.db.models import F, Q -from django.test import TestCase, ignore_warnings, mock, skipUnlessDBFeature +from django.test import TestCase, ignore_warnings, skipUnlessDBFeature from django.utils.deprecation import RemovedInDjango20Warning from ..utils import no_oracle, oracle, postgis @@ -497,7 +494,6 @@ class DistanceFunctionsTests(TestCase): tol ) - @skipUnless(HAS_GDAL, "GDAL is required.") @skipUnlessDBFeature("has_Distance_function", "has_Transform_function") def test_distance_projected(self): """ @@ -520,26 +516,24 @@ class DistanceFunctionsTests(TestCase): 455411.438904354, 519386.252102563, 696139.009211594, 232513.278304279, 542445.630586414, 456679.155883207] - for has_gdal in [False, True]: - with mock.patch('django.contrib.gis.gdal.HAS_GDAL', has_gdal): - # Testing using different variations of parameters and using models - # with different projected coordinate systems. - dist1 = SouthTexasCity.objects.annotate(distance=Distance('point', lagrange)).order_by('id') - if oracle: - dist_qs = [dist1] - else: - dist2 = SouthTexasCityFt.objects.annotate(distance=Distance('point', lagrange)).order_by('id') - dist_qs = [dist1, dist2] + # Testing using different variations of parameters and using models + # with different projected coordinate systems. + dist1 = SouthTexasCity.objects.annotate(distance=Distance('point', lagrange)).order_by('id') + if oracle: + dist_qs = [dist1] + else: + dist2 = SouthTexasCityFt.objects.annotate(distance=Distance('point', lagrange)).order_by('id') + dist_qs = [dist1, dist2] - # Original query done on PostGIS, have to adjust AlmostEqual tolerance - # for Oracle. - tol = 2 if oracle else 5 + # Original query done on PostGIS, have to adjust AlmostEqual tolerance + # for Oracle. + tol = 2 if oracle else 5 - # Ensuring expected distances are returned for each distance queryset. - for qs in dist_qs: - for i, c in enumerate(qs): - self.assertAlmostEqual(m_distances[i], c.distance.m, tol) - self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) + # Ensuring expected distances are returned for each distance queryset. + for qs in dist_qs: + for i, c in enumerate(qs): + self.assertAlmostEqual(m_distances[i], c.distance.m, tol) + self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) @skipUnlessDBFeature("has_Distance_function", "supports_distance_geodetic") def test_distance_geodetic(self): diff --git a/tests/gis_tests/geo3d/tests.py b/tests/gis_tests/geo3d/tests.py index 1181e4b6c2..34ed371d48 100644 --- a/tests/gis_tests/geo3d/tests.py +++ b/tests/gis_tests/geo3d/tests.py @@ -2,13 +2,11 @@ from __future__ import unicode_literals import os import re -from unittest import skipUnless from django.contrib.gis.db.models import Extent3D, Union from django.contrib.gis.db.models.functions import ( AsGeoJSON, AsKML, Length, Perimeter, Scale, Translate, ) -from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon from django.test import TestCase, ignore_warnings, skipUnlessDBFeature from django.utils._os import upath @@ -19,10 +17,6 @@ from .models import ( MultiPoint3D, Point2D, Point3D, Polygon2D, Polygon3D, ) -if HAS_GDAL: - from django.contrib.gis.utils import LayerMapping, LayerMapError - - data_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data')) city_file = os.path.join(data_path, 'cities', 'cities.shp') vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt') @@ -101,7 +95,6 @@ class Geo3DLoadingHelper(object): Polygon3D.objects.create(name='3D BBox', poly=bbox_3d) -@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.") @skipUnlessDBFeature("gis_enabled", "supports_3d_storage") class Geo3DTest(Geo3DLoadingHelper, TestCase): """ @@ -147,6 +140,9 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase): """ Testing LayerMapping on 3D models. """ + # Import here as GDAL is required for those imports + from django.contrib.gis.utils import LayerMapping, LayerMapError + point_mapping = {'point': 'POINT'} mpoint_mapping = {'mpoint': 'MULTIPOINT'} @@ -310,7 +306,6 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase): self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z) -@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.") @skipUnlessDBFeature("gis_enabled", "supports_3d_functions") class Geo3DFunctionsTests(Geo3DLoadingHelper, TestCase): def test_kml(self): diff --git a/tests/gis_tests/geoadmin/models.py b/tests/gis_tests/geoadmin/models.py index 3a9724789c..6945dc0376 100644 --- a/tests/gis_tests/geoadmin/models.py +++ b/tests/gis_tests/geoadmin/models.py @@ -17,17 +17,5 @@ class City(models.Model): return self.name -@python_2_unicode_compatible -class CityMercator(models.Model): - name = models.CharField(max_length=30) - point = models.PointField(srid=3857) - - class Meta: - required_db_features = ['gis_enabled'] - - def __str__(self): - return self.name - site = admin.AdminSite(name='admin_gis') site.register(City, admin.OSMGeoAdmin) -site.register(CityMercator, admin.OSMGeoAdmin) diff --git a/tests/gis_tests/geoadmin/tests.py b/tests/gis_tests/geoadmin/tests.py index be25b41d21..fc6c031dc0 100644 --- a/tests/gis_tests/geoadmin/tests.py +++ b/tests/gis_tests/geoadmin/tests.py @@ -1,15 +1,11 @@ from __future__ import unicode_literals -from unittest import skipUnless - from django.contrib.gis import admin -from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import Point -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase, mock, override_settings, skipUnlessDBFeature +from django.test import TestCase, override_settings, skipUnlessDBFeature from .admin import UnmodifiableAdmin -from .models import City, CityMercator, site +from .models import City, site @skipUnlessDBFeature("gis_enabled") @@ -56,22 +52,6 @@ class GeoAdminTest(TestCase): """"http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic', format: 'image/jpeg'});""", result) - @mock.patch('django.contrib.gis.admin.options.HAS_GDAL', False) - def test_no_gdal_admin_model_diffent_srid(self): - msg = ( - 'Map SRID is 3857 and SRID of `geoadmin.City.point` is 4326. ' - 'GDAL must be installed to perform the transformation.' - ) - with self.assertRaisesMessage(ImproperlyConfigured, msg): - geoadmin = site._registry[City] - geoadmin.get_changelist_form(None)() - - @mock.patch('django.contrib.gis.admin.options.HAS_GDAL', False) - def test_no_gdal_admin_model_same_srid(self): - geoadmin = site._registry[CityMercator] - geoadmin.get_changelist_form(None)() - - @skipUnless(HAS_GDAL, "GDAL is required.") def test_olwidget_has_changed(self): """ Check that changes are accurately noticed by OpenLayersWidget. diff --git a/tests/gis_tests/geoapp/test_serializers.py b/tests/gis_tests/geoapp/test_serializers.py index 4dfc250621..2b7cf95dd3 100644 --- a/tests/gis_tests/geoapp/test_serializers.py +++ b/tests/gis_tests/geoapp/test_serializers.py @@ -4,8 +4,7 @@ import json from django.contrib.gis.geos import LinearRing, Point, Polygon from django.core import serializers -from django.test import TestCase, mock, skipUnlessDBFeature -from django.utils import six +from django.test import TestCase, skipUnlessDBFeature from .models import City, MultiFields, PennsylvaniaCity @@ -89,14 +88,6 @@ class GeoJSONSerializerTests(TestCase): [1564802, 5613214] ) - @mock.patch('django.contrib.gis.serializers.geojson.HAS_GDAL', False) - def test_without_gdal(self): - # Without coordinate transformation, the serialization should succeed: - serializers.serialize('geojson', City.objects.all()) - with six.assertRaisesRegex(self, serializers.base.SerializationError, '.*GDAL is not installed'): - # Coordinate transformations need GDAL - serializers.serialize('geojson', City.objects.all(), srid=2847) - def test_deserialization_exception(self): """ GeoJSON cannot be deserialized. diff --git a/tests/gis_tests/geogapp/tests.py b/tests/gis_tests/geogapp/tests.py index 0711e26bc1..530b9ac4b6 100644 --- a/tests/gis_tests/geogapp/tests.py +++ b/tests/gis_tests/geogapp/tests.py @@ -8,7 +8,6 @@ from unittest import skipUnless from django.contrib.gis.db import models from django.contrib.gis.db.models.functions import Area, Distance -from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.measure import D from django.db import connection from django.db.models.functions import Cast @@ -70,7 +69,6 @@ class GeographyTest(TestCase): with self.assertRaises(ValueError): City.objects.get(point__exact=htown.point) - @skipUnless(HAS_GDAL, "GDAL is required.") def test05_geography_layermapping(self): "Testing LayerMapping support on models with geography fields." # There is a similar test in `layermap` that uses the same data set, diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py index 5a2ae5f919..36457f4f51 100644 --- a/tests/gis_tests/geos_tests/test_geos.py +++ b/tests/gis_tests/geos_tests/test_geos.py @@ -1092,19 +1092,6 @@ class GEOSTest(SimpleTestCase, TestDataMixin): self.assertEqual(g1.srid, 4326) self.assertIsNot(g1, g, "Clone didn't happen") - with mock.patch('django.contrib.gis.gdal.HAS_GDAL', False): - g = GEOSGeometry('POINT (-104.609 38.255)', 4326) - gt = g.tuple - g.transform(4326) - self.assertEqual(g.tuple, gt) - self.assertEqual(g.srid, 4326) - - g = GEOSGeometry('POINT (-104.609 38.255)', 4326) - g1 = g.transform(4326, clone=True) - self.assertEqual(g1.tuple, g.tuple) - self.assertEqual(g1.srid, 4326) - self.assertIsNot(g1, g, "Clone didn't happen") - @skipUnless(HAS_GDAL, "GDAL is required.") def test_transform_nosrid(self): """ Testing `transform` method (no SRID or negative SRID) """ @@ -1125,17 +1112,6 @@ class GEOSTest(SimpleTestCase, TestDataMixin): with self.assertRaises(GEOSException): g.transform(2774, clone=True) - @mock.patch('django.contrib.gis.gdal.HAS_GDAL', False) - def test_transform_nogdal(self): - """ Testing `transform` method (GDAL not available) """ - g = GEOSGeometry('POINT (-104.609 38.255)', 4326) - with self.assertRaises(GEOSException): - g.transform(2774) - - g = GEOSGeometry('POINT (-104.609 38.255)', 4326) - with self.assertRaises(GEOSException): - g.transform(2774, clone=True) - def test_extent(self): "Testing `extent` method." # The xmin, ymin, xmax, ymax of the MultiPoint should be returned. diff --git a/tests/gis_tests/gis_migrations/test_operations.py b/tests/gis_tests/gis_migrations/test_operations.py index a6f89e6106..aa6b13bdda 100644 --- a/tests/gis_tests/gis_migrations/test_operations.py +++ b/tests/gis_tests/gis_migrations/test_operations.py @@ -1,9 +1,6 @@ from __future__ import unicode_literals -from unittest import skipUnless - from django.contrib.gis.db.models import fields -from django.contrib.gis.gdal import HAS_GDAL from django.core.exceptions import ImproperlyConfigured from django.db import connection, migrations, models from django.db.migrations.migration import Migration @@ -117,7 +114,6 @@ class OperationTests(TransactionTestCase): self.assertSpatialIndexExists('gis_neighborhood', 'heatmap') @skipIfDBFeature('supports_raster') - @skipUnless(HAS_GDAL, 'A different error is raised if GDAL is not installed.') def test_create_raster_model_on_db_without_raster_support(self): """ Test creating a model with a raster field on a db without raster support. @@ -127,7 +123,6 @@ class OperationTests(TransactionTestCase): self.set_up_test_model(True) @skipIfDBFeature('supports_raster') - @skipUnless(HAS_GDAL, 'A different error is raised if GDAL is not installed.') def test_add_raster_field_on_db_without_raster_support(self): """ Test adding a raster field on a db without raster support. diff --git a/tests/gis_tests/inspectapp/tests.py b/tests/gis_tests/inspectapp/tests.py index 1e774218a2..532037e803 100644 --- a/tests/gis_tests/inspectapp/tests.py +++ b/tests/gis_tests/inspectapp/tests.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import os import re -from unittest import skipUnless from django.contrib.gis.gdal import HAS_GDAL from django.core.management import call_command @@ -20,9 +19,8 @@ if HAS_GDAL: from .models import AllOGRFields -@skipUnless(HAS_GDAL, "InspectDbTests needs GDAL support") +@skipUnlessDBFeature("gis_enabled") class InspectDbTests(TestCase): - @skipUnlessDBFeature("gis_enabled") def test_geom_columns(self): """ Test the geo-enabled inspectdb command. @@ -60,7 +58,7 @@ class InspectDbTests(TestCase): self.assertIn('poly = models.GeometryField(', output) -@skipUnless(HAS_GDAL, "OGRInspectTest needs GDAL support") +@skipUnlessDBFeature("gis_enabled") @modify_settings( INSTALLED_APPS={'append': 'django.contrib.gis'}, ) diff --git a/tests/gis_tests/layermap/tests.py b/tests/gis_tests/layermap/tests.py index 7dc8683595..c57ba47f32 100644 --- a/tests/gis_tests/layermap/tests.py +++ b/tests/gis_tests/layermap/tests.py @@ -5,7 +5,6 @@ import os import unittest from copy import copy from decimal import Decimal -from unittest import skipUnless from django.conf import settings from django.contrib.gis.gdal import HAS_GDAL @@ -39,7 +38,6 @@ NUMS = [1, 2, 1, 19, 1] # Number of polygons for each. STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] -@skipUnless(HAS_GDAL, "LayerMapTest needs GDAL support") @skipUnlessDBFeature("gis_enabled") class LayerMapTest(TestCase): @@ -337,7 +335,6 @@ class OtherRouter(object): return True -@skipUnless(HAS_GDAL, "LayerMapRouterTest needs GDAL support") @skipUnlessDBFeature("gis_enabled") @override_settings(DATABASE_ROUTERS=[OtherRouter()]) class LayerMapRouterTest(TestCase): diff --git a/tests/gis_tests/rasterapp/models.py b/tests/gis_tests/rasterapp/models.py index 81f039b8d6..6cd97e40eb 100644 --- a/tests/gis_tests/rasterapp/models.py +++ b/tests/gis_tests/rasterapp/models.py @@ -1,23 +1,24 @@ +from django.contrib.gis.gdal import HAS_GDAL + from ..models import models +if HAS_GDAL: + class RasterModel(models.Model): + rast = models.RasterField('A Verbose Raster Name', null=True, srid=4326, spatial_index=True, blank=True) + rastprojected = models.RasterField('A Projected Raster Table', srid=3086, null=True) + geom = models.PointField(null=True) -class RasterModel(models.Model): - rast = models.RasterField('A Verbose Raster Name', null=True, srid=4326, spatial_index=True, blank=True) - rastprojected = models.RasterField('A Projected Raster Table', srid=3086, null=True) - geom = models.PointField(null=True) + class Meta: + required_db_features = ['supports_raster'] - class Meta: - required_db_features = ['supports_raster'] + def __str__(self): + return str(self.id) - def __str__(self): - return str(self.id) + class RasterRelatedModel(models.Model): + rastermodel = models.ForeignKey(RasterModel, models.CASCADE) + class Meta: + required_db_features = ['supports_raster'] -class RasterRelatedModel(models.Model): - rastermodel = models.ForeignKey(RasterModel, models.CASCADE) - - class Meta: - required_db_features = ['supports_raster'] - - def __str__(self): - return str(self.id) + def __str__(self): + return str(self.id) diff --git a/tests/gis_tests/rasterapp/test_rasterfield.py b/tests/gis_tests/rasterapp/test_rasterfield.py index 0dbeebd9d8..f838f56bdc 100644 --- a/tests/gis_tests/rasterapp/test_rasterfield.py +++ b/tests/gis_tests/rasterapp/test_rasterfield.py @@ -7,18 +7,14 @@ from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.measure import D from django.contrib.gis.shortcuts import numpy -from django.core.exceptions import ImproperlyConfigured from django.db.models import Q -from django.test import ( - TestCase, TransactionTestCase, mock, skipUnlessDBFeature, -) +from django.test import TransactionTestCase, skipUnlessDBFeature from ..data.rasters.textrasters import JSON_RASTER -from ..models import models -from .models import RasterModel, RasterRelatedModel if HAS_GDAL: from django.contrib.gis.gdal import GDALRaster + from .models import RasterModel, RasterRelatedModel @skipUnlessDBFeature('supports_raster') @@ -330,12 +326,3 @@ class RasterFieldTest(TransactionTestCase): msg = "Couldn't create spatial object from lookup value '%s'." % obj with self.assertRaisesMessage(ValueError, msg): RasterModel.objects.filter(geom__intersects=obj) - - -@mock.patch('django.contrib.gis.db.models.fields.HAS_GDAL', False) -class RasterFieldWithoutGDALTest(TestCase): - - def test_raster_field_without_gdal_exception(self): - msg = 'RasterField requires GDAL.' - with self.assertRaisesMessage(ImproperlyConfigured, msg): - models.OriginalRasterField() diff --git a/tests/gis_tests/test_geoforms.py b/tests/gis_tests/test_geoforms.py index e07153df51..5425443a82 100644 --- a/tests/gis_tests/test_geoforms.py +++ b/tests/gis_tests/test_geoforms.py @@ -1,14 +1,10 @@ -from unittest import skipUnless - from django.contrib.gis import forms -from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import GEOSGeometry from django.forms import ValidationError from django.test import SimpleTestCase, override_settings, skipUnlessDBFeature from django.utils.html import escape -@skipUnless(HAS_GDAL, "GeometryFieldTest needs GDAL support") @skipUnlessDBFeature("gis_enabled") class GeometryFieldTest(SimpleTestCase): @@ -89,7 +85,6 @@ class GeometryFieldTest(SimpleTestCase): self.assertFalse(form.has_changed()) -@skipUnless(HAS_GDAL, "SpecializedFieldTest needs GDAL support") @skipUnlessDBFeature("gis_enabled") class SpecializedFieldTest(SimpleTestCase): def setUp(self): @@ -260,7 +255,6 @@ class SpecializedFieldTest(SimpleTestCase): self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid()) -@skipUnless(HAS_GDAL, "OSMWidgetTest needs GDAL support") @skipUnlessDBFeature("gis_enabled") class OSMWidgetTest(SimpleTestCase): def setUp(self): @@ -302,7 +296,6 @@ class OSMWidgetTest(SimpleTestCase): rendered) -@skipUnless(HAS_GDAL, "CustomGeometryWidgetTest needs GDAL support") @skipUnlessDBFeature("gis_enabled") class CustomGeometryWidgetTest(SimpleTestCase): diff --git a/tests/gis_tests/test_spatialrefsys.py b/tests/gis_tests/test_spatialrefsys.py index f576758016..652dab3130 100644 --- a/tests/gis_tests/test_spatialrefsys.py +++ b/tests/gis_tests/test_spatialrefsys.py @@ -1,8 +1,7 @@ import re import unittest -from django.contrib.gis.gdal import HAS_GDAL -from django.test import mock, skipUnlessDBFeature +from django.test import skipUnlessDBFeature from django.utils import six from .utils import SpatialRefSys, oracle, postgis, spatialite @@ -51,7 +50,6 @@ test_srs = ({ }) -@unittest.skipUnless(HAS_GDAL, "SpatialRefSysTest needs gdal support") @skipUnlessDBFeature("has_spatialrefsys_table") class SpatialRefSysTest(unittest.TestCase): @@ -61,13 +59,6 @@ class SpatialRefSysTest(unittest.TestCase): self.assertEqual(unit_name, 'degree') self.assertAlmostEqual(unit, 0.01745329251994328) - @mock.patch('django.contrib.gis.gdal.HAS_GDAL', False) - def test_get_units_without_gdal(self): - epsg_4326 = next(f for f in test_srs if f['srid'] == 4326) - unit, unit_name = SpatialRefSys().get_units(epsg_4326['wkt']) - self.assertEqual(unit_name, 'degree') - self.assertAlmostEqual(unit, 0.01745329251994328) - def test_retrieve(self): """ Test retrieval of SpatialRefSys model objects.