Skip to content

Commit

Permalink
Merge pull request #1327 from psavery/dicomweb-filters
Browse files Browse the repository at this point in the history
Add DICOMweb filters and "limit" parameter
  • Loading branch information
manthey authored Oct 18, 2023
2 parents 3adbd11 + 0845ef9 commit a468819
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
:param params: Additional parameters required for the import process.
This dictionary may include the following keys:
:auth: (optional) if the DICOMweb server requires authentication,
this should be an authentication handler derived from
requests.auth.AuthBase.
:limit: (optional) limit the number of studies imported.
:search_filters: (optional) a dictionary of additional search
filters to use with dicomweb_client's `search_for_series()`
function.
:auth: (optional) if the DICOMweb server requires authentication,
this should be an authentication handler derived from
requests.auth.AuthBase.
:type params: dict
:param progress: Object on which to record progress if possible.
Expand All @@ -138,6 +139,7 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):

from wsidicom.uid import WSI_SOP_CLASS_UID

limit = params.get('limit')
search_filters = params.get('search_filters', {})

meta = self.assetstore[DICOMWEB_META_KEY]
Expand All @@ -148,7 +150,9 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
series_uid_key = dicom_key_to_tag('SeriesInstanceUID')

# We are only searching for WSI datasets. Ignore all others.
# FIXME: is this actually working?
# FIXME: is this actually working? For the SLIM server at
# https://imagingdatacommons.github.io/slim/, none of the series
# report a SOPClassUID, but we still get all results anyways.
search_filters = {
'SOPClassUID': WSI_SOP_CLASS_UID,
**search_filters,
Expand All @@ -162,8 +166,7 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):

# FIXME: might need to search in chunks for larger web servers
series_results = client.search_for_series(
fields=fields, search_filters=search_filters)

fields=fields, limit=limit, search_filters=search_filters)
items = []
for i, result in enumerate(series_results):
if progress:
Expand Down
30 changes: 28 additions & 2 deletions sources/dicom/large_image_source_dicom/assetstore/rest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from girder.api import access
from girder.api.describe import Description, describeRoute
from girder.api.rest import Resource, loadmodel
Expand Down Expand Up @@ -32,24 +34,46 @@ def _importData(self, assetstore, params):
parent = ModelImporter.model(parentType).load(params['parentId'], force=True,
exc=True)

limit = params.get('limit') or None
if limit is not None:
error_msg = 'Invalid limit'
try:
limit = int(limit)
except ValueError:
raise RestException(error_msg)

if limit < 1:
raise RestException(error_msg)

try:
search_filters = json.loads(params.get('filters') or '{}')
except json.JSONDecodeError as e:
msg = f'Invalid filters: {e}'
raise RestException(msg)

progress = self.boolParam('progress', params, default=False)

adapter = assetstore_utilities.getAssetstoreAdapter(assetstore)

with ProgressContext(
progress, user=user, title='Importing DICOM references',
) as ctx:
adapter.importData(
items = adapter.importData(
parent,
parentType,
{
'search_filters': params.get('filters', {}),
'limit': limit,
'search_filters': search_filters,
'auth': None,
},
ctx,
user,
)

if not items:
msg = 'No DICOM objects matching the search filters were found'
raise RestException(msg)

@access.admin
@loadmodel(model='assetstore')
@describeRoute(
Expand All @@ -61,6 +85,8 @@ def _importData(self, assetstore, params):
.param('parentType', 'The type of the parent object to import into.',
enum=('folder', 'user', 'collection'),
required=False)
.param('limit', 'The maximum number of results to import.',
required=False, dataType='int')
.param('filters', 'Any search parameters to filter DICOM objects.',
required=False)
.param('progress', 'Whether to record progress on this operation ('
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ form.g-dwas-import-form
.input-group-btn
button.g-open-browser.btn.btn-default(type="button")
i.icon-folder-open
label(for="g-dwas-import-limit") Limit (Studies)
input#g-dwas-import-limit.form-control(
type="number", step="1", min="1", value="")
label(for="g-dwas-import-filters") Filters
textarea#g-dwas-import-filters.form-control(rows="10",
placeholder="Valid JSON. e.g.:\n\n{\n \"PatientID\": \"ABC123\"\n}")
.g-validation-failed-message
button.g-submit-assetstore-import.btn.btn-success(type="submit")
i.icon-link-ext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ const AssetstoreImportView = View.extend({
'submit .g-dwas-import-form': function (e) {
e.preventDefault();
this.$('.g-validation-failed-message').empty();
this.$('.g-submit-dwas-import').addClass('disabled');

const parentType = this.$('#g-dwas-import-dest-type').val();
const parentId = this.$('#g-dwas-import-dest-id').val().trim().split(/\s/)[0];
const filters = this.$('#g-dwas-import-filters').val().trim();
const limit = this.$('#g-dwas-import-limit').val().trim();

if (!parentId) {
this.$('.g-validation-failed-message').html('Invalid Destination ID');
return;
}

this.$('.g-submit-dwas-import').addClass('disabled');
this.model.off().on('g:imported', function () {
router.navigate(parentType + '/' + parentId, { trigger: true });
}, this).on('g:error', function (err) {
this.$('.g-submit-dwas-import').removeClass('disabled');
this.$('.g-validation-failed-message').html(err.responseText);
this.$('.g-validation-failed-message').html(err.responseJSON.message);
}, this).dicomwebImport({
parentId,
parentType,
limit,
filters,
progress: true
});
},
Expand Down
68 changes: 67 additions & 1 deletion sources/dicom/test_dicom/web_client_specs/dicomWebSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,75 @@ describe('DICOMWeb assetstore', function () {
}, 'Import page to load');

runs(function () {
// Set the needed options and begin the import
// In the import page, trigger a few errors to check validation.
// Test error when no ID is set.
$('.g-submit-assetstore-import').trigger('click');
});

waitsFor(function () {
return $('.g-validation-failed-message').html() === 'Invalid Destination ID';
}, 'Invalid ID check');

runs(function () {
// Set dest type and dest id
$('#g-dwas-import-dest-type').val(parentType);
$('#g-dwas-import-dest-id').val(parentId);

// Test error for an invalid limit
$('#g-dwas-import-limit').val('1.3');
$('.g-submit-assetstore-import').trigger('click');
});

waitsFor(function () {
return $('.g-validation-failed-message').html() === 'Invalid limit';
}, 'Invalid limit check (float)');

runs(function () {
// Make sure this is cleared (we will be checking the same message).
$('.g-validation-failed-message').html('');

// Test error for negative limit
$('#g-dwas-import-limit').val('-1');
$('.g-submit-assetstore-import').trigger('click');
});

waitsFor(function () {
return $('.g-validation-failed-message').html() === 'Invalid limit';
}, 'Invalid limit check (negative)');

runs(function () {
// Fix the limit
$('#g-dwas-import-limit').val('1');

// Test error for invalid JSON in the filters parameter
const filters = '{';
$('#g-dwas-import-filters').val(filters);
$('.g-submit-assetstore-import').trigger('click');
});

waitsFor(function () {
return $('.g-validation-failed-message').html().startsWith('Invalid filters');
}, 'Invalid filters check');

runs(function () {
// Perform a search where no results are returned
const filters = '{"SeriesInstanceUID": "DOES_NOT_EXIST"}';
$('#g-dwas-import-filters').val(filters);
$('.g-submit-assetstore-import').trigger('click');
});

waitsFor(function () {
const msg = 'No DICOM objects matching the search filters were found';
return $('.g-validation-failed-message').html() === msg;
}, 'No results check');

runs(function () {
// Fix the filters
// We will only import this specific SeriesInstanceUID
const filters = '{"SeriesInstanceUID": "' + verifyItemName + '"}';
$('#g-dwas-import-filters').val(filters);

// This one should work fine
$('.g-submit-assetstore-import').trigger('click');
});

Expand Down

0 comments on commit a468819

Please sign in to comment.