Skip to content

Commit b3b121b

Browse files
committed
Correct multi-value and uid-list matching filters
The multi-value match should be a logical AND of all values. > Such a match is successful only if each and every value in the Key Attribute matches a value in the corresponding Attribute in the entity. https://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.2.2.2.8.html The UID list matching has separate semantics and should be a logical OR. The two value types are now parsed unambiguously.
1 parent 808cbbb commit b3b121b

2 files changed

Lines changed: 39 additions & 11 deletions

File tree

chris_backend/dicomweb/query.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ class MultipleValue[T]:
137137
values: list[T]
138138

139139

140+
@dataclass
141+
class UIDListValue:
142+
values: list[str]
143+
144+
140145
@dataclass
141146
class Attribute:
142147
keyword: str
@@ -174,19 +179,28 @@ def cap_multi_value(self) -> bool:
174179
return False
175180
return self.vm != '1'
176181

182+
def cap_uid_list(self) -> bool:
183+
return self.vr in UID_LIST_VRS
184+
177185
def parse(self, value: str, multi_value: bool = False) -> Any:
178186
if value == '':
179187
return UniversalValue()
180188
if value == '""':
181189
return EmptyValue()
182190
if self.cap_range() and "-" in value:
183191
return self.parse_range(value)
184-
if self.vr in UID_LIST_VRS or (self.cap_multi_value() and multi_value):
192+
if self.cap_multi_value() and multi_value:
185193
parsed_value = self.parse_multiple(value)
186194
length = len(parsed_value.values)
187195
if length == 1:
188196
return parsed_value.values[0]
189197
return parsed_value
198+
if self.cap_uid_list():
199+
uid_list_value = self.parse_uid_list(value)
200+
length = len(uid_list_value.values)
201+
if length == 1:
202+
return uid_list_value.values[0]
203+
return uid_list_value
190204
return self.parse_single(value)
191205

192206
def parse_single(self, value: str) -> Any:
@@ -208,6 +222,9 @@ def parse_range(self, value: str) -> RangeValue:
208222
def parse_multiple(self, value: str) -> MultipleValue:
209223
return MultipleValue([self.parse_single(v) for v in re.split(r'[\\,]', value) if len(v) > 0])
210224

225+
def parse_uid_list(self, value: str) -> UIDListValue:
226+
return UIDListValue([self.parse_single(v) for v in re.split(',', value) if len(v) > 0])
227+
211228

212229
# ---------------------------------------------------------------------------
213230
# Supported Query fields
@@ -304,7 +321,13 @@ def apply(self, qs: QuerySet, search_query: SearchQuery) -> QuerySet:
304321
q |= range_q
305322
continue
306323
case MultipleValue(multi_values):
307-
q |= Q(**{f'{attribute.orm_field}__in': multi_values})
324+
multi_value_q = Q()
325+
for value in multi_values:
326+
multi_value_q &= Q(**{f'{attribute.orm_field}__contains': value})
327+
q |= multi_value_q
328+
continue
329+
case UIDListValue(uid_list):
330+
q |= Q(**{f'{attribute.orm_field}__in': uid_list})
308331
continue
309332
case UniversalValue():
310333
# PS3.4 §C.2.2.2.3 Universal Matching

chris_backend/dicomweb/tests/test_query.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,20 @@ def test_multiple_value_emits_in(self):
277277
# VM>1) has no such field in the resource maps, so drive it with a
278278
# synthetic ModalitiesInStudy (CS VM 1-n) mapped onto a real column. The
279279
# ungated UID-list IN path is covered separately by the real-map tests.
280-
from pacsfiles.models import PACSSeries
281-
attr = Attribute('ModalitiesInStudy', orm_field='Modality')
282-
qf = QueryFilter('series')
280+
from dicomweb.models import PACSStudy
281+
qf = QueryFilter('study')
282+
283+
# TODO: remove monkeypatch after ModalitiesInStudy implemented
284+
attr = Attribute('ModalitiesInStudy', orm_field='PatientName')
283285
qf.tag_map = {attr.tag: attr}
286+
284287
sq = SearchQuery.from_query_dict(
285288
QueryDict('ModalitiesInStudy=CT,MR&multiplevaluematching=true'))
286-
sql = qf.apply(PACSSeries.objects.all(), sq).query.sql_with_params()[0]
287-
self.assertIn('IN (', sql)
289+
sql, params = qf.apply(PACSStudy.objects.all(), sq).query.sql_with_params()
290+
self.assertIn(' AND ', sql)
291+
self.assertIn(' LIKE ', sql)
292+
self.assertIn('%CT%', params)
293+
self.assertIn('%MR%', params)
288294

289295
def test_quoted_empty_skipped_without_flag(self):
290296
self.assertFalse(self._has_where('series', 'PatientName=""'))
@@ -419,17 +425,16 @@ def test_multiple_value_matching_monkeypatch(self):
419425
# multi-value matching path.
420426
# TODO: remove this once the above expected failure is passing
421427
from pacsfiles.models import PACSSeries
422-
self._series(PatientName='A', Modality='CT')
423-
self._series(PatientName='B', Modality='MR')
424-
self._series(PatientName='C', Modality='US')
428+
self._series(PatientName='A', StudyInstanceUID='A.1', Modality=r'CT\MR')
429+
self._series(PatientName='B', StudyInstanceUID='B.1', Modality=r'US')
425430
attr = Attribute('ModalitiesInStudy', orm_field='Modality')
426431
qf = QueryFilter('series')
427432
qf.tag_map = {attr.tag: attr}
428433
sq = SearchQuery.from_query_dict(
429434
QueryDict('ModalitiesInStudy=CT,MR&multiplevaluematching=true'))
430435
names = set(qf.apply(PACSSeries.objects.all(), sq)
431436
.values_list('PatientName', flat=True))
432-
self.assertEqual(names, {'A', 'B'})
437+
self.assertEqual(names, {'A'})
433438

434439
def test_uid_list_matches_multiple_rows(self):
435440
# StudyInstanceUID=<uid>,<uid> (UI VR, ungated IN path via UID_LIST_VRS)

0 commit comments

Comments
 (0)