Skip to content

Commit

Permalink
more analysis defensive programming
Browse files Browse the repository at this point in the history
  • Loading branch information
adkinsrs committed Sep 10, 2024
1 parent 080a0c4 commit 4d97433
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 66 deletions.
17 changes: 12 additions & 5 deletions www/cgi/get_analysis_image.cgi
Original file line number Diff line number Diff line change
Expand Up @@ -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()
11 changes: 9 additions & 2 deletions www/cgi/get_stored_analysis.cgi
Original file line number Diff line number Diff line change
Expand Up @@ -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()
151 changes: 98 additions & 53 deletions www/js/classes/analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,49 +202,6 @@ class Analysis {
logErrorInConsole(error);
createToast(`Error downloading analysis h5ad`);
}

}

/**
* Retrieves the stored analysis data from the server.
* @returns {Promise<void>} 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;

}

/**
Expand Down Expand Up @@ -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));

Expand All @@ -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));

Expand All @@ -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));

Expand All @@ -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<void>} 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<Analysis>} - 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,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 = `<a target="_blank" href="${imgSrc}"><img src="${imgSrc}" class="image" alt="${title}" /></a>`;
document.querySelector(target).innerHTML = html;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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") {
Expand Down
4 changes: 2 additions & 2 deletions www/js/sc_workbench.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand All @@ -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");
}

Expand Down
8 changes: 4 additions & 4 deletions www/js/stepper-fxns.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand All @@ -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");
}

Expand Down

0 comments on commit 4d97433

Please sign in to comment.