From d604e40449d20534a79002c44ba959d700cf24e5 Mon Sep 17 00:00:00 2001 From: Amy Chen Date: Fri, 15 Sep 2023 15:19:29 -0700 Subject: [PATCH 1/3] TN-3259 update research data download link on UI --- .../js/src/components/ExportInstruments.vue | 131 ++++++++++++++++-- .../src/components/asyncExportDataLoader.vue | 21 +-- 2 files changed, 133 insertions(+), 19 deletions(-) diff --git a/portal/static/js/src/components/ExportInstruments.vue b/portal/static/js/src/components/ExportInstruments.vue index 8acb342eea..ab2b86ac2c 100644 --- a/portal/static/js/src/components/ExportInstruments.vue +++ b/portal/static/js/src/components/ExportInstruments.vue @@ -57,6 +57,7 @@ :initElementId="getInitElementId()" :exportUrl="getExportUrl()" :exportIdentifier="currentStudy" + v-on:initExport="handleInitExport" v-on:doneExport="handleAfterExport" v-on:initExportCustomEvent="initExportEvent"> @@ -64,7 +65,7 @@
- +
@@ -98,7 +99,8 @@ subStudyIdentifier: "substudy", mainStudyInstrumentsList:[], subStudyInstrumentsList:[], - exportHistory: null + exportHistory: null, + currentTaskUrl: null }}; }, mixins: [CurrentUser], @@ -106,14 +108,14 @@ this.setCurrentMainStudy(); this.initCurrentUser(function() { this.getInstrumentList(); - this.setExportHistory(this.getCacheExportedDataInfo()); + this.handleSetExportHistory(); }.bind(this)); }, watch: { currentStudy: function(newVal, oldVal) { //watch for when study changes //reset last exported item link as it is specific to each study - this.setExportHistory(this.getCacheExportedDataInfo()); + this.handleSetExportHistory(); //reset export error this.resetExportError(); //reset instrument(s) selected @@ -228,13 +230,25 @@ resetExportError: function() { this.$refs.exportDataLoader.clearExportDataUI(); }, + setInProgress: function(inProgress) { + if (!inProgress) { + this.$refs.exportDataLoader.setInProgress(false); + this.$refs.exportDataLoader.clearExportDataUI(); + return; + } + this.$refs.asyncDataLoader.setInProgress(true); + }, initExportEvent: function() { /* * custom UI events associated with exporting data */ let self = this; $("#dataDownloadModal").on("hide.bs.modal", function () { - $("#"+self.getInitElementId()).removeAttr("data-export-in-progress"); + self.setInProgress(false); + }); + $(window).on("focus", function() { + console.log("onfocus fired") + self.handleSetExportHistory(); }); }, setDataType: function (event) { @@ -259,21 +273,95 @@ var queryStringInstruments = (this.instruments.selected).map(item => `instrument_id=${item}`).join("&"); return `/api/patient/assessment?${queryStringInstruments}&format=${this.instruments.dataType}`; }, + getDefaultExportObj: function() { + return { + study: this.currentStudy, + date: new Date().toLocaleString(), + instruments: this.instruments.selected || [] + } + }, + handleInitExport: function(statusUrl) { + if (!statusUrl) return; + // whenever the user initiates an export, we cache the associated celery task URL + this.setCacheTask({ + ...this.getDefaultExportObj(), + url: statusUrl + }); + this.currentTaskUrl = statusUrl; + }, handleAfterExport: function(resultUrl) { //export is done, save the last export to local storage this.setCacheExportedDataInfo(resultUrl); }, + getCacheExportTaskKey: function() { + return `export_data_task_${this.getUserId()}_${this.currentStudy}`; + }, + removeCacheTaskURL: function() { + localStorage.removeItem(this.getCacheExportTaskKey()); + }, + setCacheTask: function(taskObj) { + if (!taskObj) return; + localStorage.setItem(this.getCacheExportTaskKey(), JSON.stringify(taskObj)); + }, + getCacheTask: function() { + const task = localStorage.getItem(this.getCacheExportTaskKey()); + if (!task) return null; + let resultJSON = null; + try { + resultJSON = JSON.parse(task); + } catch(e) { + console.log("Unable to parse task JSON ", e); + resultJSON = null; + } + return resultJSON; + }, + getExportDataInfoFromTask: function(callback) { + callback = callback || function() {}; + const task = this.getCacheTask(); + if (!task) { + callback({data: null}); + return; + } + const taskURL = task.url; + if (!taskURL) { + callback({data: null}); + return; + } + $.getJSON(taskURL, function(data) { + if (!data) { + callback({data: null}); + return; + } + // check the status of the celery task and returns the data if it had been successful + const exportStatus = String(data["state"]).toUpperCase(); + callback({ + data : + exportStatus === "SUCCESS"? + { + ...task, + url: taskURL.replace("/status", "") + }: + null + }); + // callback({ + // data: { + // ...task, + // url: taskURL.replace("/status", "") + // } + // }) + }).fail(function() { + callback({data: null}); + }); + }, getCacheExportedDataInfoKey: function() { //uniquely identified by each user and the study - return `exporDataInfo_${this.getUserId()}_${this.currentStudy}}`; + return `exporDataInfo_${this.getUserId()}_${this.currentStudy}`; }, setCacheExportedDataInfo: function(resultUrl) { if (!resultUrl) return false; if (!this.hasInstrumentsSelection()) return; var o = { - study: this.currentStudy, - date: new Date().toLocaleString(), - instruments: this.instruments.selected, + ...this.getDefaultExportObj(), url: resultUrl }; localStorage.setItem(this.getCacheExportedDataInfoKey(), JSON.stringify(o)); @@ -284,7 +372,14 @@ if (!cachedItem) { return null; } - return JSON.parse(cachedItem); + let resultJSON = null; + try { + resultJSON = JSON.parse(cachedItem); + } catch(e) { + console.log("Unable to parse cached data export info ", e); + resultJSON = null; + } + return resultJSON; }, hasExportHistory: function() { return this.exportHistory || this.getCacheExportedDataInfo(); @@ -292,6 +387,22 @@ setExportHistory: function(o) { this.exportHistory = o; }, + handleSetExportHistory: function() { + this.getExportDataInfoFromTask(function(data) { + if (data && data.data) { + this.setExportHistory(data.data); + const task = this.getCacheTask(); + if (task && task.url && task.url === self.currentTaskUrl) { + this.setInProgress(false); + } + return; + } + const cachedDataInfo = this.getCacheExportedDataInfo(); + if (cachedDataInfo) { + this.setExportHistory(cachedDataInfo); + } + }.bind(this)); + } } }; diff --git a/portal/static/js/src/components/asyncExportDataLoader.vue b/portal/static/js/src/components/asyncExportDataLoader.vue index f111a238e2..38f7b249d0 100644 --- a/portal/static/js/src/components/asyncExportDataLoader.vue +++ b/portal/static/js/src/components/asyncExportDataLoader.vue @@ -103,7 +103,6 @@ }, onAfterExportData: function(options) { options = options || {}; - let delay = options.delay||5000; $("#" + this.initElementId).attr("disabled", false); $(".export__status").removeClass("active"); this.setInProgress(); @@ -129,6 +128,7 @@ url: exportDataUrl, success: function(data, status, request) { let statusUrl= request.getResponseHeader("Location"); + self.$emit("initExport", statusUrl); self.updateExportProgress(statusUrl, function(data) { self.onAfterExportData(data); }); @@ -165,7 +165,7 @@ let self = this; let waitTime = 3000; // send GET request to status URL - let rqId = $.getJSON(statusUrl, function(data) { + $.getJSON(statusUrl, function(data) { if (!data) { callback({error: true}); return; @@ -183,12 +183,12 @@ percent = parseInt(data['current'] * 100 / data['total']) + "%"; } else { percent = " -- %"; - //allow maximum allowed elapsed time of pending status and no progress percentage returned, - //if still no progress returned, then return error and display message - if (exportStatus === "PENDING" && self.exportTimeElapsed > self.maximumPendingTime) { - callback({error: true, message: "Processing job not responding. Please try again.", delay: 10000}); - return; - } + } + //allow maximum allowed elapsed time of pending status and no progress percentage returned, + //if still no progress returned, then return error and display message + if (exportStatus === "PENDING" && self.exportTimeElapsed > self.maximumPendingTime) { + callback({error: true, message: "Processing job not responding. Please try again.", delay: 30000}); + return; } //update status and percentage displays self.updateProgressDisplay(exportStatus, percent, true); @@ -199,6 +199,9 @@ setTimeout(function() { window.location.assign(resultUrl); }, 50); //wait a bit before retrieving results + } else { + callback({error: true, message: "Unknown status return from the processing job. Status: ", exportStatus}); + return; } self.updateProgressDisplay(data["state"], ""); setTimeout(function() { @@ -214,7 +217,7 @@ (self.arrExportDataTimeoutID).push(self.exportDataTimeoutID); } }).fail(function() { - callback({error: true}); + callback({error: true, message: "The processing job has failed."}); }); } } From 4a0a4b60fe96da48c6d63bb94c03c2d402979ab1 Mon Sep 17 00:00:00 2001 From: Amy Chen Date: Fri, 15 Sep 2023 15:37:18 -0700 Subject: [PATCH 2/3] remove debug statement --- portal/static/js/src/components/ExportInstruments.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/portal/static/js/src/components/ExportInstruments.vue b/portal/static/js/src/components/ExportInstruments.vue index ab2b86ac2c..ae6ffadcea 100644 --- a/portal/static/js/src/components/ExportInstruments.vue +++ b/portal/static/js/src/components/ExportInstruments.vue @@ -247,7 +247,6 @@ self.setInProgress(false); }); $(window).on("focus", function() { - console.log("onfocus fired") self.handleSetExportHistory(); }); }, From bb607f4448c3086b35188473937980331c90d1d8 Mon Sep 17 00:00:00 2001 From: Amy Chen Date: Tue, 19 Sep 2023 14:04:29 -0700 Subject: [PATCH 3/3] bug fix --- .../js/src/components/ExportInstruments.vue | 41 ++++++++++++------- .../src/components/asyncExportDataLoader.vue | 7 +++- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/portal/static/js/src/components/ExportInstruments.vue b/portal/static/js/src/components/ExportInstruments.vue index ae6ffadcea..d11cf5a6a6 100644 --- a/portal/static/js/src/components/ExportInstruments.vue +++ b/portal/static/js/src/components/ExportInstruments.vue @@ -116,8 +116,8 @@ //watch for when study changes //reset last exported item link as it is specific to each study this.handleSetExportHistory(); - //reset export error - this.resetExportError(); + //reset export display info + this.resetExportInfoUI(); //reset instrument(s) selected this.resetInstrumentSelections(); }, @@ -208,8 +208,11 @@ self.instruments.selected = arrSelected; }); $("#patientsInstrumentList [name='instrument'], #patientsDownloadTypeList [name='downloadType']").on("click", function() { - //clear pre-existing error - self.resetExportError(); + //clear pre-existing export info display + self.resetExportInfoUI(); + if (self.hasInstrumentsSelection()) { + $("#patientsDownloadButton").removeAttr("disabled"); + } }); //patientsDownloadTypeList downloadType $("#patientsDownloadTypeList [name='downloadType']").on("click", function() { @@ -220,23 +223,23 @@ } }); $("#dataDownloadModal").on("show.bs.modal", function () { + self.resetExportInfoUI(); + self.setInstrumentsListReady(); self.instruments.selected = []; self.instruments.dataType = "csv"; - self.resetExportError(); - self.setInstrumentsListReady(); + $(this).find("#patientsInstrumentList label").removeClass("active"); $(this).find("[name='instrument']").prop("checked", false); }); }, - resetExportError: function() { - this.$refs.exportDataLoader.clearExportDataUI(); + resetExportInfoUI: function() { + this.$refs.exportDataLoader.clearInProgress(); }, setInProgress: function(inProgress) { if (!inProgress) { - this.$refs.exportDataLoader.setInProgress(false); - this.$refs.exportDataLoader.clearExportDataUI(); + this.resetExportInfoUI(); return; } - this.$refs.asyncDataLoader.setInProgress(true); + this.$refs.exportDataLoader.setInProgress(true); }, initExportEvent: function() { /* @@ -314,9 +317,14 @@ } return resultJSON; }, + getFinishedStatusURL: function(url) { + if (!url) return ""; + return url.replace("/status", ""); + }, getExportDataInfoFromTask: function(callback) { callback = callback || function() {}; const task = this.getCacheTask(); + const self = this; if (!task) { callback({data: null}); return; @@ -338,14 +346,14 @@ exportStatus === "SUCCESS"? { ...task, - url: taskURL.replace("/status", "") + url: self.getFinishedStatusURL(taskURL) }: null }); // callback({ // data: { // ...task, - // url: taskURL.replace("/status", "") + // url: self.getFinishedStatusURL(taskURL) // } // }) }).fail(function() { @@ -387,11 +395,16 @@ this.exportHistory = o; }, handleSetExportHistory: function() { + const self = this; this.getExportDataInfoFromTask(function(data) { if (data && data.data) { this.setExportHistory(data.data); const task = this.getCacheTask(); - if (task && task.url && task.url === self.currentTaskUrl) { + // console.log("current task URL ", self.getFinishedStatusURL(self.currentTaskUrl)); + // console.log("cached task URL ", self.getFinishedStatusURL(task.url)); + if (task && + task.url && + self.getFinishedStatusURL(task.url) === self.getFinishedStatusURL(self.currentTaskUrl)) { this.setInProgress(false); } return; diff --git a/portal/static/js/src/components/asyncExportDataLoader.vue b/portal/static/js/src/components/asyncExportDataLoader.vue index 38f7b249d0..f72827ef02 100644 --- a/portal/static/js/src/components/asyncExportDataLoader.vue +++ b/portal/static/js/src/components/asyncExportDataLoader.vue @@ -87,11 +87,15 @@ clearTimeElapsed: function() { this.exportTimeElapsed = 0; }, - onBeforeExportData: function() { + clearInProgress: function() { this.updateExportProgress("", ""); this.clearExportDataTimeoutID(); this.clearTimeElapsed(); this.clearExportDataUI(); + this.setInProgress(false); + }, + onBeforeExportData: function() { + this.clearInProgress(); this.setInProgress(true); $("#" + this.initElementId).attr("disabled", true); $(".export__display-container").addClass("active"); @@ -128,6 +132,7 @@ url: exportDataUrl, success: function(data, status, request) { let statusUrl= request.getResponseHeader("Location"); + // console.log("status URL ", statusUrl) self.$emit("initExport", statusUrl); self.updateExportProgress(statusUrl, function(data) { self.onAfterExportData(data);