Skip to content

Commit 90f62c4

Browse files
authored
Merge pull request #55 from NHSDigital/fix/provide-return-key-attributes-defined-in-conformance-doc
Add the return key attributes as specified in the conformance doc
2 parents ed31516 + ded5bc7 commit 90f62c4

7 files changed

Lines changed: 136 additions & 56 deletions

File tree

src/models.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from dataclasses import dataclass, field
2+
from typing import Optional
3+
4+
from services.mwl import MWLStatus
5+
6+
7+
@dataclass
8+
class WorklistItem:
9+
accession_number: str = field(
10+
doc="A departmental Information System generated number that identifies the Imaging Service Request.",
11+
)
12+
modality: str = field(doc="Code for type of equipment that will perform the procedure.")
13+
patient_birth_date: str = field(doc="Date of Birth of the Patient.")
14+
patient_id: str = field(doc="Patient NHS Number", hash=True)
15+
patient_name: str = field(doc="Name of the patient. Lastname^Firstname.")
16+
scheduled_date: str = field(doc="Date the procedure is scheduled for.")
17+
scheduled_time: str = field(doc="Time the procedure is scheduled for.")
18+
status: str = field(doc="Status of the worklist item", default=MWLStatus.SCHEDULED.value)
19+
20+
source_message_id: Optional[str] = field(
21+
default=None, doc="Message ID from system which created this worklist item", hash=True
22+
)
23+
study_instance_uid: Optional[str] = field(default=None, doc="Instance UID for the study", hash=True)
24+
procedure_code: Optional[str] = field(default=None, doc="Code that identifies the requested procedure.")
25+
patient_sex: Optional[str] = field(default=None, doc="Sex of the patient.")
26+
study_description: Optional[str] = field(default=None, doc="Description of the study.")
27+
mpps_instance_uid: Optional[str] = field(
28+
default=None, doc="Modality Performed Procedure Step (MPPS) instance UID if available."
29+
)
30+
31+
patient_age: Optional[str] = field(default=None, doc="Age of the patient at the time of scheduling.")
32+
patient_weight: Optional[str] = field(default=None, doc="Weight of the patient at the time of scheduling.")
33+
patient_address: Optional[str] = field(default=None, doc="Address of the patient.")
34+
patient_comments: Optional[str] = field(default=None, doc="Additional comments about the patient.")
35+
36+
procedure_coding_scheme_designator: Optional[str] = field(
37+
default=None, doc="Coding scheme designator for the procedure code."
38+
)
39+
procedure_code_meaning: Optional[str] = field(default=None, doc="Code meaning for the procedure code.")
40+
41+
reason_code_value: Optional[str] = field(default=None, doc="Code value for the reason for requested procedure.")
42+
reason_coding_scheme_designator: Optional[str] = field(
43+
default=None, doc="Coding scheme designator for the reason for requested procedure."
44+
)
45+
reason_code_meaning: Optional[str] = field(default=None, doc="Code meaning for the reason for requested procedure.")
46+
47+
scheduled_performing_physician_name: Optional[str] = field(
48+
default=None, doc="Name of the scheduled performing physician."
49+
)
50+
scheduled_procedure_step_location: Optional[str] = field(
51+
default=None, doc="Location of the scheduled procedure step."
52+
)
53+
scheduled_station_aet: Optional[str] = field(default=None, doc="AE Title of the scheduled station.")
54+
scheduled_station_name: Optional[str] = field(default=None, doc="Name of the scheduled station.")
55+
scheduled_protocol_code_value: Optional[str] = field(default=None, doc="Code value for the scheduled protocol.")
56+
scheduled_protocol_coding_scheme_designator: Optional[str] = field(
57+
default=None, doc="Coding scheme designator for the scheduled protocol."
58+
)
59+
scheduled_protocol_code_meaning: Optional[str] = field(default=None, doc="Code meaning for the scheduled protocol.")

src/services/mwl/c_find.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from pydicom import Dataset
1111
from pynetdicom import evt
1212

13+
from models import WorklistItem
1314
from services.dicom import CHARSET_UTF8, FAILURE, PENDING, SUCCESS
14-
from services.storage import MWLStorage, WorklistItem
15+
from services.storage import MWLStorage
1516

1617
logger = logging.getLogger(__name__)
1718

@@ -88,26 +89,47 @@ def _build_worklist_response(self, item: WorklistItem) -> Dataset:
8889
ds.PatientID = item.patient_id
8990
ds.PatientName = item.patient_name
9091
ds.PatientBirthDate = item.patient_birth_date
91-
if item.patient_sex:
92-
ds.PatientSex = item.patient_sex
92+
ds.PatientSex = item.patient_sex
93+
ds.PatientAge = item.patient_age
94+
ds.PatientWeight = item.patient_weight
95+
ds.PatientAddress = item.patient_address
96+
ds.PatientComments = item.patient_comments
9397

9498
# Study information
9599
ds.AccessionNumber = item.accession_number
96-
if item.study_instance_uid:
97-
ds.StudyInstanceUID = item.study_instance_uid
100+
ds.StudyInstanceUID = item.study_instance_uid
98101

99-
if item.study_description:
100-
ds.StudyDescription = item.study_description
101-
sps_item.ScheduledProcedureStepDescription = ds.StudyDescription
102+
ds.StudyDescription = item.study_description
103+
sps_item.ScheduledProcedureStepDescription = ds.StudyDescription
102104

103-
if item.procedure_code:
104-
ds.RequestedProcedureID = item.procedure_code
105-
sps_item.ScheduledProcedureStepID = ds.RequestedProcedureID
105+
ds.RequestedProcedureID = item.procedure_code
106+
107+
ds.RequestedProcedureCodeSequence = [Dataset()]
108+
ds.RequestedProcedureCodeSequence[0].CodeValue = item.procedure_code
109+
ds.RequestedProcedureCodeSequence[0].CodingSchemeDesignator = item.procedure_coding_scheme_designator
110+
ds.RequestedProcedureCodeSequence[0].CodeMeaning = item.procedure_code_meaning
111+
112+
ds.ReasonForRequestedProcedureCodeSequence = [Dataset()]
113+
ds.ReasonForRequestedProcedureCodeSequence[0].CodeValue = item.reason_code_value
114+
ds.ReasonForRequestedProcedureCodeSequence[0].CodingSchemeDesignator = item.reason_coding_scheme_designator
115+
ds.ReasonForRequestedProcedureCodeSequence[0].CodeMeaning = item.reason_code_meaning
116+
117+
sps_item.ScheduledProcedureStepID = ds.RequestedProcedureID
106118

107119
# Scheduled Procedure Step Sequence
108120
sps_item.ScheduledProcedureStepStartDate = item.scheduled_date
109121
sps_item.ScheduledProcedureStepStartTime = item.scheduled_time
110122
sps_item.Modality = item.modality
123+
sps_item.ScheduledStationAETitle = item.scheduled_station_aet
124+
sps_item.ScheduledPerformingPhysicianName = item.scheduled_performing_physician_name
125+
sps_item.ScheduledStationName = item.scheduled_station_name
126+
sps_item.ScheduledProcedureStepLocation = item.scheduled_procedure_step_location
127+
sps_item.ScheduledProtocolCodeSequence = [Dataset()]
128+
sps_item.ScheduledProtocolCodeSequence[0].CodeValue = item.scheduled_protocol_code_value
129+
sps_item.ScheduledProtocolCodeSequence[
130+
0
131+
].CodingSchemeDesignator = item.scheduled_protocol_coding_scheme_designator
132+
sps_item.ScheduledProtocolCodeSequence[0].CodeMeaning = item.scheduled_protocol_code_meaning
111133

112134
ds.ScheduledProcedureStepSequence = [sps_item]
113135

src/services/mwl/create_worklist_item.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

3-
from services.storage import DuplicateWorklistItemError, MWLStorage, WorklistItem
3+
from models import WorklistItem
4+
from services.storage import DuplicateWorklistItemError, MWLStorage
45

56
logger = logging.getLogger(__name__)
67

src/services/storage.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
import os
44
import sqlite3
55
from contextlib import contextmanager
6-
from dataclasses import dataclass, field
76
from pathlib import Path
87
from typing import Dict, List, Optional
98

10-
from services.mwl import MWLStatus
9+
from models import WorklistItem
1110

1211
logger = logging.getLogger(__name__)
1312

@@ -279,30 +278,6 @@ def mark_upload_failed(self, sop_instance_uid: str, error: str, permanent: bool
279278
conn.commit()
280279

281280

282-
@dataclass
283-
class WorklistItem:
284-
accession_number: str = field(
285-
doc="A departmental Information System generated number that identifies the Imaging Service Request.",
286-
)
287-
modality: str = field(doc="Code for type of equipment that will perform the procedure.")
288-
patient_birth_date: str = field(doc="Date of Birth of the Patient.")
289-
patient_id: str = field(doc="Patient NHS Number", hash=True)
290-
patient_name: str = field(doc="Name of the patient. Lastname^Firstname.")
291-
scheduled_date: str = field(doc="Date the procedure is scheduled for.")
292-
scheduled_time: str = field(doc="Time the procedure is scheduled for.")
293-
status: str = field(doc="Status of the worklist item", default=MWLStatus.SCHEDULED.value)
294-
source_message_id: Optional[str] = field(
295-
default=None, doc="Message ID from system which created this worklist item", hash=True
296-
)
297-
study_instance_uid: Optional[str] = field(default=None, doc="Instance UID for the study", hash=True)
298-
procedure_code: Optional[str] = field(default=None, doc="Code that identifies the requested procedure.")
299-
patient_sex: Optional[str] = field(default=None, doc="Sex of the patient.")
300-
study_description: Optional[str] = field(default=None, doc="Description of the study.")
301-
mpps_instance_uid: Optional[str] = field(
302-
default=None, doc="Modality Performed Procedure Step (MPPS) instance UID if available."
303-
)
304-
305-
306281
class WorklistItemNotFoundError(Exception):
307282
"""Raised when a worklist item is not found in storage."""
308283

tests/services/mwl/test_c_find.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -215,23 +215,45 @@ def test_call_handles_storage_exception(self, handler, mock_storage, mock_event)
215215
assert status == FAILURE
216216
assert ds is None
217217

218-
def test_build_worklist_response_missing_optional_fields(self, handler):
219-
minimal_item = WorklistItem(
220-
accession_number="ACC001",
221-
patient_id="9876543210",
222-
patient_name="TEST^PATIENT",
223-
patient_birth_date="19800101",
224-
scheduled_date="20260107",
225-
scheduled_time="100000",
226-
modality="MG",
227-
)
218+
def test_call_return_key_attributes_present(self, handler, mock_storage, mock_event, sample_worklist_item):
219+
worklist_item = WorklistItem(**sample_worklist_item)
220+
mock_storage.find_worklist_items.return_value = [worklist_item]
228221

229-
ds = handler._build_worklist_response(minimal_item)
222+
results = list(handler.call(mock_event))
230223

231-
# Required fields present
224+
assert len(results) == 2
225+
status, ds = results[0]
226+
assert status == PENDING
232227
assert ds.PatientID == "9876543210"
233228
assert ds.AccessionNumber == "ACC001"
234-
# Optional fields absent
235-
assert not hasattr(ds, "PatientSex")
236-
assert not hasattr(ds, "StudyInstanceUID")
237-
assert not hasattr(ds, "StudyDescription")
229+
assert ds.PatientName == "TEST^PATIENT"
230+
assert ds.PatientBirthDate == "19800101"
231+
232+
assert ds.PatientAddress is None
233+
assert ds.PatientComments is None
234+
assert ds.PatientWeight is None
235+
assert ds.PatientAge is None
236+
assert ds.PatientSex == "F"
237+
238+
assert ds.StudyDescription == "Bilateral Screening Mammogram"
239+
assert ds.StudyInstanceUID == "1.2.3.4.5" # gitleaks:allow
240+
241+
scheduled_procedure_step = ds.ScheduledProcedureStepSequence[0]
242+
243+
assert scheduled_procedure_step.Modality == "MG"
244+
assert scheduled_procedure_step.ScheduledProcedureStepStartDate == "20260107"
245+
assert scheduled_procedure_step.ScheduledProcedureStepStartTime == "100000"
246+
assert scheduled_procedure_step.ScheduledProcedureStepID == "PROC001"
247+
assert scheduled_procedure_step.ScheduledStationAETitle is None
248+
assert scheduled_procedure_step.ScheduledStationName is None
249+
assert scheduled_procedure_step.ScheduledProtocolCodeSequence[0].CodeValue is None
250+
assert scheduled_procedure_step.ScheduledProtocolCodeSequence[0].CodingSchemeDesignator is None
251+
assert scheduled_procedure_step.ScheduledProtocolCodeSequence[0].CodeMeaning is None
252+
253+
assert ds.ReasonForRequestedProcedureCodeSequence[0].CodeValue is None
254+
assert ds.ReasonForRequestedProcedureCodeSequence[0].CodingSchemeDesignator is None
255+
assert ds.ReasonForRequestedProcedureCodeSequence[0].CodeMeaning is None
256+
257+
assert ds.RequestedProcedureCodeSequence[0].CodeValue == "PROC001"
258+
assert ds.RequestedProcedureCodeSequence[0].CodingSchemeDesignator is None
259+
assert ds.RequestedProcedureCodeSequence[0].CodeMeaning is None

tests/services/test_storage.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import pytest
66
from pydicom.uid import generate_uid
77

8-
from services.storage import MWLStorage, PACSStorage, WorklistItem, WorklistItemNotFoundError
8+
from models import WorklistItem
9+
from services.storage import MWLStorage, PACSStorage, WorklistItemNotFoundError
910

1011

1112
@patch("services.storage.sqlite3")

tests/test_relay_listener.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from websockets.exceptions import ConnectionClosedError
66
from websockets.frames import Close, CloseCode
77

8+
from models import WorklistItem
89
from relay_listener import RelayListener, RelayURI, main
9-
from services.storage import WorklistItem
1010

1111

1212
class TestRelayListener:

0 commit comments

Comments
 (0)