Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial DICOMweb assetstore support #25

Merged
merged 7 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 94 additions & 1 deletion import_tracker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time

from girder import plugin
from girder import logger, plugin
from girder.api.describe import autoDescribeRoute
from girder.api.rest import boundHandler
from girder.constants import AccessType
Expand Down Expand Up @@ -162,12 +162,100 @@ def shouldImportFileWrapper(self, path, params):
AbstractAssetstoreAdapter.shouldImportFile = shouldImportFileWrapper


def wrapDICOMImport(assetstoreResource):
baseImportData = assetstoreResource.importData
baseImportData.description.param(
'excludeExisting',
'If true, then a file with an import path that is already in the '
'system is not imported, even if it is not in the destination '
'hierarchy.', dataType='boolean', required=False, default=False)

@boundHandler(ctx=assetstoreResource)
@autoDescribeRoute(baseImportData.description)
def dwaImportDataWrapper(self, assetstore, destinationId, destinationType, filters,
progress, excludeExisting):

user = self.getCurrentUser()
params = {
'destinationId': destinationId,
'destinationType': destinationType,
'filters': filters,
'progress': str(progress).lower(),
}
if excludeExisting:
params['excludeExisting'] = str(excludeExisting).lower()

importRecord = AssetstoreImport().createAssetstoreImport(assetstore, params)
job = Job().createJob(
title=f'Import from {assetstore["name"]}',
type='assetstore_import',
public=False,
user=user,
kwargs=params,
)
job = Job().updateJob(job, '%s - Starting import from %s\n' % (
time.strftime('%Y-%m-%d %H:%M:%S'),
assetstore['name']
), status=JobStatus.RUNNING)

try:
with ProgressContext(progress, user=user, title='Importing data') as ctx:
try:
jobRec = {
'id': str(job['_id']),
'count': 0,
'skip': 0,
'lastlog': time.time(),
'logcount': 0,
}
self._importData(
assetstore,
params={
**params,
'_job': jobRec},
progress=ctx)

success = True
Job().updateJob(job, '%s - Finished. Checked %d, skipped %d\n' % (
time.strftime('%Y-%m-%d %H:%M:%S'),
jobRec['count'], jobRec['skip'],
), status=JobStatus.SUCCESS)

except ImportTrackerCancelError:
Job().updateJob(job, '%s - Canceled' % (
time.strftime('%Y-%m-%d %H:%M:%S'),
))
success = 'canceled'

except Exception as exc:
Job().updateJob(job, '%s - Failed with %s\n' % (
time.strftime('%Y-%m-%d %H:%M:%S'),
exc,
), status=JobStatus.ERROR)
success = False

importRecord = AssetstoreImport().markEnded(importRecord, success)
return importRecord

for key in {'accessLevel', 'description', 'requiredScopes'}:
setattr(dwaImportDataWrapper, key, getattr(baseImportData, key))

assetstoreResource.importData = dwaImportDataWrapper
assetstoreResource.removeRoute('POST', (':id', 'import'))
assetstoreResource.route('POST', (':id', 'import'), assetstoreResource.importData)


class GirderPlugin(plugin.GirderPlugin):
DISPLAY_NAME = 'import_tracker'
CLIENT_SOURCE_PATH = 'web_client'

def load(self, info):
plugin.getPlugin('jobs').load(info)
try:
import large_image_source_dicom # noqa
plugin.getPlugin('dicomweb').load(info)
except ImportError:
pass
ModelImporter.registerModel(
'assetstoreImport', AssetstoreImport, 'import_tracker'
)
Expand All @@ -178,3 +266,8 @@ def load(self, info):
wrapImportData(info['apiRoot'].assetstore)

info['apiRoot'].folder.route('PUT', (':id', 'move'), moveFolder)

if hasattr(info['apiRoot'], 'dicomweb_assetstore'):
wrapDICOMImport(info['apiRoot'].dicomweb_assetstore)
else:
logger.info('dicomweb_assetstore not found')
29 changes: 19 additions & 10 deletions import_tracker/web_client/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import $ from 'jquery';

import AssetstoreView from '@girder/core/views/body/AssetstoresView';
import FilesystemImportView from '@girder/core/views/body/FilesystemImportView';
import S3ImportView from '@girder/core/views/body/S3ImportView';
Expand All @@ -22,16 +24,23 @@ import './JobStatus';
wrap(AssetstoreView, 'render', function (render) {
// Call the underlying render function that we are wrapping
render.call(this);

this.$el.find('.g-current-assetstores-container .g-body-title').after(
'<a class="g-view-imports btn btn-sm btn-primary" href="#assetstore/all_imports"><i class="icon-link-ext"></i>View all past Imports</a>'
);

// Inject new button into each assetstore
const assetstores = this.collection.toArray();
this.$('.g-assetstore-import-button-container').after(
(i) => importDataButton({ importsPageLink: `#assetstore/${assetstores[i].id}/imports` })
);
// defer adding buttons so optional plugins can render first.
window.setTimeout(() => {
this.$el.find('.g-current-assetstores-container .g-body-title').after(
'<a class="g-view-imports btn btn-sm btn-primary" href="#assetstore/all_imports"><i class="icon-link-ext"></i>View all past Imports</a>'
);

// Inject new button into each assetstore
const assetstores = this.collection;
this.$('.g-assetstore-import-button-container').after(
function () {
// we can't just use the index of the after call, since not
// all assetstores will have import buttons.
const assetstore = assetstores.get($(this).closest('.g-assetstore-buttons').find('[cid]').attr('cid'));
return importDataButton({ importsPageLink: `#assetstore/${assetstore.id}/imports` });
}
);
}, 0);
});

// Add duplicate_files option to Import Asset form
Expand Down
24 changes: 13 additions & 11 deletions import_tracker/web_client/templates/importList.pug
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ table.g-imports-list-table
-
var anyRegex = false;
var anyLeafed = false;
var anyNoProgress;
var anyImportPath = false;
var anyNoProgress = false;
var otherParams = [];
var showCount = false;
for _import in imports
Expand All @@ -31,9 +32,11 @@ table.g-imports-list-table
- anyLeafed = true
if !_import.params.progress
- anyNoProgress = true
- Object.keys(_import.params).forEach((key) => { if (['importPath', 'destinationId', 'destinationType', 'leafFoldersAsItems', 'progress', 'fileIncludeRegex', 'fileExcludeRegex'].indexOf(key) < 0 && otherParams.indexOf(key) < 0) { otherParams.push(key); } });
- Object.keys(_import.params).forEach((key) => { if (['importPath', 'destinationId', 'leafFoldersAsItems', 'progress', 'fileIncludeRegex', 'fileExcludeRegex'].indexOf(key) < 0 && otherParams.indexOf(key) < 0) { otherParams.push(key); } });
if _import._count
- showCount = true;
- showCount = true
if _import.params.importPath
- anyImportPath = true
thead
tr
th Actions
Expand All @@ -42,7 +45,8 @@ table.g-imports-list-table
th Started
th Ended
th Assetstore Name
th Import Path
if anyImportPath
th Import Path
//- th Destination Type
th Destination Path
if anyLeafed
Expand All @@ -59,11 +63,11 @@ table.g-imports-list-table
tr
td
if assetstoreExists[i]
div(style='display: flex; justify-content: flex-end;')
div(style='display: flex; padding-left: 10px; padding-right: 10px;')
button.re-import-btn.btn.btn-sm.btn-success(index=i, disabled=(_import._destinationPath =='does not exist'))
i.icon-cw
| Re-Import
button.re-import-edit-btn.btn.btn-sm.btn-primary(index=i, style='margin-left: 5px;', data-toggle='tooltip', title='Edit Import Parameters')
button.re-import-edit-btn.btn.btn-sm.btn-primary(index=i, style='margin-left: 1em;', data-toggle='tooltip', title='Edit Import Parameters')
i.icon-pencil
if showCount
td(data-id=_import._count, data-toggle='tooltip', title=_import._count !== 1 ? `Imported ${_import._count} times` : 'Imported once')
Expand All @@ -74,11 +78,9 @@ table.g-imports-list-table
span= _import.ended ? moment(_import.ended).format('YYYY-MM-DD HH:mm:ss.SSS') : ''
td(data-id=_import._assetstoreName, data-toggle='tooltip', title=_import._assetstoreName)
span= _import._assetstoreName
td(data-id=_import.params.importPath, data-toggle='tooltip', title=_import.params.importPath)
span= _import.params.importPath
//-
td
span= _import.params.destinationType
if anyImportPath
td(data-id=_import.params.importPath, data-toggle='tooltip', title=_import.params.importPath)
span= _import.params.importPath
td(data-id=_import.params.destinationId, data-toggle='tooltip', title=_import._destinationPath + '\n' + _import.params.destinationId)
span= _import._destinationPath
if anyLeafed
Expand Down
32 changes: 28 additions & 4 deletions import_tracker/web_client/views/importList.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import $ from 'jquery';
import moment from 'moment';

import AssetstoreModel from '@girder/core/models/AssetstoreModel';
import { AssetstoreType } from '@girder/core/constants';
import View from '@girder/core/views/View';
import router from '@girder/core/router';
import { restRequest } from '@girder/core/rest';
Expand All @@ -22,11 +23,20 @@ var importList = View.extend({
const assetstore = new AssetstoreModel({ _id: importEvent.assetstoreId });
const destType = importEvent.params.destinationType;
const destId = importEvent.params.destinationId;

assetstore.off('g:imported').on('g:imported', function () {
router.navigate(destType + '/' + destId, { trigger: true });
}, this).on('g:error', function (resp) {
this.$('.g-validation-failed-message').text(resp.responseJSON.message);
}, this).import(importEvent.params);
}, this);

assetstore.once('g:fetched', () => {
if (assetstore.get('type') === AssetstoreType.DICOMWEB) {
assetstore.dicomwebImport(importEvent.params);
} else {
assetstore.import(importEvent.params);
}
}).fetch();
},
'click .re-import-edit-btn': function (e) {
const index = Number($(e.currentTarget).attr('index'));
Expand All @@ -35,10 +45,24 @@ var importList = View.extend({
return;
}

// Navigate to re-import page
const navigate = (assetstoreId, importId) => {
const assetstore = new AssetstoreModel({ _id: assetstoreId });
assetstore.once('g:fetched', () => {
if (assetstore.get('type') === AssetstoreType.DICOMWEB) {
// Avoid adding previous import data for DICOMweb imports by navigating to blank import
// TODO: Add DICOMweb-specific re-import view
router.navigate(`dicomweb_assetstore/${assetstoreId}/import`, { trigger: true });
} else {
router.navigate(`assetstore/${assetstoreId}/re-import/${importId}`, { trigger: true });
}
}).fetch();
};

const assetstoreId = importEvent.assetstoreId;
const importId = importEvent._id;
const importId = importEvent._id; // Only individual imports have an _id
if (importId) {
router.navigate(`assetstore/${assetstoreId}/re-import/${importId}`, { trigger: true });
navigate(assetstoreId, importId);
return;
}

Expand All @@ -53,7 +77,7 @@ var importList = View.extend({
i.params.destinationType === importEvent.params.destinationType
)[0]._id;

router.navigate(`assetstore/${assetstoreId}/re-import/${importId}`, { trigger: true });
navigate(assetstoreId, importId);
});
}
},
Expand Down