diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index dba2c9fd6..bbd812e6d 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -43,6 +43,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html * [OSDEV-1158](https://opensupplyhub.atlassian.net/browse/OSDEV-1158) - The following features and improvements have been made: 1. Introduced a new POST `/api/v1/production-locations/` endpoint based on the API v1 specification. This endpoint allows the creation of a new moderation event for the production location creation with the given details. 2. Removed redundant redefinition of paths via the `as_view` method for all the v1 API endpoints since they are already defined via `DefaultRouter`. +* [OSDEV-1468](https://opensupplyhub.atlassian.net/browse/OSDEV-1468) - Limit the `page` parameter to `100` for the GET `/api/facilities/` endpoint. This will help prevent system downtimes, as larger pages (OFFSET) make it harder for the database to retrieve data, especially considering the large amount of data we have. ### Architecture/Environment changes * [OSDEV-1170](https://opensupplyhub.atlassian.net/browse/OSDEV-1170) - Added the ability to automatically create a dump from the latest shared snapshot of the anonymized database from Production environment for use in the Test and Pre-Prod environments. diff --git a/src/django/api/constants.py b/src/django/api/constants.py index 435a4ada9..96170b2f8 100644 --- a/src/django/api/constants.py +++ b/src/django/api/constants.py @@ -203,6 +203,10 @@ class FacilitiesDownloadSettings: DEFAULT_LIMIT = 10000 +class FacilitiesListSettings: + DEFAULT_PAGE_LIMIT = 100 + + # API v1 class APIV1CommonErrorMessages: COMMON_REQ_BODY_ERROR = 'The request body is invalid.' diff --git a/src/django/api/serializers/facility/facility_list_page_parameter_serializer.py b/src/django/api/serializers/facility/facility_list_page_parameter_serializer.py new file mode 100644 index 000000000..5737eebe5 --- /dev/null +++ b/src/django/api/serializers/facility/facility_list_page_parameter_serializer.py @@ -0,0 +1,25 @@ +from rest_framework.serializers import ( + IntegerField, + Serializer +) + +from api.constants import FacilitiesListSettings + + +class FacilityListPageParameterSerializer(Serializer): + ''' + The serializer validates the page query parameter for the list action of + the FacilitiesViewSet. + ''' + page = IntegerField( + required=False, + max_value=FacilitiesListSettings.DEFAULT_PAGE_LIMIT, + min_value=1, + error_messages={ + 'max_value': ( + 'This value must be less or equal to ' + f'{FacilitiesListSettings.DEFAULT_PAGE_LIMIT}. If you need ' + 'access to more data, please contact ' + 'support@opensupplyhub.org.'), + } + ) diff --git a/src/django/api/tests/test_page_limit_in_facilities_viewset_list_action.py b/src/django/api/tests/test_page_limit_in_facilities_viewset_list_action.py new file mode 100644 index 000000000..ca1a58dc5 --- /dev/null +++ b/src/django/api/tests/test_page_limit_in_facilities_viewset_list_action.py @@ -0,0 +1,36 @@ +import json + +from rest_framework.test import APITestCase +from django.urls import reverse + + +class TestPageLimitInFacilitiesViewsetListAction(APITestCase): + def setUp(self): + self.url = reverse('facility-list') + + def test_page_limit_blocks_request_when_exceeding_limit(self): + expected_response = { + 'page': [ + ('This value must be less or equal to 100. If you need access ' + 'to more data, please contact support@opensupplyhub.org.') + ] + } + + page = {'page': 101} + + response = self.client.get(self.url, page) + self.assertEqual(response.status_code, 400) + + response_body_dict = json.loads(response.content) + self.assertEqual(response_body_dict, expected_response) + + def test_page_limit_does_not_block_request_within_limit(self): + page = {'page': 1} + + response = self.client.get(self.url, page) + response_body_dict = json.loads(response.content) + + # Made sure that the successful response with an empty facility list + # is returned even if the test database is empty. + self.assertEqual(response.status_code, 200) + self.assertEqual(response_body_dict['count'], 0) diff --git a/src/django/api/views/facility/facilities_view_set.py b/src/django/api/views/facility/facilities_view_set.py index c996881df..a3c2b6d09 100644 --- a/src/django/api/views/facility/facilities_view_set.py +++ b/src/django/api/views/facility/facilities_view_set.py @@ -91,6 +91,8 @@ FacilityQueryParamsSerializer, FacilityUpdateLocationParamsSerializer, ) +from api.serializers.facility.facility_list_page_parameter_serializer \ + import FacilityListPageParameterSerializer from ...throttles import DataUploadThrottle from ..disabled_pagination_inspector import DisabledPaginationInspector @@ -220,6 +222,12 @@ def list(self, request): ] } """ + page_serializer = FacilityListPageParameterSerializer( + data=request.query_params + ) + if not page_serializer.is_valid(): + raise ValidationError(page_serializer.errors) + params = FacilityQueryParamsSerializer(data=request.query_params) if not params.is_valid(): diff --git a/src/django/api/views/facility/facility_parameters.py b/src/django/api/views/facility/facility_parameters.py index af34aee1a..371f9db00 100644 --- a/src/django/api/views/facility/facility_parameters.py +++ b/src/django/api/views/facility/facility_parameters.py @@ -6,6 +6,8 @@ TYPE_INTEGER ) +from api.constants import FacilitiesListSettings + facility_parameters = [ Parameter( 'created_at_of_data_points', @@ -31,6 +33,17 @@ ] facilities_list_parameters = [ + Parameter( + 'page', + IN_QUERY, + type=TYPE_INTEGER, + required=False, + description=( + 'A page number within the paginated result set. As of now, the ' + 'last page that can be accessed is ' + f'{FacilitiesListSettings.DEFAULT_PAGE_LIMIT}. To access more ' + 'data, please contact support@opensupplyhub.org.'), + ), Parameter( 'q', IN_QUERY,