mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +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>
|
||||
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>
|
||||
|
@ -53,8 +53,8 @@ class GeoAggregate(Aggregate):
|
||||
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."
|
||||
)
|
||||
|
@ -839,6 +839,8 @@ Oracle ``SDO_WITHIN_DISTANCE(poly, geom, 5)``
|
||||
SpatiaLite ``PtDistWithin(poly, geom, 5)``
|
||||
========== ======================================
|
||||
|
||||
.. _gis-aggregation-functions:
|
||||
|
||||
Aggregate Functions
|
||||
-------------------
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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>`__
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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/
|
||||
|
@ -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`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -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,
|
||||
@ -244,6 +244,16 @@ class Geo3DTest(Geo3DLoadingHelper, TestCase):
|
||||
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):
|
||||
|
@ -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))"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
@ -304,6 +305,116 @@ class RelatedGeoModelTest(TestCase):
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user