Commit 480a3ca6 by Brian Jacobel

Move course import JS to Webpack

parent 49b11cb6
...@@ -116,6 +116,7 @@ GITHUB_REPO_ROOT = ENV_TOKENS.get('GITHUB_REPO_ROOT', GITHUB_REPO_ROOT) ...@@ -116,6 +116,7 @@ GITHUB_REPO_ROOT = ENV_TOKENS.get('GITHUB_REPO_ROOT', GITHUB_REPO_ROOT)
STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None) STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None)
if STATIC_ROOT_BASE: if STATIC_ROOT_BASE:
STATIC_ROOT = path(STATIC_ROOT_BASE) / EDX_PLATFORM_REVISION STATIC_ROOT = path(STATIC_ROOT_BASE) / EDX_PLATFORM_REVISION
WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json"
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND) EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', None) EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', None)
......
...@@ -100,7 +100,6 @@ ...@@ -100,7 +100,6 @@
"SERVER_EMAIL": "devops@example.com", "SERVER_EMAIL": "devops@example.com",
"SESSION_COOKIE_DOMAIN": null, "SESSION_COOKIE_DOMAIN": null,
"SITE_NAME": "localhost", "SITE_NAME": "localhost",
"STATIC_ROOT_BASE": "** OVERRIDDEN **",
"STATIC_URL_BASE": "/static/", "STATIC_URL_BASE": "/static/",
"SYSLOG_SERVER": "", "SYSLOG_SERVER": "",
"TECH_SUPPORT_EMAIL": "technical@example.com", "TECH_SUPPORT_EMAIL": "technical@example.com",
......
...@@ -64,6 +64,8 @@ STATICFILES_DIRS = [ ...@@ -64,6 +64,8 @@ STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "cms").abspath(), (TEST_ROOT / "staticfiles" / "cms").abspath(),
] ]
WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = TEST_ROOT / "staticfiles" / "cms" / "webpack-stats.json"
# Silence noisy logs # Silence noisy logs
import logging import logging
LOG_OVERRIDES = [ LOG_OVERRIDES = [
......
...@@ -747,6 +747,15 @@ REQUIRE_EXCLUDE = ("build.txt",) ...@@ -747,6 +747,15 @@ REQUIRE_EXCLUDE = ("build.txt",)
# returns a list with the command arguments to execute. # returns a list with the command arguments to execute.
REQUIRE_ENVIRONMENT = "node" REQUIRE_ENVIRONMENT = "node"
########################## DJANGO WEBPACK LOADER ##############################
WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'bundles/',
'STATS_FILE': os.path.join(STATIC_ROOT, 'webpack-stats.json')
}
}
################################# CELERY ###################################### ################################# CELERY ######################################
# Message configuration # Message configuration
...@@ -881,6 +890,7 @@ INSTALLED_APPS = ( ...@@ -881,6 +890,7 @@ INSTALLED_APPS = (
'pipeline', 'pipeline',
'static_replace', 'static_replace',
'require', 'require',
'webpack_loader',
# Theming # Theming
'openedx.core.djangoapps.theming', 'openedx.core.djangoapps.theming',
......
...@@ -66,6 +66,7 @@ TEST_ROOT = path('test_root') ...@@ -66,6 +66,7 @@ TEST_ROOT = path('test_root')
# Want static files in the same dir for running on jenkins. # Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles" STATIC_ROOT = TEST_ROOT / "staticfiles"
WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json"
GITHUB_REPO_ROOT = TEST_ROOT / "data" GITHUB_REPO_ROOT = TEST_ROOT / "data"
DATA_DIR = TEST_ROOT / "data" DATA_DIR = TEST_ROOT / "data"
......
...@@ -40,6 +40,7 @@ LOG_DIR = (TEST_ROOT / "log").abspath() ...@@ -40,6 +40,7 @@ LOG_DIR = (TEST_ROOT / "log").abspath()
# Store the static files under test root so that they don't overwrite existing static assets # Store the static files under test root so that they don't overwrite existing static assets
STATIC_ROOT = (TEST_ROOT / "staticfiles" / "cms").abspath() STATIC_ROOT = (TEST_ROOT / "staticfiles" / "cms").abspath()
WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json"
# Disable uglify when tests are running (used by build.js). # Disable uglify when tests are running (used by build.js).
# 1. Uglify is by far the slowest part of the build process # 1. Uglify is by far the slowest part of the build process
......
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
'js/factories/export', 'js/factories/export',
'js/factories/group_configurations', 'js/factories/group_configurations',
'js/certificates/factories/certificates_page_factory', 'js/certificates/factories/certificates_page_factory',
'js/factories/import',
'js/factories/index', 'js/factories/index',
'js/factories/library', 'js/factories/library',
'js/factories/login', 'js/factories/login',
......
...@@ -2,10 +2,15 @@ ...@@ -2,10 +2,15 @@
(function(AjaxPrefix) { (function(AjaxPrefix) {
'use strict'; 'use strict';
define(['domReady', 'jquery', 'underscore.string', 'backbone', 'gettext', define([
'common/js/components/views/feedback_notification', 'coffee/src/ajax_prefix', 'domReady',
'jquery.cookie'], 'jquery',
function(domReady, $, str, Backbone, gettext, NotificationView) { 'underscore.string',
'backbone',
'gettext',
'../../../../common/static/common/js/components/views/feedback_notification',
'jquery.cookie'
], function(domReady, $, str, Backbone, gettext, NotificationView) {
var main, sendJSON; var main, sendJSON;
main = function() { main = function() {
AjaxPrefix.addAjaxPrefix(jQuery, function() { AjaxPrefix.addAjaxPrefix(jQuery, function() {
......
define([ define([
'domReady', 'js/views/import', 'jquery', 'gettext', 'jquery.fileupload', 'jquery.cookie' 'domReady',
'../views/import',
'jquery',
'gettext',
'jQuery-File-Upload/js/jquery.fileupload',
'jquery.cookie',
'../../../../cms/js/main'
], function(domReady, Import, $, gettext) { ], function(domReady, Import, $, gettext) {
'use strict'; 'use strict';
return function(feedbackUrl, library) { return {
var dbError; Import: function(feedbackUrl, library) {
var dbError,
$bar = $('.progress-bar'),
$fill = $('.progress-fill'),
$submitBtn = $('.submit-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'
],
unloading = false,
previousImport = Import.storedImport(),
file,
onComplete = function() {
$bar.hide();
$chooseBtn
.find('.copy').text(gettext('Choose new file')).end()
.show();
},
showImportSubmit = function() {
var filepath = $(this).val(),
msg;
if (filepath.substr(filepath.length - 6, 6) === 'tar.gz') {
$('.error-block').hide();
$('.file-name').text($(this).val().replace('C:\\fakepath\\', ''));
$('.file-name-block').show();
$chooseBtn.hide();
$submitBtn.show();
$('.progress').show();
} else {
msg = gettext('File format not supported. Please upload a file with a {ext} extension.')
.replace('{ext}', '<code>tar.gz</code>');
$('.error-block').text(msg).show();
}
};
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.');
}
if (library) { $(window).on('beforeunload', function() { unloading = true; });
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'), // Display the status of last file upload on page load
fill = $('.progress-fill'), if (previousImport) {
submitBtn = $('.submit-button'), $('.file-name-block')
chooseBtn = $('.view-import .choose-file-button'), .find('.file-name')
defaults = [ .text(previousImport.file.name)
gettext('There was an error during the upload process.') + '\n', .end()
gettext('There was an error while unpacking the file.') + '\n', .show();
gettext('There was an error while verifying the file you submitted.') + '\n',
dbError + '\n'
],
unloading = false,
previousImport = Import.storedImport(),
file;
var onComplete = function() {
bar.hide();
chooseBtn
.find('.copy').text(gettext('Choose new file')).end()
.show();
};
$(window).on('beforeunload', function(event) { unloading = true; });
// Display the status of last file upload on page load
if (previousImport) {
$('.file-name-block')
.find('.file-name')
.text(previousImport.file.name)
.end()
.show();
if (previousImport.completed !== true) {
chooseBtn.hide();
}
Import.resume().then(onComplete); if (previousImport.completed !== true) {
} $chooseBtn.hide();
}
$('#fileupload').fileupload({ Import.resume().then(onComplete);
dataType: 'json', }
type: 'POST',
maxChunkSize: 20 * 1000000, // 20 MB
autoUpload: false,
add: function(e, data) {
Import.reset();
submitBtn.unbind('click');
file = data.files[0];
if (file.name.match(/tar\.gz$/)) {
submitBtn.click(function(event) {
event.preventDefault();
Import.start(
file.name,
feedbackUrl.replace('fillerName', file.name)
).then(onComplete);
submitBtn.hide();
data.submit().complete(function(result, textStatus, xhr) {
if (xhr.status !== 200) {
var serverMsg, errMsg, stage;
try { $('#fileupload').fileupload({
serverMsg = $.parseJSON(result.responseText) || {}; dataType: 'json',
} catch (e) { type: 'POST',
return; maxChunkSize: 20 * 1000000, // 20 MB
} autoUpload: false,
add: function(e, data) {
Import.reset();
$submitBtn.unbind('click');
errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : ''; file = data.files[0];
if (serverMsg.hasOwnProperty('Stage')) { if (file.name.match(/tar\.gz$/)) {
stage = Math.abs(serverMsg.Stage); $submitBtn.click(function(event) {
Import.cancel(defaults[stage] + errMsg, stage); event.preventDefault();
}
// It could be that the user is simply refreshing the page
// so we need to be sure this is an actual error from the server
else if (!unloading) {
$(window).off('beforeunload.import');
Import.reset(); Import.start(
onComplete(); file.name,
feedbackUrl.replace('fillerName', file.name)
).then(onComplete);
alert(gettext('Your import has failed.') + '\n\n' + errMsg); $submitBtn.hide();
data.submit().complete(function(result, textStatus, xhr) {
var serverMsg, errMsg, stage;
if (xhr.status !== 200) {
try {
serverMsg = $.parseJSON(result.responseText) || {};
} catch (err) {
return;
}
errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : '';
if (serverMsg.hasOwnProperty('Stage')) {
stage = Math.abs(serverMsg.Stage);
Import.cancel(defaults[stage] + errMsg, stage);
} else if (!unloading) {
// It could be that the user is simply refreshing the page
// so we need to be sure this is an actual error from the server
$(window).off('beforeunload.import');
Import.reset();
onComplete();
alert(gettext('Your import has failed.') + '\n\n' + errMsg);
}
} }
} });
}); });
}); } else {
} else { // Can't fix this lint error without major structural changes, which I'm not comfortable
data.files = []; // doing given this file's test coverage
} data.files = []; // eslint-disable-line no-param-reassign
}, }
},
progressall: function(e, data) {
var percentInt = data.loaded / data.total * 100, progressall: function(e, data) {
percentVal = parseInt(percentInt, 10) + '%', var percentInt = data.loaded / data.total * 100,
doneAt; percentVal = parseInt(percentInt, 10) + '%',
// Firefox makes ProgressEvent.loaded equal ProgressEvent.total only doneAt;
// after receiving a response from the server (see Mozilla bug 637002), // Firefox makes ProgressEvent.loaded equal ProgressEvent.total only
// so for Firefox we jump the gun a little. // after receiving a response from the server (see Mozilla bug 637002),
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { // so for Firefox we jump the gun a little.
doneAt = 95; if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
} else { doneAt = 95;
doneAt = 99; } else {
} doneAt = 99;
if (percentInt >= doneAt) { }
bar.hide(); if (percentInt >= doneAt) {
$bar.hide();
// Start feedback with delay so that current stage of
// import properly updates in session // Start feedback with delay so that current stage of
setTimeout(function() { Import.pollStatus(); }, 3000); // import properly updates in session
} else { setTimeout(function() { Import.pollStatus(); }, 3000);
bar.show(); } else {
fill.width(percentVal).text(percentVal); $bar.show();
} $fill.width(percentVal).text(percentVal);
}, }
sequentialUploads: true, },
notifyOnError: false 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').text($(this).val().replace('C:\\fakepath\\', ''));
$('.file-name-block').show();
chooseBtn.hide();
submitBtn.show();
$('.progress').show();
} else {
var msg = gettext('File format not supported. Please upload a file with a {file_extension} extension.')
.replace('{file_extension}', '<code>tar.gz</code>');
$('.error-block').text(msg).show(); domReady(function() {
} // import form setup
}; $('.view-import .file-input').bind('change', showImportSubmit);
$('.view-import .choose-file-button, .view-import .choose-file-button-inline')
domReady(function() { .bind('click', function(e) {
// import form setup e.preventDefault();
$('.view-import .file-input').bind('change', showImportSubmit); $('.view-import .file-input').click();
$('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function(e) { });
e.preventDefault();
$('.view-import .file-input').click();
}); });
}); }
}; };
}); });
...@@ -11,18 +11,18 @@ define( ...@@ -11,18 +11,18 @@ define(
var COOKIE_NAME = 'lastimportupload'; var COOKIE_NAME = 'lastimportupload';
var STAGE = { var STAGE = {
'UPLOADING': 0, UPLOADING: 0,
'UNPACKING': 1, UNPACKING: 1,
'VERIFYING': 2, VERIFYING: 2,
'UPDATING': 3, UPDATING: 3,
'SUCCESS': 4 SUCCESS: 4
}; };
var STATE = { var STATE = {
'READY': 1, READY: 1,
'IN_PROGRESS': 2, IN_PROGRESS: 2,
'SUCCESS': 3, SUCCESS: 3,
'ERROR': 4 ERROR: 4
}; };
var current = {stage: 0, state: STATE.READY}; var current = {stage: 0, state: STATE.READY};
...@@ -35,6 +35,8 @@ define( ...@@ -35,6 +35,8 @@ define(
wrapper: $('div.wrapper-status') wrapper: $('div.wrapper-status')
}; };
var CourseImport;
/** ******** Private functions *****************************************/ /** ******** Private functions *****************************************/
/** /**
...@@ -55,32 +57,11 @@ define( ...@@ -55,32 +57,11 @@ define(
}; };
/** /**
* 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 * Initializes the event listeners
* *
*/ */
var initEventListeners = function() { var initEventListeners = function() {
$(window).on('beforeunload.import', function() { $(window).on('beforeunload.import', function() { // eslint-disable-line consistent-return
if (current.stage < STAGE.UNPACKING) { if (current.stage < STAGE.UNPACKING) {
return gettext('Your import is in progress; navigating away will abort it.'); return gettext('Your import is in progress; navigating away will abort it.');
} }
...@@ -101,25 +82,6 @@ define( ...@@ -101,25 +82,6 @@ define(
}; };
/** /**
* 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 * Updates the Import feedback status list
* *
* @param {string} [currStageMsg=''] The message to show on the * @param {string} [currStageMsg=''] The message to show on the
...@@ -158,8 +120,11 @@ define( ...@@ -158,8 +120,11 @@ define(
$(stage) $(stage)
.removeClass('is-complete is-started has-error') .removeClass('is-complete is-started has-error')
.addClass('is-not-started') .addClass('is-not-started')
.find('p.error').remove().end() .find('p.error')
.find('p.copy').show(); .remove()
.end()
.find('p.copy')
.show();
} }
switch (current.state) { switch (current.state) {
...@@ -201,6 +166,9 @@ define( ...@@ -201,6 +166,9 @@ define(
errorStage($curr); errorStage($curr);
break; break;
default:
break;
} }
if (current.state === STATE.SUCCESS) { if (current.state === STATE.SUCCESS) {
...@@ -210,9 +178,49 @@ define( ...@@ -210,9 +178,49 @@ define(
} }
}; };
/**
* 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();
};
/**
* 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();
};
/** ******** Public functions ******************************************/ /** ******** Public functions ******************************************/
var CourseImport = { CourseImport = {
/** /**
* Cancels the import and sets the Object to the error state * Cancels the import and sets the Object to the error state
......
...@@ -239,13 +239,8 @@ else: ...@@ -239,13 +239,8 @@ else:
%endif %endif
</section> </section>
</div> </div>
<%static:webpack entry="Import">
Import('${import_status_url | n, js_escaped_string}', ${library | n, dump_js_escaped_json});
</%static:webpack>
</%block> </%block>
<%block name="requirejs">
require(["js/factories/import"], function(ImportFactory) {
ImportFactory(
"${import_status_url | n, js_escaped_string}",
${library | n, dump_js_escaped_json}
);
});
</%block>
(function(define) { define(['jquery',
'use strict'; 'underscore',
define(['jquery', 'underscore.string',
'underscore', 'backbone',
'underscore.string', 'text!../../../../common/templates/components/system-feedback.underscore'],
'backbone', function($, _, str, Backbone, systemFeedbackTemplate) {
'text!common/templates/components/system-feedback.underscore'], var tabbable_elements = [
function($, _, str, Backbone, systemFeedbackTemplate) { "a[href]:not([tabindex='-1'])",
var tabbable_elements = [ "area[href]:not([tabindex='-1'])",
"a[href]:not([tabindex='-1'])", "input:not([disabled]):not([tabindex='-1'])",
"area[href]:not([tabindex='-1'])", "select:not([disabled]):not([tabindex='-1'])",
"input:not([disabled]):not([tabindex='-1'])", "textarea:not([disabled]):not([tabindex='-1'])",
"select:not([disabled]):not([tabindex='-1'])", "button:not([disabled]):not([tabindex='-1'])",
"textarea:not([disabled]):not([tabindex='-1'])", "iframe:not([tabindex='-1'])",
"button:not([disabled]):not([tabindex='-1'])", "[tabindex]:not([tabindex='-1'])",
"iframe:not([tabindex='-1'])", "[contentEditable=true]:not([tabindex='-1'])"
"[tabindex]:not([tabindex='-1'])", ];
"[contentEditable=true]:not([tabindex='-1'])" var SystemFeedback = Backbone.View.extend({
]; options: {
var SystemFeedback = Backbone.View.extend({ title: '',
options: { message: '',
title: '', intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
message: '', type: null, // "alert", "notification", or "prompt": set by subclass
intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc shown: true, // is this view currently being shown?
type: null, // "alert", "notification", or "prompt": set by subclass icon: true, // should we render an icon related to the message intent?
shown: true, // is this view currently being shown? closeIcon: true, // should we render a close button in the top right corner?
icon: true, // should we render an icon related to the message intent? minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
closeIcon: true, // should we render a close button in the top right corner? maxShown: Infinity, // length of time after this view has been shown before it will be automatically hidden (milliseconds)
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds) outFocusElement: null // element to send focus to on hide
maxShown: Infinity, // length of time after this view has been shown before it will be automatically hidden (milliseconds)
outFocusElement: null // element to send focus to on hide
/* Could also have an "actions" hash: here is an example demonstrating /* Could also have an "actions" hash: here is an example demonstrating
the expected structure. For each action, by default the framework the expected structure. For each action, by default the framework
will call preventDefault on the click event before the function is will call preventDefault on the click event before the function is
run; to make it not do that, just pass `preventDefault: false` in run; to make it not do that, just pass `preventDefault: false` in
the action object. the action object.
actions: { actions: {
primary: { primary: {
"text": "Save", "text": "Save",
"class": "action-save", "class": "action-save",
"click": function(view) { "click": function(view) {
// do something when Save is clicked // do something when Save is clicked
}
},
secondary: [
{
"text": "Cancel",
"class": "action-cancel",
"click": function(view) {}
}, {
"text": "Discard Changes",
"class": "action-discard",
"click": function(view) {}
}
]
}
*/
},
initialize: function(options) {
this.options = _.extend({}, this.options, options);
if (!this.options.type) {
throw 'SystemFeedback: type required (given ' +
JSON.stringify(this.options) + ')';
} }
if (!this.options.intent) {
throw 'SystemFeedback: intent required (given ' +
JSON.stringify(this.options) + ')';
}
this.setElement($('#page-' + this.options.type));
// handle single "secondary" action
if (this.options.actions && this.options.actions.secondary &&
!_.isArray(this.options.actions.secondary)) {
this.options.actions.secondary = [this.options.actions.secondary];
}
return this;
}, },
secondary: [
{
"text": "Cancel",
"class": "action-cancel",
"click": function(view) {}
}, {
"text": "Discard Changes",
"class": "action-discard",
"click": function(view) {}
}
]
}
*/
},
inFocus: function(wrapperElementSelector) { initialize: function(options) {
var wrapper = wrapperElementSelector || '.wrapper', this.options = _.extend({}, this.options, options);
tabbables; if (!this.options.type) {
this.options.outFocusElement = this.options.outFocusElement || document.activeElement; throw 'SystemFeedback: type required (given ' +
JSON.stringify(this.options) + ')';
// Set focus to the container. }
this.$(wrapper).first().focus(); if (!this.options.intent) {
throw 'SystemFeedback: intent required (given ' +
JSON.stringify(this.options) + ')';
}
this.setElement($('#page-' + this.options.type));
// handle single "secondary" action
if (this.options.actions && this.options.actions.secondary &&
!_.isArray(this.options.actions.secondary)) {
this.options.actions.secondary = [this.options.actions.secondary];
}
return this;
},
// Make tabs within the prompt loop rather than setting focus inFocus: function(wrapperElementSelector) {
// back to the main content of the page. var wrapper = wrapperElementSelector || '.wrapper',
tabbables = this.$(tabbable_elements.join()); tabbables;
tabbables.on('keydown', function(event) { this.options.outFocusElement = this.options.outFocusElement || document.activeElement;
// On tab backward from the first tabbable item in the prompt
if (event.which === 9 && event.shiftKey && event.target === tabbables.first()[0]) {
event.preventDefault();
tabbables.last().focus();
}
// On tab forward from the last tabbable item in the prompt
else if (event.which === 9 && !event.shiftKey && event.target === tabbables.last()[0]) {
event.preventDefault();
tabbables.first().focus();
}
});
return this; // Set focus to the container.
}, this.$(wrapper).first().focus();
outFocus: function() { // Make tabs within the prompt loop rather than setting focus
var tabbables = this.$(tabbable_elements.join()).off('keydown'); // back to the main content of the page.
if (this.options.outFocusElement) { tabbables = this.$(tabbable_elements.join());
this.options.outFocusElement.focus(); tabbables.on('keydown', function(event) {
// On tab backward from the first tabbable item in the prompt
if (event.which === 9 && event.shiftKey && event.target === tabbables.first()[0]) {
event.preventDefault();
tabbables.last().focus();
} }
return this; // On tab forward from the last tabbable item in the prompt
}, else if (event.which === 9 && !event.shiftKey && event.target === tabbables.last()[0]) {
event.preventDefault();
tabbables.first().focus();
}
});
return this;
},
// public API: show() and hide() outFocus: function() {
show: function() { var tabbables = this.$(tabbable_elements.join()).off('keydown');
if (this.options.outFocusElement) {
this.options.outFocusElement.focus();
}
return this;
},
// public API: show() and hide()
show: function() {
clearTimeout(this.hideTimeout);
this.options.shown = true;
this.shownAt = new Date();
this.render();
if ($.isNumeric(this.options.maxShown)) {
this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.options.maxShown);
}
return this;
},
hide: function() {
if (this.shownAt && $.isNumeric(this.options.minShown) &&
this.options.minShown > new Date() - this.shownAt) {
clearTimeout(this.hideTimeout); clearTimeout(this.hideTimeout);
this.options.shown = true; this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.shownAt = new Date(); this.options.minShown - (new Date() - this.shownAt));
} else {
this.options.shown = false;
delete this.shownAt;
this.render(); this.render();
if ($.isNumeric(this.options.maxShown)) { }
this.hideTimeout = setTimeout(_.bind(this.hide, this), return this;
this.options.maxShown); },
}
return this;
},
hide: function() {
if (this.shownAt && $.isNumeric(this.options.minShown) &&
this.options.minShown > new Date() - this.shownAt) {
clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.options.minShown - (new Date() - this.shownAt));
} else {
this.options.shown = false;
delete this.shownAt;
this.render();
}
return this;
},
// the rest of the API should be considered semi-private // the rest of the API should be considered semi-private
events: { events: {
'click .action-close': 'hide', 'click .action-close': 'hide',
'click .action-primary': 'primaryClick', 'click .action-primary': 'primaryClick',
'click .action-secondary': 'secondaryClick' 'click .action-secondary': 'secondaryClick'
}, },
render: function() { render: function() {
// there can be only one active view of a given type at a time: only // there can be only one active view of a given type at a time: only
// one alert, only one notification, only one prompt. Therefore, we'll // one alert, only one notification, only one prompt. Therefore, we'll
// use a singleton approach. // use a singleton approach.
var singleton = SystemFeedback['active_' + this.options.type]; var singleton = SystemFeedback['active_' + this.options.type];
if (singleton && singleton !== this) { if (singleton && singleton !== this) {
singleton.stopListening(); singleton.stopListening();
singleton.undelegateEvents(); singleton.undelegateEvents();
} }
this.$el.html(_.template(systemFeedbackTemplate)(this.options)); this.$el.html(_.template(systemFeedbackTemplate)(this.options));
SystemFeedback['active_' + this.options.type] = this; SystemFeedback['active_' + this.options.type] = this;
return this; return this;
}, },
primaryClick: function(event) { primaryClick: function(event) {
var actions, primary; var actions, primary;
actions = this.options.actions; actions = this.options.actions;
if (!actions) { return; } if (!actions) { return; }
primary = actions.primary; primary = actions.primary;
if (!primary) { return; } if (!primary) { return; }
if (primary.preventDefault !== false) { if (primary.preventDefault !== false) {
event.preventDefault(); event.preventDefault();
} }
if (primary.click) { if (primary.click) {
primary.click.call(event.target, this, event); primary.click.call(event.target, this, event);
} }
}, },
secondaryClick: function(event) { secondaryClick: function(event) {
var actions, secondaryList, secondary, i; var actions, secondaryList, secondary, i;
actions = this.options.actions; actions = this.options.actions;
if (!actions) { return; } if (!actions) { return; }
secondaryList = actions.secondary; secondaryList = actions.secondary;
if (!secondaryList) { return; } if (!secondaryList) { return; }
// which secondary action was clicked? // which secondary action was clicked?
i = 0; // default to the first secondary action (easier for testing) i = 0; // default to the first secondary action (easier for testing)
if (event && event.target) { if (event && event.target) {
i = _.indexOf(this.$('.action-secondary'), event.target); i = _.indexOf(this.$('.action-secondary'), event.target);
} }
secondary = secondaryList[i]; secondary = secondaryList[i];
if (secondary.preventDefault !== false) { if (secondary.preventDefault !== false) {
event.preventDefault(); event.preventDefault();
} }
if (secondary.click) { if (secondary.click) {
secondary.click.call(event.target, this, event); secondary.click.call(event.target, this, event);
}
} }
}); }
return SystemFeedback;
}); });
}).call(this, define || RequireJS.define); return SystemFeedback;
});
(function(define) { define(['jquery', 'underscore', 'underscore.string', '../../../../common/js/components/views/feedback'],
'use strict'; function($, _, str, SystemFeedbackView) {
define(['jquery', 'underscore', 'underscore.string', 'common/js/components/views/feedback'], var Notification = SystemFeedbackView.extend({
function($, _, str, SystemFeedbackView) { options: $.extend({}, SystemFeedbackView.prototype.options, {
var Notification = SystemFeedbackView.extend({ type: 'notification',
options: $.extend({}, SystemFeedbackView.prototype.options, { closeIcon: false
type: 'notification', })
closeIcon: false });
})
});
// create Notification.Warning, Notification.Confirmation, etc // create Notification.Warning, Notification.Confirmation, etc
var capitalCamel, intents; var capitalCamel, intents;
capitalCamel = _.compose(str.capitalize, str.camelize); capitalCamel = _.compose(str.capitalize, str.camelize);
intents = ['warning', 'error', 'confirmation', 'announcement', 'step-required', 'help', 'mini']; intents = ['warning', 'error', 'confirmation', 'announcement', 'step-required', 'help', 'mini'];
_.each(intents, function(intent) { _.each(intents, function(intent) {
var subclass; var subclass;
subclass = Notification.extend({ subclass = Notification.extend({
options: $.extend({}, Notification.prototype.options, { options: $.extend({}, Notification.prototype.options, {
intent: intent intent: intent
}) })
});
Notification[capitalCamel(intent)] = subclass;
}); });
Notification[capitalCamel(intent)] = subclass;
});
// set more sensible defaults for Notification.Mini views // set more sensible defaults for Notification.Mini views
var miniOptions = Notification.Mini.prototype.options; var miniOptions = Notification.Mini.prototype.options;
miniOptions.minShown = 1250; miniOptions.minShown = 1250;
miniOptions.closeIcon = false; miniOptions.closeIcon = false;
return Notification; return Notification;
}); });
}).call(this, define || RequireJS.define);
module.exports = { module.exports = {
extends: 'eslint-config-edx', extends: 'eslint-config-edx',
root: true, root: true,
settings: {
'import/resolver': 'webpack',
},
}; };
/* globals Logger */ /* globals Logger */
import { keys } from 'edx-ui-toolkit/src/js/utils/constants'; import { keys } from 'edx-ui-toolkit/js/utils/constants';
// @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window' // @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window'
export class CourseOutline { // eslint-disable-line import/prefer-default-export export class CourseOutline { // eslint-disable-line import/prefer-default-export
......
/* globals Logger, loadFixtures */ /* globals Logger, loadFixtures */
import { keys } from 'edx-ui-toolkit/src/js/utils/constants'; import { keys } from 'edx-ui-toolkit/js/utils/constants';
import { CourseOutline } from '../CourseOutline'; import { CourseOutline } from '../CourseOutline';
......
...@@ -7,16 +7,20 @@ ...@@ -7,16 +7,20 @@
"babel-preset-env": "^1.2.1", "babel-preset-env": "^1.2.1",
"backbone": "~1.3.2", "backbone": "~1.3.2",
"backbone.paginator": "~2.0.3", "backbone.paginator": "~2.0.3",
"coffee-loader": "^0.7.3",
"coffee-script": "1.6.1", "coffee-script": "1.6.1",
"edx-pattern-library": "0.18.1", "edx-pattern-library": "0.18.1",
"edx-ui-toolkit": "1.5.2", "edx-ui-toolkit": "1.5.2",
"exports-loader": "^0.6.4",
"hls.js": "0.7.2", "hls.js": "0.7.2",
"imports-loader": "^0.7.1",
"jquery": "~2.2.0", "jquery": "~2.2.0",
"jquery-migrate": "^1.4.1", "jquery-migrate": "^1.4.1",
"jquery.scrollto": "~2.1.2", "jquery.scrollto": "~2.1.2",
"moment": "^2.15.1", "moment": "^2.15.1",
"moment-timezone": "~0.5.5", "moment-timezone": "~0.5.5",
"picturefill": "~3.0.2", "picturefill": "~3.0.2",
"raw-loader": "^0.5.1",
"requirejs": "~2.3.2", "requirejs": "~2.3.2",
"uglify-js": "2.7.0", "uglify-js": "2.7.0",
"underscore": "~1.8.3", "underscore": "~1.8.3",
......
...@@ -705,16 +705,26 @@ def execute_compile_sass(args): ...@@ -705,16 +705,26 @@ def execute_compile_sass(args):
def execute_webpack(prod, settings=None): def execute_webpack(prod, settings=None):
sh(cmd("NODE_ENV={node_env} STATIC_ROOT={static_root} $(npm bin)/webpack".format( sh(
node_env="production" if prod else "development", cmd(
static_root=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings) "NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} $(npm bin)/webpack"
))) .format(
node_env="production" if prod else "development",
static_root_lms=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings),
static_root_cms=Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
)
)
)
def execute_webpack_watch(settings=None): def execute_webpack_watch(settings=None):
run_background_process("STATIC_ROOT={static_root} $(npm bin)/webpack --watch --watch-poll=200".format( run_background_process(
static_root=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings) "STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} $(npm bin)/webpack --watch --watch-poll=200"
)) .format(
static_root_lms=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings),
static_root_cms=Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
)
)
def get_parsed_option(command_opts, opt_key, default=None): def get_parsed_option(command_opts, opt_key, default=None):
......
...@@ -41,11 +41,12 @@ EXPECTED_RUN_SERVER_COMMAND = ( ...@@ -41,11 +41,12 @@ EXPECTED_RUN_SERVER_COMMAND = (
EXPECTED_INDEX_COURSE_COMMAND = ( EXPECTED_INDEX_COURSE_COMMAND = (
u"python manage.py {system} --settings={settings} reindex_course --setup" u"python manage.py {system} --settings={settings} reindex_course --setup"
) )
EXPECTED_PRINT_SETTINGS_COMMAND = ( EXPECTED_PRINT_SETTINGS_COMMAND = [
u"python manage.py {system} --settings={settings} print_settings STATIC_ROOT --format=value 2>/dev/null" u"python manage.py lms --settings={settings} print_settings STATIC_ROOT --format=value 2>/dev/null",
) u"python manage.py cms --settings={settings} print_settings STATIC_ROOT --format=value 2>/dev/null"
]
EXPECTED_WEBPACK_COMMAND = ( EXPECTED_WEBPACK_COMMAND = (
u"NODE_ENV={node_env} STATIC_ROOT={static_root} $(npm bin)/webpack" u"NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} $(npm bin)/webpack"
) )
...@@ -240,13 +241,11 @@ class TestPaverServerTasks(PaverTestCase): ...@@ -240,13 +241,11 @@ class TestPaverServerTasks(PaverTestCase):
expected_messages.append(u"xmodule_assets common/static/xmodule") expected_messages.append(u"xmodule_assets common/static/xmodule")
expected_messages.append(u"install npm_assets") expected_messages.append(u"install npm_assets")
expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=self.platform_root)) expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=self.platform_root))
expected_messages.append(EXPECTED_PRINT_SETTINGS_COMMAND.format( expected_messages.extend([c.format(settings=expected_asset_settings) for c in EXPECTED_PRINT_SETTINGS_COMMAND])
system="lms",
settings=expected_asset_settings
))
expected_messages.append(EXPECTED_WEBPACK_COMMAND.format( expected_messages.append(EXPECTED_WEBPACK_COMMAND.format(
node_env="production" if expected_asset_settings != "devstack" else "development", node_env="production" if expected_asset_settings != "devstack" else "development",
static_root=None static_root_lms=None,
static_root_cms=None
)) ))
expected_messages.extend(self.expected_sass_commands(system=system, asset_settings=expected_asset_settings)) expected_messages.extend(self.expected_sass_commands(system=system, asset_settings=expected_asset_settings))
if expected_collect_static: if expected_collect_static:
...@@ -285,10 +284,11 @@ class TestPaverServerTasks(PaverTestCase): ...@@ -285,10 +284,11 @@ class TestPaverServerTasks(PaverTestCase):
expected_messages.append(u"xmodule_assets common/static/xmodule") expected_messages.append(u"xmodule_assets common/static/xmodule")
expected_messages.append(u"install npm_assets") expected_messages.append(u"install npm_assets")
expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=self.platform_root)) expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=self.platform_root))
expected_messages.append(EXPECTED_PRINT_SETTINGS_COMMAND.format(system="lms", settings=expected_asset_settings)) expected_messages.extend([c.format(settings=expected_asset_settings) for c in EXPECTED_PRINT_SETTINGS_COMMAND])
expected_messages.append(EXPECTED_WEBPACK_COMMAND.format( expected_messages.append(EXPECTED_WEBPACK_COMMAND.format(
node_env="production" if expected_asset_settings != "devstack" else "development", node_env="production" if expected_asset_settings != "devstack" else "development",
static_root=None static_root_lms=None,
static_root_cms=None
)) ))
expected_messages.extend(self.expected_sass_commands(asset_settings=expected_asset_settings)) expected_messages.extend(self.expected_sass_commands(asset_settings=expected_asset_settings))
if expected_collect_static: if expected_collect_static:
......
...@@ -12,7 +12,8 @@ var wpconfig = { ...@@ -12,7 +12,8 @@ var wpconfig = {
context: __dirname, context: __dirname,
entry: { entry: {
CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js' CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js',
Import: './cms/static/js/features/import/factories/import.js'
}, },
output: { output: {
...@@ -21,7 +22,7 @@ var wpconfig = { ...@@ -21,7 +22,7 @@ var wpconfig = {
libraryTarget: 'window' libraryTarget: 'window'
}, },
devtool: isProd ? false : 'eval-source-map', devtool: isProd ? false : 'source-map',
plugins: [ plugins: [
new webpack.NoEmitOnErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin(),
...@@ -33,8 +34,18 @@ var wpconfig = { ...@@ -33,8 +34,18 @@ var wpconfig = {
debug: !isProd debug: !isProd
}), }),
new BundleTracker({ new BundleTracker({
path: process.env.STATIC_ROOT, path: process.env.STATIC_ROOT_CMS,
filename: 'webpack-stats.json' filename: 'webpack-stats.json'
}),
new BundleTracker({
path: process.env.STATIC_ROOT_LMS,
filename: 'webpack-stats.json'
}),
new webpack.ProvidePlugin({
_: 'underscore',
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
}) })
], ],
...@@ -44,12 +55,51 @@ var wpconfig = { ...@@ -44,12 +55,51 @@ var wpconfig = {
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/, exclude: /node_modules/,
use: 'babel-loader' use: 'babel-loader'
},
{
test: /\.coffee$/,
exclude: /node_modules/,
use: 'coffee-loader'
},
{
test: /\.underscore$/,
use: 'raw-loader'
},
{
// This file is used by both RequireJS and Webpack and depends on window globals
// This is a dirty hack and shouldn't be replicated for other files.
test: path.resolve(__dirname, 'cms/static/cms/js/main.js'),
use: {
loader: 'imports-loader',
options: {
AjaxPrefix: 'exports-loader?this.AjaxPrefix!../../../../common/static/coffee/src/ajax_prefix.coffee'
}
}
} }
] ]
}, },
resolve: { resolve: {
extensions: ['.js', '.json'] extensions: ['.js', '.json', '.coffee'],
alias: {
'edx-ui-toolkit': 'edx-ui-toolkit/src/', // @TODO: some paths in toolkit are not valid relative paths
'jquery.ui': 'jQuery-File-Upload/js/vendor/jquery.ui.widget.js',
jquery: 'jquery/src/jquery' // Use the non-dist form of jQuery for better debugging + optimization
},
modules: [
'node_modules',
'common/static/js/vendor/'
]
},
resolveLoader: {
alias: {
text: 'raw-loader' // Compatibility with RequireJSText's text! loader, uses raw-loader under the hood
}
},
externals: {
gettext: 'gettext'
}, },
watchOptions: { watchOptions: {
...@@ -59,7 +109,7 @@ var wpconfig = { ...@@ -59,7 +109,7 @@ var wpconfig = {
if (isProd) { if (isProd) {
wpconfig.plugins = wpconfig.plugins.concat([ wpconfig.plugins = wpconfig.plugins.concat([
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({ // This may not be needed; legacy option for loaders written for webpack 1
minimize: true minimize: true
}), }),
new webpack.optimize.UglifyJsPlugin() new webpack.optimize.UglifyJsPlugin()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment