From 42894038dc30443eeeaa95ec32b9c96d3f4b566a Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 7 Jul 2023 19:26:35 -0400 Subject: [PATCH 1/6] add validation error handling to bbox string parsing --- mapApp/views/restApi.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mapApp/views/restApi.py b/mapApp/views/restApi.py index 9ee05fc0..c7c5c54b 100644 --- a/mapApp/views/restApi.py +++ b/mapApp/views/restApi.py @@ -1,6 +1,7 @@ from mapApp.models import Incident, Hazard, Theft, Official, AlertArea, NewInfrastructure, Weather from mapApp import serializers as s from django.http import Http404 +from django.core.exceptions import ValidationError from django.contrib.gis.geos import Polygon from rest_framework.views import APIView from rest_framework.response import Response @@ -403,19 +404,28 @@ class IncidentWeatherList(APIView): """ def get(self, request, format=None): # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - queryset = Weather.objects.select_related('incident').filter(incident__geom__within=bbox) - serializer = s.WeatherSerializer(queryset, many=True) - return Response(serializer.data) + try: + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) + queryset = Weather.objects.select_related('incident').filter(incident__geom__within=bbox) + serializer = s.WeatherSerializer(queryset, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) # Helper - Create bounding box as a polygon def stringToPolygon(bbstr): - bbsplt = bbstr.split(',') - xmin, ymin, xmax, ymax = [float(x) for x in bbsplt] - - return Polygon.from_bbox((xmin, ymin, xmax, ymax)) + try: + bbsplt = bbstr.split(',') + # error here + xmin, ymin, xmax, ymax = [float(x) for x in bbsplt] + polygon = Polygon.from_bbox((xmin, ymin, xmax, ymax)) + except: + raise ValidationError(f"There was a validation error parsing your bounding box, bbox={bbstr}. Please entry your query param as a valid polygon in the format bbox=-180,-90,180,90." + ) + + return polygon #Changes made by Ayan 02/20/18 #added 2 REST API endpoints From 264fcd2d80a96e04d7ceb6f75446f108ee5c233d Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 7 Jul 2023 19:43:04 -0400 Subject: [PATCH 2/6] Add more helpful error message with exmaple --- mapApp/views/restApi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapApp/views/restApi.py b/mapApp/views/restApi.py index c7c5c54b..84ab04db 100644 --- a/mapApp/views/restApi.py +++ b/mapApp/views/restApi.py @@ -422,7 +422,7 @@ def stringToPolygon(bbstr): xmin, ymin, xmax, ymax = [float(x) for x in bbsplt] polygon = Polygon.from_bbox((xmin, ymin, xmax, ymax)) except: - raise ValidationError(f"There was a validation error parsing your bounding box, bbox={bbstr}. Please entry your query param as a valid polygon in the format bbox=-180,-90,180,90." + raise ValidationError(f"There was a validation error parsing your bounding box, 'bbox={bbstr}'. Please entry your query param as a valid polygon in the format xmin, ymin, xmax, ymax. For example, 'bbox=-123,48,-122,49'. There should not be any parentheses or quotes in the query." ) return polygon From 47d5dcb1cb8dd83b01c4d5591f8a503652fb5e91 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 7 Jul 2023 19:43:45 -0400 Subject: [PATCH 3/6] clean up imports --- mapApp/views/restApi.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mapApp/views/restApi.py b/mapApp/views/restApi.py index 84ab04db..24c38520 100644 --- a/mapApp/views/restApi.py +++ b/mapApp/views/restApi.py @@ -1,14 +1,18 @@ -from mapApp.models import Incident, Hazard, Theft, Official, AlertArea, NewInfrastructure, Weather -from mapApp import serializers as s + from django.http import Http404 from django.core.exceptions import ValidationError from django.contrib.gis.geos import Polygon + from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import authentication, generics, permissions, status + from django.views.decorators.csrf import csrf_exempt from mapApp.permissions import IsOwnerOrReadOnly from push_notifications.models import GCMDevice, APNSDevice + +from mapApp.models import Incident, Hazard, Theft, Official, AlertArea, NewInfrastructure, Weather +from mapApp import serializers as s from mapApp.views import alertUsers, pushNotification from django.contrib.auth import get_user_model From a4cfd809803566cbed9a80b7447f7b5f92ade217 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 7 Jul 2023 19:44:22 -0400 Subject: [PATCH 4/6] clean up imports --- mapApp/views/restApi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mapApp/views/restApi.py b/mapApp/views/restApi.py index 24c38520..90c42974 100644 --- a/mapApp/views/restApi.py +++ b/mapApp/views/restApi.py @@ -1,13 +1,14 @@ -from django.http import Http404 from django.core.exceptions import ValidationError +from django.contrib.auth import get_user_model from django.contrib.gis.geos import Polygon +from django.http import Http404 +from django.views.decorators.csrf import csrf_exempt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import authentication, generics, permissions, status -from django.views.decorators.csrf import csrf_exempt from mapApp.permissions import IsOwnerOrReadOnly from push_notifications.models import GCMDevice, APNSDevice @@ -15,7 +16,6 @@ from mapApp import serializers as s from mapApp.views import alertUsers, pushNotification -from django.contrib.auth import get_user_model import datetime User = get_user_model() From fb53e306e91d890061a43df7e8c0de6a156ba0ea Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 7 Jul 2023 19:45:32 -0400 Subject: [PATCH 5/6] last cleanup --- mapApp/views/restApi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mapApp/views/restApi.py b/mapApp/views/restApi.py index 90c42974..6ac67bdb 100644 --- a/mapApp/views/restApi.py +++ b/mapApp/views/restApi.py @@ -1,3 +1,4 @@ +import datetime from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model @@ -9,14 +10,13 @@ from rest_framework.response import Response from rest_framework import authentication, generics, permissions, status -from mapApp.permissions import IsOwnerOrReadOnly -from push_notifications.models import GCMDevice, APNSDevice - -from mapApp.models import Incident, Hazard, Theft, Official, AlertArea, NewInfrastructure, Weather from mapApp import serializers as s +from mapApp.models import Incident, Hazard, Theft, Official, AlertArea, NewInfrastructure, Weather +from mapApp.permissions import IsOwnerOrReadOnly from mapApp.views import alertUsers, pushNotification -import datetime +from push_notifications.models import GCMDevice, APNSDevice + User = get_user_model() class CollisionList(APIView): From b535efe5e29e786701bb138f7ccac2ae069c13c6 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 7 Jul 2023 21:08:18 -0400 Subject: [PATCH 6/6] add try/catch blocks for every endpoint that uses a bounding box --- mapApp/views/restApi.py | 237 ++++++++++++++++++++++------------------ 1 file changed, 133 insertions(+), 104 deletions(-) diff --git a/mapApp/views/restApi.py b/mapApp/views/restApi.py index 6ac67bdb..29dba99d 100644 --- a/mapApp/views/restApi.py +++ b/mapApp/views/restApi.py @@ -24,15 +24,17 @@ class CollisionList(APIView): List all collisions, or create a new collision. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - collisions = list(Incident.objects.filter(p_type__exact="collision").filter(geom__within=bbox)) + collisions = list(Incident.objects.filter(p_type__exact="collision").filter(geom__within=bbox)) - serializer = s.IncidentSerializer(collisions, many=True) - return Response(serializer.data) + serializer = s.IncidentSerializer(collisions, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) def post(self, request, format=None): serializer = s.IncidentSerializer(data=request.data) @@ -56,14 +58,16 @@ class NearmissList(APIView): List all hazards, or create a new hazard. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - nearmiss = list(Incident.objects.filter(p_type__exact="nearmiss").filter(geom__within=bbox)) - serializer = s.IncidentSerializer(nearmiss, many=True) - return Response(serializer.data) + nearmiss = list(Incident.objects.filter(p_type__exact="nearmiss").filter(geom__within=bbox)) + serializer = s.IncidentSerializer(nearmiss, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) def post(self, request, format=None): serializer = s.IncidentSerializer(data=request.data) @@ -87,14 +91,16 @@ class HazardList(APIView): List all hazards, or create a new hazard. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - hazards = list(Hazard.objects.exclude(expires_date__lt=datetime.datetime.now()).exclude(hazard_fixed=True).filter(geom__within=bbox)) - serializer = s.HazardSerializer(hazards, many=True) - return Response(serializer.data) + hazards = list(Hazard.objects.exclude(expires_date__lt=datetime.datetime.now()).exclude(hazard_fixed=True).filter(geom__within=bbox)) + serializer = s.HazardSerializer(hazards, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) def post(self, request, format=None): serializer = s.HazardSerializer(data=request.data) @@ -118,14 +124,16 @@ class TheftList(APIView): List all thefts, or create a new theft. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - thefts = list(Theft.objects.filter(geom__within=bbox)) - serializer = s.TheftSerializer(thefts, many=True) - return Response(serializer.data) + thefts = list(Theft.objects.filter(geom__within=bbox)) + serializer = s.TheftSerializer(thefts, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) def post(self, request, format=None): serializer = s.TheftSerializer(data=request.data) @@ -150,12 +158,15 @@ class FilteredHazardList(APIView): Initial use case is for the provision of data to Biko. """ def get(self, request, format=None): - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '0,0,0,0') - bbox = stringToPolygon(bbstr) - hazards = list(Hazard.objects.filter(geom__within=bbox)) - serializer = s.FilteredHazardSerializer(hazards, many=True) - return Response(serializer.data) + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '0,0,0,0') + bbox = stringToPolygon(bbstr) + hazards = list(Hazard.objects.filter(geom__within=bbox)) + serializer = s.FilteredHazardSerializer(hazards, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class FilteredTheftList(APIView): """ @@ -164,28 +175,32 @@ class FilteredTheftList(APIView): Initial use case is for the provision of data to Biko. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '0,0,0,0') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '0,0,0,0') - bbox = stringToPolygon(bbstr) - - thefts = list(Theft.objects.filter(geom__within=bbox)) - serializer = s.FilteredTheftSerializer(thefts, many=True) - return Response(serializer.data) + thefts = list(Theft.objects.filter(geom__within=bbox)) + serializer = s.FilteredTheftSerializer(thefts, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class OfficialList(APIView): """ List all thefts, or create a new theft. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - official = list(Official.objects.filter(geom__within=bbox)) - serializer = s.OfficialSerializer(official, many=True) - return Response(serializer.data) + official = list(Official.objects.filter(geom__within=bbox)) + serializer = s.OfficialSerializer(official, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) """ No need to allow submission of official data through the API yet def post(self, request, format=None): @@ -376,15 +391,17 @@ class IncidentOnlyList(APIView): Lists incidents without joining weather data. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - incidents = list(Incident.objects.filter(geom__within=bbox)) + incidents = list(Incident.objects.filter(geom__within=bbox)) - serializer = s.IncidentSerializer(incidents, many=True) - return Response(serializer.data) + serializer = s.IncidentSerializer(incidents, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class IncidentList(APIView): @@ -392,15 +409,17 @@ class IncidentList(APIView): Old way to list all incident and weather data. """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - incidents = list(Incident.objects.filter(geom__within=bbox)) + incidents = list(Incident.objects.filter(geom__within=bbox)) - serializer = s.OldIncidentWeatherSerializer(incidents, many=True) - return Response(serializer.data) + serializer = s.OldIncidentWeatherSerializer(incidents, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class IncidentWeatherList(APIView): """ @@ -439,15 +458,17 @@ class TinyCollisionList(APIView): List all collisions """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - collisionsQuerySet = Incident.objects.filter(p_type__exact="collision").filter(geom__within=bbox).exclude(infrastructure_changed=True).order_by('-date') + collisionsQuerySet = Incident.objects.filter(p_type__exact="collision").filter(geom__within=bbox).exclude(infrastructure_changed=True).order_by('-date') - serializer = s.TinyIncidentSerializer(collisionsQuerySet, many=True) - return Response(serializer.data) + serializer = s.TinyIncidentSerializer(collisionsQuerySet, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class XHRCollisionInfo(APIView): """ @@ -466,19 +487,21 @@ class TinyNearMissList(APIView): List all Near Misses """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - nearmissQuerySet = Incident.objects.filter(p_type__exact="nearmiss").filter(geom__within=bbox).exclude(infrastructure_changed=True).order_by('-date') + nearmissQuerySet = Incident.objects.filter(p_type__exact="nearmiss").filter(geom__within=bbox).exclude(infrastructure_changed=True).order_by('-date') - serializer = s.TinyIncidentSerializer(nearmissQuerySet, many=True) - return Response(serializer.data) + serializer = s.TinyIncidentSerializer(nearmissQuerySet, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class XHRNearMissInfo(APIView): """ - List detailed info for a collision + List detailed info for a near miss """ def get(self, request, format=None): @@ -490,23 +513,25 @@ def get(self, request, format=None): class TinyHazardList(APIView): """ - List all collisions + List all hazards """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) + #select_related('point'). + #Hazard.objects.select_related('point').exclude(expires_date__lt=now).exclude(hazard_fixed=True).order_by('-date')[:1], + hazardQuerySet = Hazard.objects.select_related('point').filter(geom__within=bbox).exclude(expires_date__lt=datetime.datetime.now()).exclude(hazard_fixed=True).order_by('-date') - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - #select_related('point'). - #Hazard.objects.select_related('point').exclude(expires_date__lt=now).exclude(hazard_fixed=True).order_by('-date')[:1], - hazardQuerySet = Hazard.objects.select_related('point').filter(geom__within=bbox).exclude(expires_date__lt=datetime.datetime.now()).exclude(hazard_fixed=True).order_by('-date') - - serializer = s.TinyHazSerializer(hazardQuerySet, many=True) - return Response(serializer.data) + serializer = s.TinyHazSerializer(hazardQuerySet, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class XHRHazardInfo(APIView): """ - List detailed info for a collision + List detailed info for a hazard """ def get(self, request, format=None): @@ -522,18 +547,20 @@ class TinyTheftList(APIView): List all collisions """ def get(self, request, format=None): + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - - theftQuerySet = Theft.objects.select_related('point').all().filter(geom__within=bbox).exclude(infrastructure_changed=True).order_by('-date') - serializer = s.TinyTheftSerializer(theftQuerySet, many=True) - return Response(serializer.data) + theftQuerySet = Theft.objects.select_related('point').all().filter(geom__within=bbox).exclude(infrastructure_changed=True).order_by('-date') + serializer = s.TinyTheftSerializer(theftQuerySet, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class XHRTheftInfo(APIView): """ - List detailed info for a collision + List detailed info for a theft """ def get(self, request, format=None): @@ -548,14 +575,16 @@ class TinyNewInfrastructureList(APIView): List all new infrastructures """ def get(self, request, format=None): - - # Extract bounding box Url parameter - bbstr = request.GET.get('bbox', '-180,-90,180,90') - bbox = stringToPolygon(bbstr) - #select_related('point'). - niQuerySet = NewInfrastructure.objects.select_related('point').filter(geom__within=bbox).exclude(expires_date__lt=datetime.datetime.now()).order_by('-date') - serializer = s.TinyNewInfrastructureSerializer(niQuerySet, many=True) - return Response(serializer.data) + try: + # Extract bounding box Url parameter + bbstr = request.GET.get('bbox', '-180,-90,180,90') + bbox = stringToPolygon(bbstr) + #select_related('point'). + niQuerySet = NewInfrastructure.objects.select_related('point').filter(geom__within=bbox).exclude(expires_date__lt=datetime.datetime.now()).order_by('-date') + serializer = s.TinyNewInfrastructureSerializer(niQuerySet, many=True) + return Response(serializer.data) + except ValidationError as err: + return Response(err, status=status.HTTP_400_BAD_REQUEST) class XHRNewInfrastructureInfo(APIView): """