diff --git a/django/contrib/gis/db/backends/base/features.py b/django/contrib/gis/db/backends/base/features.py index 4b4ffcc49f..7498fb2514 100644 --- a/django/contrib/gis/db/backends/base/features.py +++ b/django/contrib/gis/db/backends/base/features.py @@ -68,7 +68,7 @@ class BaseSpatialFeatures: @property def supports_isvalid_lookup(self): - return 'isvalid' in self.connection.ops.gis_operators + return self.has_IsValid_function # Is the aggregate supported by the database? @property diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index e030af1da3..62506e4227 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -51,10 +51,6 @@ class SDORelate(SpatialOperator): return super().as_sql(connection, lookup, template_params, sql_params) -class SDOIsValid(SpatialOperator): - sql_template = "%%(func)s(%%(lhs)s, %s) = 'TRUE'" % DEFAULT_TOLERANCE - - class OracleOperations(BaseSpatialOperations, DatabaseOperations): name = 'oracle' @@ -100,7 +96,6 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): 'covers': SDOOperator(func='SDO_COVERS'), 'disjoint': SDODisjoint(), 'intersects': SDOOperator(func='SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()? - 'isvalid': SDOIsValid(func='SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT'), 'equals': SDOOperator(func='SDO_EQUAL'), 'exact': SDOOperator(func='SDO_EQUAL'), 'overlaps': SDOOperator(func='SDO_OVERLAPS'), diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index d002c5d18c..62f569c754 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -134,7 +134,6 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): 'disjoint': PostGISOperator(func='ST_Disjoint', raster=BILATERAL), 'equals': PostGISOperator(func='ST_Equals'), 'intersects': PostGISOperator(func='ST_Intersects', geography=True, raster=BILATERAL), - 'isvalid': PostGISOperator(func='ST_IsValid'), 'overlaps': PostGISOperator(func='ST_Overlaps', raster=BILATERAL), 'relate': PostGISOperator(func='ST_Relate'), 'touches': PostGISOperator(func='ST_Touches', raster=BILATERAL), diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index d2fc68b4c0..46cac8c0b3 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -48,8 +48,6 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): select = 'AsText(%s)' gis_operators = { - # Unary predicates - 'isvalid': SpatialOperator(func='IsValid'), # Binary predicates 'equals': SpatialOperator(func='Equals'), 'disjoint': SpatialOperator(func='Disjoint'), diff --git a/django/contrib/gis/db/models/__init__.py b/django/contrib/gis/db/models/__init__.py index 31ca0001df..2e472557d8 100644 --- a/django/contrib/gis/db/models/__init__.py +++ b/django/contrib/gis/db/models/__init__.py @@ -1,5 +1,6 @@ from django.db.models import * # NOQA isort:skip from django.db.models import __all__ as models_all # isort:skip +import django.contrib.gis.db.models.functions # NOQA import django.contrib.gis.db.models.lookups # NOQA from django.contrib.gis.db.models.aggregates import * # NOQA from django.contrib.gis.db.models.aggregates import __all__ as aggregates_all diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index b688299cb5..fdcb562162 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -1,19 +1,23 @@ from decimal import Decimal -from django.contrib.gis.db.models.fields import GeometryField, RasterField +from django.contrib.gis.db.models.fields import ( + BaseSpatialField, GeometryField, RasterField, +) from django.contrib.gis.db.models.sql import AreaField from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import ( Area as AreaMeasure, Distance as DistanceMeasure, ) from django.core.exceptions import FieldError -from django.db.models import BooleanField, FloatField, IntegerField, TextField +from django.db.models import ( + BooleanField, FloatField, IntegerField, TextField, Transform, +) from django.db.models.expressions import Func, Value NUMERIC_TYPES = (int, float, Decimal) -class GeoFunc(Func): +class GeoFuncMixin: function = None output_field_class = None geom_param_pos = 0 @@ -70,6 +74,10 @@ class GeoFunc(Func): return value +class GeoFunc(GeoFuncMixin, Func): + pass + + class GeomValue(Value): geography = False @@ -319,8 +327,10 @@ class Intersection(OracleToleranceMixin, GeoFuncWithGeoParam): arity = 2 -class IsValid(OracleToleranceMixin, GeoFunc): - output_field_class = BooleanField +@BaseSpatialField.register_lookup +class IsValid(OracleToleranceMixin, GeoFuncMixin, Transform): + lookup_name = 'isvalid' + output_field = BooleanField() def as_oracle(self, compiler, connection, **extra_context): sql, params = super().as_oracle(compiler, connection, **extra_context) diff --git a/django/contrib/gis/db/models/lookups.py b/django/contrib/gis/db/models/lookups.py index c0d1934965..2024df45b0 100644 --- a/django/contrib/gis/db/models/lookups.py +++ b/django/contrib/gis/db/models/lookups.py @@ -261,22 +261,6 @@ class IntersectsLookup(GISLookup): lookup_name = 'intersects' -@BaseSpatialField.register_lookup -class IsValidLookup(GISLookup): - lookup_name = 'isvalid' - sql_template = '%(func)s(%(lhs)s)' - - def as_sql(self, compiler, connection): - if self.lhs.field.geom_type == 'RASTER': - raise ValueError('The isvalid lookup is only available on geometry fields.') - gis_op = connection.ops.gis_operators[self.lookup_name] - sql, params = self.process_lhs(compiler, connection) - sql, params = gis_op.as_sql(connection, self, {'func': gis_op.func, 'lhs': sql}, params) - if not self.rhs: - sql = 'NOT ' + sql - return sql, params - - @BaseSpatialField.register_lookup class OverlapsLookup(GISLookup): lookup_name = 'overlaps' diff --git a/tests/gis_tests/rasterapp/test_rasterfield.py b/tests/gis_tests/rasterapp/test_rasterfield.py index 975b68cab0..1e4ffbfbd7 100644 --- a/tests/gis_tests/rasterapp/test_rasterfield.py +++ b/tests/gis_tests/rasterapp/test_rasterfield.py @@ -271,8 +271,8 @@ class RasterFieldTest(TransactionTestCase): def test_isvalid_lookup_with_raster_error(self): qs = RasterModel.objects.filter(rast__isvalid=True) - msg = 'The isvalid lookup is only available on geometry fields.' - with self.assertRaisesMessage(ValueError, msg): + msg = 'Geometry functions not supported for raster fields.' + with self.assertRaisesMessage(TypeError, msg): qs.count() def test_result_of_gis_lookup_with_rasters(self):