From 4c20c4c333411b7f8b587cf219e3c25a961b5c55 Mon Sep 17 00:00:00 2001 From: Caroline Kessler Date: Tue, 29 Mar 2022 12:39:28 -0400 Subject: [PATCH 1/2] add dist field to DistToPointOrderingFilter --- Dockerfile | 3 +- docker-compose.yml | 4 +- requirements-test.txt | 2 + requirements.txt | 16 ++++ rest_framework_gis/filters.py | 82 +++++++++++++++++-- .../test_filters.py | 19 +++++ .../test_schema_generation.py | 53 ++++++++++++ tests/settings.py | 2 +- tox.ini | 1 + 9 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile index a6a0fc87..f4077ca8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,8 @@ RUN apk update && apk upgrade \ proj-dev ENV PYTHONUNBUFFERED=1 \ - PYTHONIOENCODING=UTF-8 + PYTHONIOENCODING=UTF-8 \ + PGPORT=5431 WORKDIR /project diff --git a/docker-compose.yml b/docker-compose.yml index 98542c31..af8ae8fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - postgres environment: - DJANGO_SETTINGS_MODULE=settings + - PGPORT=5431 volumes: - root_dir:/root - ./rest_framework_gis:/project/rest_framework_gis @@ -25,8 +26,9 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: django_restframework_gis + PGPORT: 5431 ports: - - 5432:5432 + - 5431:5431 volumes: - postgres_data:/var/lib/postgresql/data diff --git a/requirements-test.txt b/requirements-test.txt index e249bd33..d08e4023 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,3 +4,5 @@ contexttimer # QA checks openwisp-utils[qa]~=0.7.0 packaging~=20.4 +coreapi==2.3.3 +coreschema==0.0.4 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c93ece9a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +autopep8==1.6.0 +certifi==2021.10.8 +distlib==0.3.4 +filelock==3.6.0 +packaging==21.3 +platformdirs==2.5.1 +pluggy==1.0.0 +py==1.11.0 +pycodestyle==2.8.0 +pyparsing==3.0.7 +six==1.16.0 +toml==0.10.2 +tox==3.24.5 +virtualenv==20.13.4 +coreapi==2.3.3 +coreschema==0.0.4 diff --git a/rest_framework_gis/filters.py b/rest_framework_gis/filters.py index 018241a4..0e64273d 100644 --- a/rest_framework_gis/filters.py +++ b/rest_framework_gis/filters.py @@ -1,6 +1,9 @@ from math import cos, pi +import coreapi +import coreschema from django.contrib.gis import forms +import django.contrib.gis.measure from django.contrib.gis.db import models from django.contrib.gis.geos import Point, Polygon from django.core.exceptions import ImproperlyConfigured @@ -274,26 +277,91 @@ class DistanceToPointOrderingFilter(DistanceToPointFilter): def filter_queryset(self, request, queryset, view): if not GeometryDistance: - raise ValueError('GeometryDistance not available on this version of django') + raise ValueError("GeometryDistance not available on this version of django") - filter_field = getattr(view, 'distance_ordering_filter_field', None) + filter_field = getattr(view, "distance_ordering_filter_field", None) if not filter_field: return queryset point = self.get_filter_point(request, srid=self.srid) + print("\nPOINT: ", point) if not point: return queryset order = request.query_params.get(self.order_param) - if order == 'desc': - return queryset.order_by(-GeometryDistance(filter_field, point)) + if order == "desc": + queryset = queryset.order_by(-GeometryDistance(filter_field, point)) else: - return queryset.order_by(GeometryDistance(filter_field, point)) + queryset = queryset.order_by(GeometryDistance(filter_field, point)) + + # distance in meters + dist_string = request.query_params.get(self.dist_param, 1000) + try: + dist = float(dist_string) + except ValueError: + raise ParseError( + "Invalid distance string supplied for parameter {}".format( + self.dist_param, + ), + ) + + convert_distance_input = getattr(view, "distance_filter_convert_meters", False) + if convert_distance_input: + # Warning: assumes that the point is (lon,lat) + dist = self.dist_to_deg(dist, point[1]) + geoDjango_filter = "dwithin" # use dwithin for points + return queryset.filter( + Q(**{f"{filter_field}__{geoDjango_filter}": (point, dist)}), + # Using the following line gives error: + # Only numeric values of degree units are allowed on geographic DWithin queries. + # Q(**{f"{filter_field}__{geoDjango_filter}": (point, django.contrib.gis.measure.Distance(m=dist))}), + + ) + + def get_schema_fields(self, view): + params = super().get_schema_fields(view) + params.extend( + [ + coreapi.Field( + name=self.point_param, + required=False, + location="query", + schema=coreschema.Array( + title="point parameter title", + description="Point represented in **longitude,latitude** format.", + items={"type": "float"}, + min_items=2, + max_items=2, + ), + ), + coreapi.Field( + name=self.dist_param, + required=False, + location="query", + schema=coreschema.Number( + title="distance parameter", + description="Distance from the point (in meters) to limit the search area.", + ), + ), + coreapi.Field( + name=self.order_param, + required=False, + location="query", + schema=coreschema.Enum( + enum=("asc", "desc"), + title="order direction", + description="The direction to sort results when comparing distance.", + ), + ), + ], + ) + + return params def get_schema_operation_parameters(self, view): params = super().get_schema_operation_parameters(view) - params.append( + params.extend([ { "name": self.order_param, "required": False, @@ -307,4 +375,6 @@ def get_schema_operation_parameters(self, view): "style": "form", "explode": False, } + ] ) + return params diff --git a/tests/django_restframework_gis_tests/test_filters.py b/tests/django_restframework_gis_tests/test_filters.py index 26a0d6ce..b3df2e92 100644 --- a/tests/django_restframework_gis_tests/test_filters.py +++ b/tests/django_restframework_gis_tests/test_filters.py @@ -430,6 +430,25 @@ def test_DistanceToPointOrderingFilter_filtering(self): 'Chicago', ], ) + distance = 5 + url_params = '?point=%i,%i&dist=%i&format=json' % (point[0], point[1], distance) + response = self.client.get( + '%s%s' % (self.location_order_distance_to_point, url_params) + ) + self.assertEqual(len(response.data['features']), 8) + self.assertEqual( + [city['properties']['name'] for city in response.data['features']], + [ + 'Chicago', + 'Lawrence', + 'Oklahoma City', + 'Dallas', + 'Houston', + 'Pueblo', + 'Victoria', + 'Wellington', + ], + ) @skipIf( has_spatialite, diff --git a/tests/django_restframework_gis_tests/test_schema_generation.py b/tests/django_restframework_gis_tests/test_schema_generation.py index b9c45b5d..ce3f2627 100644 --- a/tests/django_restframework_gis_tests/test_schema_generation.py +++ b/tests/django_restframework_gis_tests/test_schema_generation.py @@ -27,6 +27,7 @@ GeojsonLocationContainedInBBoxList, GeojsonLocationContainedInTileList, GeojsonLocationWithinDistanceOfPointList, + GeojsonLocationOrderDistanceToPointList, ModelViewWithPolygon, geojson_location_list, ) @@ -663,3 +664,55 @@ def test_geometry_field(self): "required": ["random_field1", "random_field2", "polygon"], }, ) + + def test_distance_to_point_ordering_filter(self): + path = "/" + method = "GET" + view = create_view( + GeojsonLocationOrderDistanceToPointList, "GET", create_request("/") + ) + inspector = GeoFeatureAutoSchema() + inspector.view = view + generated_schema = inspector.get_filter_parameters(path, method) + self.assertListEqual( + generated_schema, + [ + { + "name": "dist", + "required": False, + "in": "query", + "schema": {"type": "number", "format": "float", "default": 1000}, + "description": "Represents **Distance** in **Distance to point** filter. " + "Default value is used only if ***point*** is passed.", + }, + { + "name": "point", + "required": False, + "in": "query", + "description": "Point represented in **x,y** format. " + "Represents **point** in **Distance to point filter**", + "schema": { + "type": "array", + "items": {"type": "float"}, + "minItems": 2, + "maxItems": 2, + "example": [0, 10], + }, + "style": "form", + "explode": False, + }, + { + "name": "order", + "required": False, + "in": "query", + "description": "", + "schema": { + "type": "enum", + "items": {"type": "string", "enum": ["asc", "desc"]}, + "example": "desc", + }, + "style": "form", + "explode": False, + } + ], + ) \ No newline at end of file diff --git a/tests/settings.py b/tests/settings.py index e4e05d1c..6add1657 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -11,7 +11,7 @@ 'NAME': 'django_restframework_gis', 'USER': 'postgres', 'PASSWORD': 'postgres', - 'HOST': 'localhost', # Change to 'postgres' for docker-compose.yml testing to work + 'HOST': 'postgres', # Change to 'postgres' for docker-compose.yml testing to work 'PORT': '', }, } diff --git a/tox.ini b/tox.ini index 39701337..35bac44f 100644 --- a/tox.ini +++ b/tox.ini @@ -27,5 +27,6 @@ deps = djangorestframework312: djangorestframework~=3.12.0 djangorestframework313: djangorestframework~=3.13.0 -rrequirements-test.txt + -rrequirements.txt pytest: pytest pytest: pytest-django From 95854cdf96c9b7cc73a75783025beb2a944da3aa Mon Sep 17 00:00:00 2001 From: Caroline Kessler Date: Wed, 30 Mar 2022 17:17:48 -0400 Subject: [PATCH 2/2] fix test + cleanup --- Dockerfile | 3 +-- docker-compose.yml | 4 +--- rest_framework_gis/filters.py | 12 +++-------- .../test_filters.py | 21 ++++++------------- .../test_schema_generation.py | 2 +- tests/settings.py | 2 +- 6 files changed, 13 insertions(+), 31 deletions(-) diff --git a/Dockerfile b/Dockerfile index f4077ca8..a6a0fc87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,7 @@ RUN apk update && apk upgrade \ proj-dev ENV PYTHONUNBUFFERED=1 \ - PYTHONIOENCODING=UTF-8 \ - PGPORT=5431 + PYTHONIOENCODING=UTF-8 WORKDIR /project diff --git a/docker-compose.yml b/docker-compose.yml index af8ae8fd..98542c31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,6 @@ services: - postgres environment: - DJANGO_SETTINGS_MODULE=settings - - PGPORT=5431 volumes: - root_dir:/root - ./rest_framework_gis:/project/rest_framework_gis @@ -26,9 +25,8 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: django_restframework_gis - PGPORT: 5431 ports: - - 5431:5431 + - 5432:5432 volumes: - postgres_data:/var/lib/postgresql/data diff --git a/rest_framework_gis/filters.py b/rest_framework_gis/filters.py index 0e64273d..a436a84b 100644 --- a/rest_framework_gis/filters.py +++ b/rest_framework_gis/filters.py @@ -3,7 +3,6 @@ import coreschema from django.contrib.gis import forms -import django.contrib.gis.measure from django.contrib.gis.db import models from django.contrib.gis.geos import Point, Polygon from django.core.exceptions import ImproperlyConfigured @@ -277,20 +276,19 @@ class DistanceToPointOrderingFilter(DistanceToPointFilter): def filter_queryset(self, request, queryset, view): if not GeometryDistance: - raise ValueError("GeometryDistance not available on this version of django") + raise ValueError('GeometryDistance not available on this version of django') - filter_field = getattr(view, "distance_ordering_filter_field", None) + filter_field = getattr(view, 'distance_ordering_filter_field', None) if not filter_field: return queryset point = self.get_filter_point(request, srid=self.srid) - print("\nPOINT: ", point) if not point: return queryset order = request.query_params.get(self.order_param) - if order == "desc": + if order == 'desc': queryset = queryset.order_by(-GeometryDistance(filter_field, point)) else: queryset = queryset.order_by(GeometryDistance(filter_field, point)) @@ -313,10 +311,6 @@ def filter_queryset(self, request, queryset, view): geoDjango_filter = "dwithin" # use dwithin for points return queryset.filter( Q(**{f"{filter_field}__{geoDjango_filter}": (point, dist)}), - # Using the following line gives error: - # Only numeric values of degree units are allowed on geographic DWithin queries. - # Q(**{f"{filter_field}__{geoDjango_filter}": (point, django.contrib.gis.measure.Distance(m=dist))}), - ) def get_schema_fields(self, view): diff --git a/tests/django_restframework_gis_tests/test_filters.py b/tests/django_restframework_gis_tests/test_filters.py index b3df2e92..3855a900 100644 --- a/tests/django_restframework_gis_tests/test_filters.py +++ b/tests/django_restframework_gis_tests/test_filters.py @@ -356,7 +356,8 @@ def test_DistanceToPointFilter_filtering(self): def test_DistanceToPointOrderingFilter_filtering(self): """ Checks that the DistanceOrderingFilter returns the objects in the correct order - given the geometry defined by the URL parameters + given the geometry defined by the URL parameters. Also checks that the + DistanceToPointOrderingFilter can accept a dist parameter """ self.assertEqual(Location.objects.count(), 0) @@ -430,25 +431,15 @@ def test_DistanceToPointOrderingFilter_filtering(self): 'Chicago', ], ) + + # Test dist parameter distance = 5 url_params = '?point=%i,%i&dist=%i&format=json' % (point[0], point[1], distance) response = self.client.get( '%s%s' % (self.location_order_distance_to_point, url_params) ) - self.assertEqual(len(response.data['features']), 8) - self.assertEqual( - [city['properties']['name'] for city in response.data['features']], - [ - 'Chicago', - 'Lawrence', - 'Oklahoma City', - 'Dallas', - 'Houston', - 'Pueblo', - 'Victoria', - 'Wellington', - ], - ) + self.assertEqual(len(response.data['features']), 1) + self.assertEqual(response.data['features'][0]['properties']['name'], 'Chicago') @skipIf( has_spatialite, diff --git a/tests/django_restframework_gis_tests/test_schema_generation.py b/tests/django_restframework_gis_tests/test_schema_generation.py index ce3f2627..8bd37270 100644 --- a/tests/django_restframework_gis_tests/test_schema_generation.py +++ b/tests/django_restframework_gis_tests/test_schema_generation.py @@ -715,4 +715,4 @@ def test_distance_to_point_ordering_filter(self): "explode": False, } ], - ) \ No newline at end of file + ) diff --git a/tests/settings.py b/tests/settings.py index 6add1657..e4e05d1c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -11,7 +11,7 @@ 'NAME': 'django_restframework_gis', 'USER': 'postgres', 'PASSWORD': 'postgres', - 'HOST': 'postgres', # Change to 'postgres' for docker-compose.yml testing to work + 'HOST': 'localhost', # Change to 'postgres' for docker-compose.yml testing to work 'PORT': '', }, }