From cd5352777649a1c8ab3bffe3a17786569dd11dd7 Mon Sep 17 00:00:00 2001 From: Joe Hermann Date: Tue, 6 Dec 2016 11:49:12 -0500 Subject: [PATCH 1/2] Add support for InBBox filtering on multiple attributes --- rest_framework_gis/filters.py | 47 ++++++++++++------- .../test_filters.py | 31 ++++++++---- tests/django_restframework_gis_tests/urls.py | 8 ++++ tests/django_restframework_gis_tests/views.py | 25 +++++++++- 4 files changed, 82 insertions(+), 29 deletions(-) diff --git a/rest_framework_gis/filters.py b/rest_framework_gis/filters.py index a20a3636..c2f5e8b3 100644 --- a/rest_framework_gis/filters.py +++ b/rest_framework_gis/filters.py @@ -40,34 +40,45 @@ class InBBoxFilter(BaseFilterBackend): bbox_param = 'in_bbox' # The URL query parameter which contains the bbox. - def get_filter_bbox(self, request): - bbox_string = request.query_params.get(self.bbox_param, None) + def get_filter_bbox(self, request, query_param): + bbox_string = request.query_params.get(query_param, None) if not bbox_string: return None try: p1x, p1y, p2x, p2y = (float(n) for n in bbox_string.split(',')) except ValueError: - raise ParseError('Invalid bbox string supplied for parameter {0}'.format(self.bbox_param)) + raise ParseError('Invalid bbox string supplied for parameter {0}'.format(query_param)) x = Polygon.from_bbox((p1x, p1y, p2x, p2y)) return x def filter_queryset(self, request, queryset, view): - filter_field = getattr(view, 'bbox_filter_field', None) - include_overlapping = getattr(view, 'bbox_filter_include_overlapping', False) - if include_overlapping: - geoDjango_filter = 'bboverlaps' + query_filter = {} + self.bbox_params = getattr(view,'bbox_params', {self.bbox_param: {'filter_field': getattr(view,'bbox_filter_field', None), 'include_overlapping': getattr(view,'bbox_filter_include_overlapping', False)}}) + for query_param, filter_field_attributes in self.bbox_params.iteritems(): + filter_field = filter_field_attributes['filter_field'] + include_overlapping = filter_field_attributes['include_overlapping'] + if include_overlapping: + geoDjango_filter = 'bboverlaps' + else: + geoDjango_filter = 'contained' + + if not filter_field: + continue + + bbox = self.get_filter_bbox(request, query_param) + if not bbox: + continue + # build the dict then return queryset filter that filters multiple attributes + # backwards compatibile because get_filter_bbox is private and untested method + query_filter['%s__%s' % (filter_field, geoDjango_filter)]= bbox + if query_filter == {}: + return queryset else: - geoDjango_filter = 'contained' + return queryset.filter(Q(**query_filter)) - if not filter_field: - return queryset - bbox = self.get_filter_bbox(request) - if not bbox: - return queryset - return queryset.filter(Q(**{'%s__%s' % (filter_field, geoDjango_filter): bbox})) # backward compatibility InBBOXFilter = InBBoxFilter @@ -94,17 +105,17 @@ def __new__(cls, *args, **kwargs): class TMSTileFilter(InBBoxFilter): - tile_param = 'tile' # The URL query paramater which contains the tile address + bbox_param = 'tile' # The URL query paramater which contains the tile address - def get_filter_bbox(self, request): - tile_string = request.query_params.get(self.tile_param, None) + def get_filter_bbox(self, request, query_param): + tile_string = request.query_params.get(self.bbox_param, None) if not tile_string: return None try: z, x, y = (int(n) for n in tile_string.split('/')) except ValueError: - raise ParseError('Invalid tile string supplied for parameter {0}'.format(self.tile_param)) + raise ParseError('Invalid tile string supplied for parameter {0}'.format(self.bbox_param)) bbox = Polygon.from_bbox(tile_edges(x, y, z)) return bbox diff --git a/tests/django_restframework_gis_tests/test_filters.py b/tests/django_restframework_gis_tests/test_filters.py index 4e33ea12..3fa155e4 100644 --- a/tests/django_restframework_gis_tests/test_filters.py +++ b/tests/django_restframework_gis_tests/test_filters.py @@ -19,7 +19,9 @@ class TestRestFrameworkGisFilters(TestCase): """ def setUp(self): self.location_contained_in_bbox_list_url = reverse('api_geojson_location_list_contained_in_bbox_filter') + self.location_contained_in_bbox_list_url_legacy = reverse('api_geojson_location_list_contained_in_bbox_filter_legacy') self.location_overlaps_bbox_list_url = reverse('api_geojson_location_list_overlaps_bbox_filter') + self.location_overlaps_bbox_list_url_legacy = reverse('api_geojson_location_list_overlaps_bbox_filter_legacy') self.location_contained_in_tile_list_url = reverse('api_geojson_location_list_contained_in_tile_filter') self.location_overlaps_tile_list_url = reverse('api_geojson_location_list_overlaps_tile_filter') self.location_within_distance_of_point_list_url = reverse('api_geojson_location_list_within_distance_of_point_filter') @@ -65,16 +67,18 @@ def test_inBBOXFilter_filtering(self): nonIntersecting.save() # Make sure we only get back the ones strictly contained in the bounding box - response = self.client.get(self.location_contained_in_bbox_list_url + url_params) - self.assertEqual(len(response.data['features']), 2) - for result in response.data['features']: - self.assertEqual(result['properties']['name'] in ('isContained', 'isEqualToBounds'), True) + for url in [self.location_contained_in_bbox_list_url, self.location_contained_in_bbox_list_url_legacy]: + response = self.client.get(url + url_params) + self.assertEqual(len(response.data['features']), 2) + for result in response.data['features']: + self.assertEqual(result['properties']['name'] in ('isContained', 'isEqualToBounds'), True) # Make sure we get overlapping results for the view which allows bounding box overlaps. - response = self.client.get(self.location_overlaps_bbox_list_url + url_params) - self.assertEqual(len(response.data['features']), 3) - for result in response.data['features']: - self.assertEqual(result['properties']['name'] in ('isContained', 'isEqualToBounds', 'overlaps'), True) + for url in [self.location_overlaps_bbox_list_url, self.location_overlaps_bbox_list_url_legacy]: + response = self.client.get(url + url_params) + self.assertEqual(len(response.data['features']), 3) + for result in response.data['features']: + self.assertEqual(result['properties']['name'] in ('isContained', 'isEqualToBounds', 'overlaps'), True) @skipIf(has_spatialite, 'Skipped test for spatialite backend: not accurate enough') def test_TileFilter_filtering(self): @@ -399,7 +403,7 @@ def test_inBBOXFilter_ValueError(self): self.assertEqual(response.data['detail'], 'Invalid bbox string supplied for parameter in_bbox') def test_inBBOXFilter_filter_field_none(self): - from .views import GeojsonLocationContainedInBBoxList as view + from .views import GeojsonLocationContainedInBBoxListLegacy as view original_value = view.bbox_filter_field view.bbox_filter_field = None url_params = '?in_bbox=0,0,0,0&format=json' @@ -407,6 +411,15 @@ def test_inBBOXFilter_filter_field_none(self): self.assertDictEqual(response.data, {'type':'FeatureCollection','features':[]}) view.bbox_filter_field = original_value + def test_inBBOXFilter_filter_fields_empty(self): + from .views import GeojsonLocationContainedInBBoxList as view + original_value = view.bbox_params + view.bbox_params = {} + url_params = '?in_bbox=0,0,0,0&format=json' + response = self.client.get(self.location_contained_in_bbox_list_url + url_params) + self.assertDictEqual(response.data, {'type':'FeatureCollection','features':[]}) + view.bbox_params = original_value + def test_TileFilter_filtering_none(self): url_params = '?tile=&format=json' response = self.client.get(self.location_contained_in_tile_list_url + url_params) diff --git a/tests/django_restframework_gis_tests/urls.py b/tests/django_restframework_gis_tests/urls.py index f8ea0820..1bba6f3e 100644 --- a/tests/django_restframework_gis_tests/urls.py +++ b/tests/django_restframework_gis_tests/urls.py @@ -69,6 +69,14 @@ r'^filters/overlaps_bbox$', views.geojson_location_overlaps_bbox_list, name='api_geojson_location_list_overlaps_bbox_filter'), + url( + r'^filters/contained_in_bbox_legacy$', + views.geojson_location_contained_in_bbox_list_legacy, + name='api_geojson_location_list_contained_in_bbox_filter_legacy'), + url( + r'^filters/overlaps_bbox_legacy$', + views.geojson_location_overlaps_bbox_list_legacy, + name='api_geojson_location_list_overlaps_bbox_filter_legacy'), url( r'^filters/contained_in_geometry$', views.geojson_contained_in_geometry, diff --git a/tests/django_restframework_gis_tests/views.py b/tests/django_restframework_gis_tests/views.py index 4f1e0126..ecfe33a4 100644 --- a/tests/django_restframework_gis_tests/views.py +++ b/tests/django_restframework_gis_tests/views.py @@ -33,18 +33,39 @@ class GeojsonLocationList(generics.ListCreateAPIView): geojson_location_list = GeojsonLocationList.as_view() -class GeojsonLocationContainedInBBoxList(generics.ListAPIView): +class GeojsonLocationContainedInBBoxListLegacy(generics.ListAPIView): + # Legacy test bbox view for one attribute bbox_filters model = Location serializer_class = LocationGeoFeatureSerializer queryset = Location.objects.all() bbox_filter_field = 'geometry' filter_backends = (InBBoxFilter,) +geojson_location_contained_in_bbox_list_legacy = GeojsonLocationContainedInBBoxListLegacy.as_view() + + +class GeojsonLocationOverlapsBBoxListLegacy(GeojsonLocationContainedInBBoxListLegacy): + bbox_filter_include_overlapping = True + +geojson_location_overlaps_bbox_list_legacy = GeojsonLocationOverlapsBBoxListLegacy.as_view() + +class GeojsonLocationContainedInBBoxList(generics.ListAPIView): + # Test view for bbox_params hash + model = Location + serializer_class = LocationGeoFeatureSerializer + queryset = Location.objects.all() + bbox_params = { + 'in_bbox': {'filter_field': 'geometry', 'include_overlapping': False}, + } + filter_backends = (InBBoxFilter,) + geojson_location_contained_in_bbox_list = GeojsonLocationContainedInBBoxList.as_view() class GeojsonLocationOverlapsBBoxList(GeojsonLocationContainedInBBoxList): - bbox_filter_include_overlapping = True + bbox_params = { + 'in_bbox': {'filter_field': 'geometry', 'include_overlapping': True}, + } geojson_location_overlaps_bbox_list = GeojsonLocationOverlapsBBoxList.as_view() From 23737d8b6eec2308d3724f52d3bf3e4a4dc47124 Mon Sep 17 00:00:00 2001 From: Joe Hermann Date: Wed, 7 Dec 2016 14:10:35 -0500 Subject: [PATCH 2/2] Fix changes for compatibility --- rest_framework_gis/filters.py | 40 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/rest_framework_gis/filters.py b/rest_framework_gis/filters.py index c2f5e8b3..14782b06 100644 --- a/rest_framework_gis/filters.py +++ b/rest_framework_gis/filters.py @@ -5,6 +5,7 @@ from django.contrib.gis.db import models from django.contrib.gis.geos import Polygon, Point from django.contrib.gis import forms +from django.utils import six from rest_framework.filters import BaseFilterBackend from rest_framework.exceptions import ParseError @@ -39,8 +40,9 @@ class InBBoxFilter(BaseFilterBackend): bbox_param = 'in_bbox' # The URL query parameter which contains the bbox. + tile_param = None # Compatibility for tile class - def get_filter_bbox(self, request, query_param): + def get_filter_bbox(self, request, query_param=None): bbox_string = request.query_params.get(query_param, None) if not bbox_string: return None @@ -55,28 +57,36 @@ def get_filter_bbox(self, request, query_param): def filter_queryset(self, request, queryset, view): query_filter = {} - self.bbox_params = getattr(view,'bbox_params', {self.bbox_param: {'filter_field': getattr(view,'bbox_filter_field', None), 'include_overlapping': getattr(view,'bbox_filter_include_overlapping', False)}}) - for query_param, filter_field_attributes in self.bbox_params.iteritems(): + self.bbox_params = getattr( + view, + 'bbox_params', { + self.tile_param or self.bbox_param: { + 'filter_field': getattr(view,'bbox_filter_field', None), + 'include_overlapping': getattr(view,'bbox_filter_include_overlapping', False) + } + } + ) + for query_param, filter_field_attributes in six.iteritems(self.bbox_params): filter_field = filter_field_attributes['filter_field'] - include_overlapping = filter_field_attributes['include_overlapping'] - if include_overlapping: - geoDjango_filter = 'bboverlaps' - else: - geoDjango_filter = 'contained' - if not filter_field: continue bbox = self.get_filter_bbox(request, query_param) if not bbox: continue + + include_overlapping = filter_field_attributes['include_overlapping'] + if include_overlapping: + geoDjango_filter = 'bboverlaps' + else: + geoDjango_filter = 'contained' + # build the dict then return queryset filter that filters multiple attributes # backwards compatibile because get_filter_bbox is private and untested method query_filter['%s__%s' % (filter_field, geoDjango_filter)]= bbox if query_filter == {}: return queryset - else: - return queryset.filter(Q(**query_filter)) + return queryset.filter(Q(**query_filter)) # backward compatibility @@ -105,17 +115,17 @@ def __new__(cls, *args, **kwargs): class TMSTileFilter(InBBoxFilter): - bbox_param = 'tile' # The URL query paramater which contains the tile address + tile_param = 'tile' # The URL query paramater which contains the tile address - def get_filter_bbox(self, request, query_param): - tile_string = request.query_params.get(self.bbox_param, None) + def get_filter_bbox(self, request, query_param=None): + tile_string = request.query_params.get(query_param, None) if not tile_string: return None try: z, x, y = (int(n) for n in tile_string.split('/')) except ValueError: - raise ParseError('Invalid tile string supplied for parameter {0}'.format(self.bbox_param)) + raise ParseError('Invalid tile string supplied for parameter {0}'.format(query_param)) bbox = Polygon.from_bbox(tile_edges(x, y, z)) return bbox