/** * Course import-related js. */ define( ['jquery', 'underscore', 'gettext', 'moment', 'jquery.cookie'], function($, _, gettext, moment) { 'use strict'; /** ******** Private properties ****************************************/ var COOKIE_NAME = 'lastimportupload'; var STAGE = { 'UPLOADING': 0, 'UNPACKING': 1, 'VERIFYING': 2, 'UPDATING': 3, 'SUCCESS': 4 }; var STATE = { 'READY': 1, 'IN_PROGRESS': 2, 'SUCCESS': 3, 'ERROR': 4 }; var current = {stage: 0, state: STATE.READY}; var deferred = null; var file = {name: null, url: null}; var timeout = {id: null, delay: 1000}; var $dom = { stages: $('ol.status-progress').children(), successStage: $('.item-progresspoint-success'), wrapper: $('div.wrapper-status') }; /** ******** Private functions *****************************************/ /** * Destroys any event listener Import might have needed * during the process the import * */ var destroyEventListeners = function() { $(window).off('beforeunload.import'); }; /** * Makes Import feedback status list visible * */ var displayFeedbackList = function() { $dom.wrapper.removeClass('is-hidden'); }; /** * Sets the Import 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 import process at which error occurred. */ var error = function(msg, stage) { current.stage = Math.abs(stage || current.stage); // Could be negative current.state = STATE.ERROR; destroyEventListeners(); clearTimeout(timeout.id); updateFeedbackList(msg); deferred.resolve(); }; /** * Initializes the event listeners * */ var initEventListeners = function() { $(window).on('beforeunload.import', function() { if (current.stage <= STAGE.UNPACKING) { return gettext('Your import is in progress; navigating away will abort it.'); } }); }; /** * Stores in a cookie the current import data * * @param {boolean} [completed=false] If the import has been completed or not */ var storeImport = function(completed) { $.cookie(COOKIE_NAME, JSON.stringify({ file: file, date: moment().valueOf(), completed: completed || false }), {path: window.location.pathname}); }; /** * Sets the Import on the "success" status * * If it wasn't already, marks the stored import as "completed", * and updates its date timestamp */ var success = function() { current.state = STATE.SUCCESS; if (CourseImport.storedImport().completed !== true) { storeImport(true); } destroyEventListeners(); updateFeedbackList(); deferred.resolve(); }; /** * Updates the Import 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, successUnix, time; $checkmark = $dom.successStage.find('.icon'); currStageMsg = currStageMsg || ''; function completeStage(stage) { $(stage) .removeClass('is-not-started is-started') .addClass('is-complete'); } function errorStage(stage) { if (!$(stage).hasClass('has-error')) { $(stage) .removeClass('is-started') .addClass('has-error') .find('p.copy') .hide() .after("

" + currStageMsg + '

'); } } 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: successUnix = CourseImport.storedImport().date; date = moment(successUnix).utc().format('MM/DD/YYYY'); time = moment(successUnix).utc().format('HH:mm'); _.map($dom.stages, completeStage); $dom.successStage .find('.item-progresspoint-success-date') .html('(' + 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; } if (current.state === STATE.SUCCESS) { $checkmark.removeClass('fa-square-o').addClass('fa-check-square-o'); } else { $checkmark.removeClass('fa-check-square-o').addClass('fa-square-o'); } }; /** ******** Public functions ******************************************/ var CourseImport = { /** * Cancels the import and sets the Object to the error state * * @param {string} msg Error message to display. * @param {int} stage Stage of import process at which error occurred. */ cancel: function(msg, stage) { error(msg, stage); }, /** * Entry point for server feedback * * Checks for import status updates every `timeout` milliseconds, * and updates the page accordingly. * * @param {int} [stage=0] Starting stage. */ pollStatus: function(stage) { if (current.state !== STATE.IN_PROGRESS) { return; } current.stage = stage || STAGE.UPLOADING; if (current.stage === STAGE.SUCCESS) { success(); } else if (current.stage < STAGE.UPLOADING) { // Failed error(gettext('Error importing course')); } else { // In progress updateFeedbackList(); $.getJSON(file.url, function(data) { timeout.id = setTimeout(function() { this.pollStatus(data.ImportStatus); }.bind(this), timeout.delay); }.bind(this)); } }, /** * Resets the Import internally and visually * */ reset: function() { current.stage = STAGE.UPLOADING; current.state = STATE.READY; clearTimeout(timeout.id); updateFeedbackList(); }, /** * Show last import status from server and start sending requests * to the server for status updates * * @return {jQuery promise} */ resume: function() { deferred = $.Deferred(); file = this.storedImport().file; $.getJSON(file.url, function(data) { current.stage = data.ImportStatus; displayFeedbackList(); if (current.stage !== STAGE.UPLOADING) { current.state = STATE.IN_PROGRESS; this.pollStatus(current.stage); } else { // An import in the upload stage cannot be resumed error(gettext('There was an error with the upload')); } }.bind(this)); return deferred.promise(); }, /** * Starts the importing process. * Makes status list visible and starts showing upload progress. * * @param {string} fileName The name of the file to import * @param {string} fileUrl The full URL to use to query the server * about the import status * @return {jQuery promise} */ start: function(fileName, fileUrl) { current.state = STATE.IN_PROGRESS; deferred = $.Deferred(); file.name = fileName; file.url = fileUrl; initEventListeners(); storeImport(); displayFeedbackList(); updateFeedbackList(); return deferred.promise(); }, /** * Fetches the previous stored import * * @return {JSON} the data of the previous import */ storedImport: function() { return JSON.parse($.cookie(COOKIE_NAME)); } }; return CourseImport; });