Skip to content

Commit 67396c0

Browse files
authored
Merge branch 'master' into VED-167-Audit-table-Ack-Lambda-Tests
2 parents 18c1923 + cc6a7ef commit 67396c0

File tree

11 files changed

+236
-208
lines changed

11 files changed

+236
-208
lines changed

backend/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ package: build
88
test:
99
@PYTHONPATH=src:tests python -m unittest
1010

11+
coverage-run:
12+
@PYTHONPATH=src:tests coverage run -m unittest discover
13+
14+
coverage-report:
15+
coverage report -m
16+
1117
.PHONY: build package test

backend/src/fhir_controller.py

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -482,16 +482,15 @@ def _validate_id(self, _id: str) -> Optional[dict]:
482482
else:
483483
return None
484484

485-
def _validate_identifier_system(self, _id: str, _element: str) -> Optional[dict]:
486-
485+
def _validate_identifier_system(self, _id: str, _elements: str) -> Optional[dict]:
487486
if not _id:
488487
return create_operation_outcome(
489488
resource_id=str(uuid.uuid4()),
490489
severity=Severity.error,
491490
code=Code.invalid,
492491
diagnostics=(
493-
"Search parameter immunization.identifier must have one value and must be in the format of "
494-
'"immunization.identifier.system|immunization.identifier.value" '
492+
"Search parameter identifier must have one value and must be in the format of "
493+
'"identifier.system|identifier.value" '
495494
'e.g. "http://xyz.org/vaccs|2345-gh3s-r53h7-12ny"'
496495
),
497496
)
@@ -501,27 +500,23 @@ def _validate_identifier_system(self, _id: str, _element: str) -> Optional[dict]
501500
severity=Severity.error,
502501
code=Code.invalid,
503502
diagnostics=(
504-
"Search parameter immunization.identifier must be in the format of "
505-
'"immunization.identifier.system|immunization.identifier.value" '
503+
"Search parameter identifier must be in the format of "
504+
'"identifier.system|identifier.value" '
506505
'e.g. "http://xyz.org/vaccs|2345-gh3s-r53h7-12ny"'
507506
),
508507
)
509-
if not _element:
510-
return create_operation_outcome(
511-
resource_id=str(uuid.uuid4()),
512-
severity=Severity.error,
513-
code=Code.invalid,
514-
diagnostics="_element must be one or more of the following: id,meta",
515-
)
516-
element_lower = _element.lower()
517-
result = element_lower.split(",")
518-
is_present = all(key in ["id", "meta"] for key in result)
519-
if not is_present:
508+
509+
if not _elements:
510+
return None
511+
512+
requested_elements = {e.strip().lower() for e in _elements.split(",") if e.strip()}
513+
requested_elements_valid = requested_elements.issubset({"id", "meta"})
514+
if _elements and not requested_elements_valid:
520515
return create_operation_outcome(
521516
resource_id=str(uuid.uuid4()),
522517
severity=Severity.error,
523518
code=Code.invalid,
524-
diagnostics="_element must be one or more of the following: id,meta",
519+
diagnostics="_elements must be one or more of the following: id,meta",
525520
)
526521

527522
def _create_bad_request(self, message):
@@ -549,47 +544,62 @@ def authorize_request(self, aws_event: dict) -> Optional[dict]:
549544
return self.create_response(500, id_error)
550545

551546
def fetch_identifier_system_and_element(self, event: dict):
547+
"""
548+
Extracts `identifier` and `_elements` from an incoming FHIR search request.
549+
550+
FHIR search supports two input formats:
551+
1. GET search: parameters appear in the query string (e.g. ?identifier=abc123&_elements=id,meta)
552+
2. POST search: parameters appear in the request body, form-encoded (e.g. identifier=abc123&_elements=id,meta)
553+
554+
This function handles both cases, returning:
555+
- The extracted identifier value
556+
- The extracted _elements value
557+
- Any validation check result for disallowed keys
558+
- Booleans indicating whether identifier/_elements were present
559+
"""
560+
552561
query_params = event.get("queryStringParameters", {})
553562
body = event["body"]
554563
not_required_keys = ["-date.from", "-date.to", "-immunization.target", "_include", "patient.identifier"]
564+
565+
# Get Search Query Parameters
555566
if query_params and not body:
556-
# Check for the presence of 'immunization.identifier' and '_element'
557-
query_string_has_immunization_identifier = "immunization.identifier" in event.get(
558-
"queryStringParameters", {}
559-
)
560-
query_string_has_element = "_element" in event.get("queryStringParameters", {})
561-
immunization_identifier = query_params.get("immunization.identifier", "")
562-
element = query_params.get("_element", "")
567+
query_string_has_immunization_identifier = "identifier" in query_params
568+
query_string_has_element = "_elements" in query_params
569+
identifier = query_params.get("identifier", "")
570+
element = query_params.get("_elements", "")
563571
query_check = check_keys_in_sources(event, not_required_keys)
564572

565573
return (
566-
immunization_identifier,
574+
identifier,
567575
element,
568576
query_check,
569577
query_string_has_immunization_identifier,
570578
query_string_has_element,
571579
)
580+
581+
# Post Search Identifier by body form
572582
if body and not query_params:
573583
decoded_body = base64.b64decode(body).decode("utf-8")
574584
parsed_body = urllib.parse.parse_qs(decoded_body)
575-
# Attempt to extract 'immunization.identifier' and '_element'
576-
converted_identifer = ""
577-
converted_element = ""
578-
immunization_identifier = parsed_body.get("immunization.identifier", "")
579-
if immunization_identifier:
580-
converted_identifer = "".join(immunization_identifier)
581-
_element = parsed_body.get("_element", "")
582-
if _element:
583-
converted_element = "".join(_element)
584-
body_has_immunization_identifier = "immunization.identifier" in parsed_body
585-
body_has_immunization_element = "_element" in parsed_body
585+
# Attempt to extract 'identifier' and '_elements'
586+
converted_identifier = ""
587+
converted_elements = ""
588+
identifier = parsed_body.get("identifier", "")
589+
if identifier:
590+
converted_identifier = "".join(identifier)
591+
_elements = parsed_body.get("_elements", "")
592+
if _elements:
593+
converted_elements = "".join(_elements)
594+
body_has_identifier = "identifier" in parsed_body
595+
body_has_immunization_elements = "_elements" in parsed_body
586596
body_check = check_keys_in_sources(event, not_required_keys)
587597
return (
588-
converted_identifer,
589-
converted_element,
598+
converted_identifier,
599+
converted_elements,
590600
body_check,
591-
body_has_immunization_identifier,
592-
body_has_immunization_element,
601+
body_has_identifier,
602+
body_has_immunization_elements,
593603
)
594604

595605
def create_response_for_identifier(self, not_required, has_identifier, has_element):
@@ -598,16 +608,7 @@ def create_response_for_identifier(self, not_required, has_identifier, has_eleme
598608
resource_id=str(uuid.uuid4()),
599609
severity=Severity.error,
600610
code=Code.server_error,
601-
diagnostics="Search parameter should have either immunization.identifier or patient.identifier",
602-
)
603-
return self.create_response(400, error)
604-
605-
if "patient.identifier" not in not_required and not_required and has_identifier:
606-
error = create_operation_outcome(
607-
resource_id=str(uuid.uuid4()),
608-
severity=Severity.error,
609-
code=Code.server_error,
610-
diagnostics="Search parameter immunization.identifier must have the following parameter: _element",
611+
diagnostics="Search parameter should have either identifier or patient.identifier",
611612
)
612613
return self.create_response(400, error)
613614

@@ -616,7 +617,7 @@ def create_response_for_identifier(self, not_required, has_identifier, has_eleme
616617
resource_id=str(uuid.uuid4()),
617618
severity=Severity.error,
618619
code=Code.server_error,
619-
diagnostics="Search parameter _element must have the following parameter: immunization.identifier",
620+
diagnostics="Search parameter _elements must have the following parameter: identifier",
620621
)
621622
return self.create_response(400, error)
622623

backend/src/fhir_repository.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ def get_immunization_by_identifier(
9595
)
9696
if "Items" in response and len(response["Items"]) > 0:
9797
item = response["Items"][0]
98-
resp = dict()
9998
vaccine_type = self._vaccine_type(item["PatientSK"])
10099
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.SEARCH, [vaccine_type]):
101100
raise UnauthorizedVaxError()
102101
resource = json.loads(item["Resource"])
103-
resp["id"] = resource.get("id")
104-
resp["version"] = int(response["Items"][0]["Version"])
105-
return resp
102+
version = int(response["Items"][0]["Version"])
103+
return {
104+
"resource": resource,
105+
"id": resource.get("id"),
106+
"version": version
107+
}
106108
else:
107109
return None
108110

backend/src/fhir_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def get_immunization_by_identifier(
6262
) -> Optional[dict]:
6363
"""
6464
Get an Immunization by its ID. Return None if not found. If the patient doesn't have an NHS number,
65-
return the Immunization without calling PDS or checking S flag.
65+
return the Immunization.
6666
"""
6767
imms_resp = self.immunization_repo.get_immunization_by_identifier(
6868
identifier_pk, imms_vax_type_perms
@@ -79,7 +79,7 @@ def get_immunization_by_identifier(
7979
def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: list[str]) -> Optional[dict]:
8080
"""
8181
Get an Immunization by its ID. Return None if it is not found. If the patient doesn't have an NHS number,
82-
return the Immunization without calling PDS or checking S flag.
82+
return the Immunization.
8383
"""
8484
if not (imms_resp := self.immunization_repo.get_immunization_by_id(imms_id, imms_vax_type_perms)):
8585
return None
@@ -95,7 +95,7 @@ def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: list[str]) -
9595
def get_immunization_by_id_all(self, imms_id: str, imms: dict) -> Optional[dict]:
9696
"""
9797
Get an Immunization by its ID. Return None if not found. If the patient doesn't have an NHS number,
98-
return the Immunization without calling PDS or checking S flag.
98+
return the Immunization.
9999
"""
100100
imms["id"] = imms_id
101101
try:

backend/src/models/utils/generic_utils.py

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -136,43 +136,42 @@ def create_diagnostics_error(value):
136136

137137

138138
def form_json(response, _element, identifier, baseurl):
139-
# Elements to include, based on the '_element' parameter
140-
if not response:
141-
json = {
139+
self_url = f"{baseurl}?identifier={identifier}" + (f"&_elements={_element}" if _element else "")
140+
json = {
142141
"resourceType": "Bundle",
143142
"type": "searchset",
144143
"link": [
145-
{"relation": "self", "url": f"{baseurl}?immunization.identifier={identifier}&_elements={_element}"}
146-
],
147-
"entry": [],
148-
"total": 0,
149-
}
144+
{"relation": "self", "url": self_url}
145+
]
146+
}
147+
if not response:
148+
json["entry"] = []
149+
json["total"] = 0
150150
return json
151151

152-
# Basic structure for the JSON output
153-
json = {
154-
"resourceType": "Bundle",
155-
"type": "searchset",
156-
"link": [{"relation": "self", "url": f"{baseurl}?immunization.identifier={identifier}&_elements={_element}"}],
157-
"entry": [
158-
{
159-
"fullUrl": f"https://api.service.nhs.uk/immunisation-fhir-api/Immunization/{response['id']}",
160-
"resource": {"resourceType": "Immunization"},
161-
}
162-
],
163-
"total": 1,
164-
}
165-
__elements = _element.lower()
166-
element = __elements.split(",")
167-
# Add 'id' if specified
168-
if "id" in element:
169-
json["entry"][0]["resource"]["id"] = response["id"]
152+
# Full Immunization payload to be returned if only the identifier parameter was provided
153+
if identifier and not _element:
154+
resource = response["resource"]
155+
156+
elif identifier and _element:
157+
element = {e.strip().lower() for e in _element.split(",") if e.strip()}
158+
resource = {"resourceType": "Immunization"}
170159

171-
# Add 'meta' if specified
172-
if "meta" in element:
173-
json["entry"][0]["resource"]["id"] = response["id"]
174-
json["entry"][0]["resource"]["meta"] = {"versionId": response["version"]}
160+
# Add 'id' if specified
161+
if "id" in element:
162+
resource["id"] = response["id"]
175163

164+
# Add 'meta' if specified
165+
if "meta" in element:
166+
resource["id"] = response["id"]
167+
resource["meta"] = {"versionId": response["version"]}
168+
169+
json["entry"] = [{
170+
"fullUrl": f"https://api.service.nhs.uk/immunisation-fhir-api/Immunization/{response['id']}",
171+
"resource": resource,
172+
}
173+
]
174+
json["total"] = 1
176175
return json
177176

178177

backend/src/models/utils/pre_validator_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def for_date_time(field_value: str, field_location: str, strict_timezone: bool =
142142
continue
143143

144144
raise ValueError(error_message)
145-
145+
146146
@staticmethod
147147
def for_snomed_code(field_value: str, field_location: str):
148148
"""

backend/src/search_imms_handler.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,19 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController
3232
body_has_immunization_element = False
3333
if not (query_params == None and body == None):
3434
if query_params:
35-
query_string_has_immunization_identifier = "immunization.identifier" in event.get(
35+
query_string_has_immunization_identifier = "identifier" in event.get(
3636
"queryStringParameters", {}
3737
)
38-
query_string_has_element = "_element" in event.get("queryStringParameters", {})
38+
query_string_has_element = "_elements" in event.get("queryStringParameters", {})
3939
# Decode body from base64
4040
if body:
4141
decoded_body = base64.b64decode(body).decode("utf-8")
4242
# Parse the URL encoded body
4343
parsed_body = urllib.parse.parse_qs(decoded_body)
4444

45-
# Check for 'immunization.identifier' in body
46-
body_has_immunization_identifier = "immunization.identifier" in parsed_body
47-
body_has_immunization_element = "_element" in parsed_body
45+
# Check for 'identifier' in body
46+
body_has_immunization_identifier = "identifier" in parsed_body
47+
body_has_immunization_element = "_elements" in parsed_body
4848
if (
4949
query_string_has_immunization_identifier
5050
or body_has_immunization_identifier
@@ -97,13 +97,13 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController
9797
parser.add_argument("--date.from", type=str, required=False, dest="date_from")
9898
parser.add_argument("--date.to", type=str, required=False, dest="date_to")
9999
parser.add_argument(
100-
"--immunization.identifier",
100+
"--identifier",
101101
help="Identifier of System",
102102
type=str,
103103
required=False,
104-
dest="immunization_identifier",
104+
dest="identifier",
105105
)
106-
parser.add_argument("--element", help="Identifier of System", type=str, required=False, dest="_element")
106+
parser.add_argument("--elements", help="Identifier of System", type=str, required=False, dest="_elements")
107107
args = parser.parse_args()
108108

109109
event: events.APIGatewayProxyEventV1 = {
@@ -113,8 +113,8 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController
113113
"-date.from": [args.date_from] if args.date_from else [],
114114
"-date.to": [args.date_to] if args.date_to else [],
115115
"_include": ["Immunization:patient"],
116-
"immunization_identifier": [args.immunization_identifier] if args.immunization_identifier else [],
117-
"_element": [args._element] if args._element else [],
116+
"identifier": [args.immunization_identifier] if args.immunization_identifier else [],
117+
"_elements": [args._element] if args._element else [],
118118
},
119119
"httpMethod": "POST",
120120
"headers": {

0 commit comments

Comments
 (0)