Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bca61f5
Add geo_bounding_box to the V1_PARAMETERS_LIST class
mazursasha1990 Feb 5, 2025
55b92bd
Add __process_filter method to the OpenSearchQueryDirector
mazursasha1990 Feb 5, 2025
e22d7e2
Add GeoBoundingBoxValidator class
mazursasha1990 Feb 5, 2025
d67e341
Add GeoBoundingBoxValidator to the ProductionLocationsSerializer
mazursasha1990 Feb 5, 2025
0cf8b6b
Add add_geo_bounding_box method to the ProductionLocationsQueryBuilder
mazursasha1990 Feb 5, 2025
413be10
Add geo_bounding_box for preparing params in serialize_params
mazursasha1990 Feb 5, 2025
768c8f1
Refactor __process_filter
mazursasha1990 Feb 6, 2025
e39d950
Refactor add_geo_bounding_box
mazursasha1990 Feb 6, 2025
3f97fd8
Refactor serialize_params
mazursasha1990 Feb 6, 2025
28406d2
Refactor ProductionLocationsSerializer
mazursasha1990 Feb 6, 2025
11e7c65
Refactor GeoBoundingBoxValidator
mazursasha1990 Feb 6, 2025
19e1e01
Refactor GeoBoundingBoxValidator
mazursasha1990 Feb 7, 2025
d3db0d8
Update order of parameters
mazursasha1990 Feb 7, 2025
6d4acc1
Add test_add_geo_bounding_box test to the TestProductionLocationsQuer…
mazursasha1990 Feb 7, 2025
6ebd690
Add test cases to cover serialization of geo bounding box to the V1Ut…
mazursasha1990 Feb 8, 2025
09d5563
Fix linter error
mazursasha1990 Feb 8, 2025
21045c2
Add integrational test case to test geo bounding box
mazursasha1990 Feb 8, 2025
d22f577
Merge branch 'main' into OSDEV-1577-add-geo-bounding-box-support-to-t…
mazursasha1990 Feb 8, 2025
407702c
Remove unused import json from opensearch_query_director.py
mazursasha1990 Feb 8, 2025
bc02354
Fix test_production_locations_geo_bounding_box
mazursasha1990 Feb 10, 2025
a91639c
Remove unnecessary print statements in the ProductionLocationsTest
mazursasha1990 Feb 10, 2025
e86ec7b
Add release notes
mazursasha1990 Feb 10, 2025
07f5813
Convert top, left, bottom, and right parameters to float
mazursasha1990 Feb 11, 2025
dc5b82a
Convert top, left, bottom, and right parameters to float
mazursasha1990 Feb 11, 2025
87cae94
Change order of parameters in the test_add_geo_bounding_box
mazursasha1990 Feb 11, 2025
4deeb3f
Remove unnecessary print statement
mazursasha1990 Feb 11, 2025
fe46ace
Add class CoordinateLimits and use it in the GeoBoundingBoxValidator …
mazursasha1990 Feb 11, 2025
416c0fa
Combine the two conditions into one if statement in the CoordinatesVa…
mazursasha1990 Feb 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 34 additions & 26 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,44 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* *Describe schema changes here.*

### Code/API changes
* *Describe code/API changes here.*
* [OSDEV-1577](https://opensupplyhub.atlassian.net/browse/OSDEV-1577) - Added geo-bounding box query support to the GET `/api/v1/production-locations/` endpoint. To filter production locations whose geopoints fall within the bounding box, it is necessary to specify valid values for the parameters `geo_bounding_box[top]`, `geo_bounding_box[left]`, `geo_bounding_box[bottom]`, and `geo_bounding_box[right]`.

The validation rules are as follows:
* All coordinates of the geo-boundary box (top, left, bottom, right) must be provided.
* All values must be integers.
* The top and bottom coordinates must be between -90 and 90.
* The left and right coordinates must be between -180 and 180.
* The top must be greater than the bottom.
* The right must be greater than the left.

### Architecture/Environment changes
* [OSDEV-899](https://opensupplyhub.atlassian.net/browse/OSDEV-899) - With this task, we split the Django container into two components: FE (React) and BE (Django). Requests to the frontend (React) will be processed by the CDN (CloudFront), while requests to the API will be redirected to the Django container. This approach will allow for more efficient use of ECS cluster computing resources and improve frontend performance.

The following endpoints will be redirected to the Django container:
* tile/*
* api/*
* /api-auth/*
* /api-token-auth/*
* /api-feature-flags/*
* /web/environment.js
* /admin/*
* /health-check/*
* /rest-auth/*
* /user-login/*
* /user-logout/*
* /user-signup/*
* /user-profile/*
* /user-api-info/*
* /admin
* /static/admin/*
* /static/django_extensions/*
* /static/drf-yasg/*
* /static/gis/*
* /static/rest_framework/*
* /static/static/*
* /static/staticfiles.json

All other traffic will be redirected to the React application.
The following endpoints will be redirected to the Django container:
* tile/*
* api/*
* /api-auth/*
* /api-token-auth/*
* /api-feature-flags/*
* /web/environment.js
* /admin/*
* /health-check/*
* /rest-auth/*
* /user-login/*
* /user-logout/*
* /user-signup/*
* /user-profile/*
* /user-api-info/*
* /admin
* /static/admin/*
* /static/django_extensions/*
* /static/drf-yasg/*
* /static/gis/*
* /static/rest_framework/*
* /static/static/*
* /static/staticfiles.json

All other traffic will be redirected to the React application.

### Bugfix
* *Describe bugfix here.*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from api.serializers.v1.opensearch_validation_interface import (
OpenSearchValidationInterface,
)


class GeoBoundingBoxValidator(OpenSearchValidationInterface):
def validate_opensearch_params(self, data):
errors = []

fields = ['top', 'left', 'bottom', 'right']
coords = {
field: data.get(f'geo_bounding_box_{field}') for field in fields
}

if any(value is not None for value in coords.values()) and not all(
value is not None for value in coords.values()
):
errors.append(
{
"field": "geo_bounding_box",
"detail": "All coordinates (top, left, bottom, right) "
"must be provided.",
}
)

return errors

if not any(value is not None for value in coords.values()):
return errors

range_limits = {
'top': (-90, 90),
'bottom': (-90, 90),
'left': (-180, 180),
'right': (-180, 180),
}

for field, (min_val, max_val) in range_limits.items():
value = coords[field]

if value < min_val or value > max_val:
errors.append(
{
"field": "geo_bounding_box",
"detail": f"The {field} value must be between "
f"{min_val} and {max_val}.",
}
)

if coords['top'] <= coords['bottom']:
errors.append(
{
"field": "geo_bounding_box",
"detail": "The top must be greater than bottom.",
}
)

if coords['right'] <= coords['left']:
errors.append(
{
"field": "geo_bounding_box",
"detail": "The right must be greater than left.",
}
)

return errors
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from api.serializers.v1.opensearch_common_validators. \
coordinates_validator import CoordinatesValidator
from api.constants import APIV1CommonErrorMessages
from api.serializers.v1.opensearch_common_validators.\
geo_bounding_box_validator import GeoBoundingBoxValidator


class ProductionLocationsSerializer(Serializer):
Expand Down Expand Up @@ -56,14 +58,20 @@ class ProductionLocationsSerializer(Serializer):
max_value=15,
required=False,
)
geo_bounding_box_top = FloatField(required=False)
geo_bounding_box_left = FloatField(required=False)
geo_bounding_box_bottom = FloatField(required=False)
geo_bounding_box_right = FloatField(required=False)

def validate(self, data):
print('data >>>', data)
validators = [
SizeValidator(),
NumberOfWorkersValidator(),
PercentOfFemaleWorkersValidator(),
CoordinatesValidator(),
CountryValidator(),
GeoBoundingBoxValidator(),
]

error_list_builder = OpenSearchErrorListBuilder(validators)
Expand Down
24 changes: 24 additions & 0 deletions src/django/api/tests/test_production_locations_query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,27 @@ def test_add_aggregations_where_aggregation_is_not_geohex_grid(self):
aggregation
)
self.assertNotIn('aggregations', self.builder.query_body)

def test_add_geo_bounding_box(self):
top = 40.7128
left = -74.0060
bottom = 40.7128
right = -74.0060
self.builder.add_geo_bounding_box(top, right, bottom, left)
expected = {
'geo_bounding_box': {
'coordinates': {
'top': top,
'left': left,
'bottom': bottom,
'right': right,
}
}
}
self.assertIn(
'filter', self.builder.query_body['query']['bool']
)
self.assertEqual(
expected,
self.builder.query_body['query']['bool']['filter']
)
110 changes: 110 additions & 0 deletions src/django/api/tests/test_v1_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,113 @@ def test_handle_opensearch_exception(self):
self.assertIsInstance(response, Response)
self.assertEqual(response.status_code, 500)
self.assertEqual(response.data['detail'], "OpenSearch error")

def test_serialize_geo_bounding_box(self):
query_dict = QueryDict('', mutable=True)
query_dict.update(
{
'geo_bounding_box[top]': '90',
'geo_bounding_box[left]': '-180',
'geo_bounding_box[bottom]': '-90',
'geo_bounding_box[right]': '180',
}
)
serialized_params, error_response = serialize_params(
ProductionLocationsSerializer, query_dict
)
self.assertIsNone(error_response)
self.assertEqual(serialized_params['geo_bounding_box_top'], 90)
self.assertEqual(serialized_params['geo_bounding_box_left'], -180)
self.assertEqual(serialized_params['geo_bounding_box_bottom'], -90)
self.assertEqual(serialized_params['geo_bounding_box_right'], 180)

def test_serialize_geo_bounding_box_missing_values(self):
query_dict = QueryDict('', mutable=True)
query_dict.update(
{
'geo_bounding_box[top]': '90',
'geo_bounding_box[left]': '-180',
'geo_bounding_box[bottom]': '-90',
}
)
_, error_response = serialize_params(
ProductionLocationsSerializer, query_dict
)
self.assertIsNotNone(error_response)
self.assertEqual(
error_response['detail'], "The request query is invalid."
)
self.assertIn(
{
'field': 'geo_bounding_box',
'detail': 'All coordinates (top, left, bottom, right) '
'must be provided.',
},
error_response['errors'],
)

def test_serialize_geo_bounding_box_invalid_range(self):
query_dict = QueryDict('', mutable=True)
query_dict.update(
{
'geo_bounding_box[top]': '91',
'geo_bounding_box[left]': '-180',
'geo_bounding_box[bottom]': '-90',
'geo_bounding_box[right]': '181',
}
)
_, error_response = serialize_params(
ProductionLocationsSerializer, query_dict
)

self.assertIsNotNone(error_response)
self.assertEqual(
error_response['detail'], "The request query is invalid."
)
self.assertIn(
{
'field': 'geo_bounding_box',
'detail': 'The top value must be between -90 and 90.',
},
error_response['errors'],
)
self.assertIn(
{
'field': 'geo_bounding_box',
'detail': 'The right value must be between -180 and 180.',
},
error_response['errors'],
)

def test_serialize_geo_bounding_box_invalid_order(self):
query_dict = QueryDict('', mutable=True)
query_dict.update(
{
'geo_bounding_box[top]': '-90',
'geo_bounding_box[left]': '180',
'geo_bounding_box[bottom]': '90',
'geo_bounding_box[right]': '-180',
}
)
_, error_response = serialize_params(
ProductionLocationsSerializer, query_dict
)

self.assertIsNotNone(error_response)
self.assertEqual(
error_response['detail'], "The request query is invalid."
)
self.assertIn(
{
'field': 'geo_bounding_box',
'detail': 'The right must be greater than left.',
},
error_response['errors'],
)
self.assertIn(
{
'field': 'geo_bounding_box',
'detail': 'The top must be greater than bottom.',
},
error_response['errors'],
)
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def build_query(self, query_params):
self.__process_size(query_params)
self.__process_multi_match(query_params)
self.__process_aggregation(query_params)
self.__process_filter(query_params)

return self.__builder.get_final_query_body()

Expand Down Expand Up @@ -131,3 +132,24 @@ def __process_aggregation(self, query_params):

if aggregation and hasattr(self.__builder, 'add_aggregations'):
self.__builder.add_aggregations(aggregation, geohex_grid_precision)

def __process_filter(self, query_params):
top = query_params.get(
V1_PARAMETERS_LIST.GEO_BOUNDING_BOX + '[top]'
)
left = query_params.get(
V1_PARAMETERS_LIST.GEO_BOUNDING_BOX + '[left]'
)
bottom = query_params.get(
V1_PARAMETERS_LIST.GEO_BOUNDING_BOX + '[bottom]'
)
right = query_params.get(
V1_PARAMETERS_LIST.GEO_BOUNDING_BOX + '[right]'
)

if top and left and bottom and right and hasattr(
self.__builder, 'add_geo_bounding_box'
):
self.__builder.add_geo_bounding_box(
float(top), float(left), float(bottom), float(right)
)
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,15 @@ def add_aggregations(self, aggregation, geohex_grid_precision=None):
}

return self.query_body

def add_geo_bounding_box(self, top, left, bottom, right):
self.query_body['query']['bool']['filter'] = {
'geo_bounding_box': {
'coordinates': {
'top': top,
'left': left,
'bottom': bottom,
'right': right,
}
}
}
1 change: 1 addition & 0 deletions src/django/api/views/v1/parameters_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ class V1_PARAMETERS_LIST:
DATE_LT = 'date_lt'
AGGREGATION = 'aggregation'
GEOHEX_GRID_PRECISION = 'geohex_grid_precision'
GEO_BOUNDING_BOX = 'geo_bounding_box'
6 changes: 5 additions & 1 deletion src/django/api/views/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ def serialize_params(serializer_class, query_params):
f'{V1_PARAMETERS_LIST.COORDINATES}[lat]',
f'{V1_PARAMETERS_LIST.COORDINATES}[lng]',
f'{V1_PARAMETERS_LIST.SEARCH_AFTER}[id]',
f'{V1_PARAMETERS_LIST.SEARCH_AFTER}[value]'
f'{V1_PARAMETERS_LIST.SEARCH_AFTER}[value]',
f'{V1_PARAMETERS_LIST.GEO_BOUNDING_BOX}[top]',
f'{V1_PARAMETERS_LIST.GEO_BOUNDING_BOX}[left]',
f'{V1_PARAMETERS_LIST.GEO_BOUNDING_BOX}[bottom]',
f'{V1_PARAMETERS_LIST.GEO_BOUNDING_BOX}[right]',
]:
new_key = key.replace(']', '').replace('[', '_')
flattened_query_params[new_key] = value[0]
Expand Down
Loading
Loading