Skip to content

Commit

Permalink
Merge pull request #76 from maykinmedia/feature/geo-headers
Browse files Browse the repository at this point in the history
Feature/geo headers
  • Loading branch information
annashamray committed Oct 6, 2020
2 parents 1498e0e + 7e2eb74 commit f833bff
Show file tree
Hide file tree
Showing 10 changed files with 673 additions and 16 deletions.
33 changes: 33 additions & 0 deletions src/objects/api/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rest_framework.exceptions import NotAcceptable
from rest_framework.renderers import BrowsableAPIRenderer
from vng_api_common.exceptions import PreconditionFailed
from vng_api_common.geo import (
DEFAULT_CRS,
HEADER_ACCEPT,
HEADER_CONTENT,
GeoMixin as _GeoMixin,
extract_header,
)


class GeoMixin(_GeoMixin):
def perform_crs_negotation(self, request):
# don't cripple the browsable API...
if isinstance(request.accepted_renderer, BrowsableAPIRenderer):
return

# methods with request bodies need to have the CRS specified
if request.method.lower() in ("post", "put", "patch"):
content_crs = extract_header(request, HEADER_CONTENT)
if content_crs is None:
raise PreconditionFailed(detail=f"'{HEADER_CONTENT}' header ontbreekt")
if content_crs != DEFAULT_CRS:
raise NotAcceptable(detail=f"CRS '{content_crs}' is niet ondersteund")

if request.method.lower() == "delete":
return

# check optional header
requested_crs = extract_header(request, HEADER_ACCEPT)
if requested_crs and requested_crs != DEFAULT_CRS:
raise NotAcceptable(detail=f"CRS '{requested_crs}' is niet ondersteund")
4 changes: 2 additions & 2 deletions src/objects/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from vng_api_common.geo import GeoMixin
from vng_api_common.search import SearchMixin

from objects.core.models import Object

from .filters import ObjectFilterSet
from .mixins import GeoMixin
from .serializers import (
HistoryRecordSerializer,
ObjectSearchSerializer,
ObjectSerializer,
)


class ObjectViewSet(SearchMixin, viewsets.ModelViewSet):
class ObjectViewSet(SearchMixin, GeoMixin, viewsets.ModelViewSet):
queryset = Object.objects.order_by("-pk")
serializer_class = ObjectSerializer
filterset_class = ObjectFilterSet
Expand Down
5 changes: 5 additions & 0 deletions src/objects/tests/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
GEO_READ_KWARGS = {"HTTP_ACCEPT_CRS": "EPSG:4326"}

GEO_WRITE_KWARGS = {"HTTP_CONTENT_CRS": "EPSG:4326"}


POLYGON_AMSTERDAM_CENTRUM = [
[4.897787, 52.37449846745873],
[4.900105493594382, 52.3742742607433],
Expand Down
104 changes: 104 additions & 0 deletions src/objects/tests/test_geo_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from django.urls import reverse

from rest_framework import status
from rest_framework.test import APITestCase

from objects.core.tests.factores import ObjectFactory

from .constants import GEO_READ_KWARGS, POLYGON_AMSTERDAM_CENTRUM

OBJECT_TYPE = "https://example.com/objecttypes/v1/types/a6c109"


class GeoHeaderTests(APITestCase):
def assertResponseHasGeoHeaders(self, response):
self.assertTrue("Content-Crs" in response)
self.assertEqual(response["Content-Crs"], "EPSG:4326")

def test_get_without_geo_headers(self):
object = ObjectFactory.create()
url = reverse("object-detail", args=[object.uuid])

response = self.client.get(url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertResponseHasGeoHeaders(response)

def test_get_with_geo_headers(self):
object = ObjectFactory.create()
url = reverse("object-detail", args=[object.uuid])

response = self.client.get(url, **GEO_READ_KWARGS)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertResponseHasGeoHeaders(response)

def test_get_with_incorrect_get_headers(self):
object = ObjectFactory.create()
url = reverse("object-detail", args=[object.uuid])

response = self.client.get(url, HTTP_ACCEPT_CRS="EPSG:3857")

self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)

def test_create_without_geo_headers(self):
data = {
"type": OBJECT_TYPE,
"record": {
"typeVersion": 1,
"data": {"diameter": 30},
"startDate": "2020-01-01",
},
}
url = reverse("object-list")

response = self.client.post(url, data)

self.assertEqual(response.status_code, status.HTTP_412_PRECONDITION_FAILED)

def test_update_without_geo_headers(self):
object = ObjectFactory.create()
url = reverse("object-detail", args=[object.uuid])
data = {
"type": OBJECT_TYPE,
"record": {
"typeVersion": 1,
"data": {"diameter": 30},
"startDate": "2020-01-01",
},
}

for method in ("put", "patch"):
with self.subTest(method=method):
do_request = getattr(self.client, method)

response = do_request(url, data)

self.assertEqual(
response.status_code, status.HTTP_412_PRECONDITION_FAILED
)

def test_delete_without_geo_headers(self):
object = ObjectFactory.create()
url = reverse("object-detail", args=[object.uuid])

response = self.client.delete(url)

self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

def test_search_without_geo_headers(self):
url = reverse("object-search")

response = self.client.post(
url,
{
"geometry": {
"within": {
"type": "Polygon",
"coordinates": [POLYGON_AMSTERDAM_CENTRUM],
}
}
},
)

self.assertEqual(response.status_code, status.HTTP_412_PRECONDITION_FAILED)
4 changes: 3 additions & 1 deletion src/objects/tests/test_geo_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from objects.core.tests.factores import ObjectRecordFactory

from .constants import POLYGON_AMSTERDAM_CENTRUM
from .constants import GEO_WRITE_KWARGS, POLYGON_AMSTERDAM_CENTRUM

OBJECT_TYPE = "https://example.com/objecttypes/v1/types/a6c109"

Expand All @@ -32,6 +32,7 @@ def test_filter_within(self):
}
}
},
**GEO_WRITE_KWARGS,
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand Down Expand Up @@ -61,6 +62,7 @@ def test_filter_objecttype(self):
},
"type": OBJECT_TYPE,
},
**GEO_WRITE_KWARGS,
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand Down
7 changes: 4 additions & 3 deletions src/objects/tests/test_object_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from objects.core.models import Object
from objects.core.tests.factores import ObjectFactory, ObjectRecordFactory

from .constants import GEO_WRITE_KWARGS
from .utils import mock_objecttype

OBJECT_TYPE = "https://example.com/objecttypes/v1/types/a6c109"
Expand Down Expand Up @@ -69,7 +70,7 @@ def test_create_object(self, m):
},
}

response = self.client.post(url, data)
response = self.client.post(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_201_CREATED)

Expand Down Expand Up @@ -109,7 +110,7 @@ def test_update_object(self, m):
},
}

response = self.client.put(url, data)
response = self.client.put(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_200_OK)

Expand Down Expand Up @@ -154,7 +155,7 @@ def test_patch_object_record(self, m):
},
}

response = self.client.patch(url, data)
response = self.client.patch(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_200_OK)

Expand Down
15 changes: 8 additions & 7 deletions src/objects/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from objects.core.models import Object
from objects.core.tests.factores import ObjectRecordFactory

from .constants import GEO_WRITE_KWARGS
from .utils import mock_objecttype

OBJECT_TYPE = "https://example.com/objecttypes/v1/types/a6c109"
Expand All @@ -28,7 +29,7 @@ def test_create_object_invalid_objecttype_url(self, m):
},
}

response = self.client.post(url, data)
response = self.client.post(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Object.objects.count(), 0)
Expand All @@ -53,7 +54,7 @@ def test_create_object_no_version(self, m):
},
}

response = self.client.post(url, data)
response = self.client.post(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Object.objects.count(), 0)
Expand All @@ -77,7 +78,7 @@ def test_create_object_schema_invalid(self, m):
},
}

response = self.client.post(url, data)
response = self.client.post(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Object.objects.count(), 0)
Expand All @@ -93,7 +94,7 @@ def test_create_object_without_record_invalid(self, m):
"type": OBJECT_TYPE,
}

response = self.client.post(url, data)
response = self.client.post(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Object.objects.count(), 0)
Expand All @@ -113,7 +114,7 @@ def test_create_object_correction_invalid(self, m):
},
}

response = self.client.post(url, data)
response = self.client.post(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Object.objects.exclude(id=record.object.id).count(), 0)
Expand Down Expand Up @@ -142,7 +143,7 @@ def test_update_object_with_correction_invalid(self, m):
},
}

response = self.client.put(url, data)
response = self.client.put(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

Expand All @@ -166,7 +167,7 @@ def test_update_object_type_invalid(self, m):
"type": OBJECT_TYPE,
}

response = self.client.patch(url, data)
response = self.client.patch(url, data, **GEO_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

Expand Down
40 changes: 37 additions & 3 deletions src/objects/utils/inspectors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from drf_yasg import openapi
from vng_api_common.geo import DEFAULT_CRS, HEADER_ACCEPT, HEADER_CONTENT
from vng_api_common.inspectors.geojson import (
GeometryFieldInspector as _GeometryFieldInspector,
)
Expand All @@ -7,7 +9,39 @@ class GeometryFieldInspector(_GeometryFieldInspector):
""" don't show GEO headers since they are not required now"""

def get_request_header_parameters(self, serializer):
return []
if not self.has_geo_fields(serializer):
return []

def get_response_headers(self, serializer, status=None):
return None
if self.method == "DELETE":
return []

headers = [
openapi.Parameter(
name=HEADER_ACCEPT,
type=openapi.TYPE_STRING,
in_=openapi.IN_HEADER,
required=False,
description="Het gewenste 'Coordinate Reference System' (CRS) van de "
"geometrie in het antwoord (response body). Volgens de "
"GeoJSON spec is WGS84 de default (EPSG:4326 is "
"hetzelfde als WGS84).",
enum=[DEFAULT_CRS],
),
]

if self.method in ("POST", "PUT", "PATCH"):
headers.append(
openapi.Parameter(
name=HEADER_CONTENT,
type=openapi.TYPE_STRING,
in_=openapi.IN_HEADER,
required=True,
description="Het 'Coordinate Reference System' (CRS) van de "
"geometrie in de vraag (request body). Volgens de "
"GeoJSON spec is WGS84 de default (EPSG:4326 is "
"hetzelfde als WGS84).",
enum=[DEFAULT_CRS],
),
)

return headers

0 comments on commit f833bff

Please sign in to comment.