Skip to content

Commit 3f1b6f4

Browse files
authored
Merge pull request #1115 from NHSDigital/fix/only-record-dicom-for-in-progress-appointments
Only process DICOM uploads for in progress appointments
2 parents ee51cec + b717266 commit 3f1b6f4

File tree

3 files changed

+111
-31
lines changed

3 files changed

+111
-31
lines changed

manage_breast_screening/dicom/dicom_recorder.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,30 @@
88
from django.core.files.uploadedfile import InMemoryUploadedFile
99
from PIL import Image as PILImage
1010

11+
from manage_breast_screening.gateway.models import GatewayAction
12+
1113
from .models import Image, Series, Study
1214

1315
logger = logging.getLogger(__name__)
1416

1517

18+
class DicomProcessingError(Exception):
19+
"""Custom exception for DICOM processing errors."""
20+
21+
pass
22+
23+
1624
class DicomRecorder:
1725
@staticmethod
1826
def get_or_create_records(
1927
source_message_id: str, dicom_file: File
2028
) -> tuple[Study, Series, Image]:
29+
if not __class__.appointment_in_progress(source_message_id):
30+
raise DicomProcessingError(
31+
f"Cannot process DICOM file for source_message_id={source_message_id} "
32+
"because the associated appointment is not in progress."
33+
)
34+
2135
ds = pydicom.dcmread(dicom_file)
2236
study_uid = ds.StudyInstanceUID
2337
series_uid = ds.SeriesInstanceUID
@@ -102,3 +116,10 @@ def dataset_to_jpeg(sop_uid: str, ds: pydicom.Dataset) -> InMemoryUploadedFile:
102116
size=in_memory_file.getbuffer().nbytes,
103117
charset=None,
104118
)
119+
120+
@staticmethod
121+
def appointment_in_progress(source_message_id: str) -> bool:
122+
gateway_action = GatewayAction.objects.filter(id=source_message_id).first()
123+
if not gateway_action or not gateway_action.appointment:
124+
return False
125+
return gateway_action.appointment.current_status.is_in_progress()

manage_breast_screening/dicom/tests/test_api.py

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import io
22
import os
3-
from unittest.mock import MagicMock
3+
from unittest.mock import MagicMock, patch
44

55
import pydicom
66
import pytest
77
from django.core.files.uploadedfile import SimpleUploadedFile
88
from ninja.testing import TestClient
99

1010
from manage_breast_screening.core.api import api
11+
from manage_breast_screening.dicom.dicom_recorder import DicomRecorder
1112
from manage_breast_screening.dicom.models import Study
1213

1314
os.environ["NINJA_SKIP_REGISTRY"] = "yes"
@@ -30,20 +31,21 @@ def test_upload_success(dataset, dicom_file, monkeypatch):
3031
monkeypatch.setenv("API_ENABLED", "true")
3132
monkeypatch.setenv("API_AUTH_TOKEN", "testtoken")
3233

33-
response = client.put(
34-
"/dicom/abc123",
35-
FILES={"file": dicom_file},
36-
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
37-
)
34+
with patch.object(DicomRecorder, "appointment_in_progress", return_value=True):
35+
response = client.put(
36+
"/dicom/abc123",
37+
FILES={"file": dicom_file},
38+
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
39+
)
3840

39-
assert response.status_code == 201
40-
assert response.json() == {
41-
"study_instance_uid": dataset.StudyInstanceUID,
42-
"series_instance_uid": dataset.SeriesInstanceUID,
43-
"sop_instance_uid": dataset.SOPInstanceUID,
44-
"instance_id": 1,
45-
}
46-
assert Study.objects.last().source_message_id == "abc123"
41+
assert response.status_code == 201
42+
assert response.json() == {
43+
"study_instance_uid": dataset.StudyInstanceUID,
44+
"series_instance_uid": dataset.SeriesInstanceUID,
45+
"sop_instance_uid": dataset.SOPInstanceUID,
46+
"instance_id": 1,
47+
}
48+
assert Study.objects.last().source_message_id == "abc123"
4749

4850

4951
def test_upload_no_file(monkeypatch):
@@ -67,11 +69,12 @@ def test_upload_invalid_file(monkeypatch):
6769
"invalid.dcm", b"not a dicom file", content_type="application/dicom"
6870
)
6971

70-
response = client.put(
71-
"/dicom/abc123",
72-
FILES={"file": invalid_file},
73-
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
74-
)
72+
with patch.object(DicomRecorder, "appointment_in_progress", return_value=True):
73+
response = client.put(
74+
"/dicom/abc123",
75+
FILES={"file": invalid_file},
76+
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
77+
)
7578

7679
assert response.status_code == 400
7780
assert response.json()["title"] == "Invalid DICOM file"
@@ -112,11 +115,12 @@ def test_upload_missing_uids(dataset, monkeypatch):
112115
"temp.dcm", buffer.read(), content_type="application/dicom"
113116
)
114117

115-
response = client.put(
116-
"/dicom/abc123",
117-
FILES={"file": dicom_file},
118-
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
119-
)
118+
with patch.object(DicomRecorder, "appointment_in_progress", return_value=True):
119+
response = client.put(
120+
"/dicom/abc123",
121+
FILES={"file": dicom_file},
122+
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
123+
)
120124

121125
assert response.status_code == 400
122126
assert response.json()["title"] == "Missing DICOM attributes"
@@ -127,6 +131,21 @@ def test_upload_missing_uids(dataset, monkeypatch):
127131
)
128132

129133

134+
def test_upload_appointment_not_in_progress(dicom_file, monkeypatch):
135+
monkeypatch.setenv("API_ENABLED", "true")
136+
monkeypatch.setenv("API_AUTH_TOKEN", "testtoken")
137+
138+
with patch.object(DicomRecorder, "appointment_in_progress", return_value=False):
139+
response = client.put(
140+
"/dicom/abc123",
141+
FILES={"file": dicom_file},
142+
headers={"Authorization": "Bearer " + os.getenv("API_AUTH_TOKEN", "")},
143+
)
144+
145+
assert response.status_code == 500
146+
assert response.json()["title"] == "Internal Server Error"
147+
148+
130149
@pytest.mark.django_db
131150
def test_upload_when_api_disabled(dicom_file, monkeypatch):
132151
monkeypatch.setenv("API_ENABLED", "false")

manage_breast_screening/dicom/tests/test_dicom_recorder.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
import tempfile
2+
import uuid
23
from datetime import datetime
34
from unittest.mock import patch
45

56
import numpy as np
67
import pydicom
78
import pytest
89

9-
from manage_breast_screening.dicom.dicom_recorder import DicomRecorder
10+
from manage_breast_screening.dicom.dicom_recorder import (
11+
DicomProcessingError,
12+
DicomRecorder,
13+
)
1014
from manage_breast_screening.dicom.models import Image, Series, Study
15+
from manage_breast_screening.gateway.tests.factories import GatewayActionFactory
16+
from manage_breast_screening.participants.models.appointment import (
17+
AppointmentStatusNames,
18+
)
19+
from manage_breast_screening.participants.tests.factories import AppointmentFactory
1120

1221

22+
@pytest.mark.django_db
1323
class TestDicomRecorder:
24+
@pytest.fixture
25+
def gateway_action(self, source_message_id):
26+
return GatewayActionFactory(
27+
id=source_message_id,
28+
appointment=AppointmentFactory(
29+
current_status=AppointmentStatusNames.IN_PROGRESS
30+
),
31+
)
32+
1433
@pytest.fixture
1534
def source_message_id(self):
16-
return "test-source-message-id"
35+
return str(uuid.uuid4())
1736

18-
@pytest.mark.django_db
19-
def test_get_or_create_records(self, source_message_id, dataset):
37+
def test_get_or_create_records(self, source_message_id, dataset, gateway_action):
2038
with tempfile.NamedTemporaryFile() as temp_file:
2139
pydicom.filewriter.dcmwrite(
2240
temp_file.name, dataset, write_like_original=False
@@ -52,8 +70,9 @@ def test_get_or_create_records(self, source_message_id, dataset):
5270
assert image.image_file.size > 0
5371
assert image.image_file.storage.exists(image.image_file.name)
5472

55-
@pytest.mark.django_db
56-
def test_get_or_create_records_duplicate(self, source_message_id, dataset):
73+
def test_get_or_create_records_duplicate(
74+
self, source_message_id, dataset, gateway_action
75+
):
5776
with tempfile.NamedTemporaryFile() as temp_file:
5877
pydicom.filewriter.dcmwrite(
5978
temp_file.name, dataset, write_like_original=False
@@ -72,7 +91,9 @@ def test_get_or_create_records_duplicate(self, source_message_id, dataset):
7291
image,
7392
)
7493

75-
def test_get_or_create_records_invalid_dicom(self, source_message_id):
94+
def test_get_or_create_records_invalid_dicom(
95+
self, source_message_id, gateway_action
96+
):
7697
with tempfile.NamedTemporaryFile() as temp_file:
7798
invalid_ds = pydicom.Dataset()
7899
invalid_ds.transfer_syntax_uid = pydicom.uid.ExplicitVRLittleEndian
@@ -89,6 +110,25 @@ def test_get_or_create_records_invalid_dicom(self, source_message_id):
89110
with pytest.raises(AttributeError):
90111
DicomRecorder.get_or_create_records(source_message_id, dicom_file)
91112

113+
def test_get_or_create_records_appointment_not_in_progress(
114+
self, source_message_id, dataset
115+
):
116+
GatewayActionFactory(
117+
id=source_message_id,
118+
appointment=AppointmentFactory(
119+
current_status=AppointmentStatusNames.SCREENED
120+
),
121+
)
122+
with tempfile.NamedTemporaryFile() as temp_file:
123+
pydicom.filewriter.dcmwrite(
124+
temp_file.name, dataset, write_like_original=False
125+
)
126+
with open(temp_file.name, "rb") as dicom_file:
127+
with pytest.raises(DicomProcessingError):
128+
DicomRecorder.get_or_create_records(source_message_id, dicom_file)
129+
130+
131+
class TestJpegConversion:
92132
def test_dataset_to_jpeg(self, dataset):
93133
expected_pixel_array = self.expected_pixel_array(dataset)
94134

0 commit comments

Comments
 (0)