Skip to content

Commit 95fa43c

Browse files
authored
Merge pull request #49 from NHSDigital/feat/support-date-range-c-find-queries
Support date ranges in C-FIND query identifier
2 parents 4201ce2 + 9352a19 commit 95fa43c

5 files changed

Lines changed: 300 additions & 11 deletions

File tree

src/services/mwl/c_find.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ def call(self, event: evt.Event) -> Iterator[Tuple[int, Dataset | None]]:
4444
query_modality = procedure_sequence[0].get("Modality")
4545
query_date = procedure_sequence[0].get("ScheduledProcedureStepStartDate")
4646
query_accession_number = identifier.get("AccessionNumber")
47+
query_time = procedure_sequence[0].get("ScheduledProcedureStepStartTime")
4748

4849
try:
4950
items = self.storage.find_worklist_items(
5051
accession_number=query_accession_number if query_accession_number else None,
5152
modality=query_modality if query_modality else None,
5253
scheduled_date=query_date if query_date else None,
54+
scheduled_time=query_time if query_time else None,
5355
patient_id=query_patient_id if query_patient_id else None,
5456
)
5557

src/services/storage.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ def find_worklist_items(
367367
accession_number: Optional[str] = None,
368368
modality: Optional[str] = None,
369369
scheduled_date: Optional[str] = None,
370+
scheduled_time: Optional[str] = None,
370371
patient_id: Optional[str] = None,
371372
) -> List[WorklistItem]:
372373
"""
@@ -375,7 +376,8 @@ def find_worklist_items(
375376
Args:
376377
accession_number: Filter by accession number
377378
modality: Filter by modality (e.g., "MG")
378-
scheduled_date: Filter by scheduled date (YYYYMMDD)
379+
scheduled_date: Filter by scheduled date (YYYYMMDD, or range like "20240101-20240131")
380+
scheduled_time: Filter by scheduled time (HHMMSS, or range like "080000-170000")
379381
patient_id: Filter by patient ID
380382
381383
Returns:
@@ -399,8 +401,14 @@ def find_worklist_items(
399401
params.append(modality)
400402

401403
if scheduled_date:
402-
where_clauses.append("scheduled_date = ?")
403-
params.append(scheduled_date)
404+
where_clause, clause_params = self.scheduled_query_clause("scheduled_date", scheduled_date)
405+
where_clauses.append(where_clause)
406+
params.extend(clause_params)
407+
408+
if scheduled_time:
409+
where_clause, clause_params = self.scheduled_query_clause("scheduled_time", scheduled_time)
410+
where_clauses.append(where_clause)
411+
params.extend(clause_params)
404412

405413
if patient_id:
406414
where_clauses.append("patient_id = ?")
@@ -416,6 +424,27 @@ def find_worklist_items(
416424

417425
return [WorklistItem(**row) for row in cursor.fetchall()]
418426

427+
def scheduled_query_clause(self, param_name: str, param_value: str) -> tuple[str, List[str]]:
428+
"""
429+
Helper to build SQL clause for scheduled date/time parameters.
430+
431+
Args:
432+
param_name: "scheduled_date" or "scheduled_time"
433+
param_value: Value to filter by (e.g., "20240101", "20240101-20240131", "-20240131", "20240101-")
434+
435+
Returns:
436+
Tuple of (SQL clause string, list of parameters)
437+
"""
438+
if param_value.endswith("-"):
439+
return f"{param_name} >= ?", [param_value[:-1].strip()]
440+
elif param_value.startswith("-"):
441+
return f"{param_name} <= ?", [param_value[1:].strip()]
442+
elif "-" in param_value:
443+
start, end = [s.strip() for s in param_value.split("-", 1)]
444+
return f"{param_name} >= ? AND {param_name} <= ?", [start, end]
445+
else:
446+
return f"{param_name} = ?", [param_value.strip()]
447+
419448
def get_worklist_item(self, accession_number: str) -> Optional[WorklistItem]:
420449
"""
421450
Get a single WorklistItem instance by accession number.

tests/integration/test_c_find_returns_worklist_items.py

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def with_worklist_items(self, storage):
4040
patient_name="JONES^MARY",
4141
patient_birth_date="19900202",
4242
patient_sex="F",
43-
scheduled_date="20240102",
43+
scheduled_date="20240302",
4444
scheduled_time="094500",
4545
modality="MG",
4646
procedure_code="12345-6",
@@ -89,15 +89,15 @@ def test_cfind_returns_scheduled_items(self, event, storage):
8989
assert ds.StudyDescription == "MAMMOGRAPHY"
9090
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
9191
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepDescription == "MAMMOGRAPHY"
92-
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240102"
92+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240302"
9393
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "094500"
9494

9595
status, ds = results[2]
9696
assert status == SUCCESS
9797
assert ds is None
9898

99-
def test_cfind_filters_by_scheduled_date(self, event, storage):
100-
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240101"
99+
def test_cfind_filters_by_scheduled_date_range(self, event, storage):
100+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240101-20240201"
101101

102102
results = list(CFind(storage).call(event))
103103

@@ -118,7 +118,37 @@ def test_cfind_filters_by_scheduled_date(self, event, storage):
118118

119119
def test_cfind_filters_by_accession_number(self, event, storage):
120120
event.identifier.AccessionNumber = "ACC234567"
121+
results = list(CFind(storage).call(event))
122+
assert len(results) == 2
123+
status, ds = results[0]
124+
assert status == PENDING
125+
assert ds.PatientID == "999234567"
126+
assert ds.PatientName == "JONES^MARY"
127+
assert ds.PatientBirthDate == "19900202"
128+
assert ds.AccessionNumber == "ACC234567"
129+
130+
def test_cfind_filters_by_before_scheduled_date(self, event, storage):
131+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "-20240101"
132+
133+
results = list(CFind(storage).call(event))
134+
135+
assert len(results) == 2
136+
137+
status, ds = results[0]
138+
assert status == PENDING
139+
assert ds.PatientID == "999123456"
140+
assert ds.PatientName == "SMITH^JANE"
141+
assert ds.PatientBirthDate == "19800101"
142+
assert ds.AccessionNumber == "ACC123456"
143+
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
144+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240101"
145+
146+
status, ds = results[1]
147+
assert status == SUCCESS
148+
assert ds is None
121149

150+
def test_cfind_filters_by_after_scheduled_date(self, event, storage):
151+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240201-"
122152
results = list(CFind(storage).call(event))
123153

124154
assert len(results) == 2
@@ -127,7 +157,92 @@ def test_cfind_filters_by_accession_number(self, event, storage):
127157
assert status == PENDING
128158
assert ds.PatientID == "999234567"
129159
assert ds.PatientName == "JONES^MARY"
160+
assert ds.PatientBirthDate == "19900202"
130161
assert ds.AccessionNumber == "ACC234567"
162+
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
163+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240302"
164+
165+
status, ds = results[1]
166+
assert status == SUCCESS
167+
assert ds is None
168+
169+
def test_cfind_filters_by_scheduled_time_range(self, event, storage):
170+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "090000-093000"
171+
172+
results = list(CFind(storage).call(event))
173+
174+
assert len(results) == 2
175+
176+
status, ds = results[0]
177+
assert status == PENDING
178+
assert ds.PatientID == "999123456"
179+
assert ds.PatientName == "SMITH^JANE"
180+
assert ds.PatientBirthDate == "19800101"
181+
assert ds.AccessionNumber == "ACC123456"
182+
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
183+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "090000"
184+
185+
status, ds = results[1]
186+
assert status == SUCCESS
187+
assert ds is None
188+
189+
def test_cfind_filters_by_before_scheduled_time(self, event, storage):
190+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "-093000"
191+
192+
results = list(CFind(storage).call(event))
193+
194+
assert len(results) == 2
195+
196+
status, ds = results[0]
197+
assert status == PENDING
198+
assert ds.PatientID == "999123456"
199+
assert ds.PatientName == "SMITH^JANE"
200+
assert ds.PatientBirthDate == "19800101"
201+
assert ds.AccessionNumber == "ACC123456"
202+
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
203+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "090000"
204+
205+
status, ds = results[1]
206+
assert status == SUCCESS
207+
assert ds is None
208+
209+
def test_cfind_filters_by_after_scheduled_time(self, event, storage):
210+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "093000-"
211+
212+
results = list(CFind(storage).call(event))
213+
214+
assert len(results) == 2
215+
216+
status, ds = results[0]
217+
assert status == PENDING
218+
assert ds.PatientID == "999234567"
219+
assert ds.PatientName == "JONES^MARY"
220+
assert ds.PatientBirthDate == "19900202"
221+
assert ds.AccessionNumber == "ACC234567"
222+
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
223+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "094500"
224+
225+
status, ds = results[1]
226+
assert status == SUCCESS
227+
assert ds is None
228+
229+
def test_cfind_filters_by_date_and_time_range(self, event, storage):
230+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240101-20240201"
231+
event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "090000-093000"
232+
233+
results = list(CFind(storage).call(event))
234+
235+
assert len(results) == 2
236+
237+
status, ds = results[0]
238+
assert status == PENDING
239+
assert ds.PatientID == "999123456"
240+
assert ds.PatientName == "SMITH^JANE"
241+
assert ds.PatientBirthDate == "19800101"
242+
assert ds.AccessionNumber == "ACC123456"
243+
assert ds.ScheduledProcedureStepSequence[0].Modality == "MG"
244+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240101"
245+
assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "090000"
131246

132247
status, ds = results[1]
133248
assert status == SUCCESS

tests/services/mwl/test_c_find.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def test_call_with_accession_number_filter(self, handler, mock_storage, mock_eve
114114
list(handler.call(mock_event))
115115

116116
mock_storage.find_worklist_items.assert_called_once_with(
117-
accession_number="ACC12345", modality=None, scheduled_date=None, patient_id=None
117+
accession_number="ACC12345", modality=None, scheduled_date=None, scheduled_time=None, patient_id=None
118118
)
119119

120120
def test_call_with_modality_filter(self, handler, mock_storage, mock_event):
@@ -127,7 +127,7 @@ def test_call_with_modality_filter(self, handler, mock_storage, mock_event):
127127
list(handler.call(mock_event))
128128

129129
mock_storage.find_worklist_items.assert_called_once_with(
130-
accession_number=None, modality="MG", scheduled_date=None, patient_id=None
130+
accession_number=None, modality="MG", scheduled_date=None, scheduled_time=None, patient_id=None
131131
)
132132

133133
def test_call_with_date_filter(self, handler, mock_storage, mock_event):
@@ -139,7 +139,19 @@ def test_call_with_date_filter(self, handler, mock_storage, mock_event):
139139
list(handler.call(mock_event))
140140

141141
mock_storage.find_worklist_items.assert_called_once_with(
142-
accession_number=None, modality=None, scheduled_date="20260107", patient_id=None
142+
accession_number=None, modality=None, scheduled_date="20260107", scheduled_time=None, patient_id=None
143+
)
144+
145+
def test_call_with_time_filter(self, handler, mock_storage, mock_event):
146+
sps_item = Dataset()
147+
sps_item.ScheduledProcedureStepStartTime = "100000"
148+
mock_event.identifier.ScheduledProcedureStepSequence = [sps_item]
149+
mock_storage.find_worklist_items.return_value = []
150+
151+
list(handler.call(mock_event))
152+
153+
mock_storage.find_worklist_items.assert_called_once_with(
154+
accession_number=None, modality=None, scheduled_date=None, scheduled_time="100000", patient_id=None
143155
)
144156

145157
def test_call_with_patient_id_filter(self, handler, mock_storage, mock_event):
@@ -149,7 +161,7 @@ def test_call_with_patient_id_filter(self, handler, mock_storage, mock_event):
149161
list(handler.call(mock_event))
150162

151163
mock_storage.find_worklist_items.assert_called_once_with(
152-
accession_number=None, modality=None, scheduled_date=None, patient_id="9876543210"
164+
accession_number=None, modality=None, scheduled_date=None, scheduled_time=None, patient_id="9876543210"
153165
)
154166

155167
def test_call_handles_storage_exception(self, handler, mock_storage, mock_event):

0 commit comments

Comments
 (0)