diff --git a/cms/static/js/factories/import.js b/cms/static/js/factories/import.js
index f1ca27e329..04181823df 100644
--- a/cms/static/js/factories/import.js
+++ b/cms/static/js/factories/import.js
@@ -1,30 +1,49 @@
define([
- 'js/views/import', 'jquery', 'gettext', 'jquery.fileupload', 'jquery.cookie'
-], function(Import, $, gettext) {
+ 'domReady', 'js/views/import', 'jquery', 'gettext', 'jquery.fileupload', 'jquery.cookie'
+], function(domReady, Import, $, gettext) {
+
'use strict';
+
return function (feedbackUrl, library) {
var dbError;
+
if (library) {
dbError = gettext('There was an error while importing the new library to our database.');
} else {
dbError = gettext('There was an error while importing the new course to our database.');
}
+
var bar = $('.progress-bar'),
fill = $('.progress-fill'),
submitBtn = $('.submit-button'),
- chooseBtn = $('.choose-file-button'),
+ chooseBtn = $('.view-import .choose-file-button'),
defaults = [
gettext('There was an error during the upload process.') + '\n',
gettext('There was an error while unpacking the file.') + '\n',
gettext('There was an error while verifying the file you submitted.') + '\n',
dbError + '\n'
],
- // Display the status of last file upload on page load
- lastFileUpload = $.cookie('lastfileupload'),
+ previousImport = Import.storedImport(),
file;
- if (lastFileUpload){
- Import.getAndStartUploadFeedback(feedbackUrl.replace('fillerName', lastFileUpload), lastFileUpload);
+ Import.callbacks.complete = function () {
+ bar.hide();
+ chooseBtn
+ .find('.copy').html(gettext("Choose new file")).end()
+ .show();
+ };
+
+ // Display the status of last file upload on page load
+ if (previousImport) {
+ $('.file-name-block')
+ .find('.file-name').html(previousImport.file.name).end()
+ .show();
+
+ if (previousImport.completed !== true) {
+ chooseBtn.hide();
+ }
+
+ Import.resume();
}
$('#fileupload').fileupload({
@@ -33,39 +52,41 @@ define([
maxChunkSize: 20 * 1000000, // 20 MB
autoUpload: false,
add: function(e, data) {
- Import.clearImportDisplay();
- Import.okayToNavigateAway = false;
+ Import.reset();
submitBtn.unbind('click');
+
file = data.files[0];
+
if (file.name.match(/tar\.gz$/)) {
- submitBtn.click(function(event){
+ submitBtn.click(function(event) {
event.preventDefault();
- $.cookie('lastfileupload', file.name);
+
+ Import.start(
+ file.name,
+ feedbackUrl.replace('fillerName', file.name)
+ );
+
submitBtn.hide();
- Import.startUploadFeedback();
- data.submit().complete(function(result, textStatus, xhr) {
- window.onbeforeunload = null;
- if (xhr.status != 200) {
+ data.submit().complete(function (result, textStatus, xhr) {
+ if (xhr.status !== 200) {
var serverMsg, errMsg, stage;
+
try{
serverMsg = $.parseJSON(result.responseText);
} catch (e) {
return;
}
- errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : '' ;
+
+ errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : '' ;
+
if (serverMsg.hasOwnProperty('Stage')) {
stage = Math.abs(serverMsg.Stage);
- Import.stageError(stage, defaults[stage] + errMsg);
+ Import.error(defaults[stage] + errMsg, stage);
}
else {
alert(gettext('Your import has failed.') + '\n\n' + errMsg);
}
- chooseBtn.html(gettext('Choose new file')).show();
- bar.hide();
}
- Import.stopGetStatus = true;
- chooseBtn.html(gettext('Choose new file')).show();
- bar.hide();
});
});
} else {
@@ -87,30 +108,42 @@ define([
}
if (percentInt >= doneAt) {
bar.hide();
- // Start feedback with delay so that current stage of import properly updates in session
- setTimeout(
- function () { Import.startServerFeedback(feedbackUrl.replace('fillerName', file.name));},
- 3000
- );
+
+ // Start feedback with delay so that current stage of
+ // import properly updates in session
+ setTimeout(function () { Import.pollStatus(); }, 3000);
} else {
bar.show();
fill.width(percentVal).html(percentVal);
}
},
- done: function(event, data){
- bar.hide();
- window.onbeforeunload = null;
- Import.displayFinishedImport();
- },
- start: function(event) {
- window.onbeforeunload = function() {
- if (!Import.okayToNavigateAway) {
- return "${_('Your import is in progress; navigating away will abort it.')}";
- }
- };
- },
sequentialUploads: true,
notifyOnError: false
});
+
+
+ var showImportSubmit = function (e) {
+ var filepath = $(this).val();
+
+ if (filepath.substr(filepath.length - 6, 6) === 'tar.gz') {
+ $('.error-block').hide();
+ $('.file-name').html($(this).val().replace('C:\\fakepath\\', ''));
+ $('.file-name-block').show();
+ chooseBtn.hide();
+ submitBtn.show();
+ $('.progress').show();
+ } else {
+ $('.error-block').html(gettext('File format not supported. Please upload a file with a tar.gz extension.')).show();
+ }
+ };
+
+ domReady(function () {
+ // import form setup
+ $('.view-import .file-input').bind('change', showImportSubmit);
+ $('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function (e) {
+ e.preventDefault();
+ $('.view-import .file-input').click();
+ });
+ });
};
});
diff --git a/cms/static/js/views/import.js b/cms/static/js/views/import.js
index 7e64c7aedc..b5980284de 100644
--- a/cms/static/js/views/import.js
+++ b/cms/static/js/views/import.js
@@ -2,241 +2,287 @@
* Course import-related js.
*/
define(
- ["domReady", "jquery", "underscore", "gettext"],
- function(domReady, $, _, gettext) {
+ ["jquery", "underscore", "gettext", "moment", "jquery.cookie"],
+ function($, _, gettext, moment) {
"use strict";
- /********** Private functions ************************************************/
+ /********** Private properties ****************************************/
- /**
- * Toggle the spin on the progress cog.
- * @param {boolean} isSpinning Turns cog spin on if true, off otherwise.
- */
- var updateCog = function (elem, isSpinning) {
- var cogI = elem.find('i.fa-cog');
- if (isSpinning) { cogI.addClass("fa-spin");}
- else { cogI.removeClass("fa-spin");}
+ var STATE = {
+ 'READY' : 1,
+ 'IN_PROGRESS': 2,
+ 'SUCCESS' : 3,
+ 'ERROR' : 4
+ }
+
+ var error = null;
+ var current = { stage: 0, state: STATE.READY };
+ 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 *****************************************/
/**
- * Manipulate the DOM to reflect current status of upload.
- * @param {int} stageNo Current stage.
+ * Makes Import feedback status list visible
+ *
*/
- var updateStage = function (stageNo){
- var all = $('ol.status-progress').children();
- var prevList = all.slice(0, stageNo);
- _.map(prevList, function (elem){
- $(elem).
- removeClass("is-not-started").
- removeClass("is-started").
- addClass("is-complete");
- updateCog($(elem), false);
- });
- var curList = all.eq(stageNo);
- curList.removeClass("is-not-started").addClass("is-started");
- updateCog(curList, true);
+ var displayFeedbackList = function () {
+ $dom.wrapper.removeClass('is-hidden');
};
/**
- * Check for import status updates every `timeout` milliseconds, and update
- * the page accordingly.
- * @param {string} url Url to call for status updates.
- * @param {int} timeout Number of milliseconds to wait in between ajax calls
- * for new updates.
- * @param {int} stage Starting stage.
+ * Sets the Import on the "success" status
+ *
*/
- var getStatus = function (url, timeout, stage) {
- var currentStage = stage || 0;
- if (currentStage > 1) { CourseImport.okayToNavigateAway = true; }
- if (CourseImport.stopGetStatus) { return ;}
+ var success = function () {
+ window.onbeforeunload = null;
+ current.state = STATE.SUCCESS;
- if (currentStage === 4) {
- // Succeeded
- CourseImport.displayFinishedImport();
- $('.view-import .choose-file-button').html(gettext("Choose new file")).show();
- } else if (currentStage < 0) {
- // Failed
- var errMsg = gettext("Error importing course");
- var failedStage = Math.abs(currentStage);
- CourseImport.stageError(failedStage, errMsg);
- $('.view-import .choose-file-button').html(gettext("Choose new file")).show();
- } else {
- // In progress
- updateStage(currentStage);
+ if (CourseImport.storedImport().completed !== true) {
+ storeImport(true);
}
- var time = timeout || 1000;
- $.getJSON(url,
- function (data) {
- setTimeout(function () {
- getStatus(url, time, data.ImportStatus);
- }, time);
- }
- );
+ updateFeedbackList();
+
+ if (typeof CourseImport.callbacks.complete === 'function') {
+ CourseImport.callbacks.complete();
+ }
+ };
+
+ /**
+ * Updates the Import feedback status list
+ *
+ */
+ var updateFeedbackList = function () {
+
+ function completeStage(stage) {
+ $(stage)
+ .removeClass("is-not-started is-started")
+ .addClass("is-complete");
+ }
+
+ function resetStage(stage) {
+ $(stage)
+ .removeClass("is-complete is-started has-error")
+ .addClass("is-not-started")
+ .find('p.error').remove().end()
+ .find('p.copy').show();
+ }
+
+ var $checkmark = $dom.successStage.find('.icon');
+
+ switch (current.state) {
+ case STATE.READY:
+ _.map($dom.stages, resetStage);
+
+ break;
+
+ case STATE.IN_PROGRESS:
+ var $prev = $dom.stages.slice(0, current.stage);
+ var $curr = $dom.stages.eq(current.stage);
+
+ _.map($prev, completeStage);
+ $curr.removeClass("is-not-started").addClass("is-started");
+
+ break;
+
+ case STATE.SUCCESS:
+ var successUnix = CourseImport.storedImport().date;
+ var date = moment(successUnix).utc().format('MM/DD/YYYY');
+ var 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'.
+ var $prev = $dom.stages.slice(0, current.stage + 1);
+ var $curr = $dom.stages.eq(current.stage);
+ var $next = $dom.stages.slice(current.stage + 1);
+ var message = error || gettext("There was an error with the upload");
+
+ _.map($prev, completeStage);
+ _.map($next, resetStage);
+
+ if (!$curr.hasClass('has-error')) {
+ $curr
+ .removeClass('is-started')
+ .addClass('has-error')
+ .find('p.copy')
+ .hide()
+ .after("
" + message + "
"); + } + + 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'); + } }; + /** + * 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('lastfileupload', JSON.stringify({ + file: file, + date: moment().valueOf(), + completed: completed || false + })); + } /********** Public functions *************************************************/ var CourseImport = { /** - * Whether to stop sending AJAX requests for updates on the import - * progress. + * A collection of callbacks. + * For now the only supported is 'complete', called on success/error + * */ - stopGetStatus: false, - /** - * Whether its fine to navigate away while import is in progress - */ - okayToNavigateAway: false, + callbacks: {}, /** - * Update DOM to set all stages as not-started (for retrying an upload that - * failed). - */ - clearImportDisplay: function () { - var all = $('ol.status-progress').children(); - _.map(all, function (elem){ - $(elem).removeClass("is-complete"). - removeClass("is-started"). - removeClass("has-error"). - addClass("is-not-started"); - $(elem).find('p.error').remove(); // remove error messages - $(elem).find('p.copy').show(); - updateCog($(elem), false); - }); - all.find('.fa-check-square-o'). // Replace checkmark with unchecked box - removeClass('fa-check-square-o'). - addClass('fa-square-o'); - this.stopGetStatus = false; - }, - - /** - * Update DOM to set all stages as complete, and stop asking for status - * updates. - */ - displayFinishedImport: function () { - this.stopGetStatus = true; - var all = $('ol.status-progress').children(); - _.map(all, function (elem){ - elem = $(elem); - $(elem). - removeClass("is-not-started"). - removeClass("is-started"). - addClass("is-complete"); - updateCog($(elem), false); - }); - all.find('.fa-square-o'). - removeClass('fa-square-o'). - addClass('fa-check-square-o'); - }, - - /** - * Make Import feedback status list visible. - */ - displayFeedbackList: function (){ - this.stopGetStatus = false; - $('div.wrapper-status').removeClass('is-hidden'); - $('.status-info').show(); - }, - - /** - * Start upload feedback. Makes status list visible and starts - * showing upload progress. - */ - startUploadFeedback: function (){ - this.displayFeedbackList(); - updateStage(0); - }, - - /** - * Show last import status from server and start sending requests to the server for status updates. - */ - getAndStartUploadFeedback: function (url, fileName){ - var self = this; - $.getJSON(url, - function (data) { - if (data.ImportStatus != 0) { - $('.file-name').html(fileName); - $('.file-name-block').show(); - self.displayFeedbackList(); - if (data.ImportStatus === 4){ - self.displayFinishedImport(); - } else { - $('.view-import .choose-file-button').hide(); - var time = 1000; - setTimeout(function () { - getStatus(url, time, data.ImportStatus); - }, time); - } - } - } - ); - }, - - /** - * Entry point for server feedback. Makes status list visible and starts - * sending requests to the server for status updates. - * @param {string} url The url to send Ajax GET requests for updates. - */ - startServerFeedback: function (url){ - this.stopGetStatus = false; - getStatus(url, 1000, 0); - }, - - /** - * Give error message at the list element that corresponds to the stage - * where the error occurred. - * @param {int} stageNo Stage of import process at which error occurred. + * 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. */ - stageError: function (stageNo, msg) { - this.stopGetStatus = true; - var all = $('ol.status-progress').children(); - // Make all stages up to, and including, the error stage 'complete'. - var prevList = all.slice(0, stageNo + 1); - _.map(prevList, function (elem){ - $(elem). - removeClass("is-not-started"). - removeClass("is-started"). - addClass("is-complete"); - updateCog($(elem), false); - }); - var message = msg || gettext("There was an error with the upload"); - var elem = $('ol.status-progress').children().eq(stageNo); - if (!elem.hasClass('has-error')) { - elem.removeClass('is-started').addClass('has-error'); - elem.find('p.copy').hide().after("" + message + "
"); + error: function (msg, stage) { + window.onbeforeunload = null + + current.stage = Math.abs(stage || current.stage); // Could be negative + current.state = STATE.ERROR; + error = msg; + + clearTimeout(timeout.id); + updateFeedbackList(); + + if (typeof this.callbacks.complete === 'function') { + this.callbacks.complete(); } - } + }, - }; + /** + * 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) { + var self = this; - var showImportSubmit = function (e) { - var filepath = $(this).val(); - if (filepath.substr(filepath.length - 6, 6) == 'tar.gz') { - $('.error-block').hide(); - $('.file-name').html($(this).val().replace('C:\\fakepath\\', '')); - $('.file-name-block').show(); - $('.view-import .choose-file-button').hide(); - $('.submit-button').show(); - $('.progress').show(); - } else { - $('.error-block').html(gettext('File format not supported. Please upload a file with atar.gz extension.')).show();
+ if (current.state !== STATE.IN_PROGRESS) {
+ return;
+ }
+
+ current.stage = stage || 0;
+
+ if (current.stage === 4) { // Succeeded
+ success();
+ } else if (current.stage < 0) { // Failed
+ this.error(gettext("Error importing course"));
+ } else { // In progress
+ updateFeedbackList();
+
+ $.getJSON(file.url, function (data) {
+ timeout.id = setTimeout(function () {
+ self.pollStatus(data.ImportStatus);
+ }, timeout.delay);
+ });
+ }
+ },
+
+ /**
+ * Resets the Import internally and visually
+ *
+ */
+ reset: function () {
+ current.stage = 0;
+ current.state = STATE.READY;
+
+ updateFeedbackList();
+ },
+
+ /**
+ * Show last import status from server and start sending requests
+ * to the server for status updates
+ *
+ */
+ resume: function () {
+ var self = this;
+
+ file = self.storedImport().file;
+
+ $.getJSON(file.url, function (data) {
+ current.stage = data.ImportStatus;
+
+ if (current.stage !== 0) {
+ current.state = STATE.IN_PROGRESS;
+ displayFeedbackList();
+
+ self.pollStatus(current.stage);
+ }
+ });
+ },
+
+ /**
+ * 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
+ */
+ start: function (fileName, fileUrl) {
+ window.onbeforeunload = function () {
+ if (current.stage <= 1 ) {
+ return gettext('Your import is in progress; navigating away will abort it.');
+ }
+ }
+
+ file.name = fileName;
+ file.url = fileUrl;
+
+ current.state = STATE.IN_PROGRESS;
+
+ storeImport();
+ displayFeedbackList();
+ updateFeedbackList();
+ },
+
+ /**
+ * Fetches the previous stored import
+ *
+ * @return {JSON} the data of the previous import
+ */
+ storedImport: function () {
+ return JSON.parse($.cookie('lastfileupload'));
}
};
- domReady(function () {
- // import form setup
- $('.view-import .file-input').bind('change', showImportSubmit);
- $('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function (e) {
- e.preventDefault();
- $('.view-import .file-input').click();
- });
- });
-
return CourseImport;
});
diff --git a/cms/static/sass/views/_import.scss b/cms/static/sass/views/_import.scss
index f101f905a8..0c8dd47125 100644
--- a/cms/static/sass/views/_import.scss
+++ b/cms/static/sass/views/_import.scss
@@ -186,6 +186,17 @@
// TYPE: success
&.item-progresspoint-success {
+ .item-progresspoint-success-date {
+ display: none;
+ margin-left: 5px;
+ }
+
+ &.is-complete {
+
+ .item-progresspoint-success-date {
+ display: inline;
+ }
+ }
}
@@ -217,6 +228,8 @@
}
.fa-cog {
+ @include animation(fa-spin 2s infinite linear);
+
visibility: visible;
opacity: 1.0;
}
diff --git a/cms/templates/import.html b/cms/templates/import.html
index ad0bf457f2..61fcbb5f25 100644
--- a/cms/templates/import.html
+++ b/cms/templates/import.html
@@ -115,7 +115,7 @@ else:
%if library: ${_("Your imported content has now been integrated into this library")}