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

Extend DICOMweb assetstore support #28

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
114 changes: 12 additions & 102 deletions import_tracker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time

from girder import logger, plugin
from girder import plugin
from girder.api.describe import autoDescribeRoute
from girder.api.rest import boundHandler
from girder.constants import AccessType
Expand Down Expand Up @@ -28,30 +28,34 @@ def wrapImportData(assetstoreResource):
def importDataWrapper(
self, assetstore, importPath, destinationId, destinationType,
progress, leafFoldersAsItems, fileIncludeRegex, fileExcludeRegex,
excludeExisting):
excludeExisting, **kwargs):
# We don't actually wrap importData, as it would be excessive
# monkey-patching to make the import trackable and cancelable.
user = self.getCurrentUser()
parent = ModelImporter.model(destinationType).load(
destinationId, user=user, level=AccessType.ADMIN, exc=True)

# create a job, set it to running
# alter ProgressContext so update updates the job or raises if
# canceling
# Capture any additional parameters passed to route
extraParams = kwargs.get('params', {})

params = {
'destinationId': destinationId,
'destinationType': destinationType,
'importPath': importPath,
'leafFoldersAsItems': str(leafFoldersAsItems).lower(),
'progress': str(progress).lower(),
**extraParams
}

if fileIncludeRegex:
params['fileIncludeRegex'] = fileIncludeRegex
if fileExcludeRegex:
params['fileExcludeRegex'] = fileExcludeRegex
if excludeExisting:
params['excludeExisting'] = str(excludeExisting).lower()

importRecord = AssetstoreImport().createAssetstoreImport(assetstore, params)

job = Job().createJob(
title='Import from %s : %s' % (assetstore['name'], importPath),
type='assetstore_import',
Expand All @@ -63,6 +67,7 @@ def importDataWrapper(
time.strftime('%Y-%m-%d %H:%M:%S'),
assetstore['name'], importPath,
), status=JobStatus.RUNNING)

try:
with ProgressContext(progress, user=user, title='Importing data') as ctx:
try:
Expand All @@ -76,10 +81,7 @@ def importDataWrapper(
self._model.importData(
assetstore, parent=parent, parentType=destinationType,
params={
'fileIncludeRegex': fileIncludeRegex,
'fileExcludeRegex': fileExcludeRegex,
'importPath': importPath,
'excludeExisting': excludeExisting,
**params,
'_job': jobRec,
},
progress=ctx, user=user,
Expand All @@ -103,6 +105,7 @@ def importDataWrapper(
exc,
), status=JobStatus.ERROR)
success = False

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

Expand Down Expand Up @@ -158,100 +161,12 @@ 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, AttributeError):
pass
ModelImporter.registerModel(
'assetstoreImport', AssetstoreImport, 'import_tracker'
)
Expand All @@ -263,8 +178,3 @@ 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')
97 changes: 53 additions & 44 deletions import_tracker/web_client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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';
import DICOMwebImportView from '@girder/dicomweb/views/DICOMwebImportView';

import CollectionModel from '@girder/core/models/CollectionModel';
import FolderModel from '@girder/core/models/FolderModel';
Expand Down Expand Up @@ -54,21 +55,26 @@ wrap(S3ImportView, 'render', function (render) {

this.$('.form-group').last().after(excludeExistingInput({ type: 's3' }));
});
wrap(DICOMwebImportView, 'render', function (render) {
render.call(this);

this.$('.form-group').last().after(excludeExistingInput({ type: 'dwas' }));
});

const setBrowserRoot = (view, type) => {
const browserWidget = view._browserWidgetView;
const destType = view.$(`#g-${type}-import-dest-type`).val();
const destId = view.$(`#g-${type}-import-dest-id`).val();
const resourceId = destId.trim().split(/\s/)[0];
const destinationType = view.$(`#g-${type}-import-dest-type`).val();
const destinationId = view.$(`#g-${type}-import-dest-id`).val();
const resourceId = destinationId.trim().split(/\s/)[0];

const models = {
collection: CollectionModel,
folder: FolderModel,
user: UserModel
};

if (resourceId && destType in models) {
const model = new models[destType]({ _id: resourceId });
if (resourceId && destinationType in models) {
const model = new models[destinationType]({ _id: resourceId });

model.once('g:fetched', () => {
if (!browserWidget.root || browserWidget.root.id !== model.id) {
Expand All @@ -79,7 +85,7 @@ const setBrowserRoot = (view, type) => {
if (err.status === 400) {
events.trigger('g:alert', {
icon: 'cancel',
text: `No ${destType.toUpperCase()} with ID ${resourceId} found.`,
text: `No ${destinationType.toUpperCase()} with ID ${resourceId} found.`,
type: 'danger',
timeout: 4000
});
Expand All @@ -97,52 +103,55 @@ wrap(S3ImportView, '_openBrowser', function (_openBrowser) {
setBrowserRoot(this, 's3');
_openBrowser.call(this);
});
wrap(DICOMwebImportView, '_openBrowser', function (_openBrowser) {
setBrowserRoot(this, 'dwas');
_openBrowser.call(this);
});

// We can't just wrap the submit events, as we need to modify what is passed to
// the assetstore import method
const importSubmit = (view, type) => {
const destinationId = view.$(`#g-${type}-import-dest-id`).val().trim().split(/\s/)[0];
const destinationType = view.$(`#g-${type}-import-dest-type`).val();
const excludeExisting = view.$(`#g-${type}-import-exclude-existing`).val();

const importParams = { destinationId, destinationType, excludeExisting, progress: true };

if (type === 'filesystem') {
importParams.leafFoldersAsItems = view.$(`#g-${type}-import-leaf-items`).val();
}
if (type === 'dwas') {
importParams.filters = view.$(`#g-${type}-import-filters`).val().trim();
importParams.limit = view.$(`#g-${type}-import-limit`).val().trim();
} else {
importParams.importPath = view.$(`#g-${type}-import-path`).val().trim();
}

view.$('.g-validation-failed-message').empty();
view.$(`.g-submit-${type}-import`).addClass('disabled');

let model = view.assetstore;
model = model.off().on('g:imported', function () {
router.navigate(destinationType + '/' + destinationId, { trigger: true });
}, view).on('g:error', function (err) {
view.$(`.g-submit-${type}-import`).removeClass('disabled');
view.$('.g-validation-failed-message').html(err.responseJSON.message);
}, view);

model.import(importParams);
};

FilesystemImportView.prototype.events['submit .g-filesystem-import-form'] = function (e) {
e.preventDefault();

var destId = this.$('#g-filesystem-import-dest-id').val().trim().split(/\s/)[0],
destType = this.$('#g-filesystem-import-dest-type').val(),
foldersAsItems = this.$('#g-filesystem-import-leaf-items').val(),
excludeExisting = this.$('#g-filesystem-import-exclude-existing').val();

this.$('.g-validation-failed-message').empty();

this.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({
importPath: this.$('#g-filesystem-import-path').val().trim(),
leafFoldersAsItems: foldersAsItems,
destinationId: destId,
destinationType: destType,
excludeExisting: excludeExisting,
progress: true
});
importSubmit(this, 'filesystem');
};
S3ImportView.prototype.events['submit .g-s3-import-form'] = function (e) {
e.preventDefault();

var destId = this.$('#g-s3-import-dest-id').val().trim().split(/\s/)[0],
destType = this.$('#g-s3-import-dest-type').val(),
excludeExisting = this.$('#g-s3-import-exclude-existing').val();

this.$('.g-validation-failed-message').empty();

this.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({
importPath: this.$('#g-s3-import-path').val().trim(),
destinationId: destId,
destinationType: destType,
excludeExisting: excludeExisting,
progress: true
});
importSubmit(this, 's3');
};
DICOMwebImportView.prototype.events['submit .g-dwas-import-form'] = function (e) {
e.preventDefault();
importSubmit(this, 'dwas');
};

// Setup router to assetstore imports view
Expand Down
16 changes: 3 additions & 13 deletions import_tracker/web_client/views/importList.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PaginateWidget from '@girder/core/views/widgets/PaginateWidget';
import Collection from '@girder/core/collections/Collection';

import AssetstoreModel from '@girder/core/models/AssetstoreModel';
import { AssetstoreType, SORT_DESC } from '@girder/core/constants';
import { SORT_DESC } from '@girder/core/constants';
import View from '@girder/core/views/View';
import router from '@girder/core/router';
import { restRequest } from '@girder/core/rest';
Expand Down Expand Up @@ -34,11 +34,7 @@ var importList = View.extend({
}, this);

assetstore.once('g:fetched', () => {
if (assetstore.get('type') === AssetstoreType.DICOMWEB) {
assetstore.dicomwebImport(importEvent.get('params'));
} else {
assetstore.import(importEvent.get('params'));
}
assetstore.import(importEvent.get('params'));
}).fetch();
},
'click .re-import-edit-btn': function (e) {
Expand All @@ -52,13 +48,7 @@ var importList = View.extend({
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 });
}
router.navigate(`assetstore/${assetstoreId}/re-import/${importId}`, { trigger: true });
}).fetch();
};

Expand Down
Loading