From b14bd604043ba1d89493558f4f6cb7a9bcf17923 Mon Sep 17 00:00:00 2001
From: Claude Paroz <claude@2xlibre.net>
Date: Mon, 27 May 2013 11:54:14 +0200
Subject: [PATCH] Reimplemented PostGIS spatial_version with cached_property

---
 .../gis/db/backends/postgis/operations.py     | 81 ++++++++-----------
 1 file changed, 35 insertions(+), 46 deletions(-)

diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index 734f39c752e..84dbda32396 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -11,6 +11,10 @@ from django.core.exceptions import ImproperlyConfigured
 from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
 from django.db.utils import DatabaseError
 from django.utils import six
+from django.utils.functional import cached_property
+
+from .models import GeometryColumns, SpatialRefSys
+
 
 #### Classes used in constructing PostGIS spatial SQL ####
 class PostGISOperator(SpatialOperation):
@@ -62,6 +66,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
     compiler_module = 'django.contrib.gis.db.models.sql.compiler'
     name = 'postgis'
     postgis = True
+    geom_func_prefix = 'ST_'
     version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
     valid_aggregates = dict([(k, None) for k in
                              ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
@@ -72,45 +77,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
     def __init__(self, connection):
         super(PostGISOperations, self).__init__(connection)
 
-        # Trying to get the PostGIS version because the function
-        # signatures will depend on the version used.  The cost
-        # here is a database query to determine the version, which
-        # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
-        # comprising user-supplied values for the major, minor, and
-        # subminor revision of PostGIS.
-        try:
-            if hasattr(settings, 'POSTGIS_VERSION'):
-                vtup = settings.POSTGIS_VERSION
-                if len(vtup) == 3:
-                    # The user-supplied PostGIS version.
-                    version = vtup
-                else:
-                    # This was the old documented way, but it's stupid to
-                    # include the string.
-                    version = vtup[1:4]
-            else:
-                vtup = self.postgis_version_tuple()
-                version = vtup[1:]
-
-            # Getting the prefix -- even though we don't officially support
-            # PostGIS 1.2 anymore, keeping it anyway in case a prefix change
-            # for something else is necessary.
-            if version >= (1, 2, 2):
-                prefix = 'ST_'
-            else:
-                prefix = ''
-
-            self.geom_func_prefix = prefix
-            self.spatial_version = version
-        except DatabaseError:
-            raise ImproperlyConfigured(
-                'Cannot determine PostGIS version for database "%s". '
-                'GeoDjango requires at least PostGIS version 1.3. '
-                'Was the database created from a spatial database '
-                'template?' % self.connection.settings_dict['NAME']
-                )
-        # TODO: Raise helpful exceptions as they become known.
-
+        prefix = self.geom_func_prefix
         # PostGIS-specific operators. The commented descriptions of these
         # operators come from Section 7.6 of the PostGIS 1.4 documentation.
         self.geometry_operators = {
@@ -188,13 +155,13 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
         self.geometry_functions.update(self.distance_functions)
 
         # Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
-        if version < (1, 3, 4):
+        if self.spatial_version < (1, 3, 4):
             GEOJSON = False
         else:
             GEOJSON = prefix + 'AsGeoJson'
 
         # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
-        if version >= (1, 4, 0):
+        if self.spatial_version >= (1, 4, 0):
             GEOHASH = 'ST_GeoHash'
             BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
             self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
@@ -202,7 +169,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
             GEOHASH, BOUNDINGCIRCLE = False, False
 
         # Geography type support added in 1.5.
-        if version >= (1, 5, 0):
+        if self.spatial_version >= (1, 5, 0):
             self.geography = True
             # Only a subset of the operators and functions are available
             # for the geography type.
@@ -217,7 +184,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
             }
 
         # Native geometry type support added in PostGIS 2.0.
-        if version >= (2, 0, 0):
+        if self.spatial_version >= (2, 0, 0):
             self.geometry = True
 
         # Creating a dictionary lookup of all GIS terms for PostGIS.
@@ -260,7 +227,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
         self.union = prefix + 'Union'
         self.unionagg = prefix + 'Union'
 
-        if version >= (2, 0, 0):
+        if self.spatial_version >= (2, 0, 0):
             self.extent3d = prefix + '3DExtent'
             self.length3d = prefix + '3DLength'
             self.perimeter3d = prefix + '3DPerimeter'
@@ -269,6 +236,30 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
             self.length3d = prefix + 'Length3D'
             self.perimeter3d = prefix + 'Perimeter3D'
 
+    @cached_property
+    def spatial_version(self):
+        """Determine the version of the PostGIS library."""
+        # Trying to get the PostGIS version because the function
+        # signatures will depend on the version used.  The cost
+        # here is a database query to determine the version, which
+        # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
+        # comprising user-supplied values for the major, minor, and
+        # subminor revision of PostGIS.
+        if hasattr(settings, 'POSTGIS_VERSION'):
+            version = settings.POSTGIS_VERSION
+        else:
+            try:
+                vtup = self.postgis_version_tuple()
+            except DatabaseError:
+                raise ImproperlyConfigured(
+                    'Cannot determine PostGIS version for database "%s". '
+                    'GeoDjango requires at least PostGIS version 1.3. '
+                    'Was the database created from a spatial database '
+                    'template?' % self.connection.settings_dict['NAME']
+                    )
+            version = vtup[1:]
+        return version
+
     def check_aggregate_support(self, aggregate):
         """
         Checks if the given aggregate name is supported (that is, if it's
@@ -572,9 +563,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
 
     # Routines for getting the OGC-compliant models.
     def geometry_columns(self):
-        from django.contrib.gis.db.backends.postgis.models import GeometryColumns
         return GeometryColumns
 
     def spatial_ref_sys(self):
-        from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
         return SpatialRefSys