diff --git a/src/services/mwl/c_find.py b/src/services/mwl/c_find.py index ac6a62c6..ce1bda20 100644 --- a/src/services/mwl/c_find.py +++ b/src/services/mwl/c_find.py @@ -44,12 +44,14 @@ def call(self, event: evt.Event) -> Iterator[Tuple[int, Dataset | None]]: query_modality = procedure_sequence[0].get("Modality") query_date = procedure_sequence[0].get("ScheduledProcedureStepStartDate") query_accession_number = identifier.get("AccessionNumber") + query_time = procedure_sequence[0].get("ScheduledProcedureStepStartTime") try: items = self.storage.find_worklist_items( accession_number=query_accession_number if query_accession_number else None, modality=query_modality if query_modality else None, scheduled_date=query_date if query_date else None, + scheduled_time=query_time if query_time else None, patient_id=query_patient_id if query_patient_id else None, ) diff --git a/src/services/storage.py b/src/services/storage.py index 67f8e249..5f563cf2 100644 --- a/src/services/storage.py +++ b/src/services/storage.py @@ -367,6 +367,7 @@ def find_worklist_items( accession_number: Optional[str] = None, modality: Optional[str] = None, scheduled_date: Optional[str] = None, + scheduled_time: Optional[str] = None, patient_id: Optional[str] = None, ) -> List[WorklistItem]: """ @@ -375,7 +376,8 @@ def find_worklist_items( Args: accession_number: Filter by accession number modality: Filter by modality (e.g., "MG") - scheduled_date: Filter by scheduled date (YYYYMMDD) + scheduled_date: Filter by scheduled date (YYYYMMDD, or range like "20240101-20240131") + scheduled_time: Filter by scheduled time (HHMMSS, or range like "080000-170000") patient_id: Filter by patient ID Returns: @@ -399,8 +401,14 @@ def find_worklist_items( params.append(modality) if scheduled_date: - where_clauses.append("scheduled_date = ?") - params.append(scheduled_date) + where_clause, clause_params = self.scheduled_query_clause("scheduled_date", scheduled_date) + where_clauses.append(where_clause) + params.extend(clause_params) + + if scheduled_time: + where_clause, clause_params = self.scheduled_query_clause("scheduled_time", scheduled_time) + where_clauses.append(where_clause) + params.extend(clause_params) if patient_id: where_clauses.append("patient_id = ?") @@ -416,6 +424,27 @@ def find_worklist_items( return [WorklistItem(**row) for row in cursor.fetchall()] + def scheduled_query_clause(self, param_name: str, param_value: str) -> tuple[str, List[str]]: + """ + Helper to build SQL clause for scheduled date/time parameters. + + Args: + param_name: "scheduled_date" or "scheduled_time" + param_value: Value to filter by (e.g., "20240101", "20240101-20240131", "-20240131", "20240101-") + + Returns: + Tuple of (SQL clause string, list of parameters) + """ + if param_value.endswith("-"): + return f"{param_name} >= ?", [param_value[:-1].strip()] + elif param_value.startswith("-"): + return f"{param_name} <= ?", [param_value[1:].strip()] + elif "-" in param_value: + start, end = [s.strip() for s in param_value.split("-", 1)] + return f"{param_name} >= ? AND {param_name} <= ?", [start, end] + else: + return f"{param_name} = ?", [param_value.strip()] + def get_worklist_item(self, accession_number: str) -> Optional[WorklistItem]: """ Get a single WorklistItem instance by accession number. diff --git a/tests/integration/test_c_find_returns_worklist_items.py b/tests/integration/test_c_find_returns_worklist_items.py index 921a07be..ceee4a7e 100644 --- a/tests/integration/test_c_find_returns_worklist_items.py +++ b/tests/integration/test_c_find_returns_worklist_items.py @@ -40,7 +40,7 @@ def with_worklist_items(self, storage): patient_name="JONES^MARY", patient_birth_date="19900202", patient_sex="F", - scheduled_date="20240102", + scheduled_date="20240302", scheduled_time="094500", modality="MG", procedure_code="12345-6", @@ -89,15 +89,15 @@ def test_cfind_returns_scheduled_items(self, event, storage): assert ds.StudyDescription == "MAMMOGRAPHY" assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepDescription == "MAMMOGRAPHY" - assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240102" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240302" assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "094500" status, ds = results[2] assert status == SUCCESS assert ds is None - def test_cfind_filters_by_scheduled_date(self, event, storage): - event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240101" + def test_cfind_filters_by_scheduled_date_range(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240101-20240201" results = list(CFind(storage).call(event)) @@ -118,7 +118,37 @@ def test_cfind_filters_by_scheduled_date(self, event, storage): def test_cfind_filters_by_accession_number(self, event, storage): event.identifier.AccessionNumber = "ACC234567" + results = list(CFind(storage).call(event)) + assert len(results) == 2 + status, ds = results[0] + assert status == PENDING + assert ds.PatientID == "999234567" + assert ds.PatientName == "JONES^MARY" + assert ds.PatientBirthDate == "19900202" + assert ds.AccessionNumber == "ACC234567" + + def test_cfind_filters_by_before_scheduled_date(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "-20240101" + + results = list(CFind(storage).call(event)) + + assert len(results) == 2 + + status, ds = results[0] + assert status == PENDING + assert ds.PatientID == "999123456" + assert ds.PatientName == "SMITH^JANE" + assert ds.PatientBirthDate == "19800101" + assert ds.AccessionNumber == "ACC123456" + assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240101" + + status, ds = results[1] + assert status == SUCCESS + assert ds is None + def test_cfind_filters_by_after_scheduled_date(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240201-" results = list(CFind(storage).call(event)) assert len(results) == 2 @@ -127,7 +157,92 @@ def test_cfind_filters_by_accession_number(self, event, storage): assert status == PENDING assert ds.PatientID == "999234567" assert ds.PatientName == "JONES^MARY" + assert ds.PatientBirthDate == "19900202" assert ds.AccessionNumber == "ACC234567" + assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240302" + + status, ds = results[1] + assert status == SUCCESS + assert ds is None + + def test_cfind_filters_by_scheduled_time_range(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "090000-093000" + + results = list(CFind(storage).call(event)) + + assert len(results) == 2 + + status, ds = results[0] + assert status == PENDING + assert ds.PatientID == "999123456" + assert ds.PatientName == "SMITH^JANE" + assert ds.PatientBirthDate == "19800101" + assert ds.AccessionNumber == "ACC123456" + assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "090000" + + status, ds = results[1] + assert status == SUCCESS + assert ds is None + + def test_cfind_filters_by_before_scheduled_time(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "-093000" + + results = list(CFind(storage).call(event)) + + assert len(results) == 2 + + status, ds = results[0] + assert status == PENDING + assert ds.PatientID == "999123456" + assert ds.PatientName == "SMITH^JANE" + assert ds.PatientBirthDate == "19800101" + assert ds.AccessionNumber == "ACC123456" + assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "090000" + + status, ds = results[1] + assert status == SUCCESS + assert ds is None + + def test_cfind_filters_by_after_scheduled_time(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "093000-" + + results = list(CFind(storage).call(event)) + + assert len(results) == 2 + + status, ds = results[0] + assert status == PENDING + assert ds.PatientID == "999234567" + assert ds.PatientName == "JONES^MARY" + assert ds.PatientBirthDate == "19900202" + assert ds.AccessionNumber == "ACC234567" + assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "094500" + + status, ds = results[1] + assert status == SUCCESS + assert ds is None + + def test_cfind_filters_by_date_and_time_range(self, event, storage): + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate = "20240101-20240201" + event.identifier.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime = "090000-093000" + + results = list(CFind(storage).call(event)) + + assert len(results) == 2 + + status, ds = results[0] + assert status == PENDING + assert ds.PatientID == "999123456" + assert ds.PatientName == "SMITH^JANE" + assert ds.PatientBirthDate == "19800101" + assert ds.AccessionNumber == "ACC123456" + assert ds.ScheduledProcedureStepSequence[0].Modality == "MG" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate == "20240101" + assert ds.ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartTime == "090000" status, ds = results[1] assert status == SUCCESS diff --git a/tests/services/mwl/test_c_find.py b/tests/services/mwl/test_c_find.py index 36d8c358..fd13eb92 100644 --- a/tests/services/mwl/test_c_find.py +++ b/tests/services/mwl/test_c_find.py @@ -114,7 +114,7 @@ def test_call_with_accession_number_filter(self, handler, mock_storage, mock_eve list(handler.call(mock_event)) mock_storage.find_worklist_items.assert_called_once_with( - accession_number="ACC12345", modality=None, scheduled_date=None, patient_id=None + accession_number="ACC12345", modality=None, scheduled_date=None, scheduled_time=None, patient_id=None ) 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): list(handler.call(mock_event)) mock_storage.find_worklist_items.assert_called_once_with( - accession_number=None, modality="MG", scheduled_date=None, patient_id=None + accession_number=None, modality="MG", scheduled_date=None, scheduled_time=None, patient_id=None ) 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): list(handler.call(mock_event)) mock_storage.find_worklist_items.assert_called_once_with( - accession_number=None, modality=None, scheduled_date="20260107", patient_id=None + accession_number=None, modality=None, scheduled_date="20260107", scheduled_time=None, patient_id=None + ) + + def test_call_with_time_filter(self, handler, mock_storage, mock_event): + sps_item = Dataset() + sps_item.ScheduledProcedureStepStartTime = "100000" + mock_event.identifier.ScheduledProcedureStepSequence = [sps_item] + mock_storage.find_worklist_items.return_value = [] + + list(handler.call(mock_event)) + + mock_storage.find_worklist_items.assert_called_once_with( + accession_number=None, modality=None, scheduled_date=None, scheduled_time="100000", patient_id=None ) 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): list(handler.call(mock_event)) mock_storage.find_worklist_items.assert_called_once_with( - accession_number=None, modality=None, scheduled_date=None, patient_id="9876543210" + accession_number=None, modality=None, scheduled_date=None, scheduled_time=None, patient_id="9876543210" ) def test_call_handles_storage_exception(self, handler, mock_storage, mock_event): diff --git a/tests/services/test_storage.py b/tests/services/test_storage.py index f2e28cef..8627ee38 100644 --- a/tests/services/test_storage.py +++ b/tests/services/test_storage.py @@ -276,6 +276,137 @@ def test_find_worklist_items_with_multiple_filters(self, mock_db, tmp_dir): ["ACC123456", "MG", "20240101", "999123456"], ) + def test_find_worklist_items_with_date_range(self, mock_db, tmp_dir): + mock_cursor = MagicMock() + mock_cursor.fetchall.return_value = [] + mock_connection = MagicMock() + mock_connection.execute.return_value = mock_cursor + mock_db.connect.return_value = mock_connection + + subject = MWLStorage(tmp_dir) + mock_connection.reset_mock() + + subject.find_worklist_items(scheduled_date="20240101 - 20240131") + + mock_connection.execute.assert_called_once_with( + ( + "SELECT accession_number, modality, patient_birth_date, patient_id, " + "patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, " + "source_message_id, study_description, study_instance_uid, status, mpps_instance_uid " + "FROM worklist_items WHERE scheduled_date >= ? AND scheduled_date <= ? " + "ORDER BY scheduled_date, scheduled_time" + ), + ["20240101", "20240131"], + ) + + def test_find_worklist_items_with_open_ended_date_range(self, mock_db, tmp_dir): + mock_cursor = MagicMock() + mock_cursor.fetchall.return_value = [] + mock_connection = MagicMock() + mock_connection.execute.return_value = mock_cursor + mock_db.connect.return_value = mock_connection + + subject = MWLStorage(tmp_dir) + mock_connection.reset_mock() + + subject.find_worklist_items(scheduled_date="20240101 -") + + mock_connection.execute.assert_called_once_with( + ( + "SELECT accession_number, modality, patient_birth_date, patient_id, " + "patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, " + "source_message_id, study_description, study_instance_uid, status, mpps_instance_uid " + "FROM worklist_items WHERE scheduled_date >= ? " + "ORDER BY scheduled_date, scheduled_time" + ), + ["20240101"], + ) + + mock_connection.reset_mock() + + subject.find_worklist_items(scheduled_date="-20240101") + + mock_connection.execute.assert_called_once_with( + ( + "SELECT accession_number, modality, patient_birth_date, patient_id, " + "patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, " + "source_message_id, study_description, study_instance_uid, status, mpps_instance_uid " + "FROM worklist_items WHERE scheduled_date <= ? " + "ORDER BY scheduled_date, scheduled_time" + ), + ["20240101"], + ) + + def test_find_worklist_items_with_time_range(self, mock_db, tmp_dir): + mock_cursor = MagicMock() + mock_cursor.fetchall.return_value = [] + mock_connection = MagicMock() + mock_connection.execute.return_value = mock_cursor + mock_db.connect.return_value = mock_connection + + subject = MWLStorage(tmp_dir) + mock_connection.reset_mock() + + subject.find_worklist_items(scheduled_time="090000 - 170000") + + mock_connection.execute.assert_called_once_with( + ( + "SELECT accession_number, modality, patient_birth_date, patient_id, " + "patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, " + "source_message_id, study_description, study_instance_uid, status, mpps_instance_uid " + "FROM worklist_items WHERE scheduled_time >= ? AND scheduled_time <= ? " + "ORDER BY scheduled_date, scheduled_time" + ), + ["090000", "170000"], + ) + + def test_find_worklist_items_with_open_ended_time_range(self, mock_db, tmp_dir): + mock_cursor = MagicMock() + mock_cursor.fetchall.return_value = [] + mock_connection = MagicMock() + mock_connection.execute.return_value = mock_cursor + mock_db.connect.return_value = mock_connection + + subject = MWLStorage(tmp_dir) + mock_connection.reset_mock() + + subject.find_worklist_items(scheduled_time="090000 -") + + mock_connection.execute.assert_called_once_with( + ( + "SELECT accession_number, modality, patient_birth_date, patient_id, " + "patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, " + "source_message_id, study_description, study_instance_uid, status, mpps_instance_uid " + "FROM worklist_items WHERE scheduled_time >= ? " + "ORDER BY scheduled_date, scheduled_time" + ), + ["090000"], + ) + + def test_find_worklist_items_with_date_and_time_range(self, mock_db, tmp_dir): + mock_cursor = MagicMock() + mock_cursor.fetchall.return_value = [] + mock_connection = MagicMock() + mock_connection.execute.return_value = mock_cursor + mock_db.connect.return_value = mock_connection + + subject = MWLStorage(tmp_dir) + mock_connection.reset_mock() + + subject.find_worklist_items(scheduled_date="20240101 - 20240131", scheduled_time="090000 - 170000") + + mock_connection.execute.assert_called_once_with( + ( + "SELECT accession_number, modality, patient_birth_date, patient_id, " + "patient_name, patient_sex, procedure_code, scheduled_date, scheduled_time, " + "source_message_id, study_description, study_instance_uid, status, mpps_instance_uid " + "FROM worklist_items WHERE scheduled_date >= ? AND scheduled_date <= ? " + "AND scheduled_time >= ? AND scheduled_time <= ? " + "ORDER BY scheduled_date, scheduled_time" + ), + ["20240101", "20240131", "090000", "170000"], + ) + def test_get_worklist_item(self, mock_db, tmp_dir, result): mock_cursor = MagicMock() mock_cursor.fetchone.return_value = result