diff --git a/www/cgi/get_analysis_image.cgi b/www/cgi/get_analysis_image.cgi index 62d54559..557fd06f 100755 --- a/www/cgi/get_analysis_image.cgi +++ b/www/cgi/get_analysis_image.cgi @@ -41,11 +41,18 @@ def main(): if not image_path.startswith(ana_directory): raise Exception("Invalid filename: {}".format(image_path)) - with open(image_path, 'rb') as f: - print("Content-Type: image/png\n") - sys.stdout.flush() # <--- - sys.stdout.buffer.write(f.read()) - + try: + with open(image_path, 'rb') as f: + print("Content-Type: image/png\n") + sys.stdout.flush() # <--- + sys.stdout.buffer.write(f.read()) + except FileNotFoundError as e: + print(str(e), file=sys.stderr) + # ensure a 404 response + print("Status: 404 Not Found\n") + print("Content-Type: text/plain\n") + print("File not found: {0}".format(image_path)) + print("Error: {0}".format(e)) if __name__ == '__main__': main() diff --git a/www/cgi/get_stored_analysis.cgi b/www/cgi/get_stored_analysis.cgi index 10774d56..c2beca36 100755 --- a/www/cgi/get_stored_analysis.cgi +++ b/www/cgi/get_stored_analysis.cgi @@ -29,8 +29,15 @@ def main(): user_id=user.id, session_id=session_id, type=analysis_type) - print('Content-Type: application/json\n\n') - print(ana) + # try to read the analysis object and raise exception if FileNotFoundError + + try: + print('Content-Type: application/json\n\n') + print(ana) + except FileNotFoundError: + print('Content-Type: application/json\n\n') + print('{"error": "Analysis config file not found"}') + return if __name__ == '__main__': main() diff --git a/www/js/classes/analysis.js b/www/js/classes/analysis.js index 69863bc9..f5773ae4 100644 --- a/www/js/classes/analysis.js +++ b/www/js/classes/analysis.js @@ -202,49 +202,6 @@ class Analysis { logErrorInConsole(error); createToast(`Error downloading analysis h5ad`); } - - } - - /** - * Retrieves the stored analysis data from the server. - * @returns {Promise} A promise that resolves when the analysis data is retrieved. - */ - async getStoredAnalysis() { - - // Some dataset info (like organism ID) may be lost when loading an analysis from JSON - const datasetObj = this.dataset; - - try { - const {data} = await axios.post("./cgi/get_stored_analysis.cgi", convertToFormData({ - analysis_id: this.id, - analysis_type: this.type, - session_id: this.userSessionId, - dataset_id: this.dataset.id - })); - - // Load the analysis data and assign it to the current instance - const ana = Analysis.loadFromJson(data); - Object.assign(this, ana); - - // If tSNE was calculate, show the labeled tSNE section - // Mainly for primary analyses - // ? verify claim - document.querySelector(UI.labeledTsneSection).classList.add("is-hidden"); - if (this.type === "primary" && data['tsne']['tsne_calculated']) { - document.querySelector(UI.labeledTsneSection).classList.remove("is-hidden"); - - // Initialize the labeled tSNE step - this.labeledTsne = new AnalysisStepLabeledTsne(this); - } - - } catch (error) { - logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error}`); - createToast(`Error getting stored analysis`); - } - - // Restore the dataset object - this.dataset = datasetObj; - } /** @@ -321,6 +278,15 @@ class Analysis { // unsaved if (data.user_unsaved.length) { + // if data has no label, it will be "Unlabeled" + const count = 1 + data.user_unsaved.forEach(analysis => { + if (!analysis.label) { + analysis.label = `Unlabeled ${count}`; + count++; + } + }); + // sort by label data.user_unsaved.sort((a, b) => a.label.localeCompare(b.label)); @@ -334,6 +300,15 @@ class Analysis { // saved if (data.user_saved.length) { + // if data has no label, it will be "Unlabeled" + const count = 1 + data.user_saved.forEach(analysis => { + if (!analysis.label) { + analysis.label = `Unlabeled ${count}`; + count++; + } + }); + // sort by label data.user_saved.sort((a, b) => a.label.localeCompare(b.label)); @@ -347,6 +322,15 @@ class Analysis { // public if (data.public.length) { + // if data has no label, it will be "Unlabeled" + const count = 1 + data.public.forEach(analysis => { + if (!analysis.label) { + analysis.label = `Unlabeled ${count}`; + count++; + } + }); + // sort by label data.public.sort((a, b) => a.label.localeCompare(b.label)); @@ -368,19 +352,63 @@ class Analysis { createToast(`Error getting saved analyses: ${error}`); logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error}`); } + } + + + /** + * Retrieves the stored analysis data from the server. + * @returns {Promise} A promise that resolves when the analysis data is retrieved. + */ + async getStoredAnalysis() { + + // Some dataset info (like organism ID) may be lost when loading an analysis from JSON + const datasetObj = this.dataset; + + try { + const {data} = await axios.post("./cgi/get_stored_analysis.cgi", convertToFormData({ + analysis_id: this.id, + analysis_type: this.type, + session_id: this.userSessionId, + dataset_id: this.dataset.id + })); + + // Load the analysis data and assign it to the current instance + const ana = await Analysis.loadFromJson(data, datasetObj); + Object.assign(this, ana); + + // If tSNE was calculate, show the labeled tSNE section + // Mainly for primary analyses + // ? verify claim + document.querySelector(UI.labeledTsneSection).classList.add("is-hidden"); + if (this.type === "primary" && data['tsne']['tsne_calculated']) { + document.querySelector(UI.labeledTsneSection).classList.remove("is-hidden"); + + // Initialize the labeled tSNE step + this.labeledTsne = new AnalysisStepLabeledTsne(this); + } + + } catch (error) { + logErrorInConsole(`Failed ID was: ${datasetId} because msg: ${error}`); + createToast(`Error getting stored analysis`); + } + + // Restore the dataset object + this.dataset = datasetObj; } /** * Loads an Analysis object from JSON data. * - * @param {Object} data - The JSON data representing the Analysis object. - * @returns {Analysis} The loaded Analysis object. + * @param {Object} data - The JSON data representing the Analysis. + * @param {Object} datasetObj - The dataset object associated with the Analysis. + * @returns {Promise} - The loaded Analysis object. */ - static async loadFromJson(data) { + static async loadFromJson(data, datasetObj) { + const analysis = new Analysis({ id: data.id, - datasetObj: data.dataset, + datasetObj, datasetIsRaw: data.dataset_is_raw, label: data.label, type: data.type, @@ -443,6 +471,7 @@ class Analysis { // Support legacy data. const clusteringEditData = data.clustering_edit || data.clustering + clusteringEditData.mode = "edit"; analysis.clusteringEdit = AnalysisStepClustering.loadFromJson(clusteringEditData, analysis); @@ -542,7 +571,7 @@ class Analysis { const url = "./cgi/get_analysis_image.cgi"; const response = await axios.get(url, { params }); - if (response.status === 200) { + if (response?.status === 200) { const imgSrc = response.request.responseURL; const html = `${title}`; document.querySelector(target).innerHTML = html; @@ -638,6 +667,7 @@ class Analysis { // Some legacy things to change around state.dataset_id = state.dataset.id; + delete state.dataset; // redundant with other saved things. We can retrieve dataset info when loading if (state.qc_by_mito) { state.qc_by_mito.filter_mito_perc = state.qc_by_mito.filter_mito_percent; delete state.qc_by_mito.filter_mito_percent; @@ -690,7 +720,7 @@ class Analysis { this.type = 'user_saved'; document.querySelector(UI.btnSaveAnalysisElt).textContent = "Saved"; document.querySelector(UI.analysisActionContainer).classList.add("is-hidden"); - createToast("This analysis is stored in your profile.", "is-info", true); + createToast("This analysis has been saved in your private user profile.", "is-info", true); document.querySelector(UI.analysisStatusInfoContainer).classList.remove("is-hidden"); document.querySelector(UI.btnDeleteSavedAnalysisElt).classList.remove("is-hidden"); document.querySelector(UI.btnMakePublicCopyElt).classList.remove("is-hidden"); @@ -2141,14 +2171,29 @@ class AnalysisStepClustering { } if (Boolean(this.plotTsne)) { - ana.placeAnalysisImage( - {'params': params, 'title': 'Cluster groups', 'target': tsneTarget}); + try { + ana.placeAnalysisImage( + {'params': params, 'title': 'tSNE clustering', 'target': tsneTarget}); + } catch (error) { + // legacy + params['analysis_name'] = 'tsne_louvain' + ana.placeAnalysisImage( + {'params': params, 'title': 'tSNE clustering', 'target': tsneTarget}); + } } if (Boolean(this.plotUmap)) { params['analysis_name'] = 'umap_clustering' - ana.placeAnalysisImage( - {'params': params, 'title': 'Cluster groups', 'target': umapTarget}); + try { + ana.placeAnalysisImage( + {'params': params, 'title': 'Cluster groups', 'target': umapTarget}); + } catch (error) { + // legacy + params['analysis_name'] = 'umap_louvain' + ana.placeAnalysisImage( + {'params': params, 'title': 'Cluster groups', 'target': umapTarget}); + } + } if (this.mode === "initial") { diff --git a/www/js/sc_workbench.js b/www/js/sc_workbench.js index d2b9cb6e..9aace25b 100644 --- a/www/js/sc_workbench.js +++ b/www/js/sc_workbench.js @@ -933,7 +933,7 @@ document.querySelector(UI.analysisSelect).addEventListener("change", async (even document.querySelector(UI.analysisPrimaryNotificationElt).classList.add("is-hidden"); document.querySelector(UI.analysisActionContainer).classList.add("is-hidden"); document.querySelector(UI.analysisStatusInfoContainer).classList.remove("is-hidden"); - createToast("This analysis is stored in your profile.", "is-info", true); + createToast("This analysis is stored in your private user profile.", "is-info", true); document.querySelector(UI.btnMakePublicCopyElt).classList.remove("is-hidden"); } @@ -948,7 +948,7 @@ document.querySelector(UI.analysisSelect).addEventListener("change", async (even document.querySelector(UI.analysisPrimaryNotificationElt).classList.add("is-hidden"); document.querySelector(UI.analysisActionContainer).classList.add("is-hidden"); document.querySelector(UI.analysisStatusInfoContainer).classList.add("is-hidden"); - createToast("Changes made to this public analysis will spawn a local copy within your profile.", "is-info", true); + createToast("Changes made to this public analysis will create a local private copy within your profile.", "is-info", true); document.querySelector(UI.btnMakePublicCopyElt).classList.add("is-hidden"); } diff --git a/www/js/stepper-fxns.js b/www/js/stepper-fxns.js index d0b8258b..ff59a176 100644 --- a/www/js/stepper-fxns.js +++ b/www/js/stepper-fxns.js @@ -34,9 +34,9 @@ * @param {string} selectorHref - The href of the step to be blocked. */ const blockStepWithHref = (selectorHref) => { - document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}']`).parentElement.classList.remove("is-dashed", "is-active"); + document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}']`).parentElement.classList.remove("is-dashed", "is-active", "is-light"); document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}']`).classList.add("is-dark"); - document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}'] i`).classList.remove("mdi-check"); + document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}'] i`).classList.remove("mdi-check", "mdi-pencil"); document.querySelector(`.steps:not(.is-hidden) a[href='${selectorHref}'] i`).classList.add("mdi-lock"); } @@ -45,9 +45,9 @@ const blockStepWithHref = (selectorHref) => { * @param {string} selector - The CSS selector of the step element. */ const blockStep = (selector) => { - document.querySelector(`.steps:not(.is-hidden) ${selector}`).parentElement.classList.remove("is-dashed", "is-active"); + document.querySelector(`.steps:not(.is-hidden) ${selector}`).parentElement.classList.remove("is-dashed", "is-active", "is-light"); document.querySelector(`.steps:not(.is-hidden) ${selector}`).classList.add("is-dark"); - document.querySelector(`.steps:not(.is-hidden) ${selector} i`).classList.remove("mdi-check"); + document.querySelector(`.steps:not(.is-hidden) ${selector} i`).classList.remove("mdi-check", "mdi-pencil"); document.querySelector(`.steps:not(.is-hidden) ${selector} i`).classList.add("mdi-lock"); }