From 29af215eb6197050cd7522ba232e4f87d709afc5 Mon Sep 17 00:00:00 2001 From: abbas-khan10 <127417949+abbas-khan10@users.noreply.github.com> Date: Thu, 2 May 2024 14:03:05 +0100 Subject: [PATCH 1/6] PRMDR-785: Upload retry logic in the event of upload failure (#356) * Add Upload Retry logic in the event of upload failure * fix pip-audit issues and format * pr changes --- .../helpers/requests/uploadDocument.test.ts | 2 +- app/src/helpers/requests/uploadDocuments.ts | 17 +++--- .../LloydGeorgeUploadPage.tsx | 43 +++++++++------ lambdas/models/auth_policy.py | 1 + lambdas/models/document_reference.py | 10 ++++ lambdas/models/pds_models.py | 14 ++--- lambdas/models/staging_metadata.py | 4 +- lambdas/requirements.txt | 4 +- lambdas/scripts/batch_update_ods_code.py | 6 +-- .../create_document_reference_service.py | 6 +-- .../document_reference_search_service.py | 12 ++--- .../services/update_upload_state_service.py | 54 +++++++++---------- .../unit/helpers/data/update_upload_state.py | 8 ++- .../test_update_upload_state_service.py | 6 +-- 14 files changed, 100 insertions(+), 87 deletions(-) diff --git a/app/src/helpers/requests/uploadDocument.test.ts b/app/src/helpers/requests/uploadDocument.test.ts index 6e9bc72da..87c8c4c99 100644 --- a/app/src/helpers/requests/uploadDocument.test.ts +++ b/app/src/helpers/requests/uploadDocument.test.ts @@ -12,7 +12,7 @@ describe('[POST] updateDocumentState', () => { const document = buildDocument(buildTextFile('test1'), documentUploadStates.SUCCEEDED); mockedAxios.post.mockImplementation(() => Promise.resolve({ status: 200 })); const args = { - document, + documents: [document], uploadingState: true, documentReference: 'test/test/test/key', baseUrl: '/test', diff --git a/app/src/helpers/requests/uploadDocuments.ts b/app/src/helpers/requests/uploadDocuments.ts index fbc65f2db..3bbd249b4 100644 --- a/app/src/helpers/requests/uploadDocuments.ts +++ b/app/src/helpers/requests/uploadDocuments.ts @@ -33,7 +33,7 @@ type DocRefResponse = { }; type UpdateStateArgs = { - document: UploadDocument; + documents: UploadDocument[]; uploadingState: boolean; baseUrl: string; baseHeaders: AuthHeaders; @@ -187,21 +187,20 @@ const uploadDocuments = async ({ throw error; } }; + export const updateDocumentState = async ({ - document, + documents, uploadingState, baseUrl, baseHeaders, }: UpdateStateArgs) => { const updateUploadStateUrl = baseUrl + endpoints.UPLOAD_DOCUMENT_STATE; const body = { - files: [ - { - reference: document.ref, - type: document.docType, - fields: { Uploading: uploadingState }, - }, - ], + files: documents.map((document) => ({ + reference: document.ref, + type: document.docType, + fields: { Uploading: uploadingState }, + })), }; try { return await axios.post(updateUploadStateUrl, body, { diff --git a/app/src/pages/lloydGeorgeUploadPage/LloydGeorgeUploadPage.tsx b/app/src/pages/lloydGeorgeUploadPage/LloydGeorgeUploadPage.tsx index 1cd3258f3..4e7614a43 100644 --- a/app/src/pages/lloydGeorgeUploadPage/LloydGeorgeUploadPage.tsx +++ b/app/src/pages/lloydGeorgeUploadPage/LloydGeorgeUploadPage.tsx @@ -80,6 +80,15 @@ function LloydGeorgeUploadPage() { const hasNoVirus = documents.length && documents.every((d) => d.state === DOCUMENT_UPLOAD_STATE.CLEAN); + const setUploadStateFailed = async () => { + await updateDocumentState({ + documents: documents, + uploadingState: false, + baseUrl, + baseHeaders, + }); + }; + const confirmUpload = async () => { if (uploadSession) { setStage(LG_UPLOAD_STAGE.CONFIRMATION); @@ -105,6 +114,7 @@ function LloydGeorgeUploadPage() { navigate(routes.SESSION_EXPIRED); return; } + await setUploadStateFailed(); setStage(LG_UPLOAD_STAGE.FAILED); } } @@ -112,9 +122,11 @@ function LloydGeorgeUploadPage() { if (hasExceededUploadAttempts) { window.clearInterval(intervalTimer); + void setUploadStateFailed(); setStage(LG_UPLOAD_STAGE.FAILED); } else if (hasVirus) { window.clearInterval(intervalTimer); + void setUploadStateFailed(); setStage(LG_UPLOAD_STAGE.INFECTED); } else if (hasNoVirus && !confirmed.current) { confirmed.current = true; @@ -161,19 +173,12 @@ function LloydGeorgeUploadPage() { progress: 100, }); } catch (e) { - window.clearInterval(intervalTimer); setDocument(setDocuments, { id: document.id, state: DOCUMENT_UPLOAD_STATE.FAILED, attempts: document.attempts + 1, progress: 0, }); - await updateDocumentState({ - document, - uploadingState: false, - baseUrl, - baseHeaders, - }); } }); }; @@ -233,21 +238,25 @@ function LloydGeorgeUploadPage() { setDocuments([]); setStage(LG_UPLOAD_STAGE.SELECT); }; + const startIntervalTimer = (uploadDocuments: Array) => { return window.setInterval(() => { - uploadDocuments.forEach(async (document) => { - try { - await updateDocumentState({ - document, - uploadingState: true, - baseUrl, - baseHeaders, - }); - } catch (e) {} - }); + void setDocumentUploadingState(uploadDocuments, true); }, 120000); }; + const setDocumentUploadingState = async ( + uploadDocuments: Array, + uploadingState: boolean, + ) => { + await updateDocumentState({ + documents: uploadDocuments, + uploadingState: uploadingState, + baseUrl, + baseHeaders, + }); + }; + switch (stage) { case LG_UPLOAD_STAGE.SELECT: return ( diff --git a/lambdas/models/auth_policy.py b/lambdas/models/auth_policy.py index 27817cad1..b5205d7c2 100644 --- a/lambdas/models/auth_policy.py +++ b/lambdas/models/auth_policy.py @@ -9,6 +9,7 @@ This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ + import os import re diff --git a/lambdas/models/document_reference.py b/lambdas/models/document_reference.py index f46c9ca5a..19037a51c 100644 --- a/lambdas/models/document_reference.py +++ b/lambdas/models/document_reference.py @@ -7,6 +7,16 @@ from utils.exceptions import InvalidDocumentReferenceException +class UploadDocumentReference(BaseModel): + reference: str = Field(...) + doc_type: str = Field(..., alias="type") + fields: dict[str, bool] = Field(...) + + +class UploadDocumentReferences(BaseModel): + files: list[UploadDocumentReference] = Field(...) + + class DocumentReference(BaseModel): id: str = Field(..., alias=str(DocumentReferenceMetadataFields.ID.value)) content_type: str = Field( diff --git a/lambdas/models/pds_models.py b/lambdas/models/pds_models.py index 8e484264d..ae55779bf 100644 --- a/lambdas/models/pds_models.py +++ b/lambdas/models/pds_models.py @@ -111,9 +111,11 @@ def get_patient_details(self, nhs_number) -> PatientDetails: givenName=self.get_current_usual_name().given, familyName=self.get_current_usual_name().family, birthDate=self.birth_date, - postalCode=self.get_current_home_address().postal_code - if self.is_unrestricted() - else "", + postalCode=( + self.get_current_home_address().postal_code + if self.is_unrestricted() + else "" + ), nhsNumber=self.id, superseded=bool(nhs_number == id), restricted=not self.is_unrestricted(), @@ -128,9 +130,9 @@ def get_minimum_patient_details(self, nhs_number) -> PatientDetails: givenName=self.get_current_usual_name().given, familyName=self.get_current_usual_name().family, birthDate=self.birth_date, - generalPracticeOds=self.get_active_ods_code_for_gp() - if self.is_unrestricted() - else "", + generalPracticeOds=( + self.get_active_ods_code_for_gp() if self.is_unrestricted() else "" + ), nhsNumber=self.id, superseded=bool(nhs_number == id), restricted=not self.is_unrestricted(), diff --git a/lambdas/models/staging_metadata.py b/lambdas/models/staging_metadata.py index 94a70f114..2dcff1621 100644 --- a/lambdas/models/staging_metadata.py +++ b/lambdas/models/staging_metadata.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, ConfigDict, Field, FieldValidationInfo, field_validator +from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator from pydantic_core import PydanticCustomError METADATA_FILENAME = "metadata.csv" @@ -36,7 +36,7 @@ class MetadataFile(BaseModel): @field_validator("gp_practice_code") @classmethod def ensure_gp_practice_code_non_empty( - cls, gp_practice_code: str, info: FieldValidationInfo + cls, gp_practice_code: str, info: ValidationInfo ) -> str: if not gp_practice_code: patient_nhs_number = info.data.get("nhs_number", "") diff --git a/lambdas/requirements.txt b/lambdas/requirements.txt index 4785b3417..ba03f52ab 100644 --- a/lambdas/requirements.txt +++ b/lambdas/requirements.txt @@ -11,13 +11,13 @@ charset-normalizer==3.2.0 cryptography==42.0.4 email-validator==2.1.0.post1 fhir.resources[xml]==7.1.0 -idna==3.4 +idna==3.7 inflection==0.5.1 jmespath==1.0.1 oauthlib==3.2.2 packaging==23.0.0 pycparser==2.21 -pydantic==2.3.0 +pydantic==2.4.0 pypdf==3.17.2 python-dateutil==2.8.2 requests==2.31.0 diff --git a/lambdas/scripts/batch_update_ods_code.py b/lambdas/scripts/batch_update_ods_code.py index 596a9fd4a..c0626d179 100644 --- a/lambdas/scripts/batch_update_ods_code.py +++ b/lambdas/scripts/batch_update_ods_code.py @@ -192,9 +192,9 @@ def build_progress_dict(self, dynamodb_records: list[dict]) -> dict: progress_dict[nhs_number].doc_ref_ids.append(doc_ref_id) pds_code_at_current_row = ods_code if progress_dict[nhs_number].prev_ods_code != pds_code_at_current_row: - progress_dict[ - nhs_number - ].prev_ods_code = "[multiple ods codes in records]" + progress_dict[nhs_number].prev_ods_code = ( + "[multiple ods codes in records]" + ) self.logger.info(f"Totally {len(progress_dict)} patients found in record.") return progress_dict diff --git a/lambdas/services/create_document_reference_service.py b/lambdas/services/create_document_reference_service.py index 8a989f70b..c26c3ffa4 100644 --- a/lambdas/services/create_document_reference_service.py +++ b/lambdas/services/create_document_reference_service.py @@ -70,9 +70,9 @@ def create_document_reference_request( 400, LambdaError.CreateDocInvalidType ) - url_responses[ - document_reference.file_name - ] = self.prepare_pre_signed_url(document_reference) + url_responses[document_reference.file_name] = ( + self.prepare_pre_signed_url(document_reference) + ) if lg_documents: validate_lg_files(lg_documents, nhs_number) diff --git a/lambdas/services/document_reference_search_service.py b/lambdas/services/document_reference_search_service.py index 6ce6f4232..9f1315537 100644 --- a/lambdas/services/document_reference_search_service.py +++ b/lambdas/services/document_reference_search_service.py @@ -33,12 +33,12 @@ def get_document_references(self, nhs_number: str): for table_name in list_of_table_names: logger.info(f"Searching for results in {table_name}") - documents: list[ - DocumentReference - ] = self.fetch_documents_from_table_with_filter( - nhs_number, - table_name, - query_filter=delete_filter_expression, + documents: list[DocumentReference] = ( + self.fetch_documents_from_table_with_filter( + nhs_number, + table_name, + query_filter=delete_filter_expression, + ) ) results.extend( diff --git a/lambdas/services/update_upload_state_service.py b/lambdas/services/update_upload_state_service.py index a192b46f2..e3e5fc9c7 100644 --- a/lambdas/services/update_upload_state_service.py +++ b/lambdas/services/update_upload_state_service.py @@ -1,12 +1,17 @@ -import os from datetime import datetime, timezone from botocore.exceptions import ClientError from enums.lambda_error import LambdaError from enums.metadata_field_names import DocumentReferenceMetadataFields from enums.supported_document_types import SupportedDocumentTypes +from models.document_reference import UploadDocumentReferences +from pydantic import ValidationError from services.base.dynamo_service import DynamoDBService from utils.audit_logging_setup import LoggingService +from utils.decorators.validate_document_type import ( + doc_type_is_valid, + extract_document_type_as_enum, +) from utils.lambda_exceptions import UpdateUploadStateException logger = LoggingService(__name__) @@ -18,50 +23,41 @@ class UpdateUploadStateService: def __init__(self): self.dynamo_service = DynamoDBService() - self.lg_dynamo_table = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME") - self.arf_dynamo_table = os.getenv("DOCUMENT_STORE_DYNAMODB_NAME") def handle_update_state(self, event_body: dict): try: - files = event_body["files"] - for file in files: - doc_ref = file["reference"] - doc_type = file["type"] - uploaded = file["fields"][Fields.UPLOADING.value] - not_valid = ( - not doc_type - or not doc_ref - or not uploaded - or not isinstance(uploaded, bool) - ) - if not_valid: - raise UpdateUploadStateException( - 400, LambdaError.UpdateUploadStateValidation - ) - elif doc_type not in [ - SupportedDocumentTypes.ARF.value, - SupportedDocumentTypes.LG.value, - ]: + upload_document_references = UploadDocumentReferences.model_validate( + event_body + ) + for file in upload_document_references.files: + if not doc_type_is_valid(file.doc_type): raise UpdateUploadStateException( 400, LambdaError.UpdateUploadStateDocType ) - else: - self.update_document(doc_ref, doc_type, uploaded) + + self.update_document( + file.reference, + extract_document_type_as_enum(file.doc_type), + file.fields[str(Fields.UPLOADING.value)], + ) except (KeyError, TypeError) as e: logger.error( f"{LambdaError.UpdateUploadStateKey.to_str()} :{str(e)}", ) raise UpdateUploadStateException(400, LambdaError.UpdateUploadStateKey) + except ValidationError as e: + logger.error( + f"{LambdaError.UpdateUploadStateValidation.to_str()} :{str(e)}", + ) + raise UpdateUploadStateException( + 400, LambdaError.UpdateUploadStateValidation + ) def update_document( self, doc_ref: str, doc_type: SupportedDocumentTypes, uploaded: bool ): updated_fields = self.format_update({Fields.UPLOADING.value: uploaded}) - table = ( - self.lg_dynamo_table - if doc_type == SupportedDocumentTypes.LG.value - else self.arf_dynamo_table - ) + table = SupportedDocumentTypes.get_dynamodb_table_name(doc_type) try: self.dynamo_service.update_item( table_name=table, diff --git a/lambdas/tests/unit/helpers/data/update_upload_state.py b/lambdas/tests/unit/helpers/data/update_upload_state.py index 4e7531256..894146494 100644 --- a/lambdas/tests/unit/helpers/data/update_upload_state.py +++ b/lambdas/tests/unit/helpers/data/update_upload_state.py @@ -8,23 +8,21 @@ MOCK_DOCUMENT_REFERENCE = TEST_FILE_KEY -MOCK_LG_DOCTYPE = SupportedDocumentTypes.LG.value MOCK_LG_DOCUMENTS_REQUEST = { "files": [ { "reference": TEST_FILE_KEY, - "type": SupportedDocumentTypes.LG.value, + "type": str(SupportedDocumentTypes.LG.value), "fields": {Fields.UPLOADING.value: True}, } ] } -MOCK_ARF_DOCTYPE = SupportedDocumentTypes.ARF.value MOCK_ARF_DOCUMENTS_REQUEST = { "files": [ { "reference": TEST_FILE_KEY, - "type": SupportedDocumentTypes.ARF.value, + "type": str(SupportedDocumentTypes.ARF.value), "fields": {Fields.UPLOADING.value: True}, } ] @@ -34,7 +32,7 @@ "files": [ { "reference": TEST_FILE_KEY, - "type": SupportedDocumentTypes.ALL.value, + "type": str(SupportedDocumentTypes.ALL.value), "fields": {Fields.UPLOADING.value: True}, } ] diff --git a/lambdas/tests/unit/services/test_update_upload_state_service.py b/lambdas/tests/unit/services/test_update_upload_state_service.py index fb22d5b53..4dbf1641d 100644 --- a/lambdas/tests/unit/services/test_update_upload_state_service.py +++ b/lambdas/tests/unit/services/test_update_upload_state_service.py @@ -6,11 +6,9 @@ from services.update_upload_state_service import UpdateUploadStateService from tests.unit.helpers.data.update_upload_state import ( MOCK_ALL_DOCUMENTS_REQUEST, - MOCK_ARF_DOCTYPE, MOCK_ARF_DOCUMENTS_REQUEST, MOCK_DOCUMENT_REFERENCE, MOCK_EMPTY_LIST, - MOCK_LG_DOCTYPE, MOCK_LG_DOCUMENTS_REQUEST, MOCK_NO_DOCTYPE_REQUEST, MOCK_NO_FIELDS_REQUEST, @@ -46,7 +44,7 @@ def test_handle_update_state_with_lg_document_references( patched_service.handle_update_state(MOCK_LG_DOCUMENTS_REQUEST) mock_update_document.assert_called_with( - MOCK_DOCUMENT_REFERENCE, MOCK_LG_DOCTYPE, True + MOCK_DOCUMENT_REFERENCE, SupportedDocumentTypes.LG, True ) @@ -56,7 +54,7 @@ def test_handle_update_state_with_arf_document_references( patched_service.handle_update_state(MOCK_ARF_DOCUMENTS_REQUEST) mock_update_document.assert_called_with( - MOCK_DOCUMENT_REFERENCE, MOCK_ARF_DOCTYPE, True + MOCK_DOCUMENT_REFERENCE, SupportedDocumentTypes.ARF, True ) From f21ddcf6ff9f96fb446ef184fe6ff52c2d1404dd Mon Sep 17 00:00:00 2001 From: NogaNHS <127490765+NogaNHS@users.noreply.github.com> Date: Thu, 2 May 2024 14:19:52 +0100 Subject: [PATCH 2/6] PRMDR 0000 - bugs fixes (#358) * LG validator changes following pilot bugs --- lambdas/models/pds_models.py | 31 +-- lambdas/scripts/batch_update_ods_code.py | 6 +- .../create_document_reference_service.py | 6 +- .../document_reference_search_service.py | 12 +- .../helpers/data/pds/pds_patient_response.py | 7 + lambdas/tests/unit/models/test_pds_models.py | 21 ++ .../unit/utils/test_lloyd_george_validator.py | 188 ++++++++++++++++-- lambdas/utils/lloyd_george_validator.py | 58 ++++-- sonar-project.properties | 2 +- 9 files changed, 271 insertions(+), 60 deletions(-) diff --git a/lambdas/models/pds_models.py b/lambdas/models/pds_models.py index ae55779bf..6aa54eaed 100644 --- a/lambdas/models/pds_models.py +++ b/lambdas/models/pds_models.py @@ -14,14 +14,14 @@ class Address(BaseModel): model_config = conf use: str - period: Period + period: Optional[Period] = None postal_code: Optional[str] = "" class Name(BaseModel): use: str - period: Period - given: list[str] + period: Optional[Period] = None + given: Optional[list[str]] = None family: str @@ -36,21 +36,21 @@ class Meta(BaseModel): class GPIdentifier(BaseModel): - system: Optional[str] + system: Optional[str] = "" value: str - period: Optional[Period] + period: Optional[Period] = None class GeneralPractitioner(BaseModel): - id: Optional[str] - type: Optional[str] + id: Optional[str] = "" + type: Optional[str] = "" identifier: GPIdentifier class PatientDetails(BaseModel): model_config = conf - given_Name: Optional[list[str]] = [] + given_name: Optional[list[str]] = [""] family_name: Optional[str] = "" birth_date: Optional[date] = None postal_code: Optional[str] = "" @@ -69,7 +69,7 @@ class Patient(BaseModel): address: Optional[list[Address]] = [] name: list[Name] meta: Meta - general_practitioner: Optional[list[GeneralPractitioner]] = [] + general_practitioner: Optional[list[GeneralPractitioner]] = None def get_security(self) -> Security: security = self.meta.security[0] if self.meta.security[0] else None @@ -90,16 +90,17 @@ def get_current_usual_name(self) -> [Optional[Name]]: return entry def get_current_home_address(self) -> Optional[Address]: - if self.is_unrestricted(): + if self.is_unrestricted() and self.address: for entry in self.address: if entry.use.lower() == "home": return entry def get_active_ods_code_for_gp(self) -> str: - for entry in self.general_practitioner: - gp_end_date = entry.identifier.period.end - if not gp_end_date or gp_end_date >= date.today(): - return entry.identifier.value + if self.general_practitioner: + for entry in self.general_practitioner: + gp_end_date = entry.identifier.period.end + if not gp_end_date or gp_end_date >= date.today(): + return entry.identifier.value return "" def get_is_active_status(self) -> bool: @@ -113,7 +114,7 @@ def get_patient_details(self, nhs_number) -> PatientDetails: birthDate=self.birth_date, postalCode=( self.get_current_home_address().postal_code - if self.is_unrestricted() + if self.is_unrestricted() and self.address else "" ), nhsNumber=self.id, diff --git a/lambdas/scripts/batch_update_ods_code.py b/lambdas/scripts/batch_update_ods_code.py index c0626d179..596a9fd4a 100644 --- a/lambdas/scripts/batch_update_ods_code.py +++ b/lambdas/scripts/batch_update_ods_code.py @@ -192,9 +192,9 @@ def build_progress_dict(self, dynamodb_records: list[dict]) -> dict: progress_dict[nhs_number].doc_ref_ids.append(doc_ref_id) pds_code_at_current_row = ods_code if progress_dict[nhs_number].prev_ods_code != pds_code_at_current_row: - progress_dict[nhs_number].prev_ods_code = ( - "[multiple ods codes in records]" - ) + progress_dict[ + nhs_number + ].prev_ods_code = "[multiple ods codes in records]" self.logger.info(f"Totally {len(progress_dict)} patients found in record.") return progress_dict diff --git a/lambdas/services/create_document_reference_service.py b/lambdas/services/create_document_reference_service.py index c26c3ffa4..8a989f70b 100644 --- a/lambdas/services/create_document_reference_service.py +++ b/lambdas/services/create_document_reference_service.py @@ -70,9 +70,9 @@ def create_document_reference_request( 400, LambdaError.CreateDocInvalidType ) - url_responses[document_reference.file_name] = ( - self.prepare_pre_signed_url(document_reference) - ) + url_responses[ + document_reference.file_name + ] = self.prepare_pre_signed_url(document_reference) if lg_documents: validate_lg_files(lg_documents, nhs_number) diff --git a/lambdas/services/document_reference_search_service.py b/lambdas/services/document_reference_search_service.py index 9f1315537..6ce6f4232 100644 --- a/lambdas/services/document_reference_search_service.py +++ b/lambdas/services/document_reference_search_service.py @@ -33,12 +33,12 @@ def get_document_references(self, nhs_number: str): for table_name in list_of_table_names: logger.info(f"Searching for results in {table_name}") - documents: list[DocumentReference] = ( - self.fetch_documents_from_table_with_filter( - nhs_number, - table_name, - query_filter=delete_filter_expression, - ) + documents: list[ + DocumentReference + ] = self.fetch_documents_from_table_with_filter( + nhs_number, + table_name, + query_filter=delete_filter_expression, ) results.extend( diff --git a/lambdas/tests/unit/helpers/data/pds/pds_patient_response.py b/lambdas/tests/unit/helpers/data/pds/pds_patient_response.py index acefdd7a4..2eb44a5b5 100644 --- a/lambdas/tests/unit/helpers/data/pds/pds_patient_response.py +++ b/lambdas/tests/unit/helpers/data/pds/pds_patient_response.py @@ -1,3 +1,5 @@ +import copy + PDS_PATIENT = { "resourceType": "Patient", "id": "9000000009", @@ -524,3 +526,8 @@ }, }, } +PDS_PATIENT_WITH_MIDDLE_NAME = copy.deepcopy(PDS_PATIENT) +PDS_PATIENT_WITH_MIDDLE_NAME["name"][0]["given"].append("Jake") + +PDS_PATIENT_WITHOUT_ADDRESS = copy.deepcopy(PDS_PATIENT) +PDS_PATIENT_WITHOUT_ADDRESS.pop("address") diff --git a/lambdas/tests/unit/models/test_pds_models.py b/lambdas/tests/unit/models/test_pds_models.py index b4e8b304b..4e0d73287 100644 --- a/lambdas/tests/unit/models/test_pds_models.py +++ b/lambdas/tests/unit/models/test_pds_models.py @@ -5,6 +5,7 @@ PDS_PATIENT_RESTRICTED, PDS_PATIENT_WITH_GP_END_DATE, PDS_PATIENT_WITHOUT_ACTIVE_GP, + PDS_PATIENT_WITHOUT_ADDRESS, ) from tests.unit.helpers.data.pds.utils import create_patient from utils.utilities import validate_nhs_number @@ -109,3 +110,23 @@ def test_not_raise_error_when_gp_end_date_is_in_the_future(): patient.get_minimum_patient_details(patient.id) except ValueError: assert False, "No active GP practice for the patient" + + +def test_get_minimum_patient_details_missing_address(): + patient = create_patient(PDS_PATIENT_WITHOUT_ADDRESS) + + expected_patient_details = PatientDetails( + givenName=["Jane"], + familyName="Smith", + birthDate="2010-10-22", + postalCode="", + nhsNumber="9000000009", + superseded=False, + restricted=False, + generalPracticeOds="Y12345", + active=True, + ) + + result = patient.get_patient_details(patient.id) + + assert expected_patient_details == result diff --git a/lambdas/tests/unit/utils/test_lloyd_george_validator.py b/lambdas/tests/unit/utils/test_lloyd_george_validator.py index 0b941c86d..4f77a47e7 100644 --- a/lambdas/tests/unit/utils/test_lloyd_george_validator.py +++ b/lambdas/tests/unit/utils/test_lloyd_george_validator.py @@ -11,7 +11,10 @@ TEST_NHS_NUMBER_FOR_BULK_UPLOAD, TEST_STAGING_METADATA_WITH_INVALID_FILENAME, ) -from tests.unit.helpers.data.pds.pds_patient_response import PDS_PATIENT +from tests.unit.helpers.data.pds.pds_patient_response import ( + PDS_PATIENT, + PDS_PATIENT_WITH_MIDDLE_NAME, +) from tests.unit.models.test_document_reference import MOCK_DOCUMENT_REFERENCE from utils.common_query_filters import NotDeleted from utils.exceptions import ( @@ -25,6 +28,7 @@ check_for_file_names_agrees_with_each_other, check_for_number_of_files_match_expected, check_for_patient_already_exist_in_repo, + check_pds_response_status, extract_info_from_filename, get_allowed_ods_codes, getting_patient_info_from_pds, @@ -33,6 +37,8 @@ validate_lg_file_names, validate_lg_file_type, validate_lg_files, + validate_patient_date_of_birth, + validate_patient_name, ) @@ -224,12 +230,15 @@ def test_files_for_different_patients(): assert str(e.value) == "File names does not match with each other" -def test_validate_nhs_id_with_pds_service(mocker, mock_pds_patient_details): +def test_validate_nhs_id_with_pds_service(mock_pds_patient_details): lg_file_list = [ "1of2_Lloyd_George_Record_[Jane Smith]_[9000000009]_[22-10-2010].pdf", "2of2_Lloyd_George_Record_[Jane Smith]_[9000000009]_[22-10-2010].pdf", ] - validate_filename_with_patient_details(lg_file_list, mock_pds_patient_details) + try: + validate_filename_with_patient_details(lg_file_list, mock_pds_patient_details) + except LGInvalidFilesException: + assert False def test_mismatch_nhs_id(mocker): @@ -248,17 +257,112 @@ def test_mismatch_nhs_id(mocker): validate_lg_file_names(lg_file_list, "9000000009") -def test_mismatch_name_with_pds_service(mocker, mock_pds_patient_details): +def test_mismatch_name_with_pds_service(mock_pds_patient_details): lg_file_list = [ - "1of2_Lloyd_George_Record_[Jane Plain Smith]_[9000000009]_[22-10-2010].pdf", - "2of2_Lloyd_George_Record_[Jane Plain Smith]_[9000000009]_[22-10-2010].pdf", + "1of2_Lloyd_George_Record_[Jake Plain Smith]_[9000000009]_[22-10-2010].pdf", + "2of2_Lloyd_George_Record_[Jake Plain Smith]_[9000000009]_[22-10-2010].pdf", ] with pytest.raises(LGInvalidFilesException): validate_filename_with_patient_details(lg_file_list, mock_pds_patient_details) -def test_mismatch_dob_with_pds_service(mocker, mock_pds_patient_details): +def test_order_names_with_pds_service(): + lg_file_list = [ + "1of2_Lloyd_George_Record_[Jake Jane Smith]_[9000000009]_[22-10-2010].pdf", + "2of2_Lloyd_George_Record_[Jake Jane Smith]_[9000000009]_[22-10-2010].pdf", + ] + patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) + patient_details = patient.get_minimum_patient_details("9000000009") + try: + validate_filename_with_patient_details(lg_file_list, patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_name_with_correct_name(mock_pds_patient_details): + lg_file_patient_name = "Jane Smith" + try: + validate_patient_name(lg_file_patient_name, mock_pds_patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_name_with_file_missing_middle_name(): + lg_file_patient_name = "Jane Smith" + patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) + patient_details = patient.get_minimum_patient_details("9000000009") + try: + validate_patient_name(lg_file_patient_name, patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_name_with_additional_middle_name_in_file_mismatching_pds(): + lg_file_patient_name = "Jane David Smith" + patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) + patient_details = patient.get_minimum_patient_details("9000000009") + try: + validate_patient_name(lg_file_patient_name, patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_name_with_additional_middle_name_in_file_but_none_in_pds( + mock_pds_patient_details, +): + lg_file_patient_name = "Jane David Smith" + try: + validate_patient_name(lg_file_patient_name, mock_pds_patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_name_with_wrong_order(): + lg_file_patient_name = "Jake Jane Smith" + patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) + patient_details = patient.get_minimum_patient_details("9000000009") + try: + validate_patient_name(lg_file_patient_name, patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_name_with_wrong_first_name(mock_pds_patient_details): + lg_file_patient_name = "John Smith" + with pytest.raises(LGInvalidFilesException): + validate_patient_name(lg_file_patient_name, mock_pds_patient_details) + + +def test_validate_name_with_wrong_family_name(mock_pds_patient_details): + lg_file_patient_name = "Jane Johnson" + with pytest.raises(LGInvalidFilesException): + validate_patient_name(lg_file_patient_name, mock_pds_patient_details) + + +def test_validate_name_without_given_name(mock_pds_patient_details): + lg_file_patient_name = "Jane Smith" + mock_pds_patient_details.given_name = [""] + try: + validate_patient_name(lg_file_patient_name, mock_pds_patient_details) + except LGInvalidFilesException: + assert False + + +def test_missing_middle_name_names_with_pds_service(): + lg_file_list = [ + "1of2_Lloyd_George_Record_[Jane Smith]_[9000000009]_[22-10-2010].pdf", + "2of2_Lloyd_George_Record_[Jane Smith]_[9000000009]_[22-10-2010].pdf", + ] + patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) + patient_details = patient.get_minimum_patient_details("9000000009") + try: + validate_filename_with_patient_details(lg_file_list, patient_details) + except LGInvalidFilesException: + assert False + + +def test_mismatch_dob_with_pds_service(mock_pds_patient_details): lg_file_list = [ "1of2_Lloyd_George_Record_[Jane Plain Smith]_[9000000009]_[14-01-2000].pdf", "2of2_Lloyd_George_Record_[Jane Plain Smith]_[9000000009]_[14-01-2000].pdf", @@ -268,7 +372,31 @@ def test_mismatch_dob_with_pds_service(mocker, mock_pds_patient_details): validate_filename_with_patient_details(lg_file_list, mock_pds_patient_details) -def test_patient_not_found_with_pds_service(mocker, mock_pds_call): +def test_validate_date_of_birth_when_mismatch_dob_with_pds_service( + mock_pds_patient_details, +): + file_date_of_birth = "14-01-2000" + + with pytest.raises(LGInvalidFilesException): + validate_patient_date_of_birth(file_date_of_birth, mock_pds_patient_details) + + +def test_validate_date_of_birth_valid_with_pds_service(mock_pds_patient_details): + file_date_of_birth = "22-10-2010" + try: + validate_patient_date_of_birth(file_date_of_birth, mock_pds_patient_details) + except LGInvalidFilesException: + assert False + + +def test_validate_date_of_birth_none_with_pds_service(mock_pds_patient_details): + file_date_of_birth = "22-10-2010" + mock_pds_patient_details.birth_date = None + with pytest.raises(LGInvalidFilesException): + validate_patient_date_of_birth(file_date_of_birth, mock_pds_patient_details) + + +def test_patient_not_found_with_pds_service(mock_pds_call): response = Response() response.status_code = 404 @@ -281,7 +409,7 @@ def test_patient_not_found_with_pds_service(mocker, mock_pds_call): mock_pds_call.assert_called_with(nhs_number="9000000009", retry_on_expired=True) -def test_bad_request_with_pds_service(mocker, mock_pds_call): +def test_bad_request_with_pds_service(mock_pds_call): response = Response() response.status_code = 400 @@ -294,9 +422,7 @@ def test_bad_request_with_pds_service(mocker, mock_pds_call): mock_pds_call.assert_called_with(nhs_number="9000000009", retry_on_expired=True) -def test_validate_with_pds_service_raise_PdsTooManyRequestsException( - mocker, mock_pds_call -): +def test_validate_with_pds_service_raise_pds_too_many_requests_exception(mock_pds_call): response = Response() response.status_code = 429 response._content = b"Too Many Requests" @@ -308,6 +434,40 @@ def test_validate_with_pds_service_raise_PdsTooManyRequestsException( mock_pds_call.assert_called_with(nhs_number="9000000009", retry_on_expired=True) +def test_check_pds_response_429_status_raise_too_many_requests_exception(): + response = Response() + response.status_code = 429 + + with pytest.raises(PdsTooManyRequestsException): + check_pds_response_status(response) + + +def test_check_pds_response_404_status_raise_lg_invalid_files_exception(): + response = Response() + response.status_code = 404 + + with pytest.raises(LGInvalidFilesException): + check_pds_response_status(response) + + +def test_check_pds_response_4xx_or_5xx_status_raise_lg_invalid_files_exception(): + response = Response() + response.status_code = 500 + + with pytest.raises(LGInvalidFilesException): + check_pds_response_status(response) + + +def test_check_pds_response_200_status_not_raise_exception(): + response = Response() + response.status_code = 200 + + try: + check_pds_response_status(response) + except LGInvalidFilesException: + assert False + + def test_check_for_patient_already_exist_in_repo_return_none_when_patient_record_not_exist( set_env, mock_fetch_available_document_references_by_type ): @@ -341,7 +501,7 @@ def test_check_check_for_patient_already_exist_in_repo_raise_exception_when_pati ) -def test_validate_bulk_files_raises_PatientRecordAlreadyExistException_when_patient_record_already_exists( +def test_validate_bulk_files_raises_patient_record_already_exist_exception_when_patient_record_already_exists( set_env, mocker ): mocker.patch( @@ -364,7 +524,7 @@ def test_get_allowed_ods_codes_return_a_list_of_ods_codes(mock_get_ssm_parameter assert actual == expected -def test_get_allowed_ods_codes_can_handle_the_ALL_option(mock_get_ssm_parameter): +def test_get_allowed_ods_codes_can_handle_the_all_option(mock_get_ssm_parameter): mock_get_ssm_parameter.return_value = "ALL" expected = ["ALL"] diff --git a/lambdas/utils/lloyd_george_validator.py b/lambdas/utils/lloyd_george_validator.py index 500f7c500..49732d23a 100644 --- a/lambdas/utils/lloyd_george_validator.py +++ b/lambdas/utils/lloyd_george_validator.py @@ -140,31 +140,56 @@ def validate_filename_with_patient_details( ): try: file_name_info = extract_info_from_filename(file_name_list[0]) - patient_name = file_name_info["patient_name"] - date_of_birth = file_name_info["date_of_birth"] - - date_of_birth = datetime.datetime.strptime(date_of_birth, "%d-%m-%Y").date() - if patient_details.birth_date != date_of_birth: - raise LGInvalidFilesException("Patient DoB does not match our records") - patient_full_name = ( - " ".join([name for name in patient_details.given_Name]) - + " " - + patient_details.family_name - ) - logger.info("Verifying patient name against the record in PDS...") - - if not names_are_matching(patient_name, patient_full_name): - raise LGInvalidFilesException("Patient name does not match our records") + file_patient_name = file_name_info["patient_name"] + file_date_of_birth = file_name_info["date_of_birth"] + validate_patient_date_of_birth(file_date_of_birth, patient_details) + validate_patient_name(file_patient_name, patient_details) except (ClientError, ValueError) as e: logger.error(e) raise LGInvalidFilesException(e) +def validate_patient_name(file_patient_name, pds_patient_details): + logger.info("Verifying patient name against the record in PDS...") + patient_name_split = file_patient_name.split(" ") + file_patient_first_name = patient_name_split[0] + file_patient_last_name = patient_name_split[-1] + is_file_first_name_in_patient_details = False + for patient_name in pds_patient_details.given_name: + if ( + names_are_matching(file_patient_first_name, patient_name) + or not patient_name + ): + is_file_first_name_in_patient_details = True + break + + if not is_file_first_name_in_patient_details or not names_are_matching( + file_patient_last_name, pds_patient_details.family_name + ): + raise LGInvalidFilesException("Patient name does not match our records") + + +def validate_patient_date_of_birth(file_date_of_birth, pds_patient_details): + date_of_birth = datetime.datetime.strptime(file_date_of_birth, "%d-%m-%Y").date() + if ( + not pds_patient_details.birth_date + or pds_patient_details.birth_date != date_of_birth + ): + raise LGInvalidFilesException("Patient DoB does not match our records") + + def getting_patient_info_from_pds(nhs_number: str) -> PatientDetails: pds_service_class = get_pds_service() pds_service = pds_service_class(SSMService()) pds_response = pds_service.pds_request(nhs_number=nhs_number, retry_on_expired=True) + check_pds_response_status(pds_response) + patient = Patient.model_validate(pds_response.json()) + patient_details = patient.get_minimum_patient_details(nhs_number) + return patient_details + + +def check_pds_response_status(pds_response): if pds_response.status_code == 429: logger.error("Got 429 Too Many Requests error from PDS.") raise PdsTooManyRequestsException( @@ -178,9 +203,6 @@ def getting_patient_info_from_pds(nhs_number: str) -> PatientDetails: except HTTPError as e: logger.error(e) raise LGInvalidFilesException("Failed to retrieve patient data from PDS") - patient = Patient.model_validate(pds_response.json()) - patient_details = patient.get_minimum_patient_details(nhs_number) - return patient_details def get_allowed_ods_codes() -> list[str]: diff --git a/sonar-project.properties b/sonar-project.properties index 849bb345e..26a730cba 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -14,7 +14,7 @@ sonar.python.coverage.reportPaths=lambdas/coverage.xml sonar.sources=lambdas/,app/src/ sonar.tests=lambdas/tests/,app/src/ -sonar.exclusions=**/*.test.tsx,app/src/helpers/test/,**/*.story.tsx,lambdas/scripts/,**/*.test.ts,**/*.story.ts +sonar.exclusions=**/*.test.tsx,app/src/helpers/test/,**/*.story.tsx,lambdas/scripts/,**/*.test.ts,**/*.story.ts,lambdas/tests/ sonar.test.inclusions=**/*.test.tsx,app/src/helpers/test/,**/*.test.ts # Encoding of the source code. Default is default system encoding From a39ca85bb5f362f84e5f6a39f80a934cd4f708cf Mon Sep 17 00:00:00 2001 From: Joe Fong <127404525+joefong-nhs@users.noreply.github.com> Date: Thu, 2 May 2024 16:17:53 +0100 Subject: [PATCH 3/6] PRMDR-808 Side Menu for Non-BSOL LG record page (#357) * [PRMDR-849] Rectify side menu for BSOL GP ADMIN * [PRMDR-849] Fix wrong class name & wrong heading number for LG upload panel * [PRMDR-849] Address wrong heading error (replace wrong h4 with label of bold and size=m) * [PRMDR-849] Refactor RecordMenuCard and related unit tests * [PRMDR-849] Minor change * [PRMDR-849] edit test name * [PRMDR-849] try address github action specific cypress test issue * [PRMDR-849] minor fix for clarity * [PRMDR-808] Update side menu panel for non-bsol * [PRMDR-808] Fix broken unit tests * [PRMDR-808] Remove commented out test, add unit test for new part, update key to match cypress test * [PRMDR-808] Remove duplicated h1 header * [PRMDR-808] Fixed tests related to H1 heading * [PRMDR-808] some refactoring to matchup name changes in prev ticket * [PRMDR-808] Remove unused props / imports, remove non-applicable old tests * [PRMDR-808] remove unused const assignment * [PRMDR-808] Improve test coverage, rephrase tests name to reflect design change: green button --> side menu button --- .../LloydGeorgeRecordDetails.test.tsx | 120 +----------------- .../LloydGeorgeRecordDetails.tsx | 44 +------ .../LloydGeorgeRecordStage.test.tsx | 42 ++++-- .../LloydGeorgeRecordStage.tsx | 33 +++-- .../recordMenuCard/RecordMenuCard.test.tsx | 32 ++++- .../generic/recordMenuCard/RecordMenuCard.tsx | 50 +++++--- .../types/blocks/lloydGeorgeActions.test.ts | 80 ++++++++++-- app/src/types/blocks/lloydGeorgeActions.ts | 66 ++++++++-- 8 files changed, 246 insertions(+), 221 deletions(-) diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx index 4306e2072..fcb4aebc4 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx @@ -2,39 +2,18 @@ import { render, screen } from '@testing-library/react'; import LgRecordDetails, { Props } from './LloydGeorgeRecordDetails'; import { buildLgSearchResult } from '../../../../helpers/test/testBuilders'; import formatFileSize from '../../../../helpers/utils/formatFileSize'; -import { REPOSITORY_ROLE } from '../../../../types/generic/authRole'; -import useRole from '../../../../helpers/hooks/useRole'; -import { lloydGeorgeRecordLinks } from '../../../../types/blocks/lloydGeorgeActions'; -import { LinkProps } from 'react-router-dom'; -import useIsBSOL from '../../../../helpers/hooks/useIsBSOL'; -jest.mock('../../../../helpers/hooks/useRole'); -jest.mock('../../../../helpers/hooks/useIsBSOL'); - -const mockedUseNavigate = jest.fn(); const mockPdf = buildLgSearchResult(); -const mockSetDownloadRemoveButtonClicked = jest.fn(); -const mockSetError = jest.fn(); -const mockSetFocus = jest.fn(); -const mockedUseRole = useRole as jest.Mock; -const mockedUseIsBSOL = useIsBSOL as jest.Mock; - -jest.mock('react-router', () => ({ - useNavigate: () => mockedUseNavigate, -})); -jest.mock('react-router-dom', () => ({ - __esModule: true, - Link: (props: LinkProps) => , -})); describe('LloydGeorgeRecordDetails', () => { beforeEach(() => { - mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); process.env.REACT_APP_ENVIRONMENT = 'jest'; }); + afterEach(() => { jest.clearAllMocks(); }); + describe('Rendering', () => { it('renders the record details component', () => { renderComponent(); @@ -47,105 +26,14 @@ describe('LloydGeorgeRecordDetails', () => { expect(screen.getByText('File format: PDF')).toBeInTheDocument(); }); }); - - describe('Unauthorised', () => { - const unauthorisedLinks = lloydGeorgeRecordLinks.filter((a) => - Array.isArray(a.unauthorised), - ); - - it.each(unauthorisedLinks)( - "does not render actionLink '$label' if role is unauthorised", - async (action) => { - const [unauthorisedRole] = action.unauthorised ?? []; - mockedUseRole.mockReturnValue(unauthorisedRole); - - renderComponent(); - - expect(screen.queryByText(`Select an action...`)).not.toBeInTheDocument(); - expect(screen.queryByTestId('actions-menu')).not.toBeInTheDocument(); - }, - ); - - it.each(unauthorisedLinks)( - "does not render actionLink '$label' for GP Clinical Role", - async (action) => { - expect(action.unauthorised).toContain(REPOSITORY_ROLE.GP_CLINICAL); - }, - ); - }); - - describe('GP admin non BSOL user', () => { - it('renders the record details component with button', () => { - mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); - mockedUseIsBSOL.mockReturnValue(false); - renderComponent(); - - expect(screen.getByText(`Last updated: ${mockPdf.last_updated}`)).toBeInTheDocument(); - expect(screen.getByText(`${mockPdf.number_of_files} files`)).toBeInTheDocument(); - expect( - screen.getByText(`File size: ${formatFileSize(mockPdf.total_file_size_in_byte)}`), - ).toBeInTheDocument(); - expect(screen.getByText('File format: PDF')).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Download and remove record' }), - ).toBeInTheDocument(); - - expect(screen.queryByText(`Select an action...`)).not.toBeInTheDocument(); - expect(screen.queryByTestId('actions-menu')).not.toBeInTheDocument(); - }); - - it('set downloadRemoveButtonClicked to true when button is clicked', () => { - mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); - mockedUseIsBSOL.mockReturnValue(false); - renderComponent(); - - const button = screen.getByRole('button', { name: 'Download and remove record' }); - - button.click(); - - expect(mockSetDownloadRemoveButtonClicked).toHaveBeenCalledWith(true); - }); - - it('calls setFocus and setError when the button is clicked again after warning box shown up', () => { - mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); - mockedUseIsBSOL.mockReturnValue(false); - renderComponent({ downloadRemoveButtonClicked: true }); - - const button = screen.getByRole('button', { name: 'Download and remove record' }); - - button.click(); - - expect(mockSetError).toHaveBeenCalledWith('confirmDownloadRemove', { - type: 'custom', - message: 'true', - }); - expect(mockSetFocus).toHaveBeenCalledWith('confirmDownloadRemove'); - }); - }); }); -type mockedProps = Omit< - Props, - 'setStage' | 'stage' | 'setDownloadRemoveButtonClicked' | 'setError' | 'setFocus' ->; -const TestApp = (props: mockedProps) => { - return ( - - ); -}; - const renderComponent = (propsOverride?: Partial) => { - const props: mockedProps = { + const props: Props = { lastUpdated: mockPdf.last_updated, numberOfFiles: mockPdf.number_of_files, totalFileSizeInByte: mockPdf.total_file_size_in_byte, - downloadRemoveButtonClicked: false, ...propsOverride, }; - return render(); + return render(); }; diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx index 8ff53b8f7..54a99773f 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx @@ -1,42 +1,13 @@ -import React, { Dispatch, SetStateAction } from 'react'; +import React from 'react'; import formatFileSize from '../../../../helpers/utils/formatFileSize'; -import { Button } from 'nhsuk-react-components'; -import useRole from '../../../../helpers/hooks/useRole'; -import { FieldValues, UseFormSetError, UseFormSetFocus } from 'react-hook-form'; -import useIsBSOL from '../../../../helpers/hooks/useIsBSOL'; -import { REPOSITORY_ROLE } from '../../../../types/generic/authRole'; export type Props = { lastUpdated: string; numberOfFiles: number; totalFileSizeInByte: number; - setDownloadRemoveButtonClicked: Dispatch>; - downloadRemoveButtonClicked: boolean; - setError: UseFormSetError; - setFocus: UseFormSetFocus; }; -function LloydGeorgeRecordDetails({ - lastUpdated, - numberOfFiles, - totalFileSizeInByte, - setDownloadRemoveButtonClicked, - downloadRemoveButtonClicked, - setError, - setFocus, -}: Props) { - const role = useRole(); - const isBSOL = useIsBSOL(); - const userIsGpAdminNonBSOL = role === REPOSITORY_ROLE.GP_ADMIN && !isBSOL; - - const handleDownloadAndRemoveRecordButton = () => { - if (downloadRemoveButtonClicked) { - setError('confirmDownloadRemove', { type: 'custom', message: 'true' }); - } - setFocus('confirmDownloadRemove'); - setDownloadRemoveButtonClicked(true); - }; - +function LloydGeorgeRecordDetails({ lastUpdated, numberOfFiles, totalFileSizeInByte }: Props) { return (
@@ -52,17 +23,6 @@ function LloydGeorgeRecordDetails({ {' |'}
- {userIsGpAdminNonBSOL && ( -
- -
- )} ); } diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx index c52c7060f..3835bf448 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx @@ -136,12 +136,12 @@ describe('LloydGeorgeRecordStage', () => { }; const showConfirmationMessage = async () => { - const greenDownloadButton = screen.getByRole('button', { - name: 'Download and remove record', + const sideMenuDownloadButton = screen.getByRole('button', { + name: 'Download and remove files', }); act(() => { - userEvent.click(greenDownloadButton); + userEvent.click(sideMenuDownloadButton); }); await waitFor(() => { expect( @@ -166,15 +166,15 @@ describe('LloydGeorgeRecordStage', () => { expect(screen.getByText('Before downloading')).toBeInTheDocument(); expect(screen.getByText('Available records')).toBeInTheDocument(); expect( - screen.getByRole('button', { name: 'Download and remove record' }), + screen.getByRole('button', { name: 'Download and remove files' }), ).toBeInTheDocument(); }); - it('clicking the green download button should show confirmation message, checkbox, red download button and cancel button', async () => { + it('clicking the side menu download button should show confirmation message, checkbox, red download button and cancel button', async () => { renderComponentForNonBSOLGPAdmin(); const downloadButton = screen.getByRole('button', { - name: 'Download and remove record', + name: 'Download and remove files', }); act(() => { @@ -222,6 +222,27 @@ describe('LloydGeorgeRecordStage', () => { expect(mockSetStage).not.toBeCalled(); }); + it('when checkbox is unchecked, clicking "Download and remove" button twice will bring up a warning callout message', async () => { + renderComponentForNonBSOLGPAdmin(); + await showConfirmationMessage(); + + act(() => { + userEvent.click( + screen.getByRole('button', { + name: 'Download and remove files', + }), + ); + }); + + expect( + screen.getByText('You must confirm if you want to download and remove this record'), + ).toBeInTheDocument(); + expect( + screen.getByText('Confirm if you want to download and remove this record'), + ).toBeInTheDocument(); + expect(mockSetStage).not.toBeCalled(); + }); + it('when checkbox is checked, clicking red download button should proceed to download and delete process', async () => { renderComponentForNonBSOLGPAdmin(); await showConfirmationMessage(); @@ -280,9 +301,8 @@ describe('LloydGeorgeRecordStage', () => { renderComponent(); expect(screen.queryByText('Before downloading')).not.toBeInTheDocument(); - expect(screen.queryByText('Available records')).not.toBeInTheDocument(); expect( - screen.queryByRole('button', { name: 'Download and remove record' }), + screen.queryByRole('button', { name: 'Download and remove files' }), ).not.toBeInTheDocument(); }); @@ -293,9 +313,8 @@ describe('LloydGeorgeRecordStage', () => { renderComponent(); expect(screen.queryByText('Before downloading')).not.toBeInTheDocument(); - expect(screen.queryByText('Available records')).not.toBeInTheDocument(); expect( - screen.queryByRole('button', { name: 'Download and remove record' }), + screen.queryByRole('button', { name: 'Download and remove files' }), ).not.toBeInTheDocument(); }); @@ -306,9 +325,8 @@ describe('LloydGeorgeRecordStage', () => { renderComponent(); expect(screen.queryByText('Before downloading')).not.toBeInTheDocument(); - expect(screen.queryByText('Available records')).not.toBeInTheDocument(); expect( - screen.queryByRole('button', { name: 'Download and remove record' }), + screen.queryByRole('button', { name: 'Download and remove files' }), ).not.toBeInTheDocument(); }); }); diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx index 134f2743d..3e43b756b 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx @@ -23,7 +23,10 @@ import ErrorBox from '../../../layout/errorBox/ErrorBox'; import { useForm } from 'react-hook-form'; import { InputRef } from '../../../../types/generic/inputRef'; import BackButton from '../../../generic/backButton/BackButton'; -import { getRecordActionLinksAllowedForRole } from '../../../../types/blocks/lloydGeorgeActions'; +import { + getBSOLUserRecordActionLinks, + getNonBSOLUserRecordActionLinks, +} from '../../../../types/blocks/lloydGeorgeActions'; import RecordCard from '../../../generic/recordCard/RecordCard'; import RecordMenuCard from '../../../generic/recordMenuCard/RecordMenuCard'; import useTitle from '../../../../helpers/hooks/useTitle'; @@ -59,6 +62,14 @@ function LloydGeorgeRecordStage({ required: true, }); + const handleDownloadAndRemoveRecordButton = () => { + if (downloadRemoveButtonClicked) { + setError('confirmDownloadRemove', { type: 'custom', message: 'true' }); + } + setFocus('confirmDownloadRemove'); + setDownloadRemoveButtonClicked(true); + }; + const nhsNumber: string = patientDetails?.nhsNumber ?? ''; const formattedNhsNumber = formatNhsNumber(nhsNumber); @@ -67,10 +78,14 @@ function LloydGeorgeRecordStage({ const userIsGpAdminNonBSOL = role === REPOSITORY_ROLE.GP_ADMIN && !isBSOL; const hasRecordInStorage = downloadStage === DOWNLOAD_STAGE.SUCCEEDED; - const recordLinksToShow = getRecordActionLinksAllowedForRole({ - role, - hasRecordInRepo: hasRecordInStorage, - }); + + const recordLinksToShow = isBSOL + ? getBSOLUserRecordActionLinks({ role, hasRecordInStorage }) + : getNonBSOLUserRecordActionLinks({ + role, + hasRecordInStorage, + onClickFunctionForDownloadAndRemove: handleDownloadAndRemoveRecordButton, + }); const showMenu = recordLinksToShow.length > 0; const handleConfirmDownloadAndRemoveButton = () => { @@ -89,11 +104,6 @@ function LloydGeorgeRecordStage({ lastUpdated, numberOfFiles, totalFileSizeInByte, - setStage, - setDownloadRemoveButtonClicked, - downloadRemoveButtonClicked, - setError, - setFocus, }; return ; } else { @@ -127,7 +137,7 @@ function LloydGeorgeRecordStage({ ) : ( )} -

Available Records

+

{pageHeader}

{!fullScreen && userIsGpAdminNonBSOL && (
)} -

{pageHeader}

)}
diff --git a/app/src/components/generic/recordMenuCard/RecordMenuCard.test.tsx b/app/src/components/generic/recordMenuCard/RecordMenuCard.test.tsx index e7218e50e..bf5ffdb06 100644 --- a/app/src/components/generic/recordMenuCard/RecordMenuCard.test.tsx +++ b/app/src/components/generic/recordMenuCard/RecordMenuCard.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import RecordMenuCard from './RecordMenuCard'; import useRole from '../../../helpers/hooks/useRole'; -import { PdfActionLink, RECORD_ACTION } from '../../../types/blocks/lloydGeorgeActions'; +import { LGRecordActionLink, RECORD_ACTION } from '../../../types/blocks/lloydGeorgeActions'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { LinkProps } from 'react-router-dom'; import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; @@ -13,15 +13,16 @@ jest.mock('../../../helpers/hooks/useRole'); const mockSetStage = jest.fn(); const mockedUseNavigate = jest.fn(); const mockedUseRole = useRole as jest.Mock; +const mockShowDownloadAndRemoveConfirmation = jest.fn(); -const mockLinks: Array = [ +const mockLinks: Array = [ { label: 'Upload files', key: 'upload-files-link', type: RECORD_ACTION.UPDATE, href: routes.HOME, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - showIfRecordInRepo: false, + showIfRecordInStorage: false, }, { label: 'Remove files', @@ -29,7 +30,7 @@ const mockLinks: Array = [ type: RECORD_ACTION.UPDATE, stage: LG_RECORD_STAGE.DELETE_ALL, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - showIfRecordInRepo: true, + showIfRecordInStorage: true, }, { label: 'Download files', @@ -37,7 +38,15 @@ const mockLinks: Array = [ type: RECORD_ACTION.DOWNLOAD, stage: LG_RECORD_STAGE.DOWNLOAD_ALL, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - showIfRecordInRepo: true, + showIfRecordInStorage: true, + }, + { + label: 'Download and remove files', + key: 'download-and-remove-all-files-link', + type: RECORD_ACTION.DOWNLOAD, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + showIfRecordInStorage: true, + onClick: mockShowDownloadAndRemoveConfirmation, }, ]; @@ -105,6 +114,19 @@ describe('RecordMenuCard', () => { ); expect(container).toBeEmptyDOMElement(); }); + + it('render menu item as a + ); + } +}; export default RecordMenuCard; diff --git a/app/src/types/blocks/lloydGeorgeActions.test.ts b/app/src/types/blocks/lloydGeorgeActions.test.ts index c646bf080..f613e6e1b 100644 --- a/app/src/types/blocks/lloydGeorgeActions.test.ts +++ b/app/src/types/blocks/lloydGeorgeActions.test.ts @@ -1,12 +1,13 @@ import { REPOSITORY_ROLE } from '../generic/authRole'; import { - getRecordActionLinksAllowedForRole, - PdfActionLink, + getBSOLUserRecordActionLinks, + getNonBSOLUserRecordActionLinks, + LGRecordActionLink, RECORD_ACTION, } from './lloydGeorgeActions'; -describe('getAllowedRecordLinks', () => { - describe('When role = GP_ADMIN, isBSOL = true', () => { +describe('getBSOLUserRecordActionLinks', () => { + describe('When role = GP_ADMIN', () => { it('returns record links for remove record and download record', () => { const role = REPOSITORY_ROLE.GP_ADMIN; const hasRecordInRepo = true; @@ -23,15 +24,21 @@ describe('getAllowedRecordLinks', () => { }), ]); - const actual = getRecordActionLinksAllowedForRole({ role, hasRecordInRepo }); + const actual = getBSOLUserRecordActionLinks({ + role, + hasRecordInStorage: hasRecordInRepo, + }); expect(actual).toEqual(expectedOutput); }); it('returns an empty array if no record in repo (aka nothing to download or remove)', () => { const role = REPOSITORY_ROLE.GP_ADMIN; const hasRecordInRepo = false; - const expectedOutput: Array = []; - const actual = getRecordActionLinksAllowedForRole({ role, hasRecordInRepo }); + const expectedOutput: Array = []; + const actual = getBSOLUserRecordActionLinks({ + role, + hasRecordInStorage: hasRecordInRepo, + }); expect(actual).toEqual(expectedOutput); }); @@ -41,10 +48,65 @@ describe('getAllowedRecordLinks', () => { it('returns an empty array in any case', () => { const role = REPOSITORY_ROLE.GP_CLINICAL; - expect(getRecordActionLinksAllowedForRole({ role, hasRecordInRepo: true })).toEqual([]); - expect(getRecordActionLinksAllowedForRole({ role, hasRecordInRepo: false })).toEqual( + expect(getBSOLUserRecordActionLinks({ role, hasRecordInStorage: true })).toEqual([]); + expect(getBSOLUserRecordActionLinks({ role, hasRecordInStorage: false })).toEqual([]); + }); + }); +}); + +describe('getNonBSOLUserRecordActionLinks', () => { + const mockDownloadAndRemoveOnClick = jest.fn(); + describe('When role = GP_ADMIN', () => { + it('returns record links for "download and remove"', () => { + const role = REPOSITORY_ROLE.GP_ADMIN; + const hasRecordInRepo = true; + const expectedOutput = expect.arrayContaining([ + expect.objectContaining({ + label: 'Download and remove files', + key: 'download-and-remove-record-btn', + type: RECORD_ACTION.DOWNLOAD, + onClick: mockDownloadAndRemoveOnClick, + }), + ]); + + const actual = getNonBSOLUserRecordActionLinks({ + role, + hasRecordInStorage: hasRecordInRepo, + onClickFunctionForDownloadAndRemove: mockDownloadAndRemoveOnClick, + }); + + expect(actual).toEqual(expectedOutput); + }); + + it('returns an empty array if no record in repo (aka nothing to download or remove)', () => { + const role = REPOSITORY_ROLE.GP_ADMIN; + const hasRecordInRepo = false; + const expectedOutput: Array = []; + const actual = getNonBSOLUserRecordActionLinks({ + role, + hasRecordInStorage: hasRecordInRepo, + onClickFunctionForDownloadAndRemove: mockDownloadAndRemoveOnClick, + }); + + expect(actual).toEqual(expectedOutput); + }); + }); + + describe('When role = GP_CLINICAL', () => { + const args = { + role: REPOSITORY_ROLE.GP_CLINICAL, + onClickFunctionForDownloadAndRemove: mockDownloadAndRemoveOnClick, + }; + it('returns an empty array in any case', () => { + expect(getNonBSOLUserRecordActionLinks({ ...args, hasRecordInStorage: true })).toEqual( [], ); + expect( + getNonBSOLUserRecordActionLinks({ + ...args, + hasRecordInStorage: false, + }), + ).toEqual([]); }); }); }); diff --git a/app/src/types/blocks/lloydGeorgeActions.ts b/app/src/types/blocks/lloydGeorgeActions.ts index 761799a30..62688c241 100644 --- a/app/src/types/blocks/lloydGeorgeActions.ts +++ b/app/src/types/blocks/lloydGeorgeActions.ts @@ -7,24 +7,25 @@ export enum RECORD_ACTION { DOWNLOAD = 1, } -export type PdfActionLink = { +export type LGRecordActionLink = { label: string; key: string; stage?: LG_RECORD_STAGE; href?: routes; + onClick?: () => void; type: RECORD_ACTION; unauthorised?: Array; - showIfRecordInRepo: boolean; + showIfRecordInStorage: boolean; }; -export const lloydGeorgeRecordLinks: Array = [ +export const lloydGeorgeRecordLinksInBSOL: Array = [ { label: 'Remove files', key: 'delete-all-files-link', type: RECORD_ACTION.UPDATE, stage: LG_RECORD_STAGE.DELETE_ALL, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - showIfRecordInRepo: true, + showIfRecordInStorage: true, }, { label: 'Download files', @@ -32,24 +33,69 @@ export const lloydGeorgeRecordLinks: Array = [ type: RECORD_ACTION.DOWNLOAD, stage: LG_RECORD_STAGE.DOWNLOAD_ALL, unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - showIfRecordInRepo: true, + showIfRecordInStorage: true, }, ]; type Args = { role: REPOSITORY_ROLE | null; - hasRecordInRepo: boolean; + hasRecordInStorage: boolean; + inputLinks: Array; }; export function getRecordActionLinksAllowedForRole({ role, - hasRecordInRepo, -}: Args): Array { - const allowedLinks = lloydGeorgeRecordLinks.filter((link) => { + hasRecordInStorage, + inputLinks, +}: Args): Array { + const allowedLinks = inputLinks.filter((link) => { if (!role || link.unauthorised?.includes(role)) { return false; } - return hasRecordInRepo === link.showIfRecordInRepo; + return hasRecordInStorage === link.showIfRecordInStorage; }); return allowedLinks; } + +type ArgsInBsol = Omit; + +export function getBSOLUserRecordActionLinks({ + role, + hasRecordInStorage, +}: ArgsInBsol): Array { + const allowedLinks = getRecordActionLinksAllowedForRole({ + role, + hasRecordInStorage, + inputLinks: lloydGeorgeRecordLinksInBSOL, + }); + + return allowedLinks; +} + +type ArgsNonBsol = { + onClickFunctionForDownloadAndRemove: () => void; +} & Omit; + +export function getNonBSOLUserRecordActionLinks({ + role, + hasRecordInStorage, + onClickFunctionForDownloadAndRemove, +}: ArgsNonBsol): Array { + const lloydGeorgeRecordLinksNonBSOL: Array = [ + { + label: 'Download and remove files', + key: 'download-and-remove-record-btn', + type: RECORD_ACTION.DOWNLOAD, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + showIfRecordInStorage: true, + onClick: onClickFunctionForDownloadAndRemove, + }, + ]; + const allowedLinks = getRecordActionLinksAllowedForRole({ + role, + hasRecordInStorage, + inputLinks: lloydGeorgeRecordLinksNonBSOL, + }); + + return allowedLinks; +} From 1c940aa5eae76fc9fa49b124d196520bb5476216 Mon Sep 17 00:00:00 2001 From: scott alexander Date: Thu, 2 May 2024 16:44:15 +0100 Subject: [PATCH 4/6] enabling aal1 level auth on pre-prod to be able to switch between password and smartcard auth (#360) Co-authored-by: Scott Alexander --- lambdas/services/oidc_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambdas/services/oidc_service.py b/lambdas/services/oidc_service.py index f1a8e486e..523bf5171 100644 --- a/lambdas/services/oidc_service.py +++ b/lambdas/services/oidc_service.py @@ -22,7 +22,7 @@ class OidcService: "verify_iss": True, } - AAL_EXEMPT_ENVIRONMENTS = ["dev", "test"] + AAL_EXEMPT_ENVIRONMENTS = ["dev", "test", "pre-prod"] def __init__(self): self._client_id = "" From 350a914476b981c949a01754c451c3dda2a8d650 Mon Sep 17 00:00:00 2001 From: scott alexander Date: Tue, 7 May 2024 13:01:33 +0100 Subject: [PATCH 5/6] 806: Add view full screen style (#361) Add new PDF full screen style --- .../LloydGeorgeRecordStage.test.tsx | 6 +++--- .../lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx | 10 +++++++--- app/src/styles/App.scss | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx index 3835bf448..22411801a 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx @@ -294,7 +294,7 @@ describe('LloydGeorgeRecordStage', () => { }); }); - it('does not render warning callout, header and button when user is GP admin and BSOL', async () => { + it('does not render warning callout or button when user is GP admin and BSOL', async () => { mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); mockedIsBSOL.mockReturnValue(true); @@ -306,7 +306,7 @@ describe('LloydGeorgeRecordStage', () => { ).not.toBeInTheDocument(); }); - it('does not render warning callout, header and button when user is GP clinical and non BSOL', async () => { + it('does not render warning callout or button when user is GP clinical and non BSOL', async () => { mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_CLINICAL); mockedIsBSOL.mockReturnValue(false); @@ -318,7 +318,7 @@ describe('LloydGeorgeRecordStage', () => { ).not.toBeInTheDocument(); }); - it('does not render warning callout, header and button when user is GP clinical and BSOL', async () => { + it('does not render warning callout or button when user is GP clinical and BSOL', async () => { mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_CLINICAL); mockedIsBSOL.mockReturnValue(true); diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx index 3e43b756b..4284a79d7 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx @@ -128,7 +128,8 @@ function LloydGeorgeRecordStage({ { + onClick={(e) => { + e.preventDefault(); setFullScreen(false); }} > @@ -137,7 +138,6 @@ function LloydGeorgeRecordStage({ ) : ( )} -

{pageHeader}

{!fullScreen && userIsGpAdminNonBSOL && (
)} + +

{pageHeader}

{`${patientDetails?.givenName} ${patientDetails?.familyName}`} @@ -255,7 +257,9 @@ function LloydGeorgeRecordStage({ )} ) : ( - +

+ +
)}
); diff --git a/app/src/styles/App.scss b/app/src/styles/App.scss index af234cce7..07e6b704d 100644 --- a/app/src/styles/App.scss +++ b/app/src/styles/App.scss @@ -144,6 +144,10 @@ $govuk-compatibility-govukelements: true; flex-basis: calc(70% - 17px); } } + &_fs { + border-top: 'solid 1px'; + padding-top: '48px'; + } &_patient-info { p { font-size: 1.2rem; From 7c29ffcf43fc8c92441cd571c90cfb0491fdb9f0 Mon Sep 17 00:00:00 2001 From: RachelHowellNHS <127406911+RachelHowellNHS@users.noreply.github.com> Date: Wed, 8 May 2024 14:55:08 +0100 Subject: [PATCH 6/6] [UPDATE-PYTHON-PACKAGES)] update jinja and werkzeug versions --- lambdas/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambdas/requirements.txt b/lambdas/requirements.txt index ba03f52ab..18fb8fd51 100644 --- a/lambdas/requirements.txt +++ b/lambdas/requirements.txt @@ -1,8 +1,8 @@ -Jinja2==3.1.3 +Jinja2==3.1.4 MarkupSafe==2.1.3 PyJWT==2.8.0 PyYAML==6.0.1 -Werkzeug==3.0.1 +Werkzeug==3.0.3 boto3==1.33.11 botocore==1.33.11 certifi==2023.7.22