`
+ }
+}
module.exports = {
- deleteFileHtml : (fileType) => {
- let modalID = "deleteFileModal"
- let title = `Permanently delete this ${fileType}?`
-
- return templates.confirmation(modalID, title)
- },
- moveToTrashConfirmHtml : (fileType, newProjectFormat=false) => {
- let modalID = "moveToTrashConfirmModal"
- let title = `Move this ${fileType} to trash?`
-
- if(newProjectFormat) {
- let message = "The workflows for this model will be archived"
- return templates.confirmation_with_message(modalID, title, message);
- }
- return templates.confirmation(modalID, title)
- },
- emptyTrashConfirmHtml : () => {
- let modalID = "emptyTrashConfirmModal"
- let title = "Are you sure you want to permanently erase the items in the Trash?"
-
- return templates.confirmation(modalID, title)
- },
- operationInfoModalHtml : (page) => {
- let modalID = "operationInfoModal"
- let title = "Help"
- let message = help[page]
-
- return templates.message(modalID, title, message)
- },
- newProjectModalHtml : () => {
- let modalID = "newProjectModal"
- let inputID = "projectNameInput"
- let title = "New Project"
- let label = "Project Name"
- let value = ""
-
- return templates.input(modalID, inputID, title, label, value)
- },
- newWorkflowGroupModalHtml : () => {
- let modalID = "newWorkflowGroupModal"
- let inputID = "workflowGroupNameInput"
- let title = "New Workflow Group"
- let label = "Workflow Group Name"
- let value = ""
-
- return templates.input(modalID, inputID, title, label, value)
- },
- newProjectModelHtml : (files) => {
- let modalID = "newProjectModelModal"
- let fileID = "modelFileInput"
- let locationID = "modelPathInput"
- let title = "Add Existing Model to Project"
- let label = "Models: "
- files = files.map(function (file) {
- return ``
- })
- files.unshift(``)
- files = files.join(" ")
-
- return templates.fileSelect(modalID, fileID, locationID, title, label, files)
- },
- newProjectModelSuccessHtml : (message) => {
- let modalID = "newProjectModelSuccessModal"
- let title = "Success!"
-
- return templates.message(modalID, title, message)
- },
- newProjectModelErrorHtml : (title, message) => {
- let modalID = "newProjectModelErrorModal"
-
- return templates.message(modalID, title, message)
- },
- newProjectOrWorkflowGroupErrorHtml : (title, error) => {
- let modalID = "newProjectOrWorkflowGroupModal"
-
- return templates.message(modalID, title, error)
- },
- newWorkflowGroupSuccessHtml: (message) => {
- let modalID = "newWorkflowGroupSuccessModal"
- let title = "Success!"
-
- return templates.message(modalID, title, message)
- },
- newProjectModelWarningHtml : (message) => {
- let modalID = "newProjectModelWarningModal"
- let title = "Warnings"
-
- return templates.confirmation_with_message(modalID, title, message)
- },
- renderCreateModalHtml : (isModel, isSpatial) => {
- var title = 'Directory';
- if(isModel){
- title = isSpatial ? 'Spatial Model' : 'Model';
- }
- title = "New " + title
- let modalID = "newModalModel"
- let inputID = "modelNameInput"
- let label = "Name:"
- let value = ""
-
- return templates.input(modalID, inputID, title, label, value)
- },
- sbmlToModelHtml : (title, errors) => {
- let modalID = "sbmlToModelModal"
- for(var i = 0; i < errors.length; i++) {
- if(errors[i].startsWith("SBML Error") || errors[i].startsWith("Error")){
- errors[i] = "Error: " + errors[i]
- }else{
- errors[i] = "Warning: " + errors[i]
- }
- }
- let message = errors.join(" ")
-
- return templates.message(modalID, title, message)
- },
- uploadFileErrorsHtml : (file, type, statusMessage, errors) => {
- let modalID = "sbmlToModelModal"
- let types = {"mdl":"model file", "sbml":"sbml file", "smdl":"spatial model file", "zip":"zip archive"}
- if(type === "file") {
- type = types[file.split('.').pop()]
- }else{
- type += " file"
- }
- let title = `Errors uploading ${file} as a ${type}`
- for(var i = 0; i < errors.length; i++) {
- errors[i] = "Error: " + errors[i]
- }
- let message = `
${errors.join(" ")}
Upload status: ${statusMessage}
`
-
- return templates.message(modalID, title, message)
- },
- uploadFileHtml : (type, isSafariV14Plus) => {
- let modalID = "uploadFileModal"
- let title = `Upload a ${type}`
- var accept = "";
- if(type === "model") {
- accept = 'accept=".json, .mdl, .smdl"'
- }else if(type === "sbml") {
- accept = 'accept=".xml, .sbml"'
- }else if(type === "file" && isSafariV14Plus){
- // only used if using Safari v14+ and only needed to fix upload issue
- accept = 'accept=".json, .mdl, .smdl, .xml, .sbml, .ipynb, .zip, .md, .csv, .p, .omex, .domn, .txt, .pdf, audio/*, video/*, image/*"'
- }
-
- return templates.upload(modalID, title, accept)
- },
- duplicateWorkflowHtml : (wkflFile, body) => {
- let modalID = "duplicateWorkflowModal"
- let title = `Model for ${wkflFile}`
-
- return templates.message(modalID, title, body)
- },
- modelNotFoundHtml : (title, errorMessage) => {
- let modalID = "modelNotFoundModal"
- let message = `
${errorMessage}
Please correct the model path and press enter to reset the workflow.
`
-
- return templates.message(modalID, title, message)
- },
- wkflModelPathErrorHtml : () => {
- let modalID = "wkflModelPathErrorModal"
- let title = "Premission Denied"
- let message = "Models for workflow in a project must also be in the same project."
-
- return templates.message(modalID, title, message)
- },
- annotationModalHtml : (type, name, annotation) => {
- let modalID = `${type}AnnotationModal`
- let inputID = `${type}AnnotationInput`
- let title = `Annotation for ${name}`
- let label = "Annotation:"
- if(!annotation) {
- annotation = ""
- }
-
- return templates.input_long(modalID, inputID, title, label, annotation)
- },
- modelSaveErrorHtml : (title, error) => {
- let modalID = "modelSaveErrorModal"
-
- return templates.message(modalID, title, error)
- },
- newDirectoryErrorHtml : (title, error) => {
- let modalID = "newDirectoryErrorModal"
-
- return templates.message(modalID, title, error)
- },
- newProjectWorkflowHtml : (label, options) => {
- let modalID = "newProjectWorkflowModal"
- let selectID = "select"
- let title = "New Workflow"
- options = options.map(function (name) {
- return ``
- })
- options = options.join(" ")
-
- return templates.select(modalID, selectID, title, label, options)
- },
- selectPreviewTargetHTML : (species) => {
- let modalID = "previewTargetSelectModal";
- let selectID = "previewTargetSelectList";
- let title = "Preview Target Selection";
- let label = "Select a variable or property to preview: ";
- var options = species.map(function (name) {
- return ``
- });
- options = `
- `;
-
- return templates.select(modalID, selectID, title, label, options)
- },
- projectExportSuccessHtml : (fileType, message) => {
- let modalID = "projectExportSuccessModal"
- let title = `Successfully Exported the ${fileType}`
+ annotationModalHtml: (type, name, annotation) => {
+ let modalID = `${type}AnnotationModal`;
+ let inputID = `${type}AnnotationInput`;
+ let title = `Annotation for ${name}`;
+ let label = "Annotation:";
+ if(!annotation) {
+ annotation = "";
+ }
- return templates.message(modalID, title, message)
- },
- projectExportErrorHtml : (title, message) => {
- let modalID = "projectExportErrorModal"
+ return templates.input_long(modalID, inputID, title, label, annotation);
+ },
+ createDirectoryHtml: () => {
+ let title = "New Directory";
+ let modalID = "newDirectoryModal";
+ let inputID = "directoryNameInput";
+ let label = "Name:";
+ let value = "";
+
+ return templates.input(modalID, inputID, title, label, value);
+ },
+ createDomainHtml: () => {
+ let title = "New Domain";
+ let modalID = "newDomainModal";
+ let inputID = "domainNameInput";
+ let label = "Name:";
+ let value = "";
+
+ return templates.input(modalID, inputID, title, label, value);
+ },
+ createModelHtml: (isSpatial) => {
+ let title = `New ${isSpatial ? 'Spatial Model' : 'Model'}`;
+ let modalID = "newModelModal";
+ let inputID = "modelNameInput";
+ let label = "Name:";
+ let value = "";
+
+ return templates.input(modalID, inputID, title, label, value);
+ },
+ createProjectHtml: () => {
+ let modalID = "newProjectModal";
+ let inputID = "projectNameInput";
+ let title = "New Project";
+ let label = "Project Name";
+ let value = "";
+
+ return templates.input(modalID, inputID, title, label, value);
+ },
+ createWorkflowHtml: (name, type) => {
+ let modalID = "newWorkflowModal";
+ let inputID = "workflowNameInput";
+ let title = `New ${type} Workflow`;
+ let label = "Name:";
+ let value = name;
+
+ return templates.input(modalID, inputID, title, label, value);
+ },
+ defaultModeHtml: () => {
+ let concentrationDesciption = `Variables will only be represented using continuous (floating point) values.`;
+ let populationDescription = `Population - Variables will only be represented using discrete (integer count) values.`;
+ let hybridDescription = `Allows a variable to be represented using continuous and/or discrete values.`;
+
+ return `
+
+
+
+
+
Default Variable Mode (required)
+
+
+
+
+
+ The default mode is used to set the mode of all variables added to the model.
+ The mode of a variable is used to determine how it will be represented in a simulation.
+
+
Select one of the following:
+
+
+
+
${concentrationDesciption}
+
+
+
+
${populationDescription}
+
+
+
+
${hybridDescription}
+
+
+
+
+
+
`
+ },
+ deleteFileHtml: (fileType) => {
+ let modalID = "deleteFileModal";
+ let title = `Permanently delete this ${fileType}?`;
+
+ return templates.confirmation(modalID, title);
+ },
+ emptyTrashConfirmHtml: () => {
+ let modalID = "emptyTrashConfirmModal";
+ let title = "Are you sure you want to permanently erase the items in the Trash?";
+
+ return templates.confirmation(modalID, title);
+ },
+ errorHtml: (title, error) => {
+ let modalID = "errorModal";
+
+ return templates.message(modalID, title, error);
+ },
+ importModelHtml: (files) => {
+ let modalID = "importModelModal";
+ let fileID = "modelFileSelect";
+ let locationID = "modelPathSelect";
+ let title = "Add Existing Model to Project";
+ let label = "Models: ";
+ files = files.map(function (file) {
+ return ``;
+ });
+ files.unshift(``);
+ files = files.join(" ");
+
+ return templates.fileSelect(modalID, fileID, locationID, title, label, files);
+ },
+ moveToTrashConfirmHtml: (fileType, {newFormat=false}={}) => {
+ let modalID = "moveToTrashConfirmModal";
+ let title = `Move this ${fileType} to trash?`;
+
+ if(newFormat) {
+ let message = "The workflows for this model will be archived";
+ return templates.confirmation_with_message(modalID, title, message);
+ }
+ return templates.confirmation(modalID, title);
+ },
+ newProjectModelWarningHtml: (message) => {
+ let modalID = "newProjectModelWarningModal";
+ let title = "Warnings";
+
+ return templates.confirmation_with_message(modalID, title, message);
+ },
+ newProjectWorkflowHtml: (label, options) => {
+ let modalID = "newProjectWorkflowModal";
+ let selectID = "select";
+ let title = "New Workflow";
+ options = options.map(function (name) {
+ return ``;
+ })
+ options = options.join(" ");
+
+ return templates.select(modalID, selectID, title, label, options);
+ },
+ operationInfoModalHtml: (page) => {
+ let modalID = "operationInfoModal";
+ let title = "Help";
+ let message = help[page];
- return templates.message(modalID, title, message)
- },
- existingCreatorConfirmationHtml : (title) => {
- let modalID = "existingCreatorConfirmationModal"
-
- return templates.confirmation(modalID, title)
- },
- addMetaDataHtml: (title) => {
- let modalID = "addMetaDataModal"
-
- return templates.confirmation(modalID, title)
- },
- noModelsMessageHtml: (title, message) => {
- let modalID = "noModelsMessageModal"
-
- return templates.message(modalID, title, message)
- },
- noWorkflowGroupMessageHtml: (title, message) => {
- let modalID = "noWorkflowGroupMessageModal"
-
- return templates.message(modalID, title, message)
- },
- newWorkflowHtml: (name, type) => {
- let modalID = "newWorkflowModal"
- let inputID = "workflowNameInput"
- let title = `New ${type} Workflow`
- let label = "Name:"
- let value = name
-
- return templates.input(modalID, inputID, title, label, value);
- },
- updateFormatHtml: (fileType) => {
- let modalID = `update${fileType}FormatModal`
- let title = `Update ${fileType} Format`
- if(fileType === "Project") {
- fileType += "s and Workflow";
- var target = "project and its workflows";
+ return templates.message(modalID, title, message);
+ },
+ presentationLinks: (title, headers, links) => {
+ let modalID = "presentationLinksModal";
+
+ return templates.presentationLinks(modalID, title, headers, links);
+ },
+ sbmlToModelHtml: (title, errors) => {
+ let modalID = "sbmlToModelModal";
+ for(var i = 0; i < errors.length; i++) {
+ if(errors[i].startsWith("SBML Error") || errors[i].startsWith("Error")){
+ errors[i] = "Error: " + errors[i];
}else{
- var target = "workflow";
+ errors[i] = "Warning: " + errors[i];
}
- let message = `StochSS ${fileType}s have a new format. Would you like to update this ${target} to the new format?`
-
- return templates.confirmation_with_message(modalID, title, message);
- },
- renderDefaultModeModalHtml : () => {
- let concentrationDesciption = `Variables will only be represented using continuous (floating point) values.`;
- let populationDescription = `Population - Variables will only be represented using discrete (integer count) values.`;
- let hybridDescription = `Allows a variable to be represented using continuous and/or discrete values.`;
-
- return `
-
-
-
-
-
Default Variable Mode (required)
-
-
-
-
-
- The default mode is used to set the mode of all variables added to the model.
- The mode of a variable is used to determine how it will be represented in a simulation.
-
-
Select one of the following:
-
-
-
-
${concentrationDesciption}
-
-
-
-
${populationDescription}
-
-
-
-
${hybridDescription}
-
-
-
-
-
-
`
- },
- presentationLinks : (title, headers, links) => {
- let modalID = "presentationLinksModal"
-
- return templates.presentationLinks(modalID, title, headers, links);
- },
- modelErrorHtml: (title, message) => {
- let modalID = "modelErrorModal";
-
- return templates.message(modalID, title, message);
}
+ let message = errors.join(" ");
+
+ return templates.message(modalID, title, message);
+ },
+ selectPreviewTargetHTML: (species) => {
+ let modalID = "previewTargetSelectModal";
+ let selectID = "previewTargetSelectList";
+ let title = "Preview Target Selection";
+ let label = "Select a variable or property to preview: ";
+ var options = species.map(function (name) {
+ return ``;
+ });
+ options = `
+ `;
+
+ return templates.select(modalID, selectID, title, label, options);
+ },
+ successHtml: (message, {title="Success!"}={}) => {
+ let modalID = "successModal";
+
+ return templates.message(modalID, title, message);
+ },
+ uploadFileHtml: (type, isSafariV14Plus) => {
+ let modalID = "uploadFileModal";
+ let title = `Upload a ${type}`;
+ var accept = "";
+ if(type === "model") {
+ accept = 'accept=".json, .mdl, .smdl"';
+ }else if(type === "sbml") {
+ accept = 'accept=".xml, .sbml"';
+ }else if(type === "file" && isSafariV14Plus){
+ // only used if using Safari v14+ and only needed to fix upload issue
+ accept = 'accept=".json, .mdl, .smdl, .xml, .sbml, .ipynb, .zip, .md, .csv, .p, .omex, .domn, .txt, .pdf, audio/*, video/*, image/*"';
+ }
+
+ return templates.upload(modalID, title, accept);
+ },
+ uploadFileErrorsHtml: (file, type, statusMessage, errors) => {
+ let modalID = "uploadFileErrorsModal";
+ let types = {mdl: "model file", sbml: "sbml file", smdl: "spatial model file", zip: "zip archive"};
+ if(type === "file") {
+ type = types[file.split('.').pop()];
+ }else{
+ type += " file";
+ }
+ let title = `Errors uploading ${file} as a ${type}`;
+ for(var i = 0; i < errors.length; i++) {
+ errors[i] = `Error: ${errors[i]}`;
+ }
+ let message = `
${errors.join(" ")}
Upload status: ${statusMessage}
`;
+
+ return templates.message(modalID, title, message);
+ },
+ uploadFileExistsHtml: (title, message) => {
+ let modalID = 'uploadFileExistsModal';
+
+ return templates.confirmation_with_message(modalID, title, message);
+ },
+ updateFormatHtml: (fileType) => {
+ let modalID = `update${fileType}FormatModal`;
+ let title = `Update ${fileType} Format`;
+ if(fileType === "Project") {
+ fileType += "s and Workflow";
+ var target = "project and its workflows";
+ }else{
+ var target = "workflow";
+ }
+ let message = `StochSS ${fileType}s have a new format. Would you like to update this ${target} to the new format?`;
+
+ return templates.confirmation_with_message(modalID, title, message);
+ }
}
\ No newline at end of file
diff --git a/client/model-view/model-view.js b/client/model-view/model-view.js
index 47482848d6..4d95951b00 100644
--- a/client/model-view/model-view.js
+++ b/client/model-view/model-view.js
@@ -55,6 +55,8 @@ module.exports = View.extend({
this.readOnly = attrs.readOnly ? attrs.readOnly : false;
let modeMap = {continuous: "Concentration", discrete: "Population", dynamic: "Hybrid Concentration/Population"};
this.modelMode = modeMap[this.model.defaultMode];
+ this.domainElements = Boolean(attrs.domainElements) ? attrs.domainElements : null;
+ this.domainPlot = Boolean(attrs.domainPlot) ? attrs.domainPlot : null;
},
render: function(attrs, options) {
View.prototype.render.apply(this, arguments);
@@ -72,7 +74,7 @@ module.exports = View.extend({
this.renderSystemVolumeView();
if(this.readOnly) {
this.setReadOnlyMode("model-mode");
- this.setReadOnlyMode("system-volume")
+ this.setReadOnlyMode("system-volume");
}else {
if(this.model.defaultMode === "" && !this.model.is_spatial){
this.getInitialDefaultMode();
@@ -84,44 +86,75 @@ module.exports = View.extend({
$(this.queryByHook("system-volume-container")).css("display", "none");
}
}
- this.model.reactions.on("change", function (reactions) {
+ this.model.reactions.on("change", (reactions) => {
this.updateSpeciesInUse();
this.updateParametersInUse();
- }, this);
- this.model.eventsCollection.on("add change remove", function (){
+ });
+ this.model.eventsCollection.on("add change remove", () => {
this.updateSpeciesInUse();
this.updateParametersInUse();
- }, this);
- this.model.rules.on("add change remove", function() {
+ });
+ this.model.rules.on("add change remove", () => {
this.updateSpeciesInUse();
this.updateParametersInUse();
- }, this);
+ });
}
},
changeCollapseButtonText: function (e) {
app.changeCollapseButtonText(this, e);
},
getInitialDefaultMode: function () {
- let self = this;
if(document.querySelector('#defaultModeModal')) {
document.querySelector('#defaultModeModal').remove();
}
- let modal = $(modals.renderDefaultModeModalHtml()).modal();
+ let modal = $(modals.defaultModeHtml()).modal();
let continuous = document.querySelector('#defaultModeModal .concentration-btn');
let discrete = document.querySelector('#defaultModeModal .population-btn');
let dynamic = document.querySelector('#defaultModeModal .hybrid-btn');
- continuous.addEventListener('click', function (e) {
- self.setInitialDefaultMode(modal, "continuous");
+ continuous.addEventListener('click', (e) => {
+ this.setInitialDefaultMode(modal, "continuous");
});
- discrete.addEventListener('click', function (e) {
- self.setInitialDefaultMode(modal, "discrete");
+ discrete.addEventListener('click', (e) => {
+ this.setInitialDefaultMode(modal, "discrete");
});
- dynamic.addEventListener('click', function (e) {
- self.setInitialDefaultMode(modal, "dynamic");
+ dynamic.addEventListener('click', (e) => {
+ this.setInitialDefaultMode(modal, "dynamic");
});
},
+ openAdvancedSection: function () {
+ if(!$(this.queryByHook("me-advanced-section")).hasClass("show")) {
+ console.log("opening advanced section")
+ let advCollapseBtn = $(this.queryByHook("collapse-mv-advanced-section"));
+ advCollapseBtn.click();
+ advCollapseBtn.html('-');
+ }
+ },
+ openSection: function () {
+ let error = this.model.error;
+ if(["event", "rule", "volume"].includes(error.type)) {
+ this.openAdvancedSection();
+ }
+ if(error.type === "volume") {
+ this.openVolumeSection();
+ }else{
+ let views = {
+ species: this.speciesView,
+ parameter: this.parametersView,
+ reaction: this.reactionsView,
+ process: this.reactionsView,
+ event: this.eventsView,
+ rule: this.rulesView,
+ domain: this.domainViewer
+ }
+ if(["reaction", "process", "event"].includes(error.type)) {
+ views[error.type].openSection(error);
+ }else{
+ views[error.type].openSection();
+ }
+ }
+ },
renderBoundaryConditionsView: function () {
- if(!this.model.is_spatial) { return }
+ if(!this.model.is_spatial) { return };
if(this.boundaryConditionsView) {
this.boundaryConditionsView.remove();
}
@@ -132,25 +165,26 @@ module.exports = View.extend({
let hook = "boundary-conditions-view-container";
app.registerRenderSubview(this, this.boundaryConditionsView, hook);
},
- renderDomainViewer: function(domainPath=null) {
- if(!this.model.is_spatial) { return }
+ renderDomainViewer: function (domainPath=null) {
+ if(!this.model.is_spatial) { return };
if(this.domainViewer) {
this.domainViewer.remove();
}
if(domainPath && domainPath !== "viewing") {
- let self = this;
- let queryStr = "?path=" + this.model.directory + "&domain_path=" + domainPath;
+ let queryStr = `?path=${this.model.directory}&domain_path=${domainPath}`;
let endpoint = path.join(app.getApiPath(), "spatial-model/load-domain") + queryStr;
app.getXHR(endpoint, {
- always: function (err, response, body) {
+ always: (err, response, body) => {
let domain = new Domain(body.domain);
- self.domainViewer = new DomainViewer({
- parent: self,
+ this.domainViewer = new DomainViewer({
+ parent: this,
model: domain,
domainPath: domainPath,
- readOnly: self.readOnly
+ readOnly: this.readOnly,
+ domainElements: this.domainElements,
+ domainPlot: this.domainPlot
});
- app.registerRenderSubview(self, self.domainViewer, 'domain-viewer-container');
+ app.registerRenderSubview(this, this.domainViewer, 'domain-viewer-container');
}
});
}else{
@@ -158,13 +192,15 @@ module.exports = View.extend({
parent: this,
model: this.model.domain,
domainPath: domainPath,
- readOnly: this.readOnly
+ readOnly: this.readOnly,
+ domainElements: this.domainElements,
+ domainPlot: this.domainPlot
});
app.registerRenderSubview(this, this.domainViewer, 'domain-viewer-container');
}
},
renderEventsView: function () {
- if(this.model.is_spatial) { return }
+ if(this.model.is_spatial) { return };
if(this.eventsView) {
this.eventsView.remove();
}
@@ -176,7 +212,7 @@ module.exports = View.extend({
app.registerRenderSubview(this, this.eventsView, hook);
},
renderInitialConditionsView: function () {
- if(!this.model.is_spatial) { return }
+ if(!this.model.is_spatial) { return };
if(this.initialConditionsView) {
this.initialConditionsView.remove();
}
@@ -210,7 +246,7 @@ module.exports = View.extend({
app.registerRenderSubview(this, this.reactionsView, hook);
},
renderRulesView: function () {
- if(this.model.is_spatial) { return }
+ if(this.model.is_spatial) { return };
if(this.rulesView) {
this.rulesView.remove();
}
@@ -222,7 +258,7 @@ module.exports = View.extend({
app.registerRenderSubview(this, this.rulesView, hook);
},
renderSbmlComponentView: function () {
- if(this.model.is_spatial || !this.model.functionDefinitions.length) { return }
+ if(this.model.is_spatial || !this.model.functionDefinitions.length) { return };
if(this.sbmlComponentView) {
this.sbmlComponentView.remove();
}
@@ -268,9 +304,9 @@ module.exports = View.extend({
},
setAllSpeciesModes: function (prevMode) {
let self = this;
- this.model.species.forEach(function (specie) {
- specie.mode = self.model.defaultMode;
- self.model.species.trigger('update-species', specie.compID, specie, false, true);
+ this.model.species.forEach((specie) => {
+ specie.mode = this.model.defaultMode;
+ this.model.species.trigger('update-species', specie.compID, specie, false, true);
});
let switchToDynamic = (!Boolean(prevMode) || prevMode !== "dynamic") && this.model.defaultMode === "dynamic";
let switchFromDynamic = Boolean(prevMode) && prevMode === "dynamic" && this.model.defaultMode !== "dynamic";
@@ -297,7 +333,7 @@ module.exports = View.extend({
},
setReadOnlyMode: function (component) {
$(this.queryByHook(component + '-edit-tab')).addClass("disabled");
- $(".nav .disabled>a").on("click", function(e) {
+ $(".nav .disabled>a").on("click", (e) => {
e.preventDefault();
return false;
});
@@ -316,59 +352,59 @@ module.exports = View.extend({
},
update: function () {},
updateParametersInUse: function () {
- var parameters = this.model.parameters;
- var reactions = this.model.reactions;
- var events = this.model.eventsCollection;
- var rules = this.model.rules;
- parameters.forEach(function (param) { param.inUse = false; });
- var updateInUse = function (param) {
+ let parameters = this.model.parameters;
+ let reactions = this.model.reactions;
+ let events = this.model.eventsCollection;
+ let rules = this.model.rules;
+ parameters.forEach((param) => { param.inUse = false; });
+ let updateInUse = (param) => {
_.where(parameters.models, { compID: param.compID })
- .forEach(function (param) {
+ .forEach((param) => {
param.inUse = true;
});
}
- reactions.forEach(function (reaction) {
+ reactions.forEach((reaction) => {
if(reaction.reactionType !== "custom-propensity"){
updateInUse(reaction.rate);
}
});
- events.forEach(function (event) {
- event.eventAssignments.forEach(function (assignment) {
- updateInUse(assignment.variable)
+ events.forEach((event) => {
+ event.eventAssignments.forEach((assignment) => {
+ updateInUse(assignment.variable);
});
});
- rules.forEach(function (rule) {
+ rules.forEach((rule) => {
updateInUse(rule.variable);
});
},
updateSpeciesInUse: function () {
- var species = this.model.species;
- var reactions = this.model.reactions;
- var events = this.model.eventsCollection;
- var rules = this.model.rules;
- species.forEach(function (specie) { specie.inUse = false; });
- var updateInUseForReaction = function (stoichSpecie) {
+ let species = this.model.species;
+ let reactions = this.model.reactions;
+ let events = this.model.eventsCollection;
+ let rules = this.model.rules;
+ species.forEach((specie) => { specie.inUse = false; });
+ let updateInUseForReaction = (stoichSpecie) => {
_.where(species.models, { compID: stoichSpecie.specie.compID })
- .forEach(function (specie) {
+ .forEach((specie) => {
specie.inUse = true;
});
}
- var updateInUseForOther = function (specie) {
+ let updateInUseForOther = (specie) => {
_.where(species.models, { compID: specie.compID })
- .forEach(function (specie) {
+ .forEach((specie) => {
specie.inUse = true;
});
}
- reactions.forEach(function (reaction) {
+ reactions.forEach((reaction) => {
reaction.products.forEach(updateInUseForReaction);
reaction.reactants.forEach(updateInUseForReaction);
});
- events.forEach(function (event) {
- event.eventAssignments.forEach(function (assignment) {
- updateInUseForOther(assignment.variable)
+ events.forEach((event) => {
+ event.eventAssignments.forEach((assignment) => {
+ updateInUseForOther(assignment.variable);
});
});
- rules.forEach(function (rule) {
+ rules.forEach((rule) => {
updateInUseForOther(rule.variable);
});
},
diff --git a/client/model-view/templates/domainViewer.pug b/client/model-view/templates/domainViewer.pug
index 14590cd722..41b57b714f 100644
--- a/client/model-view/templates/domainViewer.pug
+++ b/client/model-view/templates/domainViewer.pug
@@ -144,3 +144,29 @@ div#domain-viewer.card
button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="save-to-model" disabled) Import into Model
div.tab-pane(id="view-domain" data-hook="view-domain")
+
+ div(data-hook="view-domain-plot-container" style="display: none")
+
+ div: h4 Domain and Particle Preview
+
+ div.row
+
+ div.col-sm-8
+
+ div(data-hook="view-domain-plot")
+
+ div.col-sm-4
+
+ div.mt-2.particle-view(data-hook="domain-particle-viewer")
+
+ div.mt-2.particle-view(data-hook="domain-select-particle")
+
+ div.text-info
+ | Click on a particle to view its properties.
+
+ div.mt-2
+
+ h4 Particles per Type
+
+ table.table
+ tbody(data-hook="domain-types-quick-view")
diff --git a/client/model-view/views/domain-viewer.js b/client/model-view/views/domain-viewer.js
index 619347c0f5..4452cb854c 100644
--- a/client/model-view/views/domain-viewer.js
+++ b/client/model-view/views/domain-viewer.js
@@ -16,20 +16,21 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-var $ = require('jquery');
+let $ = require('jquery');
let path = require('path');
-var _ = require('underscore');
+let _ = require('underscore');
//support files
-var app = require('../../app');
-var Plotly = require('../../lib/plotly');
-var Tooltips = require('../../tooltips');
+let app = require('../../app');
+let Plotly = require('../../lib/plotly');
+let Tooltips = require('../../tooltips');
//views
-var View = require('ampersand-view');
-var TypesViewer = require('../../views/edit-domain-type');
-var SelectView = require('ampersand-select-view');
-var ParticleViewer = require('../../views/view-particle');
+let View = require('ampersand-view');
+let SelectView = require('ampersand-select-view');
+let TypesViewer = require('../../views/edit-domain-type');
+let ParticleViewer = require('../../views/view-particle');
+let QuickviewDomainTypes = require('../../views/quickview-domain-types');
//templates
-var template = require('../templates/domainViewer.pug');
+let template = require('../templates/domainViewer.pug');
module.exports = View.extend({
template: template,
@@ -49,19 +50,19 @@ module.exports = View.extend({
this.readOnly = attrs.readOnly ? attrs.readOnly : false;
this.tooltips = Tooltips.domainEditor;
this.domainPath = attrs.domainPath;
- let self = this;
- this.model.particles.forEach(function (particle) {
- self.model.types.get(particle.type, "typeID").numParticles += 1;
+ this.plot = Boolean(attrs.domainPlot) ? attrs.domainPlot : null;
+ this.elements = Boolean(attrs.domainElements) ? attrs.domainElements : null;
+ this.model.particles.forEach((particle) => {
+ this.model.types.get(particle.type, "typeID").numParticles += 1;
});
- this.gravity = this.getGravityString()
+ this.gravity = this.getGravityString();
},
- render: function () {
+ render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
- let self = this;
- var queryStr = "?path=" + this.parent.model.directory
+ this.queryStr = `?path=${this.parent.model.directory}`;
if(this.readOnly) {
$(this.queryByHook('domain-edit-tab')).addClass("disabled");
- $(".nav .disabled>a").on("click", function(e) {
+ $(".nav .disabled>a").on("click", (e) => {
e.preventDefault();
return false;
});
@@ -72,32 +73,58 @@ module.exports = View.extend({
}else {
this.renderDomainSelectView();
if(this.domainPath) {
- $(this.queryByHook("domain-container")).collapse("show")
- $(this.queryByHook("collapse")).text("-")
+ $(this.queryByHook("domain-container")).collapse("show");
+ $(this.queryByHook("collapse")).text("-");
if(this.domainPath !== "viewing") {
- $(this.queryByHook("save-to-model")).prop("disabled", false)
- $(this.queryByHook("external-domain-select")).css("display", "block")
- $(this.queryByHook("select-external-domain")).text("View Model's Domain")
- queryStr += "&domain_path=" + this.domainPath
+ $(this.queryByHook("save-to-model")).prop("disabled", false);
+ $(this.queryByHook("external-domain-select")).css("display", "block");
+ $(this.queryByHook("select-external-domain")).text("View Model's Domain");
+ this.queryStr += `&domain_path=${this.domainPath}`;
}
}
this.toggleDomainError();
}
this.renderTypesViewer();
- this.parent.parent.renderParticleViewer(null);
- let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot") + queryStr;
- app.getXHR(endpoint, {
- always: function (err, response, body) {
- self.plot = body.fig;
- self.displayDomain();
- }
- });
+ this.renderPlotParticleViewer();
+ },
+ changeCollapseButtonText: function (e) {
+ app.changeCollapseButtonText(this, e);
+ },
+ displayDomain: function (domainPreview) {
+ Plotly.newPlot(domainPreview, this.plot);
+ domainPreview.on('plotly_click', _.bind(this.selectParticle, this));
+ },
+ editDomain: function (e) {
+ var queryStr = `?path=${this.parent.model.directory}`;
+ if(e.target.dataset.hook === "create-domain") {
+ queryStr += "&new"
+ }else if(this.domainPath) {
+ queryStr += `&domainPath=${this.domainPath}`;
+ }
+ let endpoint = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr;
+ window.location.href = endpoint;
+ },
+ getDomainSelectValue: function (files) {
+ if(!this.domainPath || this.domainPath === "viewing") {
+ return null;
+ }
+ let domainFile = this.domainPath.split('/').pop();
+ let value = files.filter((file) => {
+ return file[1] === domainFile;
+ })[0][0];
+ return value;
+ },
+ getGravityString: function () {
+ var gravity = `(X: ${this.model.gravity[0]}`;
+ gravity += `, Y: ${this.model.gravity[1]}`;
+ gravity += `, Z: ${this.model.gravity[2]})`;
+ return gravity;
},
handleLoadExternalDomain: function (e) {
- let text = e.target.textContent
+ let text = e.target.textContent;
if(text === "View External Domain") {
- $(this.queryByHook("external-domain-select")).css("display", "block")
- $(this.queryByHook("select-external-domain")).text("View Model's Domain")
+ $(this.queryByHook("external-domain-select")).css("display", "block");
+ $(this.queryByHook("select-external-domain")).text("View Model's Domain");
}else{
this.reloadDomain("viewing");
}
@@ -117,9 +144,17 @@ module.exports = View.extend({
handleSelectDomainLocation: function (e) {
this.reloadDomain(e.srcElement.value);
},
+ openSection: function () {
+ if(!$(this.queryByHook("domain-container")).hasClass("show")) {
+ let domainCollapseBtn = $(this.queryByHook("collapse"));
+ domainCollapseBtn.click();
+ domainCollapseBtn.html('-');
+ }
+ app.switchToEditTab(this, "domain");
+ },
reloadDomain: function (domainPath) {
if(this.domainPath !== domainPath || domainPath === "viewing") {
- var el = this.parent.parent.queryByHook("domain-plot-container");
+ let el = this.elements === null ? this.queryByHook("view-domain-plot") : this.elements.plot;
el.removeListener('plotly_click', this.selectParticle);
Plotly.purge(el);
this.plot = null;
@@ -129,103 +164,133 @@ module.exports = View.extend({
this.parent.renderDomainViewer(domainPath);
}
},
- getGravityString: function () {
- var gravity = "(X: " + this.model.gravity[0];
- gravity += ", Y: " + this.model.gravity[1];
- gravity += ", Z: " + this.model.gravity[2] + ")";
- return gravity;
+ renderDomainLocationSelectView: function (options) {
+ if(this.domainLocationSelectView) {
+ this.domainLocationSelectView.remove();
+ }
+ this.domainLocationSelectView = new SelectView({
+ name: 'locations',
+ required: false,
+ idAttributes: 'cid',
+ options: options,
+ unselectedText: "-- Select Location --"
+ });
+ app.registerRenderSubview(this, this.domainLocationSelectView, "select-location");
},
renderDomainSelectView: function () {
- let self = this;
let endpoint = path.join(app.getApiPath(), "spatial-model/domain-list");
app.getXHR(endpoint, {
- always: function (err, response, body) {
- self.externalDomains = body.paths;
- var domainSelectView = new SelectView({
- label: '',
+ always: (err, response, body) => {
+ this.externalDomains = body.paths;
+ let domainSelectView = new SelectView({
name: 'domains',
required: false,
idAttributes: 'cid',
options: body.files,
unselectedText: "-- Select Domain --",
- value: self.getDomainSelectValue(body.files)
+ value: this.getDomainSelectValue(body.files)
});
- app.registerRenderSubview(self, domainSelectView, "select-domain")
+ app.registerRenderSubview(this, domainSelectView, "select-domain");
}
});
},
- getDomainSelectValue: function (files) {
- if(!this.domainPath || this.domainPath === "viewing") {
- return null;
+ renderLocalParticlesViewer: function ({particle=null}) {
+ if(particle){
+ $(this.queryByHook("domain-select-particle")).css("display", "none");
+ this.particleViewer = new ParticleViewer({
+ model: particle
+ });
+ app.registerRenderSubview(this, this.particleViewer, "domain-particle-viewer");
+ }else{
+ $(this.queryByHook("domain-select-particle")).css("display", "block");
+ this.typeQuickViewer = this.renderCollection(
+ this.model.types,
+ QuickviewDomainTypes,
+ this.queryByHook("domain-types-quick-view")
+ );
}
- let domainFile = this.domainPath.split('/').pop();
- let value = files.filter(function (file) {
- return file[1] === domainFile;
- })[0][0];
- return value
},
- renderDomainLocationSelectView: function (options) {
- if(this.domainLocationSelectView) {
- this.domainLocationSelectView.remove();
+ renderMEParticlesViewer: function ({particle=null}) {
+ if(particle){
+ this.elements.select.css("display", "none");
+ this.particleViewer = new ParticleViewer({
+ model: particle
+ });
+ app.registerRenderSubview(this.elements.particle.view, this.particleViewer, this.elements.particle.hook);
+ }else{
+ this.elements.select.css("display", "block");
+ this.typeQuickViewer = this.renderCollection(
+ this.model.types,
+ QuickviewDomainTypes,
+ this.elements.type
+ );
+ }
+ },
+ renderPlot: function (plotElement) {
+ if(this.plot === null) {
+ let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot") + this.queryStr;
+ app.getXHR(endpoint, {
+ always: (err, response, body) => {
+ this.plot = body.fig;
+ this.displayDomain(plotElement);
+ }
+ });
+ }else{
+ this.displayDomain(plotElement);
+ }
+ },
+ renderPlotParticleViewer: function ({particle=null}={}) {
+ if(this.particleViewer) {
+ this.particleViewer.remove();
+ }
+ if(this.typeQuickViewer) {
+ this.typeQuickViewer.remove();
+ }
+ if(this.elements === null) {
+ $(this.queryByHook("view-domain-plot-container")).css('display', 'block');
+ this.renderLocalParticlesViewer({particle: particle});
+ if(particle === null) {
+ this.renderPlot(this.queryByHook("view-domain-plot"));
+ }
+ }else{
+ this.renderMEParticlesViewer({particle: particle});
+ if(particle === null) {
+ this.renderPlot(this.elements.plot);
+ }
}
- this.domainLocationSelectView = new SelectView({
- label: '',
- name: 'locations',
- required: false,
- idAttributes: 'cid',
- options: options,
- unselectedText: "-- Select Location --"
- });
- app.registerRenderSubview(this, this.domainLocationSelectView, "select-location")
},
renderTypesViewer: function () {
if(this.typesViewer) {
- this.typesViewer.remove()
+ this.typesViewer.remove();
}
this.typesViewer = this.renderCollection(
this.model.types,
TypesViewer,
this.queryByHook("domain-types-list"),
- {filter: function (model) {
+ {filter: (model) => {
return model.typeID != 0;
}, viewOptions: {viewMode: true}}
);
},
- displayDomain: function () {
- let self = this;
- var domainPreview = this.parent.parent.queryByHook("domain-plot-container");
- Plotly.newPlot(domainPreview, this.plot);
- domainPreview.on('plotly_click', _.bind(this.selectParticle, this));
+ saveDomainToModel: function (e) {
+ this.parent.model.domain = this.model;
+ this.parent.modelStateButtons.clickSaveHandler(e);
+ this.reloadDomain();
},
selectParticle: function (data) {
let point = data.points[0];
let particle = this.model.particles.get(point.id, "particle_id");
- this.parent.parent.renderParticleViewer(particle);
- },
- editDomain: function (e) {
- var queryStr = "?path=" + this.parent.model.directory;
- if(e.target.dataset.hook === "create-domain") {
- queryStr += "&new"
- }else if(this.domainPath) {
- queryStr += "&domainPath=" + this.domainPath
- }
- let endpoint = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr;
- window.location.href = endpoint;
- },
- saveDomainToModel: function (e) {
- this.parent.model.domain = this.model;
- this.parent.modelStateButtons.clickSaveHandler(e);
- this.reloadDomain()
+ this.renderPlotParticleViewer({particle: particle});
},
toggleDomainError: function () {
- let errorMsg = $(this.queryByHook('domain-error'))
+ let errorMsg = $(this.queryByHook('domain-error'));
this.model.updateValid();
if(!this.model.valid) {
- errorMsg.addClass('component-invalid')
- errorMsg.removeClass('component-valid')
+ errorMsg.addClass('component-invalid');
+ errorMsg.removeClass('component-valid');
}else{
- errorMsg.addClass('component-valid')
- errorMsg.removeClass('component-invalid')
+ errorMsg.addClass('component-valid');
+ errorMsg.removeClass('component-invalid');
}
},
toggleViewExternalDomainBtn: function (e) {
@@ -238,8 +303,5 @@ module.exports = View.extend({
let display = this.readOnly ? "none" : "block";
$(this.queryByHook("external-domains-container")).css("display", display);
}
- },
- changeCollapseButtonText: function (e) {
- app.changeCollapseButtonText(this, e);
}
});
\ No newline at end of file
diff --git a/client/model-view/views/events-view.js b/client/model-view/views/events-view.js
index 629470944a..cc0086b218 100644
--- a/client/model-view/views/events-view.js
+++ b/client/model-view/views/events-view.js
@@ -98,6 +98,19 @@ module.exports = View.extend({
detailsView.parent = this;
return detailsView;
},
+ openSection: function (error) {
+ if(!$(this.queryByHook("events")).hasClass("show")) {
+ let evtCollapseBtn = $(this.queryByHook("collapse"));
+ evtCollapseBtn.click();
+ evtCollapseBtn.html('-');
+ }
+ app.switchToEditTab(this, "events");
+ var event = this.collection.filter((e) => {
+ return e.compID === error.id;
+ })[0];
+ this.collection.trigger("select", event);
+ event.detailsView.openAdvancedSection();
+ },
renderEditEventListingsView: function () {
if(this.editEventListingsView){
this.editEventListingsView.remove();
diff --git a/client/model-view/views/parameters-view.js b/client/model-view/views/parameters-view.js
index 13bdf608a9..981116e3d5 100644
--- a/client/model-view/views/parameters-view.js
+++ b/client/model-view/views/parameters-view.js
@@ -57,7 +57,9 @@ module.exports = View.extend({
rule.variable = parameter;
}
});
- self.parent.rulesView.renderEditRules();
+ if(self.parent.rulesView) {
+ self.parent.rulesView.renderEditRules();
+ }
});
},
render: function () {
@@ -83,6 +85,14 @@ module.exports = View.extend({
changeCollapseButtonText: function (e) {
app.changeCollapseButtonText(this, e);
},
+ openSection: function () {
+ if(!$(this.queryByHook("parameters-list-container")).hasClass("show")) {
+ let paramCollapseBtn = $(this.queryByHook("collapse"));
+ paramCollapseBtn.click();
+ paramCollapseBtn.html('-');
+ }
+ app.switchToEditTab(this, "parameters");
+ },
renderEditParameter: function () {
if(this.editParameterView){
this.editParameterView.remove();
diff --git a/client/model-view/views/reactions-view.js b/client/model-view/views/reactions-view.js
index 912745f565..5a88e8879e 100644
--- a/client/model-view/views/reactions-view.js
+++ b/client/model-view/views/reactions-view.js
@@ -144,6 +144,20 @@ module.exports = View.extend({
let collapseBtn = $(this.queryByHook('collapse'));
collapseBtn.trigger('click');
},
+ openSection: function (error, {isCollection=false}={}) {
+ if(!$(this.queryByHook("reactions-list-container")).hasClass("show")) {
+ let reacCollapseBtn = $(this.queryByHook("collapse"));
+ reacCollapseBtn.click();
+ reacCollapseBtn.html('-');
+ }
+ app.switchToEditTab(this, "reactions");
+ if(error.type !== "process") {
+ let reaction = this.collection.filter((react) => {
+ return react.compID === error.id;
+ })[0];
+ this.collection.trigger("select", reaction);
+ }
+ },
renderEditReactionListingView: function () {
if(this.editReactionListingView){
this.editReactionListingView.remove();
diff --git a/client/model-view/views/rules-view.js b/client/model-view/views/rules-view.js
index c61ca2d3b7..cddc016390 100644
--- a/client/model-view/views/rules-view.js
+++ b/client/model-view/views/rules-view.js
@@ -67,6 +67,14 @@ module.exports = View.extend({
changeCollapseButtonText: function (e) {
app.changeCollapseButtonText(this, e);
},
+ openSection: function () {
+ if(!$(this.queryByHook("rules-list-container")).hasClass("show")) {
+ let ruleCollapseBtn = $(this.queryByHook("collapse"));
+ ruleCollapseBtn.click();
+ ruleCollapseBtn.html('-');
+ }
+ app.switchToEditTab(this, "rules");
+ },
renderDocs: function () {
let options = {
displayMode: true,
diff --git a/client/model-view/views/species-view.js b/client/model-view/views/species-view.js
index f9aca59ac9..30fa6ea253 100644
--- a/client/model-view/views/species-view.js
+++ b/client/model-view/views/species-view.js
@@ -123,6 +123,14 @@ module.exports = View.extend({
this.addSpecies();
}
},
+ openSection: function ({editMode=true}={}) {
+ if(!$(this.queryByHook("species-list-container")).hasClass("show")) {
+ let specCollapseBtn = $(this.queryByHook("collapse"));
+ specCollapseBtn.click();
+ specCollapseBtn.html('-');
+ }
+ app.switchToEditTab(this, "species");
+ },
renderEditSpeciesView: function () {
if(this.editSpeciesView){
this.editSpeciesView.remove();
diff --git a/client/models/presentation.js b/client/models/presentation.js
new file mode 100644
index 0000000000..17a07f2295
--- /dev/null
+++ b/client/models/presentation.js
@@ -0,0 +1,43 @@
+/*
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2021 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+let State = require('ampersand-state');
+
+module.exports = State.extend({
+ session: {
+ file: 'string',
+ link: 'string',
+ size: 'number',
+ tag: 'string'
+ },
+ initialize: function (attrs, options) {
+ State.prototype.initialize.apply(this, arguments);
+ this.formatSize();
+ },
+ formatSize: function () {
+ let tags = ["B", "KB", "MB", "GB", "TB"];
+ var size = this.size;
+ var tag_index = 0;
+ while(size >= 1e3) {
+ size = size / 1e3;
+ tag_index += 1;
+ }
+ this.size = Number(Number.parseFloat(size).toFixed(2));
+ this.tag = tags[tag_index]
+ }
+});
\ No newline at end of file
diff --git a/client/pages/browser.js b/client/pages/browser.js
new file mode 100644
index 0000000000..63a9639aeb
--- /dev/null
+++ b/client/pages/browser.js
@@ -0,0 +1,174 @@
+/*
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2021 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+let $ = require('jquery');
+let path = require('path');
+//support files
+let app = require('../app');
+let modals = require('../modals');
+//collections
+let Collection = require('ampersand-collection'); // form presentation browser
+//model
+let Project = require('../models/project'); // from project browser
+let Presentation = require('../models/presentation'); // form presentation browser
+//views
+let PageView = require('./base');
+let EditProjectView = require('../views/edit-project'); // from project browser
+let PresentationView = require('../views/presentation-view'); // form presentation browser
+let JSTreeView = require('../views/jstree-view');
+//templates
+let template = require('../templates/pages/browser.pug');
+
+import initPage from './page.js';
+
+let FileBrowser = PageView.extend({
+ pageTitle: 'StochSS | File Browser',
+ template: template,
+ events: {
+ 'click [data-hook=file-browser-help]' : function () {
+ let modal = $(modals.operationInfoModalHtml('file-browser')).modal();
+ },
+ 'click [data-hook=collapse-projects]' : 'changeCollapseButtonText',
+ 'click [data-hook=new-project-btn]' : 'handleNewProjectClick',
+ 'click [data-hook=collapse-presentations]' : 'changeCollapseButtonText',
+ 'click [data-hook=collapse-files]' : 'changeCollapseButtonText'
+ },
+ initialize: function (attrs, options) {
+ PageView.prototype.initialize.apply(this, arguments)
+ this.getProjects();
+ },
+ render: function (attrs, options) {
+ PageView.prototype.render.apply(this, arguments)
+ window.addEventListener('pageshow', (e) => {
+ let navType = window.performance.navigation.type;
+ if(navType === 2){
+ window.location.reload();
+ }
+ });
+ app.documentSetup();
+ if(app.getBasePath() === "/") {
+ $("#presentations").css("display", "none");
+ }else{
+ this.getPresentations();
+ }
+ this.renderJSTreeView();
+ },
+ changeCollapseButtonText: function (e) {
+ app.changeCollapseButtonText(this, e);
+ },
+ getPresentations: function () {
+ let endpoint = path.join(app.getApiPath(), "file/presentations");
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ this.renderPresentationView(body.presentations);
+ }
+ });
+ },
+ getProjects: function () {
+ let endpoint = path.join(app.getApiPath(), "project/load-browser");
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ this.renderProjectsView(body.projects);
+ }
+ });
+ },
+ handleNewProjectClick: function (e) {
+ if(document.querySelector("#newProjectModal")) {
+ document.querySelector("#newProjectModal").remove();
+ }
+ let modal = $(modals.createProjectHtml()).modal();
+ let input = document.querySelector("#newProjectModal #projectNameInput");
+ let okBtn = document.querySelector("#newProjectModal .ok-model-btn");
+ input.focus();
+ input.addEventListener("keyup", (event) => {
+ if(event.keyCode === 13){
+ event.preventDefault();
+ okBtn.click();
+ }
+ });
+ input.addEventListener("input", (e) => {
+ let endErrMsg = document.querySelector('#newProjectModal #projectNameInputEndCharError');
+ let charErrMsg = document.querySelector('#newProjectModal #projectNameInputSpecCharError');
+ let error = app.validateName(input.value);
+ okBtn.disabled = error !== "";
+ charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none";
+ endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none";
+ });
+ okBtn.addEventListener("click", (e) => {
+ modal.modal('hide');
+ let queryStr = `?path=${input.value.trim()}.proj`;
+ let endpoint = path.join(app.getApiPath(), "project/new-project") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ let queryStr = `?path=${body.path}`;
+ window.location.href = path.join(app.getBasePath(), "stochss/project/manager") + queryStr;
+ },
+ error: (err, response, body) => {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
+ let errorModal = $(modals.errorHtml(body.Reason, body.Message)).modal();
+ }
+ });
+ });
+ },
+ renderJSTreeView: function () {
+ if(this.jstreeView) {
+ this.jstreeView.remove();
+ }
+ this.jstreeView = new JSTreeView({
+ configKey: "file"
+ });
+ app.registerRenderSubview(this, this.jstreeView, "jstree-view-container");
+ },
+ renderPresentationView: function (presentations) {
+ if(this.presentationsView) {
+ this.presentationsView.remove();
+ }
+ let options = {model: Presentation};
+ let presentCollection = new Collection(presentations, options);
+ this.presentationsView = this.renderCollection(
+ presentCollection,
+ PresentationView,
+ this.queryByHook("presentation-list")
+ );
+ },
+ renderProjectsView: function (projects) {
+ if(this.projectsView) {
+ this.projectsView.remove();
+ }
+ let options = {model: Project, comparator: 'parentDir'};
+ let projectCollection = new Collection(projects, options);
+ this.projectsView = this.renderCollection(
+ projectCollection,
+ EditProjectView,
+ this.queryByHook("projects-view-container")
+ );
+ },
+ update: function (target) {
+ if(target === "Projects") {
+ this.getProjects();
+ }else if(target === "Files") {
+ this.jstreeView.refreshJSTree(null);
+ }else if(target === "Presentations") {
+ this.getPresentations();
+ }
+ }
+});
+
+initPage(FileBrowser);
diff --git a/client/pages/domain-editor.js b/client/pages/domain-editor.js
index ef6bc1e3ab..670be49142 100644
--- a/client/pages/domain-editor.js
+++ b/client/pages/domain-editor.js
@@ -167,12 +167,12 @@ let DomainEditor = PageView.extend({
this.saveDomain()
}else{
var self = this
- if(document.querySelector('#newModalModel')) {
- document.querySelector('#newModalModel').remove()
+ if(document.querySelector('#newDomainModal')) {
+ document.querySelector('#newDomainModal').remove()
}
- let modal = $(modals.renderCreateModalHtml(true, true)).modal();
- let okBtn = document.querySelector('#newModalModel .ok-model-btn');
- let input = document.querySelector('#newModalModel #modelNameInput');
+ let modal = $(modals.createDomainHtml()).modal();
+ let okBtn = document.querySelector('#newDomainModal .ok-model-btn');
+ let input = document.querySelector('#newDomainModal #domainNameInput');
input.addEventListener("keyup", function (event) {
if(event.keyCode === 13){
event.preventDefault();
@@ -180,9 +180,9 @@ let DomainEditor = PageView.extend({
}
});
input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newModalModel #modelNameInputEndCharError')
- var charErrMsg = document.querySelector('#newModalModel #modelNameInputSpecCharError')
- let error = self.validateName(input.value)
+ var endErrMsg = document.querySelector('#newDomainModal #domainNameInputEndCharError')
+ var charErrMsg = document.querySelector('#newDomainModal #domainNameInputSpecCharError')
+ let error = app.validateName(input.value)
okBtn.disabled = error !== "" || input.value.trim() === ""
charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
@@ -892,22 +892,6 @@ let DomainEditor = PageView.extend({
}
},
updateValid: function () {},
- validateName(input, rename = false) {
- var error = ""
- if(input.endsWith('/')) {
- error = 'forward'
- }
- var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"
- if(rename) {
- invalidChars += "/"
- }
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both"
- }
- }
- return error
- },
toggleDomainError: function () {
let errorMsg = $(this.queryByHook('domain-error'))
if(!this.domain.valid) {
diff --git a/client/pages/file-browser.js b/client/pages/file-browser.js
deleted file mode 100644
index cfe2aa7ced..0000000000
--- a/client/pages/file-browser.js
+++ /dev/null
@@ -1,1474 +0,0 @@
-/*
-StochSS is a platform for simulating biochemical systems
-Copyright (C) 2019-2021 StochSS developers.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-let jstree = require('jstree');
-let path = require('path');
-let $ = require('jquery');
-let _ = require('underscore');
-//support files
-let app = require('../app');
-let modals = require('../modals');
-//models
-let Model = require('../models/model');
-//views
-let PageView = require('./base');
-//templates
-let template = require('../templates/pages/fileBrowser.pug');
-
-import initPage from './page.js';
-
-let FileBrowser = PageView.extend({
- pageTitle: 'StochSS | File Browser',
- template: template,
- events: {
- 'click [data-hook=refresh-jstree]' : 'refreshJSTree',
- 'click [data-hook=options-for-node]' : 'showContextMenuForNode',
- 'click [data-hook=new-directory]' : 'handleCreateDirectoryClick',
- 'click [data-hook=new-project]' : 'handleCreateProjectClick',
- 'click [data-hook=new-model]' : 'handleCreateModelClick',
- 'click [data-hook=new-domain]' : 'handleCreateDomain',
- 'click [data-hook=upload-file-btn]' : 'handleUploadFileClick',
- 'click [data-hook=file-browser-help]' : function () {
- let modal = $(modals.operationInfoModalHtml('file-browser')).modal();
- },
- 'click [data-hook=empty-trash]' : 'emptyTrash'
- },
- initialize: function (attrs, options) {
- PageView.prototype.initialize.apply(this, arguments)
- var self = this
- this.root = "none"
- this.ajaxData = {
- "url" : function (node) {
- if(node.parent === null){
- return path.join(app.getApiPath(), "file/browser-list")+"?path="+self.root
- }
- return path.join(app.getApiPath(), "file/browser-list")+"?path="+ node.original._path
- },
- "dataType" : "json",
- "data" : function (node) {
- return { 'id' : node.id}
- },
- }
- this.treeSettings = {
- 'plugins': ['types', 'wholerow', 'changed', 'contextmenu', 'dnd'],
- 'core': {'multiple' : false, 'animation': 0,
- 'check_callback': function (op, node, par, pos, more) {
- if(op === "rename_node" && self.validateName(pos, true) !== ""){
- document.querySelector("#renameSpecialCharError").style.display = "block"
- setTimeout(function () {
- document.querySelector("#renameSpecialCharError").style.display = "none"
- }, 5000)
- return false
- }
- if(op === 'move_node' && node && node.type && node.type === "workflow" && node.original && node.original._status && node.original._status === "running"){
- return false
- }
- if(op === 'move_node' && more && more.ref && more.ref.type && !(more.ref.type == 'folder' || more.ref.type == 'root')){
- return false
- }
- if(op === 'move_node' && more && more.ref && more.ref.original && path.dirname(more.ref.original._path).split("/").includes("trash")){
- return false
- }
- if(op === 'move_node' && more && more.ref && more.ref.type && more.ref.type === 'folder' && more.ref.text !== "trash"){
- if(!more.ref.state.loaded){
- return false
- }
- var exists = false
- var BreakException = {}
- try{
- more.ref.children.forEach(function (child) {
- var child_node = $('#models-jstree').jstree().get_node(child)
- exists = child_node.text === node.text
- if(exists){
- throw BreakException;
- }
- })
- }catch{
- return false;
- }
- }
- if(op === 'move_node' && more && (pos != 0 || more.pos !== "i") && !more.core){
- return false
- }
- if(op === 'move_node' && more && more.core) {
- var newDir = par.type !== "root" ? par.original._path : ""
- var file = node.original._path.split('/').pop()
- var oldPath = node.original._path
- let queryStr = "?srcPath="+oldPath+"&dstPath="+path.join(newDir, file)
- var endpoint = path.join(app.getApiPath(), "file/move")+queryStr
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- node.original._path = path.join(newDir, file);
- if(node.type === "folder") {
- $('#models-jstree').jstree().refresh_node(node);
- }else if(newDir.endsWith("trash")) {
- $(self.queryByHook('empty-trash')).prop('disabled', false);
- $('#models-jstree').jstree().refresh_node(par);
- }else if(oldPath.split("/").includes("trash")) {
- $('#models-jstree').jstree().refresh_node(par);
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- $('#models-jstree').jstree().refresh();
- }
- });
- }
- return true
- },
- 'themes': {'stripes': true, 'variant': 'large'},
- 'data': this.ajaxData,
- },
- 'types' : {
- 'root' : {"icon": "jstree-icon jstree-folder"},
- 'folder' : {"icon": "jstree-icon jstree-folder"},
- 'spatial' : {"icon": "jstree-icon jstree-file"},
- 'nonspatial' : {"icon": "jstree-icon jstree-file"},
- 'project' : {"icon": "jstree-icon jstree-file"},
- 'workflow-group' : {"icon": "jstree-icon jstree-file"},
- 'workflow' : {"icon": "jstree-icon jstree-file"},
- 'notebook' : {"icon": "jstree-icon jstree-file"},
- 'domain' : {"icon": "jstree-icon jstree-file"},
- 'sbml-model' : {"icon": "jstree-icon jstree-file"},
- 'other' : {"icon": "jstree-icon jstree-file"},
- },
- }
- },
- render: function () {
- var self = this;
- this.nodeForContextMenu = "";
- this.renderWithTemplate();
- this.jstreeIsLoaded = false
- window.addEventListener('pageshow', function (e) {
- var navType = window.performance.navigation.type
- if(navType === 2){
- window.location.reload()
- }
- });
- this.setupJstree(function () {
- setTimeout(function () {
- self.refreshInitialJSTree();
- }, 3000);
- });
- $(document).on('hide.bs.modal', '.modal', function (e) {
- e.target.remove()
- });
- },
- refreshJSTree: function () {
- this.jstreeIsLoaded = false
- $('#models-jstree').jstree().deselect_all(true)
- $('#models-jstree').jstree().refresh()
- },
- refreshInitialJSTree: function () {
- var self = this;
- var count = $('#models-jstree').jstree()._model.data['#'].children.length;
- if(count == 0) {
- self.refreshJSTree();
- setTimeout(function () {
- self.refreshInitialJSTree();
- }, 3000);
- }
- },
- selectNode: function (node, fileName) {
- let self = this
- if(!this.jstreeIsLoaded || !$('#models-jstree').jstree().is_loaded(node) && $('#models-jstree').jstree().is_loading(node)) {
- setTimeout(_.bind(self.selectNode, self, node, fileName), 1000);
- }else{
- node = $('#models-jstree').jstree().get_node(node)
- var child = ""
- for(var i = 0; i < node.children.length; i++) {
- var child = $('#models-jstree').jstree().get_node(node.children[i])
- if(child.original.text === fileName) {
- $('#models-jstree').jstree().select_node(child)
- let optionsButton = $(self.queryByHook("options-for-node"))
- if(!self.nodeForContextMenu){
- optionsButton.prop('disabled', false)
- }
- optionsButton.text("Actions for " + child.original.text)
- self.nodeForContextMenu = child;
- break
- }
- }
- }
- },
- handleUploadFileClick: function (e) {
- let type = e.target.dataset.type
- this.uploadFile(undefined, type)
- },
- uploadFile: function (o, type) {
- var self = this
- if(document.querySelector('#uploadFileModal')) {
- document.querySelector('#uploadFileModal').remove()
- }
- if(this.browser == undefined) {
- this.browser = app.getBrowser();
- }
- if(this.isSafariV14Plus == undefined){
- this.isSafariV14Plus = (this.browser.name === "Safari" && this.browser.version >= 14)
- }
- let modal = $(modals.uploadFileHtml(type, this.isSafariV14Plus)).modal();
- let uploadBtn = document.querySelector('#uploadFileModal .upload-modal-btn');
- let fileInput = document.querySelector('#uploadFileModal #fileForUpload');
- let input = document.querySelector('#uploadFileModal #fileNameInput');
- let fileCharErrMsg = document.querySelector('#uploadFileModal #fileSpecCharError')
- let nameEndErrMsg = document.querySelector('#uploadFileModal #fileNameInputEndCharError')
- let nameCharErrMsg = document.querySelector('#uploadFileModal #fileNameInputSpecCharError')
- let nameUsageMsg = document.querySelector('#uploadFileModal #fileNameUsageMessage')
- fileInput.addEventListener('change', function (e) {
- let fileErr = !fileInput.files.length ? "" : self.validateName(fileInput.files[0].name)
- let nameErr = self.validateName(input.value)
- if(!fileInput.files.length) {
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'none'
- }else if(fileErr === "" || (Boolean(input.value) && nameErr === "")){
- uploadBtn.disabled = false
- fileCharErrMsg.style.display = 'none'
- }else{
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'block'
- }
- })
- input.addEventListener("input", function (e) {
- let fileErr = !fileInput.files.length ? "" : self.validateName(fileInput.files[0].name)
- let nameErr = self.validateName(input.value)
- if(!fileInput.files.length) {
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'none'
- }else if(fileErr === "" || (Boolean(input.value) && nameErr === "")){
- uploadBtn.disabled = false
- fileCharErrMsg.style.display = 'none'
- }else{
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'block'
- }
- nameCharErrMsg.style.display = nameErr === "both" || nameErr === "special" ? "block" : "none"
- nameEndErrMsg.style.display = nameErr === "both" || nameErr === "forward" ? "block" : "none"
- nameUsageMsg.style.display = nameErr !== "" ? "block" : "none"
- });
- uploadBtn.addEventListener('click', function (e) {
- let file = fileInput.files[0]
- var fileinfo = {"type":type,"name":"","path":"/"}
- if(o && o.original){
- fileinfo.path = o.original._path
- }
- if(Boolean(input.value) && self.validateName(input.value) === ""){
- fileinfo.name = input.value.trim()
- }
- let formData = new FormData()
- formData.append("datafile", file)
- formData.append("fileinfo", JSON.stringify(fileinfo))
- let endpoint = path.join(app.getApiPath(), 'file/upload');
- app.postXHR(endpoint, formData, {
- success: function (err, response, body) {
- body = JSON.parse(body);
- if(o){
- var node = $('#models-jstree').jstree().get_node(o.parent);
- if(node.type === "root" || node.type === "#"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- }else{
- self.refreshJSTree();
- }
- if(body.errors.length > 0){
- let errorModal = $(modals.uploadFileErrorsHtml(file.name, type, body.message, body.errors)).modal();
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- let zipErrorModal = $(modals.projectExportErrorHtml(resp.Reason, resp.Message)).modal();
- }
- }, false);
- modal.modal('hide')
- });
- },
- deleteFile: function (o) {
- var fileType = o.type
- if(fileType === "nonspatial")
- fileType = "model";
- else if(fileType === "spatial")
- fileType = "spatial model"
- else if(fileType === "sbml-model")
- fileType = "sbml model"
- else if(fileType === "other")
- fileType = "file"
- var self = this
- if(document.querySelector('#deleteFileModal')) {
- document.querySelector('#deleteFileModal').remove()
- }
- let modal = $(modals.deleteFileHtml(fileType)).modal();
- let yesBtn = document.querySelector('#deleteFileModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- var endpoint = path.join(app.getApiPath(), "file/delete")+"?path="+o.original._path
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree').jstree().get_node(o.parent);
- if(node.type === "root"){
- self.refreshJSTree();
- let actionsBtn = $(self.queryByHook("options-for-node"));
- if(actionsBtn.text().endsWith(o.text)) {
- actionsBtn.text("Actions");
- actionsBtn.prop("disabled", true);
- self.nodeForContextMenu = "";
- }
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- }
- });
- modal.modal('hide')
- });
- },
- duplicateFileOrDirectory: function(o, type) {
- var self = this;
- var parentID = o.parent;
- var queryStr = "?path="+o.original._path
- if(!type && o.original.type === 'folder'){
- type = "directory"
- }else if(!type && o.original.type === 'workflow'){
- type = "workflow"
- }else if(!type){
- type = "file"
- }
- if(type === "directory"){
- var identifier = "directory/duplicate"
- }else if(type === "workflow" || type === "wkfl_model"){
- var timeStamp = type === "workflow" ? this.getTimeStamp() : "None"
- var identifier = "workflow/duplicate"
- queryStr = queryStr.concat("&target="+type+"&stamp="+timeStamp)
- }else{
- var identifier = "file/duplicate"
- }
- var endpoint = path.join(app.getApiPath(), identifier)+queryStr
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree').jstree().get_node(parentID);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- if(type === "workflow"){
- var message = ""
- if(body.error){
- message = body.error;
- }else{
- message = "The model for "+body.File+" is located here: "+body.mdlPath+"";
- }
- let modal = $(modals.duplicateWorkflowHtml(body.File, message)).modal();
- }
- self.selectNode(node, body.File);
- }
- });
- },
- getTimeStamp: function () {
- var date = new Date();
- var year = date.getFullYear();
- var month = date.getMonth() + 1;
- if(month < 10){
- month = "0" + month
- }
- var day = date.getDate();
- if(day < 10){
- day = "0" + day
- }
- var hours = date.getHours();
- if(hours < 10){
- hours = "0" + hours
- }
- var minutes = date.getMinutes();
- if(minutes < 10){
- minutes = "0" + minutes
- }
- var seconds = date.getSeconds();
- if(seconds < 10){
- seconds = "0" + seconds
- }
- return "_" + month + day + year + "_" + hours + minutes + seconds;
- },
- toSpatial: function (o) {
- var self = this;
- var parentID = o.parent;
- var endpoint = path.join(app.getApiPath(), "model/to-spatial")+"?path="+o.original._path;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree').jstree().get_node(parentID);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- self.selectNode(node, body.File);
- }
- });
- },
- toModel: function (o, from) {
- var self = this;
- var parentID = o.parent;
- if(from === "Spatial"){
- var identifier = "spatial/to-model"
- }else{
- var identifier = "sbml/to-model"
- }
- let endpoint = path.join(app.getApiPath(), identifier)+"?path="+o.original._path;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree').jstree().get_node(parentID);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- self.selectNode(node, body.File);
- if(from === "SBML" && body.errors.length > 0){
- var title = "";
- var msg = body.message;
- var errors = body.errors;
- let modal = $(modals.sbmlToModelHtml(msg, errors)).modal();
- }
- }
- });
- },
- toNotebook: function (o, type) {
- let self = this
- var endpoint = ""
- if(type === "model"){
- endpoint = path.join(app.getApiPath(), "model/to-notebook")+"?path="+o.original._path
- }else{
- endpoint = path.join(app.getApiPath(), "workflow/notebook")+"?type=none&path="+o.original._path
- }
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree').jstree().get_node(o.parent);
- if(node.type === 'root'){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- var notebookPath = path.join(app.getBasePath(), "notebooks", body.FilePath);
- self.selectNode(node, body.File);
- window.open(notebookPath);
- }
- });
- },
- toSBML: function (o) {
- var self = this;
- var parentID = o.parent;
- var endpoint = path.join(app.getApiPath(), "model/to-sbml")+"?path="+o.original._path;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree').jstree().get_node(parentID);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- self.selectNode(node, body.File);
- }
- });
- },
- renameNode: function (o) {
- var self = this
- var text = o.text;
- var parent = $('#models-jstree').jstree().get_node(o.parent)
- var extensionWarning = $(this.queryByHook('extension-warning'));
- var nameWarning = $(this.queryByHook('rename-warning'));
- extensionWarning.collapse('show')
- $('#models-jstree').jstree().edit(o, null, function(node, status) {
- if(text != node.text){
- var endpoint = path.join(app.getApiPath(), "file/rename")+"?path="+ o.original._path+"&name="+node.text
- app.getXHR(endpoint, {
- always: function (err, response, body) {
- if(parent.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(parent);
- }
- },
- success: function (err, response, body) {
- if(body.changed) {
- nameWarning.text(body.message);
- nameWarning.collapse('show');
- window.scrollTo(0,0);
- setTimeout(_.bind(self.hideNameWarning, self), 10000);
- }
- node.original._path = body._path;
- }
- });
- }
- extensionWarning.collapse('hide');
- nameWarning.collapse('hide');
- });
- },
- hideNameWarning: function () {
- $(this.queryByHook('rename-warning')).collapse('hide')
- },
- getExportData: function (o, asZip) {
- var self = this;
- let nodeType = o.original.type
- let isJSON = nodeType === "sbml-model" ? false : true
- if(nodeType === "sbml-model"){
- var dataType = "plain-text"
- var identifier = "file/download"
- }else if(nodeType === "domain") {
- var dataType = "json"
- var identifier = "spatial-model/load-domain"
- }else if(asZip) {
- var dataType = "zip"
- var identifier = "file/download-zip"
- }else{
- var dataType = "json"
- var identifier = "file/json-data"
- }
- if(nodeType === "domain") {
- var queryStr = "?domain_path=" + o.original._path
- }else{
- var queryStr = "?path="+o.original._path
- if(dataType === "json"){
- queryStr = queryStr.concat("&for=None")
- }else if(dataType === "zip"){
- queryStr = queryStr.concat("&action=generate")
- }
- }
- var endpoint = path.join(app.getApiPath(), identifier)+queryStr
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(dataType === "json") {
- let data = nodeType === "domain" ? body.domain : body;
- self.exportToJsonFile(data, o.original.text);
- }else if(dataType === "zip") {
- var node = $('#models-jstree').jstree().get_node(o.parent);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- self.exportToZipFile(body.Path);
- }else{
- self.exportToFile(body, o.original.text);
- }
- },
- error: function (err, response, body) {
- if(dataType === "plain-text") {
- body = JSON.parse(body);
- }
- }
- });
- },
- exportToJsonFile: function (fileData, fileName) {
- let dataStr = JSON.stringify(fileData);
- let dataURI = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
- let exportFileDefaultName = fileName
-
- let linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataURI);
- linkElement.setAttribute('download', exportFileDefaultName);
- linkElement.click();
- },
- exportToFile: function (fileData, fileName) {
- let dataURI = 'data:text/plain;charset=utf-8,' + encodeURIComponent(fileData);
-
- let linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataURI);
- linkElement.setAttribute('download', fileName);
- linkElement.click();
- },
- exportToZipFile: function (o) {
- var targetPath = o
- if(o.original){
- targetPath = o.original._path
- }
- var endpoint = path.join(app.getBasePath(), "/files", targetPath);
- window.open(endpoint)
- },
- validateName: function (input, rename = false) {
- var error = ""
- if(input.endsWith('/')) {
- error = 'forward'
- }
- var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"
- if(rename) {
- invalidChars += "/"
- }
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both"
- }
- }
- return error
- },
- newProjectOrWorkflowGroup: function (o, isProject) {
- var self = this
- if(document.querySelector("#newProjectModal")) {
- document.querySelector("#newProjectModal").remove()
- }
- var modal = $(modals.newProjectModalHtml()).modal();
- var okBtn = document.querySelector('#newProjectModal .ok-model-btn');
- var input = document.querySelector('#newProjectModal #projectNameInput');
- input.addEventListener("keyup", function (event) {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newProjectModal #projectNameInputEndCharError')
- var charErrMsg = document.querySelector('#newProjectModal #projectNameInputSpecCharError')
- let error = self.validateName(input.value)
- okBtn.disabled = error !== "" || input.value.trim() === ""
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
- });
- okBtn.addEventListener("click", function (e) {
- if(Boolean(input.value)) {
- modal.modal('hide')
- var parentPath = ""
- if(o && o.original && o.original.type !== "root") {
- parentPath = o.original._path
- }
- var projectName = input.value.trim() + ".proj"
- var projectPath = path.join(parentPath, projectName)
- var endpoint = path.join(app.getApiPath(), "project/new-project")+"?path="+projectPath
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let queryStr = "?path=" + body.path;
- let endpoint = path.join(app.getBasePath(), 'stochss/project/manager') + queryStr;
- window.location.href = endpoint;
- },
- error: function (err, response, body) {
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- }
- });
- },
- newWorkflow: function (o, type) {
- let self = this;
- let model = new Model({
- directory: o.original._path
- });
- app.getXHR(model.url(), {
- success: function (err, response, body) {
- model.set(body);
- model.updateValid();
- if(model.valid){
- app.newWorkflow(self, o.original._path, o.type === "spatial", type);
- }else{
- let title = "Model Errors Detected";
- let endpoint = path.join(app.getBasePath(), "stochss/models/edit") + '?path=' + model.directory + '&validate';
- let message = 'Errors were detected in you model click here to fix your model';
- $(modals.modelErrorHtml(title, message)).modal();
- }
- }
- });
- },
- addExistingModel: function (o) {
- var self = this
- if(document.querySelector('#newProjectModelModal')){
- document.querySelector('#newProjectModelModal').remove()
- }
- let mdlListEP = path.join(app.getApiPath(), 'project/add-existing-model') + "?path="+o.original._path
- app.getXHR(mdlListEP, {
- always: function (err, response, body) {
- let modal = $(modals.newProjectModelHtml(body.files)).modal();
- let okBtn = document.querySelector('#newProjectModelModal .ok-model-btn');
- let select = document.querySelector('#newProjectModelModal #modelFileInput');
- let location = document.querySelector('#newProjectModelModal #modelPathInput');
- select.addEventListener("change", function (e) {
- okBtn.disabled = e.target.value && body.paths[e.target.value].length >= 2;
- if(body.paths[e.target.value].length >= 2) {
- var locations = body.paths[e.target.value].map(function (path) {
- return ``;
- });
- locations.unshift(``);
- locations = locations.join(" ");
- $("#modelPathInput").find('option').remove().end().append(locations);
- $("#location-container").css("display", "block");
- }else{
- $("#location-container").css("display", "none");
- $("#modelPathInput").find('option').remove().end();
- }
- });
- location.addEventListener("change", function (e) {
- okBtn.disabled = !Boolean(e.target.value);
- });
- okBtn.addEventListener("click", function (e) {
- let mdlPath = body.paths[select.value].length < 2 ? body.paths[select.value][0] : location.value;
- let queryString = "?path="+o.original._path+"&mdlPath="+mdlPath;
- let endpoint = path.join(app.getApiPath(), 'project/add-existing-model') + queryString;
- app.postXHR(endpoint, null, {
- success: function (err, response, body) {
- let successModal = $(modals.newProjectModelSuccessHtml(body.message)).modal();
- },
- error: function (err, response, body) {
- let errorModal = $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- modal.modal('hide');
- });
- }
- });
- },
- addModel: function (parentPath, modelName, message) {
- var endpoint = path.join(app.getBasePath(), "stochss/models/edit")
- if(parentPath.endsWith(".proj")) {
- let queryString = "?path=" + parentPath + "&mdlFile=" + modelName
- let newMdlEP = path.join(app.getApiPath(), "project/new-model") + queryString
- app.getXHR(newMdlEP, {
- success: function (err, response, body) {
- endpoint += "?path="+body.path;
- window.location.href = endpoint;
- },
- error: function (err, response, body) {
- let title = "Model Already Exists";
- let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
- }
- });
- }else{
- let modelPath = path.join(parentPath, modelName)
- let queryString = "?path="+modelPath+"&message="+message;
- endpoint += queryString
- let existEP = path.join(app.getApiPath(), "model/exists")+queryString
- app.getXHR(existEP, {
- always: function (err, response, body) {
- if(body.exists) {
- let title = "Model Already Exists";
- let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
- }else{
- window.location.href = endpoint;
- }
- }
- });
- }
- },
- newModelOrDirectory: function (o, isModel, isSpatial) {
- var self = this
- if(document.querySelector('#newModalModel')) {
- document.querySelector('#newModalModel').remove()
- }
- let modal = $(modals.renderCreateModalHtml(isModel, isSpatial)).modal();
- let okBtn = document.querySelector('#newModalModel .ok-model-btn');
- let input = document.querySelector('#newModalModel #modelNameInput');
- input.addEventListener("keyup", function (event) {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newModalModel #modelNameInputEndCharError')
- var charErrMsg = document.querySelector('#newModalModel #modelNameInputSpecCharError')
- let error = self.validateName(input.value)
- okBtn.disabled = error !== "" || input.value.trim() === ""
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
- });
- okBtn.addEventListener('click', function (e) {
- if (Boolean(input.value)) {
- modal.modal('hide')
- var parentPath = ""
- if(o && o.original && o.original.type !== "root"){
- parentPath = o.original._path
- }
- if(isModel) {
- let ext = isSpatial ? ".smdl" : ".mdl"
- let modelName = o && o.type === "project" ? input.value.trim().split("/").pop() + ext : input.value.trim() + ext;
- let message = modelName !== input.value.trim() + ext?
- "Warning: Models are saved directly in StochSS Projects and cannot be saved to the "+input.value.trim().split("/")[0]+" directory in the project.
Do you wish to save your model directly in your project?
" : ""
- if(message){
- let warningModal = $(modals.newProjectModelWarningHtml(message)).modal()
- let yesBtn = document.querySelector('#newProjectModelWarningModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- warningModal.modal('hide')
- self.addModel(parentPath, modelName, message);
- });
- }else{
- self.addModel(parentPath, modelName, message);
- }
- }else{
- let dirName = input.value.trim();
- let endpoint = path.join(app.getApiPath(), "directory/create")+"?path="+path.join(parentPath, dirName);
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(o){//directory was created with context menu option
- var node = $('#models-jstree').jstree().get_node(o);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- }else{//directory was created with create directory button
- self.refreshJSTree();
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- let errorModal = $(modals.newDirectoryErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- }
- }
- });
- },
- handleCreateDirectoryClick: function (e) {
- this.newModelOrDirectory(undefined, false, false);
- },
- handleCreateProjectClick: function (e) {
- this.newProjectOrWorkflowGroup(undefined, true)
- },
- handleCreateModelClick: function (e) {
- let isSpatial = e.target.dataset.type === "spatial"
- this.newModelOrDirectory(undefined, true, isSpatial);
- },
- handleCreateDomain: function (e) {
- let queryStr = "?domainPath=/&new"
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- },
- showContextMenuForNode: function (e) {
- $('#models-jstree').jstree().show_contextmenu(this.nodeForContextMenu)
- },
- editWorkflowModel: function (o) {
- let endpoint = path.join(app.getApiPath(), "workflow/edit-model")+"?path="+o.original._path
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(body.error){
- let title = o.text + " Not Found";
- let message = body.error;
- let modal = $(modals.duplicateWorkflowHtml(title, message)).modal();
- }else{
- window.location.href = path.join(app.routePrefix, "models/edit")+"?path="+body.file;
- }
- }
- });
- },
- extractAll: function (o) {
- let self = this;
- let queryStr = "?path=" + o.original._path;
- let endpoint = path.join(app.getApiPath(), "file/unzip") + queryStr;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let node = $('#models-jstree').jstree().get_node(o.parent);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(node);
- }
- },
- error: function (err, response, body) {
- let modal = $(modals.newProjectModelErrorHtml(body.Reason, body.message)).modal();
- }
- });
- },
- moveToTrash: function (o) {
- if(document.querySelector('#moveToTrashConfirmModal')) {
- document.querySelector('#moveToTrashConfirmModal').remove();
- }
- let self = this;
- let modal = $(modals.moveToTrashConfirmHtml("model")).modal();
- let yesBtn = document.querySelector('#moveToTrashConfirmModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- modal.modal('hide');
- let queryStr = "?srcPath=" + o.original._path + "&dstPath=" + path.join("trash", o.text)
- let endpoint = path.join(app.getApiPath(), "file/move") + queryStr
- app.getXHR(endpoint, {
- always: function (err, response, body) {
- $(self.queryByHook('empty-trash')).prop('disabled', false);
- $('#models-jstree').jstree().refresh();
- }
- });
- });
- },
- emptyTrash: function (e) {
- if(document.querySelector("#emptyTrashConfirmModal")) {
- document.querySelector("#emptyTrashConfirmModal").remove()
- }
- let self = this;
- let modal = $(modals.emptyTrashConfirmHtml()).modal();
- let yesBtn = document.querySelector('#emptyTrashConfirmModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- modal.modal('hide');
- let endpoint = path.join(app.getApiPath(), "file/empty-trash") + "?path=trash";
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- self.refreshJSTree();
- $(self.queryByHook('empty-trash')).prop('disabled', true);
- }
- });
- });
- },
- publishNotebookPresentation: function (o) {
- let queryStr = "?path=" + o.original._path;
- let endpoint = path.join(app.getApiPath(), "notebook/presentation") + queryStr;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let title = body.message;
- let linkHeaders = "Shareable Presentation";
- let links = body.links;
- $(modals.presentationLinks(title, linkHeaders, links)).modal();
- let copyBtn = document.querySelector('#presentationLinksModal #copy-to-clipboard');
- copyBtn.addEventListener('click', function (e) {
- let onFulfilled = (value) => {
- $("#copy-link-success").css("display", "inline-block");
- }
- let onReject = (reason) => {
- let msg = $("#copy-link-failed");
- msg.html(reason);
- msg.css("display", "inline-block");
- }
- app.copyToClipboard(links.presentation, onFulfilled, onReject);
- });
- },
- error: function (err, response, body) {
- $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- },
- setupJstree: function () {
- var self = this;
- $.jstree.defaults.contextmenu.items = (o, cb) => {
- let optionsButton = $(self.queryByHook("options-for-node"))
- if(!self.nodeForContextMenu){
- optionsButton.prop('disabled', false)
- }
- optionsButton.text("Actions for " + o.original.text)
- self.nodeForContextMenu = o;
- let nodeType = o.original.type
- let zipTypes = ["workflow", "folder", "other", "project", "workflow-group"]
- let asZip = zipTypes.includes(nodeType)
- // common to all type except root
- let common = {
- "Download" : {
- "label" : asZip ? "Download as .zip" : "Download",
- "_disabled" : false,
- "separator_before" : true,
- "separator_after" : false,
- "action" : function (data) {
- if(o.original.text.endsWith('.zip')){
- self.exportToZipFile(o);
- }else{
- self.getExportData(o, asZip)
- }
- }
- },
- "Rename" : {
- "label" : "Rename",
- "_disabled" : (o.type === "workflow" && o.original._status === "running"),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.renameNode(o);
- }
- },
- "Duplicate" : {
- "label" : (nodeType === "workflow") ? "Duplicate as new" : "Duplicate",
- "_disabled" : (nodeType === "project"),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.duplicateFileOrDirectory(o, null)
- }
- },
- "MoveToTrash" : {
- "label" : "Move To Trash",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.moveToTrash(o);
- }
- }
- }
- let delete_node = {
- "Delete" : {
- "label" : "Delete",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.deleteFile(o);
- }
- }
- }
- // common to root and folders
- let folder = {
- "Refresh" : {
- "label" : "Refresh",
- "_disabled" : false,
- "_class" : "font-weight-bold",
- "separator_before" : false,
- "separator_after" : true,
- "action" : function (data) {
- if(nodeType === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree').jstree().refresh_node(o);
- }
- }
- },
- "New_Directory" : {
- "label" : "New Directory",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newModelOrDirectory(o, false, false);
- }
- },
- "New Project" : {
- "label" : "New Project",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newProjectOrWorkflowGroup(o, true)
- }
- },
- "New_model" : {
- "label" : "New Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "spatial" : {
- "label" : "Spatial (beta)",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newModelOrDirectory(o, true, true);
- }
- },
- "nonspatial" : {
- "label" : "Non-Spatial",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newModelOrDirectory(o, true, false);
- }
- }
- }
- },
- "New Domain" : {
- "label" : "New Domain (beta)",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- let queryStr = "?domainPath=" + o.original._path + "&new"
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- }
- },
- "Upload" : {
- "label" : "Upload File",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Model" : {
- "label" : "StochSS Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.uploadFile(o, "model")
- }
- },
- "SBML" : {
- "label" : "SBML Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.uploadFile(o, "sbml")
- }
- },
- "File" : {
- "label" : "File",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.uploadFile(o, "file")
- }
- }
- }
- }
- }
- // common to both spatial and non-spatial models
- let newWorkflow = {
- "ensembleSimulation" : {
- "label" : "Ensemble Simulation",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newWorkflow(o, "Ensemble Simulation")
- }
- },
- "parameterSweep" : {
- "label" : "Parameter Sweep",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newWorkflow(o, "Parameter Sweep")
- }
- },
- "jupyterNotebook" : {
- "label" : "Jupyter Notebook",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- window.location.href = path.join(app.getBasePath(), "stochss/workflow/selection")+"?path="+o.original._path;
- }
- }
- }
- let model = {
- "Edit" : {
- "label" : "Edit",
- "_disabled" : false,
- "_class" : "font-weight-bolder",
- "separator_before" : false,
- "separator_after" : true,
- "action" : function (data) {
- window.location.href = path.join(app.getBasePath(), "stochss/models/edit")+"?path="+o.original._path;
- }
- },
- "New Workflow" : {
- "label" : "New Workflow",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : o.type === "nonspatial" ? newWorkflow : {"jupyterNotebook":newWorkflow.jupyterNotebook}
- }
- }
- // convert options for spatial models
- let spatialConvert = {
- "Convert" : {
- "label" : "Convert",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Convert to Model" : {
- "label" : "Convert to Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toModel(o, "Spatial");
- }
- },
- "Convert to Notebook" : {
- "label" : "Convert to Notebook",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toNotebook(o, "model")
- }
- }
- }
- }
- }
- // convert options for non-spatial models
- let modelConvert = {
- "Convert" : {
- "label" : "Convert",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Convert to Spatial" : {
- "label" : "To Spatial Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toSpatial(o)
- }
- },
- "Convert to Notebook" : {
- "label" : "To Notebook",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toNotebook(o, "model")
- }
- },
- "Convert to SBML" : {
- "label" : "To SBML Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toSBML(o)
- }
- }
- }
- }
- }
- // For notebooks, workflows, sbml models, and other files
- let open = {
- "Open" : {
- "label" : "Open",
- "_disabled" : false,
- "_class" : "font-weight-bolder",
- "separator_before" : false,
- "separator_after" : true,
- "action" : function (data) {
- if(nodeType === "workflow"){
- window.location.href = path.join(app.getBasePath(), "stochss/workflow/edit")+"?path="+o.original._path+"&type=none";
- }else if(nodeType === "project"){
- window.location.href = path.join(app.getBasePath(), "stochss/project/manager")+"?path="+o.original._path
- }else if(nodeType === "domain") {
- let queryStr = "?domainPath=" + o.original._path
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- }else{
- if(nodeType === "notebook") {
- var identifier = "notebooks"
- }else if(nodeType === "sbml-model") {
- var identifier = "edit"
- }else{
- var identifier = "view"
- }
- window.open(path.join(app.getBasePath(), identifier, o.original._path));
- }
- }
- }
- }
- let project = {
- "Add Model" : {
- "label" : "Add Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "New Model" : folder.New_model,
- "Existing Model" : {
- "label" : "Existing Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.addExistingModel(o)
- }
- }
- }
- }
- }
- // specific to workflows
- let workflow = {
- "Start/Restart Workflow" : {
- "label" : (o.original._status === "ready") ? "Start Workflow" : "Restart Workflow",
- "_disabled" : true,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
-
- }
- },
- "Stop Workflow" : {
- "label" : "Stop Workflow",
- "_disabled" : true,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
-
- }
- },
- "Model" : {
- "label" : "Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Edit" : {
- "label" : " Edit",
- "_disabled" : (!o.original._newFormat && o.original._status !== "ready"),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.editWorkflowModel(o)
- }
- },
- "Extract" : {
- "label" : "Extract",
- "_disabled" : (o.original._newFormat && !o.original._hasJobs),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.duplicateFileOrDirectory(o, "wkfl_model")
- }
- }
- }
- }
- }
- // Specific to sbml files
- let sbml = {
- "Convert" : {
- "label" : "Convert",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Convert to Model" : {
- "label" : "To Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toModel(o, "SBML");
- }
- }
- }
- }
- }
- //Specific to zip archives
- let extractAll = {
- "extractAll" : {
- "label" : "Extract All",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.extractAll(o);
- }
- }
- }
- let notebook = {
- "publish" : {
- "label" : "Publish",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.publishNotebookPresentation(o);
- }
- }
- }
- if (o.type === 'root'){
- return folder
- }
- if (o.text === "trash") {
- return {"Refresh": folder.Refresh}
- }
- if (o.original._path.split("/")[0] === "trash") {
- return delete_node
- }
- if (o.type === 'folder') {
- return $.extend(folder, common)
- }
- if (o.type === 'spatial') {
- return $.extend(model, spatialConvert, common)
- }
- if (o.type === 'nonspatial') {
- return $.extend(model, modelConvert, common)
- }
- if (o.type === 'project'){
- return $.extend(open, project, common)
- }
- if (o.type === 'workflow') {
- return $.extend(open, workflow, common)
- }
- if (o.text.endsWith(".zip")) {
- return $.extend(open, extractAll, common)
- }
- if (o.type === 'notebook') {
- if(app.getBasePath() === "/") {
- return $.extend(open, common)
- }
- return $.extend(open, notebook, common)
- }
- if (o.type === 'other') {
- return $.extend(open, common)
- }
- if (o.type === 'sbml-model') {
- return $.extend(open, sbml, common)
- }
- if (o.type === "domain") {
- return $.extend(open, common)
- }
- }
- $(document).on('shown.bs.modal', function (e) {
- $('[autofocus]', e.target).focus();
- });
- $(document).on('dnd_start.vakata', function (data, element, helper, event) {
- $('#models-jstree').jstree().load_all()
- });
- $('#models-jstree').jstree(this.treeSettings).bind("loaded.jstree", function (event, data) {
- self.jstreeIsLoaded = true
- }).bind("refresh.jstree", function (event, data) {
- self.jstreeIsLoaded = true
- });
- $('#models-jstree').on('click.jstree', function(e) {
- var parent = e.target.parentElement
- var _node = parent.children[parent.children.length - 1]
- var node = $('#models-jstree').jstree().get_node(_node)
- if(_node.nodeName === "A" && $('#models-jstree').jstree().is_loaded(node) && node.type === "folder"){
- $('#models-jstree').jstree().refresh_node(node)
- }else{
- let optionsButton = $(self.queryByHook("options-for-node"))
- if(!self.nodeForContextMenu){
- optionsButton.prop('disabled', false)
- }
- optionsButton.text("Actions for " + node.original.text)
- self.nodeForContextMenu = node;
- }
- });
- $('#models-jstree').on('dblclick.jstree', function(e) {
- var file = e.target.text
- var node = $('#models-jstree').jstree().get_node(e.target)
- var _path = node.original._path;
- if(!(_path.split("/")[0] === "trash")) {
- if(file.endsWith('.mdl') || file.endsWith('.smdl')){
- window.location.href = path.join(app.getBasePath(), "stochss/models/edit")+"?path="+_path;
- }else if(file.endsWith('.ipynb')){
- var notebookPath = path.join(app.getBasePath(), "notebooks", _path)
- window.open(notebookPath, '_blank')
- }else if(file.endsWith('.sbml')){
- var openPath = path.join(app.getBasePath(), "edit", _path)
- window.open(openPath, '_blank')
- }else if(file.endsWith('.proj')){
- window.location.href = path.join(app.getBasePath(), "stochss/project/manager")+"?path="+_path;
- }else if(file.endsWith('.wkfl')){
- window.location.href = path.join(app.getBasePath(), "stochss/workflow/edit")+"?path="+_path+"&type=none";
- }else if(file.endsWith('.domn')) {
- let queryStr = "?domainPath=" + _path
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- }else if(node.type === "folder" && $('#models-jstree').jstree().is_open(node) && $('#models-jstree').jstree().is_loaded(node)){
- $('#models-jstree').jstree().refresh_node(node)
- }else if(node.type === "other"){
- var openPath = path.join(app.getBasePath(), "view", _path);
- window.open(openPath, "_blank");
- }
- }
- });
- }
-});
-
-initPage(FileBrowser);
diff --git a/client/pages/job-presentation.js b/client/pages/job-presentation.js
index 05bdc10b45..ac698ec8f0 100644
--- a/client/pages/job-presentation.js
+++ b/client/pages/job-presentation.js
@@ -26,63 +26,69 @@ let app = require("../app");
let Job = require("../models/job");
//views
let PageView = require('./base');
-let ModelView = require('../model-view/model-view');
-let ResultsView = require('../job-view/views/job-results-view');
-let SettingsView = require('../settings-view/settings-view');
+let JobView = require('../job-view/job-view');
//templates
let template = require('../templates/pages/jobPresentation.pug');
+let loadingTemplate = require('../templates/pages/loadingPage.pug');
+let errorTemplate = require('../templates/pages/errorTemplate.pug');
import bootstrapStyles from '../styles/bootstrap.css';
import styles from '../styles/styles.css';
import fontawesomeStyles from '@fortawesome/fontawesome-free/css/svg-with-js.min.css'
let JobPresentationPage = PageView.extend({
- template: template,
+ template: loadingTemplate,
initialize: function () {
PageView.prototype.initialize.apply(this, arguments);
- console.log("TODO: get the path to the job from the url")
- // let urlParams = new URLSearchParams(window.location.search)
- // this.model = new Job({
- // directory: urlParams.get("path")
- // });
- console.log("TODO: get job from file system using the app.getXHR function")
- // let self = this;
- // let queryStr = "?path=" + this.model.directory;
- // let endpoint = path.join();
- // app.getXHR(endpoint, {
- // success: function (err, response, body) {
- // self.model.set(body);
- // self.model.type = body.titleType;
- // self.renderSubviews();
- // }
- // });
- console.log("TODO: generate the open link and store in this.open")
+ let urlParams = new URLSearchParams(window.location.search);
+ let owner = urlParams.get("owner");
+ let file = urlParams.get("file");
+ this.fileType = "Job"
+ this.model = new Job({
+ directory: file,
+ });
+ let self = this;
+ let queryStr = "?file=" + file + "&owner=" + owner;
+ let endpoint = "api/job/load" + queryStr;
+ app.getXHR(endpoint, {
+ success: function (err, response, body) {
+ self.title = body.name;
+ self.titleType = body.titleType;
+ self.model.set(body);
+ self.renderSubviews(false);
+ },
+ error: function (err, response, body) {
+ self.renderSubviews(true);
+ }
+ });
+ let downloadStart = "https://staging.stochss.org/stochss/job/download_presentation";
+ this.downloadLink = downloadStart + "/" + owner + "/" + file;
+ this.openLink = "https://open.stochss.org?open=" + this.downloadLink;
},
- renderSubviews: function () {
+ render: function (attrs, options) {
PageView.prototype.render.apply(this, arguments);
- this.renderResultsContainer();
- this.renderSettingsContainer();
- this.renderModelContainer();
+ $(this.queryByHook("loading-header")).html(`Loading ${this.fileType}`);
+ $(this.queryByHook("loading-target")).css("display", "none");
+ $(this.queryByHook("loading-spinner")).css("display", "block");
+ let message = `This ${this.fileType} can be downloaded or opened in your own StochSS Live! account using the buttons at the bottom of the page.`;
+ $(this.queryByHook("loading-message")).html(message);
},
- renderModelContainer: function () {
- let modelView = new ModelView({
- model: this.model.model,
- });
- app.registerRenderSubview(this, modelView, "job-model");
+ renderSubviews: function (notFound) {
+ this.template = notFound ? errorTemplate : template
+ PageView.prototype.render.apply(this, arguments);
+ if(!notFound) {
+ this.renderJobView();
+ }
},
- renderResultsContainer: function () {
- let resultsView = new ResultsView({
+ renderJobView: function () {
+ let jobView = new JobView({
model: this.model,
- mode: "presentation"
- });
- app.registerRenderSubview(this, resultsView, "job-results");
- },
- renderSettingsContainer: function () {
- let settingsView = new SettingsView({
- model: this.model.settings,
- mode: "presentation"
+ wkflName: this.title,
+ titleType: this.titleType,
+ newFormat: true,
+ readOnly: true
});
- app.registerRenderSubview(this, settingsView, "job-settings");
+ app.registerRenderSubview(this, jobView, "job-view");
}
});
diff --git a/client/pages/loading-page.js b/client/pages/loading-page.js
index 8a83f98360..1b553faef3 100644
--- a/client/pages/loading-page.js
+++ b/client/pages/loading-page.js
@@ -44,24 +44,52 @@ let LoadingPage = PageView.extend({
$(document.querySelector("main[data-hook=page-main]")).removeClass().addClass("col-md-12 body");
$(this.queryByHook("loading-spinner")).css("display", "block");
if(this.action === "open") {
- this.uploadFileFromLink(this.filePath);
- setTimeout(function () {
- $(self.queryByHook("loading-problem").css("display", "block"));
- }, 30000);
+ this.checkForDuplicateFile(this.filePath);
}else if(this.action === "update-workflow") {
this.updateWorkflowFormat(this.filePath);
}else if(this.action === "update-project") {
this.updateProjectFormat(this.filePath);
}
},
+ checkForDuplicateFile: function (filePath) {
+ $(this.queryByHook("loading-header")).html("Uploading file");
+ $(this.queryByHook("loading-target")).css("display", "none")
+ let message = `If the file is a Project, Workflow, Model, Domain, or Notebook it will be opened when the upload has completed.`;
+ $(this.queryByHook("loading-message")).html(message);
+ let self = this;
+ let queryStr = "?path=" + filePath + "&cmd=validate";
+ let endpoint = path.join(app.getApiPath(), 'file/upload-from-link') + queryStr;
+ app.getXHR(endpoint, {
+ success: function (err, response, body) {
+ if(!body.exists) {
+ self.uploadFileFromLink(filePath, false);
+ }else{
+ let title = "File Already Exists";
+ let message = "A file with that name already exists, do you wish to overwrite this file?";
+ let modal = $(modals.uploadFileExistsHtml(title, message)).modal();
+ let yesBtn = document.querySelector("#uploadFileExistsModal .yes-modal-btn");
+ let noBtn = document.querySelector("#uploadFileExistsModal .btn-secondary")
+ yesBtn.addEventListener('click', function (e) {
+ self.uploadFileFromLink(filePath, true);
+ });
+ noBtn.addEventListener('click', function (e) {
+ window.location.href = self.homeLink;
+ });
+ }
+ }
+ });
+ },
getUploadResponse: function () {
let self = this;
setTimeout(function () {
let queryStr = "?path=" + self.responsePath + "&cmd=read";
let endpoint = path.join(app.getApiPath(), 'file/upload-from-link') + queryStr;
let errorCB = function (err, response, body) {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
$(self.queryByHook("loading-spinner")).css("display", "none");
- let modal = $(modals.projectExportErrorHtml(body.reason, body.message)).modal();
+ let modal = $(modals.errorHtml(body.reason, body.message)).modal();
modal.on('hidden.bs.modal', function (e) {
window.location.href = this.homeLink;
});
@@ -128,13 +156,15 @@ let LoadingPage = PageView.extend({
let identifier = "stochss/workflow/edit"
this.updateFormat(filePath, message, "workflow", identifier);
},
- uploadFileFromLink: function (filePath) {
- $(this.queryByHook("loading-header")).html("Uploading file");
- $(this.queryByHook("loading-target")).css("display", "none")
- let message = `If the file is a Project, Workflow, Model, Domain, or Notebook it will be opened when the upload has completed.`;
- $(this.queryByHook("loading-message")).html(message);
+ uploadFileFromLink: function (filePath, overwrite) {
+ setTimeout(function () {
+ $(self.queryByHook("loading-problem").css("display", "block"));
+ }, 30000);
let self = this;
- let queryStr = "?path=" + filePath;
+ var queryStr = "?path=" + filePath;
+ if(overwrite) {
+ queryStr += "&overwrite=" + overwrite;
+ }
let endpoint = path.join(app.getApiPath(), 'file/upload-from-link') + queryStr;
app.getXHR(endpoint, {
success: function (err, response, body) {
diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js
index 16abd038ed..850b8a9946 100644
--- a/client/pages/model-editor.js
+++ b/client/pages/model-editor.js
@@ -16,206 +16,416 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-var _ = require('underscore');
-var $ = require('jquery');
+let $ = require('jquery');
let path = require('path');
//support files
-var app = require('../app');
-var modals = require('../modals');
-let Tooltips = require("../tooltips");
+let app = require('../app');
+let modals = require('../modals');
+let Plotly = require('../lib/plotly');
//views
-var PageView = require('../pages/base');
+let PageView = require('../pages/base');
let ModelView = require('../model-view/model-view');
-var ParticleViewer = require('../views/view-particle');
-var TimespanSettingsView = require('../settings-view/views/timespan-settings-view');
-var ModelStateButtonsView = require('../views/model-state-buttons');
-var QuickviewDomainTypes = require('../views/quickview-domain-types');
+let TimespanSettingsView = require('../settings-view/views/timespan-settings-view');
//models
-var Model = require('../models/model');
-var Domain = require('../models/domain');
+let Model = require('../models/model');
//templates
-var template = require('../templates/pages/modelEditor.pug');
+let template = require('../templates/pages/modelEditor.pug');
import initPage from './page.js';
let ModelEditor = PageView.extend({
template: template,
events: {
- 'click [data-hook=edit-model-help]' : function () {
+ 'click [data-hook=edit-model-help]': () => {
let modal = $(modals.operationInfoModalHtml('model-editor')).modal();
},
- 'click [data-hook=project-breadcrumb-link]' : 'handleProjectBreadcrumbClick',
+ 'click [data-hook=project-breadcrumb-link]' : 'clickReturnToProjectHandler',
+ 'click [data-hook=save]' : 'clickSaveHandler',
+ 'click [data-hook=run]' : 'handleSimulateClick',
+ "click [data-hook=stochss-es]" : "handleSimulateClick",
+ "click [data-hook=stochss-ps]" : "handleSimulateClick",
+ 'click [data-hook=new-workflow]' : 'handleSimulateClick',
+ 'click [data-hook=return-to-project-btn]' : 'clickReturnToProjectHandler',
+ 'click [data-hook=presentation]' : 'handlePresentationClick',
'click [data-hook=toggle-preview-plot]' : 'togglePreviewPlot',
'click [data-hook=toggle-preview-domain]' : 'toggleDomainPlot',
'click [data-hook=download-png]' : 'clickDownloadPNGButton'
},
initialize: function (attrs, options) {
PageView.prototype.initialize.apply(this, arguments);
- var self = this;
- let urlParams = new URLSearchParams(window.location.search)
- var directory = urlParams.get('path');
- let validate = urlParams.has('validate')
- var modelFile = directory.split('/').pop();
- var name = this.getFileName(decodeURI(modelFile));
- var isSpatial = modelFile.split('.').pop().startsWith('s');
+ let urlParams = new URLSearchParams(window.location.search);
+ let directory = urlParams.get('path');
+ let modelFile = directory.split('/').pop();
this.model = new Model({
- name: name,
+ name: this.getFileName(decodeURI(modelFile)),
directory: directory,
- is_spatial: isSpatial,
+ is_spatial: modelFile.split('.').pop().startsWith('s'),
isPreview: true,
- for: "edit",
+ for: "edit"
});
if(directory.includes('.proj')) {
- this.projectPath = path.dirname(directory)
+ this.projectPath = path.dirname(directory);
if(this.projectPath.endsWith(".wkgp")) {
- this.projectPath = path.dirname(this.projectPath)
+ this.projectPath = path.dirname(this.projectPath);
}
- this.projectName = this.getFileName(this.projectPath)
+ this.projectName = this.getFileName(this.projectPath);
}
app.getXHR(this.model.url(), {
- success: function (err, response, body) {
- self.model.set(body)
- if(directory.includes('.proj')) {
- self.queryByHook("project-breadcrumb-links").style.display = "block"
- self.queryByHook("model-name-header").style.display = "none"
- }
- self.renderSubviews(validate);
- self.model.updateValid()
+ success: (err, response, body) => {
+ this.model.set(body);
+ this.model.updateValid();
+ this.model.autoSave();
+ this.renderSubviews(urlParams.has('validate'));
}
- })
- window.addEventListener("pageshow", function (event) {
- var navType = window.performance.navigation.type
+ });
+ window.addEventListener("pageshow", (event) => {
+ let navType = window.performance.navigation.type;
if(navType === 2){
- window.location.reload()
+ window.location.reload();
}
});
},
+ clickDownloadPNGButton: function (e) {
+ $('div[data-hook=preview-plot-container] a[data-title*="Download plot as a png"]')[0].click();
+ },
+ clickReturnToProjectHandler: function () {
+ this.saveModel((e) => {
+ let queryStr = `?path=${this.projectPath}`;
+ let endpoint = path.join(app.getBasePath(), "stochss/project/manager") + queryStr;
+ window.location.href = endpoint;
+ });
+ },
+ clickSaveHandler: function (e) {
+ this.saveModel(() => { this.endAction("save"); });
+ },
+ closeDomainPlot: function () {
+ $(this.queryByHook("domain-plot-viewer-container")).css("display", "none");
+ $(this.queryByHook("toggle-preview-domain")).html("Show Domain");
+ },
+ closePlot: function () {
+ $(this.queryByHook("model-run-container")).css("display", "none");
+ $(this.queryByHook("toggle-preview-plot")).html("Show Preview");
+ },
+ displayError: function (errorMsg, e) {
+ $(this.queryByHook('toggle-preview-plot')).click();
+ errorMsg.css('display', 'block');
+ this.focusOnError(e);
+ },
+ endAction: function (action) {
+ if(action === "save") {
+ $(this.queryByHook("saving")).css("display", "none");
+ var msg = $(this.queryByHook("saved"));
+ }else{
+ $(this.queryByHook("publishing")).css("display", "none");
+ var msg = $(this.queryByHook("published"));
+ }
+ msg.css("display", "inline-block");
+ $(this.queryByHook('mdl-action-start')).css("display", "none");
+ let saved = $(this.queryByHook('mdl-action-end'));
+ saved.css("display", "inline-block");
+ setTimeout(() => {
+ saved.css("display", "none");
+ msg.css("display", "none");
+ }, 5000);
+ },
+ errorAction: function () {
+ $(this.queryByHook("publishing")).css("display", "none");
+ $(this.queryByHook('mdl-action-start')).css("display", "none");
+ let error = $(this.queryByHook('mdl-action-err'));
+ error.css("display", "inline-block");
+ setTimeout(() => {
+ error.css("display", "none");
+ }, 5000);
+ },
+ focusOnError: function (e) {
+ let mdlSections = ["species", "parameter", "reaction", "process", "event", "rule", "volume", "domain"];
+ if(this.model.error) {
+ if(this.model.error.type === "timespan") {
+ this.openTimespanSection();
+ }else if(mdlSections.includes(this.model.error.type)) {
+ this.modelView.openSection();
+ }
+ setTimeout(() => {
+ let inputErrors = this.queryAll(".input-invalid");
+ let componentErrors = this.queryAll(".component-invalid");
+ if(componentErrors.length > 0) {
+ componentErrors[0].scrollIntoView({'block':"center"});
+ }else if(inputErrors.length > 0) {
+ inputErrors[0].focus();
+ }
+ }, 300);
+ }
+ },
getFileName: function (file) {
if(file.endsWith('/')) {
- file.slice(0, -1)
+ file.slice(0, -1);
}
if(file.includes('/')) {
- file = file.split('/').pop()
+ file = file.split('/').pop();
}
if(!file.includes('.')) {
- return file
+ return file;
}
- return file.split('.').slice(0, -1).join('.')
+ return file.split('.').slice(0, -1).join('.');
},
- handleProjectBreadcrumbClick: function () {
- this.modelStateButtons.saveModel(_.bind(function (e) {
- let endpoint = path.join(app.getBasePath(), "stochss/project/manager")+"?path="+this.projectPath
- window.location.href = endpoint
- }, this))
+ getPreviewTarget: function () {
+ this.endAction("save");
+ let species = this.model.species.map((species) => { return species.name; });
+ let modal = $(modals.selectPreviewTargetHTML(species)).modal();
+ let okBtn = document.querySelector("#previewTargetSelectModal .ok-model-btn");
+ let select = document.querySelector("#previewTargetSelectModal #previewTargetSelectList");
+ okBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ this.runModel({target: select.value});
+ });
},
- renderParticleViewer: function (particle=null) {
- if(this.particleViewer) {
- this.particleViewer.remove();
+ getResults: function () {
+ setTimeout(() => {
+ let queryStr = `?cmd=read&outfile=${this.outfile}&path=${this.model.directory}`;
+ let endpoint = path.join(app.getApiPath(), 'model/run') + queryStr;
+ let errorCB = (err, response, body) => {
+ this.ran(false);
+ $(this.queryByHook('model-run-error-message')).text(body.Results.errors);
+ }
+ app.getXHR(endpoint, {
+ always: (err, response, body) => {
+ if(typeof body === "string") {
+ body = body.replace(/NaN/g, null);
+ body = JSON.parse(body);
+ }
+ if(response.statusCode >= 400 || body.Results.errors){
+ errorCB(err, response, body);
+ }
+ else if(!body.Running){
+ if(body.Results.timeout){
+ $(this.queryByHook('model-timeout-message')).collapse('show');
+ }
+ this.plotResults(body.Results.results);
+ }else{
+ this.getResults();
+ }
+ },
+ error: errorCB
+ });
+ }, 2000);
+ },
+ handlePresentationClick: function (e) {
+ let errorMsg = $(this.queryByHook("error-detected-msg"));
+ if(!this.model.valid) {
+ this.displayError(errorMsg, e);
+ }else{
+ this.startAction("publish");
+ let queryStr = `?path=${this.model.directory}`;
+ let endpoint = path.join(app.getApiPath(), "model/presentation") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ this.endAction("publish");
+ $(modals.presentationLinks(body.message, "Shareable Presentation", body.links)).modal();
+ let copyBtn = document.querySelector('#presentationLinksModal #copy-to-clipboard');
+ copyBtn.addEventListener('click', (e) => {
+ let onFulfilled = (value) => {
+ $("#copy-link-success").css("display", "inline-block");
+ }
+ let onReject = (reason) => {
+ let msg = $("#copy-link-failed");
+ msg.html(reason);
+ msg.css("display", "inline-block");
+ }
+ app.copyToClipboard(body.links.presentation, onFulfilled, onReject);
+ });
+ },
+ error: (err, response, body) => {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
+ this.errorAction();
+ $(modals.errorHtml(body.Reason, body.Message)).modal();
+ }
+ });
}
- if(this.typeQuickViewer) {
- this.typeQuickViewer.remove();
+ },
+ handleSimulateClick: function (e) {
+ let errorMsg = $(this.queryByHook("error-detected-msg"));
+ if(!this.model.valid) {
+ this.displayError(errorMsg, e);
+ }else{
+ errorMsg.css('display', 'none');
+ if(e.target.dataset.type === "preview") {
+ this.runPreview();
+ }else if(e.target.dataset.type === "notebook"){
+ this.notebookWorkflow(e);
+ }else if(!this.model.is_spatial) {
+ if(e.target.dataset.type === "ensemble") {
+ app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Ensemble Simulation");
+ }else if(e.target.dataset.type === "psweep") {
+ app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Parameter Sweep");
+ }
+ }
}
- if(particle){
- $(this.queryByHook("me-select-particle")).css("display", "none")
- this.particleViewer = new ParticleViewer({
- model: particle
- });
- app.registerRenderSubview(this, this.particleViewer, "me-particle-viewer")
+ },
+ notebookWorkflow: function () {
+ this.saveModel(() => {
+ this.endAction("save");
+ var queryString = `?path=${this.model.directory}`;
+ if(this.model.directory.includes('.proj')) {
+ let wkgp = this.model.directory.includes('.wkgp') ? `${this.model.name}.wkgp` : "WorkflowGroup1.wkgp";
+ let parentPath = path.join(path.dirname(this.model.directory), wkgp);
+ queryString += `&parentPath=${parentPath}`;
+ }
+ window.location.href = path.join(app.getBasePath(), "stochss/workflow/selection") + queryString;
+ });
+ },
+ openDomainPlot: function () {
+ if($(this.queryByHook("model-run-container")).css("display") !== "none") {
+ this.closePlot();
+ }
+ $(this.queryByHook("domain-plot-viewer-container")).css("display", "block");
+ $(this.queryByHook("toggle-preview-domain")).html("Hide Domain");
+ },
+ openPlot: function () {
+ if($(this.queryByHook("domain-plot-viewer-container")).css("display") !== "none") {
+ this.closeDomainPlot();
+ }
+ $(this.queryByHook("model-run-container")).css("display", "block");
+ $(this.queryByHook("toggle-preview-plot")).html("Hide Preview");
+ },
+ openTimespanSection: function () {
+ if(!$(this.modelSettings.queryByHook("timespan-container")).hasClass("show")) {
+ let tspnCollapseBtn = $(this.modelSettings.queryByHook("collapse"));
+ tspnCollapseBtn.click();
+ tspnCollapseBtn.html('-');
+ }
+ this.switchToEditTab(this.modelSettings, "timespan");
+ },
+ plotResults: function (data) {
+ this.ran(true);
+ Plotly.newPlot(this.queryByHook('preview-plot-container'), data);
+ window.scrollTo(0, document.body.scrollHeight);
+ },
+ ran: function (noErrors) {
+ let runContainer = $(this.queryByHook("model-run-container"));
+ if(runContainer.css("display") === "none") {
+ runContainer.css("display", 'block');
+ }
+ $(this.queryByHook('preview-plot-buttons')).css('display', 'inline-block');
+ let plotBtn = $(this.queryByHook('toggle-preview-plot'));
+ if(plotBtn.text() === "Show Preview") {
+ plotBtn.text("Hide Preview");
+ }
+ if(noErrors){
+ $(this.queryByHook('preview-plot-container')).css("display", "block");
}else{
- $(this.queryByHook("me-select-particle")).css("display", "block")
- this.typeQuickViewer = this.renderCollection(
- this.modelView.domainViewer.model.types,
- QuickviewDomainTypes,
- this.queryByHook("me-types-quick-view")
- );
+ $(this.queryByHook('model-run-error-container')).css("display", "block");
}
+ $(this.queryByHook('plot-loader')).css("display", "none");
},
renderModelView: function () {
+ let domainElements = {
+ select: $(this.queryByHook("me-select-particle")),
+ particle: {view: this, hook: "me-particle-viewer"},
+ plot: this.queryByHook("domain-plot-container"),
+ type: this.queryByHook("me-types-quick-view")
+ }
this.modelView = new ModelView({
- model: this.model
+ model: this.model,
+ domainElements: domainElements
});
- app.registerRenderSubview(this, this.modelView, "model-view-container")
+ app.registerRenderSubview(this, this.modelView, "model-view-container");
},
renderSubviews: function (validate) {
- this.renderModelView()
- this.modelSettings = new TimespanSettingsView({
- parent: this,
- model: this.model.modelSettings,
- });
- this.modelStateButtons = new ModelStateButtonsView({
- model: this.model,
- validate: validate
- });
- app.registerRenderSubview(this, this.modelSettings, 'model-settings-container');
- app.registerRenderSubview(this, this.modelStateButtons, 'model-state-buttons-container');
+ if(this.model.directory.includes('.proj')) {
+ $(this.queryByHook("project-breadcrumb-links")).css("display", "block");
+ $(this.queryByHook("model-name-header")).css("display", "none");
+ $(this.queryByHook("return-to-project-btn")).css("display", "inline-block");
+ }
if(this.model.is_spatial) {
$(this.queryByHook("spatial-beta-message")).css("display", "block");
$(this.queryByHook("toggle-preview-domain")).css("display", "inline-block");
this.openDomainPlot();
+ $(this.queryByHook("stochss-es")).addClass("disabled");
+ $(this.queryByHook("stochss-ps")).addClass("disabled");
+ }
+ if(app.getBasePath() === "/") {
+ $(this.queryByHook("presentation")).css("display", "none");
}
- this.model.autoSave();
- $(function () {
- $('[data-toggle="tooltip"]').tooltip();
- $('[data-toggle="tooltip"]').click(function () {
- $('[data-toggle="tooltip"]').tooltip("hide");
- });
+ this.renderModelView();
+ this.modelSettings = new TimespanSettingsView({
+ parent: this,
+ model: this.model.modelSettings
});
- $(document).on('hide.bs.modal', '.modal', function (e) {
- e.target.remove()
+ app.registerRenderSubview(this, this.modelSettings, 'model-settings-container');
+ if(validate && !this.model.valid) {
+ let errorMsg = $(this.queryByHook("error-detected-msg"));
+ this.displayError(errorMsg);
+ }
+ app.documentSetup();
+ },
+ runModel: function ({target=null}={}) {
+ this.running();
+ let queryStr = `?cmd=start&outfile=none&path=${this.model.directory}`;
+ if(target !== null) {
+ queryStr += `&target=${target}`;
+ }else {
+ this.endAction("save");
+ }
+ $(this.queryByHook('model-run-container')).css("display", "block");
+ let endpoint = path.join(app.getApiPath(), 'model/run') + queryStr;
+ app.getXHR(endpoint, {
+ always: (err, response, body) => {
+ this.outfile = body.Outfile;
+ this.getResults();
+ }
});
},
- changeCollapseButtonText: function (e) {
- app.changeCollapseButtonText(this, e);
+ running: function () {
+ $(this.queryByHook('preview-plot-container')).css("display", "none");
+ $(this.queryByHook('plot-loader')).css("display", "block");
+ $(this.queryByHook('model-run-error-container')).css("display", "none");
},
- togglePreviewPlot: function (e) {
- let action = e.target.innerText
- if(action === "Hide Preview") {
- this.closePlot()
+ runPreview: function () {
+ if(this.model.is_spatial && $(this.queryByHook("domain-plot-viewer-container")).css("display") !== "none") {
+ this.closeDomainPlot();
+ }
+ $(this.queryByHook('model-run-error-container')).collapse('hide');
+ $(this.queryByHook('model-timeout-message')).collapse('hide');
+ Plotly.purge(this.queryByHook('preview-plot-container'));
+ $(this.queryByHook('preview-plot-buttons')).css("display", "none");
+ if(this.model.is_spatial) {
+ this.saveModel(() => { this.getPreviewTarget(); });
}else{
- this.openPlot()
+ this.saveModel(() => { this.runModel(); });
}
},
- closePlot: function () {
- let runContainer = this.queryByHook("model-run-container")
- let button = this.queryByHook("toggle-preview-plot")
- runContainer.style.display = "none"
- button.innerText = "Show Preview"
+ saveModel: function (cb) {
+ this.startAction("save");
+ if(cb) {
+ this.model.saveModel(cb);
+ }else {
+ this.model.saveModel();
+ }
},
- openPlot: function () {
- if($(this.queryByHook("domain-plot-viewer-container")).css("display") !== "none") {
- this.closeDomainPlot()
+ startAction: function (action) {
+ if(action === "save") {
+ $(this.queryByHook("saving")).css("display", "inline-block");
+ }else{
+ $(this.queryByHook("publishing")).css("display", "inline-block");
}
- let runContainer = this.queryByHook("model-run-container")
- let button = this.queryByHook("toggle-preview-plot")
- runContainer.style.display = "block"
- button.innerText = "Hide Preview"
+ $(this.queryByHook('mdl-action-start')).css("display", "inline-block");
+ $(this.queryByHook('mdl-action-end')).css("display", "none");
},
toggleDomainPlot: function (e) {
- let action = e.target.innerText
- if(action === "Hide Domain") {
+ if(e.target.innerText === "Hide Domain") {
this.closeDomainPlot();
}else{
this.openDomainPlot();
}
},
- openDomainPlot: function () {
- if($(this.queryByHook("model-run-container")).css("display") !== "none") {
+ togglePreviewPlot: function (e) {
+ if(e.target.innerText === "Hide Preview") {
this.closePlot();
+ }else{
+ this.openPlot();
}
- let domainView = this.queryByHook("domain-plot-viewer-container")
- let button = this.queryByHook("toggle-preview-domain")
- domainView.style.display = "block"
- button.innerText = "Hide Domain"
- },
- closeDomainPlot: function () {
- let domainView = this.queryByHook("domain-plot-viewer-container")
- let button = this.queryByHook("toggle-preview-domain")
- domainView.style.display = "none"
- button.innerText = "Show Domain"
- },
- clickDownloadPNGButton: function (e) {
- let pngButton = $('div[data-hook=preview-plot-container] a[data-title*="Download plot as a png"]')[0]
- pngButton.click()
}
});
diff --git a/client/pages/model-presentation.js b/client/pages/model-presentation.js
index ebedef0f60..d7e77d9adf 100644
--- a/client/pages/model-presentation.js
+++ b/client/pages/model-presentation.js
@@ -48,21 +48,20 @@ let ModelPresentationPage = PageView.extend({
directory: file,
for: "presentation"
});
- let self = this;
- let queryStr = "?file=" + this.model.directory + "&owner=" + owner;
- let endpoint = "api/file/json-data" + queryStr;
+ let endpoint = `api/file/json-data?file=${this.model.directory}&owner=${owner}`;
app.getXHR(endpoint, {
- success: function (err, response, body) {
- self.model.set(body);
- self.renderSubviews(false);
+ success: (err, response, body) => {
+ this.model.set(body.model);
+ let domainPlot = Boolean(body.domainPlot) ? body.domainPlot : null;
+ this.renderSubviews(false, domainPlot);
},
- error: function (err, response, body) {
- self.notFound = true;
- self.renderSubviews(true);
+ error: (err, response, body) => {
+ this.notFound = true;
+ this.renderSubviews(true);
}
});
let downloadStart = "https://live.stochss.org/stochss/download_presentation";
- this.downloadLink = downloadStart + "/" + owner + "/" + file;
+ this.downloadLink = path.join(downloadStart, owner, file);
this.openLink = "https://open.stochss.org?open=" + this.downloadLink;
},
render: function (attrs, options) {
@@ -73,17 +72,18 @@ let ModelPresentationPage = PageView.extend({
let message = `This ${this.fileType} can be downloaded or opened in your own StochSS Live! account using the buttons at the bottom of the page.`;
$(this.queryByHook("loading-message")).html(message);
},
- renderSubviews: function (notFound) {
- this.template = notFound ? errorTemplate : template
+ renderSubviews: function (notFound, domainPlot) {
+ this.template = notFound ? errorTemplate : template;
PageView.prototype.render.apply(this, arguments);
if(!notFound) {
- this.renderModelView();
+ this.renderModelView(domainPlot);
}
},
- renderModelView: function () {
+ renderModelView: function (domainPlot) {
let modelView = new ModelView({
model: this.model,
- readOnly: true
+ readOnly: true,
+ domainPlot: domainPlot
});
app.registerRenderSubview(this, modelView, "model-view");
}
diff --git a/client/pages/multiple-plots.js b/client/pages/multiple-plots.js
index 108f9b60ce..84ea3cbd4a 100644
--- a/client/pages/multiple-plots.js
+++ b/client/pages/multiple-plots.js
@@ -24,11 +24,11 @@ let Plotly = require('../lib/plotly');
var PageView = require('../pages/base');
//templates
let template = require("../templates/pages/multiplePlots.pug");
+let presTemplate = require("../templates/pages/multiplePlotsPresentation.pug");
import initPage from './page.js';
let ModelEditor = PageView.extend({
- template: template,
initialize: function (attrs, options) {
PageView.prototype.initialize.apply(this, arguments);
let urlParams = new URLSearchParams(window.location.search);
@@ -36,6 +36,7 @@ let ModelEditor = PageView.extend({
this.job = urlParams.get("job");
this.path = urlParams.get("path");
this.data = urlParams.get("data");
+ this.template = this.path.includes("presentation_cache") ? presTemplate : template;
},
render: function (attrs, options) {
PageView.prototype.render.apply(this, arguments);
diff --git a/client/pages/project-browser.js b/client/pages/project-browser.js
deleted file mode 100644
index 6286c611a8..0000000000
--- a/client/pages/project-browser.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
-StochSS is a platform for simulating biochemical systems
-Copyright (C) 2019-2021 StochSS developers.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-let $ = require('jquery');
-let path = require('path');
-//support files
-let app = require('../app');
-let modals = require('../modals');
-//models
-let Project = require('../models/project');
-//collections
-let Collection = require('ampersand-collection');
-//views
-let PageView = require('./base');
-let EditProjectView = require('../views/edit-project');
-//templates
-let template = require('../templates/pages/projectBrowser.pug');
-
-import initPage from './page.js';
-
-let projectBrowser = PageView.extend({
- template: template,
- events: {
- 'click [data-hook=new-project-btn]' : 'handleNewProjectClick'
- },
- initialize: function (attrs, options) {
- PageView.prototype.initialize.apply(this, arguments);
- let self = this;
- let endpoint = path.join(app.getApiPath(), "project/load-browser");
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- self.projects = body.projects;
- self.renderProjectsView();
- }
- });
- },
- render: function (attrs, options) {
- PageView.prototype.render.apply(this, arguments)
- $(document).on('hide.bs.modal', '.modal', function (e) {
- e.target.remove()
- });
- },
- handleNewProjectClick: function (e) {
- let self = this;
- if(document.querySelector("#newProjectModal")) {
- document.querySelector("#newProjectModal").remove();
- }
- let modal = $(modals.newProjectModalHtml()).modal();
- let input = document.querySelector("#newProjectModal #projectNameInput");
- input.focus();
- let okBtn = document.querySelector("#newProjectModal .ok-model-btn");
- input.addEventListener("keyup", function (event) {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newProjectModal #projectNameInputEndCharError');
- var charErrMsg = document.querySelector('#newProjectModal #projectNameInputSpecCharError');
- let error = self.validateName(input.value);
- okBtn.disabled = error !== "";
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none";
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none";
- });
- okBtn.addEventListener("click", function (e) {
- if(Boolean(input.value)) {
- modal.modal('hide');
- let projectPath = input.value.trim() + ".proj";
- let endpoint = path.join(app.getApiPath(), "project/new-project") + "?path=" + projectPath;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- window.location.href = path.join(app.getBasePath(), "stochss/project/manager")+"?path="+body.path;
- },
- error: function (err, response, body) {
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- }
- });
- },
- renderProjectsView: function () {
- if(this.projectsView) {
- this.projectsView.remove()
- }
- let projects = new Collection(this.projects, {model: Project, comparator: 'parentDir'})
- this.projectsView = this.renderCollection(projects, EditProjectView, this.queryByHook("projects-view-container"))
- },
- validateName(input) {
- var error = "";
- if(input.endsWith('/')) {
- error = 'forward';
- }
- let invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\";
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both";
- }
- }
- return error;
- }
-});
-
-initPage(projectBrowser);
diff --git a/client/pages/project-manager.js b/client/pages/project-manager.js
index 0ac554d7ce..ccae2200b1 100644
--- a/client/pages/project-manager.js
+++ b/client/pages/project-manager.js
@@ -28,9 +28,9 @@ let Model = require('../models/model');
let Project = require('../models/project');
//views
let PageView = require('./base');
+let JSTreeView = require('../views/jstree-view');
let MetaDataView = require('../views/meta-data');
let ModelListing = require('../views/model-listing');
-let FileBrowser = require('../views/file-browser-view');
let ArchiveListing = require('../views/archive-listing');
let WorkflowListing = require('../views/workflow-listing');
let WorkflowGroupListing = require('../views/workflow-group-listing');
@@ -47,12 +47,13 @@ let ProjectManager = PageView.extend({
'click [data-hook=collapse-annotation-text]' : 'changeCollapseButtonText',
'click [data-hook=new-model]' : 'handleNewModelClick',
'click [data-hook=existing-model]' : 'handleExistingModelClick',
- 'click [data-hook=upload-file-btn]' : 'handleUploadModelClick',
+ 'click [data-hook=upload-model-btn]' : 'handleUploadModelClick',
'click [data-hook=new-ensemble-simulation]' : 'handleNewWorkflowClick',
'click [data-hook=new-parameter-sweep]' : 'handleNewWorkflowClick',
'click [data-hook=new-jupyter-notebook]' : 'handleNewWorkflowClick',
'click [data-hook=project-manager-advanced-btn]' : 'changeCollapseButtonText',
'click [data-hook=archive-btn]' : 'changeCollapseButtonText',
+ 'click [data-hook=collapse-browse-files]' : 'changeCollapseButtonText',
'click [data-hook=export-project-as-zip]' : 'handleExportZipClick',
'click [data-hook=export-project-as-combine]' : 'handleExportCombineClick',
'click [data-hook=empty-project-trash]' : 'handleEmptyTrashClick'
@@ -82,17 +83,17 @@ let ProjectManager = PageView.extend({
});
},
addExistingModel: function () {
- if(document.querySelector('#newProjectModelModal')){
- document.querySelector('#newProjectModelModal').remove();
+ if(document.querySelector('#importModelModal')){
+ document.querySelector('#importModelModal').remove();
}
let self = this
let mdlListEP = path.join(app.getApiPath(), 'project/add-existing-model') + "?path=" + self.model.directory;
app.getXHR(mdlListEP, {
always: function (err, response, body) {
- let modal = $(modals.newProjectModelHtml(body.files)).modal();
- let okBtn = document.querySelector('#newProjectModelModal .ok-model-btn');
- let select = document.querySelector('#newProjectModelModal #modelFileInput');
- let location = document.querySelector('#newProjectModelModal #modelPathInput');
+ let modal = $(modals.importModelHtml(body.files)).modal();
+ let okBtn = document.querySelector('#importModelModal .ok-model-btn');
+ let select = document.querySelector('#importModelModal #modelFileInput');
+ let location = document.querySelector('#importModelModal #modelPathInput');
select.addEventListener("change", function (e) {
okBtn.disabled = e.target.value && body.paths[e.target.value].length >= 2;
if(body.paths[e.target.value].length >= 2) {
@@ -118,10 +119,16 @@ let ProjectManager = PageView.extend({
let endpoint = path.join(app.getApiPath(), 'project/add-existing-model') + queryString
app.postXHR(endpoint, null, {
success: function (err, response, body) {
- let successModal = $(modals.newProjectModelSuccessHtml(body.message)).modal();
+ if(document.querySelector("#successModal")) {
+ document.querySelector("#successModal").remove();
+ }
+ let successModal = $(modals.successHtml(body.message)).modal();
},
error: function (err, response, body) {
- let errorModal = $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal();
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
+ let errorModal = $(modals.errorHtml(body.Reason, body.Message)).modal();
}
});
self.update("Model");
@@ -140,9 +147,12 @@ let ProjectManager = PageView.extend({
window.location.href = endpoint;
},
error: function (err, response, body) {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
let title = "Model Already Exists";
let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
+ let errorModel = $(modals.errorHtml(title, message)).modal();
}
});
}else{
@@ -153,9 +163,12 @@ let ProjectManager = PageView.extend({
app.getXHR(existEP, {
always: function (err, response, body) {
if(body.exists) {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
let title = "Model Already Exists";
let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
+ let errorModel = $(modals.errorHtml(title, message)).modal();
}else{
window.location.href = endpoint;
}
@@ -164,13 +177,13 @@ let ProjectManager = PageView.extend({
}
},
addNewModel: function (isSpatial) {
- if(document.querySelector('#newModalModel')) {
- document.querySelector('#newModalModel').remove();
+ if(document.querySelector('#newModelModal')) {
+ document.querySelector('#newModelModal').remove();
}
let self = this;
- let modal = $(modals.renderCreateModalHtml(true, isSpatial)).modal();
- let okBtn = document.querySelector('#newModalModel .ok-model-btn');
- let input = document.querySelector('#newModalModel #modelNameInput');
+ let modal = $(modals.createModelHtml(isSpatial)).modal();
+ let okBtn = document.querySelector('#newModelModal .ok-model-btn');
+ let input = document.querySelector('#newModelModal #modelNameInput');
okBtn.addEventListener('click', function (e) {
modal.modal('hide');
if (Boolean(input.value)) {
@@ -191,9 +204,9 @@ let ProjectManager = PageView.extend({
}
});
input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newModalModel #modelNameInputEndCharError')
- var charErrMsg = document.querySelector('#newModalModel #modelNameInputSpecCharError')
- let error = self.validateName(input.value)
+ var endErrMsg = document.querySelector('#newModelModal #modelNameInputEndCharError')
+ var charErrMsg = document.querySelector('#newModelModal #modelNameInputSpecCharError')
+ let error = app.validateName(input.value, {saveAs: false})
okBtn.disabled = error !== "" || input.value.trim() === ""
charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
@@ -288,18 +301,20 @@ let ProjectManager = PageView.extend({
}
});
}else{
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").error();
+ }
let title = "No Models Found for " + type + " Workflows";
if(this.models.length > 0) {
var message = "Jupyter Notebook workflows are the only workflows available for spatial models.";
}else{
var message = "You need to add a model before you can create a new workflow.";
}
- let modal = $(modals.noModelsMessageHtml(title, message)).modal();
+ let modal = $(modals.errorHtml(title, message)).modal();
}
},
handleUploadModelClick: function (e) {
- let type = e.target.dataset.type
- this.projectFileBrowser.uploadFile(undefined, type)
+ this.projectFileBrowser.uploadFile(undefined, "model")
},
renderArchiveCollection: function () {
if(this.archiveCollectionView) {
@@ -335,8 +350,9 @@ let ProjectManager = PageView.extend({
this.projectFileBrowser.remove();
}
let self = this;
- this.projectFileBrowser = new FileBrowser({
- root: self.model.directory
+ this.projectFileBrowser = new JSTreeView({
+ root: self.model.directory,
+ configKey: "project"
});
app.registerRenderSubview(this, this.projectFileBrowser, "file-browser");
},
@@ -401,8 +417,10 @@ let ProjectManager = PageView.extend({
this.queryByHook("model-listing")
);
},
- update: function (target) {
- this.projectFileBrowser.refreshJSTree();
+ update: function (target, from) {
+ if(from !== "file-browser") {
+ this.projectFileBrowser.refreshJSTree(null);
+ }
let fetchTypes = ["Model", "Workflow", "WorkflowGroup", "Archive"];
if(fetchTypes.includes(target)) {
let self = this;
@@ -439,19 +457,6 @@ let ProjectManager = PageView.extend({
}
});
},
- validateName(input) {
- var error = "";
- if(input.endsWith('/')) {
- error = 'forward';
- }
- let invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\";
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both";
- }
- }
- return error;
- }
});
initPage(ProjectManager)
\ No newline at end of file
diff --git a/client/pages/users-home.js b/client/pages/users-home.js
index 2eb2e8ee93..88b140e67c 100644
--- a/client/pages/users-home.js
+++ b/client/pages/users-home.js
@@ -21,8 +21,13 @@ let path = require('path');
//support files
let app = require('../app');
let modals = require('../modals');
+//collections
+let Collection = require('ampersand-collection');
+//model
+let Presentation = require('../models/presentation')
//views
let PageView = require('./base');
+let PresentationView = require('../views/presentation-view');
//templates
let template = require('../templates/pages/usersHome.pug');
@@ -31,10 +36,8 @@ import initPage from './page.js';
let usersHomePage = PageView.extend({
template: template,
events: {
- 'click [data-hook=new-model-btn]' : 'handleNewModelClick',
'click [data-hook=new-project-btn]' : 'handleNewProjectClick',
'click [data-hook=browse-projects-btn]' : 'handleBrowseProjectsClick',
- 'click [data-hook=browse-files-btn]' : 'handleBrowseFilesClick',
'click [data-hook=quickstart-btn]' : 'handleQuickstartClick'
},
initialize: function (attrs, options) {
@@ -48,73 +51,14 @@ let usersHomePage = PageView.extend({
},
render: function (attrs, options) {
PageView.prototype.render.apply(this, arguments);
- $(document).on('hide.bs.modal', '.modal', function (e) {
- e.target.remove()
- });
- },
- validateName(input) {
- var error = ""
- if(input.endsWith('/')) {
- error = 'forward'
- }
- let invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both"
- }
- }
- return error
- },
- handleNewModelClick: function (e) {
- let self = this
- if(document.querySelector("#newModalModel")) {
- document.querySelector("#newModalModel").remove()
- }
- let modal = $(modals.renderCreateModalHtml(true, false)).modal()
- let okBtn = document.querySelector("#newModalModel .ok-model-btn")
- let input = document.querySelector("#newModalModel #modelNameInput")
- input.focus()
- input.addEventListener("keyup", function (event) {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newModalModel #modelNameInputEndCharError')
- var charErrMsg = document.querySelector('#newModalModel #modelNameInputSpecCharError')
- let error = self.validateName(input.value)
- okBtn.disabled = error !== "" || input.value.trim() === ""
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
- });
- okBtn.addEventListener("click", function (e) {
- if(Boolean(input.value)){
- modal.modal('hide')
- let modelPath = input.value + '.mdl'
- let queryString = "?path="+modelPath
- let existEP = path.join(app.getApiPath(), "model/exists")+queryString
- app.getXHR(existEP, {
- always: function (err, response, body) {
- if(body.exists) {
- let title = "Model Already Exists";
- let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
- }else{
- let endpoint = path.join(app.getBasePath(), "stochss/models/edit")+queryString;
- self.navToPage(endpoint);
- }
- }
- });
- }
- });
+ app.documentSetup();
},
handleNewProjectClick: function (e) {
let self = this
if(document.querySelector("#newProjectModal")) {
document.querySelector("#newProjectModal").remove()
}
- let modal = $(modals.newProjectModalHtml()).modal()
+ let modal = $(modals.createProjectHtml()).modal()
let okBtn = document.querySelector("#newProjectModal .ok-model-btn")
let input = document.querySelector("#newProjectModal #projectNameInput")
input.focus()
@@ -127,7 +71,7 @@ let usersHomePage = PageView.extend({
input.addEventListener("input", function (e) {
var endErrMsg = document.querySelector('#newProjectModal #projectNameInputEndCharError')
var charErrMsg = document.querySelector('#newProjectModal #projectNameInputSpecCharError')
- let error = self.validateName(input.value)
+ let error = app.validateName(input.value)
okBtn.disabled = error !== "" || input.value.trim() === ""
charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
@@ -145,18 +89,17 @@ let usersHomePage = PageView.extend({
self.navToPage(projectEP);
},
error: function (err, response, body) {
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(body.Reason, body.Message)).modal();
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
+ let errorModel = $(modals.errorHtml(body.Reason, body.Message)).modal();
}
});
}
});
},
handleBrowseProjectsClick: function (e) {
- let endpoint = path.join(app.getBasePath(), "stochss/project/browser")
- this.navToPage(endpoint)
- },
- handleBrowseFilesClick: function (e) {
- let endpoint = path.join(app.getBasePath(), "stochss/files")
+ let endpoint = path.join(app.getBasePath(), "stochss/files#project-browser-section")
this.navToPage(endpoint)
},
handleQuickstartClick: function (e) {
diff --git a/client/project-config.js b/client/project-config.js
new file mode 100644
index 0000000000..edf1cef1b0
--- /dev/null
+++ b/client/project-config.js
@@ -0,0 +1,461 @@
+/*
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2021 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+let $ = require('jquery');
+let path = require('path');
+//support files
+let app = require('./app');
+let modals = require('./modals');
+
+let contextZipTypes = ["workflow", "folder", "other", "root", "workflowGroup"];
+
+let doubleClick = (view, e) => {
+ let node = $('#files-jstree').jstree().get_node(e.target);
+ if(!node.original._path.includes(".proj/trash/")){
+ if((node.type === "folder" || node.type === "workflowGroup") && $('#files-jstree').jstree().is_open(node) && $('#files-jstree').jstree().is_loaded(node)){
+ view.refreshJSTree(node);
+ }else if(node.type === "nonspatial" || node.type === "spatial"){
+ view.openModel(node.original._path);
+ }else if(node.type === "notebook"){
+ view.openNotebook(node.original._path);
+ }else if(node.type === "sbmlModel"){
+ view.openSBML(node.original._path);
+ }else if(node.type === "workflow"){
+ view.openWorkflow(node.original._path);
+ }else if(node.type === "domain") {
+ view.openDomain(node.original._path);
+ }else if(node.type === "other"){
+ if(node.text.endsWith(".zip")) {
+ view.extractAll(node);
+ }else{
+ view.openFile(node.original._path);
+ }
+ }
+ }
+}
+
+let extract = (view, node, type) => {
+ let projectPar = path.dirname(view.root) === '.' ? "" : path.dirname(view.root);
+ let dstPath = path.join(projectPar, node.original._path.split('/').pop());
+ let queryStr = `?srcPath=${node.original._path}&dstPath=${dstPath}`;
+ let endpoint = path.join(app.getApiPath(), `project/extract-${type}`) + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ let title = `Successfully Exported the ${type.replace(type.charAt(0), type.charAt(0).toUpperCase())}`;
+ let successModel = $(modals.successHtml(body, {title: title})).modal();
+ },
+ error: (err, response, body) => {
+ view.reportError(JSON.parse(body));
+ }
+ });
+}
+
+let getDomainContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "directory")};
+ }
+ let downloadOptions = {dataType: "json", identifier: "spatial-model/load-domain"};
+ return {
+ open: view.buildContextBaseWithClass({
+ label: "Open",
+ action: (data) => {
+ view.openDomain(node.original._path);
+ }
+ }),
+ download: view.getDownloadContext(node, downloadOptions),
+ rename: view.getRenameContext(node),
+ duplicate: view.getDuplicateContext(node, "file/duplicate"),
+ moveToTrash: view.getMoveToTrashContext(node, "domain")
+ }
+}
+
+let getExtractContext = (view, node, type) => {
+ return view.buildContextBase({
+ label: "Extract",
+ action: (data) => {
+ extract(view, node, type);
+ }
+ });
+}
+
+let getFolderContext = (view, node) => {
+ if(node.text === "trash"){ // Trash node
+ return {refresh: view.getRefreshContext(node)};
+ }
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "directory")};
+ }
+ let downloadOptions = {dataType: "zip", identifier: "file/download-zip"};
+ let options = {asZip: true};
+ return {
+ refresh: view.getRefreshContext(node),
+ newDirectory: view.getNewDirectoryContext(node),
+ newDomain: view.getNewDomainContext(node),
+ upload: view.getFileUploadContext(node, true, {label: "Uplaod File"}),
+ download: view.getDownloadContext(node, downloadOptions, options),
+ rename: view.getRenameContext(node),
+ duplicate: view.getDuplicateContext(node, "directory/duplicate"),
+ moveToTrash: view.getMoveToTrashContext(node, "directory")
+ }
+}
+
+let getModelContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "model")};
+ }
+ let downloadOptions = {dataType: "json", identifier: "file/json-data"};
+ return {
+ edit: view.getEditModelContext(node),
+ extract: getExtractContext(view, node, "model"),
+ newWorkflow: view.getFullNewWorkflowContext(node),
+ convert: view.getMdlConvertContext(node),
+ download: view.getDownloadContext(node, downloadOptions),
+ rename: view.getRenameContext(node),
+ duplicate: view.getDuplicateContext(node, "file/duplicate"),
+ moveToTrash: view.getMoveToTrashContext(node, "model")
+ }
+}
+
+let getNotebookContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "directory")};
+ }
+ let open = view.getOpenNotebookContext(node);
+ let downloadOptions = {dataType: "json", identifier: "file/json-data"};
+ let download = view.getDownloadContext(node, downloadOptions);
+ let rename = view.getRenameContext(node);
+ let duplicate = view.getDuplicateContext(node, "file/duplicate");
+ let moveToTrash = view.getMoveToTrashContext(node, "notebook");
+ if(app.getBasePath() === "/") {
+ return {
+ open: open, download: download, rename: rename,
+ duplicate: duplicate, moveToTrash: moveToTrash
+ }
+ }
+ return {
+ open: open,
+ publish: view.getPublishNotebookContext(node),
+ download: download, rename: rename,
+ duplicate: duplicate, delete: deleteFile
+ }
+}
+
+let getOtherContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "file")};
+ }
+ let open = view.getOpenFileContext(node);
+ let downloadOptions = {dataType: "zip", identifier: "file/download-zip"};
+ let options = {asZip: true};
+ let download = view.getDownloadContext(node, downloadOptions, options);
+ let rename = view.getRenameContext(node);
+ let duplicate = view.getDuplicateContext(node, "file/duplicate");
+ let moveToTrash = view.getMoveToTrashContext(node, "file");
+ if(node.text.endsWith(".zip")) {
+ return {
+ extractAll: view.getExtractAllContext(node),
+ download: download, rename: rename,
+ duplicate: duplicate, moveToTrash: moveToTrash
+ }
+ }
+ return {
+ open: open, download: download, rename: rename,
+ duplicate: duplicate, moveToTrash: moveToTrash
+ }
+}
+
+let getRootContext = (view, node) => {
+ return {
+ refresh: view.getRefreshContext(node),
+ addModel: view.getAddModelContext(node),
+ newDirectory: view.getNewDirectoryContext(node),
+ newDomain: view.getNewDomainContext(node),
+ upload: view.getFullUploadContext(node, true),
+ download: view.getDownloadWCombineContext(node),
+ rename: view.getRenameContext(node)
+ }
+}
+
+let getSBMLContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "sbml model")};
+ }
+ let downloadOptions = {dataType: "plain-text", identifier: "file/download"};
+ return {
+ open: view.getOpenSBMLContext(node),
+ convert: view.getSBMLConvertContext(node, "sbml/to-model"),
+ download: view.getDownloadContext(node, downloadOptions),
+ rename: view.getRenameContext(node),
+ duplicate: view.getDuplicateContext(node, "file/duplicate"),
+ moveToTrash: view.getMoveToTrashContext(node, "sbml model")
+ }
+}
+
+let getSpatialModelContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "spatial model")};
+ }
+ let downloadOptions = {dataType: "json", identifier: "file/json-data"};
+ return {
+ edit: view.getEditModelContext(node),
+ extract: getExtractContext(view, node, "model"),
+ newWorkflow: view.buildContextWithSubmenus({
+ label: "New Workflow",
+ submenu: {
+ jupyterNotebook: view.getNotebookNewWorkflowContext(node)
+ }
+ }),
+ convert: view.getSmdlConvertContext(node, "spatial/to-model"),
+ download: view.getDownloadContext(node, downloadOptions),
+ rename: view.getRenameContext(node),
+ duplicate: view.getDuplicateContext(node, "file/duplicate"),
+ moveToTrash: view.getMoveToTrashContext(node, "model")
+ }
+}
+
+let getWorkflowContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "workflow")};
+ }
+ let options = {target: "workflow", cb: (body) => {
+ let title = `Model for ${body.File}`;
+ if(body.error){
+ view.reportError({Reason: title, Message: body.error});
+ }else{
+ if(document.querySelector("#successModal")) {
+ document.querySelector("#successModal").remove();
+ }
+ let message = `The model for ${body.File} is located here: ${body.mdlPath}`;
+ let modal = $(modals.successHtml(message, {title: title})).modal();
+ }
+ }}
+ if(!node.original._newFormat) {
+ options['timeStamp'] = view.getTimeStamp();
+ }
+ return {
+ open: view.getOpenWorkflowContext(node),
+ model: view.getWorkflowMdlContext(node),
+ extract: getExtractContext(view, node, "workflow"),
+ download: view.getDownloadWCombineContext(node),
+ rename: view.getRenameContext(node),
+ duplicate: view.getDuplicateContext(node, "workflow/duplicate", options),
+ moveToTrash: view.getMoveToTrashContext(node, "workflow")
+ }
+}
+
+let getWorkflowGroupContext = (view, node) => {
+ if(node.original._path.includes(".proj/trash/")) { //item in trash
+ return {delete: view.getDeleteContext(node, "workflow group")};
+ }
+ return {
+ refresh: view.getRefreshContext(node),
+ download: view.getDownloadWCombineContext(node),
+ moveToTrash: view.getMoveToTrashContext(node, "workflow group")
+ }
+}
+
+let move = (view, par, node) => {
+ let newDir = par.original._path !== "/" ? par.original._path : "";
+ let file = node.original._path.split('/').pop();
+ let oldPath = node.original._path;
+ let queryStr = `?srcPath=${oldPath}&dstPath=${path.join(newDir, file)}`;
+ let endpoint = path.join(app.getApiPath(), "file/move") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ node.original._path = path.join(newDir, file);
+ if((node.type === "nonspatial" || node.type === "spatial") && (oldPath.includes("trash") || newDir.includes("trash"))) {
+ updateParent(view, "Archive");
+ }else if(node.type !== "notebook" || node.original._path.includes(".wkgp") || newDir.includes(".wkgp")) {
+ updateParent(view, node.type);
+ }
+ view.refreshJSTree(par);
+ },
+ error: (err, response, body) => {
+ body = JSON.parse(body);
+ view.refreshJSTree(par);
+ }
+ });
+}
+
+let setup = (view) => {
+ $(view.queryByHook("fb-new-project")).css("display", "none");
+ $(view.queryByHook("fb-empty-trash")).css("display", "none");
+}
+
+let toModel = (view, node, identifier) => {
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), identifier) + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ var root = $('#files-jstree').jstree().get_node(node.parent);
+ while(root.type !== "root") {
+ root = $('#files-jstree').jstree().get_node(root.parent);
+ }
+ view.refreshJSTree(root);
+ view.selectNode(root, body.File.replace(".mdl", ".wkgp"));
+ if(identifier.startsWith("sbml") && body.errors.length > 0){
+ if(document.querySelector('#sbmlToModelModal')) {
+ document.querySelector('#sbmlToModelModal').remove();
+ }
+ let modal = $(modals.sbmlToModelHtml(body.message, body.errors)).modal();
+ }else{
+ updateParent(view, "model");
+ }
+ }
+ });
+}
+
+let toSBML = (view, node) => {
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), "model/to-sbml") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ let par = $('#files-jstree').jstree().get_node(node.parent);
+ let grandPar = $('#files-jstree').jstree().get_node(par.parent);
+ view.refreshJSTree(grandPar);
+ view.selectNode(grandPar, body.File);
+ }
+ });
+}
+
+let toSpatial = (view, node) => {
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), "model/to-spatial") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ let par = $('#files-jstree').jstree().get_node(node.parent);
+ let grandPar = $('#files-jstree').jstree().get_node(par.parent);
+ view.refreshJSTree(grandPar);
+ updateParent(view, "spatial");
+ view.selectNode(grandPar, body.File.replace(".smdl", ".wkgp"));
+ }
+ });
+}
+
+let types = {
+ 'root' : {"icon": "jstree-icon jstree-folder"},
+ 'folder' : {"icon": "jstree-icon jstree-folder"},
+ 'spatial' : {"icon": "jstree-icon jstree-file"},
+ 'nonspatial' : {"icon": "jstree-icon jstree-file"},
+ 'workflowGroup' : {"icon": "jstree-icon jstree-folder"},
+ 'workflow' : {"icon": "jstree-icon jstree-file"},
+ 'notebook' : {"icon": "jstree-icon jstree-file"},
+ 'domain' : {"icon": "jstree-icon jstree-file"},
+ 'sbmlModel' : {"icon": "jstree-icon jstree-file"},
+ 'other' : {"icon": "jstree-icon jstree-file"}
+}
+
+let updateParent = (view, type) => {
+ let models = ["nonspatial", "spatial", "sbml", "model"];
+ let workflows = ["workflow", "notebook"];
+ if(models.includes(type)) {
+ view.parent.update("Model", "file-browser");
+ }else if(workflows.includes(type)) {
+ view.parent.update("Workflow", "file-browser");
+ }else if(type === "workflowGroup") {
+ view.parent.update("WorkflowGroup", "file-browser");
+ }else if(type === "Archive") {
+ view.parent.update(type, "file-browser");
+ }
+}
+
+let validateMove = (view, node, more, pos) => {
+ // Check if files are being move directly into the trash and remain static with respect to the trash
+ let validDst = Boolean(more && more.ref && more.ref.type && more.ref.original);
+ if(validDst && path.dirname(more.ref.original._path).includes("trash")) { return false };
+ let validSrc = Boolean(node && node.type && node.original && node.original.text !== "trash");
+ if(validSrc && validDst && node.original._path.includes("trash") && more.ref.original.text === 'trash') { return false };
+
+ // Check if workflow is running
+ let isWorkflow = Boolean(validSrc && node.type === "workflow");
+ if(isWorkflow && node.original._status && node.original._status === "running") { return false };
+
+ // Check if model, workflow, or workflow group is moving to or from trash
+ let isWkgp = Boolean(validSrc && node.type === "workflowGroup");
+ let trashAction = Boolean((validSrc && node.original._path.includes("trash")) || (validDst && more.ref.original.text === "trash"));
+ if(isWkgp && !(view.parent.model.newFormat && trashAction)) { return false };
+ let isModel = Boolean(validSrc && (node.type === "nonspatial" || node.type === "spatial"));
+ if((isModel || isWorkflow) && !trashAction) { return false };
+
+ // Check if model, workflow, or workflow group is moving from trash to the correct location
+ if(validSrc && node.original._path.includes("trash")) {
+ if(isWkgp && (!view.parent.model.newFormat || (validDst && more.ref.type !== "root"))) { return false };
+ if(isWorkflow && validDst && more.ref.type !== "workflowGroup") { return false };
+ if(isModel && validDst) {
+ if(!view.parent.model.newFormat && more.ref.type !== "root") { return false };
+ let length = node.original.text.split(".").length;
+ let modelName = node.original.text.split(".").slice(0, length - 1).join(".");
+ if(view.parent.model.newFormat && (more.ref.type !== "workflowGroup" || !more.ref.original.text.startsWith(modelName))) { return false };
+ }
+ }
+
+ // Check if notebook or other file is moving to a valid location.
+ let validDsts = ["root", "folder"];
+ let isNotebook = Boolean(validSrc && node.type === "notebook");
+ let isOther = Boolean(validSrc && !isModel && !isWorkflow && !isWkgp && !isNotebook);
+ if(isOther && validDst && !validDsts.includes(more.ref.type)) { return false };
+ validDsts.push("workflowGroup");
+ if(isNotebook && validDst && !validDsts.includes(more.ref.type)) { return false };
+
+ // Check if file already exists with that name in folder
+ if(validDst && validDsts.includes(more.ref.type)){
+ if(!more.ref.state.loaded) { return false };
+ var text = node.text;
+ if(!isNaN(text.split(' ').pop().split('.').join(""))){
+ text = text.replace(text.split(' ').pop(), '').trim();
+ }
+ if(more.ref.text !== "trash"){
+ try{
+ let BreakException = {};
+ more.ref.children.forEach((child) => {
+ let child_node = $('#files-jstree').jstree().get_node(child);
+ let exists = child_node.text === text;
+ if(exists) { throw BreakException; };
+ })
+ }catch { return false; };
+ }
+ }
+
+ // Check if curser is over the correct location
+ if(more && (pos != 0 || more.pos !== "i") && !more.core) { return false };
+ return true;
+}
+
+module.exports = {
+ contextZipTypes: contextZipTypes,
+ doubleClick: doubleClick,
+ getDomainContext: getDomainContext,
+ getFolderContext: getFolderContext,
+ getModelContext: getModelContext,
+ getNotebookContext: getNotebookContext,
+ getOtherContext: getOtherContext,
+ getProjectContext: getOtherContext,
+ getRootContext: getRootContext,
+ getSBMLContext: getSBMLContext,
+ getSpatialModelContext: getSpatialModelContext,
+ getWorkflowContext: getWorkflowContext,
+ getWorkflowGroupContext: getWorkflowGroupContext,
+ move: move,
+ setup: setup,
+ toModel: toModel,
+ toSBML: toSBML,
+ toSpatial: toSpatial,
+ types: types,
+ updateParent: updateParent,
+ validateMove: validateMove
+}
diff --git a/client/settings-view/templates/simulationSettingsView.pug b/client/settings-view/templates/simulationSettingsView.pug
index 8bfcc398d3..d06f917689 100644
--- a/client/settings-view/templates/simulationSettingsView.pug
+++ b/client/settings-view/templates/simulationSettingsView.pug
@@ -18,12 +18,22 @@ div#simulation-settings.card
button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#" + this.model.elementID + "collapse-settings", data-hook="collapse-settings-view") -
- div.collapse(class="show" id=this.model.elementID + "collapse-settings")
+ div.card-body
- div.card-body.tab-content
+ p.mb-0
+ | Consider the following when selecting an algorithm for your job. If you would like StochSS to choose the algorithm, select Choose for me.
+
+ ul.mb-0
+ li Algorithms ODE and Hybrid ODE/SSA work best to simulate models with a mode of Concentration.
+ li Algorithns SSA, Tau Leaping, and Hybrid ODE/SSA work best to simulate models with a mode of Population.
+ li Algorithm Hybrid ODE/SSA is required if the model contains advanced components.
+
+ div.collapse.tab-content(class="show" id=this.model.elementID + "collapse-settings")
div.tab-pane.active(id=this.model.elementID + "-edit-sim-settings" data-hook=this.model.elementID + "-edit-sim-settings")
+ hr
+
div.mx-1.head.align-items-baseline
h4.inline Simulation Algorithm
@@ -138,6 +148,8 @@ div#simulation-settings.card
div.tab-pane(id=this.model.elementID + "-view-sim-settings" data-hook=this.model.elementID + "-view-sim-settings")
+ hr
+
div
h6.inline.pr-2 Simulation Algorithm:
diff --git a/client/styles/styles.css b/client/styles/styles.css
index f9d28c9eb0..9ac266902d 100644
--- a/client/styles/styles.css
+++ b/client/styles/styles.css
@@ -163,6 +163,13 @@ a.disabled {
cursor: default;
}
+a.anchor {
+ display: block;
+ position: relative;
+ top: -80px;
+ visibility: hidden;
+}
+
.nav-item {
width: fit-content;
}
@@ -302,9 +309,12 @@ input[type="file"]::-ms-browse {
max-width: none;
}
+.presentation {
+ max-width: 70%;
+}
+
.information-btn {
color: #007bff;
- border-color: #ffffff;
padding: 0.5rem 1rem;
text-align: left;
}
diff --git a/client/templates/body.pug b/client/templates/body.pug
index 1137b23683..7cab6d1365 100644
--- a/client/templates/body.pug
+++ b/client/templates/body.pug
@@ -36,20 +36,25 @@ body
nav.sidebar.d-md-block
div.sidebar-sticky
ul.nav.nav-flex-column
- li.nav-item: a.nav-link(href="stochss/home", title="Users Home Page")
- | Home
+ li.nav-item: a.nav-link(href="stochss/home" title="Users Home Page") Home
- li.nav-item
- div: a.nav-link(href="stochss/project/browser", title="Explore your StochSS projects") Projects
+ li.nav-item: a.nav-link.pb-0(href="stochss/files" title="Browser Page") Browse:
- li.nav-item
- div: a.nav-link(href="stochss/files", title="Explore your StochSS files") Files
+ ul.nav.nav-flex-column
+ li.nav-item.ml-3
+ a.nav-link.pb-0(href="stochss/files#project-browser-section" title="Explore your StochSS projects") Projects
+
+ li.nav-item.ml-3(id="presentation-nav-link")
+ a.nav-link.pb-0(href="stochss/files#presentation-browser-section" title="Explore your StochSS presentations") Presentations
+
+ li.nav-item.ml-3
+ a.nav-link(href="stochss/files#file-browser-section" title="Explore your StochSS files") Files
li.nav-item
- div: a.nav-link(target="_blank", href="tree", title="Browse your files in a Jupyter interface") Jupyter
+ div: a.nav-link(target="_blank" href="tree" title="Browse your files in a Jupyter interface") Jupyter Notebooks
li.nav-item
- div: a.nav-link(href="stochss/quickstart", title="Quickstart") Tutorial
+ div: a.nav-link(href="stochss/quickstart" title="Quickstart") Tutorial
div.user-logs.card.card-body
diff --git a/client/templates/head.pug b/client/templates/head.pug
index 1c592483bd..c86352c5c3 100644
--- a/client/templates/head.pug
+++ b/client/templates/head.pug
@@ -1,6 +1,7 @@
head
meta(charset="utf-8")
title StochSS: Stochastic Simulation Service
+ link(rel="shortcut icon" href="static/favicon.ico")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(name="description", content="")
meta(name="author", content="")
diff --git a/client/templates/includes/editProject.pug b/client/templates/includes/editProject.pug
index 4104a043ca..c5e1dd9c1f 100644
--- a/client/templates/includes/editProject.pug
+++ b/client/templates/includes/editProject.pug
@@ -1,18 +1,26 @@
-tr
-
- td
- a.btn.btn-outline-secondary.box-shadow.name(
- id=this.model.elementID+"-open-btn"
- href=this.model.open
- role="button"
- style="width: 100%"
- )=this.model.name
-
- td
- div=this.model.location
-
- td
- button.btn.btn-outline-secondary.box-shadow(
- id=this.model.elementID+"-remove-btn"
- data-hook="remove-project-btn"
- )
\ No newline at end of file
+div.mx-1
+
+ if(this.model.collection.indexOf(this.model) !== 0)
+ hr
+
+ div.row
+
+ div.col-sm-4
+
+ a.btn.btn-outline-secondary.box-shadow.name(
+ id=this.model.elementID+"-open-btn"
+ href=this.model.open
+ role="button"
+ style="width: 100%"
+ )=this.model.name
+
+ div.col-sm-6
+
+ div=this.model.location
+
+ div.col-sm-2
+
+ button.btn.btn-outline-secondary.box-shadow(
+ id=this.model.elementID+"-remove-btn"
+ data-hook="remove-project-btn"
+ )
diff --git a/client/templates/includes/fileBrowserView.pug b/client/templates/includes/fileBrowserView.pug
deleted file mode 100644
index 3d1e23dc30..0000000000
--- a/client/templates/includes/fileBrowserView.pug
+++ /dev/null
@@ -1,44 +0,0 @@
-div#browse-files.card.card-body
-
- div
-
- h3.inline Browse Files
- div.inline
- button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#collapse-browse-files", id="collapse-browse-files-btn" data-hook="collapse-browse-files") +
-
- div.collapse(id="collapse-browse-files")
-
- div.alert-warning(class="collapse", id="extension-warning" data-hook="extension-warning") You should avoid changing the file extension unless you know what you are doing!
-
- div.alert-warning(class="collapse", id="rename-warning" data-hook="rename-warning") MESSAGE
-
- div.alert-danger(id="renameSpecialCharError" style="display: none;") Names can only include the following characters: (0-9), (a-z), (A-Z) and (., -, _, (, or ))
-
- div#models-jstree-view
-
- div
-
- button.btn.btn-primary.inline.box-shadow(
- id="project-browse-files-add-btn"
- data-hook="new-file-directory",
- data-toggle="dropdown",
- aria-haspopup="true",
- aria-expanded="false",
- type="button"
- ) +
-
- ul.dropdown-menu(aria-labelledby="project-browse-files-add-btn")
- li.dropdown-item(id="new-directory" data-hook="new-directory") Create Directory
- li.dropdown-item(id="browser-new-model" data-hook="browser-new-model" data-type="model") Create Model
- li.dropdown-item(id="browser-new-spatial-model" data-hook="browser-new-model" data-type="spatial") Create Spatial Model (beta)
- li.dropdown-item(id="browser-new-domain" data-hook="browser-new-domain") Create Domain (beta)
- li.dropdown-divider
- li.dropdown-item(id="browser-existing-model" data-hook="browser-existing-model") Add Existing Model
- li.dropdown-divider
- li.dropdown-item(id="upload-file-btn-bf" data-hook="upload-file-btn-bf", data-type="model") Upload StochSS Model
- li.dropdown-item(id="upload-file-btn-bf" data-hook="upload-file-btn-bf", data-type="sbml") Upload SBML Model
- li.dropdown-item(id="upload-file-btn-bf" data-hook="upload-file-btn-bf", data-type="file") Upload File
-
- button.btn.btn-primary.box-shadow(id="options-for-node" data-hook="options-for-node" disabled) Actions
-
- button.btn.btn-primary.inline.box-shadow(id="refresh-jstree" data-hook="refresh-jstree") Refresh
\ No newline at end of file
diff --git a/client/templates/includes/jstreeView.pug b/client/templates/includes/jstreeView.pug
new file mode 100644
index 0000000000..64c3109a8f
--- /dev/null
+++ b/client/templates/includes/jstreeView.pug
@@ -0,0 +1,39 @@
+div
+
+ div.alert-warning(class="collapse" id="extension-warning" data-hook="extension-warning") You should avoid changing the file extension unless you know what you are doing!
+
+ div.alert-warning(class="collapse", id="rename-warning" data-hook="rename-warning") MESSAGE
+
+ div.alert-danger(id="renameSpecialCharError" style="display: none;") Names can only include the following characters: (0-9), (a-z), (A-Z) and (., -, _, (, or ))
+
+ div#files-jstree
+
+ div
+
+ button.btn.btn-primary.inline.box-shadow(
+ id="files-add-btn",
+ data-hook="files-add-btn",
+ data-toggle="dropdown",
+ aria-haspopup="true",
+ aria-expanded="false",
+ type="button"
+ ) +
+
+ ul.dropdown-menu(aria-labelledby="files-add-btn")
+ li.dropdown-item(id="fb-new-directory" data-hook="fb-new-directory") Create Directory
+ li.dropdown-item(id="fb-new-project" data-hook="fb-new-project") Create Project
+ li.dropdown-item(id="fb-new-model" data-hook="fb-new-model" data-type="model") Create Model
+ li.dropdown-item(id="fb-new-model" data-hook="fb-new-model" data-type="spatial") Create Spatial Model (beta)
+ li.dropdown-item(id="fb-new-model" data-hook="fb-new-domain") Create Domain (beta)
+ li.dropdown-divider(data-hook="fb-proj-seperator")
+ li.dropdown-item(id="fb-import-model" data-hook="fb-import-model") Add Existing Model
+ li.dropdown-divider
+ li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn" data-type="model") Upload StochSS Model
+ li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn" data-type="sbml") Upload SBML Model
+ li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn" data-type="file") Upload File
+
+ button.btn.btn-primary.inline.box-shadow(id="options-for-node" data-hook="options-for-node" disabled) Actions
+
+ button.btn.btn-primary.inline.box-shadow(id="refresh-jstree" data-hook="refresh-jstree") Refresh
+
+ button.btn.btn-primary.inline.box-shadow(id="fb-empty-trash" data-hook="fb-empty-trash") Empty Trash
diff --git a/client/templates/includes/modelStateButtons.pug b/client/templates/includes/modelStateButtons.pug
deleted file mode 100644
index 5cc08474a9..0000000000
--- a/client/templates/includes/modelStateButtons.pug
+++ /dev/null
@@ -1,42 +0,0 @@
-div.mdl-edit-btn
-
- div.mdl-edit-btn.saving-status(data-hook="mdl-action-start")
-
- div.spinner-grow
-
- span(data-hook="saving" style="display: none") Saving ...
-
- span(data-hook="publishing" style="display: none") Publishing ...
-
- div.mdl-edit-btn.saved-status(data-hook="mdl-action-end")
-
- span(data-hook="saved" style="display: none") Saved
-
- span(data-hook="published" style="display: none") Published
-
- div.mdl-edit-btn.save-error-status(data-hook="mdl-action-err")
-
- span Error
-
- button.btn.btn-primary.box-shadow(data-hook="save") Save
-
- button.btn.btn-primary.box-shadow(data-hook="return-to-project-btn" style="display: none;") Return to Project
-
- button.btn.btn-primary.box-shadow.dropdown-toggle(
- id="simulate-model",
- data-hook="simulate-model",
- data-toggle="dropdown",
- aria-haspopup="true",
- aria-expanded="false",
- type="button"
- ) Simulate
-
- ul.dropdown-menu(aria-labelledby="preview-options")
- li.dropdown-item(data-hook="run" data-type="preview") Preview
- li.dropdown-divider
- li.dropdown-item(id="stochss-es" data-hook="stochss-es" data-type="ensemble") Ensemble Simulation
- li.dropdown-item(id="stochss-ps" data-hook="stochss-ps" data-type="psweep") Parameter Sweep
- li.dropdown-item(data-hook="new-workflow" data-type="notebook") Jupyter Notebook
-
- button.btn.btn-primary.box-shadow(data-hook="presentation") Publish
-
\ No newline at end of file
diff --git a/client/templates/includes/presentationView.pug b/client/templates/includes/presentationView.pug
new file mode 100644
index 0000000000..113ec43540
--- /dev/null
+++ b/client/templates/includes/presentationView.pug
@@ -0,0 +1,26 @@
+div.mx-1
+
+ if(this.model.collection.indexOf(this.model) !== 0)
+ hr
+
+ div.row
+
+ div.col-sm-6
+
+ a.pl-2(href=this.model.link)=this.model.file
+
+ div.col-sm-2
+
+ button.btn.btn-outline-secondary.box-shadow(data-hook="copy-link") Copy Link
+
+ div.col-sm-2
+
+ div=this.model.size + " " + this.model.tag
+
+ div.col-sm-2
+
+ button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X
+
+ div.text-success(data-hook="copy-link-success" style="display: none;") Link copied to clipboard
+
+ div.text-danger(data-hook="copy-link-failed" style="display: none;")
diff --git a/client/templates/pages/browser.pug b/client/templates/pages/browser.pug
new file mode 100644
index 0000000000..a634614fe5
--- /dev/null
+++ b/client/templates/pages/browser.pug
@@ -0,0 +1,62 @@
+section.page
+
+ div.card.mt-3
+
+ a.anchor#project-browser-section
+
+ div.card-header.pb-0
+
+ h2.inline Project Browser
+
+ button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-projects" data-hook="collapse-projects") -
+
+ div.collapse.show(id="collapse-projects")
+
+ div.card-body
+
+ div.mb-3
+
+ div(id="projects-view-container" data-hook="projects-view-container")
+
+ button.btn.btn-outline-primary.box-shadow(id="new-project-btn" data-hook="new-project-btn") New
+
+ div.card.mt-4#presentations
+
+ a.anchor#presentation-browser-section
+
+ div.card-header.pb-0
+
+ h2.inline Presentations
+
+ button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-presentations" data-hook="collapse-presentations") -
+
+ div.collapse.show(id="collapse-presentations")
+
+ div.card-body
+
+ div.mx-1.row.head.align-items-baseline
+
+ div.col-sm-8: h6 File
+
+ div.col-sm-2: h6 Size
+
+ div.col-sm-2: h6 Remove
+
+ div.mt-3(data-hook="presentation-list")
+
+ div.card.mt-4
+
+ a.anchor#file-browser-section
+
+ div.card-header.pb-0
+ h2.inline File Browser
+
+ button.big-tip.btn.information-btn.help(id="file-browser-help" data-hook="file-browser-help")
+
+ button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-files" data-hook="collapse-files") -
+
+ div.collapse.show(id="collapse-files")
+
+ div.card-body
+
+ div(data-hook="jstree-view-container")
diff --git a/client/templates/pages/fileBrowser.pug b/client/templates/pages/fileBrowser.pug
deleted file mode 100644
index ea87593238..0000000000
--- a/client/templates/pages/fileBrowser.pug
+++ /dev/null
@@ -1,40 +0,0 @@
-section.page
-
- div
- h2.inline File Browser
-
- button.big-tip.btn.information-btn.help(id="file-browser-help" data-hook="file-browser-help")
-
- div.alert-warning(class="collapse", id="extension-warning" data-hook="extension-warning") You should avoid changing the file extension unless you know what you are doing!
-
- div.alert-warning(class="collapse", id="rename-warning" data-hook="rename-warning") MESSAGE
-
- div.alert-danger(id="renameSpecialCharError" style="display: none;") Names can only include the following characters: (0-9), (a-z), (A-Z) and (., -, _, (, or ))
-
- div#models-jstree
-
- button.btn.btn-primary.inline.box-shadow(
- id="new-file-directory",
- data-hook="new-file-directory",
- data-toggle="dropdown",
- aria-haspopup="true",
- aria-expanded="false",
- type="button"
- ) +
-
- ul.dropdown-menu(aria-labelledby="new-file-directory")
- li.dropdown-item(id="new-directory" data-hook="new-directory") Create Directory
- li.dropdown-item(id="new-project" data-hook="new-project") Create Project
- li.dropdown-item(id="new-model" data-hook="new-model" data-type="model") Create Model
- li.dropdown-item(id="new-model" data-hook="new-model" data-type="spatial") Create Spatial Model (beta)
- li.dropdown-item(id="new-model" data-hook="new-domain") Create Domain (beta)
- li.dropdown-divider
- li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn", data-type="model") Upload StochSS Model
- li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn", data-type="sbml") Upload SBML Model
- li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn", data-type="file") Upload File
-
- button.btn.btn-primary.box-shadow(id="options-for-node" data-hook="options-for-node" disabled) Actions
-
- button.btn.btn-primary.inline.box-shadow(id="refresh-jstree" data-hook="refresh-jstree") Refresh
-
- button.btn.btn-primary.inline.box-shadow(id="empty-trash" data-hook="empty-trash") Empty Trash
\ No newline at end of file
diff --git a/client/templates/pages/home.pug b/client/templates/pages/home.pug
index 6adba77cb5..a23a82beb9 100644
--- a/client/templates/pages/home.pug
+++ b/client/templates/pages/home.pug
@@ -1,50 +1,52 @@
-section.page
- div#home-wrapper.container.card.card-body
-
- div.logo(id="stochss-logo" data-hook="stochss-logo")
- img(src="/hub/static/stochss-logo.png")
+body
- div.centered
- h1.display-5 Welcome to StochSS Live!
+ section.page
+ div#home-wrapper.container.card.card-body
+
+ div.logo(id="stochss-logo" data-hook="stochss-logo")
+ img(src="/hub/static/stochss-logo.png")
- p.home-p
- | An accessible platform for modeling mathematical, biological, and biochemical systems.
+ div.centered
+ h1.display-5 Welcome to StochSS Live!
- div.row.home-highlights
- div.col-md-2
- div.col-md-5
+ p.home-p
+ | An accessible platform for modeling mathematical, biological, and biochemical systems.
- h5 Features
- ul.home-list
- li Discrete Stochastic Modeling
- li Continuous ODE Modeling
- li Convert Between Stochastic and ODE Models
- li Import SBML Models
- li Parameter Sweep Workflows
- li Spatial Simulations
- li Publish Models and Notebooks
-
- div.col-md-5
+ div.row.home-highlights
+ div.col-md-2
+ div.col-md-5
- h5 Coming soon...
- ul.home-list
- li Sciope Integration
- li Bring Your Own Cloud
+ h5 Features
+ ul.home-list
+ li Discrete Stochastic Modeling
+ li Continuous ODE Modeling
+ li Convert Between Stochastic and ODE Models
+ li Import SBML Models
+ li Parameter Sweep Workflows
+ li Spatial Simulations
+ li Publish Models, Jobs, and Notebooks
+
+ div.col-md-5
- div.centered
- a.try-stochss-btn.btn.btn-primary.box-shadow(href="/hub/spawn" role="button") Click here to login
+ h5 Coming soon...
+ ul.home-list
+ li Sciope Integration
+ li Bring Your Own Cloud
- div#registration-home.container.card.card-body
+ div.centered
+ a.try-stochss-btn.btn.btn-primary.box-shadow(href="/hub/spawn" role="button") Click here to login
- div.centered
+ div#registration-home.container.card.card-body
- p.home-p
- | StochSS is open-source software. To obtain continued funding for sustained development, we must show that StochSS is making an impact in the scientific community. For this, we need to know who is using the software, and a bit about what they are using it for. Please consider registering StochSS to show your support.
+ div.centered
- div.collapse.show(data-hook="registration-link")
+ p.home-p
+ | StochSS is open-source software. To obtain continued funding for sustained development, we must show that StochSS is making an impact in the scientific community. For this, we need to know who is using the software, and a bit about what they are using it for. Please consider registering StochSS to show your support.
- button.register-stochss-btn.btn.btn-primary.box-shadow(data-hook="registration-link-button" style="margin: 1.5rem;") Register Stochss
+ div.collapse.show(data-hook="registration-link")
- div.collapse(data-hook="registration-form" id="registration-form")
+ button.register-stochss-btn.btn.btn-primary.box-shadow(data-hook="registration-link-button" style="margin: 1.5rem;") Register Stochss
- iframe(src="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true" width="100%" height="800" frameborder="0")
+ div.collapse(data-hook="registration-form" id="registration-form")
+
+ iframe(src="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true" width="100%" height="800" frameborder="0")
diff --git a/client/templates/pages/jobPresentation.pug b/client/templates/pages/jobPresentation.pug
index 34e51b0375..61af14146d 100644
--- a/client/templates/pages/jobPresentation.pug
+++ b/client/templates/pages/jobPresentation.pug
@@ -1,13 +1,45 @@
-section.page
+body
- div#job-presentation.container
+ section.page
- h2=this.model.name
+ nav.navbar.navbar-expand-md.p-1.navbar-light.bg-light.fixed-top
+ a.navbar-brand(href="/hub/spawn")
+ img(style="height: 38px;margin-left: -1.2em;" src="/hub/static/stochss-logo.png")
- div(data-hook="job-results")
+ button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation")
+ span.navbar-toggler-icon
- div(data-hook="job-settings")
+ div#navbar.collapse.navbar-collapse.justify-content-end(role="navigation")
+ ul.navbar-nav.float-right
+ li.nav-item: a.nav-link(href="http://www.stochss.org", target="_blank")
+ | About
- div(data-hook="job-model")
+ li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true", target="_blank")
+ | Register
- a.btn.btn-outline-secondary.box-shadow.text-break(href=this.open role="button") Open in StochSS
\ No newline at end of file
+ li.nav-item: a.nav-link(href="http://www.stochss.org/documentation", target="_blank")
+ | Documentation
+
+ li.nav-item: a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" target="_blank")
+ | Submit Feedback
+
+ li.nav-item
+ div.dropdown
+ button.dropdown-toggle.nav-item.nav-link.dropbtn Contact
+ div.dropdown-content
+ a.nav-link(href="https://gitter.im/StochSS/community#", target="_blank") Community Chat
+ a.nav-link(href="https://github.com/StochSS/stochss/issues/new", target="_blank") Report Issue
+
+ div#job-presentation.container.presentation
+
+ div.presentation-header
+
+ h1=this.title
+
+ div(data-hook="job-view")
+
+ div.mt-3
+
+ a.btn.btn-outline-secondary.box-shadow.text-break.mr-3(href=this.openLink role="button") Open in StochSS
+
+ a.btn.btn-outline-secondary.box-shadow.text-break(href=this.downloadLink role="button") Download
\ No newline at end of file
diff --git a/client/templates/pages/modelEditor.pug b/client/templates/pages/modelEditor.pug
index 2d30773d17..d77e1e2d0c 100644
--- a/client/templates/pages/modelEditor.pug
+++ b/client/templates/pages/modelEditor.pug
@@ -96,6 +96,47 @@ section.page
ul.dropdown-menu(aria-labelledby="preview-options")
li.dropdown-item(data-hook="download-png") Download as PNG
-
+
div.model-state-buttons(data-hook="model-state-buttons-container")
+
+ div.mdl-edit-btn
+
+ div.mdl-edit-btn.saving-status(data-hook="mdl-action-start")
+
+ div.spinner-grow
+
+ span(data-hook="saving" style="display: none") Saving ...
+
+ span(data-hook="publishing" style="display: none") Publishing ...
+
+ div.mdl-edit-btn.saved-status(data-hook="mdl-action-end")
+
+ span(data-hook="saved" style="display: none") Saved
+
+ span(data-hook="published" style="display: none") Published
+
+ div.mdl-edit-btn.save-error-status(data-hook="mdl-action-err")
+
+ span Error
+
+ button.btn.btn-primary.box-shadow(data-hook="save") Save
+
+ button.btn.btn-primary.box-shadow(data-hook="return-to-project-btn" style="display: none;") Return to Project
+
+ button.btn.btn-primary.box-shadow.dropdown-toggle(
+ id="simulate-model",
+ data-hook="simulate-model",
+ data-toggle="dropdown",
+ aria-haspopup="true",
+ aria-expanded="false",
+ type="button"
+ ) Simulate
+
+ ul.dropdown-menu(aria-labelledby="preview-options")
+ li.dropdown-item(data-hook="run" data-type="preview") Preview
+ li.dropdown-divider
+ li.dropdown-item(id="stochss-es" data-hook="stochss-es" data-type="ensemble") Ensemble Simulation
+ li.dropdown-item(id="stochss-ps" data-hook="stochss-ps" data-type="psweep") Parameter Sweep
+ li.dropdown-item(data-hook="new-workflow" data-type="notebook") Jupyter Notebook
+ button.btn.btn-primary.box-shadow(data-hook="presentation") Publish
diff --git a/client/templates/pages/modelPresentation.pug b/client/templates/pages/modelPresentation.pug
index f5735e7810..0df42dde47 100644
--- a/client/templates/pages/modelPresentation.pug
+++ b/client/templates/pages/modelPresentation.pug
@@ -1,43 +1,45 @@
-section.page
+body
- nav.navbar.navbar-expand-md.p-1.navbar-light.bg-light.fixed-top
- a.navbar-brand(href="/hub/spawn")
- img(style="height: 38px;margin-left: -1.2em;" src="/hub/static/stochss-logo.png")
+ section.page
- button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation")
- span.navbar-toggler-icon
+ nav.navbar.navbar-expand-md.p-1.navbar-light.bg-light.fixed-top
+ a.navbar-brand(href="/hub/spawn")
+ img(style="height: 38px;margin-left: -1.2em;" src="/hub/static/stochss-logo.png")
- div#navbar.collapse.navbar-collapse.justify-content-end(role="navigation")
- ul.navbar-nav.float-right
- li.nav-item: a.nav-link(href="http://www.stochss.org", target="_blank")
- | About
+ button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation")
+ span.navbar-toggler-icon
- li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true", target="_blank")
- | Register
+ div#navbar.collapse.navbar-collapse.justify-content-end(role="navigation")
+ ul.navbar-nav.float-right
+ li.nav-item: a.nav-link(href="http://www.stochss.org", target="_blank")
+ | About
- li.nav-item: a.nav-link(href="http://www.stochss.org/documentation", target="_blank")
- | Documentation
+ li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true", target="_blank")
+ | Register
- li.nav-item: a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" target="_blank")
- | Submit Feedback
+ li.nav-item: a.nav-link(href="http://www.stochss.org/documentation", target="_blank")
+ | Documentation
- li.nav-item
- div.dropdown
- button.dropdown-toggle.nav-item.nav-link.dropbtn Contact
- div.dropdown-content
- a.nav-link(href="https://gitter.im/StochSS/community#", target="_blank") Community Chat
- a.nav-link(href="https://github.com/StochSS/stochss/issues/new", target="_blank") Report Issue
+ li.nav-item: a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" target="_blank")
+ | Submit Feedback
- div#model-presentation.container
+ li.nav-item
+ div.dropdown
+ button.dropdown-toggle.nav-item.nav-link.dropbtn Contact
+ div.dropdown-content
+ a.nav-link(href="https://gitter.im/StochSS/community#", target="_blank") Community Chat
+ a.nav-link(href="https://github.com/StochSS/stochss/issues/new", target="_blank") Report Issue
- div.presentation-header
+ div#model-presentation.container.presentation
- h1=this.model.name
+ div.presentation-header
- div(data-hook="model-view")
+ h1=this.model.name
- div.mt-3
+ div(data-hook="model-view")
- a.btn.btn-outline-secondary.box-shadow.text-break.mr-3(href=this.openLink role="button") Open in StochSS
+ div.mt-3
- a.btn.btn-outline-secondary.box-shadow.text-break(href=this.downloadLink role="button") Download (.mdl)
+ a.btn.btn-outline-secondary.box-shadow.text-break.mr-3(href=this.openLink role="button") Open in StochSS
+
+ a.btn.btn-outline-secondary.box-shadow.text-break(href=this.downloadLink role="button") Download (.mdl)
diff --git a/client/templates/pages/multiplePlotsPresentation.pug b/client/templates/pages/multiplePlotsPresentation.pug
new file mode 100644
index 0000000000..d53915d27f
--- /dev/null
+++ b/client/templates/pages/multiplePlotsPresentation.pug
@@ -0,0 +1,11 @@
+body
+
+ section.page
+
+ h3="Workflow: "+this.workflow
+
+ div.card.card-body
+
+ h5 Plot Trajectories: Multiple Graphs
+
+ div(data-hook="figures")
\ No newline at end of file
diff --git a/client/templates/pages/notebookPresentation.pug b/client/templates/pages/notebookPresentation.pug
index ba03a11038..d3b75aecad 100644
--- a/client/templates/pages/notebookPresentation.pug
+++ b/client/templates/pages/notebookPresentation.pug
@@ -1,45 +1,47 @@
-section.page
+body
- nav.navbar.navbar-expand-md.p-1.navbar-light.bg-light.fixed-top
- a.navbar-brand(href="/hub/spawn")
- img(style="height: 38px;margin-left: -1.2em;" src="/hub/static/stochss-logo.png")
+ section.page
- button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation")
- span.navbar-toggler-icon
+ nav.navbar.navbar-expand-md.p-1.navbar-light.bg-light.fixed-top
+ a.navbar-brand(href="/hub/spawn")
+ img(style="height: 38px;margin-left: -1.2em;" src="/hub/static/stochss-logo.png")
- div#navbar.collapse.navbar-collapse.justify-content-end(role="navigation")
- ul.navbar-nav.float-right
- li.nav-item: a.nav-link(href="http://www.stochss.org", target="_blank")
- | About
+ button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation")
+ span.navbar-toggler-icon
- li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true", target="_blank")
- | Register
+ div#navbar.collapse.navbar-collapse.justify-content-end(role="navigation")
+ ul.navbar-nav.float-right
+ li.nav-item: a.nav-link(href="http://www.stochss.org", target="_blank")
+ | About
- li.nav-item: a.nav-link(href="http://www.stochss.org/documentation", target="_blank")
- | Documentation
+ li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true", target="_blank")
+ | Register
- li.nav-item: a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" target="_blank")
- | Submit Feedback
+ li.nav-item: a.nav-link(href="http://www.stochss.org/documentation", target="_blank")
+ | Documentation
- li.nav-item
- div.dropdown
- button.dropdown-toggle.nav-item.nav-link.dropbtn Contact
- div.dropdown-content
- a.nav-link(href="https://gitter.im/StochSS/community#", target="_blank") Community Chat
- a.nav-link(href="https://github.com/StochSS/stochss/issues/new", target="_blank") Report Issue
+ li.nav-item: a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" target="_blank")
+ | Submit Feedback
- div#notebook-presentation.container
+ li.nav-item
+ div.dropdown
+ button.dropdown-toggle.nav-item.nav-link.dropbtn Contact
+ div.dropdown-content
+ a.nav-link(href="https://gitter.im/StochSS/community#", target="_blank") Community Chat
+ a.nav-link(href="https://github.com/StochSS/stochss/issues/new", target="_blank") Report Issue
- div.presentation-header
+ div#notebook-presentation.container.presentation
- h1=this.name
+ div.presentation-header
- div.card.card-body
+ h1=this.name
- iframe(id="notebook" width="100%" height="100%" frameborder="0" onload="this.height=this.contentWindow.document.body.scrollHeight;")
+ div.card.card-body
- div.mt-3
+ iframe(id="notebook" width="100%" height="100%" frameborder="0" onload="this.height=this.contentWindow.document.body.scrollHeight;")
- a.btn.btn-outline-secondary.box-shadow.text-break.mr-3(href=this.openLink role="button") Open in StochSS
+ div.mt-3
- a.btn.btn-outline-secondary.box-shadow.text-break(href=this.downloadLink role="button") Download (.ipynb)
+ a.btn.btn-outline-secondary.box-shadow.text-break.mr-3(href=this.openLink role="button") Open in StochSS
+
+ a.btn.btn-outline-secondary.box-shadow.text-break(href=this.downloadLink role="button") Download (.ipynb)
diff --git a/client/templates/pages/projectBrowser.pug b/client/templates/pages/projectBrowser.pug
deleted file mode 100644
index 9fb26fdafc..0000000000
--- a/client/templates/pages/projectBrowser.pug
+++ /dev/null
@@ -1,9 +0,0 @@
-section.page
-
- h2 Project Browser
-
- table.table
-
- tbody(id="projects-view-container" data-hook="projects-view-container")
-
- button.btn.btn-outline-primary.box-shadow(id="new-project-btn" data-hook="new-project-btn") New
\ No newline at end of file
diff --git a/client/templates/pages/projectManager.pug b/client/templates/pages/projectManager.pug
index 6481baa211..59bd4742bb 100644
--- a/client/templates/pages/projectManager.pug
+++ b/client/templates/pages/projectManager.pug
@@ -55,7 +55,7 @@ section.page
li.dropdown-divider
li.dropdown-item(id="existing-model" data-hook="existing-model") Existing Model
li.dropdown-divider
- li.dropdown-item(id="upload-file-btn" data-hook="upload-file-btn", data-type="model") Upload StochSS Model (.mdl or .smdl)
+ li.dropdown-item(id="upload-model-btn" data-hook="upload-model-btn") Upload StochSS Model (.mdl or .smdl)
div.verticle-space-3(id=this.model.elementID + "-workflows-section" style="display: none;")
@@ -109,7 +109,17 @@ section.page
div(id="project-meta-data-container" data-hook="project-meta-data-container")
- div(id="project-manager-file-browser" data-hook="file-browser")
+ div#browse-files.card.card-body
+
+ div
+
+ h3.inline Browse Files
+ div.inline
+ button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#collapse-browse-files", id="collapse-browse-files-btn" data-hook="collapse-browse-files") +
+
+ div.collapse(id="collapse-browse-files")
+
+ div(id="project-manager-file-browser" data-hook="file-browser")
div
diff --git a/client/templates/pages/usersHome.pug b/client/templates/pages/usersHome.pug
index 83046269d8..01a19a7b2b 100644
--- a/client/templates/pages/usersHome.pug
+++ b/client/templates/pages/usersHome.pug
@@ -52,4 +52,4 @@ selction.page
button.btn.btn-primary.box-shadow.user-home-btn(id="quickstart-btn" data-hook="quickstart-btn")
| Click here for a tutorial
- div.col-md-2.col-lg-3.col-xl-4
\ No newline at end of file
+ div.col-md-2.col-lg-3.col-xl-4
diff --git a/client/views/edit-project.js b/client/views/edit-project.js
index 2dcf7d3f90..9ec7b4c713 100644
--- a/client/views/edit-project.js
+++ b/client/views/edit-project.js
@@ -41,19 +41,23 @@ module.exports = View.extend({
if(document.querySelector('#moveToTrashConfirmModal')) {
document.querySelector('#moveToTrashConfirmModal').remove();
}
- let self = this;
let modal = $(modals.moveToTrashConfirmHtml("model")).modal();
let yesBtn = document.querySelector('#moveToTrashConfirmModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
+ yesBtn.addEventListener('click', (e) => {
modal.modal('hide');
- let queryStr = "?srcPath=" + self.model.directory + "&dstPath=" + path.join("trash", self.model.directory.split("/").pop());
- let endpoint = path.join(app.getApiPath(), "file/move") + queryStr
+ let queryStr = `?srcPath=${this.model.directory}&dstPath=${path.join("trash", this.model.directory.split("/").pop())}`;
+ let endpoint = path.join(app.getApiPath(), "file/move") + queryStr;
app.getXHR(endpoint, {
- success: function (err, response, body) {
- self.model.collection.remove(self.model);
+ success: (err, response, body) => {
+ this.parent.update("Files");
+ this.model.collection.remove(this.model);
},
- error: function (err, response, body) {
+ error: (err, response, body) => {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
body = JSON.parse(body);
+ let errorModal = $(modals.errorHtml(body.Reason, body.Message)).modal();
}
});
});
diff --git a/client/views/file-browser-view.js b/client/views/file-browser-view.js
deleted file mode 100644
index 78df88eb52..0000000000
--- a/client/views/file-browser-view.js
+++ /dev/null
@@ -1,1536 +0,0 @@
-/*
-StochSS is a platform for simulating biochemical systems
-Copyright (C) 2019-2021 StochSS developers.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-let jstree = require('jstree');
-let path = require('path');
-let $ = require('jquery');
-let _ = require('underscore');
-//support files
-let app = require('../app');
-let modals = require('../modals');
-//models
-let Model = require('../models/model');
-//views
-let View = require('ampersand-view');
-//templates
-let template = require('../templates/includes/fileBrowserView.pug');
-
-module.exports = View.extend({
- template: template,
- events: {
- 'click [data-hook=collapse-browse-files]' : 'changeCollapseButtonText',
- 'click [data-hook=refresh-jstree]' : 'refreshJSTree',
- 'click [data-hook=options-for-node]' : 'showContextMenuForNode',
- 'click [data-hook=new-directory]' : 'handleCreateDirectoryClick',
- 'click [data-hook=browser-new-workflow-group]' : 'handleCreateWorkflowGroupClick',
- 'click [data-hook=browser-new-model]' : 'handleCreateModelClick',
- 'click [data-hook=browser-new-domain]' : 'handelCreateDomainClick',
- 'click [data-hook=browser-existing-model]' : 'handleAddExistingModelClick',
- 'click [data-hook=upload-file-btn-bf]' : 'handleUploadFileClick',
- 'click [data-hook=file-browser-help]' : function () {
- let modal = $(modals.operationInfoModalHtml('file-browser')).modal();
- },
- },
- initialize: function (attrs, options) {
- View.prototype.initialize.apply(this, arguments)
- var self = this
- this.root = "none"
- if(attrs && attrs.root){
- this.root = attrs.root
- }
- this.ajaxData = {
- "url" : function (node) {
- if(node.parent === null){
- var endpoint = path.join(app.getApiPath(), "file/browser-list")+"?path="+self.root
- if(self.root !== "none") {
- endpoint += "&isRoot=True"
- }
- return endpoint
- }
- return path.join(app.getApiPath(), "file/browser-list")+"?path="+ node.original._path
- },
- "dataType" : "json",
- "data" : function (node) {
- return { 'id' : node.id}
- },
- }
- this.treeSettings = {
- 'plugins': ['types', 'wholerow', 'changed', 'contextmenu', 'dnd'],
- 'core': {'multiple' : false, 'animation': 0,
- 'check_callback': function (op, node, par, pos, more) {
- if(op === "rename_node" && self.validateName(pos, true) !== ""){
- document.querySelector("#renameSpecialCharError").style.display = "block"
- setTimeout(function () {
- document.querySelector("#renameSpecialCharError").style.display = "none"
- }, 5000)
- return false
- }
- if(op === 'move_node' && more && more.core) {
- var newDir = par.original._path !== "/" ? par.original._path : ""
- var file = node.original._path.split('/').pop()
- var oldPath = node.original._path
- let queryStr = "?srcPath="+oldPath+"&dstPath="+path.join(newDir, file)
- var endpoint = path.join(app.getApiPath(), "file/move")+queryStr
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- node.original._path = path.join(newDir, file)
- if((node.type === "nonspatial" || node.type === "spatial") && (oldPath.includes("trash") || newDir.includes("trash"))) {
- self.updateParent("Archive");
- }else if(node.type !== "notebook" || node.original._path.includes(".wkgp") || newDir.includes(".wkgp")) {
- self.updateParent(node.type)
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body)
- if(par.type === 'root'){
- $('#models-jstree-view').jstree().refresh()
- }else{
- $('#models-jstree-view').jstree().refresh_node(par);
- }
- }
- });
- }else{
- let isMove = op === 'move_node'
- let validSrc = Boolean(node && node.type && node.original && node.original.text !== "trash")
- let validDst = Boolean(more && more.ref && more.ref.type && more.ref.original)
- let validDsts = ["root", "folder"]
- let isModel = Boolean(validSrc && (node.type === "nonspatial" || node.type === "spatial"))
- let isWorkflow = Boolean(validSrc && node.type === "workflow")
- let isWkgp = Boolean(validSrc && node.type === "workflow-group")
- let isNotebook = Boolean(validSrc && node.type === "notebook")
- let isOther = Boolean(validSrc && !isModel && !isWorkflow && !isWkgp && !isNotebook)
- let trashAction = Boolean((validSrc && node.original._path.includes("trash")) || (validDst && more.ref.original.text === "trash"))
- // Check if files are being move directly into the trash and remain static with respect to the trash
- if(isMove && validDst && path.dirname(more.ref.original._path).includes("trash")) { return false }
- if(isMove && validSrc && validDst && node.original._path.includes("trash") && more.ref.original.text === 'trash') { return false }
- // Check if workflow is running
- if(isMove && isWorkflow && node.original._status && node.original._status === "running") { return false };
- // Check if model, workflow, or workflow group is moving to or from trash
- if(isMove && (isModel || isWorkflow) && !trashAction) { return false };
- if(isMove && isWkgp && !(self.parent.model.newFormat && trashAction)) { return false };
- // Check if model, workflow, or workflow group is moving from trash to the correct location
- if(isMove && validSrc && node.original._path.includes("trash")) {
- if(isWkgp && (!self.parent.model.newFormat || (validDst && more.ref.type !== "root"))) { return false };
- if(isWorkflow && validDst && more.ref.type !== "workflow-group") { return false };
- if(isModel && validDst) {
- if(!self.parent.model.newFormat && more.ref.type !== "root") { return false };
- let length = node.original.text.split(".").length;
- let modelName = node.original.text.split(".").slice(0, length - 1).join(".")
- if(self.parent.model.newFormat && (more.ref.type !== "workflow-group" || !more.ref.original.text.startsWith(modelName))) { return false };
- }
- }
- // Check if notebook or other file is moving to a valid location.
- if(isOther && validDst && !validDsts.includes(more.ref.type)) { return false };
- validDsts.push("workflow-group")
- if(isNotebook && validDst && !validDsts.includes(more.ref.type)) { return false };
- if(isMove && validDst && validDsts.includes(more.ref.type)){
- if(!more.ref.state.loaded) { return false };
- var exists = false
- var BreakException = {}
- var text = node.text
- if(!isNaN(text.split(' ').pop().split('.').join(""))){
- text = text.replace(text.split(' ').pop(), '').trim()
- }
- if(more.ref.text !== "trash"){
- try{
- more.ref.children.forEach(function (child) {
- var child_node = $('#models-jstree-view').jstree().get_node(child)
- exists = child_node.text === text
- if(exists) { throw BreakException; };
- })
- }catch { return false; };
- }
- }
- if(isMove && more && (pos != 0 || more.pos !== "i") && !more.core) { return false }
- return true
- }
- },
- 'themes': {'stripes': true, 'variant': 'large'},
- 'data': self.ajaxData,
- },
- 'types' : {
- 'root' : {"icon": "jstree-icon jstree-folder"},
- 'folder' : {"icon": "jstree-icon jstree-folder"},
- 'spatial' : {"icon": "jstree-icon jstree-file"},
- 'nonspatial' : {"icon": "jstree-icon jstree-file"},
- 'project' : {"icon": "jstree-icon jstree-file"},
- 'workflow-group' : {"icon": "jstree-icon jstree-folder"},
- 'workflow' : {"icon": "jstree-icon jstree-file"},
- 'notebook' : {"icon": "jstree-icon jstree-file"},
- 'domain' : {"icon": "jstree-icon jstree-file"},
- 'sbml-model' : {"icon": "jstree-icon jstree-file"},
- 'other' : {"icon": "jstree-icon jstree-file"},
- },
- }
- this.setupJstree()
- },
- render: function () {
- View.prototype.render.apply(this, arguments)
- var self = this;
- this.nodeForContextMenu = "";
- this.jstreeIsLoaded = false
- window.addEventListener('pageshow', function (e) {
- var navType = window.performance.navigation.type
- if(navType === 2){
- window.location.reload()
- }
- });
- },
- updateParent: function (type) {
- let models = ["nonspatial", "spatial", "sbml", "model"]
- let workflows = ["workflow", "notebook"]
- if(models.includes(type)) {
- this.parent.update("Model")
- }else if(workflows.includes(type)) {
- this.parent.update("Workflow")
- }else if(type === "workflow-group") {
- this.parent.update("WorkflowGroup")
- }else if(type === "Archive") {
- this.parent.update(type);
- }
- },
- refreshJSTree: function () {
- this.jstreeIsLoaded = false
- $('#models-jstree-view').jstree().deselect_all(true)
- $('#models-jstree-view').jstree().refresh()
- },
- refreshInitialJSTree: function () {
- var self = this;
- var count = $('#models-jstree-view').jstree()._model.data['#'].children.length;
- if(count == 0) {
- self.refreshJSTree();
- setTimeout(function () {
- self.refreshInitialJSTree();
- }, 3000);
- }
- },
- selectNode: function (node, fileName) {
- let self = this
- if(!this.jstreeIsLoaded || !$('#models-jstree-view').jstree().is_loaded(node) && $('#models-jstree-view').jstree().is_loading(node)) {
- setTimeout(_.bind(self.selectNode, self, node, fileName), 1000);
- }else{
- node = $('#models-jstree-view').jstree().get_node(node)
- var child = ""
- for(var i = 0; i < node.children.length; i++) {
- var child = $('#models-jstree-view').jstree().get_node(node.children[i])
- if(child.original.text === fileName) {
- $('#models-jstree-view').jstree().select_node(child)
- let optionsButton = $(self.queryByHook("options-for-node"))
- if(!self.nodeForContextMenu){
- optionsButton.prop('disabled', false)
- }
- optionsButton.text("Actions for " + child.original.text)
- self.nodeForContextMenu = child;
- break
- }
- }
- }
- },
- handleUploadFileClick: function (e) {
- let type = e.target.dataset.type
- this.uploadFile(undefined, type)
- },
- uploadFile: function (o, type) {
- var self = this
- if(document.querySelector('#uploadFileModal')) {
- document.querySelector('#uploadFileModal').remove()
- }
- if(this.browser == undefined) {
- this.browser = app.getBrowser();
- }
- if(this.isSafariV14Plus == undefined){
- this.isSafariV14Plus = (this.browser.name === "Safari" && this.browser.version >= 14)
- }
- let modal = $(modals.uploadFileHtml(type, this.isSafariV14Plus)).modal();
- let uploadBtn = document.querySelector('#uploadFileModal .upload-modal-btn');
- let fileInput = document.querySelector('#uploadFileModal #fileForUpload');
- let input = document.querySelector('#uploadFileModal #fileNameInput');
- let fileCharErrMsg = document.querySelector('#uploadFileModal #fileSpecCharError')
- let nameEndErrMsg = document.querySelector('#uploadFileModal #fileNameInputEndCharError')
- let nameCharErrMsg = document.querySelector('#uploadFileModal #fileNameInputSpecCharError')
- let nameUsageMsg = document.querySelector('#uploadFileModal #fileNameUsageMessage')
- fileInput.addEventListener('change', function (e) {
- let fileErr = !fileInput.files.length ? "" : self.validateName(fileInput.files[0].name)
- let nameErr = self.validateName(input.value)
- if(!fileInput.files.length) {
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'none'
- }else if(fileErr === "" || (Boolean(input.value) && nameErr === "")){
- uploadBtn.disabled = false
- fileCharErrMsg.style.display = 'none'
- }else{
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'block'
- }
- })
- input.addEventListener("input", function (e) {
- let fileErr = !fileInput.files.length ? "" : self.validateName(fileInput.files[0].name)
- let nameErr = self.validateName(input.value)
- if(!fileInput.files.length) {
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'none'
- }else if(fileErr === "" || (Boolean(input.value) && nameErr === "")){
- uploadBtn.disabled = false
- fileCharErrMsg.style.display = 'none'
- }else{
- uploadBtn.disabled = true
- fileCharErrMsg.style.display = 'block'
- }
- nameCharErrMsg.style.display = nameErr === "both" || nameErr === "special" ? "block" : "none"
- nameEndErrMsg.style.display = nameErr === "both" || nameErr === "forward" ? "block" : "none"
- nameUsageMsg.style.display = nameErr !== "" ? "block" : "none"
- });
- uploadBtn.addEventListener('click', function (e) {
- let file = fileInput.files[0]
- var fileinfo = {"type":type,"name":"","path":self.parent.model.directory}
- if(o && o.original){
- fileinfo.path = o.original._path
- }
- if(Boolean(input.value) && self.validateName(input.value) === ""){
- let name = input.value.trim()
- if(file.name.endsWith(".mdl") || (type === "model" && file.name.endsWith(".json"))){
- fileinfo.name = name.split('/').pop()
- }else if(file.name.endsWith(".sbml") || (type === "sbml" && file.name.endsWith(".xml"))){
- fileinfo.name = name.split('/').pop()
- }else{
- fileinfo.name = name
- }
- }
- let formData = new FormData()
- formData.append("datafile", file)
- formData.append("fileinfo", JSON.stringify(fileinfo))
- let endpoint = path.join(app.getApiPath(), 'file/upload');
- if(Boolean(input.value) && self.validateName(input.value) === "" && fileinfo.name !== input.value.trim()){
- let message = "Warning: Models are saved directly in StochSS Projects and cannot be saved to the "+input.value.trim().split("/")[0]+" directory in the project.
Do you wish to save your model directly in your project?
"
- let warningModal = $(modals.newProjectModelWarningHtml(message)).modal()
- let yesBtn = document.querySelector('#newProjectModelWarningModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- warningModal.modal('hide')
- self.openUploadRequest(endpoint, formData, file, type, o)
- })
- }else{
- self.openUploadRequest(endpoint, formData, file, type, o)
- }
- modal.modal('hide')
- })
- },
- openUploadRequest: function (endpoint, formData, file, type, o) {
- let self = this
- app.postXHR(endpoint, formData, {
- success: function (err, response, body) {
- body = JSON.parse(body);
- if(o){
- var node = $('#models-jstree-view').jstree().get_node(o.parent);
- if(node.type === "root" || node.type === "#"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- }else{
- self.refreshJSTree();
- }
- if(body.file.endsWith(".mdl") || body.file.endsWith(".smdl") ||body.file.endsWith(".sbml")) {
- self.updateParent("model")
- }
- if(body.errors.length > 0){
- let errorModal = $(modals.uploadFileErrorsHtml(file.name, type, body.message, body.errors)).modal();
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- let zipErrorModal = $(modals.projectExportErrorHtml(body.Reason, body.Message)).modal()
- }
- }, false);
- },
- deleteFile: function (o) {
- var fileType = o.type
- if(fileType === "nonspatial")
- fileType = "model";
- else if(fileType === "spatial")
- fileType = "spatial model"
- else if(fileType === "sbml-model")
- fileType = "sbml model"
- else if(fileType === "other")
- fileType = "file"
- var self = this
- if(document.querySelector('#deleteFileModal')) {
- document.querySelector('#deleteFileModal').remove()
- }
- let modal = $(modals.deleteFileHtml(fileType)).modal();
- let yesBtn = document.querySelector('#deleteFileModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- var endpoint = path.join(app.getApiPath(), "file/delete")+"?path="+o.original._path
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree-view').jstree().get_node(o.parent);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- }
- });
- modal.modal('hide')
- if(o.type !== "notebook" || o.original._path.includes(".wkgp")) {
- self.updateParent(o.type)
- }
- });
- },
- duplicateFileOrDirectory: function(o, type) {
- var self = this;
- var parentID = o.parent;
- var queryStr = "?path="+o.original._path
- if(!type && o.original.type === 'folder'){
- type = "directory"
- }else if(!type && o.original.type === 'workflow'){
- type = "workflow"
- }else if(!type){
- type = "file"
- }
- if(type === "directory"){
- var identifier = "directory/duplicate"
- }else if(type === "workflow" || type === "wkfl_model"){
- var timeStamp = type === "workflow" ? this.getTimeStamp() : "None"
- var identifier = "workflow/duplicate"
- queryStr = queryStr.concat("&target="+type+"&stamp="+timeStamp)
- }else{
- var identifier = "file/duplicate"
- }
- var endpoint = path.join(app.getApiPath(), identifier)+queryStr
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree-view').jstree().get_node(parentID);
- self.refreshJSTree()
- if(type === "workflow"){
- var message = ""
- if(body.error){
- message = body.error
- }else{
- message = "The model for "+body.File+" is located here: "+body.mdlPath+""
- }
- let modal = $(modals.duplicateWorkflowHtml(body.File, message)).modal()
- }
- self.selectNode(node, body.File)
- if(o.type !== "notebook" || o.original._path.includes(".wkgp")) {
- self.updateParent(o.type)
- }
- }
- });
- },
- getTimeStamp: function () {
- var date = new Date();
- var year = date.getFullYear();
- var month = date.getMonth() + 1;
- if(month < 10){
- month = "0" + month
- }
- var day = date.getDate();
- if(day < 10){
- day = "0" + day
- }
- var hours = date.getHours();
- if(hours < 10){
- hours = "0" + hours
- }
- var minutes = date.getMinutes();
- if(minutes < 10){
- minutes = "0" + minutes
- }
- var seconds = date.getSeconds();
- if(seconds < 10){
- seconds = "0" + seconds
- }
- return "_" + month + day + year + "_" + hours + minutes + seconds;
- },
- toSpatial: function (o) {
- var self = this;
- var parentID = o.parent;
- var endpoint = path.join(app.getApiPath(), "model/to-spatial")+"?path="+o.original._path;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree-view').jstree().get_node(parentID);
- self.refreshJSTree()
- self.updateParent("spatial")
- self.selectNode(node, body.File)
- }
- });
- },
- toModel: function (o, from) {
- var self = this;
- var parentID = o.parent;
- if(from === "Spatial"){
- var identifier = "spatial/to-model"
- }else{
- var identifier = "sbml/to-model"
- }
- let endpoint = path.join(app.getApiPath(), identifier)+"?path="+o.original._path;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree-view').jstree().get_node(parentID);
- self.refreshJSTree()
- self.selectNode(node, body.File)
- if(from === "SBML" && body.errors.length > 0){
- var title = ""
- var msg = body.message
- var errors = body.errors
- let modal = $(modals.sbmlToModelHtml(msg, errors)).modal();
- }else{
- self.updateParent("nonspatial")
- }
- }
- });
- },
- toNotebook: function (o, type) {
- let self = this
- var endpoint = path.join(app.getApiPath(), "workflow/notebook")+"?type=none&path="+o.original._path
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree-view').jstree().get_node(o.parent)
- if(node.type === 'root'){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- var notebookPath = path.join(app.getBasePath(), "notebooks", body.FilePath)
- self.selectNode(node, body.File)
- window.open(notebookPath, '_blank')
- }
- });
- },
- toSBML: function (o) {
- var self = this;
- var parentID = o.parent;
- var endpoint = path.join(app.getApiPath(), "model/to-sbml")+"?path="+o.original._path;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- var node = $('#models-jstree-view').jstree().get_node(parentID);
- self.refreshJSTree()
- self.selectNode(node, body.File)
- }
- });
- },
- renameNode: function (o) {
- var self = this
- var text = o.text;
- var parent = $('#models-jstree-view').jstree().get_node(o.parent)
- var extensionWarning = $(this.queryByHook('extension-warning'));
- var nameWarning = $(this.queryByHook('rename-warning'));
- extensionWarning.collapse('show')
- $('#models-jstree-view').jstree().edit(o, null, function(node, status) {
- if(text != node.text){
- let name = node.type === "root" ? node.text + ".proj" : node.text
- var endpoint = path.join(app.getApiPath(), "file/rename")+"?path="+ o.original._path+"&name="+name
- app.getXHR(endpoint, {
- always: function (err, response, body) {
- if(parent.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(parent);
- }
- },
- success: function (err, response, body) {
- if(body.changed) {
- nameWarning.text(body.message);
- nameWarning.collapse('show');
- window.scrollTo(0,0);
- setTimeout(_.bind(self.hideNameWarning, self), 10000);
- }
- node.original._path = body._path;
- }
- });
- }
- extensionWarning.collapse('hide');
- nameWarning.collapse('hide');
- });
- },
- hideNameWarning: function () {
- $(this.queryByHook('rename-warning')).collapse('hide')
- },
- getExportData: function (o, asZip) {
- var self = this;
- let nodeType = o.original.type
- let isJSON = nodeType === "sbml-model" ? false : true
- if(nodeType === "sbml-model"){
- var dataType = "plain-text"
- var identifier = "file/download"
- }else if(nodeType === "domain") {
- var dataType = "json"
- var identifier = "spatial-model/load-domain"
- }else if(asZip) {
- var dataType = "zip"
- var identifier = "file/download-zip"
- }else{
- var dataType = "json"
- var identifier = "file/json-data"
- }
- if(nodeType === "domain") {
- var queryStr = "?domain_path=" + o.original._path
- }else{
- var queryStr = "?path="+o.original._path
- if(dataType === "json"){
- queryStr = queryStr.concat("&for=None")
- }else if(dataType === "zip"){
- queryStr = queryStr.concat("&action=generate")
- }
- }
- var endpoint = path.join(app.getApiPath(), identifier)+queryStr
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(dataType === "json") {
- let data = nodeType === "domain" ? body.domain : body;
- self.exportToJsonFile(data, o.original.text);
- }else if(dataType === "zip") {
- var node = $('#models-jstree-view').jstree().get_node(o.parent);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- self.exportToZipFile(body.Path);
- }else{
- self.exportToFile(body, o.original.text);
- }
- }
- });
- },
- exportToJsonFile: function (fileData, fileName) {
- let dataStr = JSON.stringify(fileData);
- let dataURI = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
- let exportFileDefaultName = fileName
-
- let linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataURI);
- linkElement.setAttribute('download', exportFileDefaultName);
- linkElement.click();
- },
- exportToFile: function (fileData, fileName) {
- let dataURI = 'data:text/plain;charset=utf-8,' + encodeURIComponent(fileData);
-
- let linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataURI);
- linkElement.setAttribute('download', fileName);
- linkElement.click();
- },
- exportToZipFile: function (o) {
- var targetPath = o
- if(o.original){
- targetPath = o.original._path
- }
- var endpoint = path.join(app.getBasePath(), "/files", targetPath);
- window.open(endpoint)
- },
- validateName(input, rename = false) {
- var error = ""
- if(input.endsWith('/')) {
- error = 'forward'
- }
- var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"
- if(rename) {
- invalidChars += "/"
- }
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both"
- }
- }
- return error
- },
- newWorkflowGroup: function (o) {
- var self = this
- if(document.querySelector("#newWorkflowGroupModal")) {
- document.querySelector("#newWorkflowGroupModal").remove()
- }
- let modal = $(modals.newWorkflowGroupModalHtml()).modal();
- let okBtn = document.querySelector('#newWorkflowGroupModal .ok-model-btn');
- let input = document.querySelector('#newWorkflowGroupModal #workflowGroupNameInput');
- input.addEventListener("keyup", function (event) {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newWorkflowGroupModal #workflowGroupNameInputEndCharError')
- var charErrMsg = document.querySelector('#newWorkflowGroupModal #workflowGroupNameInputSpecCharError')
- let error = self.validateName(input.value)
- okBtn.disabled = error !== ""
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
- });
- okBtn.addEventListener("click", function (e) {
- if(Boolean(input.value)) {
- modal.modal('hide')
- var parentPath = self.parent.model.directory
- if(o && o.original && o.original._path !== "/") {
- parentPath = o.original._path
- }
- var workflowGroupName = input.value.trim() + ".wkgp"
- var workflowGroupPath = path.join(parentPath, workflowGroupName)
- let endpoint = path.join(app.getApiPath(), "project/new-workflow-group")+"?path="+workflowGroupPath
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(o){//directory was created with context menu option
- var node = $('#models-jstree-view').jstree().get_node(o);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- }else{//directory was created with create directory button
- self.refreshJSTree();
- }
- self.updateParent('workflow-group');
- },
- error: function (err, response, body) {
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- }
- })
- },
- handleAddExistingModelClick: function () {
- this.addExistingModel(undefined)
- },
- addExistingModel: function (o) {
- var self = this
- if(document.querySelector('#newProjectModelModal')){
- document.querySelector('#newProjectModelModal').remove()
- }
- let mdlListEP = path.join(app.getApiPath(), 'project/add-existing-model') + "?path="+self.parent.model.directory
- app.getXHR(mdlListEP, {
- always: function (err, response, body) {
- let modal = $(modals.newProjectModelHtml(body.files)).modal();
- let okBtn = document.querySelector('#newProjectModelModal .ok-model-btn');
- let select = document.querySelector('#newProjectModelModal #modelFileInput');
- let location = document.querySelector('#newProjectModelModal #modelPathInput');
- select.addEventListener("change", function (e) {
- okBtn.disabled = e.target.value && body.paths[e.target.value].length >= 2;
- if(body.paths[e.target.value].length >= 2) {
- var locations = body.paths[e.target.value].map(function (path) {
- return ``;
- });
- locations.unshift(``);
- locations = locations.join(" ");
- $("#modelPathInput").find('option').remove().end().append(locations);
- $("#location-container").css("display", "block");
- }else{
- $("#location-container").css("display", "none");
- $("#modelPathInput").find('option').remove().end();
- }
- });
- location.addEventListener("change", function (e) {
- okBtn.disabled = !Boolean(e.target.value);
- });
- okBtn.addEventListener("click", function (e) {
- modal.modal('hide');
- let mdlPath = body.paths[select.value].length < 2 ? body.paths[select.value][0] : location.value;
- let queryString = "?path="+self.parent.model.directory+"&mdlPath="+mdlPath;
- let endpoint = path.join(app.getApiPath(), 'project/add-existing-model') + queryString;
- app.postXHR(endpoint, null, {
- success: function (err, response, body) {
- let successModal = $(modals.newProjectModelSuccessHtml(body.message)).modal();
- self.updateParent("model");
- self.refreshJSTree();
- },
- error: function (err, response, body) {
- let errorModal = $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- });
- }
- });
- },
- newWorkflow: function (o, type) {
- let self = this;
- let model = new Model({
- directory: o.original._path
- });
- app.getXHR(model.url(), {
- success: function (err, response, body) {
- model.set(body);
- model.updateValid();
- if(model.valid){
- app.newWorkflow(self, o.original._path, o.type === "spatial", type);
- }else{
- let title = "Model Errors Detected";
- let endpoint = path.join(app.getBasePath(), "stochss/models/edit") + '?path=' + model.directory + '&validate';
- let message = 'Errors were detected in you model click here to fix your model';
- $(modals.modelErrorHtml(title, message)).modal();
- }
- }
- });
- },
- addModel: function (parentPath, modelName, message) {
- var endpoint = path.join(app.getBasePath(), "stochss/models/edit")
- if(parentPath.endsWith(".proj")) {
- let queryString = "?path=" + parentPath + "&mdlFile=" + modelName
- let newMdlEP = path.join(app.getApiPath(), "project/new-model") + queryString
- app.getXHR(newMdlEP, {
- success: function (err, response, body) {
- endpoint += "?path="+body.path;
- window.location.href = endpoint;
- },
- error: function (err, response, body) {
- let title = "Model Already Exists";
- let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
- }
- });
- }else{
- let modelPath = path.join(parentPath, modelName)
- let queryString = "?path="+modelPath+"&message="+message;
- endpoint += queryString
- let existEP = path.join(app.getApiPath(), "model/exists")+queryString
- app.getXHR(existEP, {
- always: function (err, response, body) {
- if(body.exists) {
- let title = "Model Already Exists";
- let message = "A model already exists with that name";
- let errorModel = $(modals.newProjectOrWorkflowGroupErrorHtml(title, message)).modal();
- }else{
- window.location.href = endpoint;
- }
- }
- });
- }
- },
- newModelOrDirectory: function (o, isModel, isSpatial) {
- var self = this
- if(document.querySelector('#newModalModel')) {
- document.querySelector('#newModalModel').remove()
- }
- let modal = $(modals.renderCreateModalHtml(isModel, isSpatial)).modal();
- let okBtn = document.querySelector('#newModalModel .ok-model-btn');
- let input = document.querySelector('#newModalModel #modelNameInput');
- input.addEventListener("keyup", function (event) {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", function (e) {
- var endErrMsg = document.querySelector('#newModalModel #modelNameInputEndCharError')
- var charErrMsg = document.querySelector('#newModalModel #modelNameInputSpecCharError')
- let error = self.validateName(input.value)
- okBtn.disabled = error !== "" || input.value.trim() === ""
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
- });
- okBtn.addEventListener('click', function (e) {
- if (Boolean(input.value)) {
- modal.modal('hide')
- var parentPath = self.parent.model.directory
- if(o && o.original && o.original._path !== "/"){
- parentPath = o.original._path
- }
- if(isModel) {
- let ext = isSpatial ? ".smdl" : ".mdl";
- let modelName = !o || (o && o.type === "root") ? input.value.trim().split("/").pop() + ext : input.value.trim() + ext;
- let message = modelName !== input.value.trim() + ext?
- "Warning: Models are saved directly in StochSS Projects and cannot be saved to the "+input.value.trim().split("/")[0]+" directory in the project.
Your model will be saved directly in your project.
" : ""
- if(message){
- let warningModal = $(modals.newProjectModelWarningHtml(message)).modal()
- let yesBtn = document.querySelector('#newProjectModelWarningModal .yes-modal-btn');
- yesBtn.addEventListener('click', function (e) {
- warningModal.modal('hide');
- self.addModel(parentPath, modelName, message);
- });
- }else{
- self.addModel(parentPath, modelName, message);
- }
- }else{
- let dirName = input.value.trim();
- let endpoint = path.join(app.getApiPath(), "directory/create")+"?path="+path.join(parentPath, dirName);
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(o){//directory was created with context menu option
- var node = $('#models-jstree-view').jstree().get_node(o);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- }else{//directory was created with create directory button
- self.refreshJSTree();
- }
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- let errorModal = $(modals.newDirectoryErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- }
- }
- });
- },
- handleCreateDirectoryClick: function (e) {
- this.newModelOrDirectory(undefined, false, false);
- },
- handleCreateWorkflowGroupClick: function (e) {
- this.newWorkflowGroup(undefined)
- },
- handleCreateModelClick: function (e) {
- let isSpatial = e.target.dataset.type === "spatial"
- this.newModelOrDirectory(undefined, true, isSpatial);
- },
- handelCreateDomainClick: function (e) {
- let queryStr = "?domainPath=" + this.parent.model.directory + "&new"
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- },
- handleExtractModelClick: function (o) {
- let self = this
- let projectParent = path.dirname(this.parent.model.directory) === '.' ? "" : path.dirname(this.parent.model.directory)
- let queryString = "?srcPath="+o.original._path+"&dstPath="+path.join(projectParent, o.original._path.split('/').pop())
- let endpoint = path.join(app.getApiPath(), "project/extract-model")+queryString
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let successModel = $(modals.projectExportSuccessHtml("Model", body)).modal();
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- let successModel = $(modals.projectExportErrorHtml(body.Reason, body.message)).modal();
- }
- });
- },
- handleExportWorkflowClick: function (o) {
- let self = this
- let projectParent = path.dirname(this.parent.model.directory) === '.' ? "" : path.dirname(this.parent.model.directory)
- let queryString = "?srcPath="+o.original._path+"&dstPath="+path.join(projectParent, o.original._path.split('/').pop())
- let endpoint = path.join(app.getApiPath(), "project/extract-workflow")+queryString
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let successModel = $(modals.projectExportSuccessHtml("Workflow", body)).modal();
- },
- error: function (err, response, body) {
- body = JSON.parse(body);
- let successModel = $(modals.projectExportErrorHtml(body.Reason, body.message)).modal();
- }
- });
- },
- handleExportCombineClick: function (o, download) {
- let target = o.original._path
- this.parent.exportAsCombine()
- },
- showContextMenuForNode: function (e) {
- $('#models-jstree-view').jstree().show_contextmenu(this.nodeForContextMenu)
- },
- editWorkflowModel: function (o) {
- let endpoint = path.join(app.getApiPath(), "workflow/edit-model")+"?path="+o.original._path
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- if(body.error){
- let title = o.text + " Not Found";
- let message = body.error;
- let modal = $(modals.duplicateWorkflowHtml(title, message)).modal();
- }else{
- window.location.href = path.join(app.routePrefix, "models/edit")+"?path="+body.file;
- }
- }
- });
- },
- extractAll: function (o) {
- let self = this;
- let queryStr = "?path=" + o.original._path;
- let endpoint = path.join(app.getApiPath(), "file/unzip") + queryStr;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let node = $('#models-jstree-view').jstree().get_node(o.parent);
- if(node.type === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(node);
- }
- },
- error: function (err, response, body) {
- let modal = $(modals.newProjectModelErrorHtml(body.Reason, body.message)).modal();
- }
- });
- },
- publishNotebookPresentation: function (o) {
- let queryStr = "?path=" + o.original._path;
- let endpoint = path.join(app.getApiPath(), "notebook/presentation") + queryStr;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- let title = body.message;
- let linkHeaders = "Shareable Presentation";
- let links = body.links;
- $(modals.presentationLinks(title, linkHeaders, links)).modal();
- let copyBtn = document.querySelector('#presentationLinksModal #copy-to-clipboard');
- copyBtn.addEventListener('click', function (e) {
- let onFulfilled = (value) => {
- $("#copy-link-success").css("display", "inline-block");
- }
- let onReject = (reason) => {
- let msg = $("#copy-link-failed");
- msg.html(reason);
- msg.css("display", "inline-block");
- }
- app.copyToClipboard(links.presentation, onFulfilled, onReject);
- });
- },
- error: function (err, response, body) {
- $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- },
- setupJstree: function () {
- var self = this;
- $.jstree.defaults.contextmenu.items = (o, cb) => {
- let nodeType = o.original.type
- let zipTypes = ["workflow", "folder", "other", "root", "workflow-group"]
- let asZip = zipTypes.includes(nodeType)
- // refresh context menu option
- let refresh = {
- "Refresh" : {
- "label" : "Refresh",
- "_disabled" : false,
- "_class" : "font-weight-bold",
- "separator_before" : false,
- "separator_after" : o.text !== "trash",
- "action" : function (data) {
- if(nodeType === "root"){
- self.refreshJSTree();
- }else{
- $('#models-jstree-view').jstree().refresh_node(o);
- }
- }
- }
- }
- // For notebooks, workflows, sbml models, and other files
- let open = {
- "Open" : {
- "label" : "Open",
- "_disabled" : false,
- "_class" : "font-weight-bolder",
- "separator_before" : false,
- "separator_after" : true,
- "action" : function (data) {
- if(nodeType === "workflow"){
- window.location.href = path.join(app.getBasePath(), "stochss/workflow/edit")+"?path="+o.original._path+"&type=none";
- }else if(nodeType === "project"){
- window.location.href = path.join(app.getBasePath(), "stochss/project/manager")+"?path="+o.original._path
- }else if(nodeType === "domain") {
- let queryStr = "?domainPath=" + o.original._path
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- }else{
- if(nodeType === "notebook") {
- var identifier = "notebooks"
- }else if(nodeType === "sbml-model") {
- var identifier = "edit"
- }else{
- var identifier = "view"
- }
- window.open(path.join(app.getBasePath(), identifier, o.original._path));
- }
- }
- }
- }
- // project contect menu option
- let project = {
- "Add_Model" : {
- "label" : "Add Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "New_model" : {
- "label" : "New Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "spatial" : {
- "label" : "Spatial (beta)",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newModelOrDirectory(o, true, true);
- }
- },
- "nonspatial" : {
- "label" : "Non-Spatial",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newModelOrDirectory(o, true, false);
- }
- }
- }
- },
- "Existing Model" : {
- "label" : "Existing Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.addExistingModel(o)
- }
- }
- }
- }
- }
- // option for uploading files
- let uploadFile = {
- "Upload": {
- "label" : o.type === "root" ? "File" : "Upload File",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : o.type !== "root",
- "action" : function (data) {
- self.uploadFile(o, "file")
- }
- }
- }
- // all upload options
- let uploadAll = {
- "Upload" : {
- "label" : "Upload File",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : true,
- "submenu" : {
- "Model" : {
- "label" : "StochSS Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.uploadFile(o, "model")
- }
- },
- "SBML" : {
- "label" : "SBML Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.uploadFile(o, "sbml")
- }
- },
- "File" : uploadFile.Upload
- }
- }
- }
- // common to folder and root
- let commonFolder = {
- "New_Directory" : {
- "label" : "New Directory",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newModelOrDirectory(o, false, false);
- }
- },
- "New Domain" : {
- "label" : "New Domain (beta)",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- let queryStr = "?domainPath=" + o.original._path + "&new"
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- }
- },
- "Upload": o.type === "root" ? uploadAll.Upload : uploadFile.Upload
- }
- if(o.type === "root" || o.type === "workflow-group" || o.type === "workflow")
- var downloadLabel = "as .zip"
- else if(asZip)
- var downloadLabel = "Download as .zip"
- else
- var downloadLabel = "Download"
- let download = {
- "Download" : {
- "label" : downloadLabel,
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : !(o.type === "root" || o.type === "workflow-group" || o.type === "workflow"),
- "action" : function (data) {
- if(o.original.text.endsWith('.zip')){
- self.exportToZipFile(o);
- }else{
- self.getExportData(o, asZip)
- }
- }
- }
- }
- // download options for .zip and COMBINE
- let downloadWCombine = {
- "Download" : {
- "label" : "Download",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : true,
- "submenu" : {
- "DownloadAsZip": download.Download,
- "downloadAsCombine" : {
- "label" : "as COMBINE",
- "_disabled" : true,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.handleExportCombineClick(o, true)
- }
- }
- }
- }
- }
- // menu option for creating new workflows
- let newWorkflow = {
- "ensembleSimulation" : {
- "label" : "Ensemble Simulation",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newWorkflow(o, "Ensemble Simulation")
- }
- },
- "parameterSweep" : {
- "label" : "Parameter Sweep",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.newWorkflow(o, "Parameter Sweep")
- }
- },
- "jupyterNotebook" : {
- "label" : "Jupyter Notebook",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- window.location.href = path.join(app.getBasePath(), "stochss/workflow/selection")+"?path="+o.original._path;
- }
- }
- }
- // common to all models
- let commonModel = {
- "Edit" : {
- "label" : "Edit",
- "_disabled" : false,
- "_class" : "font-weight-bolder",
- "separator_before" : false,
- "separator_after" : true,
- "action" : function (data) {
- window.location.href = path.join(app.getBasePath(), "stochss/models/edit")+"?path="+o.original._path;
- }
- },
- "Extract" : {
- "label" : "Extract",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.handleExtractModelClick(o);
- }
- },
- "New Workflow" : {
- "label" : "New Workflow",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : o.type === "nonspatial" ? newWorkflow : {"jupyterNotebook":newWorkflow.jupyterNotebook}
- }
- }
- // convert options for non-spatial models
- let modelConvert = {
- "Convert" : {
- "label" : "Convert",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : true,
- "submenu" : {
- "Convert to Spatial" : {
- "label" : "To Spatial Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toSpatial(o)
- }
- },
- "Convert to SBML" : {
- "label" : "To SBML Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toSBML(o)
- }
- }
- }
- }
- }
- // convert options for spatial models
- let spatialConvert = {
- "Convert" : {
- "label" : "Convert",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : true,
- "submenu" : {
- "Convert to Model" : {
- "label" : "Convert to Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toModel(o, "Spatial");
- }
- }
- }
- }
- }
- // specific to workflows
- let workflow = {
- "Start/Restart Workflow" : {
- "label" : (o.original._status === "ready") ? "Start Workflow" : "Restart Workflow",
- "_disabled" : true,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
-
- }
- },
- "Stop Workflow" : {
- "label" : "Stop Workflow",
- "_disabled" : true,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
-
- }
- },
- "Model" : {
- "label" : "Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Edit" : {
- "label" : " Edit",
- "_disabled" : (!o.original._newFormat && o.original._status !== "ready"),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.editWorkflowModel(o)
- }
- },
- "Extract" : {
- "label" : "Extract",
- "_disabled" : (o.original._newFormat && !o.original._hasJobs),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.duplicateFileOrDirectory(o, "wkfl_model")
- }
- }
- }
- },
- "Extract" : {
- "label" : "Extract",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : true,
- "action" : function (data) {
- self.handleExportWorkflowClick(o)
- }
- },
- }
- // Specific to sbml files
- let sbml = {
- "Convert" : {
- "label" : "Convert",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "submenu" : {
- "Convert to Model" : {
- "label" : "To Model",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.toModel(o, "SBML");
- }
- }
- }
- }
- }
- // common to all type except root and trash
- let common = {
- "Rename" : {
- "label" : "Rename",
- "_disabled" : (o.type === "workflow" && o.original._status === "running"),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.renameNode(o);
- }
- },
- "Duplicate" : {
- "label" : (nodeType === "workflow") ? "Duplicate as new" : "Duplicate",
- "_disabled" : (nodeType === "project" || nodeType === "workflow-group"),
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.duplicateFileOrDirectory(o, null)
- }
- },
- "Delete" : {
- "label" : "Delete",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.deleteFile(o);
- }
- }
- }
- //Specific to zip archives
- let extractAll = {
- "extractAll" : {
- "label" : "Extract All",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.extractAll(o);
- }
- }
- }
- let notebook = {
- "publish" : {
- "label" : "Publish",
- "_disabled" : false,
- "separator_before" : false,
- "separator_after" : false,
- "action" : function (data) {
- self.publishNotebookPresentation(o);
- }
- }
- }
- if (o.type === 'root'){
- return $.extend(refresh, project, commonFolder, downloadWCombine, {"Rename": common.Rename})
- }
- if (o.text === "trash"){
- return refresh
- }
- if (o.original._path.includes(".proj/trash/")) {
- return {"Delete": common.Delete}
- }
- if (o.type === 'folder') {
- return $.extend(refresh, commonFolder, download, common)
- }
- if (o.type === 'spatial') {
- return $.extend(commonModel, spatialConvert, download, common)
- }
- if (o.type === 'nonspatial') {
- return $.extend(commonModel, modelConvert, download, common)
- }
- if (o.type === 'workflow-group') {
- return $.extend(refresh, downloadWCombine)
- }
- if (o.type === 'workflow') {
- return $.extend(open, workflow, downloadWCombine, common)
- }
- if (o.text.endsWith(".zip")) {
- return $.extend(open, extractAll, download, common)
- }
- if (o.type === 'notebook') {
- if(app.getBasePath() === "/") {
- return $.extend(open, download, common)
- }
- return $.extend(open, notebook, download, common)
- }
- if (o.type === 'other') {
- return $.extend(open, download, common)
- }
- if (o.type === 'sbml-model') {
- return $.extend(open, sbml, common)
- }
- if (o.type === "domain") {
- return $.extend(open, common)
- }
- }
- $(document).ready(function () {
- $(document).on('shown.bs.modal', function (e) {
- $('[autofocus]', e.target).focus();
- });
- $(document).on('dnd_start.vakata', function (data, element, helper, event) {
- $('#models-jstree-view').jstree().load_all()
- });
- $('#models-jstree-view').jstree(self.treeSettings).bind("loaded.jstree", function (event, data) {
- self.jstreeIsLoaded = true
- }).bind("refresh.jstree", function (event, data) {
- self.jstreeIsLoaded = true
- });
- $('#models-jstree-view').on('click.jstree', function(e) {
- var parent = e.target.parentElement
- var _node = parent.children[parent.children.length - 1]
- var node = $('#models-jstree-view').jstree().get_node(_node)
- if(_node.nodeName === "A" && $('#models-jstree-view').jstree().is_loaded(node) && node.type === "folder"){
- $('#models-jstree-view').jstree().refresh_node(node)
- }else{
- let optionsButton = $(self.queryByHook("options-for-node"))
- if(!self.nodeForContextMenu){
- optionsButton.prop('disabled', false)
- }
- optionsButton.text("Actions for " + node.original.text)
- self.nodeForContextMenu = node;
- }
- });
- $('#models-jstree-view').on('dblclick.jstree', function(e) {
- var file = e.target.text
- var node = $('#models-jstree-view').jstree().get_node(e.target)
- var _path = node.original._path;
- if(!_path.includes(".proj/trash/")){
- if(file.endsWith('.mdl') || file.endsWith('.smdl')){
- window.location.href = path.join(app.getBasePath(), "stochss/models/edit")+"?path="+_path;
- }else if(file.endsWith('.ipynb')){
- var notebookPath = path.join(app.getBasePath(), "notebooks", _path)
- window.open(notebookPath, '_blank')
- }else if(file.endsWith('.sbml')){
- var openPath = path.join(app.getBasePath(), "edit", _path)
- window.open(openPath, '_blank')
- }else if(file.endsWith('.proj')){
- window.location.href = path.join(app.getBasePath(), "stochss/project/manager")+"?path="+_path;
- }else if(file.endsWith('.wkfl')){
- window.location.href = path.join(app.getBasePath(), "stochss/workflow/edit")+"?path="+_path+"&type=none";
- }else if(file.endsWith('.domn')) {
- let queryStr = "?domainPath=" + _path
- window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
- }else if(node.type === "folder" && $('#models-jstree-view').jstree().is_open(node) && $('#models-jstree-view').jstree().is_loaded(node)){
- $('#models-jstree-view').jstree().refresh_node(node)
- }else if(node.type === "other"){
- var openPath = path.join(app.getBasePath(), "view", _path);
- window.open(openPath, "_blank");
- }
- }
- });
- })
- },
- changeCollapseButtonText: function (e) {
- app.changeCollapseButtonText(this, e);
- }
-});
\ No newline at end of file
diff --git a/client/views/jstree-view.js b/client/views/jstree-view.js
new file mode 100644
index 0000000000..5bed5b9f8b
--- /dev/null
+++ b/client/views/jstree-view.js
@@ -0,0 +1,1139 @@
+/*
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2021 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+let $ = require('jquery');
+let path = require('path');
+let _ = require('underscore');
+let jstree = require('jstree');
+//support files
+let app = require('../app');
+let modals = require('../modals');
+//models
+let Model = require('../models/model');
+//config
+let FileConfig = require('../file-config')
+let ProjectConfig = require('../project-config');
+//views
+let View = require('ampersand-view');
+//templates
+let template = require('../templates/includes/jstreeView.pug');
+
+module.exports = View.extend({
+ template: template,
+ events: {
+ 'click [data-hook=fb-new-directory]' : 'handleCreateDirectoryClick',
+ 'click [data-hook=fb-new-project]' : 'handleCreateProjectClick',
+ 'click [data-hook=fb-new-model]' : 'handleCreateModelClick',
+ 'click [data-hook=fb-new-domain]' : 'handleCreateDomain',
+ 'click [data-hook=fb-import-model]' : 'handleImportModelClick',
+ 'click [data-hook=upload-file-btn]' : 'handleUploadFileClick',
+ 'click [data-hook=options-for-node]' : 'showContextMenuForNode',
+ 'click [data-hook=refresh-jstree]' : 'handleRefreshJSTreeClick',
+ 'click [data-hook=fb-empty-trash]' : 'emptyTrash',
+ },
+ initialize: function (attrs, options) {
+ View.prototype.initialize.apply(this, arguments);
+ this.config = attrs.configKey === "file" ? FileConfig : ProjectConfig;
+ this.root = Boolean(attrs.root) ? attrs.root : "none";
+ this.nodeForContextMenu = null;
+ this.jstreeIsLoaded = false;
+ this.ajaxData = {
+ "url" : (node) => {
+ if(node.parent === null){
+ var queryStr = `?path=${this.root}`;
+ if(this.root !== "none") {
+ queryStr += "&isRoot=True";
+ }
+ return path.join(app.getApiPath(), "file/browser-list") + queryStr;
+ }
+ var queryStr = `?path=${node.original._path}`;
+ return path.join(app.getApiPath(), "file/browser-list") + queryStr;
+ },
+ "dataType" : "json",
+ "data" : (node) => {
+ return { 'id' : node.id};
+ }
+ }
+ this.treeSettings = {
+ 'plugins': ['types', 'wholerow', 'changed', 'contextmenu', 'dnd'],
+ 'core': {
+ 'multiple': false,
+ 'animation': 0,
+ 'check_callback': (op, node, par, pos, more) => {
+ if(op === "rename_node" && app.validateName(pos, {rename: true}) !== ""){
+ let err = $("#renameSpecialCharError");
+ err.css("display", "block");
+ setTimeout(() => {
+ err.css("display", "none");
+ }, 5000)
+ return false
+ }
+ if(op === 'move_node' && more && more.core) {
+ this.config.move(this, par, node);
+ }else if(op === "move_node") {
+ return this.config.validateMove(this, node, more, pos);
+ }
+ },
+ 'themes': {'stripes': true, 'variant': 'large'},
+ 'data': this.ajaxData
+ },
+ 'types': this.config.types
+ }
+ },
+ render: function (attrs, options) {
+ View.prototype.render.apply(this, arguments);
+ this.config.setup(this);
+ this.setupJstree(() => {
+ setTimeout(() => {
+ this.refreshInitialJSTree();
+ }, 3000);
+ });
+ },
+ addInputKeyupEvent: function (input, okBtn) {
+ input.addEventListener("keyup", (event) => {
+ if(event.keyCode === 13){
+ event.preventDefault();
+ okBtn.click();
+ }
+ });
+ },
+ addInputValidateEvent: function (input, okBtn, modalID, inputID, {inProject = false}={}) {
+ input.addEventListener("input", (e) => {
+ let endErrMsg = document.querySelector(`${modalID} ${inputID}EndCharError`);
+ let charErrMsg = document.querySelector(`${modalID} ${inputID}SpecCharError`);
+ let error = app.validateName(input.value, {saveAs: !inProject});
+ okBtn.disabled = error !== "" || input.value.trim() === "";
+ charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none";
+ endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none";
+ });
+ },
+ addModel: function (dirname, file) {
+ let queryStr = `?path=${path.join(dirname, file)}`;
+ let existEP = path.join(app.getApiPath(), "model/exists") + queryStr;
+ app.getXHR(existEP, {
+ always: (err, response, body) => {
+ if(body.exists) {
+ let title = "Model Already Exists";
+ let message = "A model already exists with that name";
+ this.reportError({Reason: title, Measage: message});
+ }else{
+ this.openModel(path.join(dirname, file));
+ }
+ }
+ });
+ },
+ addProjectModel: function (dirname, file) {
+ let queryStr = `?path=${dirname}&mdlFile=${file}`;
+ let newMdlEP = path.join(app.getApiPath(), "project/new-model") + queryStr;
+ app.getXHR(newMdlEP, {
+ success: (err, response, body) => {
+ this.openModel(body.path);
+ },
+ error: (err, response, body) => {
+ let title = "Model Already Exists";
+ let message = "A model already exists with that name";
+ this.reportError({Reason: title, Measage: message});
+ }
+ });
+ },
+ buildContextBase: function ({label="", disabled=false, bSep=false, aSep=false, action=(data)=>{}}={}) {
+ return {
+ label: label,
+ _disabled: disabled,
+ separator_before: bSep,
+ separator_after: aSep,
+ action: action
+ }
+ },
+ buildContextBaseWithClass: function ({label="", disabled=false, bSep=false, aSep=true, action=(data)=>{}}={}) {
+ return {
+ label: label,
+ _disabled: disabled,
+ _class: "font-weight-bolder",
+ separator_before: bSep,
+ separator_after: aSep,
+ action: action
+ }
+ },
+ buildContextWithSubmenus: function ({label="", disabled=false, bSep=false, aSep=false, submenu={}}={}) {
+ return {
+ label: label,
+ _disabled: disabled,
+ separator_before: bSep,
+ separator_after: aSep,
+ submenu: submenu
+ }
+ },
+ createDirectory: function (node, dirname) {
+ if(document.querySelector('#newDirectoryModal')) {
+ document.querySelector('#newDirectoryModal').remove();
+ }
+ let modal = $(modals.createDirectoryHtml()).modal('show');
+ let okBtn = document.querySelector('#newDirectoryModal .ok-model-btn');
+ let input = document.querySelector('#newDirectoryModal #directoryNameInput');
+ this.addInputKeyupEvent(input, okBtn);
+ this.addInputValidateEvent(input, okBtn, "#newDirectoryModal", "#directoryNameInput");
+ okBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ let queryStr = `?path=${path.join(dirname, input.value.trim())}`;
+ let endpoint = path.join(app.getApiPath(), "directory/create") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ this.refreshJSTree(node);
+ },
+ error: (err, response, body) => {
+ this.reportError(JSON.parse(body));
+ }
+ });
+ });
+ },
+ createDomain: function (dirname) {
+ let queryStr = `?domainPath=${dirname}&new`;
+ window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr;
+ },
+ createModel: function (node, dirname, isSpatial, inProject) {
+ if(document.querySelector('#newModelModal')) {
+ document.querySelector('#newModelModal').remove();
+ }
+ let modal = $(modals.createModelHtml(isSpatial)).modal();
+ let okBtn = document.querySelector('#newModelModal .ok-model-btn');
+ let input = document.querySelector('#newModelModal #modelNameInput');
+ this.addInputKeyupEvent(input, okBtn);
+ this.addInputValidateEvent(input, okBtn, "#newModelModal", "#modelNameInput", {inProject: inProject});
+ okBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ let file = `${input.value.trim()}.${isSpatial ? "smdl" : "mdl"}`;
+ if(inProject) {
+ this.addProjectModel(dirname, file);
+ }else{
+ this.addModel(dirname, file);
+ }
+ });
+ },
+ createProject: function (node, dirname) {
+ if(document.querySelector("#newProjectModal")) {
+ document.querySelector("#newProjectModal").remove();
+ }
+ let modal = $(modals.createProjectHtml()).modal();
+ let okBtn = document.querySelector('#newProjectModal .ok-model-btn');
+ let input = document.querySelector('#newProjectModal #projectNameInput');
+ this.addInputKeyupEvent(input, okBtn);
+ this.addInputValidateEvent(input, okBtn, "#newProjectModal", "#projectNameInput");
+ okBtn.addEventListener("click", (e) => {
+ modal.modal('hide');
+ let queryStr = `?path=${path.join(dirname, `${input.value.trim()}.proj`)}`;
+ let endpoint = path.join(app.getApiPath(), "project/new-project") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ this.config.updateParent(this, "project");
+ this.openProject(body.path);
+ },
+ error: (err, response, body) => {
+ this.reportError(body);
+ }
+ });
+ });
+ },
+ delete: function (node, type) {
+ if(document.querySelector('#deleteFileModal')) {
+ document.querySelector('#deleteFileModal').remove();
+ }
+ let modal = $(modals.deleteFileHtml(type)).modal();
+ let yesBtn = document.querySelector('#deleteFileModal .yes-modal-btn');
+ yesBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), "file/delete") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ let par = $('#files-jstree').jstree().get_node(node.parent);
+ this.refreshJSTree(par);
+ this.config.updateParent(node.type);
+ let actionsBtn = $(this.queryByHook("options-for-node"));
+ if(actionsBtn.text().endsWith(node.text)) {
+ actionsBtn.text("Actions");
+ actionsBtn.prop("disabled", true);
+ this.nodeForContextMenu = "";
+ }
+ },
+ error: (err, response, body) => {
+ this.reportError(JSON.parse(body));
+ }
+ });
+ });
+ },
+ duplicate: function (node, identifier, {cb=null, target=null, timeStamp=null}={}) {
+ var queryStr = `?path=${node.original._path}`;
+ if(target) {
+ queryStr += `&target=${target}`;
+ }
+ if(timeStamp) {
+ queryStr += `&stamp=${timeStamp}`;
+ }
+ let endpoint = path.join(app.getApiPath(), identifier) + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ var par = $('#files-jstree').jstree().get_node(node.parent);
+ if(this.root !== "none" && (["nonspatial", "spatial"].includes(node.type) || target === "wkfl_model")){
+ par = $('#files-jstree').jstree().get_node(par.parent);
+ var file = body.File.replace(node.type === "spatial" ? ".smdl" : ".mdl", ".wkgp");
+ }else{
+ var file = body.File;
+ }
+ this.refreshJSTree(par);
+ if(cb) {
+ cb(body);
+ }
+ this.selectNode(par, file);
+ this.config.updateParent(this, node.type);
+ }
+ });
+ },
+ emptyTrash: function (e) {
+ if(document.querySelector("#emptyTrashConfirmModal")) {
+ document.querySelector("#emptyTrashConfirmModal").remove();
+ }
+ let modal = $(modals.emptyTrashConfirmHtml()).modal();
+ let yesBtn = document.querySelector('#emptyTrashConfirmModal .yes-modal-btn');
+ yesBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ let endpoint = path.join(app.getApiPath(), "file/empty-trash") + "?path=trash";
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ this.refreshJSTree(null);
+ $(this.queryByHook('empty-trash')).prop('disabled', true);
+ }
+ });
+ });
+ },
+ exportToFile: function (fileData, fileName) {
+ let dataURI = `data:text/plain;charset=utf-8,${encodeURIComponent(fileData)}`;
+
+ let linkElement = document.createElement('a');
+ linkElement.setAttribute('href', dataURI);
+ linkElement.setAttribute('download', fileName);
+ linkElement.click();
+ },
+ exportToJsonFile: function (fileData, fileName) {
+ let dataStr = JSON.stringify(fileData);
+ let dataURI = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;
+ let exportFileDefaultName = fileName;
+
+ let linkElement = document.createElement('a');
+ linkElement.setAttribute('href', dataURI);
+ linkElement.setAttribute('download', exportFileDefaultName);
+ linkElement.click();
+ },
+ exportToZipFile: function (targetPath) {
+ let endpoint = path.join(app.getBasePath(), "/files", targetPath);
+ window.open(endpoint, "_blank");
+ },
+ extractAll: function (node) {
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), "file/unzip") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ let par = $('#files-jstree').jstree().get_node(node.parent);
+ this.refreshJSTree(par);
+ },
+ error: (err, response, body) => {
+ this.reportError(body);
+ }
+ });
+ },
+ getAddModelContext: function (node) {
+ let newModel = this.getNewModelContext(node, true);
+ return this.buildContextWithSubmenus({
+ label: "Add Model",
+ submenu: {
+ newModel: newModel,
+ existingModel: this.buildContextBase({
+ label: "Existing Model",
+ action: (data) => {
+ this.importModel(node, node.original._path);
+ }
+ })
+ }
+ });
+ },
+ getDeleteContext: function (node, type) {
+ return this.buildContextBase({
+ label: "Delete",
+ action: (data) => {
+ this.delete(node, type);
+ }
+ });
+ },
+ getDownloadContext: function (node, options, {asZip=false, withCombine=false}={}) {
+ if(withCombine) {
+ var label = "as .zip";
+ }else if(asZip) {
+ var label = "Download as .zip";
+ }else {
+ var label = "Download";
+ }
+ return this.buildContextBase({
+ label: label,
+ bSep: !withCombine,
+ action: (data) => {
+ this.getExportData(node, options);
+ }
+ });
+ },
+ getDownloadWCombineContext: function (node) {
+ let options = {dataType: "zip", identifier: "file/download-zip"};
+ let download = this.getDownloadContext(node, options, {withCombine: true});
+ return this.buildContextWithSubmenus({
+ label: "Download",
+ bSep: true,
+ submenu: {
+ downloadAsZip: download,
+ downloadAsCombine: this.buildContextBase({
+ label: "as COMBINE",
+ disabled: true,
+ })
+ }
+ });
+ },
+ getDuplicateContext: function (node, identifier, options) {
+ if(!options) {
+ options = {};
+ }
+ let label = Boolean(options.cb) ? "Duplicate as new" : "Duplicate";
+ return this.buildContextBase({
+ label: label,
+ action: (data) => {
+ this.duplicate(node, identifier, options);
+ }
+ });
+ },
+ getEditModelContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Edit",
+ action: (data) => {
+ this.openModel(node.original._path);
+ }
+ });
+ },
+ getExportData: function (node, {dataType="", identifier=""}={}) {
+ if(node.original.text.endsWith('.zip')) {
+ return this.exportToZipFile(node.original._path);
+ }
+ if(node.original.type === "domain") {
+ var queryStr = `?domain_path=${node.original._path}`;
+ }else{
+ var queryStr = `?path=${node.original._path}`;
+ if(dataType === "json"){
+ queryStr += "&for=None";
+ }else if(dataType === "zip"){
+ queryStr += "&action=generate";
+ }
+ }
+ let endpoint = path.join(app.getApiPath(), identifier) + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ if(dataType === "json") {
+ let data = node.original.type === "domain" ? body.domain : body;
+ this.exportToJsonFile(data, node.original.text);
+ }else if(dataType === "zip") {
+ let par = $('#files-jstree').jstree().get_node(node.parent);
+ this.refreshJSTree(par);
+ this.exportToZipFile(body.Path);
+ }else{
+ this.exportToFile(body, node.original.text);
+ }
+ },
+ error: (err, response, body) => {
+ if(dataType === "plain-text") {
+ body = JSON.parse(body);
+ }
+ this.reportError(body);
+ }
+ });
+ },
+ getExtractAllContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Extract All",
+ action: (data) => {
+ this.extractAll(node);
+ }
+ });
+ },
+ getFileUploadContext: function (node, inProject, {label="File"}={}) {
+ let dirname = node.original._path === "/" ? "" : node.original._path;
+ return this.buildContextBase({
+ label: label,
+ bSep: label !== "File",
+ action: (data) => {
+ this.uploadFile(node, dirname, "file", inProject);
+ }
+ });
+ },
+ getFullNewWorkflowContext: function (node) {
+ return this.buildContextWithSubmenus({
+ label: "New Workflow",
+ submenu: {
+ ensembleSimulation: this.buildContextBase({
+ label: "Ensemble Simulation",
+ action: (data) => {
+ this.newWorkflow(node, "Ensemble Simulation");
+ }
+ }),
+ parameterSweep: this.buildContextBase({
+ label: "Parameter Sweep",
+ action: (data) => {
+ this.newWorkflow(node, "Parameter Sweep");
+ }
+ }),
+ jupyterNotebook: this.getNotebookNewWorkflowContext(node)
+ }
+ });
+ },
+ getFullUploadContext: function (node, inProject) {
+ let dirname = node.original._path === "/" ? "" : node.original._path;
+ let file = this.getFileUploadContext(node, inProject);
+ return this.buildContextWithSubmenus({
+ label: "Upload",
+ bSep: true,
+ submenu: {
+ model: this.buildContextBase({
+ label: "StochSS Model",
+ action: (data) => {
+ this.uploadFile(node, dirname, "model", inProject);
+ }
+ }),
+ sbml: this.buildContextBase({
+ label: "SBML Model",
+ action: (data) => {
+ this.uploadFile(node, dirname, "sbml", inProject);
+ }
+ }),
+ file: file
+ }
+ });
+ },
+ getMdlConvertContext: function (node) {
+ return this.buildContextWithSubmenus({
+ label: "Convert",
+ submenu: {
+ toSpatial: this.buildContextBase({
+ label: "To Spatial Model",
+ action: (data) => {
+ this.config.toSpatial(this, node);
+ }
+ }),
+ toSBML: this.buildContextBase({
+ label: "To SBML Model",
+ action: (data) => {
+ this.config.toSBML(this, node);
+ }
+ })
+ }
+ });
+ },
+ getMoveToTrashContext: function (node, type) {
+ return this.buildContextBase({
+ label: "Move To Trash",
+ action: (data) => {
+ this.moveToTrash(node, type);
+ }
+ });
+ },
+ getOpenFileContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Open",
+ action: (data) => {
+ this.openFile(node.original._path);
+ }
+ });
+ },
+ getOpenNotebookContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Open",
+ action: (data) => {
+ this.openNotebook(node.original._path);
+ }
+ });
+ },
+ getOpenSBMLContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Open",
+ action: (data) => {
+ this.openSBML(node.original._path);
+ }
+ });
+ },
+ getOpenWorkflowContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Open",
+ action : (data) => {
+ this.openWorkflow(node.original._path);
+ }
+ });
+ },
+ getNewDirectoryContext: function (node) {
+ let dirname = node.original._path === "/" ? "" : node.original._path;
+ return this.buildContextBase({
+ label: "New Directory",
+ action: (data) => {
+ this.createDirectory(node, dirname);
+ }
+ });
+ },
+ getNewDomainContext: function (node) {
+ return this.buildContextBase({
+ label: "New Domain (beta)",
+ action: (data) => {
+ this.createDomain(node.original._path);
+ }
+ });
+ },
+ getNewModelContext: function (node, inProject) {
+ let dirname = node.original._path === "/" ? "" : node.original._path;
+ return this.buildContextWithSubmenus({
+ label: "New Model",
+ submenu: {
+ spatial: this.buildContextBase({
+ label: "Spatial (beta)",
+ action: (data) => {
+ this.createModel(node, dirname, true, inProject);
+ }
+ }),
+ nonspatial: this.buildContextBase({
+ label: "Non-Spatial",
+ action: (data) => {
+ this.createModel(node, dirname, false, inProject);
+ }
+ })
+ }
+ });
+ },
+ getNotebookNewWorkflowContext: function (node) {
+ return this.buildContextBase({
+ label: "Jupyter Notebook",
+ action: (data) => {
+ let queryStr = `?path=${node.original._path}`;
+ window.location.href = path.join(app.getBasePath(), "stochss/workflow/selection") + queryStr;
+ }
+ });
+ },
+ getPublishNotebookContext: function (node) {
+ return this.buildContextBase({
+ label: "Publish",
+ action: (data) => {
+ this.publishNotebookPresentation(node);
+ }
+ });
+ },
+ getRefreshContext: function (node) {
+ return this.buildContextBaseWithClass({
+ label: "Refresh",
+ action: (data) => {
+ this.refreshJSTree(node);
+ }
+ });
+ },
+ getRenameContext: function (node) {
+ let disabled = node.type === "workflow" && node.original._status === "running";
+ return this.buildContextBase({
+ label: "Rename",
+ disabled: disabled,
+ action: (data) => {
+ this.renameNode(node);
+ }
+ });
+ },
+ getSBMLConvertContext: function (node, identifier) {
+ return this.buildContextWithSubmenus({
+ label: "Convert",
+ submenu: {
+ convertToModel: this.buildContextBase({
+ label: "To Model",
+ action: (data) => {
+ this.config.toModel(this, node, identifier);
+ }
+ })
+ }
+ });
+ },
+ getSmdlConvertContext: function (node, identifier) {
+ return this.buildContextWithSubmenus({
+ label: "Convert",
+ aSep: true,
+ submenu: {
+ convertToModel: this.buildContextBase({
+ label: "To Model",
+ action: (data) => {
+ this.config.toModel(this, node, identifier);
+ }
+ })
+ }
+ });
+ },
+ getTimeStamp: function () {
+ let date = new Date();
+ let year = date.getFullYear();
+ let month = (date.getMonth() + 1).toString().padStart(2, "0");
+ let day = date.getDate().toString().padStart(2, "0");
+ let hours = date.getHours().toString().padStart(2, "0");
+ let minutes = date.getMinutes().toString().padStart(2, "0");
+ let seconds = date.getSeconds().toString().padStart(2, "0");
+ return `_${month}${day}${year}_${hours}${minutes}${seconds}`;
+ },
+ getWorkflowMdlContext: function (node) {
+ return this.buildContextWithSubmenus({
+ label: "Model",
+ submenu: {
+ edit: this.buildContextBase({
+ label: "Edit",
+ disabled: (!node.original._newFormat && node.original._status !== "ready"),
+ action: (data) => {
+ this.openWorkflowModel(node);
+ }
+ }),
+ extract: this.buildContextBase({
+ label: "Extract",
+ disabled: (node.original._newFormat && !node.original._hasJobs),
+ action: (data) => {
+ this.duplicate(node, "workflow/duplicate", {target: "wkfl_model"});
+ }
+ })
+ }
+ });
+ },
+ handleCreateDirectoryClick: function (e) {
+ let dirname = this.root === "none" ? "" : this.root;
+ this.createDirectory(null, dirname);
+ },
+ handleCreateModelClick: function (e) {
+ let dirname = this.root === "none" ? "" : this.root;
+ let inProject = this.root !== "none";
+ let isSpatial = e.target.dataset.type === "spatial";
+ this.createModel(null, dirname, isSpatial, inProject);
+ },
+ handleCreateProjectClick: function (e) {
+ let dirname = this.root === "none" ? "" : this.root;
+ this.createProject(null, dirname);
+ },
+ handleCreateDomain: function (e) {
+ let dirname = this.root === "none" ? "/" : this.root;
+ this.createDomain(dirname);
+ },
+ handleImportModelClick: function () {
+ this.importModel(null, this.root);
+ },
+ handleRefreshJSTreeClick: function (e) {
+ this.refreshJSTree(null);
+ },
+ handleUploadFileClick: function (e) {
+ let dirname = this.root === "none" ? "/" : this.root;
+ let inProject = this.root !== "none";
+ let type = e.target.dataset.type;
+ this.uploadFile(null, dirname, type, inProject);
+ },
+ hideNameWarning: function () {
+ $(this.queryByHook('rename-warning')).collapse('hide');
+ },
+ importModel: function (node, projectPath) {
+ if(document.querySelector('#importModelModal')){
+ document.querySelector('#importModelModal').remove();
+ }
+ let mdlListEP = path.join(app.getApiPath(), 'project/add-existing-model') + `?path=${projectPath}`;
+ app.getXHR(mdlListEP, {
+ always: (err, response, body) => {
+ let modal = $(modals.importModelHtml(body.files)).modal();
+ let okBtn = document.querySelector('#importModelModal .ok-model-btn');
+ let select = document.querySelector('#importModelModal #modelFileSelect');
+ let location = document.querySelector('#importModelModal #modelPathSelect');
+ select.addEventListener("change", (e) => {
+ okBtn.disabled = e.target.value && body.paths[e.target.value].length >= 2;
+ if(body.paths[e.target.value].length >= 2) {
+ var locations = body.paths[e.target.value].map((path) => {
+ return ``;
+ });
+ locations.unshift(``);
+ locations = locations.join(" ");
+ $("#modelPathSelect").find('option').remove().end().append(locations);
+ $("#location-container").css("display", "block");
+ }else{
+ $("#location-container").css("display", "none");
+ $("#modelPathSelect").find('option').remove().end();
+ }
+ });
+ location.addEventListener("change", (e) => {
+ okBtn.disabled = !Boolean(e.target.value);
+ });
+ okBtn.addEventListener("click", (e) => {
+ modal.modal('hide');
+ let mdlPath = body.paths[select.value].length < 2 ? body.paths[select.value][0] : location.value;
+ let queryStr = `?path=${projectPath}&mdlPath=${mdlPath}`;
+ let endpoint = path.join(app.getApiPath(), 'project/add-existing-model') + queryStr;
+ app.postXHR(endpoint, null, {
+ success: (err, response, body) => {
+ if(document.querySelector("#successModal")) {
+ document.querySelector("#successModal").remove();
+ }
+ let successModal = $(modals.successHtml(body.message)).modal();
+ this.config.updateParent(this, "model");
+ if(this.root !== "none") {
+ this.refreshJSTree(null);
+ }
+ },
+ error: (err, response, body) => {
+ this.reportError(body);
+ }
+ });
+ });
+ }
+ });
+ },
+ moveToTrash: function (node, type) {
+ if(document.querySelector('#moveToTrashConfirmModal')) {
+ document.querySelector('#moveToTrashConfirmModal').remove();
+ }
+ let modal = $(modals.moveToTrashConfirmHtml(type)).modal();
+ let yesBtn = document.querySelector('#moveToTrashConfirmModal .yes-modal-btn');
+ yesBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ let dirname = this.root === "none" ? "" : this.root;
+ let queryStr = `?srcPath=${node.original._path}&dstPath=${path.join(dirname, "trash", node.text)}`;
+ let endpoint = path.join(app.getApiPath(), "file/move") + queryStr;
+ app.getXHR(endpoint, {
+ always: (err, response, body) => {
+ $(this.queryByHook('empty-trash')).prop('disabled', false);
+ this.refreshJSTree(null);
+ this.config.updateParent(this, node.type);
+ }
+ });
+ });
+ },
+ newWorkflow: function (node, type) {
+ let model = new Model({
+ directory: node.original._path
+ });
+ app.getXHR(model.url(), {
+ success: (err, response, body) => {
+ model.set(body);
+ model.updateValid();
+ if(model.valid){
+ app.newWorkflow(self, node.original._path, node.type === "spatial", type);
+ }else{
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
+ let title = "Model Errors Detected";
+ let endpoint = path.join(app.getBasePath(), "stochss/models/edit") + '?path=' + model.directory + '&validate';
+ let message = `Errors were detected in you model click here to fix your model`;
+ $(modals.errorHtml(title, message)).modal();
+ }
+ }
+ });
+ },
+ openDomain: function (domainPath) {
+ let queryStr = `?domainPath=${domainPath}`;
+ window.location.href = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr
+ },
+ openFile: function (filePath) {
+ window.open(path.join(app.getBasePath(), "view", filePath), "_blank");
+ },
+ openModel: function (modelPath) {
+ let queryStr = `?path=${modelPath}`;
+ window.location.href = path.join(app.getBasePath(), "stochss/models/edit") + queryStr;
+ },
+ openNotebook: function (notebookPath) {
+ window.open(path.join(app.getBasePath(), "notebooks", notebookPath), '_blank');
+ },
+ openSBML: function (sbmlPath) {
+ window.open(path.join(app.getBasePath(), "edit", sbmlPath), '_blank');
+ },
+ openProject: function (projectPath) {
+ let queryStr = `?path=${projectPath}`;
+ window.location.href = path.join(app.getBasePath(), "stochss/project/manager") + queryStr;
+ },
+ openWorkflow: function (workflowPath) {
+ let queryStr = `?path=${workflowPath}&type=none`;
+ window.location.href = path.join(app.getBasePath(), "stochss/workflow/edit") + queryStr;
+ },
+ openWorkflowModel: function (node) {
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), "workflow/edit-model") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ if(body.error){
+ let title = `Model for ${node.text} Not Found`;
+ this.reportError({Reason: title, Message: body.error});
+ }else{
+ this.openModel(body.file);
+ }
+ }
+ });
+ },
+ publishNotebookPresentation: function (node) {
+ let queryStr = `?path=${node.original._path}`;
+ let endpoint = path.join(app.getApiPath(), "notebook/presentation") + queryStr;
+ app.getXHR(endpoint, {
+ success: (err, response, body) => {
+ $(modals.presentationLinks(body.message, "Shareable Presentation", body.links)).modal();
+ let copyBtn = document.querySelector('#presentationLinksModal #copy-to-clipboard');
+ this.config.updateParent(this, "Presentations");
+ copyBtn.addEventListener('click', (e) => {
+ let onFulfilled = (value) => {
+ $("#copy-link-success").css("display", "inline-block");
+ }
+ let onReject = (reason) => {
+ let msg = $("#copy-link-failed");
+ msg.html(reason);
+ msg.css("display", "inline-block");
+ }
+ app.copyToClipboard(links.presentation, onFulfilled, onReject);
+ });
+ },
+ error: (err, response, body) => {
+ this.reportError(body);
+ }
+ });
+ },
+ refreshInitialJSTree: function () {
+ let count = $('#files-jstree').jstree()._model.data['#'].children.length;
+ if(count == 0) {
+ this.refreshJSTree(null);
+ setTimeout(() => {
+ this.refreshInitialJSTree();
+ }, 3000);
+ }
+ },
+ refreshJSTree: function (node) {
+ if(node === null || node.type === 'root'){
+ this.jstreeIsLoaded = false;
+ $('#files-jstree').jstree().deselect_all(true);
+ $('#files-jstree').jstree().refresh();
+ }else{
+ $('#files-jstree').jstree().refresh_node(node);
+ }
+ },
+ renameNode: function (node) {
+ let currentName = node.text;
+ let par = $('#files-jstree').jstree().get_node(node.parent);
+ let extensionWarning = $(this.queryByHook('extension-warning'));
+ let nameWarning = $(this.queryByHook('rename-warning'));
+ extensionWarning.collapse('show');
+ $('#files-jstree').jstree().edit(node, null, (node, status) => {
+ if(currentName != node.text){
+ let name = node.type === "root" ? `${node.text}.proj` : node.text;
+ let queryStr = `?path=${node.original._path}&name=${name}`;
+ let endpoint = path.join(app.getApiPath(), "file/rename") + queryStr;
+ app.getXHR(endpoint, {
+ always: (err, response, body) => {
+ if(this.root !== "none" && ["nonspatial", "spatial"].includes(node.type)){
+ this.refreshJSTree(null);
+ }else{
+ this.refreshJSTree(par);
+ }
+ },
+ success: (err, response, body) => {
+ if(this.root !== "none" && node.type === "root") {
+ this.openProject(body._path);
+ }else if(body.changed) {
+ nameWarning.text(body.message);
+ nameWarning.collapse('show');
+ window.scrollTo(0,0);
+ setTimeout(_.bind(this.hideNameWarning, this), 10000);
+ }
+ node.original._path = body._path;
+ this.config.updateParent(this, node.type);
+ }
+ });
+ }
+ extensionWarning.collapse('hide');
+ nameWarning.collapse('hide');
+ });
+ },
+ reportError: function (body) {
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
+ let errorModal = $(modals.errorHtml(body.Reason, body.Message)).modal();
+ },
+ selectNode: function (node, fileName) {
+ if(!this.jstreeIsLoaded || !$('#files-jstree').jstree().is_loaded(node) && $('#files-jstree').jstree().is_loading(node)) {
+ setTimeout(_.bind(this.selectNode, this, node, fileName), 1000);
+ }else{
+ node = $('#files-jstree').jstree().get_node(node);
+ var child = "";
+ for(var i = 0; i < node.children.length; i++) {
+ var child = $('#files-jstree').jstree().get_node(node.children[i]);
+ if(child.original.text === fileName) {
+ $('#files-jstree').jstree().select_node(child);
+ let optionsButton = $(this.queryByHook("options-for-node"));
+ if(!this.nodeForContextMenu){
+ optionsButton.prop('disabled', false);
+ }
+ optionsButton.text(`Actions for ${child.original.text}`);
+ this.nodeForContextMenu = child;
+ break;
+ }
+ }
+ }
+ },
+ setupJstree: function (cb) {
+ $.jstree.defaults.contextmenu.items = (node, cb) => {
+ let zipTypes = this.config.contextZipTypes;
+ let asZip = zipTypes.includes(node.original.type);
+ let optionsButton = $(this.queryByHook("options-for-node"));
+ if(!this.nodeForContextMenu) {
+ optionsButton.prop('disabled', false);
+ }
+ optionsButton.text(`Actions for ${node.original.text}`);
+ this.nodeForContextMenu = node;
+ let contextMenus = {
+ root: this.config.getRootContext,
+ project: this.config.getProjectContext,
+ workflowGroup: this.config.getWorkflowGroupContext,
+ folder: this.config.getFolderContext,
+ nonspatial: this.config.getModelContext,
+ spatial: this.config.getSpatialModelContext,
+ domain: this.config.getDomainContext,
+ workflow: this.config.getWorkflowContext,
+ notebook: this.config.getNotebookContext,
+ sbmlModel: this.config.getSBMLContext,
+ }
+ if(Object.keys(contextMenus).includes(node.type)){
+ return contextMenus[node.type](this, node);
+ }
+ return this.config.getOtherContext(this, node);
+ }
+ $(() => {
+ $(document).on('shown.bs.modal', (e) => {
+ $('[autofocus]', e.target).focus();
+ });
+ $(document).on('dnd_start.vakata', (data, element, helper, event) => {
+ $('#files-jstree').jstree().load_all();
+ });
+ $('#files-jstree').jstree(this.treeSettings).bind("loaded.jstree", (event, data) => {
+ this.jstreeIsLoaded = true;
+ }).bind("refresh.jstree", (event, data) => {
+ this.jstreeIsLoaded = true;
+ });
+ $('#files-jstree').on('click.jstree', (e) => {
+ let parent = e.target.parentElement;
+ let _node = parent.children[parent.children.length - 1];
+ let node = $('#files-jstree').jstree().get_node(_node);
+ if(_node.nodeName === "A" && $('#files-jstree').jstree().is_loaded(node) && node.type === "folder"){
+ this.refreshJSTree(node);
+ }
+ let optionsButton = $(this.queryByHook("options-for-node"));
+ if(this.nodeForContextMenu === null){
+ optionsButton.prop('disabled', false);
+ }
+ optionsButton.text(`Actions for ${node.original.text}`);
+ this.nodeForContextMenu = node;
+ });
+ $('#files-jstree').on('dblclick.jstree', (e) => {
+ this.config.doubleClick(this, e);
+ });
+ });
+ },
+ showContextMenuForNode: function (e) {
+ $('#files-jstree').jstree().show_contextmenu(this.nodeForContextMenu);
+ },
+ uploadFile: function (node, dirname, type, inProject) {
+ if(document.querySelector('#uploadFileModal')) {
+ document.querySelector('#uploadFileModal').remove();
+ }
+ if(this.isSafariV14Plus == undefined){
+ let browser = app.getBrowser();
+ this.isSafariV14Plus = (browser.name === "Safari" && browser.version >= 14);
+ }
+ let modal = $(modals.uploadFileHtml(type, this.isSafariV14Plus)).modal();
+ let uploadBtn = document.querySelector('#uploadFileModal .upload-modal-btn');
+ let fileInput = document.querySelector('#uploadFileModal #fileForUpload');
+ let input = document.querySelector('#uploadFileModal #fileNameInput');
+ let fileCharErrMsg = document.querySelector('#uploadFileModal #fileSpecCharError');
+ let nameEndErrMsg = document.querySelector('#uploadFileModal #fileNameInputEndCharError');
+ let nameCharErrMsg = document.querySelector('#uploadFileModal #fileNameInputSpecCharError');
+ let nameUsageMsg = document.querySelector('#uploadFileModal #fileNameUsageMessage');
+ let getOptions = (file) => {
+ if(!inProject) { return {saveAs: true}; };
+ if(file) {
+ if(file.name.endsWith(".mdl")) { return {saveAs: false}; }
+ if(file.name.endsWith(".sbml")) { return {saveAs: false}; }
+ }
+ if(type === "model" || type === "sbml") {
+ if(!file) { return {saveAs: false}; }
+ if(type === "model" && file.name.endsWith(".json")) { return {saveAs: false}; }
+ if(type === "sbml" && file.name.endsWith(".xml")) { return {saveAs: false}; }
+ }
+ return {saveAs: true};
+ }
+ let validateFile = () => {
+ let options = getOptions(fileInput.files[0]);
+ let fileErr = !fileInput.files.length ? "" : app.validateName(fileInput.files[0].name, options);
+ let nameErr = app.validateName(input.value, options);
+ if(!fileInput.files.length) {
+ uploadBtn.disabled = true;
+ fileCharErrMsg.style.display = 'none';
+ }else if(fileErr === "" || (Boolean(input.value) && nameErr === "")){
+ uploadBtn.disabled = false;
+ fileCharErrMsg.style.display = 'none';
+ }else{
+ uploadBtn.disabled = true;
+ fileCharErrMsg.style.display = 'block';
+ }
+ return nameErr;
+ }
+ fileInput.addEventListener('change', (e) => {
+ validateFile();
+ });
+ input.addEventListener("input", (e) => {
+ let nameErr = validateFile();
+ nameCharErrMsg.style.display = nameErr === "both" || nameErr === "special" ? "block" : "none";
+ nameEndErrMsg.style.display = nameErr === "both" || nameErr === "forward" ? "block" : "none";
+ nameUsageMsg.style.display = nameErr !== "" ? "block" : "none";
+ });
+ uploadBtn.addEventListener('click', (e) => {
+ modal.modal('hide');
+ let file = fileInput.files[0];
+ let options = getOptions(file);
+ let fileinfo = {type: type, name: "", path: dirname};
+ if(Boolean(input.value) && app.validateName(input.value.trim(), options) === ""){
+ fileinfo.name = input.value.trim();
+ }
+ let formData = new FormData();
+ formData.append("datafile", file);
+ formData.append("fileinfo", JSON.stringify(fileinfo));
+ let endpoint = path.join(app.getApiPath(), 'file/upload');
+ app.postXHR(endpoint, formData, {
+ success: (err, response, body) => {
+ body = JSON.parse(body);
+ this.refreshJSTree(node);
+ this.config.updateParent(this, "model");
+ if(body.errors.length > 0){
+ if(document.querySelector("#uploadFileErrorsModal")) {
+ document.querySelector("#uploadFileErrorsModal").remove();
+ }
+ let errorModal = $(modals.uploadFileErrorsHtml(file.name, type, body.message, body.errors)).modal();
+ }
+ },
+ error: (err, response, body) => {
+ body = JSON.parse(body);
+ this.reportError(body);
+ }
+ }, false);
+ });
+ }
+});
\ No newline at end of file
diff --git a/client/views/main.js b/client/views/main.js
index 7a90ddadfc..c77d0ce9ce 100644
--- a/client/views/main.js
+++ b/client/views/main.js
@@ -129,7 +129,9 @@ module.exports = View.extend({
App.currentPage = newView;
}
});
-
+ if(app.getBasePath() === "/") {
+ $("#presentation-nav-link").css("display", "none");
+ }
let self = this;
let message = app.getBasePath() === "/" ? "Welcome to StochSS!" : "Welcome to StochSS Live!";
$("#user-logs").html(message)
diff --git a/client/views/model-listing.js b/client/views/model-listing.js
index f2b15add4e..5585193117 100644
--- a/client/views/model-listing.js
+++ b/client/views/model-listing.js
@@ -107,10 +107,13 @@ module.exports = View.extend({
if(model.valid){
app.newWorkflow(self, self.model.directory, self.model.is_spatial, type);
}else{
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
let title = "Model Errors Detected";
let endpoint = path.join(app.getBasePath(), "stochss/models/edit") + '?path=' + model.directory + '&validate';
let message = 'Errors were detected in you model click here to fix your model';
- $(modals.modelErrorHtml(title, message)).modal();
+ $(modals.errorHtml(title, message)).modal();
}
}
});
diff --git a/client/views/model-state-buttons.js b/client/views/model-state-buttons.js
deleted file mode 100644
index ade3574bd0..0000000000
--- a/client/views/model-state-buttons.js
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
-StochSS is a platform for simulating biochemical systems
-Copyright (C) 2019-2021 StochSS developers.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-var path = require('path');
-var Plotly = require('../lib/plotly');
-var $ = require('jquery');
-let _ = require('underscore');
-//support file
-var app = require('../app');
-var modals = require('../modals');
-//views
-var View = require('ampersand-view');
-//templates
-var template = require('../templates/includes/modelStateButtons.pug');
-
-module.exports = View.extend({
- template: template,
- events: {
- 'click [data-hook=save]' : 'clickSaveHandler',
- 'click [data-hook=run]' : 'handleSimulateClick',
- "click [data-hook=stochss-es]" : "handleSimulateClick",
- "click [data-hook=stochss-ps]" : "handleSimulateClick",
- 'click [data-hook=new-workflow]' : 'handleSimulateClick',
- 'click [data-hook=return-to-project-btn]' : 'clickReturnToProjectHandler',
- 'click [data-hook=presentation]' : 'handlePresentationClick'
- },
- initialize: function (attrs, options) {
- View.prototype.initialize.apply(this, arguments);
- this.validate = attrs.validate;
- },
- render: function () {
- View.prototype.render.apply(this, arguments);
- if(this.model.directory.includes('.proj')) {
- this.queryByHook("return-to-project-btn").style.display = "inline-block"
- }
- if(this.model.is_spatial) {
- $(this.queryByHook("stochss-es")).addClass("disabled");
- $(this.queryByHook("stochss-ps")).addClass("disabled");
- $(this.queryByHook("presentation")).css("display", "none");
- }else if(app.getBasePath() === "/") {
- $(this.queryByHook("presentation")).css("display", "none");
- }
- if(this.validate && !this.model.valid) {
- let errorMsg = $(this.parent.queryByHook("error-detected-msg"))
- this.displayError(errorMsg)
- }
- },
- clickSaveHandler: function (e) {
- let self = this;
- this.saveModel(_.bind(self.endAction, self, "save"));
- },
- clickRunHandler: function (e) {
- if(this.model.is_spatial && $(this.queryByHook("domain-plot-viewer-container")).css("display") !== "none") {
- this.parent.closeDomainPlot()
- }
- $(this.parent.queryByHook('model-run-error-container')).collapse('hide');
- $(this.parent.queryByHook('model-timeout-message')).collapse('hide');
- var el = this.parent.queryByHook('preview-plot-container');
- Plotly.purge(el)
- $(this.parent.queryByHook('preview-plot-buttons')).css("display", "none");
- if(this.model.is_spatial) {
- this.saveModel(this.getPreviewTarget.bind(this));
- }else{
- this.saveModel(this.runModel.bind(this));
- }
- },
- clickReturnToProjectHandler: function (e) {
- let self = this
- this.saveModel(function () {
- self.endAction("save")
- var dirname = path.dirname(self.model.directory)
- if(dirname.endsWith(".wkgp")) {
- dirname = path.dirname(dirname)
- }
- var queryString = "?path="+dirname
- window.location.href = path.join(app.getBasePath(), "/stochss/project/manager")+queryString;
- })
- },
- clickNewWorkflowHandler: function (e) {
- let self = this
- this.saveModel(function () {
- self.endAction("save")
- var queryString = "?path="+self.model.directory
- if(self.model.directory.includes('.proj')) {
- let wkgp = self.model.directory.includes('.wkgp') ? self.model.name + ".wkgp" : "WorkflowGroup1.wkgp"
- let parentPath = path.join(path.dirname(self.model.directory), wkgp)
- queryString += "&parentPath="+parentPath
- }
- let endpoint = path.join(app.getBasePath(), "stochss/workflow/selection")+queryString
- window.location.href = endpoint
- })
- },
- getPreviewTarget: function () {
- this.endAction("save");
- let species = this.model.species.map(function (species) {
- return species.name
- });
- let self = this;
- let modal = $(modals.selectPreviewTargetHTML(species)).modal();
- let okBtn = document.querySelector("#previewTargetSelectModal .ok-model-btn");
- let select = document.querySelector("#previewTargetSelectModal #previewTargetSelectList");
- okBtn.addEventListener('click', function (e) {
- modal.modal('hide');
- let target = select.value;
- self.runModel(target);
- });
- },
- togglePreviewWorkflowBtn: function () {
- $(this.queryByHook('simulate-model')).prop('disabled', !this.model.valid)
- if(this.model.valid) {
- if(this.model.parameters.length <= 0) {
- $(this.queryByHook("stochss-ps")).addClass("disabled")
- }else{
- $(this.queryByHook("stochss-ps")).removeClass("disabled")
- }
- $(".disabled").click(function(event) {
- event.preventDefaults();
- event.stopPropagation();
- return false;
- });
- }
- },
- saveModel: function (cb) {
- this.startAction("save");
- // this.model is a ModelVersion, the parent of the collection is Model
- var model = this.model;
- if (cb) {
- model.saveModel(cb);
- } else {
- model.saveModel();
- }
- },
- startAction: function (action) {
- if(action === "save") {
- msg = $(this.queryByHook("saving"));
- }else{
- msg = $(this.queryByHook("publishing"));
- }
- msg.css("display", "inline-block");
- var saving = this.queryByHook('mdl-action-start');
- var saved = this.queryByHook('mdl-action-end');
- saved.style.display = "none";
- saving.style.display = "inline-block";
- },
- errorAction: function () {
- oldMsg = $(this.queryByHook("publishing")).css("display", "none");
- var saving = this.queryByHook('mdl-action-start');
- var error = this.queryByHook('mdl-action-err');
- saving.style.display = "none";
- error.style.display = "inline-block";
- setTimeout(function () {
- error.style.display = "none";
- }, 5000);
- },
- endAction: function (action) {
- if(action === "save") {
- oldMsg = $(this.queryByHook("saving"));
- msg = $(this.queryByHook("saved"));
- }else{
- oldMsg = $(this.queryByHook("publishing"));
- msg = $(this.queryByHook("published"));
- }
- oldMsg.css("display", "none");
- msg.css("display", "inline-block");
- var saving = this.queryByHook('mdl-action-start');
- var saved = this.queryByHook('mdl-action-end');
- saving.style.display = "none";
- saved.style.display = "inline-block";
- setTimeout(function () {
- saved.style.display = "none";
- msg.css("display", "none");
- }, 5000);
- },
- runModel: function (target=null) {
- if(typeof target !== "string") {
- this.endAction("save");
- }
- this.running();
- $(this.parent.queryByHook('model-run-container')).css("display", "block")
- var model = this.model
- var queryStr = "?cmd=start&outfile=none&path="+model.directory
- if(target) {
- queryStr += "&target=" + target;
- }
- var endpoint = path.join(app.getApiPath(), 'model/run')+queryStr;
- var self = this;
- app.getXHR(endpoint, {
- always: function (err, response, body) {
- self.outfile = body.Outfile;
- self.getResults();
- }
- });
- },
- running: function () {
- var plot = this.parent.queryByHook('preview-plot-container');
- var spinner = this.parent.queryByHook('plot-loader');
- var errors = this.parent.queryByHook('model-run-error-container');
- plot.style.display = "none";
- spinner.style.display = "block";
- errors.style.display = "none";
- },
- ran: function (noErrors) {
- var runContainer = $(this.parent.queryByHook("model-run-container"));
- if(runContainer.css("display") === "none") {
- runContainer.css("display", 'block');
- }
- $(this.parent.queryByHook('preview-plot-buttons')).css('display', 'inline-block')
- let plotBtn = $(this.parent.queryByHook('toggle-preview-plot'))
- if(plotBtn.text() === "Show Preview") {
- plotBtn.text("Hide Preview")
- }
- var plot = this.parent.queryByHook('preview-plot-container');
- var spinner = this.parent.queryByHook('plot-loader');
- var errors = this.parent.queryByHook('model-run-error-container');
- if(noErrors){
- plot.style.display = "block";
- }else{
- errors.style.display = "block"
- }
- spinner.style.display = "none";
- },
- getResults: function () {
- var self = this;
- var model = this.model;
- setTimeout(function () {
- let queryStr = "?cmd=read&outfile="+self.outfile+"&path="+model.directory
- endpoint = path.join(app.getApiPath(), 'model/run')+queryStr;
- let errorCB = function (err, response, body) {
- self.ran(false);
- $(self.parent.queryByHook('model-run-error-message')).text(body.Results.errors);
- }
- app.getXHR(endpoint, {
- always: function (err, response, body) {
- if(typeof body === "string") {
- body = body.replace(/NaN/g, null)
- body = JSON.parse(body)
- }
- var data = body.Results;
- if(response.statusCode >= 400 || data.errors){
- errorCB(err, response, body);
- }
- else if(!body.Running){
- if(data.timeout){
- $(self.parent.queryByHook('model-timeout-message')).collapse('show');
- }
- self.plotResults(data.results);
- }else{
- self.getResults();
- }
- },
- error: errorCB
- });
- }, 2000);
- },
- plotResults: function (data) {
- // TODO abstract this into an event probably
- var title = this.model.name + " Model Preview"
- this.ran(true)
- el = this.parent.queryByHook('preview-plot-container');
- Plotly.newPlot(el, data);
- window.scrollTo(0, document.body.scrollHeight)
- },
- displayError: function (errorMsg, e) {
- $(this.parent.queryByHook('toggle-preview-plot')).click()
- errorMsg.css('display', 'block')
- this.focusOnError(e)
- },
- handleSimulateClick: function (e) {
- var errorMsg = $(this.parent.queryByHook("error-detected-msg"))
- if(!this.model.valid) {
- this.displayError(errorMsg, e);
- }else{
- errorMsg.css('display', 'none')
- let simType = e.target.dataset.type
- if(simType === "preview") {
- this.clickRunHandler(e)
- }else if(simType === "notebook"){
- this.clickNewWorkflowHandler(e)
- }else if(!this.model.is_spatial) {
- if(simType === "ensemble") {
- app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Ensemble Simulation");
- }else if(simType === "psweep") {
- app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Parameter Sweep");
- }
- }
- }
- },
- handlePresentationClick: function (e) {
- var errorMsg = $(this.parent.queryByHook("error-detected-msg"));
- if(!this.model.valid) {
- this.displayError(errorMsg, e);
- }else{
- let self = this;
- this.startAction("publish")
- let queryStr = "?path=" + this.model.directory;
- let endpoint = path.join(app.getApiPath(), "model/presentation") + queryStr;
- app.getXHR(endpoint, {
- success: function (err, response, body) {
- self.endAction("publish");
- let title = body.message;
- let linkHeaders = "Shareable Presentation";
- let links = body.links;
- $(modals.presentationLinks(title, linkHeaders, links)).modal();
- let copyBtn = document.querySelector('#presentationLinksModal #copy-to-clipboard');
- copyBtn.addEventListener('click', function (e) {
- let onFulfilled = (value) => {
- $("#copy-link-success").css("display", "inline-block");
- }
- let onReject = (reason) => {
- let msg = $("#copy-link-failed");
- msg.html(reason);
- msg.css("display", "inline-block");
- }
- app.copyToClipboard(links.presentation, onFulfilled, onReject);
- });
- },
- error: function (err, response, body) {
- self.errorAction();
- $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal();
- }
- });
- }
- },
- focusOnError: function (e) {
- if(this.model.error) {
- let self = this
- if(this.model.error.type === "species") {
- this.openSpeciesSection()
- }else if(this.model.error.type === "parameter") {
- this.openParametersSection()
- }else if(this.model.error.type === "reaction") {
- this.openReactionsSection()
- }else if(this.model.error.type === "process"){
- this.openReactionsSection(true)
- }else if(this.model.error.type === "event") {
- this.openEventsSection()
- }else if(this.model.error.type === "rule") {
- this.openRulesSection()
- }else if(this.model.error.type === "volume") {
- this.openVolumeSection()
- }else if(this.model.error.type === "timespan") {
- this.openTimespanSection()
- }else if(this.model.error.type === "domain") {
- this.openDomainSection()
- }
- setTimeout(function () {
- let inputErrors = self.parent.queryAll(".input-invalid")
- let componentErrors = self.parent.queryAll(".component-invalid")
- if(componentErrors.length > 0) {
- componentErrors[0].scrollIntoView({'block':"center"})
- }else if(inputErrors.length > 0) {
- inputErrors[0].focus()
- }
- }, 300)
- }
- },
- openSpeciesSection: function () {
- let specSection = $(this.parent.modelView.speciesView.queryByHook("species-list-container"))
- if(!specSection.hasClass("show")) {
- let specCollapseBtn = $(this.parent.modelView.speciesView.queryByHook("collapse"))
- specCollapseBtn.click()
- specCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelView.speciesView, "species");
- },
- openDomainSection: function () {
- let domainSection = $(this.parent.modelView.domainViewer.queryByHook("domain-container"))
- if(!domainSection.hasClass("show")) {
- let domainCollapseBtn = $(this.parent.modelView.domainViewer.queryByHook("collapse"))
- domainCollapseBtn.click()
- domainCollapseBtn.html('-')
- }
- },
- openParametersSection: function () {
- let paramSection = $(this.parent.modelView.parametersView.queryByHook("parameters-list-container"))
- if(!paramSection.hasClass("show")) {
- let paramCollapseBtn = $(this.parent.modelView.parametersView.queryByHook("collapse"))
- paramCollapseBtn.click()
- paramCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelView.parametersView, "parameters");
- },
- openReactionsSection: function (isCollection = false) {
- let error = this.model.error
- let reacSection = $(this.parent.modelView.reactionsView.queryByHook("reactions-list-container"))
- if(!reacSection.hasClass("show")) {
- let reacCollapseBtn = $(this.parent.modelView.reactionsView.queryByHook("collapse"))
- reacCollapseBtn.click()
- reacCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelView.reactionsView, "reactions");
- if(!isCollection) {
- var reaction = this.model.reactions.filter(function (r) {
- return r.compID === error.id
- })[0]
- this.model.reactions.trigger("select", reaction);
- }
- },
- openEventsSection: function () {
- let error = this.model.error
- let advSection = $(this.parent.modelView.queryByHook("mv-advanced-section"))
- if(!advSection.hasClass("show")) {
- let advCollapseBtn = $(this.parent.modelView.queryByHook("collapse-mv-advanced-section"))
- advCollapseBtn.click()
- advCollapseBtn.html('-')
- }
- let evtSection = $(this.parent.modelView.eventsView.queryByHook("events"))
- if(!evtSection.hasClass("show")) {
- let evtCollapseBtn = $(this.parent.modelView.eventsView.queryByHook("collapse"))
- evtCollapseBtn.click()
- evtCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelView.eventsView, "events");
- var event = this.model.eventsCollection.filter(function (e) {
- return e.compID === error.id
- })[0]
- this.model.eventsCollection.trigger("select", event);
- event.detailsView.openAdvancedSection()
- },
- openRulesSection: function () {
- let advSection = $(this.parent.modelView.queryByHook("me-advanced-section"))
- if(!advSection.hasClass("show")) {
- let advCollapseBtn = $(this.parent.modelView.queryByHook("collapse-me-advanced-section"))
- advCollapseBtn.click()
- advCollapseBtn.html('-')
- }
- let ruleSection = $(this.parent.modelView.rulesView.queryByHook("rules-list-container"))
- if(!ruleSection.hasClass("show")) {
- let ruleCollapseBtn = $(this.parent.modelView.rulesView.queryByHook("collapse"))
- ruleCollapseBtn.click()
- ruleCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelView.rulesView, "rules");
- },
- openVolumeSection: function () {
- let advSection = $(this.parent.modelView.queryByHook("me-advanced-section"))
- if(!advSection.hasClass("show")) {
- let advCollapseBtn = $(this.parent.modelView.queryByHook("collapse-me-advanced-section"))
- advCollapseBtn.click()
- advCollapseBtn.html('-')
- }
- let volSection = $(this.parent.modelView.queryByHook("system-volume-section"))
- if(!volSection.hasClass("show")) {
- let volCollapseBtn = $(this.parent.modelView.queryByHook("collapse-system-volume"))
- volCollapseBtn.click()
- volCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelView, "system-volume");
- },
- openTimespanSection: function () {
- let tspnSection = $(this.parent.modelSettings.queryByHook("timespan-container"))
- if(!tspnSection.hasClass("show")) {
- let tspnCollapseBtn = $(this.parent.modelSettings.queryByHook("collapse"))
- tspnCollapseBtn.click()
- tspnCollapseBtn.html('-')
- }
- this.switchToEditTab(this.parent.modelSettings, "timespan");
- },
- switchToEditTab: function (view, section) {
- let elementID = Boolean(view.model && view.model.elementID) ? view.model.elementID + "-" : "";
- if($(view.queryByHook(elementID + 'view-' + section)).hasClass('active')) {
- $(view.queryByHook(elementID + section + '-edit-tab')).tab('show');
- $(view.queryByHook(elementID + 'edit-' + section)).addClass('active');
- $(view.queryByHook(elementID + 'view-' + section)).removeClass('active');
- }
- }
-});
diff --git a/client/views/presentation-view.js b/client/views/presentation-view.js
new file mode 100644
index 0000000000..df5fb3dfe5
--- /dev/null
+++ b/client/views/presentation-view.js
@@ -0,0 +1,64 @@
+/*
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2021 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+let $ = require('jquery');
+let path = require('path');
+//support files
+let app = require('../app');
+//views
+let View = require('ampersand-view');
+//templates
+let template = require('../templates/includes/presentationView.pug');
+
+module.exports = View.extend({
+ template: template,
+ events: {
+ 'click [data-hook=copy-link]' : 'copyLink',
+ 'click [data-hook=remove]' : 'removePresentation'
+ },
+ initialize: function (attrs, options) {
+ View.prototype.initialize.apply(this, arguments);
+ },
+ render: function ( attrs, options) {
+ View.prototype.render.apply(this, arguments);
+ },
+ copyLink: function (e) {
+ let onFulfilled = (value) => {
+ $(this.queryByHook("copy-link-success")).css("display", "inline-block");
+ setTimeout(() => {
+ $(this.queryByHook("copy-link-success")).css("display", "none");
+ }, 5000);
+ };
+ let onReject = (reason) => {
+ let msg = $(this.queryByHook("copy-link-failed"));
+ msg.html(reason);
+ msg.css("display", "inline-block");
+ };
+ app.copyToClipboard(this.model.link, onFulfilled, onReject);
+ },
+ removePresentation: function (e) {
+ let self = this;
+ let filePath = path.join('.presentations', this.model.file);
+ let endpoint = path.join(app.getApiPath(), "file/delete") + "?path=" + filePath;
+ app.getXHR(endpoint, {
+ success: function (err, response, body) {
+ self.model.collection.remove(self.model);
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/client/views/workflow-group-listing.js b/client/views/workflow-group-listing.js
index 02b3d447ff..8eb83ca81b 100644
--- a/client/views/workflow-group-listing.js
+++ b/client/views/workflow-group-listing.js
@@ -93,10 +93,13 @@ module.exports = View.extend({
if(model.valid){
app.newWorkflow(self, self.model.model.directory, self.model.model.is_spatial, type);
}else{
+ if(document.querySelector("#errorModal")) {
+ document.querySelector("#errorModal").remove();
+ }
let title = "Model Errors Detected";
let endpoint = path.join(app.getBasePath(), "stochss/models/edit") + '?path=' + model.directory + '&validate';
let message = 'Errors were detected in you model click here to fix your model';
- $(modals.modelErrorHtml(title, message)).modal();
+ $(modals.errorHtml(title, message)).modal();
}
}
});
diff --git a/custom.js b/custom.js
new file mode 100644
index 0000000000..afd7589cfc
--- /dev/null
+++ b/custom.js
@@ -0,0 +1,8 @@
+requirejs([
+ 'jquery',
+ 'base/js/utils',
+], function($, utils
+ ){
+
+ utils.change_favicon("static/favicon.ico")
+});
diff --git a/jupyterhub/Dockerfile.jupyterhub b/jupyterhub/Dockerfile.jupyterhub
index eed510b44b..8d8d1dc221 100644
--- a/jupyterhub/Dockerfile.jupyterhub
+++ b/jupyterhub/Dockerfile.jupyterhub
@@ -41,10 +41,15 @@ RUN python3 -m pip install --no-cache-dir \
dockerspawner==0.11.* \
psycopg2==2.7.* \
nbviewer==1.0.1 \
- notebook
+ notebook \
+ gillespy2==1.6.3
COPY static/* /usr/local/share/jupyterhub/static/
+COPY custom/favicon.ico /usr/local/share/jupyterhub/static/favicon.ico
+
+COPY custom/custom.js /opt/stochss-config/.jupyter/custom/custom.js
+
COPY cull_idle_servers.py /srv/jupyterhub/cull_idle_servers.py
CMD ["jupyterhub" "-f" "/srv/jupyterhub/jupyterhub_config.py"]
diff --git a/jupyterhub/custom/custom.js b/jupyterhub/custom/custom.js
new file mode 100644
index 0000000000..9be2875002
--- /dev/null
+++ b/jupyterhub/custom/custom.js
@@ -0,0 +1,8 @@
+requirejs([
+ 'jquery',
+ 'base/js/utils',
+], function($, utils
+ ){
+
+ utils.change_favicon("/hub/static/favicon.ico")
+});
diff --git a/jupyterhub/custom/favicon.ico b/jupyterhub/custom/favicon.ico
new file mode 100644
index 0000000000..df7bf93e65
Binary files /dev/null and b/jupyterhub/custom/favicon.ico differ
diff --git a/jupyterhub/handlers.py b/jupyterhub/handlers.py
index d891ed670f..ca700b20e7 100644
--- a/jupyterhub/handlers.py
+++ b/jupyterhub/handlers.py
@@ -86,3 +86,20 @@ async def get(self):
'''
html = self.render_template("stochss-notebook-presentation.html")
self.finish(html)
+
+
+class MultiplePlotsHandler(BaseHandler):
+ '''
+ ################################################################################################
+ Handler for rendering jupyterhub job presentations multiple plots page.
+ ################################################################################################
+ '''
+ async def get(self):
+ '''
+ Render the jupyterhub job presentations multiple plots page.
+
+ Attributes
+ ----------
+ '''
+ html = self.render_template("multiple-plots-page.html")
+ self.finish(html)
diff --git a/jupyterhub/home_template.pug b/jupyterhub/home_template.pug
index 334896fd28..4be68f032e 100644
--- a/jupyterhub/home_template.pug
+++ b/jupyterhub/home_template.pug
@@ -4,6 +4,7 @@
html
head
meta(charset="UTF-8")
+ link(rel="shortcut icon" href="/hub/static/favicon.ico")
link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.8/themes/default/style.min.css")
title StochSS: Stochastic Simulation Service
meta(name="viewport", content="width=device-width, initial-scale=1.0")
diff --git a/jupyterhub/job_presentation.py b/jupyterhub/job_presentation.py
new file mode 100644
index 0000000000..23e5399567
--- /dev/null
+++ b/jupyterhub/job_presentation.py
@@ -0,0 +1,816 @@
+'''
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2021 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+'''
+
+import os
+import csv
+import json
+import shutil
+import pickle
+import logging
+import tempfile
+import traceback
+
+from pathlib import Path
+
+import numpy
+import plotly
+
+from presentation_base import StochSSBase, get_presentation_from_user
+from presentation_error import StochSSJobResultsError, StochSSFileNotFoundError, report_error, \
+ PlotNotAvailableError, StochSSAPIError
+
+from jupyterhub.handlers.base import BaseHandler
+
+log = logging.getLogger('stochss')
+
+# pylint: disable=abstract-method
+# pylint: disable=too-few-public-methods
+class JobAPIHandler(BaseHandler):
+ '''
+ ################################################################################################
+ Base Handler for getting job presentations from user containers.
+ ################################################################################################
+ '''
+ async def get(self):
+ '''
+ Load the job presentation from User's presentations directory.
+
+ Attributes
+ ----------
+ '''
+ owner = self.get_query_argument(name="owner")
+ log.debug(f"Container id of the owner: {owner}")
+ file = self.get_query_argument(name="file")
+ log.debug(f"Name to the file: {file}")
+ self.set_header('Content-Type', 'application/json')
+ try:
+ path = os.path.join("/tmp/presentation_cache", file)
+ if os.path.exists(path):
+ job = StochSSJob(path=path).load()
+ else:
+ job = get_presentation_from_user(owner=owner, file=file, kwargs={"file": file},
+ process_func=process_job_presentation)
+ log.debug(f"Contents of the json file: {job}")
+ self.write(job)
+ except StochSSAPIError as load_err:
+ report_error(self, log, load_err)
+ self.finish()
+
+
+class DownJobPresentationAPIHandler(BaseHandler):
+ '''
+ ################################################################################################
+ Base Handler for downloading job presentations from user containers.
+ ################################################################################################
+ '''
+ async def get(self, owner, file):
+ '''
+ Download the job presentation from User's presentations directory.
+
+ Attributes
+ ----------
+ '''
+ log.debug(f"Container id of the owner: {owner}")
+ log.debug(f"Name to the file: {file}")
+ self.set_header('Content-Type', 'application/zip')
+ try:
+ job, name = get_presentation_from_user(owner=owner, file=file,
+ kwargs={"for_download": True},
+ process_func=process_job_presentation)
+ self.set_header('Content-Disposition', f'attachment; filename="{name}.zip"')
+ self.write(job)
+ except StochSSAPIError as load_err:
+ report_error(self, log, load_err)
+ self.finish()
+
+
+class PlotJobResultsAPIHandler(BaseHandler):
+ '''
+ ################################################################################################
+ Handler for getting result plots based on plot type.
+ ################################################################################################
+ '''
+ async def get(self):
+ '''
+ Retrieve a plot figure of the job results based on the plot type in the request body.
+
+ Attributes
+ ----------
+ '''
+ self.set_header('Content-Type', 'application/json')
+ path = self.get_query_argument(name="path")
+ log.debug(f"The path to the workflow: {path}")
+ body = json.loads(self.get_query_argument(name='data'))
+ log.debug(f"Plot args passed to the plot: {body}")
+ try:
+ job = StochSSJob(path=path)
+ if body['sim_type'] in ("GillesPy2", "GillesPy2_PS"):
+ fig = job.get_plot_from_results(data_keys=body['data_keys'],
+ plt_key=body['plt_key'], add_config=True)
+ job.print_logs(log)
+ else:
+ fig = job.get_psweep_plot_from_results(fixed=body['data_keys'],
+ kwargs=body['plt_key'], add_config=True)
+ job.print_logs(log)
+ if "plt_data" in body.keys():
+ fig = job.update_fig_layout(fig=fig, plt_data=body['plt_data'])
+ log.debug(f"Plot figure: {fig}")
+ self.write(fig)
+ except StochSSAPIError as err:
+ report_error(self, log, err)
+ self.finish()
+
+
+class DownloadCSVAPIHandler(BaseHandler):
+ '''
+ ################################################################################################
+ Handler for getting result plots based on plot type.
+ ################################################################################################
+ '''
+ async def get(self):
+ '''
+ Retrieve a plot figure of the job results based on the plot type in the request body.
+
+ Attributes
+ ----------
+ '''
+ self.set_header('Content-Type', 'application/zip')
+ path = self.get_query_argument(name="path")
+ csv_type = self.get_query_argument(name="type")
+ data = json.loads(self.get_query_argument(name="data", default=None))
+ try:
+ job = StochSSJob(path=path)
+ name = job.load()['name']
+ self.set_header('Content-Disposition', f'attachment; filename="{name}.zip"')
+ if csv_type == "time series":
+ csv_data = job.get_csvzip_from_results(**data, name=name)
+ elif csv_type == "psweep":
+ csv_data = job.get_psweep_csvzip_from_results(fixed=data, name=name)
+ else:
+ csv_data = job.get_full_csvzip_from_results(name=name)
+ self.write(csv_data)
+ except StochSSAPIError as err:
+ report_error(self, log, err)
+ self.finish()
+
+
+def process_job_presentation(path, file=None, for_download=False):
+ '''
+ Get the job presentation data from the file.
+
+ Attributes
+ ----------
+ path : str
+ Path to the job presentation file.
+ for_download : bool
+ Whether or not the job presentation is being downloaded.
+ '''
+ with open(path, "rb") as job_file:
+ job = pickle.load(job_file)
+ job['job']['name'] = job['name']
+ if not for_download:
+ dirname = "/tmp/presentation_cache"
+ if not os.path.exists(dirname):
+ os.mkdir(dirname)
+ job_dir = os.path.join(dirname, file)
+ job['job']['directory'] = job_dir
+ os.mkdir(job_dir)
+ with open(os.path.join(job_dir, "job.json"), "w") as job_file:
+ json.dump(job['job'], job_file, sort_keys=True, indent=4)
+ with open(os.path.join(job_dir, "results.p"), "wb") as res_file:
+ pickle.dump(job['results'], res_file)
+ return job['job']
+ job_zip = make_zip_for_download(job)
+ return job_zip, job['name']
+
+
+def make_zip_for_download(job):
+ '''
+ Make an editable job for users to download.
+
+ Attributes
+ ----------
+ job : dict
+ StochSS job presentation
+ '''
+ tmp_dir = tempfile.TemporaryDirectory()
+ res_path = os.path.join(tmp_dir.name, job['name'],
+ '/'.join(job['job']['directory'].split('/')[2:]), "results")
+ Path(res_path).mkdir(parents=True)
+ with open(os.path.join(res_path, "results.p"), "wb") as res_file:
+ pickle.dump(job['results'], res_file)
+ job_path = os.path.dirname(res_path)
+ Path(os.path.join(job_path, "RUNNING")).touch()
+ Path(os.path.join(job_path, "COMPLETE")).touch()
+ write_json(path=os.path.join(job_path, "settings.json"), body=job['job']['settings'])
+ write_json(path=os.path.join(job_path, job['job']['mdlPath'].split('/').pop()),
+ body=job['job']['model'])
+ info = {"annotation": "", "wkfl_model": job['job']['mdlPath'].split('/').pop(),
+ "start_time": job['job']['startTime'], "type": job['job']['type'],
+ "source_model": os.path.join(job['name'], job['job']['mdlPath'].split('/').pop())}
+ write_json(path=os.path.join(job_path, "info.json"), body=info)
+ if "No logs" in job['job']['logs']:
+ Path(os.path.join(job_path, "logs.txt")).touch()
+ else:
+ with open(os.path.join(job_path, "logs.txt"), "w") as logs_file:
+ logs_file.write(job['job']['logs'])
+ wkfl_path = os.path.dirname(job_path)
+ settings = {"model": info['source_model'], "settings": job['job']['settings'],
+ "type": job['job']['titleType']}
+ write_json(path=os.path.join(wkfl_path, "settings.json"), body=settings)
+ write_json(path=os.path.join(os.path.dirname(wkfl_path),
+ job['job']['mdlPath'].split('/').pop()), body=job['job']['model'])
+ zip_path = os.path.join(tmp_dir.name, job['name'])
+ shutil.make_archive(zip_path, "zip", tmp_dir.name, job['name'])
+ with open(f"{zip_path}.zip", "rb") as zip_file:
+ return zip_file.read()
+
+
+def write_json(path, body):
+ '''
+ Write a json file to disc.
+
+ Attributes
+ ----------
+ path : str
+ Path to the file.
+ body : dict
+ Contents of the file.
+ '''
+ with open(path, "w") as file:
+ json.dump(body, file, sort_keys=True, indent=4)
+
+
+class StochSSJob(StochSSBase):
+ '''
+ ################################################################################################
+ StochSS model object
+ ################################################################################################
+ '''
+
+ def __init__(self, path):
+ '''
+ Intitialize a job object
+
+ Attributes
+ ----------
+ path : str
+ Path to the job presentation.
+ '''
+ super().__init__(path=path)
+
+
+ @classmethod
+ def __get_csvzip(cls, dirname, name):
+ shutil.make_archive(os.path.join(dirname, name), "zip", dirname, name)
+ path = os.path.join(dirname, f"{name}.zip")
+ with open(path, "rb") as zip_file:
+ return zip_file.read()
+
+
+ def __get_filtered_1d_results(self, f_keys):
+ results = self.__get_pickled_results()
+ f_results = []
+ for key, result in results.items():
+ if self.__is_result_valid(f_keys, key):
+ f_results.append(result)
+ return f_results
+
+
+ def __get_filtered_2d_results(self, f_keys, param):
+ results = self.__get_pickled_results()
+ f_results = []
+ for value in param['range']:
+ p_key = f"{param['name']}:{value}"
+ p_results = []
+ for key, result in results.items():
+ if p_key in key.split(',') and self.__is_result_valid(f_keys, key):
+ p_results.append(result)
+ f_results.append(p_results)
+ return f_results
+
+
+ def __get_filtered_ensemble_results(self, data_keys):
+ result = self.__get_pickled_results()
+ if data_keys:
+ key = [f"{name}:{value}" for name, value in data_keys.items()]
+ key = ','.join(key)
+ result = result[key]
+ return result
+
+
+ def __get_full_timeseries_csv(self, b_path, results, get_name, name):
+ ts_path = os.path.join(b_path, "Time_Series")
+ os.makedirs(ts_path)
+ for i, (key, result) in enumerate(results.items()):
+ data = [_data.split(':') for _data in key.split(',')]
+ data_keys = {_data[0]: _data[1] for _data in data}
+ result.to_csv(path=ts_path, nametag=get_name(name, i), stamp="")
+ self.__write_parameters_csv(path=ts_path, name=get_name(name, i), data_keys=data_keys)
+
+
+ @classmethod
+ def __get_fixed_keys_and_dims(cls, settings, fixed):
+ p_len = len(settings['parameterSweepSettings']['parameters'])
+ dims = p_len - len(fixed.keys())
+ if dims <= 0:
+ message = "Too many fixed parameters were provided."
+ message += "At least one variable parameter is required."
+ raise StochSSJobResultsError(message)
+ if dims > 2:
+ message = "Not enough fixed parameters were provided."
+ message += "Variable parameters cannot exceed 2."
+ raise StochSSJobResultsError(message)
+ f_keys = [f"{name}:{value}" for name, value in fixed.items()]
+ return dims, f_keys
+
+
+ def __get_pickled_results(self):
+ path = os.path.join(self.path, "results.p")
+ with open(path, "rb") as results_file:
+ return pickle.load(results_file)
+
+
+ @classmethod
+ def __is_result_valid(cls, f_keys, key):
+ for f_key in f_keys:
+ if f_key not in key.split(','):
+ return False
+ return True
+
+
+ @classmethod
+ def __write_parameters_csv(cls, path, name, data_keys):
+ csv_path = os.path.join(path, name, "parameters.csv")
+ with open(csv_path, "w") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ csv_writer.writerow(list(data_keys.keys()))
+ csv_writer.writerow(list(data_keys.values()))
+
+
+ def get_csvzip_from_results(self, data_keys, proc_key, name):
+ '''
+ Get the csv files of the plot data for download.
+
+ Attributes
+ ----------
+ data_keys : dict
+ Dictionary of param names and values used to identify the correct data.
+ proc_key : str
+ Type post processing to preform.
+ name : str
+ Name of the csv directory
+ '''
+ try:
+ self.log("info", "Getting job results...")
+ result = self.__get_filtered_ensemble_results(data_keys)
+ self.log("info", "Processing results...")
+ if proc_key == "stddev":
+ result = result.stddev_ensemble()
+ elif proc_key == "avg":
+ result = result.average_ensemble()
+ self.log("info", "Generating CSV files...")
+ tmp_dir = tempfile.TemporaryDirectory()
+ result.to_csv(path=tmp_dir.name, nametag=name, stamp="")
+ if data_keys:
+ self.__write_parameters_csv(path=tmp_dir.name, name=name, data_keys=data_keys)
+ self.log("info", "Generating zip archive...")
+ return self.__get_csvzip(dirname=tmp_dir.name, name=name)
+ except FileNotFoundError as err:
+ message = f"Could not find the results pickle file: {str(err)}"
+ raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ except KeyError as err:
+ message = f"The requested results are not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
+ def get_full_csvzip_from_results(self, name):
+ '''
+ Get the csv files for the full set of results data
+
+ Attributes
+ ----------
+ name : str
+ Name of the csv directory.
+ '''
+ results = self.__get_pickled_results()
+ tmp_dir = tempfile.TemporaryDirectory()
+ if not isinstance(results, dict):
+ results.to_csv(path=tmp_dir.name, nametag=name, stamp="")
+ return self.__get_csvzip(dirname=tmp_dir.name, name=name)
+ def get_name(b_name, tag):
+ return f"{b_name}_{tag}"
+ b_path = os.path.join(tmp_dir.name, get_name(name, "full"))
+ self.__get_full_timeseries_csv(b_path, results, get_name, name)
+ return self.__get_csvzip(dirname=tmp_dir.name, name=get_name(name, "full"))
+
+
+ def get_plot_from_results(self, data_keys, plt_key, add_config=False):
+ '''
+ Get the plotly figure for the results of a job
+
+ Attributes
+ ----------
+ data_keys : dict
+ Dictionary of param names and values used to identify the correct data.
+ plt_key : str
+ Type of plot to generate.
+ add_config : bool
+ Whether or not to add the config key to the plot fig
+ '''
+ try:
+ result = self.__get_filtered_ensemble_results(data_keys)
+ if plt_key == "mltplplt":
+ fig = result.plotplotly(return_plotly_figure=True, multiple_graphs=True)
+ elif plt_key == "stddevran":
+ fig = result.plotplotly_std_dev_range(return_plotly_figure=True)
+ else:
+ if plt_key == "stddev":
+ result = result.stddev_ensemble()
+ elif plt_key == "avg":
+ result = result.average_ensemble()
+ fig = result.plotplotly(return_plotly_figure=True)
+ if add_config and plt_key != "mltplplt":
+ fig["config"] = {"responsive":True}
+ return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder))
+ except FileNotFoundError as err:
+ message = f"Could not find the results pickle file: {str(err)}"
+ raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ except KeyError as err:
+ message = f"The requested plot is not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
+ def get_psweep_csvzip_from_results(self, fixed, name):
+ '''
+ Get the csv file of the parameter sweep plot data for download
+
+ Attributes
+ ----------
+ fixed : dict
+ Dictionary for parameters that remain at a fixed value.
+ '''
+ settings = self.load()['settings']
+ try:
+ self.log("info", "Loading job results...")
+ dims, f_keys = self.__get_fixed_keys_and_dims(settings, fixed)
+ params = list(filter(lambda param: param['name'] not in fixed.keys(),
+ settings['parameterSweepSettings']['parameters']))
+ tmp_dir = tempfile.TemporaryDirectory()
+ if dims == 1:
+ kwargs = {"results": self.__get_filtered_1d_results(f_keys)}
+ kwargs["species"] = list(kwargs['results'][0][0].model.listOfSpecies.keys())
+ self.log("info", "Generating CSV files...")
+ ParameterSweep1D.to_csv(
+ param=params[0], kwargs=kwargs, path=tmp_dir.name, nametag=name
+ )
+ else:
+ kwargs = {"results": self.__get_filtered_2d_results(f_keys, params[0])}
+ kwargs["species"] = list(kwargs['results'][0][0][0].model.listOfSpecies.keys())
+ self.log("info", "Generating CSV files...")
+ ParameterSweep2D.to_csv(
+ params=params, kwargs=kwargs, path=tmp_dir.name, nametag=name
+ )
+ if fixed:
+ self.__write_parameters_csv(path=tmp_dir.name, name=name, data_keys=fixed)
+ return self.__get_csvzip(dirname=tmp_dir.name, name=name)
+ except FileNotFoundError as err:
+ message = f"Could not find the results pickle file: {str(err)}"
+ raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ except KeyError as err:
+ message = f"The requested results are not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
+ def get_psweep_plot_from_results(self, fixed, kwargs, add_config=False):
+ '''
+ Generate and return the parameter sweep plot form the time series results.
+
+ Attributes
+ ----------
+ fixed : dict
+ Dictionary for parameters that remain at a fixed value.
+ kwarps : dict
+ Dictionary of keys used for post proccessing the results.
+ '''
+ settings = self.load()['settings']
+ try:
+ dims, f_keys = self.__get_fixed_keys_and_dims(settings, fixed)
+ params = list(filter(lambda param: param['name'] not in fixed.keys(),
+ settings['parameterSweepSettings']['parameters']))
+ if dims == 1:
+ kwargs['param'] = params[0]
+ kwargs['results'] = self.__get_filtered_1d_results(f_keys)
+ fig = ParameterSweep1D.plot(**kwargs)
+ else:
+ kwargs['params'] = params
+ kwargs['results'] = self.__get_filtered_2d_results(f_keys, params[0])
+ fig = ParameterSweep2D.plot(**kwargs)
+ if add_config:
+ fig['config'] = {"responsive": True}
+ return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder))
+ except FileNotFoundError as err:
+ message = f"Could not find the results pickle file: {str(err)}"
+ raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ except KeyError as err:
+ message = f"The requested plot is not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
+ def load(self):
+ '''
+ Loads a job presentation from cache
+
+ Attributes
+ ----------
+ '''
+ with open(os.path.join(self.path, "job.json"), "r") as job_file:
+ return json.load(job_file)
+
+
+ def update_fig_layout(self, fig=None, plt_data=None):
+ '''
+ Update the figure layout.
+
+ Attributes
+ ----------
+ fig : dict
+ Plotly figure to be updated
+ plt_data : dict
+ Title and axes data for the plot
+ '''
+ self.log("debug", f"Title and axis data for the plot: {plt_data}")
+ try:
+ if plt_data is None:
+ return fig
+ for key in plt_data.keys():
+ if key == "title":
+ fig['layout']['title']['text'] = plt_data[key]
+ else:
+ fig['layout'][key]['title']['text'] = plt_data[key]
+ return fig
+ except KeyError as err:
+ message = f"The requested plot is not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
+class ParameterSweep1D():
+ '''
+ ################################################################################################
+ StochSS 1D parameter sweep job object
+ ################################################################################################
+ '''
+
+ @classmethod
+ def __get_csv_data(cls, results=None, species=None, mapper=None, reducer=None):
+ data = []
+ for specie in species:
+ p_data, _ = cls.__process_results(
+ results=results, species=specie, mapper=mapper, reducer=reducer
+ )
+ data.extend([list(p_data[:,0]), list(p_data[:,1])])
+ return data
+
+
+ @classmethod
+ def __process_results(cls, results, species, mapper="final", reducer="avg"):
+ func_map = {"min": numpy.min, "max": numpy.max, "avg": numpy.mean,
+ "var": numpy.var, "final": lambda res: res[-1]}
+ map_results = [[func_map[mapper](traj[species]) for traj in result] for result in results]
+ if len(map_results[0]) > 1:
+ data = [[func_map[reducer](map_result),
+ numpy.std(map_result)] for map_result in map_results]
+ visible = True
+ else:
+ data = [[map_result[0], 0] for map_result in map_results]
+ visible = False
+ return numpy.array(data), visible
+
+
+ @classmethod
+ def __write_csv_file(cls, path, header, param, data):
+ with open(path, "w") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ csv_writer.writerow(header)
+ for i, val in enumerate(param['range']):
+ line = [val]
+ for col in data:
+ line.append(col[i])
+ csv_writer.writerow(line)
+
+
+ @classmethod
+ def plot(cls, results, species, param, mapper="final", reducer="avg"):
+ '''
+ Plot the results with error bar from time series results.
+
+ Attributes
+ ----------
+ results : list
+ List of GillesPy2 results objects.
+ species : str
+ Species of interest name.
+ param : dict
+ StochSS sweep parameter dictionary.
+ mapper : str
+ Key indicating the feature extraction function to use.
+ reducer : str
+ Key indicating the ensemble aggragation function to use.
+ '''
+ data, visible = cls.__process_results(results=results, species=species,
+ mapper=mapper, reducer=reducer)
+
+ error_y = dict(type="data", array=data[:, 1], visible=visible)
+ trace_list = [plotly.graph_objs.Scatter(x=param['range'],
+ y=data[:, 0], error_y=error_y)]
+
+ title = f"Parameter Sweep - Variable: {species}"
+ layout = plotly.graph_objs.Layout(title=dict(text=title, x=0.5),
+ xaxis=dict(title=f"{param['name']}"),
+ yaxis=dict(title="Population"))
+
+ fig = dict(data=trace_list, layout=layout)
+ return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder))
+
+
+ @classmethod
+ def to_csv(cls, param, kwargs, path=None, nametag="results_csv"):
+ '''
+ Output the post-process results as a series of CSV files.
+
+ Attributes
+ ----------
+ param : dict
+ Sweep parameter object
+ kwargs : dict
+ Filtered results and full list of Model species
+ path : str
+ Parent path to the csv directory
+ nametag : str
+ Name of the csv directory
+ '''
+ if path is None:
+ directory = os.path.join(".", str(nametag))
+ else:
+ directory = os.path.join(path, str(nametag))
+ os.mkdir(directory)
+ # Define header row for all files
+ header = [param['name']]
+ for specie in kwargs['species']:
+ header.extend([specie, f"{specie}-stddev"])
+ # Get all CSV file data
+ mappers = ['min', 'max', 'avg', 'var', 'final']
+ if len(kwargs['results'][0].data) == 1:
+ for mapper in mappers:
+ path = os.path.join(directory, f"{mapper}.csv")
+ # Get csv data for a mapper
+ data = cls.__get_csv_data(**kwargs, mapper=mapper)
+ # Write csv file
+ cls.__write_csv_file(path, header, param, data)
+ else:
+ reducers = mappers[:-1]
+ for mapper in mappers:
+ for reducer in reducers:
+ path = os.path.join(directory, f"{mapper}-{reducer}.csv")
+ # Get csv data for a mapper and a reducer
+ data = cls.__get_csv_data(**kwargs, mapper=mapper, reducer=reducer)
+ # Write csv file
+ cls.__write_csv_file(path, header, param, data)
+
+
+class ParameterSweep2D():
+ '''
+ ################################################################################################
+ StochSS 2D parameter sweep job object
+ ################################################################################################
+ '''
+
+ @classmethod
+ def __get_csv_data(cls, results=None, species=None, mapper=None, reducer=None):
+ data = []
+ for specie in species:
+ p_data = cls.__process_results(
+ results=results, species=specie, mapper=mapper, reducer=reducer
+ )
+ data.append([list(_data) for _data in p_data])
+ return data
+
+
+ @classmethod
+ def __process_results(cls, results, species, mapper="final", reducer="avg"):
+ func_map = {"min": numpy.min, "max": numpy.max, "avg": numpy.mean,
+ "var": numpy.var, "final": lambda res: res[-1]}
+ data = []
+ for p_results in results:
+ map_results = [[func_map[mapper](traj[species]) for traj in result]
+ for result in p_results]
+ if len(map_results[0]) > 1:
+ red_results = [func_map[reducer](map_result) for map_result in map_results]
+ else:
+ red_results = [map_result[0] for map_result in map_results]
+ data.append(red_results)
+ return numpy.array(data)
+
+
+ @classmethod
+ def __write_csv_file(cls, path, header, params, data):
+ with open(path, "w") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ csv_writer.writerow(header)
+ for i, val1 in enumerate(params[0]['range']):
+ for j, val2 in enumerate(params[1]['range']):
+ line = [val1, val2]
+ for col in data:
+ line.append(col[i][j])
+ csv_writer.writerow(line)
+
+
+ @classmethod
+ def plot(cls, results, species, params, mapper="final", reducer="avg"):
+ '''
+ Plot the results with error bar from time series results.
+
+ Attributes
+ ----------
+ results : list
+ List of GillesPy2 results objects.
+ species : str
+ Species of interest name.
+ params : list
+ List of StochSS sweep parameter dictionaries.
+ mapper : str
+ Key indicating the feature extraction function to use.
+ reducer : str
+ Key indicating the ensemble aggragation function to use.
+ '''
+ data = cls.__process_results(results=results, species=species,
+ mapper=mapper, reducer=reducer)
+
+ trace_list = [plotly.graph_objs.Heatmap(z=data, x=params[0]['range'],
+ y=params[1]['range'])]
+
+ title = f"Parameter Sweep - Variable: {species}"
+ layout = plotly.graph_objs.Layout(title=dict(text=title, x=0.5),
+ xaxis=dict(title=f"{params[0]['name']}"),
+ yaxis=dict(title=f"{params[1]['name']}"))
+ fig = dict(data=trace_list, layout=layout)
+ return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder))
+
+
+ @classmethod
+ def to_csv(cls, params, kwargs, path=None, nametag="results_csv"):
+ '''
+ Output the post-process results as a series of CSV files.
+
+ Attributes
+ ----------
+ params : list
+ List of sweep parameter objects
+ kwargs : dict
+ Filtered results and full list of Model species
+ path : str
+ Parent path to the csv directory
+ nametag : str
+ Name of the csv directory
+ '''
+ if path is None:
+ directory = os.path.join(".", str(nametag))
+ else:
+ directory = os.path.join(path, str(nametag))
+ os.mkdir(directory)
+ # Define header row for all files
+ header = [params[0]['name'], params[1]['name']]
+ header.extend(kwargs['species'])
+ # Get all CSV file data
+ mappers = ['min', 'max', 'avg', 'var', 'final']
+ if len(kwargs['results'][0][0].data) == 1:
+ for mapper in mappers:
+ path = os.path.join(directory, f"{mapper}.csv")
+ # Get csv data for a mapper
+ data = cls.__get_csv_data(**kwargs, mapper=mapper)
+ # Write csv file
+ cls.__write_csv_file(path, header, params, data)
+ else:
+ reducers = mappers[:-1]
+ for mapper in mappers:
+ for reducer in reducers:
+ path = os.path.join(directory, f"{mapper}-{reducer}.csv")
+ # Get csv data for a mapper and a reducer
+ data = cls.__get_csv_data(**kwargs, mapper=mapper, reducer=reducer)
+ # Write csv file
+ cls.__write_csv_file(path, header, params, data)
diff --git a/jupyterhub/jupyterhub_config.py b/jupyterhub/jupyterhub_config.py
index 091e9602ab..dc4ddcbfe0 100644
--- a/jupyterhub/jupyterhub_config.py
+++ b/jupyterhub/jupyterhub_config.py
@@ -21,16 +21,19 @@
import os
import os.path
import sys
-import logging
sys.path.append('/srv/jupyterhub/') # pylint: disable=wrong-import-position
# API Handlers
from model_presentation import JsonFileAPIHandler, DownModelPresentationAPIHandler
from notebook_presentation import NotebookAPIHandler, DownNotebookPresentationAPIHandler
+from job_presentation import (
+ JobAPIHandler, DownJobPresentationAPIHandler, PlotJobResultsAPIHandler, DownloadCSVAPIHandler
+)
# Page handlers
from handlers import (
- HomeHandler, JobPresentationHandler, ModelPresentationHandler, NotebookPresentationHandler
+ HomeHandler, JobPresentationHandler, ModelPresentationHandler, NotebookPresentationHandler,
+ MultiplePlotsHandler
)
## Class for authenticating users.
@@ -88,11 +91,16 @@
(r"/stochss/present-job\/?", JobPresentationHandler),
(r"/stochss/present-model\/?", ModelPresentationHandler),
(r"/stochss/present-notebook\/?", NotebookPresentationHandler),
+ (r"/stochss/multiple-plots\/?", MultiplePlotsHandler),
(r"/stochss/api/file/json-data\/?", JsonFileAPIHandler),
(r"/stochss/download_presentation/(\w+)/(.+)\/?", DownModelPresentationAPIHandler),
(r"/stochss/api/notebook/load\/?", NotebookAPIHandler),
(r"/stochss/notebook/download_presentation/(\w+)/(.+)\/?",
- DownNotebookPresentationAPIHandler)
+ DownNotebookPresentationAPIHandler),
+ (r"/stochss/api/job/load\/?", JobAPIHandler),
+ (r"/stochss/job/download_presentation/(\w+)/(.+)\/?", DownJobPresentationAPIHandler),
+ (r"/stochss/api/workflow/plot-results\/?", PlotJobResultsAPIHandler),
+ (r"/stochss/api/job/csv\/?", DownloadCSVAPIHandler)
]
## Paths to search for jinja templates, before using the default templates.
@@ -190,23 +198,21 @@
def get_user_cpu_count_or_fail():
'''
- Get the user cpu count or raise error
+ Get the user cpu count or raise error.
'''
- log = logging.getLogger()
reserve_count = int(os.environ['RESERVED_CPUS'])
- log.info("RESERVED_CPUS environment variable is set to %s", reserve_count)
# Round up to an even number of reserved cpus
+ total_cpus = os.cpu_count()
+ if total_cpus <= 2:
+ c.StochSS.reserved_cpu_count = 0
+ return 0
if reserve_count % 2 > 0:
message = "Increasing reserved cpu count by one so it's an even number."
message += " This helps allocate logical cpus to users more easily."
- log.warning(message)
reserve_count += 1
- total_cpus = os.cpu_count()
- log.info("Total cpu count as reported by os.count: %s", total_cpus)
if reserve_count >= total_cpus:
e_message = "RESERVED_CPUS environment cannot be greater than or equal to the number of"
e_message += " cpus returned by os.cpu_count()"
- log.error(e_message)
raise ValueError(e_message)
user_cpu_count = total_cpus - reserve_count
# If (num logical cpus) - (num reserved cpus) is odd,
@@ -214,44 +220,37 @@ def get_user_cpu_count_or_fail():
if user_cpu_count % 2 > 0 and user_cpu_count > 1:
user_cpu_count -= 1
c.StochSS.reserved_cpu_count = reserve_count
- log.info('Using %s logical cpus for user containers...', user_cpu_count)
- log.info('Reserving %s logical cpus for hub container and underlying OS', reserve_count)
return user_cpu_count
c.StochSS.user_cpu_count = get_user_cpu_count_or_fail()
c.StochSS.user_cpu_alloc = [0] * c.StochSS.user_cpu_count
-def get_power_users():
- '''
- Get the list of power users
- '''
- power_users_file = os.environ.get('POWER_USERS_FILE')
- log = logging.getLogger()
- if not os.path.exists(power_users_file):
- log.warning('No power users defined!')
- return []
- with open(power_users_file) as file:
- power_users = [ x.rstrip() for x in file.readlines() ]
- return power_users
-
-
-c.StochSS.power_users = get_power_users()
-
def pre_spawn_hook(spawner):
'''
Function that runs before DockerSpawner spawns a user container.
Limits the resources available to user containers, excluding a list of power users.
'''
- log = logging.getLogger()
+ log = spawner.log
# Remove the memory limit for power users
- if spawner.user.name in c.StochSS.power_users:
+ if c.StochSS.user_cpu_count == 0:
spawner.mem_limit = None
+ msg = 'Skipping resource limitations since the host machine has a limited number of cpus.'
+ log.info(msg)
+ return
+ user_type = None
+ if spawner.user.name in c.Authenticator.admin_users:
+ user_type = 'admin'
+ elif spawner.user.name in c.StochSS.power_users:
+ user_type = 'power'
+ if user_type:
+ spawner.mem_limit = None
+ log.info(f'Skipping resource limitation for {user_type} user: {spawner.user.name}')
return
palloc = c.StochSS.user_cpu_alloc
div = len(palloc) // 2
reserved = c.StochSS.reserved_cpu_count
- log.warning('Reserved CPUs: %s', reserved)
- log.warning('Number of user containers using each logical core: %s', palloc)
+ log.debug(f'Reserved CPUs: {reserved}')
+ log.debug(f'Number of user containers using each logical core: {palloc}')
# We want to allocate logical cores that are on the same physical core
# whenever possible.
#
@@ -281,37 +280,37 @@ def pre_spawn_hook(spawner):
avail_cpus = palloc
least_used_cpu = min(avail_cpus)
cpu1_index = avail_cpus.index(least_used_cpu)
- log.info("User %s to use logical cpu %s", spawner.user.name, str(cpu1_index))
+ log.info(f"User {spawner.user.name} to use logical cpu {cpu1_index}")
palloc[cpu1_index] += 1
- spawner.extra_host_config['cpuset_cpus'] = '{}'.format(cpu1_index)
+ spawner.extra_host_config['cpuset_cpus'] = f'{cpu1_index}'
else:
least_used_cpu = min(avail_cpus)
cpu1_index = avail_cpus.index(least_used_cpu)
palloc[cpu1_index] += 1
cpu2_index = cpu1_index+div
palloc[cpu2_index] += 1
- log.info("User %s to use logical cpus %s and %s",
- spawner.user.name, str(cpu1_index), str(cpu2_index))
- spawner.extra_host_config['cpuset_cpus'] = '{},{}'.format(cpu1_index, cpu2_index)
+ log.info(f"User {spawner.user.name} to use logical cpus {cpu1_index} and {cpu2_index}")
+ spawner.extra_host_config['cpuset_cpus'] = f'{cpu1_index},{cpu2_index}'
def post_stop_hook(spawner):
'''
Post stop hook
'''
- log = logging.getLogger()
+ log = spawner.log
reserved = c.StochSS.reserved_cpu_count
+ if reserved == 0:
+ return
palloc = c.StochSS.user_cpu_alloc
try:
cpu1_index, cpu2_index = spawner.extra_host_config['cpuset_cpus'].split(',')
palloc[int(cpu1_index)] -= 1
palloc[int(cpu2_index)] -= 1
- log.warning('Reserved CPUs: %s', reserved)
- log.warning('Number of user containers using each logical core: %s', palloc)
+ log.warning(f'Reserved CPUs: {reserved}')
+ log.warning(f'Number of user containers using each logical core: {palloc}')
except Exception as err:
message = "Exception thrown due to cpuset_cpus not being set (power user)"
- log.error("%s\n%s", message, err)
- # Exception thrown due to cpuset_cpus not being set (power user)
+ log.error(f"{message}\n{err}")
pass
c.Spawner.pre_spawn_hook = pre_spawn_hook
@@ -431,6 +430,7 @@ def post_stop_hook(spawner):
#
# Defaults to an empty set, in which case no user has admin access.
c.Authenticator.admin_users = admin = set([])
+c.StochSS.power_users = power_users = set([])
pwd = os.path.dirname(__file__)
with open(os.path.join(pwd, 'userlist')) as f:
@@ -442,5 +442,9 @@ def post_stop_hook(spawner):
if len(parts) >= 1:
name = parts[0]
#whitelist.add(name)
- if len(parts) > 1 and parts[1] == 'admin':
- admin.add(name)
+ if len(parts) > 1:
+ if parts[1] == 'admin':
+ admin.add(name)
+ power_users.add(name)
+ elif parts[1] == 'power':
+ power_users.add(name)
diff --git a/jupyterhub/model_presentation.py b/jupyterhub/model_presentation.py
index c6ba719315..08e320d89d 100644
--- a/jupyterhub/model_presentation.py
+++ b/jupyterhub/model_presentation.py
@@ -20,6 +20,8 @@
import json
import logging
+import plotly
+
from presentation_base import StochSSBase, get_presentation_from_user
from presentation_error import StochSSAPIError, report_error
@@ -50,7 +52,7 @@ async def get(self):
try:
model = get_presentation_from_user(owner=owner, file=file, kwargs={"file": file},
process_func=process_model_presentation)
- log.debug(f"Contents of the json file: {model}")
+ log.debug(f"Contents of the json file: {model['model']}")
self.write(model)
except StochSSAPIError as load_err:
report_error(self, log, load_err)
@@ -73,13 +75,16 @@ async def get(self, owner, file):
log.debug(f"Container id of the owner: {owner}")
log.debug(f"Name to the file: {file}")
self.set_header('Content-Type', 'application/json')
- model = get_presentation_from_user(owner=owner, file=file,
- kwargs={"for_download": True},
- process_func=process_model_presentation)
- ext = file.split(".").pop()
- self.set_header('Content-Disposition', f'attachment; filename="{model["name"]}.{ext}"')
- log.debug(f"Contents of the json file: {model}")
- self.write(model)
+ try:
+ model = get_presentation_from_user(owner=owner, file=file,
+ kwargs={"for_download": True},
+ process_func=process_model_presentation)
+ ext = file.split(".").pop()
+ self.set_header('Content-Disposition', f'attachment; filename="{model["name"]}.{ext}"')
+ log.debug(f"Contents of the json file: {model}")
+ self.write(model)
+ except StochSSAPIError as load_err:
+ report_error(self, log, load_err)
self.finish()
@@ -212,7 +217,7 @@ def load(self):
self.__update_reactions()
self.__update_events(param_ids=param_ids)
self.__update_rules(param_ids=param_ids)
- return self.model
+ return {"model": self.model}
class StochSSSpatialModel(StochSSBase):
@@ -235,6 +240,41 @@ def __init__(self, model):
self.model = model
+ @classmethod
+ def __get_trace_data(cls, particles, name=""):
+ ids = []
+ x_data = []
+ y_data = []
+ z_data = []
+ for particle in particles:
+ ids.append(str(particle['particle_id']))
+ x_data.append(particle['point'][0])
+ y_data.append(particle['point'][1])
+ z_data.append(particle['point'][2])
+ return plotly.graph_objs.Scatter3d(ids=ids, x=x_data, y=y_data, z=z_data,
+ name=name, mode="markers", marker={"size":5})
+
+
+ def __load_domain_plot(self):
+ domain = self.model['domain']
+ trace_list = []
+ for i, d_type in enumerate(domain['types']):
+ if len(domain['types']) > 1:
+ particles = list(filter(lambda particle, key=i: particle['type'] == key,
+ domain['particles']))
+ else:
+ particles = domain['particles']
+ trace = self.__get_trace_data(particles=particles, name=d_type['name'])
+ trace_list.append(trace)
+ layout = {"scene":{"aspectmode":'data'}, "autosize":True}
+ if len(domain['x_lim']) == 2:
+ layout["xaxis"] = {"range":domain['x_lim']}
+ if len(domain['y_lim']) == 2:
+ layout["yaxis"] = {"range":domain['y_lim']}
+ return json.dumps({"data":trace_list, "layout":layout, "config":{"responsive":True}},
+ cls=plotly.utils.PlotlyJSONEncoder)
+
+
def load(self):
'''
Reads the spatial model file, updates it to the current format, and stores it in self.model
@@ -255,4 +295,5 @@ def load(self):
for reaction in self.model['reactions']:
if "types" not in reaction.keys():
reaction['types'] = list(range(1, len(self.model['domain']['types'])))
- return self.model
+ plot = json.loads(self.__load_domain_plot())
+ return {"model": self.model, "domainPlot": plot}
diff --git a/jupyterhub/notebook_presentation.py b/jupyterhub/notebook_presentation.py
index 9d74152c2c..56ac284fab 100644
--- a/jupyterhub/notebook_presentation.py
+++ b/jupyterhub/notebook_presentation.py
@@ -24,6 +24,7 @@
from nbconvert.exporters import HTMLExporter
from presentation_base import get_presentation_from_user
+from presentation_error import StochSSAPIError, report_error
from jupyterhub.handlers.base import BaseHandler
@@ -49,9 +50,12 @@ async def get(self):
log.debug(f"Container id of the owner: {owner}")
file = self.get_query_argument(name="file")
log.debug(f"Name to the file: {file}")
- html = get_presentation_from_user(owner=owner, file=file,
- process_func=process_notebook_presentation)
- self.write(html)
+ try:
+ html = get_presentation_from_user(owner=owner, file=file,
+ process_func=process_notebook_presentation)
+ self.write(html)
+ except StochSSAPIError as load_err:
+ report_error(self, log, load_err)
self.finish()
@@ -71,12 +75,16 @@ async def get(self, owner, file):
log.debug(f"Container id of the owner: {owner}")
log.debug(f"Name to the file: {file}")
self.set_header('Content-Type', 'application/json')
- nb_presentation = get_presentation_from_user(owner=owner, file=file,
- kwargs={"as_dict": True},
- process_func=process_notebook_presentation)
- self.set_header('Content-Disposition', f'attachment; filename="{nb_presentation["file"]}"')
- log.debug(f"Contents of the json file: {nb_presentation['notebook']}")
- self.write(nb_presentation['notebook'])
+ try:
+ nb_presentation = get_presentation_from_user(owner=owner, file=file,
+ kwargs={"as_dict": True},
+ process_func=process_notebook_presentation)
+ self.set_header('Content-Disposition',
+ f'attachment; filename="{nb_presentation["file"]}"')
+ log.debug(f"Contents of the json file: {nb_presentation['notebook']}")
+ self.write(nb_presentation['notebook'])
+ except StochSSAPIError as load_err:
+ report_error(self, log, load_err)
self.finish()
diff --git a/jupyterhub/presentation_error.py b/jupyterhub/presentation_error.py
index 79cfe04e07..4f33d6bb78 100644
--- a/jupyterhub/presentation_error.py
+++ b/jupyterhub/presentation_error.py
@@ -116,3 +116,48 @@ def __init__(self, msg, trace=None):
Error traceback for the error
'''
super().__init__(406, "File Data Not JSON Format", msg, trace)
+
+####################################################################################################
+# Job Errors
+####################################################################################################
+
+class PlotNotAvailableError(StochSSAPIError):
+ '''
+ ################################################################################################
+ StochSS Result Plot Not Found
+ ################################################################################################
+ '''
+
+ def __init__(self, msg, trace=None):
+ '''
+ Indicates that the requested plot was not found in the plots.json file
+
+ Attributes
+ ----------
+ msg : str
+ Details on what caused the error
+ trace : str
+ Error traceback for the error
+ '''
+ super().__init__(406, "Plot Figure Not Available", msg, trace)
+
+
+class StochSSJobResultsError(StochSSAPIError):
+ '''
+ ################################################################################################
+ StochSS Job Results Error
+ ################################################################################################
+ '''
+
+ def __init__(self, msg, trace=None):
+ '''
+ Indicates that the job results object was corrupted
+
+ Attributes
+ ----------
+ msg : str
+ Details on what caused the error
+ trace : str
+ Error traceback for the error
+ '''
+ super().__init__(500, "Job Results Error", msg, trace)
diff --git a/stochss/dist/favicon.ico b/stochss/dist/favicon.ico
new file mode 100644
index 0000000000..df7bf93e65
Binary files /dev/null and b/stochss/dist/favicon.ico differ
diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py
index de707d4ddb..30adcddd42 100644
--- a/stochss/handlers/__init__.py
+++ b/stochss/handlers/__init__.py
@@ -46,7 +46,6 @@ def get_page_handlers(route_start):
(r'/stochss/workflow/selection\/?', WorkflowSelectionHandler),
(r'/stochss/workflow/edit\/?', WorkflowEditorHandler),
(r'/stochss/quickstart\/?', QuickstartHandler),
- (r'/stochss/project/browser\/?', ProjectBrowserHandler),
(r'/stochss/project/manager\/?', ProjectManagerHandler),
(r'/stochss/loading-page\/?', LoadingPageHandler),
(r'/stochss/multiple-plots\/?', MultiplePlotsHandler),
@@ -66,6 +65,7 @@ def get_page_handlers(route_start):
(r"/stochss/api/file/json-data\/?", JsonFileAPIHandler),
(r"/stochss/api/file/duplicate\/?", DuplicateModelHandler),
(r"/stochss/api/file/unzip\/?", UnzipFileAPIHandler),
+ (r"/stochss/api/file/presentations\/?", PresentationListAPIHandler),
(r"/stochss/api/notebook/presentation\/?", NotebookPresentationAPIHandler),
(r"/stochss/api/directory/duplicate\/?", DuplicateDirectoryHandler),
(r"/stochss/api/directory/create\/?", CreateDirectoryHandler),
@@ -108,7 +108,8 @@ def get_page_handlers(route_start):
(r"/stochss/api/workflow/save-plot\/?", SavePlotAPIHandler),
(r"/stochss/api/workflow/save-annotation\/?", SaveAnnotationAPIHandler),
(r"/stochss/api/workflow/update-format\/?", UpadteWorkflowAPIHandler),
- (r"/stochss/api/job/presentation\/?", JobPresentationAPIHandler)
+ (r"/stochss/api/job/presentation\/?", JobPresentationAPIHandler),
+ (r"/stochss/api/job/csv\/?", DownloadCSVZipAPIHandler)
]
full_handlers = list(map(lambda h: (url_path_join(route_start, h[0]), h[1]), handlers))
return full_handlers
diff --git a/stochss/handlers/file_browser.py b/stochss/handlers/file_browser.py
index 7f16724892..a64ebe3734 100644
--- a/stochss/handlers/file_browser.py
+++ b/stochss/handlers/file_browser.py
@@ -684,10 +684,18 @@ async def get(self):
cmd = self.get_query_argument(name="cmd", default=None)
log.debug(f"The command for the upload script: {cmd}")
script = '/stochss/stochss/handlers/util/scripts/upload_remote_file.py'
- if cmd is None:
+ if cmd == "validate":
+ folder = StochSSFolder(path="")
+ resp = {'exists': folder.validate_upload_link(remote_path=path)}
+ log.debug(f"Response: {resp}")
+ self.write(resp)
+ elif cmd is None:
+ overwrite = self.get_query_argument(name='overwrite', default=False)
outfile = f"{str(uuid.uuid4()).replace('-', '_')}.tmp"
log.debug(f"Response file name: {outfile}")
exec_cmd = [script, f'{path}', f'{outfile}'] # Script commands for read run_cmd
+ if overwrite:
+ exec_cmd.append('-o')
log.debug(f"Exec command: {exec_cmd}")
pipe = subprocess.Popen(exec_cmd)
resp = {"responsePath": outfile}
@@ -764,3 +772,29 @@ async def get(self):
except StochSSAPIError as err:
report_error(self, log, err)
self.finish()
+
+
+class PresentationListAPIHandler(APIHandler):
+ '''
+ ################################################################################################
+ Handler for getting the users full list of presentations.
+ ################################################################################################
+ '''
+ @web.authenticated
+ async def get(self):
+ '''
+ Get the list of presentations.
+
+ Attributes
+ ----------
+ '''
+ self.set_header('Content-Type', 'application/json')
+ try:
+ log.info("Loading presentations...")
+ presentations = StochSSFolder.get_presentations()
+ log.debug(f"List of presentations: {presentations}")
+ log.info("Loading complete.")
+ self.write({"presentations": presentations})
+ except StochSSAPIError as err:
+ report_error(self, log, err)
+ self.finish()
diff --git a/stochss/handlers/pages.py b/stochss/handlers/pages.py
index e2961bb4fa..7ca148a5c0 100644
--- a/stochss/handlers/pages.py
+++ b/stochss/handlers/pages.py
@@ -197,23 +197,6 @@ async def get(self):
self.render("stochss-workflow-manager.html", server_path=self.get_server_path())
-class ProjectBrowserHandler(PageHandler):
- '''
- ################################################################################################
- StochSS Project Browser Page Handler
- ################################################################################################
- '''
- @web.authenticated
- async def get(self):
- '''
- Render the StochSS project browser page.
-
- Attributes
- ----------
- '''
- self.render("stochss-project-browser.html", server_path=self.get_server_path())
-
-
class ProjectManagerHandler(PageHandler):
'''
################################################################################################
diff --git a/stochss/handlers/util/ensemble_simulation.py b/stochss/handlers/util/ensemble_simulation.py
index 5dd7a7761a..949fe70b36 100644
--- a/stochss/handlers/util/ensemble_simulation.py
+++ b/stochss/handlers/util/ensemble_simulation.py
@@ -69,13 +69,6 @@ def __get_run_settings(self):
return run_settings
- def __store_csv_results(self, results):
- try:
- results.to_csv(path="results", nametag="results_csv", stamp=self.get_time_stamp())
- except Exception as err:
- log.error(f"Error storing csv results: {err}\n{traceback.format_exc()}")
-
-
@classmethod
def __store_pickled_results(cls, results):
try:
@@ -131,10 +124,9 @@ def run(self, preview=False, verbose=True):
results = self.g_model.run(**kwargs)
if verbose:
log.info("The ensemble simulation has completed")
- log.info("Storing the results as pickle and csv")
+ log.info("Storing the results as pickle")
if not 'results' in os.listdir():
os.mkdir('results')
- self.__store_csv_results(results=results)
pkl_err = self.__store_pickled_results(results=results)
if verbose:
log.info("Storing the polts of the results")
diff --git a/stochss/handlers/util/parameter_sweep.py b/stochss/handlers/util/parameter_sweep.py
index 2ed6212db5..62390f4e55 100644
--- a/stochss/handlers/util/parameter_sweep.py
+++ b/stochss/handlers/util/parameter_sweep.py
@@ -96,32 +96,6 @@ def __report_result_error(cls, trace):
raise StochSSJobResultsError(message, trace)
- def __store_csv_results(self, job):
- try:
- if "solver" in job.settings.keys():
- solver_name = job.settings['solver'].name
- else:
- solver_name = job.model.get_best_solver().name
- if "ODE" not in solver_name and job.settings['number_of_trajectories'] > 1:
- csv_keys = list(itertools.product(["min", "max", "avg", "var", "final"],
- ["min", "max", "avg", "var"]))
- else:
- csv_keys = [["min"], ["max"], ["avg"], ["var"], ["final"]]
- stamp = self.get_time_stamp()
- dirname = f"results/results_csv{stamp}"
- if not os.path.exists(dirname):
- os.mkdir(dirname)
- for key in csv_keys:
- if not isinstance(key, list):
- key = list(key)
- path = os.path.join(dirname, f"{'-'.join(key)}.csv")
- with open(path, "w", newline="") as csv_file:
- csv_writer = csv.writer(csv_file)
- job.to_csv(keys=key, csv_writer=csv_writer)
- except Exception as err:
- log.error(f"Error storing csv results: {err}\n{traceback.format_exc()}")
-
-
@classmethod
def __store_pickled_results(cls, job):
try:
@@ -134,18 +108,6 @@ def __store_pickled_results(cls, job):
return False
- @classmethod
- def __store_results(cls, job):
- try:
- with open('results/results.json', 'w') as json_file:
- json.dump(job.results, json_file, indent=4, sort_keys=True, cls=NumpyEncoder)
- except Exception as err:
- message = f"Error storing results dictionary: {err}\n{traceback.format_exc()}"
- log.err(message)
- return message
- return False
-
-
def configure(self):
'''
Get the configuration arguments for 1D or 2D parameter sweep
@@ -199,16 +161,9 @@ def run(self, verbose=True):
raise StochSSJobError(message)
if verbose:
log.info(f"The {sim_type} has completed")
- log.info("Storing the results as pickle and csv")
+ log.info("Storing the results as pickle.")
if not 'results' in os.listdir():
os.mkdir('results')
pkl_err = self.__store_pickled_results(job=job)
- if job.name != "ParameterScan":
- if verbose:
- log.info("Storing the polts of the results")
- res_err = self.__store_results(job=job)
- self.__store_csv_results(job=job)
- if pkl_err and res_err:
- self.__report_result_error(trace=f"{res_err}\n{pkl_err}")
- elif pkl_err:
+ if pkl_err:
self.__report_result_error(trace=pkl_err)
diff --git a/stochss/handlers/util/parameter_sweep_1d.py b/stochss/handlers/util/parameter_sweep_1d.py
index d84471094f..14e0d2b3df 100644
--- a/stochss/handlers/util/parameter_sweep_1d.py
+++ b/stochss/handlers/util/parameter_sweep_1d.py
@@ -16,6 +16,8 @@
along with this program. If not, see .
'''
+import os
+import csv
import json
import copy
import logging
@@ -59,111 +61,42 @@ def __init__(self, model, settings, param):
self.ts_results = {}
- def __ensemble_feature_extraction(self, results, index, verbose=False):
- func_map = {"min":numpy.min, "max":numpy.max, "avg":numpy.mean, "var":numpy.var}
- for species in self.list_of_species:
- for m_key in self.MAPPER_KEYS:
- if m_key == "final":
- m_data = [x[species][-1] for x in results]
- else:
- m_data = [func_map[m_key](x[species]) for x in results]
- if verbose:
- log.debug(f' {m_key} population {species}={m_data}')
- std = numpy.std(m_data)
- for r_key in self.REDUCER_KEYS:
- r_data = func_map[r_key](m_data)
- self.results[species][m_key][r_key][index, 0] = r_data
- self.results[species][m_key][r_key][index, 1] = std
- if verbose:
- log.debug(f' {r_key} std of ensemble m:{r_data} s:{std}')
-
-
- def __feature_extraction(self, results, index, verbose=False):
- func_map = {"min":numpy.min, "max":numpy.max, "avg":numpy.mean, "var":numpy.var}
- for species in self.list_of_species:
- spec_res = results[species]
- for key in self.MAPPER_KEYS:
- if key == "final":
- data = spec_res[-1]
- else:
- data = func_map[key](spec_res)
- self.results[species][key][index, 0] = data
- if verbose:
- log.debug(f' {key} population {species}={data}')
-
-
- def __setup_results(self, solver_name):
- for species in self.list_of_species:
- spec_res = {}
- if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1:
- for m_key in self.MAPPER_KEYS:
- spec_res[m_key] = {}
- for r_key in self.REDUCER_KEYS:
- spec_res[m_key][r_key] = numpy.zeros((len(self.param['range']), 2))
- else:
- for key in self.MAPPER_KEYS:
- spec_res[key] = numpy.zeros((len(self.param['range']), 2))
- self.results[species] = spec_res
-
-
- def get_plotly_layout_data(self, plt_data):
- '''
- Get plotly axes labels for layout
-
- Attributes
- ----------
- plt_data : dict
- Existing layout data
- '''
- if "xaxis_label" not in plt_data:
- plt_data['xaxis_label'] = f"{self.param['parameter']}"
- if "yaxis_label" not in plt_data:
- plt_data['yaxis_label'] = "Population"
-
+ @classmethod
+ def __get_csv_data(cls, results=None, species=None, mapper=None, reducer=None):
+ data = []
+ for specie in species:
+ p_data, _ = cls.__process_results(
+ results=results, species=specie, mapper=mapper, reducer=reducer
+ )
+ data.extend([list(p_data[:,0]), list(p_data[:,1])])
+ return data
- def get_plotly_traces(self, keys):
- '''
- Get the plotly trace list
- Attributes
- ----------
- key : list
- Identifiers for the results data
- '''
- if len(keys) > 2:
- results = self.results[keys[0]][keys[1]][keys[2]]
+ @classmethod
+ def __process_results(cls, results, species, mapper="final", reducer="avg"):
+ func_map = {"min": numpy.min, "max": numpy.max, "avg": numpy.mean,
+ "var": numpy.var, "final": lambda res: res[-1]}
+ map_results = [[func_map[mapper](traj[species]) for traj in result] for result in results]
+ if len(map_results[0]) > 1:
+ data = [[func_map[reducer](map_result),
+ numpy.std(map_result)] for map_result in map_results]
+ visible = True
else:
- results = self.results[keys[0]][keys[1]]
-
- visible = "number_of_trajectories" in self.settings.keys() and \
- self.settings['number_of_trajectories'] > 1
- error_y = dict(type="data", array=results[:, 1], visible=visible)
-
- trace_list = [plotly.graph_objs.Scatter(x=self.param['range'],
- y=results[:, 0], error_y=error_y)]
- return trace_list
-
-
- # def plot(self, keys=None):
- # '''
- # Plot the results based on the keys using matplotlib
+ data = [[map_result[0], 0] for map_result in map_results]
+ visible = False
+ return numpy.array(data), visible
- # Attributes
- # ----------
- # key : list
- # Identifiers for the results data
- # '''
- # if len(keys) > 2:
- # results = self.results[keys[0]][keys[1]][keys[2]]
- # else:
- # results = self.results[keys[0]][keys[1]]
- # matplotlib.pyplot.subplots(figsize=(8, 8))
- # matplotlib.pyplot.title(f"Parameter Sweep - Variable: {keys[0]}")
- # matplotlib.pyplot.errorbar(self.param['range'], results[:, 0], results[:, 1])
- # matplotlib.pyplot.xlabel(self.param['parameter'],
- # fontsize=16, fontweight='bold')
- # matplotlib.pyplot.ylabel("Population", fontsize=16, fontweight='bold')
+ @classmethod
+ def __write_csv_file(cls, path, header, param, data):
+ with open(path, "w") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ csv_writer.writerow(header)
+ for i, val in enumerate(param['range']):
+ line = [val]
+ for col in data:
+ line.append(col[i])
+ csv_writer.writerow(line)
@classmethod
@@ -184,17 +117,8 @@ def plot(cls, results, species, param, mapper="final", reducer="avg"):
reducer : str
Key indicating the ensemble aggragation function to use.
'''
- func_map = {"min": numpy.min, "max": numpy.max, "avg": numpy.mean,
- "var": numpy.var, "final": lambda res: res[-1]}
- map_results = [[func_map[mapper](traj[species]) for traj in result] for result in results]
- if len(map_results[0]) > 1:
- data = [[func_map[reducer](map_result),
- numpy.std(map_result)] for map_result in map_results]
- visible = True
- else:
- data = [[map_result[0], 0] for map_result in map_results]
- visible = False
- data = numpy.array(data)
+ data, visible = cls.__process_results(results=results, species=species,
+ mapper=mapper, reducer=reducer)
error_y = dict(type="data", array=data[:, 1], visible=visible)
trace_list = [plotly.graph_objs.Scatter(x=param['range'],
@@ -220,8 +144,7 @@ def run(self, job_id, verbose=False):
solver_name = self.settings['solver'].name
else:
solver_name = self.model.get_best_solver().name
- self.__setup_results(solver_name=solver_name)
- for i, val in enumerate(self.param['range']):
+ for val in self.param['range']:
if solver_name in ["SSACSolver", "TauLeapingCSolver", "ODECSolver"]:
tmp_mdl = self.model
self.settings['variables'] = {self.param['parameter']:val}
@@ -237,34 +160,48 @@ def run(self, job_id, verbose=False):
else:
key = f"{self.param['parameter']}:{val}"
self.ts_results[key] = tmp_res
- if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1:
- self.__ensemble_feature_extraction(results=tmp_res, index=i, verbose=verbose)
- else:
- self.__feature_extraction(results=tmp_res, index=i, verbose=verbose)
- def to_csv(self, keys, csv_writer):
+ @classmethod
+ def to_csv(cls, param, kwargs, path=None, nametag="results_csv"):
'''
- Convert self.results to a csv file
+ Output the post-process results as a series of CSV files.
Attributes
----------
- key : list
- Identifiers for the results data
- csv_writer : obj
- CSV file writer.
+ param : dict
+ Sweep parameter object
+ kwargs : dict
+ Filtered results and full list of Model species
+ path : str
+ Parent path to the csv directory
+ nametag : str
+ Name of the csv directory
'''
- results = []
- names = [self.param['parameter']]
- for species in self.list_of_species:
- names.extend([species, f"{species}-stddev"])
- if len(keys) > 1:
- results.append(self.results[species][keys[0]][keys[1]])
- else:
- results.append(self.results[species][keys[0]])
- csv_writer.writerow(names)
- for i, step in enumerate(self.param['range']):
- line = [step]
- for res in results:
- line.extend([res[i, 0], res[i, 1]])
- csv_writer.writerow(line)
+ if path is None:
+ directory = os.path.join(".", str(nametag))
+ else:
+ directory = os.path.join(path, str(nametag))
+ os.mkdir(directory)
+ # Define header row for all files
+ header = [param['name']]
+ for specie in kwargs['species']:
+ header.extend([specie, f"{specie}-stddev"])
+ # Get all CSV file data
+ mappers = ['min', 'max', 'avg', 'var', 'final']
+ if len(kwargs['results'][0].data) == 1:
+ for mapper in mappers:
+ path = os.path.join(directory, f"{mapper}.csv")
+ # Get csv data for a mapper
+ data = cls.__get_csv_data(**kwargs, mapper=mapper)
+ # Write csv file
+ cls.__write_csv_file(path, header, param, data)
+ else:
+ reducers = mappers[:-1]
+ for mapper in mappers:
+ for reducer in reducers:
+ path = os.path.join(directory, f"{mapper}-{reducer}.csv")
+ # Get csv data for a mapper and a reducer
+ data = cls.__get_csv_data(**kwargs, mapper=mapper, reducer=reducer)
+ # Write csv file
+ cls.__write_csv_file(path, header, param, data)
diff --git a/stochss/handlers/util/parameter_sweep_2d.py b/stochss/handlers/util/parameter_sweep_2d.py
index 44a5ef9f6f..f4cd173d57 100644
--- a/stochss/handlers/util/parameter_sweep_2d.py
+++ b/stochss/handlers/util/parameter_sweep_2d.py
@@ -16,6 +16,8 @@
along with this program. If not, see .
'''
+import os
+import csv
import json
import copy
import logging
@@ -60,113 +62,44 @@ def __init__(self, model, settings, params):
self.results = {}
- def __ensemble_feature_extraction(self, results, i_ndx, j_ndx, verbose=False):
- func_map = {"min":numpy.min, "max":numpy.max, "avg":numpy.mean, "var":numpy.var}
- for species in self.list_of_species:
- for m_key in self.MAPPER_KEYS:
- if m_key == "final":
- m_data = [x[species][-1] for x in results]
- else:
- m_data = [func_map[m_key](x[species]) for x in results]
- if verbose:
- log.debug(f' {m_key} population {species}={m_data}')
- for r_key in self.REDUCER_KEYS:
- r_data = func_map[r_key](m_data)
- self.results[species][m_key][r_key][i_ndx, j_ndx] = r_data
- if verbose:
- log.debug(f' {r_key} of ensemble = {r_data}')
-
-
- def __feature_extraction(self, results, i_ndx, j_ndx, verbose=False):
- func_map = {"min":numpy.min, "max":numpy.max, "avg":numpy.mean, "var":numpy.var}
- for species in self.list_of_species:
- spec_res = results[species]
- for key in self.MAPPER_KEYS:
- if key == "final":
- data = spec_res[-1]
- else:
- data = func_map[key](spec_res)
- self.results[species][key][i_ndx, j_ndx] = data
- if verbose:
- log.debug(f' {key} population {species}={data}')
+ @classmethod
+ def __get_csv_data(cls, results=None, species=None, mapper=None, reducer=None):
+ data = []
+ for specie in species:
+ p_data = cls.__process_results(
+ results=results, species=specie, mapper=mapper, reducer=reducer
+ )
+ data.append([list(_data) for _data in p_data])
+ return data
- def __setup_results(self, solver_name):
- for species in self.list_of_species:
- spec_res = {}
- if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1:
- for m_key in self.MAPPER_KEYS:
- spec_res[m_key] = {}
- for r_key in self.REDUCER_KEYS:
- spec_res[m_key][r_key] = numpy.zeros((len(self.params[0]['range']),
- len(self.params[1]['range'])))
+ @classmethod
+ def __process_results(cls, results, species, mapper="final", reducer="avg"):
+ func_map = {"min": numpy.min, "max": numpy.max, "avg": numpy.mean,
+ "var": numpy.var, "final": lambda res: res[-1]}
+ data = []
+ for p_results in results:
+ map_results = [[func_map[mapper](traj[species]) for traj in result]
+ for result in p_results]
+ if len(map_results[0]) > 1:
+ red_results = [func_map[reducer](map_result) for map_result in map_results]
else:
- for key in self.MAPPER_KEYS:
- spec_res[key] = numpy.zeros((len(self.params[0]['range']),
- len(self.params[1]['range'])))
- self.results[species] = spec_res
-
-
- def get_plotly_layout_data(self, plt_data):
- '''
- Get plotly axes labels for layout
-
- Attributes
- ----------
- plt_data : dict
- Existing layout data
- '''
- if "yaxis_label" not in plt_data:
- plt_data['yaxis_label'] = f"{self.params[1]['parameter']}"
- if "xaxis_label" not in plt_data:
- plt_data['xaxis_label'] = f"{self.params[0]['parameter']}"
-
-
- def get_plotly_traces(self, keys):
- '''
- Get the plotly trace list
-
- Attributes
- ----------
- key : list
- Identifiers for the results data
- '''
- if len(keys) <= 2:
- results = self.results[keys[0]][keys[1]]
- else:
- results = self.results[keys[0]][keys[1]][keys[2]]
-
- trace_list = [plotly.graph_objs.Heatmap(z=results, x=self.params[0]['range'],
- y=self.params[1]['range'])]
- return trace_list
-
-
- # def plot(self, keys=None):
- # '''
- # Plot the results based on the keys using matplotlib
+ red_results = [map_result[0] for map_result in map_results]
+ data.append(red_results)
+ return numpy.array(data)
- # Attributes
- # ----------
- # key : list
- # Identifiers for the results data
- # '''
- # if len(keys) <= 2:
- # results = self.results[keys[0]][keys[1]]
- # else:
- # results = self.results[keys[0]][keys[1]][keys[2]]
- # _, axis = matplotlib.pyplot.subplots(figsize=(8, 8))
- # matplotlib.pyplot.imshow(results)
- # axis.set_xticks(numpy.arange(results.shape[1])+0.5, minor=False)
- # axis.set_yticks(numpy.arange(results.shape[0])+0.5, minor=False)
- # matplotlib.pyplot.title(f"Parameter Sweep - Variable: {keys[0]}")
- # axis.set_xticklabels(self.params[0]['range'], minor=False, rotation=90)
- # axis.set_yticklabels(self.params[1]['range'], minor=False)
- # axis.set_xlabel(self.params[0]['parameter'], fontsize=16, fontweight='bold')
- # axis.set_ylabel(self.params[1]['parameter'], fontsize=16, fontweight='bold')
- # divider = mpl_toolkits.axes_grid1.make_axes_locatable(axis)
- # cax = divider.append_axes("right", size="5%", pad=0.2)
- # _ = matplotlib.pyplot.colorbar(ax=axis, cax=cax)
+ @classmethod
+ def __write_csv_file(cls, path, header, params, data):
+ with open(path, "w") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ csv_writer.writerow(header)
+ for i, val1 in enumerate(params[0]['range']):
+ for j, val2 in enumerate(params[1]['range']):
+ line = [val1, val2]
+ for col in data:
+ line.append(col[i][j])
+ csv_writer.writerow(line)
@classmethod
@@ -187,18 +120,8 @@ def plot(cls, results, species, params, mapper="final", reducer="avg"):
reducer : str
Key indicating the ensemble aggragation function to use.
'''
- func_map = {"min": numpy.min, "max": numpy.max, "avg": numpy.mean,
- "var": numpy.var, "final": lambda res: res[-1]}
- data = []
- for p_results in results:
- map_results = [[func_map[mapper](traj[species]) for traj in result]
- for result in p_results]
- if len(map_results[0]) > 1:
- red_results = [func_map[reducer](map_result) for map_result in map_results]
- else:
- red_results = [map_result[0] for map_result in map_results]
- data.append(red_results)
- data = numpy.array(data)
+ data = cls.__process_results(results=results, species=species,
+ mapper=mapper, reducer=reducer)
trace_list = [plotly.graph_objs.Heatmap(z=data, x=params[0]['range'],
y=params[1]['range'])]
@@ -222,9 +145,8 @@ def run(self, job_id, verbose=False):
solver_name = self.settings['solver'].name
else:
solver_name = self.model.get_best_solver().name
- self.__setup_results(solver_name=solver_name)
- for i, val1 in enumerate(self.params[0]['range']):
- for j, val2 in enumerate(self.params[1]['range']):
+ for val1 in self.params[0]['range']:
+ for val2 in self.params[1]['range']:
if solver_name in ["SSACSolver", "TauLeapingCSolver", "ODECSolver"]:
tmp_mdl = self.model
variables = {self.params[0]['parameter']:val1, self.params[1]['parameter']:val2}
@@ -245,37 +167,47 @@ def run(self, job_id, verbose=False):
key = f"{self.params[0]['parameter']}:{val1},"
key += f"{self.params[1]['parameter']}:{val2}"
self.ts_results[key] = tmp_res
- if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1:
- self.__ensemble_feature_extraction(results=tmp_res, i_ndx=i, j_ndx=j,
- verbose=verbose)
- else:
- self.__feature_extraction(results=tmp_res, i_ndx=i, j_ndx=j,
- verbose=verbose)
- def to_csv(self, keys, csv_writer):
+ @classmethod
+ def to_csv(cls, params, kwargs, path=None, nametag="results_csv"):
'''
- Convert self.results to a csv file
+ Output the post-process results as a series of CSV files.
Attributes
----------
- key : list
- Identifiers for the results data
- csv_writer : obj
- CSV file writer.
+ params : list
+ List of sweep parameter objects
+ kwargs : dict
+ Filtered results and full list of Model species
+ path : str
+ Parent path to the csv directory
+ nametag : str
+ Name of the csv directory
'''
- results = []
- names = [self.params[0]['parameter'], self.params[1]['parameter']]
- names.extend(self.list_of_species)
- for species in self.list_of_species:
- if len(keys) <= 1:
- results.append(self.results[species][keys[0]])
- else:
- results.append(self.results[species][keys[0]][keys[1]])
- csv_writer.writerow(names)
- for i, step1 in enumerate(self.params[0]['range']):
- for j, step2 in enumerate(self.params[1]['range']):
- line = [step1, step2]
- for res in results:
- line.append(res[i, j])
- csv_writer.writerow(line)
+ if path is None:
+ directory = os.path.join(".", str(nametag))
+ else:
+ directory = os.path.join(path, str(nametag))
+ os.mkdir(directory)
+ # Define header row for all files
+ header = [params[0]['name'], params[1]['name']]
+ header.extend(kwargs['species'])
+ # Get all CSV file data
+ mappers = ['min', 'max', 'avg', 'var', 'final']
+ if len(kwargs['results'][0][0].data) == 1:
+ for mapper in mappers:
+ path = os.path.join(directory, f"{mapper}.csv")
+ # Get csv data for a mapper
+ data = cls.__get_csv_data(**kwargs, mapper=mapper)
+ # Write csv file
+ cls.__write_csv_file(path, header, params, data)
+ else:
+ reducers = mappers[:-1]
+ for mapper in mappers:
+ for reducer in reducers:
+ path = os.path.join(directory, f"{mapper}-{reducer}.csv")
+ # Get csv data for a mapper and a reducer
+ data = cls.__get_csv_data(**kwargs, mapper=mapper, reducer=reducer)
+ # Write csv file
+ cls.__write_csv_file(path, header, params, data)
diff --git a/stochss/handlers/util/scripts/run_preview.py b/stochss/handlers/util/scripts/run_preview.py
index 97c07c6253..0c2de0c989 100755
--- a/stochss/handlers/util/scripts/run_preview.py
+++ b/stochss/handlers/util/scripts/run_preview.py
@@ -79,17 +79,60 @@ def get_parsed_args():
return parser.parse_args()
-def run_preview(job):
+def run_spatialpy_preview(args):
'''
- Run the preview simulation
+ Run a spatialpy preview simulation.
- wkfl : StochSSJob instance
- The wkfl used for the preview simulation
+ Attributes
+ ----------
+ args : argparse object
+ Command line args passed to the script
+ '''
+ model = StochSSSpatialModel(path=args.path)
+ job = SpatialSimulation(path="", preview=True, target=args.target)
+ job.s_py_model = model.convert_to_spatialpy()
+ job.s_model = model.model
+ return job.run(preview=True)
+
+
+def run_gillespy2_preview(args):
+ '''
+ Run a gillespy2 preview simulation
+
+ Attributes
+ ----------
+ args : argparse object
+ Command line args passed to the script
+ '''
+ log_stm, f_handler = setup_logger()
+ model = StochSSModel(path=args.path)
+ job = EnsembleSimulation(path="", preview=True)
+ job.g_model = model.convert_to_gillespy2()
+ plot = job.run(preview=True)
+ timeout = 'GillesPy2 simulation exceeded timeout.' in log_stm.getvalue()
+ log_stm.close()
+ f_handler.close()
+ return plot, timeout
+
+
+def run_preview(args):
+ '''
+ Run a preview simulation
+
+ Attributes
+ ----------
+ args : argparse object
+ Command line args passed to the script
'''
+ is_spatial = args.path.endswith(".smdl")
response = {"timeout": False}
try:
- plot = job.run(preview=True)
- response["results"] = plot
+ if is_spatial:
+ response['results'] = run_spatialpy_preview(args)
+ else:
+ fig, timeout = run_gillespy2_preview(args)
+ response['results'] = fig
+ response['timeout'] = timeout
except ModelError as error:
response['errors'] = f"{error}"
except SimulationError as error:
@@ -102,27 +145,11 @@ def run_preview(job):
if __name__ == "__main__":
+ user_dir = StochSSModel.user_dir
log.info("Initializing the preview simulation")
- args = get_parsed_args()
- is_spatial = args.path.endswith(".smdl")
- if is_spatial:
- model = StochSSSpatialModel(path=args.path)
- wkfl = SpatialSimulation(path="", preview=True, target=args.target)
- wkfl.s_py_model = model.convert_to_spatialpy()
- wkfl.s_model = model.model
- else:
- log_stm, f_handler = setup_logger()
- model = StochSSModel(path=args.path)
- wkfl = EnsembleSimulation(path="", preview=True)
- wkfl.g_model = model.convert_to_gillespy2()
- resp = run_preview(wkfl)
- if not is_spatial:
- if 'GillesPy2 simulation exceeded timeout.' in log_stm.getvalue():
- resp['timeout'] = True
- log_stm.close()
- f_handler.close()
-
- outfile = os.path.join(model.user_dir, f".{args.outfile}.tmp")
+ cargs = get_parsed_args()
+ resp = run_preview(cargs)
+ outfile = os.path.join(user_dir, f".{cargs.outfile}.tmp")
with open(outfile, "w") as file:
json.dump(resp, file, cls=plotly.utils.PlotlyJSONEncoder,
indent=4, sort_keys=True)
diff --git a/stochss/handlers/util/scripts/upload_remote_file.py b/stochss/handlers/util/scripts/upload_remote_file.py
index e92073cdd7..12db185ba5 100755
--- a/stochss/handlers/util/scripts/upload_remote_file.py
+++ b/stochss/handlers/util/scripts/upload_remote_file.py
@@ -37,6 +37,7 @@ def get_parsed_args():
parser = argparse.ArgumentParser(description="Upload a file from an external link")
parser.add_argument("file_path", help="The path to the external file.")
parser.add_argument("outfile", help="The path to the response file")
+ parser.add_argument("-o", "--overwrite", action="store_true", help="Overwrite existing files.")
return parser.parse_args()
@@ -44,7 +45,7 @@ def get_parsed_args():
args = get_parsed_args()
if args.file_path != 'None':
folder = StochSSFolder(path="/")
- resp = folder.upload_from_link(remote_path=args.file_path)
+ resp = folder.upload_from_link(remote_path=args.file_path, overwrite=args.overwrite)
with open(args.outfile, "w") as fd:
json.dump(resp, fd)
open(args.outfile + ".done", "w").close()
diff --git a/stochss/handlers/util/stochss_folder.py b/stochss/handlers/util/stochss_folder.py
index 948d795886..3c49af47e1 100644
--- a/stochss/handlers/util/stochss_folder.py
+++ b/stochss/handlers/util/stochss_folder.py
@@ -19,9 +19,12 @@
import os
import json
import shutil
+import string
+import zipfile
import traceback
import requests
+from escapism import escape
from .stochss_base import StochSSBase
from .stochss_file import StochSSFile
@@ -71,8 +74,8 @@ def __get_rmt_upld_path(self, file):
def __build_jstree_node(self, path, file):
- types = {"mdl":"nonspatial", "smdl":"spatial", "sbml":"sbml-model", "ipynb":"notebook",
- "wkfl":"workflow", "proj":"project", "wkgp":"workflow-group", "domn":"domain"}
+ types = {"mdl":"nonspatial", "smdl":"spatial", "sbml":"sbmlModel", "ipynb":"notebook",
+ "wkfl":"workflow", "proj":"project", "wkgp":"workflowGroup", "domn":"domain"}
_path = file if self.path == "none" else os.path.join(self.path, file)
ext = file.split('.').pop() if "." in file else None
node = {"text":file, "type":"other", "_path":_path, "children":False}
@@ -86,7 +89,7 @@ def __build_jstree_node(self, path, file):
os.listdir(_path)))) > 0
else:
node['_status'] = self.get_status(path=_path)
- elif file_type == "workflow-group":
+ elif file_type == "workflowGroup":
node['children'] = True
elif os.path.isdir(os.path.join(path, file)):
node['type'] = "folder"
@@ -95,6 +98,20 @@ def __build_jstree_node(self, path, file):
return node
+ @classmethod
+ def __overwrite(cls, path, ext):
+ if ext == "zip":
+ with zipfile.ZipFile(path, "r") as zip_file:
+ members = zip_file.namelist()
+ for name in members:
+ if os.path.isdir(name):
+ shutil.rmtree(name)
+ elif os.path.exists(name):
+ os.remove(name)
+ elif os.path.exists(path):
+ os.remove(path)
+
+
def __upload_file(self, file, body, new_name=None):
if new_name is not None:
file = f"{new_name}.{file.split('.').pop()}"
@@ -334,6 +351,38 @@ def get_jstree_node(self, is_root=False):
raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ @classmethod
+ def get_presentations(cls):
+ '''
+ Get the list of presentations from the users presentation directory.
+
+ Attributes
+ ----------
+ '''
+ path = os.path.join(cls.user_dir, ".presentations")
+ presentations = []
+ if not os.path.isdir(path):
+ return presentations
+ safe_chars = set(string.ascii_letters + string.digits)
+ hostname = escape(os.environ.get('JUPYTERHUB_USER'), safe=safe_chars)
+ for file in os.listdir(path):
+ file_path = os.path.join(path, file)
+ query_str = f"?owner={hostname}&file={file}"
+ routes = {
+ "smdl": "present-model",
+ "mdl": "present-model",
+ "job": "present-job",
+ "ipynb": "present-notebook"
+ }
+ route = routes[file.split('.').pop()]
+ link = f"/stochss/{route}{query_str}"
+ presentation = {
+ "file": file, "link": link, "size": os.path.getsize(file_path)
+ }
+ presentations.append(presentation)
+ return presentations
+
+
def get_project_list(self):
'''
Get the list of project on the users disk
@@ -366,8 +415,8 @@ def move(self, location):
self.log("debug", f"Full destination directory: {dst_path}")
try:
dst = shutil.move(src_path, dst_path)
- self.path = dst.replace(self.user_dir + "/", "")
- return f"Success! {self.get_file()} was moved to {self.get_dir_name()}."
+ path = dst.replace(self.user_dir + "/", "")
+ return f"Success! {self.get_file(path=path)} was moved to {os.path.dirname(path)}."
except FileNotFoundError as err:
message = f"Could not find the directory: {str(err)}"
raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
@@ -376,31 +425,6 @@ def move(self, location):
raise StochSSPermissionsError(message, traceback.format_exc()) from err
- def publish_presentation(self, name=None):
- '''
- Publish a job, workflow, or project presentation.
-
- Attributes
- ----------
- '''
- present_dir = os.path.join(self.user_dir, ".presentations")
- if not os.path.exists(present_dir):
- os.mkdir(present_dir)
- file = self.get_file() if name is None else name
- dst = os.path.join(present_dir, file)
- if os.path.exists(dst):
- message = "A presentation with this name already exists"
- raise StochSSFileExistsError(message)
- src = self.get_path(full=True)
- try:
- shutil.copytree(src, dst)
- # INSERT JUPYTER HUB CODE HERE
- return {"message": f"Successfully published the {self.get_name()} presentation"}
- except PermissionError as err:
- message = f"You do not have permission to publish this directory: {str(err)}"
- raise StochSSPermissionsError(message, traceback.format_exc()) from err
-
-
def upload(self, file_type, file, body, new_name=None):
'''
Upload a file from a remote location to the users file system
@@ -434,7 +458,7 @@ def upload(self, file_type, file, body, new_name=None):
return resp
- def upload_from_link(self, remote_path):
+ def upload_from_link(self, remote_path, overwrite=False):
'''
Uploads a file from a remote link to the users root directory
@@ -442,6 +466,8 @@ def upload_from_link(self, remote_path):
----------
remote_path : str
Path to the remote file
+ overwrite : bool
+ Overwrite the existing files.
'''
ext = remote_path.split('.').pop()
body = requests.get(remote_path, allow_redirects=True).content
@@ -455,8 +481,10 @@ def upload_from_link(self, remote_path):
file = self.get_file(path=remote_path)
path = self.get_new_path(dst_path=file)
if os.path.exists(path):
- message = f"Could not upload this file as {file} already exists"
- return {"message":message, "reason":"File Already Exists"}
+ if not overwrite:
+ message = f"Could not upload this file as {file} already exists"
+ return {"message":message, "reason":"File Already Exists"}
+ self.__overwrite(path=path, ext=ext)
try:
file_types = {"mdl":"model", "smdl":"model", "sbml":"sbml"}
file_type = file_types[ext] if ext in file_types.keys() else "file"
@@ -466,3 +494,32 @@ def upload_from_link(self, remote_path):
return {"message":message, "file_path":new_path}
except StochSSFileExistsError as err:
return {"message":err.message, "reason":err.reason}
+
+
+ def validate_upload_link(self, remote_path):
+ '''
+ Check if the target of upload from link already exists.
+
+ Attributes
+ ----------
+ remote_path : str
+ Path to the remote file
+ '''
+ ext = remote_path.split('.').pop()
+ body = requests.get(remote_path, allow_redirects=True).content
+ if "download_presentation" in remote_path:
+ if ext in ("mdl", "smdl"):
+ file = f"{json.loads(body)['name']}.{ext}"
+ elif ext == "ipynb":
+ file = json.loads(body)['file']
+ body = json.dumps(json.loads(body)['notebook'])
+ else:
+ file = self.get_file(path=remote_path)
+ path = self.get_new_path(dst_path=file)
+ if ext == "zip":
+ with zipfile.ZipFile(path, "r") as zip_file:
+ members = zip_file.namelist()
+ for name in members:
+ if os.path.exists(name):
+ return True
+ return os.path.exists(path)
diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py
index fe295dbaca..61663da1b4 100644
--- a/stochss/handlers/util/stochss_job.py
+++ b/stochss/handlers/util/stochss_job.py
@@ -17,25 +17,30 @@
'''
import os
+import csv
import json
import shutil
import pickle
+import string
+import hashlib
+import tempfile
import datetime
import traceback
+
import numpy
import plotly
+from escapism import escape
+
from .stochss_base import StochSSBase
-from .stochss_folder import StochSSFolder
from .stochss_model import StochSSModel
from .parameter_sweep_1d import ParameterSweep1D
from .parameter_sweep_2d import ParameterSweep2D
from .stochss_spatial_model import StochSSSpatialModel
-from .stochss_errors import StochSSJobError, StochSSJobNotCompleteError, \
- StochSSFileNotFoundError, StochSSFileExistsError, \
+from .stochss_errors import StochSSFileNotFoundError, StochSSFileExistsError, \
FileNotJSONFormatError, PlotNotAvailableError, \
- StochSSJobResultsError
+ StochSSPermissionsError, StochSSJobResultsError
class StochSSJob(StochSSBase):
'''
@@ -103,8 +108,12 @@ def __create_settings(self):
self.__get_settings_path(full=True))
- def __get_settings_path(self, full=False):
- return os.path.join(self.get_path(full=full), "settings.json")
+ @classmethod
+ def __get_csvzip(cls, dirname, name):
+ shutil.make_archive(os.path.join(dirname, name), "zip", dirname, name)
+ path = os.path.join(dirname, f"{name}.zip")
+ with open(path, "rb") as zip_file:
+ return zip_file.read()
def __get_extract_dst_path(self, mdl_file):
@@ -150,6 +159,27 @@ def __get_filtered_2d_results(self, f_keys, param):
return f_results
+ def __get_filtered_ensemble_results(self, data_keys):
+ result = self.__get_pickled_results()
+ if data_keys:
+ key = [f"{name}:{value}" for name, value in data_keys.items()]
+ key = ','.join(key)
+ result = result[key]
+ return result
+
+
+ def __get_full_timeseries_csv(self, b_path, results, get_name, name):
+ ts_path = os.path.join(b_path, "Time_Series")
+ os.makedirs(ts_path)
+ for i, (key, result) in enumerate(results.items()):
+ data = [_data.split(':') for _data in key.split(',')]
+ data_keys = {_data[0]: _data[1] for _data in data}
+ nametag = get_name(name, i)
+ self.log("info", f"\tGenerating CSV files for {nametag}...")
+ result.to_csv(path=ts_path, nametag=nametag, stamp="")
+ self.__write_parameters_csv(path=ts_path, name=nametag, data_keys=data_keys)
+
+
@classmethod
def __get_fixed_keys_and_dims(cls, settings, fixed):
p_len = len(settings['parameterSweepSettings']['parameters'])
@@ -166,31 +196,49 @@ def __get_fixed_keys_and_dims(cls, settings, fixed):
return dims, f_keys
+ def __get_info_path(self, full=False):
+ return os.path.join(self.get_path(full=full), "info.json")
+
+
def __get_pickled_results(self):
path = os.path.join(self.__get_results_path(full=True), "results.p")
with open(path, "rb") as results_file:
return pickle.load(results_file)
- def __get_results_path(self, full=False):
- '''
- Return the path to the results directory
+ @classmethod
+ def __get_presentation_links(cls, file):
+ safe_chars = set(string.ascii_letters + string.digits)
+ hostname = escape(os.environ.get('JUPYTERHUB_USER'), safe=safe_chars)
+ query_str = f"?owner={hostname}&file={file}"
+ present_link = f"/stochss/present-job{query_str}"
+ dl_base = "/stochss/job/download_presentation"
+ downloadlink = os.path.join(dl_base, hostname, file)
+ open_link = f"https://open.stochss.org?open={downloadlink}"
+ links = {"presentation": present_link, "download": downloadlink, "open": open_link}
+ return links
- Attributes
- ----------
- full : bool
- Indicates whether or not to get the full path or local path
- '''
+
+ def __get_results_path(self, full=False):
return os.path.join(self.get_path(full=full), "results")
- def __is_csv_dir(self, file):
- if "results_csv" not in file:
- return False
- path = os.path.join(self.__get_results_path(), file)
- if not os.path.isdir(path):
- return False
- return True
+ def __get_run_logs(self):
+ path = os.path.join(self.get_path(full=True), "logs.txt")
+ try:
+ with open(path, "r") as file:
+ logs = file.read()
+ self.log("debug", f"Contents of the log file: {logs}")
+ if logs:
+ return logs
+ return "No logs were recoded for this job."
+ except FileNotFoundError as err:
+ message = f"Could not find the log file: {str(err)}"
+ raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+
+
+ def __get_settings_path(self, full=False):
+ return os.path.join(self.get_path(full=full), "settings.json")
@classmethod
@@ -232,6 +280,15 @@ def __update_settings(self):
"parameters": parameters}
+ @classmethod
+ def __write_parameters_csv(cls, path, name, data_keys):
+ csv_path = os.path.join(path, name, "parameters.csv")
+ with open(csv_path, "w") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ csv_writer.writerow(list(data_keys.keys()))
+ csv_writer.writerow(list(data_keys.values()))
+
+
def duplicate_as_new(self, stamp):
'''
Get all data needed to create a new job that
@@ -274,60 +331,65 @@ def extract_model(self):
return resp, kwargs
- def generate_csv_zip(self):
+ def get_csvzip_from_results(self, data_keys, proc_key, name):
'''
- Create a zip archive of the csv results for download
-
- Atrributes
- ----------
- '''
- status = self.get_status()
- if status == "error":
- message = f"The job experienced an error during run: {status}"
- raise StochSSJobError(message, traceback.format_exc())
- if status != "complete":
- message = f"The job has not finished running: {status}."
- raise StochSSJobNotCompleteError(message, traceback.format_exc())
-
- csv_path = self.get_csv_path(full=True)
- if os.path.exists(csv_path + ".zip"):
- message = f"{csv_path}.zip already exists."
- return {"Message":message, "Path":csv_path.replace(self.user_dir+"/", "") + ".zip"}
- csv_folder = StochSSFolder(path=csv_path)
- return csv_folder.generate_zip_file()
-
-
- def get_csv_path(self, full=False):
- '''
- Return the path to the csv directory
+ Get the csv files of the plot data for download.
Attributes
----------
+ data_keys : dict
+ Dictionary of param names and values used to identify the correct data.
+ proc_key : str
+ Type post processing to preform.
+ name : str
+ Name of the csv directory
'''
- res_path = self.__get_results_path(full=full)
- if not os.path.exists(res_path):
- message = f"Could not find the results directory: {res_path}"
- raise StochSSFileNotFoundError(message, traceback.format_exc())
-
try:
- res_files = os.listdir(res_path)
- csv_path = list(filter(lambda file: self.__is_csv_dir(file=file), res_files))[0]
- return os.path.join(res_path, csv_path)
- except IndexError as err:
- message = f"Could not find the job results csv directory: {str(err)}"
+ self.log("info", "Getting job results...")
+ result = self.__get_filtered_ensemble_results(data_keys)
+ self.log("info", "Processing results...")
+ if proc_key == "stddev":
+ result = result.stddev_ensemble()
+ elif proc_key == "avg":
+ result = result.average_ensemble()
+ self.log("info", "Generating CSV files...")
+ tmp_dir = tempfile.TemporaryDirectory()
+ result.to_csv(path=tmp_dir.name, nametag=name, stamp="")
+ if data_keys:
+ self.__write_parameters_csv(path=tmp_dir.name, name=name, data_keys=data_keys)
+ self.log("info", "Generating zip archive...")
+ return self.__get_csvzip(dirname=tmp_dir.name, name=name)
+ except FileNotFoundError as err:
+ message = f"Could not find the results pickle file: {str(err)}"
raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ except KeyError as err:
+ message = f"The requested results are not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
- def get_info_path(self, full=False):
+ def get_full_csvzip_from_results(self, name):
'''
- Return the path to the info.json file
+ Get the csv files for the full set of results data
Attributes
----------
- full : bool
- Indicates whether or not to get the full path or local path
+ name : str
+ Name of the csv directory.
'''
- return os.path.join(self.get_path(full=full), "info.json")
+ self.log("info", "Getting job results...")
+ results = self.__get_pickled_results()
+ tmp_dir = tempfile.TemporaryDirectory()
+ if not isinstance(results, dict):
+ self.log("info", "Generating CSV files...")
+ results.to_csv(path=tmp_dir.name, nametag=name, stamp="")
+ self.log("info", "Generating zip archive...")
+ return self.__get_csvzip(dirname=tmp_dir.name, name=name)
+ def get_name(b_name, tag):
+ return f"{b_name}_{tag}"
+ b_path = os.path.join(tmp_dir.name, get_name(name, "full"))
+ self.log("info", "Generating time series CSV files...")
+ self.__get_full_timeseries_csv(b_path, results, get_name, name)
+ return self.__get_csvzip(dirname=tmp_dir.name, name=get_name(name, "full"))
def get_model_path(self, full=False, external=False):
@@ -402,11 +464,7 @@ def get_plot_from_results(self, data_keys, plt_key, add_config=False):
self.log("debug", f"Key identifying the plot to generate: {plt_key}")
try:
self.log("info", "Loading the results...")
- result = self.__get_pickled_results()
- if data_keys:
- key = [f"{name}:{value}" for name, value in data_keys.items()]
- key = ','.join(key)
- result = result[key]
+ result = self.__get_filtered_ensemble_results(data_keys)
self.log("info", "Generating the plot...")
if plt_key == "mltplplt":
fig = result.plotplotly(return_plotly_figure=True, multiple_graphs=True)
@@ -430,6 +488,47 @@ def get_plot_from_results(self, data_keys, plt_key, add_config=False):
raise PlotNotAvailableError(message, traceback.format_exc()) from err
+ def get_psweep_csvzip_from_results(self, fixed, name):
+ '''
+ Get the csv file of the parameter sweep plot data for download
+
+ Attributes
+ ----------
+ fixed : dict
+ Dictionary for parameters that remain at a fixed value.
+ '''
+ settings = self.load_settings()
+ try:
+ self.log("info", "Loading job results...")
+ dims, f_keys = self.__get_fixed_keys_and_dims(settings, fixed)
+ params = list(filter(lambda param: param['name'] not in fixed.keys(),
+ settings['parameterSweepSettings']['parameters']))
+ tmp_dir = tempfile.TemporaryDirectory()
+ if dims == 1:
+ kwargs = {"results": self.__get_filtered_1d_results(f_keys)}
+ kwargs["species"] = list(kwargs['results'][0][0].model.listOfSpecies.keys())
+ self.log("info", "Generating CSV files...")
+ ParameterSweep1D.to_csv(
+ param=params[0], kwargs=kwargs, path=tmp_dir.name, nametag=name
+ )
+ else:
+ kwargs = {"results": self.__get_filtered_2d_results(f_keys, params[0])}
+ kwargs["species"] = list(kwargs['results'][0][0][0].model.listOfSpecies.keys())
+ self.log("info", "Generating CSV files...")
+ ParameterSweep2D.to_csv(
+ params=params, kwargs=kwargs, path=tmp_dir.name, nametag=name
+ )
+ if fixed:
+ self.__write_parameters_csv(path=tmp_dir.name, name=name, data_keys=fixed)
+ return self.__get_csvzip(dirname=tmp_dir.name, name=name)
+ except FileNotFoundError as err:
+ message = f"Could not find the results pickle file: {str(err)}"
+ raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
+ except KeyError as err:
+ message = f"The requested results are not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
def get_psweep_plot_from_results(self, fixed, kwargs, add_config=False):
'''
Generate and return the parameter sweep plot form the time series results.
@@ -441,7 +540,7 @@ def get_psweep_plot_from_results(self, fixed, kwargs, add_config=False):
kwarps : dict
Dictionary of keys used for post proccessing the results.
'''
- self.log("debug", f"Key identifying the plot to generate: {kwargs}")
+ self.log("debug", f"Keys identifying the plot to generate: {kwargs}")
settings = self.load_settings()
try:
self.log("info", "Loading the results...")
@@ -470,52 +569,6 @@ def get_psweep_plot_from_results(self, fixed, kwargs, add_config=False):
raise PlotNotAvailableError(message, traceback.format_exc()) from err
- def update_fig_layout(self, fig=None, plt_data=None):
- '''
- Get the plotly figure for the results of a job
-
- Attributes
- ----------
- fig : dict
- Plotly figure to be updated
- plt_data : dict
- Title and axes data for the plot
- '''
- self.log("debug", f"Title and axis data for the plot: {plt_data}")
- try:
- if plt_data is None:
- return fig
- for key in plt_data.keys():
- if key == "title":
- fig['layout']['title']['text'] = plt_data[key]
- else:
- fig['layout'][key]['title']['text'] = plt_data[key]
- return fig
- except KeyError as err:
- message = f"The requested plot is not available: {str(err)}"
- raise PlotNotAvailableError(message, traceback.format_exc()) from err
-
-
- def get_run_logs(self):
- '''
- Return the contents of the log file'
-
- Attributes
- ----------
- '''
- path = os.path.join(self.get_path(full=True), "logs.txt")
- try:
- with open(path, "r") as file:
- logs = file.read()
- self.log("debug", f"Contents of the log file: {logs}")
- if logs:
- return logs
- return "No logs were recoded for this job."
- except FileNotFoundError as err:
- message = f"Could not find the log file: {str(err)}"
- raise StochSSFileNotFoundError(message, traceback.format_exc()) from err
-
-
@classmethod
def get_run_settings(cls, settings, solver_map):
'''
@@ -588,7 +641,7 @@ def load(self, new=False):
finally:
settings = self.load_settings(model=model)
if os.path.exists(os.path.join(self.path, "logs.txt")):
- logs = self.get_run_logs()
+ logs = self.__get_run_logs()
else:
logs = ""
self.job = {"mdlPath":mdl_path, "model":model, "settings":settings,
@@ -609,7 +662,7 @@ def load_info(self):
----------
'''
try:
- path = self.get_info_path(full=True)
+ path = self.__get_info_path(full=True)
self.log("debug", f"The path to the job's info file: {path}")
with open(path, "r") as info_file:
info = json.load(info_file)
@@ -671,6 +724,41 @@ def load_settings(self, model=None):
raise FileNotJSONFormatError(message, traceback.format_exc()) from err
+ def publish_presentation(self, name=None):
+ '''
+ Publish a job, workflow, or project presentation.
+
+ Attributes
+ ----------
+ name : str
+ Name of the job presentation.
+ '''
+ present_dir = os.path.join(self.user_dir, ".presentations")
+ if not os.path.exists(present_dir):
+ os.mkdir(present_dir)
+ try:
+ self.load()
+ job = json.dumps(self.job, sort_keys=True)
+ file = f"{hashlib.md5(job.encode('utf-8')).hexdigest()}.job"
+ dst = os.path.join(present_dir, file)
+ if os.path.exists(dst):
+ exists = True
+ else:
+ exists = False
+ name = self.get_file() if name is None else name
+ path = os.path.join(self.__get_results_path(), "results.p")
+ with open(path, "rb") as results_file:
+ results = pickle.load(results_file)
+ data = {"name": name, "job": self.job, "results": results}
+ with open(dst, "wb") as presentation_file:
+ pickle.dump(data, presentation_file)
+ links = self.__get_presentation_links(file)
+ return links, exists
+ except PermissionError as err:
+ message = f"You do not have permission to publish this directory: {str(err)}"
+ raise StochSSPermissionsError(message, traceback.format_exc()) from err
+
+
def save(self, mdl_path, settings, initialize=False):
'''
Save the data for a new or ready state job
@@ -718,6 +806,32 @@ def save_plot(self, plot):
return {"message":"The plot was successfully saved", "data":plot}
+ def update_fig_layout(self, fig=None, plt_data=None):
+ '''
+ Update the figure layout.
+
+ Attributes
+ ----------
+ fig : dict
+ Plotly figure to be updated
+ plt_data : dict
+ Title and axes data for the plot
+ '''
+ self.log("debug", f"Title and axis data for the plot: {plt_data}")
+ try:
+ if plt_data is None:
+ return fig
+ for key in plt_data.keys():
+ if key == "title":
+ fig['layout']['title']['text'] = plt_data[key]
+ else:
+ fig['layout'][key]['title']['text'] = plt_data[key]
+ return fig
+ except KeyError as err:
+ message = f"The requested plot is not available: {str(err)}"
+ raise PlotNotAvailableError(message, traceback.format_exc()) from err
+
+
def update_info(self, new_info, new=False):
'''
Updates the contents of the info file. If source model is updated,
@@ -745,5 +859,5 @@ def update_info(self, new_info, new=False):
if "annotation" in new_info.keys():
info['annotation'] = new_info['annotation']
self.log("debug", f"New info: {info}")
- with open(self.get_info_path(full=True), "w") as file:
+ with open(self.__get_info_path(full=True), "w") as file:
json.dump(info, file, indent=4, sort_keys=True)
diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py
index a2143f1b66..3bfe947995 100644
--- a/stochss/handlers/util/stochss_model.py
+++ b/stochss/handlers/util/stochss_model.py
@@ -487,8 +487,8 @@ def publish_presentation(self):
else:
data = {"path": dst, "new":True, "model":self.model}
query_str = f"?owner={hostname}&file={file}"
- present_link = f"https://live.stochss.org/stochss/present-model{query_str}"
- downloadlink = os.path.join("https://live.stochss.org/stochss/download_presentation",
+ present_link = f"/stochss/present-model{query_str}"
+ downloadlink = os.path.join("/stochss/download_presentation",
hostname, file)
open_link = f"https://open.stochss.org?open={downloadlink}"
links = {"presentation": present_link, "download": downloadlink, "open": open_link}
diff --git a/stochss/handlers/util/stochss_notebook.py b/stochss/handlers/util/stochss_notebook.py
index 17189b85c6..bc0731e057 100644
--- a/stochss/handlers/util/stochss_notebook.py
+++ b/stochss/handlers/util/stochss_notebook.py
@@ -492,8 +492,8 @@ def __get_spatialpy_run_setting(self):
@classmethod
def __get_presentation_links(cls, hostname, file):
query_str = f"?owner={hostname}&file={file}"
- present_link = f"https://live.stochss.org/stochss/present-notebook{query_str}"
- dl_link_base = "https://live.stochss.org/stochss/notebook/download_presentation"
+ present_link = f"/stochss/present-notebook{query_str}"
+ dl_link_base = "/stochss/notebook/download_presentation"
download_link = os.path.join(dl_link_base, hostname, file)
open_link = f"https://open.stochss.org?open={download_link}"
return {"presentation": present_link, "download": download_link, "open": open_link}
diff --git a/stochss/handlers/util/stochss_spatial_model.py b/stochss/handlers/util/stochss_spatial_model.py
index 60a9191783..6e2ea225f9 100644
--- a/stochss/handlers/util/stochss_spatial_model.py
+++ b/stochss/handlers/util/stochss_spatial_model.py
@@ -18,17 +18,20 @@
import os
import json
+import string
+import hashlib
import tempfile
import traceback
import numpy
import plotly
+from escapism import escape
from spatialpy import Model, Species, Parameter, Reaction, Mesh, MeshError, BoundaryCondition, \
PlaceInitialCondition, UniformInitialCondition, ScatterInitialCondition
from .stochss_base import StochSSBase
from .stochss_errors import StochSSFileNotFoundError, FileNotJSONFormatError, DomainFormatError, \
- StochSSModelFormatError
+ StochSSModelFormatError, StochSSPermissionsError
class StochSSSpatialModel(StochSSBase):
'''
@@ -591,6 +594,40 @@ def load(self):
return self.model
+ def publish_presentation(self):
+ '''
+ Publish a model or spatial model presentation
+
+ Attributes
+ ----------
+ '''
+ present_dir = os.path.join(self.user_dir, ".presentations")
+ if not os.path.exists(present_dir):
+ os.mkdir(present_dir)
+ try:
+ self.load()
+ safe_chars = set(string.ascii_letters + string.digits)
+ hostname = escape(os.environ.get('JUPYTERHUB_USER'), safe=safe_chars)
+ model = json.dumps(self.model, sort_keys=True)
+ # replace with gillespy2.Model.to_json
+ file = f"{hashlib.md5(model.encode('utf-8')).hexdigest()}.smdl"
+ dst = os.path.join(present_dir, file)
+ if os.path.exists(dst):
+ data = None
+ else:
+ data = {"path": dst, "new":True, "model":self.model}
+ query_str = f"?owner={hostname}&file={file}"
+ present_link = f"/stochss/present-model{query_str}"
+ downloadlink = os.path.join("/stochss/download_presentation",
+ hostname, file)
+ open_link = f"https://open.stochss.org?open={downloadlink}"
+ links = {"presentation": present_link, "download": downloadlink, "open": open_link}
+ return links, data
+ except PermissionError as err:
+ message = f"You do not have permission to publish this file: {str(err)}"
+ raise StochSSPermissionsError(message, traceback.format_exc()) from err
+
+
def save_domain(self, domain):
'''
Writes a StochSS Domain to a .domn file
diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py
index d44c82cfc6..b5dc6d15d3 100644
--- a/stochss/handlers/workflows.py
+++ b/stochss/handlers/workflows.py
@@ -28,8 +28,7 @@
# Use finish() for json, write() for text
from .util import StochSSJob, StochSSModel, StochSSSpatialModel, StochSSNotebook, StochSSWorkflow, \
- StochSSParamSweepNotebook, StochSSSciopeNotebook, StochSSAPIError, report_error, \
- StochSSFolder
+ StochSSParamSweepNotebook, StochSSSciopeNotebook, StochSSAPIError, report_error
log = logging.getLogger('stochss')
@@ -424,12 +423,53 @@ async def get(self):
name = self.get_query_argument(name="name")
log.debug(f"Name of the job presentation: {name}")
try:
- folder = StochSSFolder(path=path)
- log.info(f"Publishing the {folder.get_name()} presentation")
- resp = folder.publish_presentation(name=name)
+ job = StochSSJob(path=path)
+ log.info(f"Publishing the {job.get_name()} presentation")
+ links, exists = job.publish_presentation(name=name)
+ if exists:
+ message = f"A presentation for {job.get_name()} already exists."
+ else:
+ message = f"Successfully published the {job.get_name()} presentation"
+ resp = {"message": message, "links": links}
log.info(resp['message'])
log.debug(f"Response Message: {resp}")
self.write(resp)
except StochSSAPIError as err:
report_error(self, log, err)
self.finish()
+
+
+class DownloadCSVZipAPIHandler(APIHandler):
+ '''
+ ################################################################################################
+ Handler for downloading job csv results files.
+ ################################################################################################
+ '''
+ @web.authenticated
+ async def get(self):
+ '''
+ Download a jobs results as CSV.
+
+ Attributes
+ ----------
+ '''
+ self.set_header('Content-Type', 'application/zip')
+ path = self.get_query_argument(name="path")
+ csv_type = self.get_query_argument(name="type")
+ data = self.get_query_argument(name="data", default=None)
+ if data is not None:
+ data = json.loads(data)
+ try:
+ job = StochSSJob(path=path)
+ name = job.get_name()
+ self.set_header('Content-Disposition', f'attachment; filename="{name}.zip"')
+ if csv_type == "time series":
+ csv_data = job.get_csvzip_from_results(**data, name=name)
+ elif csv_type == "psweep":
+ csv_data = job.get_psweep_csvzip_from_results(fixed=data, name=name)
+ else:
+ csv_data = job.get_full_csvzip_from_results(name=name)
+ self.write(csv_data)
+ except StochSSAPIError as err:
+ report_error(self, log, err)
+ self.finish()
diff --git a/webpack.config.js b/webpack.config.js
index 440717e916..379a96947b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -6,12 +6,11 @@ module.exports = {
entry: {
home: './client/pages/users-home.js',
quickstart: './client/pages/quickstart.js',
- browser: './client/pages/file-browser.js',
+ browser: './client/pages/browser.js',
editor: './client/pages/model-editor.js',
domainEditor: './client/pages/domain-editor.js',
workflowSelection: './client/pages/workflow-selection.js',
workflowEditor: './client/pages/workflow-manager.js',
- projectBrowser: './client/pages/project-browser.js',
projectManager: './client/pages/project-manager.js',
loadingPage: './client/pages/loading-page.js',
multiplePlots: './client/pages/multiple-plots.js'
@@ -71,13 +70,6 @@ module.exports = {
name: 'workflowEditor',
inject: false
}),
- new HtmlWebpackPlugin({
- title: 'StochSS | Project Browser',
- filename: 'stochss-project-browser.html',
- template: 'page_template.pug',
- name: 'projectBrowser',
- inject: false
- }),
new HtmlWebpackPlugin({
title: 'StochSS | Project Manager',
filename: 'stochss-project-manager.html',
diff --git a/webpack.hub.config.js b/webpack.hub.config.js
index 62330fdbe3..06ca57e61a 100644
--- a/webpack.hub.config.js
+++ b/webpack.hub.config.js
@@ -7,7 +7,8 @@ module.exports = {
home: './client/pages/home.js',
jobPresentation: './client/pages/job-presentation.js',
modelPresentation: './client/pages/model-presentation.js',
- notebookPresentation: './client/pages/notebook-presentation.js'
+ notebookPresentation: './client/pages/notebook-presentation.js',
+ multiplePlots: './client/pages/multiple-plots.js'
},
output: {
filename: 'stochss-[name].bundle.js',
@@ -42,8 +43,26 @@ module.exports = {
template: 'jupyterhub/home_template.pug',
name: 'notebookPresentation',
inject: false
+ }),
+ new HtmlWebpackPlugin({
+ title: "StochSS | Multiple Plots Presentation Page",
+ filename: '../templates/multiple-plots-page.html',
+ template: 'jupyterhub/home_template.pug',
+ name: 'multiplePlots',
+ inject: false
})
],
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ commons: {
+ name: 'common',
+ chunks: 'initial',
+ minChunks: 2
+ }
+ }
+ }
+ },
module: {
rules: [
{