Make the persistence method of hanging protocols configurable

- Implement ProtocolStore interface to allow persisting hanging protocols using different strategies
- Implement and set default strategy to persist hanging protocols in the MongoDB collection “HangingProtocols” in the application server
This commit is contained in:
Evren Ozkan 2017-01-04 17:49:31 -05:00
parent 06412b51bf
commit 77182d1916
17 changed files with 282 additions and 183 deletions

View File

@ -12,7 +12,6 @@ Template.viewer.onCreated(() => {
ViewerData = window.ViewerData || ViewerData;
const instance = Template.instance();
instance.subscribe('hangingprotocols');
ValidationErrors.remove({});

View File

@ -103,13 +103,9 @@ Meteor.startup(() => {
HP.lesionTrackerFollowupProtocol = protoFollowup;
HP.lesionTrackerFollowupProtocol.id = 'lesionTrackerFollowupProtocol';
Meteor.call('removeHangingProtocolByID', HP.lesionTrackerBaselineProtocol.id, function() {
HangingProtocols.insert(HP.lesionTrackerBaselineProtocol);
HP.ProtocolStore.onReady(() => {
console.log('Inserting lesion tracker protocols');
HP.ProtocolStore.addProtocol(HP.lesionTrackerBaselineProtocol);
HP.ProtocolStore.addProtocol(HP.lesionTrackerFollowupProtocol);
});
Meteor.call('removeHangingProtocolByID', HP.lesionTrackerFollowupProtocol.id, function() {
HangingProtocols.insert(HP.lesionTrackerFollowupProtocol);
});
HangingProtocols.insert(HP.defaultProtocol);
});

View File

@ -27,8 +27,6 @@ Template.viewer.onCreated(() => {
instance.data.state.set('leftSidebar', Session.get('leftSidebar'));
instance.data.state.set('rightSidebar', Session.get('rightSidebar'));
instance.subscribe('hangingprotocols');
const contentId = instance.data.contentId;
if (ViewerData[contentId] && ViewerData[contentId].loadedSeriesData) {

View File

@ -7,5 +7,8 @@ HangingProtocols.allow({
},
update: function() {
return true;
},
remove: function() {
return true;
}
});

View File

@ -1,46 +0,0 @@
Router.route('/protocol-export/:_id', function() {
var protocolId = this.params._id;
var protocol = HangingProtocols.findOne({
id: protocolId
});
if (!protocol) {
this.response.writeHead(404);
// This will not actually respond with a 404 because of https://github.com/iron-meteor/iron-router/issues/1055
this.response.end();
return;
}
// Remove the MongoDB _id
delete protocol._id;
var protocolJSON = JSON.stringify(protocol, null, 2),
currentDate = new Date(),
filename = protocol.name + '-' + (currentDate.getTime().toString()) + '.json';
this.response.writeHead(200, {
'Content-Type': 'application/x-download',
'Content-Disposition': 'attachment; filename=' + filename,
'Content-Length': protocolJSON.length
});
this.response.end(protocolJSON);
}, {
where: 'server'
});
Router.route('/protocol-import', {
where: 'server'
}).post(function() {
try {
var toImport = JSON.parse(this.request.body.protocol);
HangingProtocols.insert(toImport);
this.response.writeHead(200);
this.response.end(toImport.id);
} catch(e) {
this.response.writeHead(500);
this.response.end('Failed to parse protocol JSON.');
}
});

View File

@ -20,6 +20,7 @@ function getDefaultProtocol() {
function getMRTwoByTwoTest() {
var proto = new HP.Protocol('MR_TwoByTwo');
proto.id = 'MR_TwoByTwo';
proto.locked = true;
// Use http://localhost:3000/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78

View File

@ -120,7 +120,6 @@
<p class="lastSavedText">Last saved {{jsDateFromNow modifiedDate}}</p>
{{/if}}
</div>
<iframe id="download_iframe" style="display:none;"></iframe>
{{/with}}
</div>
</template>

View File

@ -6,11 +6,11 @@ function updateProtocolSelect() {
return;
}
// Loop through the available HangingProtocols
// Loop through the available hanging protocols
// to create an array with the protocols that includes
// a property labelled 'text', so that Select2 has something
// to display
var protocols = HangingProtocols.find().map(function(protocol) {
var protocols = HP.ProtocolStore.getProtocol().map(function(protocol) {
protocol.text = protocol.name;
return protocol;
});
@ -39,9 +39,8 @@ Template.protocolEditor.onRendered(() => {
Session.set('timeAgoVariable', new Date());
}, 60000);
// Subscribe to the Hanging Protocols Collection
instance.subscribe('hangingprotocols', () => {
// Update the Protocol select box when the Collection is ready
// Update the Protocol select box when the hanging protocol store is ready
HP.ProtocolStore.onReady(() => {
updateProtocolSelect();
});
});
@ -74,9 +73,7 @@ Template.protocolEditor.helpers({
updateProtocolSelect();
// Find the protocol in the database
var protocolInDatabase = HangingProtocols.findOne({
id: ProtocolEngine.protocol.id
});
var protocolInDatabase = HP.ProtocolStore.getProtocol(ProtocolEngine.protocol.id);
// Give the current Protocol an _id property from the Database
if (protocolInDatabase) {
@ -171,8 +168,8 @@ Template.protocolEditor.events({
// Change the Protocol ID from the default value
protocol.id = Random.id();
// Insert the Protocol into the HangingProtocols Collection
HangingProtocols.insert(protocol);
// Insert the protocol
HP.ProtocolStore.addProtocol(protocol);
// Activate the new Protocol using the ProtocolEngine
ProtocolEngine.setHangingProtocol(protocol);
@ -198,13 +195,8 @@ Template.protocolEditor.events({
// and fire the callback function when finished.
openTextEntryDialog(title, instructions, currentValue, function(value) {
// Update the name with the entered text
HangingProtocols.update(selectedProtocol._id, {
$set: {
name: value
}
});
selectedProtocol.name = value;
HP.ProtocolStore.updateProtocol(selectedProtocol.id, selectedProtocol)
});
},
/**
@ -240,21 +232,9 @@ Template.protocolEditor.events({
var reader = new FileReader();
reader.onload = () => {
var text = reader.result;
// POST the file to our protocol-import Route
// for it to be parsed and included in the
// HangingProtocols Collection
$.post('/protocol-import', {
protocol: text
}, function(resp, text, xhr) {
if (xhr.status !== 200) {
// If the server returns an error during importing, alert the user
// TODO: Use a custom dialog box, rather than "alert"
alert('Protocol import failed.');
}
});
// Insert the protocol
var toImport = JSON.parse(reader.result);
HP.ProtocolStore.addProtocol(toImport);
};
// Instruct the FileReader to read the (first) selected file
@ -270,10 +250,8 @@ Template.protocolEditor.events({
// Retrieve the protocolId
var protocolId = event.params.data.id;
// Retrieve the Protocol from the HangingProtocols Collection
var selectedProtocol = HangingProtocols.findOne({
id: protocolId
});
// Retrieve the protocol from the protocol store
var selectedProtocol = HP.ProtocolStore.getProtocol(protocolId);
// If it doesn't exist, stop here
if (!selectedProtocol) {
@ -291,7 +269,7 @@ Template.protocolEditor.events({
$(this).addClass('active').siblings().removeClass('active');
},
/**
* Update the HangingProtocols Collection with the latest changes to the current Protocol
* Update the protocol with the latest changes to the current Protocol
*/
'click #saveProtocol'() {
var selectedProtocol = this;
@ -299,23 +277,14 @@ Template.protocolEditor.events({
return;
}
// Store the ID for the update call
var id = selectedProtocol._id;
// Remove the MongoDB _id property so that we can
// simplify the $set value
delete selectedProtocol._id;
// Update the Protocol's modifiedDate and modifiedBy User details
selectedProtocol.protocolWasModified();
// Update the current Protocol in the database with the latest changes
HangingProtocols.update(id, {
$set: selectedProtocol
});
HP.ProtocolStore.updateProtocol(selectedProtocol.id, selectedProtocol);
},
/**
* Save the current Protocol as a new document in the HangingProtocols Collection
* Save the current Protocol as a new document
*/
'click #saveAsProtocol'() {
var selectedProtocol = this;
@ -328,9 +297,6 @@ Template.protocolEditor.events({
// Open the text entry dialog with the details above
// and fire the callback function when finished.
openTextEntryDialog(title, instructions, currentValue, function(value) {
// Erase the MongoDB _id
delete selectedProtocol._id;
// Create a new ID for the protocol
selectedProtocol.id = Random.id();
@ -341,17 +307,21 @@ Template.protocolEditor.events({
selectedProtocol.protocolWasModified();
// Insert the new Protocol
HangingProtocols.insert(selectedProtocol);
HP.ProtocolStore.addProtocol(selectedProtocol);
});
},
/**
* Export the currently selected Protocol as a JSON file
*/
'click #exportJSON'() {
// Tell the User's Browser to download the JSON file by routing a hidden iframe to our
// protocol-export Route. This prevents the tab from changing its current content.
var selectedProtocol = this;
document.getElementById('download_iframe').src = '/protocol-export/' + selectedProtocol.id;
var protocolJSON = JSON.stringify(selectedProtocol, null, 2),
currentDate = new Date(),
filename = selectedProtocol.name + '-' + (currentDate.getTime().toString()) + '.json',
protocolBlob = new Blob([protocolJSON], { type: 'application/json' });
saveAs(protocolBlob, filename);
},
/**
* Delete the currently selected Protocol
@ -368,8 +338,8 @@ Template.protocolEditor.events({
};
showConfirmDialog(() => {
// Send a call to remove the Protocol from the HangingProtocols Collection on the server
Meteor.call('removeHangingProtocol', selectedProtocol._id);
// Remove the Protocol
HP.ProtocolStore.removeProtocol(selectedProtocol.id);
// Reset the ProtocolEngine to the next best match
ProtocolEngine.reset();

View File

@ -75,7 +75,7 @@ Template.stageSortable.helpers({
}
// Retrieve the last saved copy of the current protocol
var lastSavedCopy = HangingProtocols.findOne(ProtocolEngine.protocol._id);
var lastSavedCopy = HP.ProtocolStore.getProtocol(ProtocolEngine.protocol.id);
// Try to find the index of this stage in the previously saved copy
var stageIndex = getStageIndex(lastSavedCopy, stage.id);

View File

@ -191,7 +191,7 @@ HP.ProtocolEngine = class ProtocolEngine {
findMatchByStudy(study) {
var matched = [];
HangingProtocols.find().forEach(protocol => {
HP.ProtocolStore.getProtocol().forEach(protocol => {
// Clone the protocol's protocolMatchingRules array
// We clone it so that we don't accidentally add the
// numberOfPriorsReferenced rule to the Protocol itself.
@ -220,9 +220,7 @@ HP.ProtocolEngine = class ProtocolEngine {
});
if (!matched.length) {
var defaultProtocol = HangingProtocols.findOne({
id: 'defaultProtocol'
});
var defaultProtocol = HP.ProtocolStore.getProtocol('defaultProtocol');
return [{
score: 1,

View File

@ -0,0 +1,156 @@
// The ProtocolStore default strategy is used to persist hanging protocols in
// the MongoDB collection 'HangingProtocols' in the application server.
var defaultStrategy = (function () {
var hangingProtocolSubs;
function getDatabaseIdByProtocolId(protocolId) {
const filteredProtocol = HangingProtocols.findOne({
id: protocolId
}, {
fields: {
_id: true
}
});
if (!filteredProtocol) {
return;
}
return filteredProtocol._id;
}
/**
* Registers a function to be called when the hangingprotocols collection is subscribed
* The callback is called only one time when the subscription is ready
*
* @param callback The function to be called as a callback
*/
function onReady(callback) {
if (hangingProtocolSubs && hangingProtocolSubs.ready()) {
// It is already ready
callback();
} else {
// Subscribe the hangingprotocols collection
hangingProtocolSubs = Meteor.subscribe('hangingprotocols');
// Wait for the subscription to be ready
Tracker.autorun((computation) => {
if (hangingProtocolSubs.ready()) {
computation.stop();
callback();
}
});
}
}
/**
* Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols
*
* @param protocolId The protocol ID used to find the hanging protocol
* @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols
*/
function getProtocol(protocolId) {
// Return the hanging protocol by protocolId if defined
if (protocolId) {
return HangingProtocols.findOne({
id: protocolId
});
}
// Otherwise, return all protocols
return HangingProtocols.find();
}
/**
* Stores the hanging protocol
*
* @param protocol The hanging protocol to be stored
*/
function addProtocol(protocol) {
// Collections can only be updated by database ID (_id) on client, so
// get the database ID (_id) by the hanging protocol ID firstly
const databaseId = getDatabaseIdByProtocolId(protocol.id);
// Remove any MongoDB ID the protocol may have had
delete protocol._id;
// Update the protocol with the same id if exists instead of inserting this protocol
if (databaseId) {
// Update the hanging protocol by the database ID
HangingProtocols.update(databaseId, {
$set: protocol
});
return;
}
// Insert the protocol
HangingProtocols.insert(protocol);
}
/**
* Updates the hanging protocol by protocolId
*
* @param protocolId The protocol ID used to find the hanging protocol to update
* @param protocol The updated hanging protocol
*/
function updateProtocol(protocolId, protocol) {
// Collections can only be updated by database ID (_id) on client, so
// get the database ID (_id) by the hanging protocol ID firstly
const databaseId = getDatabaseIdByProtocolId(protocolId);
// Skip if it does not exist in database
if (!databaseId) {
return;
}
// Remove any MongoDB ID the protocol may have had
delete protocol._id;
// Update the hanging protocol by the database ID
HangingProtocols.update(databaseId, {
$set: protocol
});
}
/**
* Removes the hanging protocol
*
* @param protocolId The protocol ID used to remove the hanging protocol
*/
function removeProtocol(protocolId) {
// Collections can only be removed by database ID (_id) on client, so
// get the database ID (_id) by the hanging protocol ID firstly
const databaseId = getDatabaseIdByProtocolId(protocolId);
// Skip if it does not exist in database
if (!databaseId) {
return;
}
// Remove the hanging protocol by the database ID
HangingProtocols.remove(databaseId);
}
// Module Exports
return {
onReady: onReady,
getProtocol: getProtocol,
addProtocol: addProtocol,
updateProtocol: updateProtocol,
removeProtocol: removeProtocol
};
})();
Meteor.startup(() => {
HP.ProtocolStore.setStrategy(defaultStrategy);
HP.ProtocolStore.onReady(() => {
console.log('Inserting default protocols');
HP.ProtocolStore.addProtocol(HP.defaultProtocol);
//HP.ProtocolStore.addProtocol(HP.testProtocol);
});
});

View File

@ -0,0 +1,85 @@
// The ProtocolStore module allows persisting hanging protocols using different strategies.
// For example, one strategy stores hanging protocols in the application server while
// another strategy stores them in a remote machine, but only one strategy can be used at a time.
HP.ProtocolStore = (function () {
var strategy;
/**
* Sets the strategy used to persist hanging protocols
*
* @param preferredStrategy A preferred strategy will be using to persist hanging protocols
*/
function setStrategy(preferredStrategy) {
strategy = preferredStrategy;
}
/**
* Registers a function to be called when the protocol store is ready to persist hanging protocols
*
* NOTE: Strategies should implement this function
*
* @param callback The function to be called as a callback
*/
function onReady(callback) {
strategy.onReady(callback);
}
/**
* Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols
*
* NOTE: Strategies should implement this function
*
* @param protocolId The protocol ID used to find the hanging protocol
* @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols
*/
function getProtocol(protocolId) {
return strategy.getProtocol(protocolId);
}
/**
* Stores the hanging protocol
*
* NOTE: Strategies should implement this function
*
* @param protocol The hanging protocol to be stored
*/
function addProtocol(protocol) {
strategy.addProtocol(protocol);
}
/**
* Updates the hanging protocol by protocolId
*
* NOTE: Strategies should implement this function
*
* @param protocolId The protocol ID used to find the hanging protocol to update
* @param protocol The updated hanging protocol
*/
function updateProtocol(protocolId, protocol) {
strategy.updateProtocol(protocolId, protocol);
}
/**
* Removes the hanging protocol
*
* NOTE: Strategies should implement this function
*
* @param protocolId The protocol ID used to remove the hanging protocol
*/
function removeProtocol(protocolId) {
strategy.removeProtocol(protocolId);
}
// Module Exports
return {
setStrategy: setStrategy,
onReady: onReady,
getProtocol: getProtocol,
addProtocol: addProtocol,
updateProtocol: updateProtocol,
removeProtocol: removeProtocol
};
})();

View File

@ -15,7 +15,7 @@ Package.onUse(function(api) {
api.use('templating');
api.use('natestrauser:select2@4.0.1', 'client');
api.use('clinical:router');
api.use('momentjs:moment');
api.use('validatejs');
// Our custom packages
@ -28,7 +28,6 @@ Package.onUse(function(api) {
api.addFiles('both/collections.js');
//api.addFiles('both/dicomTagDescriptions.js');
api.addFiles('both/schema.js');
api.addFiles('both/routes.js');
api.addFiles('both/hardcodedData.js');
api.addFiles('both/testData.js');
@ -37,6 +36,8 @@ Package.onUse(function(api) {
api.addFiles('client/protocolEngine.js', 'client');
api.addFiles('client/helpers/displayConstraint.js', 'client');
api.addFiles('client/helpers/attributes.js', 'client');
api.addFiles('client/protocolStore/protocolStore.js', 'client');
api.addFiles('client/protocolStore/defaultStrategy.js', 'client');
// UI Components
api.addFiles('client/components/previousPresentationGroupButton/previousPresentationGroupButton.html', 'client');
@ -83,12 +84,7 @@ Package.onUse(function(api) {
// Server-only
api.addFiles('server/collections.js', 'server');
api.addFiles('server/methods.js', 'server');
// Global exports
api.export('HP');
// Collections
api.export('HangingProtocols');
api.export('MatchedProtocols');
});

View File

@ -2,14 +2,3 @@ Meteor.publish('hangingprotocols', function() {
// TODO: filter by availableTo user
return HangingProtocols.find();
});
Meteor.startup(function() {
// Uncomment this next line to reset all your Protocols on every server reset
// HangingProtocols.remove({});
if (HangingProtocols.find().count() === 0) {
console.log('Inserting default protocols');
HangingProtocols.insert(HP.defaultProtocol);
HangingProtocols.insert(HP.testProtocol);
}
});

View File

@ -1,43 +0,0 @@
/*DICOMTags = new Meteor.Collection(null);
Object.keys(HP.tagDescriptions).forEach(function(key) {
var value = HP.tagDescriptions[key];
DICOMTags.insert({
id: key,
name: '(' + key + ') ' + value
});
});
Meteor.methods({
dicomTagSearch: function(partialName) {
check(partialName, String);
var results = DICOMTags.find({
name: {
$regex: partialName,
$options: 'i'
}
}, {
limit: 20,
fields: {
id: 1,
name: 1
}
}).fetch();
return {
results: results
};
}
}); */
Meteor.methods({
removeHangingProtocol: function(id) {
HangingProtocols.remove(id);
},
removeHangingProtocolByID: function(id) {
HangingProtocols.remove({
id: id
});
}
});

View File

@ -41,6 +41,4 @@ Template.studylist.onRendered(() => {
switchToTab(contentId);
}
}
Meteor.subscribe('hangingprotocols');
});

View File

@ -20,7 +20,7 @@ Template.viewerMain.onCreated(() => {
Template.viewerMain.onRendered(() => {
const instance = Template.instance();
instance.subscribe('hangingprotocols', () => {
HP.ProtocolStore.onReady(() => {
const { studies, currentTimepointId, measurementApi, timepointIds } = instance.data;
const parentElement = instance.$('#layoutManagerTarget').get(0);
window.layoutManager = new LayoutManager(parentElement, studies);