Skip to content

Commit

Permalink
Merge pull request #1 from Superpedestrian/feature/inbbox_filter_mult…
Browse files Browse the repository at this point in the history
…iple

Add support for InBBox filtering on multiple attributes
  • Loading branch information
joeherm authored Dec 8, 2016
2 parents 654cc8b + 23737d8 commit 882f947
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 29 deletions.
57 changes: 39 additions & 18 deletions rest_framework_gis/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,35 +40,55 @@

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):
bbox_string = request.query_params.get(self.bbox_param, None)
def get_filter_bbox(self, request, query_param=None):
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'
else:
geoDjango_filter = 'contained'

if not filter_field:
query_filter = {}
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']
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
return queryset.filter(Q(**query_filter))


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

Expand Down Expand Up @@ -96,15 +117,15 @@ def __new__(cls, *args, **kwargs):
class TMSTileFilter(InBBoxFilter):
tile_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=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.tile_param))
raise ParseError('Invalid tile string supplied for parameter {0}'.format(query_param))

bbox = Polygon.from_bbox(tile_edges(x, y, z))
return bbox
Expand Down
31 changes: 22 additions & 9 deletions tests/django_restframework_gis_tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -399,14 +403,23 @@ 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'
response = self.client.get(self.location_contained_in_bbox_list_url + url_params)
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)
Expand Down
8 changes: 8 additions & 0 deletions tests/django_restframework_gis_tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 23 additions & 2 deletions tests/django_restframework_gis_tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit 882f947

Please sign in to comment.