diff --git a/import_tracker/__init__.py b/import_tracker/__init__.py index ff3d0e4..110848e 100644 --- a/import_tracker/__init__.py +++ b/import_tracker/__init__.py @@ -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 @@ -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' ) @@ -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') diff --git a/import_tracker/web_client/main.js b/import_tracker/web_client/main.js index 2e5c407..94b072a 100644 --- a/import_tracker/web_client/main.js +++ b/import_tracker/web_client/main.js @@ -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'; @@ -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( - 'View all past Imports' - ); - - // 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( + 'View all past Imports' + ); + + // 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 diff --git a/import_tracker/web_client/templates/importList.pug b/import_tracker/web_client/templates/importList.pug index 7147c4a..a8b2754 100644 --- a/import_tracker/web_client/templates/importList.pug +++ b/import_tracker/web_client/templates/importList.pug @@ -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 @@ -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 @@ -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 @@ -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') @@ -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 diff --git a/import_tracker/web_client/views/importList.js b/import_tracker/web_client/views/importList.js index edf8354..448d252 100644 --- a/import_tracker/web_client/views/importList.js +++ b/import_tracker/web_client/views/importList.js @@ -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'; @@ -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')); @@ -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; } @@ -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); }); } },