PLAT-1121 Export courses asynchronously
This commit is contained in:
400
cms/static/js/views/export.js
Normal file
400
cms/static/js/views/export.js
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Course export-related js.
|
||||
*/
|
||||
define([
|
||||
'jquery', 'underscore', 'gettext', 'moment', 'common/js/components/views/feedback_prompt',
|
||||
'edx-ui-toolkit/js/utils/html-utils', 'jquery.cookie'
|
||||
], function($, _, gettext, moment, PromptView, HtmlUtils) {
|
||||
'use strict';
|
||||
|
||||
/** ******** Private properties ****************************************/
|
||||
|
||||
var COOKIE_NAME = 'lastexport';
|
||||
|
||||
var STAGE = {
|
||||
PREPARING: 0,
|
||||
EXPORTING: 1,
|
||||
COMPRESSING: 2,
|
||||
SUCCESS: 3
|
||||
};
|
||||
|
||||
var STATE = {
|
||||
READY: 1,
|
||||
IN_PROGRESS: 2,
|
||||
SUCCESS: 3,
|
||||
ERROR: 4
|
||||
};
|
||||
|
||||
var courselikeHomeUrl;
|
||||
var current = {stage: 0, state: STATE.READY, downloadUrl: null};
|
||||
var deferred = null;
|
||||
var isLibrary = false;
|
||||
var statusUrl = null;
|
||||
var successUnixDate = null;
|
||||
var timeout = {id: null, delay: 1000};
|
||||
var $dom = {
|
||||
downloadLink: $('#download-exported-button'),
|
||||
stages: $('ol.status-progress').children(),
|
||||
successStage: $('.item-progresspoint-success'),
|
||||
wrapper: $('div.wrapper-status')
|
||||
};
|
||||
|
||||
/** ******** Private functions *****************************************/
|
||||
|
||||
/**
|
||||
* Makes Export feedback status list visible
|
||||
*
|
||||
*/
|
||||
var displayFeedbackList = function() {
|
||||
$dom.wrapper.removeClass('is-hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the Export feedback status list
|
||||
*
|
||||
* @param {string} [currStageMsg=''] The message to show on the
|
||||
* current stage (for now only in case of error)
|
||||
*/
|
||||
var updateFeedbackList = function(currStageMsg) {
|
||||
var $checkmark, $curr, $prev, $next;
|
||||
var date, stageMsg, time;
|
||||
|
||||
$checkmark = $dom.successStage.find('.icon');
|
||||
stageMsg = currStageMsg || '';
|
||||
|
||||
function completeStage(stage) {
|
||||
$(stage)
|
||||
.removeClass('is-not-started is-started')
|
||||
.addClass('is-complete');
|
||||
}
|
||||
|
||||
function errorStage(stage) {
|
||||
if (!$(stage).hasClass('has-error')) {
|
||||
stageMsg = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<p class="copy error">'),
|
||||
stageMsg,
|
||||
HtmlUtils.HTML('</p>')
|
||||
);
|
||||
$(stage)
|
||||
.removeClass('is-started')
|
||||
.addClass('has-error')
|
||||
.find('p.copy')
|
||||
.hide()
|
||||
.after(HtmlUtils.ensureHtml(stageMsg).toString());
|
||||
}
|
||||
}
|
||||
|
||||
function resetStage(stage) {
|
||||
$(stage)
|
||||
.removeClass('is-complete is-started has-error')
|
||||
.addClass('is-not-started')
|
||||
.find('p.error')
|
||||
.remove()
|
||||
.end()
|
||||
.find('p.copy')
|
||||
.show();
|
||||
}
|
||||
|
||||
switch (current.state) {
|
||||
case STATE.READY:
|
||||
_.map($dom.stages, resetStage);
|
||||
|
||||
break;
|
||||
|
||||
case STATE.IN_PROGRESS:
|
||||
$prev = $dom.stages.slice(0, current.stage);
|
||||
$curr = $dom.stages.eq(current.stage);
|
||||
|
||||
_.map($prev, completeStage);
|
||||
$curr.removeClass('is-not-started').addClass('is-started');
|
||||
|
||||
break;
|
||||
|
||||
case STATE.SUCCESS:
|
||||
date = moment(successUnixDate).utc().format('MM/DD/YYYY');
|
||||
time = moment(successUnixDate).utc().format('HH:mm');
|
||||
|
||||
_.map($dom.stages, completeStage);
|
||||
|
||||
$dom.successStage
|
||||
.find('.item-progresspoint-success-date')
|
||||
.text('(' + date + ' at ' + time + ' UTC)');
|
||||
|
||||
break;
|
||||
|
||||
case STATE.ERROR:
|
||||
// Make all stages up to, and including, the error stage 'complete'.
|
||||
$prev = $dom.stages.slice(0, current.stage + 1);
|
||||
$curr = $dom.stages.eq(current.stage);
|
||||
$next = $dom.stages.slice(current.stage + 1);
|
||||
|
||||
_.map($prev, completeStage);
|
||||
_.map($next, resetStage);
|
||||
errorStage($curr);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Invalid state, don't change anything
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.state === STATE.SUCCESS) {
|
||||
$checkmark.removeClass('fa-square-o').addClass('fa-check-square-o');
|
||||
$dom.downloadLink.attr('href', current.downloadUrl);
|
||||
} else {
|
||||
$checkmark.removeClass('fa-check-square-o').addClass('fa-square-o');
|
||||
$dom.downloadLink.attr('href', '#');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the Export in the "error" status.
|
||||
*
|
||||
* Immediately stops any further polling from the server.
|
||||
* Displays the error message at the list element that corresponds
|
||||
* to the stage where the error occurred.
|
||||
*
|
||||
* @param {string} msg Error message to display.
|
||||
* @param {int} [stage=current.stage] Stage of export process at which error occurred.
|
||||
*/
|
||||
var error = function(msg, stage) {
|
||||
current.stage = Math.abs(stage || current.stage); // Could be negative
|
||||
current.state = STATE.ERROR;
|
||||
|
||||
clearTimeout(timeout.id);
|
||||
updateFeedbackList(msg);
|
||||
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores in a cookie the current export data
|
||||
*
|
||||
* @param {boolean} [completed=false] If the export has been completed or not
|
||||
*/
|
||||
var storeExport = function(completed) {
|
||||
$.cookie(COOKIE_NAME, JSON.stringify({
|
||||
statusUrl: statusUrl,
|
||||
date: moment().valueOf(),
|
||||
completed: completed || false
|
||||
}), {path: window.location.pathname});
|
||||
};
|
||||
|
||||
/** ******** Public functions ******************************************/
|
||||
|
||||
var CourseExport = {
|
||||
/**
|
||||
* Fetches the previous stored export
|
||||
*
|
||||
* @param {string} contentHomeUrl the full URL to the course or library being exported
|
||||
* @return {JSON} the data of the previous export
|
||||
*/
|
||||
storedExport: function(contentHomeUrl) {
|
||||
var storedData = JSON.parse($.cookie(COOKIE_NAME));
|
||||
if (storedData) {
|
||||
successUnixDate = storedData.date;
|
||||
}
|
||||
if (contentHomeUrl) {
|
||||
courselikeHomeUrl = contentHomeUrl;
|
||||
}
|
||||
return storedData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the Export on the "success" status
|
||||
*
|
||||
* If it wasn't already, marks the stored export as "completed",
|
||||
* and updates its date timestamp
|
||||
*/
|
||||
success: function() {
|
||||
current.state = STATE.SUCCESS;
|
||||
|
||||
if (this.storedExport().completed !== true) {
|
||||
storeExport(true);
|
||||
}
|
||||
|
||||
updateFeedbackList();
|
||||
|
||||
deferred.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Entry point for server feedback
|
||||
*
|
||||
* Checks for export status updates every `timeout` milliseconds,
|
||||
* and updates the page accordingly.
|
||||
*
|
||||
* @param {int} [stage=0] Starting stage.
|
||||
*/
|
||||
pollStatus: function(data) {
|
||||
var editUnitUrl = null,
|
||||
msg = data;
|
||||
if (current.state !== STATE.IN_PROGRESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
current.stage = data.ExportStatus || STAGE.PREPARING;
|
||||
|
||||
if (current.stage === STAGE.SUCCESS) {
|
||||
current.downloadUrl = data.ExportOutput;
|
||||
this.success();
|
||||
} else if (current.stage < STAGE.PREPARING) { // Failed
|
||||
if (data.ExportError) {
|
||||
msg = data.ExportError;
|
||||
}
|
||||
if (msg.raw_error_msg) {
|
||||
editUnitUrl = msg.edit_unit_url;
|
||||
msg = msg.raw_error_msg;
|
||||
}
|
||||
error(msg);
|
||||
this.showError(editUnitUrl, msg);
|
||||
} else { // In progress
|
||||
updateFeedbackList();
|
||||
|
||||
$.getJSON(statusUrl, function(result) {
|
||||
timeout.id = setTimeout(function() {
|
||||
this.pollStatus(result);
|
||||
}.bind(this), timeout.delay);
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the Export internally and visually
|
||||
*
|
||||
*/
|
||||
reset: function(library) {
|
||||
current.stage = STAGE.PREPARING;
|
||||
current.state = STATE.READY;
|
||||
current.downloadUrl = null;
|
||||
isLibrary = library;
|
||||
|
||||
clearTimeout(timeout.id);
|
||||
updateFeedbackList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show last export status from server and start sending requests
|
||||
* to the server for status updates
|
||||
*
|
||||
* @return {jQuery promise}
|
||||
*/
|
||||
resume: function(library) {
|
||||
deferred = $.Deferred();
|
||||
isLibrary = library;
|
||||
statusUrl = this.storedExport().statusUrl;
|
||||
|
||||
$.getJSON(statusUrl, function(data) {
|
||||
current.stage = data.ExportStatus;
|
||||
current.downloadUrl = data.ExportOutput;
|
||||
|
||||
displayFeedbackList();
|
||||
current.state = STATE.IN_PROGRESS;
|
||||
this.pollStatus(data);
|
||||
}.bind(this));
|
||||
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a dialog giving further information about the details of an export error.
|
||||
*
|
||||
* @param {string} editUnitUrl URL of the unit in which the error occurred, if known
|
||||
* @param {string} errMsg Detailed error message
|
||||
*/
|
||||
showError: function(editUnitUrl, errMsg) {
|
||||
var action,
|
||||
dialog,
|
||||
msg = '';
|
||||
if (editUnitUrl) {
|
||||
dialog = new PromptView({
|
||||
title: gettext('There has been an error while exporting.'),
|
||||
message: gettext('There has been a failure to export to XML at least one component. ' +
|
||||
'It is recommended that you go to the edit page and repair the error before attempting ' +
|
||||
'another export. Please check that all components on the page are valid and do not display ' +
|
||||
'any error messages.'),
|
||||
intent: 'error',
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Correct failed component'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
document.location = editUnitUrl;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Return to Export'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (isLibrary) {
|
||||
msg += gettext('Your library could not be exported to XML. There is not enough information to ' +
|
||||
'identify the failed component. Inspect your library to identify any problematic components ' +
|
||||
'and try again.');
|
||||
action = gettext('Take me to the main library page');
|
||||
} else {
|
||||
msg += gettext('Your course could not be exported to XML. There is not enough information to ' +
|
||||
'identify the failed component. Inspect your course to identify any problematic components ' +
|
||||
'and try again.');
|
||||
action = gettext('Take me to the main course page');
|
||||
}
|
||||
msg += ' ' + gettext('The raw error message is:') + ' ' + errMsg;
|
||||
dialog = new PromptView({
|
||||
title: gettext('There has been an error with your export.'),
|
||||
message: msg,
|
||||
intent: 'error',
|
||||
actions: {
|
||||
primary: {
|
||||
text: action,
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
document.location = courselikeHomeUrl;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The CSS animation for the dialog relies on the 'js' class
|
||||
// being on the body. This happens after this JavaScript is executed,
|
||||
// causing a 'bouncing' of the dialog after it is initially shown.
|
||||
// As a workaround, add this class first.
|
||||
$('body').addClass('js');
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the exporting process.
|
||||
* Makes status list visible and starts showing export progress.
|
||||
*
|
||||
* @param {string} url The full URL to use to query the server
|
||||
* about the export status
|
||||
* @return {jQuery promise}
|
||||
*/
|
||||
start: function(url) {
|
||||
current.state = STATE.IN_PROGRESS;
|
||||
deferred = $.Deferred();
|
||||
|
||||
statusUrl = url;
|
||||
|
||||
storeExport();
|
||||
displayFeedbackList();
|
||||
updateFeedbackList();
|
||||
|
||||
return deferred.promise();
|
||||
}
|
||||
};
|
||||
|
||||
return CourseExport;
|
||||
});
|
||||
Reference in New Issue
Block a user