Skip to content

Commit

Permalink
Fixed #34629 -- Added filtering support to GIS aggregates.
Browse files Browse the repository at this point in the history
  • Loading branch information
olethanh authored and felixxm committed Jun 16, 2023
1 parent c1cff3c commit 1b754d6
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 11 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -756,6 +756,7 @@ answer newbie questions, and generally made Django that much better:
oggy <ognjen.maric@gmail.com>
Oliver Beattie <oliver@obeattie.com>
Oliver Rutherfurd <http://rutherfurd.net/>
Olivier Le Thanh Duong <olivier@lethanh.be>
Olivier Sels <olivier.sels@gmail.com>
Olivier Tabone <olivier.tabone@ripplemotion.fr>
Orestis Markou <orestis@orestis.gr>
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/gis/db/models/aggregates.py
Expand Up @@ -53,8 +53,8 @@ def resolve_expression(
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
):
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
for expr in c.get_source_expressions():
if not hasattr(expr.field, "geom_type"):
for field in c.get_source_fields():
if not hasattr(field, "geom_type"):
raise ValueError(
"Geospatial aggregates only allowed on geometry fields."
)
Expand Down
32 changes: 27 additions & 5 deletions docs/ref/contrib/gis/geoquerysets.txt
Expand Up @@ -839,6 +839,8 @@ Oracle ``SDO_WITHIN_DISTANCE(poly, geom, 5)``
SpatiaLite ``PtDistWithin(poly, geom, 5)``
========== ======================================

.. _gis-aggregation-functions:

Aggregate Functions
-------------------

Expand Down Expand Up @@ -868,7 +870,7 @@ Example:
``Collect``
~~~~~~~~~~~

.. class:: Collect(geo_field)
.. class:: Collect(geo_field, filter=None)

*Availability*: `PostGIS <https://postgis.net/docs/ST_Collect.html>`__,
SpatiaLite
Expand All @@ -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
caring about dissolving boundaries.

.. versionchanged:: 5.0

Support for using the ``filter`` argument was added.

``Extent``
~~~~~~~~~~

.. class:: Extent(geo_field)
.. class:: Extent(geo_field, filter=None)

*Availability*: `PostGIS <https://postgis.net/docs/ST_Extent.html>`__,
Oracle, SpatiaLite
Expand All @@ -898,10 +904,14 @@ Example:
>>> print(qs["poly__extent"])
(-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)

.. versionchanged:: 5.0

Support for using the ``filter`` argument was added.

``Extent3D``
~~~~~~~~~~~~

.. class:: Extent3D(geo_field)
.. class:: Extent3D(geo_field, filter=None)

*Availability*: `PostGIS <https://postgis.net/docs/ST_3DExtent.html>`__

Expand All @@ -917,10 +927,14 @@ Example:
>>> print(qs["poly__extent3d"])
(-96.8016128540039, 29.7633724212646, 0, -95.3631439208984, 32.782058715820, 0)

.. versionchanged:: 5.0

Support for using the ``filter`` argument was added.

``MakeLine``
~~~~~~~~~~~~

.. class:: MakeLine(geo_field)
.. class:: MakeLine(geo_field, filter=None)

*Availability*: `PostGIS <https://postgis.net/docs/ST_MakeLine.html>`__,
SpatiaLite
Expand All @@ -936,10 +950,14 @@ Example:
>>> print(qs["poly__makeline"])
LINESTRING (-95.3631510000000020 29.7633739999999989, -96.8016109999999941 32.7820570000000018)

.. versionchanged:: 5.0

Support for using the ``filter`` argument was added.

``Union``
~~~~~~~~~

.. class:: Union(geo_field)
.. class:: Union(geo_field, filter=None)

*Availability*: `PostGIS <https://postgis.net/docs/ST_Union.html>`__,
Oracle, SpatiaLite
Expand All @@ -963,6 +981,10 @@ Example:
... Union(poly)
... ) # A more sensible approach.

.. versionchanged:: 5.0

Support for using the ``filter`` argument was added.

.. 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).
.. [#fnsdorelate] *See* `SDO_RELATE documentation <https://docs.oracle.com/en/
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.0.txt
Expand Up @@ -170,6 +170,9 @@ Minor features
function returns a 2-dimensional point on the geometry that is closest to
another geometry.

* :ref:`GIS aggregates <gis-aggregation-functions>` now support the ``filter``
argument.

:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
12 changes: 11 additions & 1 deletion tests/gis_tests/geo3d/tests.py
@@ -1,7 +1,7 @@
import os
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 (
AsGeoJSON,
AsKML,
Expand Down Expand Up @@ -244,6 +244,16 @@ def check_extent3d(extent3d, tol=6):
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")
class Geo3DFunctionsTests(FuncTestMixin, Geo3DLoadingHelper, TestCase):
Expand Down
52 changes: 50 additions & 2 deletions tests/gis_tests/relatedapp/fixtures/initial.json
Expand Up @@ -135,5 +135,53 @@
"title": "Patry on Copyright",
"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))"
}
}
]
113 changes: 112 additions & 1 deletion tests/gis_tests/relatedapp/tests.py
@@ -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.db import NotSupportedError, connection
from django.test import TestCase, skipUnlessDBFeature
Expand Down Expand Up @@ -304,6 +305,116 @@ def test_collect(self):
self.assertEqual(4, len(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):
"""
select_related on the related name manager of a unique FK.
Expand Down

0 comments on commit 1b754d6

Please sign in to comment.