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
14 changes: 8 additions & 6 deletions lambdas/models/document_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class DocumentReference(BaseModel):
author: str | None = None
content_type: str = Field(default=DEFAULT_CONTENT_TYPE)
created: str = Field(
default_factory=lambda: datetime.now(timezone.utc).strftime(DATE_FORMAT)
default_factory=lambda: datetime.now(timezone.utc).strftime(DATE_FORMAT),
)
document_scan_creation: Optional[str] = Field(
default_factory=lambda: datetime.date(datetime.now()).isoformat(),
Expand All @@ -125,10 +125,10 @@ class DocumentReference(BaseModel):
] = Field(default="preliminary")
doc_type: str = Field(default=None, exclude=True)
document_snomed_code_type: Optional[str] = Field(
default=SnomedCodes.LLOYD_GEORGE.value.code
default=SnomedCodes.LLOYD_GEORGE.value.code,
)
file_location: str = ""
file_name: str
file_name: str | None
file_size: int | None = Field(default=None)
last_updated: int = Field(
default_factory=lambda: int(datetime.now(timezone.utc).timestamp()),
Expand All @@ -140,11 +140,12 @@ class DocumentReference(BaseModel):
s3_version_id: Optional[str] = Field(default=None, alias="S3VersionID")
s3_upload_key: str = Field(default=None, exclude=True)
status: Literal["current", "superseded", "entered-in-error"] = Field(
default="current"
default="current",
)
sub_folder: str = Field(default=None, exclude=True)
ttl: Optional[int] = Field(
alias=str(DocumentReferenceMetadataFields.TTL.value), default=None
alias=str(DocumentReferenceMetadataFields.TTL.value),
default=None,
)
uploaded: bool = Field(default=None)
uploading: bool = Field(default=None)
Expand Down Expand Up @@ -174,7 +175,8 @@ def set_location_properties(cls, data, *args, **kwargs):
if "s3_file_key" not in data:
data["s3_file_key"] = cls._build_final_s3_key(data)
data["file_location"] = cls._build_s3_location(
data["s3_bucket_name"], current_s3_file_key
data["s3_bucket_name"],
current_s3_file_key,
)
return data

Expand Down
28 changes: 21 additions & 7 deletions lambdas/services/post_fhir_document_reference_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from enums.mtls import MtlsCommonNames
from enums.snomed_codes import SnomedCode, SnomedCodes
from models.document_reference import DocumentReference
from models.fhir.R4.fhir_document_reference import SNOMED_URL
from models.fhir.R4.fhir_document_reference import (
SNOMED_URL,
)
from models.fhir.R4.fhir_document_reference import (
DocumentReference as FhirDocumentReference,
)
Expand All @@ -25,7 +27,9 @@ def __init__(self):
super().__init__()

def process_fhir_document_reference(
self, fhir_document: str, api_request_context: dict = {}
self,
fhir_document: str,
api_request_context: dict = {},
) -> str:
"""
Process a FHIR Document Reference request
Expand All @@ -40,7 +44,7 @@ def process_fhir_document_reference(
common_name = validate_common_name_in_mtls(api_request_context)

validated_fhir_doc = FhirDocumentReference.model_validate_json(
fhir_document
fhir_document,
)

# Extract NHS number and author from the FHIR document
Expand Down Expand Up @@ -72,7 +76,9 @@ def process_fhir_document_reference(
)

presigned_url = self._handle_document_save(
document_reference, validated_fhir_doc, dynamo_table
document_reference,
validated_fhir_doc,
dynamo_table,
)

return self._create_fhir_response(document_reference, presigned_url)
Expand Down Expand Up @@ -107,7 +113,9 @@ def _extract_author_from_fhir(self, fhir_doc: FhirDocumentReference) -> str | No
raise DocumentRefException(400, LambdaError.DocRefNoParse)

def _determine_document_type(
self, fhir_doc: FhirDocumentReference, common_name: MtlsCommonNames | None
self,
fhir_doc: FhirDocumentReference,
common_name: MtlsCommonNames | None,
) -> SnomedCode:
if not common_name:
"""Determine the document type based on SNOMED code in the FHIR document"""
Expand All @@ -119,7 +127,7 @@ def _determine_document_type(
return snomed_code
else:
logger.error(
f"SNOMED code {coding.code} - {coding.display} is not supported"
f"SNOMED code {coding.code} - {coding.display} is not supported",
)
raise DocumentRefException(400, LambdaError.DocRefInvalidType)
logger.error("SNOMED code not found in FHIR document")
Expand Down Expand Up @@ -147,6 +155,12 @@ def _create_document_reference(
if not custodian:
custodian = current_gp_ods

title = fhir_doc.content[0].attachment.title or None

if doc_type != SnomedCodes.PATIENT_DATA.value and title is None:
logger.error("FHIR document validation error: attachment.title missing")
raise DocumentRefException(400, LambdaError.DocRefNoParse)

sub_folder, raw_request = (
("user_upload", None)
if doc_type != SnomedCodes.PATIENT_DATA.value
Expand All @@ -161,7 +175,7 @@ def _create_document_reference(
s3_bucket_name=self.staging_bucket_name,
author=author,
content_type=fhir_doc.content[0].attachment.contentType,
file_name=fhir_doc.content[0].attachment.title,
file_name=title,
document_snomed_code_type=doc_type.code,
doc_status="preliminary",
status="current",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import json
import logging
import os

Expand Down Expand Up @@ -42,7 +43,8 @@ def condition(response_json):
return response_json["content"][0]["attachment"].get("data", False)

raw_retrieve_response = retrieve_document_with_retry(
upload_response["id"], condition
upload_response["id"],
condition,
)
retrieve_response = raw_retrieve_response.json()

Expand Down Expand Up @@ -85,7 +87,8 @@ def test_create_document_without_author_or_type(test_data):
with open(sample_pdf_path, "rb") as f:
record["data"] = base64.b64encode(f.read()).decode("utf-8")
payload = pdm_data_helper.create_upload_payload(
record=record, exclude=["type", "author"]
record=record,
exclude=["type", "author"],
)

for field in ["type", "author"]:
Expand All @@ -103,3 +106,32 @@ def test_create_document_without_author_or_type(test_data):
assert doc_ref["Item"]["RawRequest"] == payload
for field in ["type", "author"]:
assert field not in doc_ref["Item"]["RawRequest"]


def test_create_document_without_title(test_data):
record = {
"ods": "H81109",
"nhs_number": "9912003071",
}

sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
with open(sample_pdf_path, "rb") as f:
record["data"] = base64.b64encode(f.read()).decode("utf-8")
payload = pdm_data_helper.create_upload_payload(record=record, exclude=["title"])
assert "title" not in payload

raw_upload_response = upload_document(payload)
assert raw_upload_response.status_code == 201
record["id"] = raw_upload_response.json()["id"].split("~")[1]
test_data.append(record)

doc_ref = pdm_data_helper.retrieve_document_reference(record=record)
assert "Item" in doc_ref
assert "RawRequest" in doc_ref["Item"]
assert doc_ref["Item"]["RawRequest"] == payload
raw_request = json.loads(doc_ref["Item"]["RawRequest"])
assert "content" in raw_request
content = raw_request["content"]
assert "attachment" in content[0]
attachment = raw_request["content"][0]["attachment"]
assert "title" not in attachment
64 changes: 50 additions & 14 deletions lambdas/tests/e2e/api/test_upload_document_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
data_helper = LloydGeorgeDataHelper()


def create_upload_payload(lloyd_george_record):
def create_upload_payload(lloyd_george_record, exclude: list[str] | None = None):
sample_payload = {
"resourceType": "DocumentReference",
"type": {
Expand All @@ -27,28 +27,28 @@ def create_upload_payload(lloyd_george_record):
"system": "http://snomed.info/sct",
"code": f"{LLOYD_GEORGE_SNOMED}",
"display": "Lloyd George record folder",
}
]
},
],
},
"subject": {
"identifier": {
"system": "https://fhir.nhs.uk/Id/nhs-number",
"value": lloyd_george_record["nhs_number"],
}
},
},
"author": [
{
"identifier": {
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
"value": lloyd_george_record["ods"],
}
}
},
},
],
"custodian": {
"identifier": {
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
"value": lloyd_george_record["ods"],
}
},
},
"content": [
{
Expand All @@ -57,11 +57,18 @@ def create_upload_payload(lloyd_george_record):
"contentType": "application/pdf",
"language": "en-GB",
"title": "1of1_Lloyd_George_Record_[Paula Esme VESEY]_[9730153973]_[22-01-1960].pdf",
}
}
},
},
],
}

if exclude:
for field in exclude:
if field == "title":
sample_payload["content"][0]["attachment"].pop(field, None)
else:
sample_payload.pop(field, None)

if "data" in lloyd_george_record:
sample_payload["content"][0]["attachment"]["data"] = lloyd_george_record["data"]
return json.dumps(sample_payload)
Expand Down Expand Up @@ -105,10 +112,10 @@ def condition(response_json):
assert base64.b64decode(base64_data, validate=True)

assert upload_response == snapshot_json(
exclude=paths("id", "date", "content.0.attachment.url")
exclude=paths("id", "date", "content.0.attachment.url"),
)
assert retrieve_response == snapshot_json(
exclude=paths("id", "date", "content.0.attachment.data")
exclude=paths("id", "date", "content.0.attachment.data"),
)


Expand Down Expand Up @@ -153,8 +160,11 @@ def condition(response_json):
assert upload_response == snapshot_json(exclude=paths("id", "date"))
assert retrieve_response == snapshot_json(
exclude=paths(
"id", "date", "content.0.attachment.url", "content.0.attachment.size"
)
"id",
"date",
"content.0.attachment.url",
"content.0.attachment.size",
),
)


Expand Down Expand Up @@ -195,7 +205,7 @@ def condition(response_json):
retrieve_response = raw_retrieve_response.json()

assert upload_response == snapshot_json(
exclude=paths("id", "date", "content.0.attachment.url")
exclude=paths("id", "date", "content.0.attachment.url"),
)
assert retrieve_response == snapshot_json(exclude=paths("id", "date"))

Expand All @@ -221,3 +231,29 @@ def test_create_document_does_not_save_raw(test_data):
doc_ref = data_helper.retrieve_document_reference(record=lloyd_george_record)
assert "Item" in doc_ref
assert "RawRequest" not in doc_ref["Item"]


def test_create_document_without_title_raises_error(test_data):
lloyd_george_record = {}
lloyd_george_record["ods"] = "H81109"
lloyd_george_record["nhs_number"] = "9449303304"

sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
with open(sample_pdf_path, "rb") as f:
lloyd_george_record["data"] = base64.b64encode(f.read()).decode("utf-8")
payload = create_upload_payload(lloyd_george_record, exclude=["title"])

url = f"https://{API_ENDPOINT}/FhirDocumentReference"
headers = {"Authorization": "Bearer 123", "X-Api-Key": API_KEY}

retrieve_response = requests.post(url, headers=headers, data=payload)
assert retrieve_response.status_code == 400

json_response = retrieve_response.json()
assert (
json_response["issue"][0]["details"]["coding"][0]["code"] == "VALIDATION_ERROR"
)
assert (
json_response["issue"][0]["diagnostics"]
== "Failed to parse document upload request data"
)
Loading
Loading