Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 0 additions & 5 deletions deployment/clear_opensearch/clear_opensearch.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
# new index mappings or to refresh the OpenSearch cluster after
# restarting Logstash, with the lock files deleted from EFS
# storage for each pipeline.
#
# The script can be modified to delete only specific templates,
# indexes, and Logstash pipeline lock files, allowing for a more
# targeted refresh without affecting the entire OpenSearch cluster.
# This can help speed up the deployment process of new changes.

echo -e "\nDelete the custom OpenSearch indexes\n"
curl -X DELETE https://$OPENSEARCH_DOMAIN/production-locations --aws-sigv4 "aws:amz:eu-west-1:es" --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
Expand Down
5 changes: 4 additions & 1 deletion doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
#### Scheme changes

### Code/API changes
* [OSDEV-1336](https://opensupplyhub.atlassian.net/browse/OSDEV-1336) - Introduced a new PATCH `/api/v1/production-locations/{os_id}/` endpoint based on the API v1 specification. This endpoint allows the creation of a new moderation event for updating the production location with the given details. Basically, the endpoint can be used to contribute to an existing location.
* [OSDEV-1336](https://opensupplyhub.atlassian.net/browse/OSDEV-1336) - Dynamic mapping for the new fields in the `moderation-events` index has been disabled for those that don't have an explicit mapping defined. This change helps avoid indexing conflicts, such as when a field is initially indexed with one data type (e.g., long), but later an entry with a different data type for the same field is indexed, causing the entire entry to fail indexing. After this change, fields with an explicit mapping will be indexed, while other fields will not be indexed or searchable, but will still be displayed in the document.

### Architecture/Environment changes

Expand All @@ -31,6 +33,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* Ensure that the following commands are included in the `post_deployment` command:
* `migrate`
* `reindex_database`
* Run `[Release] Deploy` pipeline for the target environment with the flag `Clear the custom OpenSearch indexes and templates` set to true - to refresh the index mappings for the `moderation-events` index after disabling dynamic mapping for the new fields that don't have an explicit mapping defined. The `production-locations` will also be affected since it will clean all of our custom indexes and templates within the OpenSearch cluster


## Release 1.26.0
Expand Down Expand Up @@ -108,7 +111,7 @@ This issue has been fixed by adding additional requests to delete the appropriat
* Ensure that the following commands are included in the `post_deployment` command:
* `migrate`
* `reindex_database`
* Run `[Release] Deploy` pipeline for the target environment with the flag `Clear custom OpenSearch indexes and templates` set to true - to refresh the index mappings for the `production-locations` and `moderation-events` indexes after fixing the process of clearing the custom OpenSearch indexes.
* Run `[Release] Deploy` pipeline for the target environment with the flag `Clear the custom OpenSearch indexes and templates` set to true - to refresh the index mappings for the `production-locations` and `moderation-events` indexes after fixing the process of clearing the custom OpenSearch indexes. It will clean all of our custom indexes and templates within the OpenSearch cluster.


## Release 1.25.0
Expand Down
1 change: 1 addition & 0 deletions src/django/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ class APIV1LocationContributionErrorMessages:
'address. This may be due to incorrect, incomplete, or ambiguous '
'information. Please verify and try again.'
)
LOCATION_NOT_FOUND = ('The location with the given id was not found.')

@staticmethod
def invalid_data_type_error(data_type: str) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
from rest_framework import status

from api.models.moderation_event import ModerationEvent
from api.models.contributor.contributor import Contributor
from api.models.facility.facility import Facility


@dataclass
class CreateModerationEventDTO:
contributor_id: int
contributor: Contributor
raw_data: Dict
request_type: str
os: Facility = None
cleaned_data: Dict = field(default_factory=dict)
source: str = ''
geocode_result: Dict = field(default_factory=dict)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ def perform_event_creation(
return event_dto

event_dto.moderation_event = ModerationEvent.objects.create(
contributor=processed_event.contributor_id,
contributor=processed_event.contributor,
request_type=processed_event.request_type,
raw_data=processed_event.raw_data,
cleaned_data=processed_event.cleaned_data,
geocode_result=processed_event.geocode_result,
source=processed_event.source
source=processed_event.source,
os=processed_event.os
)

return event_dto
177 changes: 163 additions & 14 deletions src/django/api/tests/test_location_contribution_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@

from unittest.mock import Mock, patch
from rest_framework.test import APITestCase
from allauth.account.models import EmailAddress
from django.contrib.gis.geos import Point

from api.models.moderation_event import ModerationEvent
from api.models.contributor.contributor import Contributor
from api.models.user import User
from api.models.facility.facility_list import FacilityList
from api.models.facility.facility_list_item import FacilityListItem
from api.models.facility.facility import Facility
from api.models.source import Source
from api.tests.test_data import (
geocoding_data,
geocoding_no_results
Expand Down Expand Up @@ -70,7 +76,7 @@ def test_source_set_as_api_regardless_of_whether_passed(self, mock_get):
self.assertNotIn('source', self.common_valid_input_data)

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=self.common_valid_input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -137,7 +143,7 @@ def test_invalid_source_value_cannot_be_accepted(self, mock_get):

# Check the length validation.
event_dto_1 = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=invalid_input_data_1,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand All @@ -150,7 +156,7 @@ def test_invalid_source_value_cannot_be_accepted(self, mock_get):

# Check validation of accepted values.
event_dto_2 = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=invalid_input_data_2,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand All @@ -163,7 +169,7 @@ def test_invalid_source_value_cannot_be_accepted(self, mock_get):

# Check the accepted data type validation for the source field.
event_dto_3 = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=invalid_input_data_3,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand All @@ -189,7 +195,7 @@ def test_mapping_of_unsupported_fields_by_contricleaner_with_valid_data(
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -256,7 +262,7 @@ def test_mapping_of_unsupported_fields_by_contricleaner_with_invalid_data(
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -289,7 +295,7 @@ def test_handling_of_cc_list_level_errors(self):
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -332,7 +338,7 @@ def test_handling_of_cc_handler_not_set_exception(self, mock_process_data):
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -368,7 +374,7 @@ def test_handling_geocoded_no_results_error(self, mock_get):
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -397,7 +403,7 @@ def test_handling_geocoding_internal_error(self, mock_geocode_address):
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand All @@ -408,8 +414,7 @@ def test_handling_geocoding_internal_error(self, mock_geocode_address):
self.assertIsNone(result.moderation_event)
self.assertEqual(result.errors, expected_error_result)

def test_moderation_event_is_created_with_coordinates_properly(self):

def test_moderation_event_creation_with_coordinates_for_create(self):
input_data = {
'source': 'SLC',
'name': 'Blue Horizon Facility',
Expand Down Expand Up @@ -454,7 +459,7 @@ def test_moderation_event_is_created_with_coordinates_properly(self):
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down Expand Up @@ -499,9 +504,153 @@ def test_moderation_event_is_created_with_coordinates_properly(self):
# was provided during the creation of the moderation event.
self.assertIsNone(moderation_event.os)

def test_moderation_event_creation_with_valid_data_for_update(self):
# Create a new user and contributor for the production location that
# already exists in the system while processing the location
# contribution.
existing_location_user_email = '[email protected]'
existing_location_user_password = '4567test'
existing_location_user = User.objects.create(
email=existing_location_user_email
)
existing_location_user.set_password(
existing_location_user_password
)
existing_location_user.save()
EmailAddress.objects.create(
user=existing_location_user,
email=existing_location_user_email,
verified=True,
primary=True
)

existing_location_contributor = Contributor.objects.create(
admin=existing_location_user,
name='test contributor 2',
contrib_type=Contributor.OTHER_CONTRIB_TYPE,
)

# Create the production location to ensure the existing location is in
# place before processing the contribution.
list = FacilityList.objects.create(
header='header', file_name='one', name='New List Test'
)
source = Source.objects.create(
source_type=Source.LIST,
facility_list=list,
contributor=existing_location_contributor
)
list_item = FacilityListItem.objects.create(
name='Gamma Tech Manufacturing Plant',
address='1574 Quantum Avenue, Building 4B, Technopolis',
country_code='YT',
sector=['Apparel'],
row_index=1,
status=FacilityListItem.CONFIRMED_MATCH,
source=source
)
production_location = Facility.objects.create(
name=list_item.name,
address=list_item.address,
country_code=list_item.country_code,
location=Point(0, 0),
created_from=list_item
)

input_data = {
'source': 'SLC',
'name': 'Blue Horizon Facility',
'address': '990 Spring Garden St., Philadelphia PA 19123',
'country': 'US',
'sector': ['Apparel', 'Equipment'],
'coordinates': {
'lat': 51.078389,
'lng': 16.978477
},
'product_type': ['Random product type']
}

expected_raw_data = deepcopy(input_data)
expected_cleaned_data = {
'raw_json': {
'lat': 51.078389,
'lng': 16.978477,
'name': 'Blue Horizon Facility',
'address': '990 Spring Garden St., Philadelphia PA 19123',
'country': 'US',
'sector': ['Apparel', 'Equipment'],
'product_type': ['Random product type']
},
'name': 'Blue Horizon Facility',
'clean_name': 'blue horizon facility',
'address': '990 Spring Garden St., Philadelphia PA 19123',
'clean_address': '990 spring garden st. philadelphia pa 19123',
'country_code': 'US',
'sector': ['Unspecified'],
'fields': {
'product_type': [
'Apparel',
'Equipment',
'Random product type'
],
'lat': 51.078389,
'lng': 16.978477,
'country': 'US'
},
'errors': []
}

event_dto = CreateModerationEventDTO(
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.UPDATE.value,
os=production_location
)
result = self.moderation_event_creator.perform_event_creation(
event_dto
)
self.assertEqual(result.status_code, 202)

moderation_event = result.moderation_event

self.assertIsNotNone(moderation_event)
self.assertTrue(self.is_valid_uuid(moderation_event.uuid))

stringified_created_at = moderation_event.created_at.strftime(
'%Y-%m-%dT%H:%M:%S.%f'
) + 'Z'
self.assertTrue(
self.is_valid_date_with_microseconds(stringified_created_at)
)

stringified_updated_at = moderation_event.updated_at.strftime(
'%Y-%m-%dT%H:%M:%S.%f'
) + 'Z'
self.assertTrue(
self.is_valid_date_with_microseconds(stringified_updated_at)
)

self.assertIsNone(moderation_event.status_change_date)
self.assertEqual(moderation_event.request_type, 'UPDATE')
self.assertEqual(moderation_event.raw_data, expected_raw_data)
self.assertEqual(moderation_event.cleaned_data, expected_cleaned_data)
# The geocode result should be empty because the coordinates provided
# did not trigger the Google API geocoding.
self.assertEqual(moderation_event.geocode_result, {})
self.assertEqual(moderation_event.status, 'PENDING')
self.assertEqual(moderation_event.source, 'SLC')
# The claim field should be None because no claim relation was
# provided during the creation of the moderation event.
self.assertIsNone(moderation_event.claim)
self.assertEqual(moderation_event.contributor, self.contributor)
self.assertEqual(moderation_event.os.id, production_location.id)

@patch('api.geocoding.requests.get')
def test_moderation_event_is_created_without_coordinates_properly(
self, mock_get):
# This test focuses on testing the case when the coordinates were not
# passed, and geocoding should be performed for the particular
# contribution.
mock_get.return_value = Mock(ok=True, status_code=200)
mock_get.return_value.json.return_value = geocoding_data

Expand Down Expand Up @@ -629,7 +778,7 @@ def test_moderation_event_is_created_without_coordinates_properly(
}

event_dto = CreateModerationEventDTO(
contributor_id=self.contributor,
contributor=self.contributor,
raw_data=input_data,
request_type=ModerationEvent.RequestType.CREATE.value
)
Expand Down
Loading