mirror of
https://github.com/django/django.git
synced 2025-03-31 19:46:42 +00:00
Fixed #34629 -- Added filtering support to GIS aggregates.
This commit is contained in:
parent
c1cff3c471
commit
1b754d638d
1
AUTHORS
1
AUTHORS
@ -756,6 +756,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
oggy <ognjen.maric@gmail.com>
|
oggy <ognjen.maric@gmail.com>
|
||||||
Oliver Beattie <oliver@obeattie.com>
|
Oliver Beattie <oliver@obeattie.com>
|
||||||
Oliver Rutherfurd <http://rutherfurd.net/>
|
Oliver Rutherfurd <http://rutherfurd.net/>
|
||||||
|
Olivier Le Thanh Duong <olivier@lethanh.be>
|
||||||
Olivier Sels <olivier.sels@gmail.com>
|
Olivier Sels <olivier.sels@gmail.com>
|
||||||
Olivier Tabone <olivier.tabone@ripplemotion.fr>
|
Olivier Tabone <olivier.tabone@ripplemotion.fr>
|
||||||
Orestis Markou <orestis@orestis.gr>
|
Orestis Markou <orestis@orestis.gr>
|
||||||
|
@ -53,8 +53,8 @@ class GeoAggregate(Aggregate):
|
|||||||
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
|
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
|
||||||
):
|
):
|
||||||
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
||||||
for expr in c.get_source_expressions():
|
for field in c.get_source_fields():
|
||||||
if not hasattr(expr.field, "geom_type"):
|
if not hasattr(field, "geom_type"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Geospatial aggregates only allowed on geometry fields."
|
"Geospatial aggregates only allowed on geometry fields."
|
||||||
)
|
)
|
||||||
|
@ -839,6 +839,8 @@ Oracle ``SDO_WITHIN_DISTANCE(poly, geom, 5)``
|
|||||||
SpatiaLite ``PtDistWithin(poly, geom, 5)``
|
SpatiaLite ``PtDistWithin(poly, geom, 5)``
|
||||||
========== ======================================
|
========== ======================================
|
||||||
|
|
||||||
|
.. _gis-aggregation-functions:
|
||||||
|
|
||||||
Aggregate Functions
|
Aggregate Functions
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
@ -868,7 +870,7 @@ Example:
|
|||||||
``Collect``
|
``Collect``
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
.. class:: Collect(geo_field)
|
.. class:: Collect(geo_field, filter=None)
|
||||||
|
|
||||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_Collect.html>`__,
|
*Availability*: `PostGIS <https://postgis.net/docs/ST_Collect.html>`__,
|
||||||
SpatiaLite
|
SpatiaLite
|
||||||
@ -879,10 +881,14 @@ aggregate, except it can be several orders of magnitude faster than performing
|
|||||||
a union because it rolls up geometries into a collection or multi object, not
|
a union because it rolls up geometries into a collection or multi object, not
|
||||||
caring about dissolving boundaries.
|
caring about dissolving boundaries.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using the ``filter`` argument was added.
|
||||||
|
|
||||||
``Extent``
|
``Extent``
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
.. class:: Extent(geo_field)
|
.. class:: Extent(geo_field, filter=None)
|
||||||
|
|
||||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_Extent.html>`__,
|
*Availability*: `PostGIS <https://postgis.net/docs/ST_Extent.html>`__,
|
||||||
Oracle, SpatiaLite
|
Oracle, SpatiaLite
|
||||||
@ -898,10 +904,14 @@ Example:
|
|||||||
>>> print(qs["poly__extent"])
|
>>> print(qs["poly__extent"])
|
||||||
(-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
|
(-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using the ``filter`` argument was added.
|
||||||
|
|
||||||
``Extent3D``
|
``Extent3D``
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
.. class:: Extent3D(geo_field)
|
.. class:: Extent3D(geo_field, filter=None)
|
||||||
|
|
||||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_3DExtent.html>`__
|
*Availability*: `PostGIS <https://postgis.net/docs/ST_3DExtent.html>`__
|
||||||
|
|
||||||
@ -917,10 +927,14 @@ Example:
|
|||||||
>>> print(qs["poly__extent3d"])
|
>>> print(qs["poly__extent3d"])
|
||||||
(-96.8016128540039, 29.7633724212646, 0, -95.3631439208984, 32.782058715820, 0)
|
(-96.8016128540039, 29.7633724212646, 0, -95.3631439208984, 32.782058715820, 0)
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using the ``filter`` argument was added.
|
||||||
|
|
||||||
``MakeLine``
|
``MakeLine``
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
.. class:: MakeLine(geo_field)
|
.. class:: MakeLine(geo_field, filter=None)
|
||||||
|
|
||||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_MakeLine.html>`__,
|
*Availability*: `PostGIS <https://postgis.net/docs/ST_MakeLine.html>`__,
|
||||||
SpatiaLite
|
SpatiaLite
|
||||||
@ -936,10 +950,14 @@ Example:
|
|||||||
>>> print(qs["poly__makeline"])
|
>>> print(qs["poly__makeline"])
|
||||||
LINESTRING (-95.3631510000000020 29.7633739999999989, -96.8016109999999941 32.7820570000000018)
|
LINESTRING (-95.3631510000000020 29.7633739999999989, -96.8016109999999941 32.7820570000000018)
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using the ``filter`` argument was added.
|
||||||
|
|
||||||
``Union``
|
``Union``
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
.. class:: Union(geo_field)
|
.. class:: Union(geo_field, filter=None)
|
||||||
|
|
||||||
*Availability*: `PostGIS <https://postgis.net/docs/ST_Union.html>`__,
|
*Availability*: `PostGIS <https://postgis.net/docs/ST_Union.html>`__,
|
||||||
Oracle, SpatiaLite
|
Oracle, SpatiaLite
|
||||||
@ -963,6 +981,10 @@ Example:
|
|||||||
... Union(poly)
|
... Union(poly)
|
||||||
... ) # A more sensible approach.
|
... ) # A more sensible approach.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using the ``filter`` argument was added.
|
||||||
|
|
||||||
.. rubric:: Footnotes
|
.. rubric:: Footnotes
|
||||||
.. [#fnde9im] *See* `OpenGIS Simple Feature Specification For SQL <https://portal.ogc.org/files/?artifact_id=829>`_, at Ch. 2.1.13.2, p. 2-13 (The Dimensionally Extended Nine-Intersection Model).
|
.. [#fnde9im] *See* `OpenGIS Simple Feature Specification For SQL <https://portal.ogc.org/files/?artifact_id=829>`_, at Ch. 2.1.13.2, p. 2-13 (The Dimensionally Extended Nine-Intersection Model).
|
||||||
.. [#fnsdorelate] *See* `SDO_RELATE documentation <https://docs.oracle.com/en/
|
.. [#fnsdorelate] *See* `SDO_RELATE documentation <https://docs.oracle.com/en/
|
||||||
|
@ -170,6 +170,9 @@ Minor features
|
|||||||
function returns a 2-dimensional point on the geometry that is closest to
|
function returns a 2-dimensional point on the geometry that is closest to
|
||||||
another geometry.
|
another geometry.
|
||||||
|
|
||||||
|
* :ref:`GIS aggregates <gis-aggregation-functions>` now support the ``filter``
|
||||||
|
argument.
|
||||||
|
|
||||||
:mod:`django.contrib.messages`
|
:mod:`django.contrib.messages`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.contrib.gis.db.models import Extent3D, Union
|
from django.contrib.gis.db.models import Extent3D, Q, Union
|
||||||
from django.contrib.gis.db.models.functions import (
|
from django.contrib.gis.db.models.functions import (
|
||||||
AsGeoJSON,
|
AsGeoJSON,
|
||||||
AsKML,
|
AsKML,
|
||||||
@ -244,6 +244,16 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase):
|
|||||||
City3D.objects.none().aggregate(Extent3D("point"))["point__extent3d"]
|
City3D.objects.none().aggregate(Extent3D("point"))["point__extent3d"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_3d_functions")
|
||||||
|
def test_extent3d_filter(self):
|
||||||
|
self._load_city_data()
|
||||||
|
extent3d = City3D.objects.aggregate(
|
||||||
|
ll_cities=Extent3D("point", filter=Q(name__contains="ll"))
|
||||||
|
)["ll_cities"]
|
||||||
|
ref_extent3d = (-96.801611, -41.315268, 14.0, 174.783117, 32.782057, 147.0)
|
||||||
|
for ref_val, ext_val in zip(ref_extent3d, extent3d):
|
||||||
|
self.assertAlmostEqual(ref_val, ext_val, 6)
|
||||||
|
|
||||||
|
|
||||||
@skipUnlessDBFeature("supports_3d_functions")
|
@skipUnlessDBFeature("supports_3d_functions")
|
||||||
class Geo3DFunctionsTests(FuncTestMixin, Geo3DLoadingHelper, TestCase):
|
class Geo3DFunctionsTests(FuncTestMixin, Geo3DLoadingHelper, TestCase):
|
||||||
|
@ -135,5 +135,53 @@
|
|||||||
"title": "Patry on Copyright",
|
"title": "Patry on Copyright",
|
||||||
"author": 2
|
"author": 2
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"model": "relatedapp.parcel",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Aurora Parcel Alpha",
|
||||||
|
"city": 1,
|
||||||
|
"center1": "POINT (1.7128 -2.0060)",
|
||||||
|
"center2": "POINT (3.7128 -5.0060)",
|
||||||
|
"border1": "POLYGON((0 0, 5 5, 12 12, 0 0))",
|
||||||
|
"border2": "POLYGON((0 0, 5 5, 8 8, 0 0))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "relatedapp.parcel",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Aurora Parcel Beta",
|
||||||
|
"city": 1,
|
||||||
|
"center1": "POINT (4.7128 5.0060)",
|
||||||
|
"center2": "POINT (12.75 10.05)",
|
||||||
|
"border1": "POLYGON((10 10, 15 15, 22 22, 10 10))",
|
||||||
|
"border2": "POLYGON((10 10, 15 15, 22 22, 10 10))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "relatedapp.parcel",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Aurora Parcel Ignore",
|
||||||
|
"city": 1,
|
||||||
|
"center1": "POINT (9.7128 12.0060)",
|
||||||
|
"center2": "POINT (1.7128 -2.0060)",
|
||||||
|
"border1": "POLYGON ((24 23, 25 25, 32 32, 24 23))",
|
||||||
|
"border2": "POLYGON ((24 23, 25 25, 32 32, 24 23))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "relatedapp.parcel",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "Roswell Parcel Ignore",
|
||||||
|
"city": 2,
|
||||||
|
"center1": "POINT (-9.7128 -12.0060)",
|
||||||
|
"center2": "POINT (-1.7128 2.0060)",
|
||||||
|
"border1": "POLYGON ((30 30, 35 35, 42 32, 30 30))",
|
||||||
|
"border2": "POLYGON ((30 30, 35 35, 42 32, 30 30))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.gis.db.models import Collect, Count, Extent, F, Union
|
from django.contrib.gis.db.models import Collect, Count, Extent, F, MakeLine, Q, Union
|
||||||
|
from django.contrib.gis.db.models.functions import Centroid
|
||||||
from django.contrib.gis.geos import GEOSGeometry, MultiPoint, Point
|
from django.contrib.gis.geos import GEOSGeometry, MultiPoint, Point
|
||||||
from django.db import NotSupportedError, connection
|
from django.db import NotSupportedError, connection
|
||||||
from django.test import TestCase, skipUnlessDBFeature
|
from django.test import TestCase, skipUnlessDBFeature
|
||||||
@ -304,6 +305,116 @@ class RelatedGeoModelTest(TestCase):
|
|||||||
self.assertEqual(4, len(coll))
|
self.assertEqual(4, len(coll))
|
||||||
self.assertTrue(ref_geom.equals(coll))
|
self.assertTrue(ref_geom.equals(coll))
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_collect_aggr")
|
||||||
|
def test_collect_filter(self):
|
||||||
|
qs = City.objects.annotate(
|
||||||
|
parcel_center=Collect(
|
||||||
|
"parcel__center1",
|
||||||
|
filter=~Q(parcel__name__icontains="ignore"),
|
||||||
|
),
|
||||||
|
parcel_center_nonexistent=Collect(
|
||||||
|
"parcel__center1",
|
||||||
|
filter=Q(parcel__name__icontains="nonexistent"),
|
||||||
|
),
|
||||||
|
parcel_center_single=Collect(
|
||||||
|
"parcel__center1",
|
||||||
|
filter=Q(parcel__name__contains="Alpha"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
city = qs.get(name="Aurora")
|
||||||
|
self.assertEqual(
|
||||||
|
city.parcel_center.wkt, "MULTIPOINT (1.7128 -2.006, 4.7128 5.006)"
|
||||||
|
)
|
||||||
|
self.assertIsNone(city.parcel_center_nonexistent)
|
||||||
|
self.assertIn(
|
||||||
|
city.parcel_center_single.wkt,
|
||||||
|
[
|
||||||
|
"MULTIPOINT (1.7128 -2.006)",
|
||||||
|
"POINT (1.7128 -2.006)", # SpatiaLite collapse to POINT.
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("has_Centroid_function", "supports_collect_aggr")
|
||||||
|
def test_centroid_collect_filter(self):
|
||||||
|
qs = City.objects.annotate(
|
||||||
|
parcel_centroid=Centroid(
|
||||||
|
Collect(
|
||||||
|
"parcel__center1",
|
||||||
|
filter=~Q(parcel__name__icontains="ignore"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
city = qs.get(name="Aurora")
|
||||||
|
self.assertEqual(city.parcel_centroid.wkt, "POINT (3.2128 1.5)")
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_make_line_aggr")
|
||||||
|
def test_make_line_filter(self):
|
||||||
|
qs = City.objects.annotate(
|
||||||
|
parcel_line=MakeLine(
|
||||||
|
"parcel__center1",
|
||||||
|
filter=~Q(parcel__name__icontains="ignore"),
|
||||||
|
),
|
||||||
|
parcel_line_nonexistent=MakeLine(
|
||||||
|
"parcel__center1",
|
||||||
|
filter=Q(parcel__name__icontains="nonexistent"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
city = qs.get(name="Aurora")
|
||||||
|
self.assertIn(
|
||||||
|
city.parcel_line.wkt,
|
||||||
|
# The default ordering is flaky, so check both.
|
||||||
|
[
|
||||||
|
"LINESTRING (1.7128 -2.006, 4.7128 5.006)",
|
||||||
|
"LINESTRING (4.7128 5.006, 1.7128 -2.006)",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertIsNone(city.parcel_line_nonexistent)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_extent_aggr")
|
||||||
|
def test_extent_filter(self):
|
||||||
|
qs = City.objects.annotate(
|
||||||
|
parcel_border=Extent(
|
||||||
|
"parcel__border1",
|
||||||
|
filter=~Q(parcel__name__icontains="ignore"),
|
||||||
|
),
|
||||||
|
parcel_border_nonexistent=Extent(
|
||||||
|
"parcel__border1",
|
||||||
|
filter=Q(parcel__name__icontains="nonexistent"),
|
||||||
|
),
|
||||||
|
parcel_border_no_filter=Extent("parcel__border1"),
|
||||||
|
)
|
||||||
|
city = qs.get(name="Aurora")
|
||||||
|
self.assertEqual(city.parcel_border, (0.0, 0.0, 22.0, 22.0))
|
||||||
|
self.assertIsNone(city.parcel_border_nonexistent)
|
||||||
|
self.assertEqual(city.parcel_border_no_filter, (0.0, 0.0, 32.0, 32.0))
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_union_aggr")
|
||||||
|
def test_union_filter(self):
|
||||||
|
qs = City.objects.annotate(
|
||||||
|
parcel_point_union=Union(
|
||||||
|
"parcel__center2",
|
||||||
|
filter=~Q(parcel__name__icontains="ignore"),
|
||||||
|
),
|
||||||
|
parcel_point_nonexistent=Union(
|
||||||
|
"parcel__center2",
|
||||||
|
filter=Q(parcel__name__icontains="nonexistent"),
|
||||||
|
),
|
||||||
|
parcel_point_union_single=Union(
|
||||||
|
"parcel__center2",
|
||||||
|
filter=Q(parcel__name__contains="Alpha"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
city = qs.get(name="Aurora")
|
||||||
|
self.assertIn(
|
||||||
|
city.parcel_point_union.wkt,
|
||||||
|
[
|
||||||
|
"MULTIPOINT (12.75 10.05, 3.7128 -5.006)",
|
||||||
|
"MULTIPOINT (3.7128 -5.006, 12.75 10.05)",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertIsNone(city.parcel_point_nonexistent)
|
||||||
|
self.assertEqual(city.parcel_point_union_single.wkt, "POINT (3.7128 -5.006)")
|
||||||
|
|
||||||
def test15_invalid_select_related(self):
|
def test15_invalid_select_related(self):
|
||||||
"""
|
"""
|
||||||
select_related on the related name manager of a unique FK.
|
select_related on the related name manager of a unique FK.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user