diff --git a/cms/envs/common.py b/cms/envs/common.py index 10c285045d..93c0fb4a74 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -325,8 +325,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' EMBARGO_SITE_REDIRECT_URL = None ############################### Pipeline ####################################### - -STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' +STATICFILES_STORAGE = 'cms.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage' from rooted_paths import rooted_glob @@ -457,6 +456,34 @@ STATICFILES_IGNORE_PATTERNS = ( PIPELINE_YUI_BINARY = 'yui-compressor' +################################# DJANGO-REQUIRE ############################### + +# The baseUrl to pass to the r.js optimizer, relative to STATIC_ROOT. +REQUIRE_BASE_URL = "./" + +# The name of a build profile to use for your project, relative to REQUIRE_BASE_URL. +# A sensible value would be 'app.build.js'. Leave blank to use the built-in default build profile. +# Set to False to disable running the default profile (e.g. if only using it to build Standalone +# Modules) +REQUIRE_BUILD_PROFILE = "build.js" + +# The name of the require.js script used by your project, relative to REQUIRE_BASE_URL. +REQUIRE_JS = "js/vendor/require.js" + +# A dictionary of standalone modules to build with almond.js. +REQUIRE_STANDALONE_MODULES = {} + +# Whether to run django-require in debug mode. +REQUIRE_DEBUG = False + +# A tuple of files to exclude from the compilation result of r.js. +REQUIRE_EXCLUDE = ("build.txt",) + +# The execution environment in which to run r.js: auto, node or rhino. +# auto will autodetect the environment and make use of node if available and rhino if not. +# It can also be a path to a custom class that subclasses require.environments.Environment and defines some "args" function that returns a list with the command arguments to execute. +REQUIRE_ENVIRONMENT = "node" + ################################# CELERY ###################################### # Message configuration @@ -563,6 +590,7 @@ INSTALLED_APPS = ( 'pipeline', 'staticfiles', 'static_replace', + 'require', # comment common 'django_comment_common', diff --git a/cms/lib/django_require/__init__.py b/cms/lib/django_require/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/lib/django_require/staticstorage.py b/cms/lib/django_require/staticstorage.py new file mode 100644 index 0000000000..4170473acb --- /dev/null +++ b/cms/lib/django_require/staticstorage.py @@ -0,0 +1,13 @@ +""" +:class:`~django_require.staticstorage.OptimizedCachedRequireJsStorage` +""" + +from pipeline.storage import PipelineCachedStorage +from require.storage import OptimizedFilesMixin + + +class OptimizedCachedRequireJsStorage(OptimizedFilesMixin, PipelineCachedStorage): + """ + Custom storage backend that is used by Django-require. + """ + pass diff --git a/cms/static/build.js b/cms/static/build.js new file mode 100644 index 0000000000..d211b2a0e6 --- /dev/null +++ b/cms/static/build.js @@ -0,0 +1,155 @@ +(function () { + 'use strict'; + var getModule = function (moduleName, excludeCommonDeps) { + var module = { + name: moduleName + }; + + if (excludeCommonDeps) { + module.exclude = ['js/factories/common_deps']; + } + + return module; + }; + + var getModulesList = function (modules) { + var result = [getModule('js/factories/common_deps')]; + return result.concat(modules.map(function (moduleName) { + return getModule(moduleName, true); + })); + }; + + return { + /** + * List the modules that will be optimized. All their immediate and deep + * dependencies will be included in the module's file when the build is + * done. + */ + modules: getModulesList([ + 'js/factories/asset_index', + 'js/factories/base', + 'js/factories/checklists', + 'js/factories/container', + 'js/factories/course', + 'js/factories/course_create_rerun', + 'js/factories/course_info', + 'js/factories/edit_tabs', + 'js/factories/export', + 'js/factories/group_configurations', + 'js/factories/import', + 'js/factories/index', + 'js/factories/login', + 'js/factories/manage_users', + 'js/factories/outline', + 'js/factories/register', + 'js/factories/settings', + 'js/factories/settings_advanced', + 'js/factories/settings_graders', + 'js/factories/textbooks' + ]), + /** + * By default all the configuration for optimization happens from the command + * line or by properties in the config file, and configuration that was + * passed to requirejs as part of the app's runtime "main" JS file is *not* + * considered. However, if you prefer the "main" JS file configuration + * to be read for the build so that you do not have to duplicate the values + * in a separate configuration, set this property to the location of that + * main JS file. The first requirejs({}), require({}), requirejs.config({}), + * or require.config({}) call found in that file will be used. + * As of 2.1.10, mainConfigFile can be an array of values, with the last + * value's config take precedence over previous values in the array. + */ + mainConfigFile: 'require-config.js', + /** + * Set paths for modules. If relative paths, set relative to baseUrl above. + * If a special value of "empty:" is used for the path value, then that + * acts like mapping the path to an empty file. It allows the optimizer to + * resolve the dependency to path, but then does not include it in the output. + * Useful to map module names that are to resources on a CDN or other + * http: URL when running in the browser and during an optimization that + * file should be skipped because it has no dependencies. + */ + paths: { + 'gettext': 'empty:', + 'xmodule': 'empty:', + 'mathjax': 'empty:', + 'tender': 'empty:', + 'youtube': 'empty:' + }, + /** + * If shim config is used in the app during runtime, duplicate the config + * here. Necessary if shim config is used, so that the shim's dependencies + * are included in the build. Using "mainConfigFile" is a better way to + * pass this information though, so that it is only listed in one place. + * However, if mainConfigFile is not an option, the shim config can be + * inlined in the build config. + */ + shim: { + 'xmodule': { + deps: [ + 'jquery', 'underscore', 'mathjax', 'codemirror', 'tinymce', + 'jquery.tinymce', 'jquery.qtip', 'jquery.scrollTo', 'jquery.flot', + 'jquery.cookie', 'utility' + ] + } + }, + /** + * Introduced in 2.1.2: If using "dir" for an output directory, normally the + * optimize setting is used to optimize the build bundles (the "modules" + * section of the config) and any other JS file in the directory. However, if + * the non-build bundle JS files will not be loaded after a build, you can + * skip the optimization of those files, to speed up builds. Set this value + * to true if you want to skip optimizing those other non-build bundle JS + * files. + */ + skipDirOptimize: true, + /** + * When the optimizer copies files from the source location to the + * destination directory, it will skip directories and files that start + * with a ".". If you want to copy .directories or certain .files, for + * instance if you keep some packages in a .packages directory, or copy + * over .htaccess files, you can set this to null. If you want to change + * the exclusion rules, change it to a different regexp. If the regexp + * matches, it means the directory will be excluded. This used to be + * called dirExclusionRegExp before the 1.0.2 release. + * As of 1.0.3, this value can also be a string that is converted to a + * RegExp via new RegExp(). + */ + fileExclusionRegExp: /^\.|spec/, + /** + * Allow CSS optimizations. Allowed values: + * - "standard": @import inlining and removal of comments, unnecessary + * whitespace and line returns. + * Removing line returns may have problems in IE, depending on the type + * of CSS. + * - "standard.keepLines": like "standard" but keeps line returns. + * - "none": skip CSS optimizations. + * - "standard.keepComments": keeps the file comments, but removes line + * returns. (r.js 1.0.8+) + * - "standard.keepComments.keepLines": keeps the file comments and line + * returns. (r.js 1.0.8+) + * - "standard.keepWhitespace": like "standard" but keeps unnecessary whitespace. + */ + optimizeCss: 'none', + /** + * How to optimize all the JS files in the build output directory. + * Right now only the following values are supported: + * - "uglify": Uses UglifyJS to minify the code. + * - "uglify2": Uses UglifyJS2. + * - "closure": Uses Google's Closure Compiler in simple optimization + * mode to minify the code. Only available if REQUIRE_ENVIRONMENT is "rhino" (the default). + * - "none": No minification will be done. + */ + optimize: 'uglify2', + /** + * Sets the logging level. It is a number: + * TRACE: 0, + * INFO: 1, + * WARN: 2, + * ERROR: 3, + * SILENT: 4 + * Default is 0. + */ + logLevel: 4 + }; +} ()) diff --git a/cms/static/js/factories/asset_index.js b/cms/static/js/factories/asset_index.js new file mode 100644 index 0000000000..f9e80c5a22 --- /dev/null +++ b/cms/static/js/factories/asset_index.js @@ -0,0 +1,13 @@ +define([ + 'jquery', 'js/collections/asset', 'js/views/assets', 'jquery.fileupload' +], function($, AssetCollection, AssetsView) { + 'use strict'; + return function (assetCallbackUrl) { + var assets = new AssetCollection(), + assetsView; + + assets.url = assetCallbackUrl; + assetsView = new AssetsView({collection: assets, el: $('.assets-wrapper')}); + assetsView.render(); + }; +}); diff --git a/cms/static/js/factories/base.js b/cms/static/js/factories/base.js new file mode 100644 index 0000000000..49671bb8ec --- /dev/null +++ b/cms/static/js/factories/base.js @@ -0,0 +1,2 @@ +define(['js/base', 'coffee/src/main', 'coffee/src/logger', 'datepair', 'accessibility', +'ieshim', 'tooltip_manager']); diff --git a/cms/static/js/factories/checklists.js b/cms/static/js/factories/checklists.js new file mode 100644 index 0000000000..d0cf812683 --- /dev/null +++ b/cms/static/js/factories/checklists.js @@ -0,0 +1,16 @@ +define([ + 'jquery', 'js/collections/checklist', 'js/views/checklist' +], function($, ChecklistCollection, ChecklistView) { + 'use strict'; + return function (handlerUrl) { + var checklistCollection = new ChecklistCollection(), + editor; + + checklistCollection.url = handlerUrl; + editor = new ChecklistView({ + el: $('.course-checklists'), + collection: checklistCollection + }); + checklistCollection.fetch({reset: true}); + }; +}); diff --git a/cms/static/js/factories/common_deps.js b/cms/static/js/factories/common_deps.js new file mode 100644 index 0000000000..1e935d6821 --- /dev/null +++ b/cms/static/js/factories/common_deps.js @@ -0,0 +1 @@ +define(['domReady!', 'jquery', 'backbone', 'underscore', 'gettext']); diff --git a/cms/static/js/factories/container.js b/cms/static/js/factories/container.js new file mode 100644 index 0000000000..93cdeb8fd9 --- /dev/null +++ b/cms/static/js/factories/container.js @@ -0,0 +1,23 @@ +define([ + 'jquery', 'js/models/xblock_info', 'js/views/pages/container', + 'js/collections/component_template', 'xmodule', 'coffee/src/main', + 'xblock/cms.runtime.v1' +], +function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) { + 'use strict'; + return function (componentTemplates, XBlockInfoJson, action, isUnitPage) { + var templates = new ComponentTemplates(componentTemplates, {parse: true}), + mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true}); + + xmoduleLoader.done(function () { + var view = new ContainerPage({ + el: $('#content'), + model: mainXBlockInfo, + action: action, + templates: templates, + isUnitPage: isUnitPage + }); + view.render(); + }); + }; +}); diff --git a/cms/static/js/factories/course.js b/cms/static/js/factories/course.js new file mode 100644 index 0000000000..b5b5f8ad84 --- /dev/null +++ b/cms/static/js/factories/course.js @@ -0,0 +1,6 @@ +define(['js/models/course'], function(Course) { + 'use strict'; + return function (courseInfo) { + window.course = new Course(courseInfo); + } +}); diff --git a/cms/static/js/factories/course_create_rerun.js b/cms/static/js/factories/course_create_rerun.js new file mode 100644 index 0000000000..98afc1d6e5 --- /dev/null +++ b/cms/static/js/factories/course_create_rerun.js @@ -0,0 +1,4 @@ +define(['jquery', 'jquery.form', 'js/views/course_rerun'], function ($) { + 'use strict'; + return function () {}; +}); diff --git a/cms/static/js/factories/course_info.js b/cms/static/js/factories/course_info.js new file mode 100644 index 0000000000..53f4f0b194 --- /dev/null +++ b/cms/static/js/factories/course_info.js @@ -0,0 +1,25 @@ +define([ + 'jquery', 'js/collections/course_update', 'js/models/module_info', + 'js/models/course_info', 'js/views/course_info_edit' +], function($, CourseUpdateCollection, ModuleInfoModel, CourseInfoModel, CourseInfoEditView) { + 'use strict'; + return function (updatesUrl, handoutsLocator, baseAssetUrl) { + var course_updates = new CourseUpdateCollection(), + course_handouts, editor; + + course_updates.url = updatesUrl; + course_updates.fetch({reset: true}); + course_handouts = new ModuleInfoModel({ + id: handoutsLocator + }); + editor = new CourseInfoEditView({ + el: $('.main-wrapper'), + model : new CourseInfoModel({ + updates : course_updates, + base_asset_url : baseAssetUrl, + handouts : course_handouts + }) + }); + editor.render(); + }; +}); diff --git a/cms/static/js/factories/edit_tabs.js b/cms/static/js/factories/edit_tabs.js new file mode 100644 index 0000000000..34ad04d12a --- /dev/null +++ b/cms/static/js/factories/edit_tabs.js @@ -0,0 +1,20 @@ +define([ + 'js/models/explicit_url', 'coffee/src/views/tabs', 'xmodule', 'coffee/src/main', 'xblock/cms.runtime.v1' +], function (TabsModel, TabsEditView, xmoduleLoader) { + 'use strict'; + return function (courseLocation, explicitUrl) { + xmoduleLoader.done(function () { + var model = new TabsModel({ + id: courseLocation, + explicit_url: explicitUrl + }), + editView; + + editView = new TabsEditView({ + el: $('.tab-list'), + model: model, + mast: $('.wrapper-mast') + }); + }); + }; +}); diff --git a/cms/static/js/factories/export.js b/cms/static/js/factories/export.js new file mode 100644 index 0000000000..295fd406d5 --- /dev/null +++ b/cms/static/js/factories/export.js @@ -0,0 +1,57 @@ +define(['gettext', 'js/views/feedback_prompt'], function(gettext, PromptView) { + 'use strict'; + return function (hasUnit, editUnitUrl, courseHomeUrl, errMsg) { + var dialog; + if(hasUnit) { + 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 { + var msg = '

' + gettext('There has been a failure to export your course to XML. Unfortunately, we do not have specific enough information to assist you in identifying the failed component. It is recommended that you inspect your courseware to identify any components in error and try again.') + '

' + 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: gettext('Yes, take me to the main course page'), + click: function(view) { + view.hide(); + document.location = courseHomeUrl; + } + }, + 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(); + }; +}); diff --git a/cms/static/js/factories/group_configurations.js b/cms/static/js/factories/group_configurations.js new file mode 100644 index 0000000000..559c4344ee --- /dev/null +++ b/cms/static/js/factories/group_configurations.js @@ -0,0 +1,16 @@ +define([ + 'js/collections/group_configuration', 'js/views/pages/group_configurations' +], function(GroupConfigurationCollection, GroupConfigurationsPage) { + 'use strict'; + return function (configurations, groupConfigurationUrl, courseOutlineUrl) { + var collection = new GroupConfigurationCollection(configurations, { parse: true }), + configurationsPage; + + collection.url = groupConfigurationUrl; + collection.outlineUrl = courseOutlineUrl; + configurationsPage = new GroupConfigurationsPage({ + el: $('#content'), + collection: collection + }).render(); + }; +}); diff --git a/cms/static/js/factories/import.js b/cms/static/js/factories/import.js new file mode 100644 index 0000000000..2577844e42 --- /dev/null +++ b/cms/static/js/factories/import.js @@ -0,0 +1,107 @@ +define([ + 'js/views/import', 'jquery', 'gettext', 'jquery.fileupload', 'jquery.cookie' +], function(CourseImport, $, gettext) { + 'use strict'; + return function (feedbackUrl) { + var bar = $('.progress-bar'), + fill = $('.progress-fill'), + submitBtn = $('.submit-button'), + chooseBtn = $('.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', + gettext('There was an error while importing the new course to our database.') + '\n' + ], + // Display the status of last file upload on page load + lastFileUpload = $.cookie('lastfileupload'), + file; + + if (lastFileUpload){ + CourseImport.getAndStartUploadFeedback(feedbackUrl.replace('fillerName', lastFileUpload), lastFileUpload); + } + + $('#fileupload').fileupload({ + dataType: 'json', + type: 'POST', + maxChunkSize: 20 * 1000000, // 20 MB + autoUpload: false, + add: function(e, data) { + CourseImport.clearImportDisplay(); + submitBtn.unbind('click'); + file = data.files[0]; + if (file.name.match(/tar\.gz$/)) { + submitBtn.click(function(event){ + event.preventDefault(); + $.cookie('lastfileupload', file.name); + submitBtn.hide(); + CourseImport.startUploadFeedback(); + data.submit().complete(function(result, textStatus, xhr) { + window.onbeforeunload = null; + if (xhr.status != 200) { + var serverMsg, errMsg, stage; + try{ + serverMsg = $.parseJSON(result.responseText); + } catch (e) { + return; + } + errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : '' ; + if (serverMsg.hasOwnProperty('Stage')) { + stage = Math.abs(serverMsg.Stage); + CourseImport.stageError(stage, defaults[stage] + errMsg); + } + else { + alert(gettext('Your import has failed.') + '\n\n' + errMsg); + } + chooseBtn.html(gettext('Choose new file')).show(); + bar.hide(); + } + CourseImport.stopGetStatus = true; + chooseBtn.html(gettext('Choose new file')).show(); + bar.hide(); + }); + }); + } else { + data.files = []; + } + }, + + progressall: function(e, data){ + var percentInt = data.loaded / data.total * 100, + percentVal = parseInt(percentInt, 10) + '%', + doneAt; + // Firefox makes ProgressEvent.loaded equal ProgressEvent.total only + // after receiving a response from the server (see Mozilla bug 637002), + // so for Firefox we jump the gun a little. + if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { + doneAt = 95; + } else { + doneAt = 99; + } + if (percentInt >= doneAt) { + bar.hide(); + // Start feedback with delay so that current stage of import properly updates in session + setTimeout( + function () { CourseImport.startServerFeedback(feedbackUrl.replace('fillerName', file.name));}, + 3000 + ); + } else { + bar.show(); + fill.width(percentVal).html(percentVal); + } + }, + done: function(event, data){ + bar.hide(); + window.onbeforeunload = null; + CourseImport.displayFinishedImport(); + }, + start: function(event) { + window.onbeforeunload = function() { + return gettext('Your import is in progress; navigating away will abort it.'); + }; + }, + sequentialUploads: true, + notifyOnError: false + }); + }; +}); diff --git a/cms/static/js/factories/index.js b/cms/static/js/factories/index.js new file mode 100644 index 0000000000..15ee2ec501 --- /dev/null +++ b/cms/static/js/factories/index.js @@ -0,0 +1,40 @@ +define(['jquery.form', 'js/index'], function() { + 'use strict'; + return function () { + // showing/hiding creation rights UI + $('.show-creationrights').click(function(e) { + e.preventDefault(); + $(this) + .closest('.wrapper-creationrights') + .toggleClass('is-shown') + .find('.ui-toggle-control') + .toggleClass('current'); + }); + + var reloadPage = function () { + location.reload(); + }; + + var showError = function () { + $('#request-coursecreator-submit') + .toggleClass('has-error') + .find('.label') + .text('Sorry, there was error with your request'); + $('#request-coursecreator-submit') + .find('.icon-cog') + .toggleClass('icon-spin'); + }; + + $('#request-coursecreator').ajaxForm({ + error: showError, + success: reloadPage + }); + + $('#request-coursecreator-submit').click(function(event){ + $(this) + .toggleClass('is-disabled is-submitting') + .find('.label') + .text('Submitting Your Request'); + }); + }; +}); diff --git a/cms/static/js/factories/login.js b/cms/static/js/factories/login.js new file mode 100644 index 0000000000..5115406044 --- /dev/null +++ b/cms/static/js/factories/login.js @@ -0,0 +1,43 @@ +define(['jquery.cookie', 'utility'], function() { + 'use strict'; + return function (homepageURL) { + function postJSON(url, data, callback) { + $.ajax({ + type:'POST', + url: url, + dataType: 'json', + data: data, + success: callback, + headers : {'X-CSRFToken':$.cookie('csrftoken')} + }); + } + + $('form#login_form').submit(function(event) { + event.preventDefault(); + var submit_data = $('#login_form').serialize(); + + postJSON('/login_post', submit_data, function(json) { + if(json.success) { + var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search)); + if (next && next.length > 1 && !isExternal(next[1])) { + location.href = next[1]; + } else { + location.href = homepageURL; + } + } else if($('#login_error').length === 0) { + $('#login_form').prepend( + '
' + + json.value + + '
' + ); + $('#login_error').addClass('is-shown'); + } else { + $('#login_error') + .stop() + .addClass('is-shown') + .html(json.value); + } + }); + }); + }; +}); diff --git a/cms/static/js/factories/manage_users.js b/cms/static/js/factories/manage_users.js new file mode 100644 index 0000000000..42272a859d --- /dev/null +++ b/cms/static/js/factories/manage_users.js @@ -0,0 +1,203 @@ +define(['jquery', 'underscore', 'gettext', 'js/views/feedback_prompt'], function($, _, gettext, PromptView) { + 'use strict'; + return function (staffEmails, tplUserURL) { + var unknownErrorMessage = gettext('Unknown'), + $createUserForm = $('#create-user-form'), + $createUserFormWrapper = $createUserForm.closest('.wrapper-create-user'), + $cancelButton; + + $createUserForm.bind('submit', function(event) { + event.preventDefault(); + var email = $('#user-email-input').val().trim(), + url, msg; + + if(!email) { + msg = new PromptView.Error({ + title: gettext('A valid email address is required'), + message: gettext('You must enter a valid email address in order to add a new team member'), + actions: { + primary: { + text: gettext('Return and add email address'), + click: function(view) { + view.hide(); + $('#user-email-input').focus(); + } + } + } + }); + msg.show(); + } + + if(_.contains(staffEmails, email)) { + msg = new PromptView.Warning({ + title: gettext('Already a course team member'), + message: _.template( + gettext("{email} is already on the “{course}” team. If you're trying to add a new member, please double-check the email address you provided."), { + email: email, + course: course.escape('name') + }, {interpolate: /\{(.+?)\}/g} + ), + actions: { + primary: { + text: gettext('Return to team listing'), + click: function(view) { + view.hide(); + $('#user-email-input').focus(); + } + } + } + }); + msg.show(); + } + + url = tplUserURL.replace('@@EMAIL@@', $('#user-email-input').val().trim()); + $.ajax({ + url: url, + type: 'POST', + dataType: 'json', + contentType: 'application/json', + notifyOnError: false, + data: JSON.stringify({role: 'staff'}), + success: function(data) {location.reload();}, + error: function(jqXHR, textStatus, errorThrown) { + var message, prompt; + try { + message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage; + } catch (e) { + message = unknownErrorMessage; + } + prompt = new PromptView.Error({ + title: gettext('Error adding user'), + message: message, + actions: { + primary: { + text: gettext('OK'), + click: function(view) { + view.hide(); + $('#user-email-input').focus(); + } + } + } + }); + prompt.show(); + } + }); + }); + + $cancelButton = $createUserForm.find('.action-cancel'); + $cancelButton.bind('click', function(event) { + event.preventDefault(); + $('.create-user-button').toggleClass('is-disabled'); + $createUserFormWrapper.toggleClass('is-shown'); + $('#user-email-input').val(''); + }); + + $('.create-user-button').bind('click', function(event) { + event.preventDefault(); + $('.create-user-button').toggleClass('is-disabled'); + $createUserFormWrapper.toggleClass('is-shown'); + $createUserForm.find('#user-email-input').focus(); + }); + + $('body').bind('keyup', function(event) { + if(event.which == 27) { + $cancelButton.click(); + } + }); + + $('.remove-user').click(function() { + var email = $(this).data('id'), + msg = new PromptView.Warning({ + title: gettext('Are you sure?'), + message: _.template(gettext('Are you sure you want to delete {email} from the course team for “{course}”?'), {email: email, course: course.get('name')}, {interpolate: /\{(.+?)\}/g}), + actions: { + primary: { + text: gettext('Delete'), + click: function(view) { + var url = tplUserURL.replace('@@EMAIL@@', email); + view.hide(); + $.ajax({ + url: url, + type: 'DELETE', + dataType: 'json', + contentType: 'application/json', + notifyOnError: false, + success: function(data) {location.reload();}, + error: function(jqXHR, textStatus, errorThrown) { + var message; + try { + message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage; + } catch (e) { + message = unknownErrorMessage; + } + var prompt = new PromptView.Error({ + title: gettext('Error removing user'), + message: message, + actions: { + primary: { + text: gettext('OK'), + click: function(view) { + view.hide(); + } + } + } + }); + prompt.show(); + } + }); + } + }, + secondary: { + text: gettext('Cancel'), + click: function(view) { + view.hide(); + } + } + } + }); + msg.show(); + }); + + $('.toggle-admin-role').click(function(event) { + event.preventDefault(); + var type, url, role; + if($(this).hasClass('add-admin-role')) { + role = 'instructor'; + } else { + role = 'staff'; + } + + url = $(this).closest('li[data-url]').data('url'); + $.ajax({ + url: url, + type: 'POST', + dataType: 'json', + contentType: 'application/json', + notifyOnError: false, + data: JSON.stringify({role: role}), + success: function(data) {location.reload();}, + error: function(jqXHR, textStatus, errorThrown) { + var message, prompt; + try { + message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage; + } catch (e) { + message = unknownErrorMessage; + } + prompt = new PromptView.Error({ + title: gettext("There was an error changing the user's role"), + message: message, + actions: { + primary: { + text: gettext('Try Again'), + click: function(view) { + view.hide(); + } + } + } + }); + prompt.show(); + } + }); + }); + }; +}); diff --git a/cms/static/js/factories/outline.js b/cms/static/js/factories/outline.js new file mode 100644 index 0000000000..a0b6cb25e5 --- /dev/null +++ b/cms/static/js/factories/outline.js @@ -0,0 +1,14 @@ +define([ + 'js/views/pages/course_outline', 'js/models/xblock_outline_info' +], function(CourseOutlinePage, XBlockOutlineInfo) { + 'use strict'; + return function (XBlockOutlineInfoJson, initialStateJson) { + var courseXBlock = new XBlockOutlineInfo(XBlockOutlineInfoJson, {parse: true}), + view = new CourseOutlinePage({ + el: $('#content'), + model: courseXBlock, + initialState: initialStateJson + }); + view.render(); + }; +}); diff --git a/cms/static/js/factories/register.js b/cms/static/js/factories/register.js new file mode 100644 index 0000000000..5f0fe5aaf6 --- /dev/null +++ b/cms/static/js/factories/register.js @@ -0,0 +1,33 @@ +define(['jquery', 'jquery.cookie'], function($) { + 'use strict'; + return function () { + $('form :input') + .focus(function() { + $('label[for="' + this.id + '"]').addClass('is-focused'); + }) + .blur(function() { + $('label').removeClass('is-focused'); + }); + + $('form#register_form').submit(function(event) { + event.preventDefault(); + var submit_data = $('#register_form').serialize(); + + $.ajax({ + url: '/create_account', + type: 'POST', + dataType: 'json', + headers: {'X-CSRFToken': $.cookie('csrftoken')}, + notifyOnError: false, + data: submit_data, + success: function(json) { + location.href = '/course/'; + }, + error: function(jqXHR, textStatus, errorThrown) { + var json = $.parseJSON(jqXHR.responseText); + $('#register_error').html(json.value).stop().addClass('is-shown'); + } + }); + }); + }; +}); diff --git a/cms/static/js/factories/settings.js b/cms/static/js/factories/settings.js new file mode 100644 index 0000000000..09ec56c0e8 --- /dev/null +++ b/cms/static/js/factories/settings.js @@ -0,0 +1,29 @@ +define([ + 'jquery', 'js/models/settings/course_details', 'js/views/settings/main' +], function($, CourseDetailsModel, MainView) { + 'use strict'; + return function (detailsUrl) { + var model; + // highlighting labels when fields are focused in + $('form :input') + .focus(function() { + $('label[for="' + this.id + '"]').addClass('is-focused'); + }) + .blur(function() { + $('label').removeClass('is-focused'); + }); + + model = new CourseDetailsModel(); + model.urlRoot = detailsUrl; + model.fetch({ + success: function(model) { + var editor = new MainView({ + el: $('.settings-details'), + model: model + }); + editor.render(); + }, + reset: true + }); + }; +}); diff --git a/cms/static/js/factories/settings_advanced.js b/cms/static/js/factories/settings_advanced.js new file mode 100644 index 0000000000..b12c0cb147 --- /dev/null +++ b/cms/static/js/factories/settings_advanced.js @@ -0,0 +1,44 @@ +define([ + 'jquery', 'gettext', 'js/models/settings/advanced', 'js/views/settings/advanced' +], function($, gettext, AdvancedSettingsModel, AdvancedSettingsView) { + 'use strict'; + return function (advancedDict, advancedSettingsUrl) { + var advancedModel, editor; + + $('form :input') + .focus(function() { + $('label[for="' + this.id + '"]').addClass('is-focused'); + }) + .blur(function() { + $('label').removeClass('is-focused'); + }); + + // proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern + advancedModel = new AdvancedSettingsModel(advancedDict, {parse: true}); + advancedModel.url = advancedSettingsUrl; + + editor = new AdvancedSettingsView({ + el: $('.settings-advanced'), + model: advancedModel + }); + editor.render(); + + $('#deprecated-settings').click(function() { + var wrapperDeprecatedSetting = $('.wrapper-deprecated-setting'), + deprecatedSettingsLabel = $('.deprecated-settings-label'); + + if ($(this).is(':checked')) { + wrapperDeprecatedSetting.addClass('is-set'); + deprecatedSettingsLabel.text(gettext('Hide Deprecated Settings')); + editor.render_deprecated = true; + } + else { + wrapperDeprecatedSetting.removeClass('is-set'); + deprecatedSettingsLabel.text(gettext('Show Deprecated Settings')); + editor.render_deprecated = false; + } + + editor.render(); + }); + }; +}); diff --git a/cms/static/js/factories/settings_graders.js b/cms/static/js/factories/settings_graders.js new file mode 100644 index 0000000000..4ed4e02df9 --- /dev/null +++ b/cms/static/js/factories/settings_graders.js @@ -0,0 +1,24 @@ +define([ + 'jquery', 'js/views/settings/grading', 'js/models/settings/course_grading_policy' +], function($, GradingView, CourseGradingPolicyModel) { + 'use strict'; + return function (courseDetails, gradingUrl) { + var model, editor; + + $('form :input') + .focus(function() { + $('label[for="' + this.id + '"]').addClass('is-focused'); + }) + .blur(function() { + $('label').removeClass('is-focused'); + }); + + model = new CourseGradingPolicyModel(courseDetails,{parse:true}); + model.urlRoot = gradingUrl; + editor = new GradingView({ + el: $('.settings-grading'), + model : model + }); + editor.render(); + }; +}); diff --git a/cms/static/js/factories/textbooks.js b/cms/static/js/factories/textbooks.js new file mode 100644 index 0000000000..fc0c301e1d --- /dev/null +++ b/cms/static/js/factories/textbooks.js @@ -0,0 +1,20 @@ +define([ + 'gettext', 'js/models/section', 'js/collections/textbook', 'js/views/list_textbooks' +], function(gettext, Section, TextbookCollection, ListTextbooksView) { + 'use strict'; + return function (textbooksJson) { + var textbooks = new TextbookCollection(textbooksJson, {parse: true}), + tbView = new ListTextbooksView({collection: textbooks}); + + $('.content-primary').append(tbView.render().el); + $('.nav-actions .new-button').click(function(event) { + tbView.addOne(event); + }); + $(window).on('beforeunload', function() { + var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); + if(dirty) { + return gettext('You have unsaved changes. Do you really want to leave this page?'); + } + }); + }; +}); diff --git a/cms/static/require-config.js b/cms/static/require-config.js new file mode 100644 index 0000000000..bbfd8a5908 --- /dev/null +++ b/cms/static/require-config.js @@ -0,0 +1,266 @@ +require.config({ + // NOTE: baseUrl has been previously set in cms/static/templates/base.html + waitSeconds: 60, + paths: { + "domReady": "js/vendor/domReady", + "gettext": "/i18n", + "mustache": "js/vendor/mustache", + "codemirror": "js/vendor/codemirror-compressed", + "codemirror/stex": "js/vendor/CodeMirror/stex", + "jquery": "js/vendor/jquery.min", + "jquery.ui": "js/vendor/jquery-ui.min", + "jquery.form": "js/vendor/jquery.form", + "jquery.markitup": "js/vendor/markitup/jquery.markitup", + "jquery.leanModal": "js/vendor/jquery.leanModal.min", + "jquery.ajaxQueue": "js/vendor/jquery.ajaxQueue", + "jquery.smoothScroll": "js/vendor/jquery.smooth-scroll.min", + "jquery.timepicker": "js/vendor/timepicker/jquery.timepicker", + "jquery.cookie": "js/vendor/jquery.cookie", + "jquery.qtip": "js/vendor/jquery.qtip.min", + "jquery.scrollTo": "js/vendor/jquery.scrollTo-1.4.2-min", + "jquery.flot": "js/vendor/flot/jquery.flot.min", + "jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload", + "jquery.iframe-transport": "js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", + "jquery.inputnumber": "js/vendor/html5-input-polyfills/number-polyfill", + "jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents", + "datepair": "js/vendor/timepicker/datepair", + "date": "js/vendor/date", + "underscore": "js/vendor/underscore-min", + "underscore.string": "js/vendor/underscore.string.min", + "backbone": "js/vendor/backbone-min", + "backbone.associations": "js/vendor/backbone-associations-min", + "backbone.paginator": "js/vendor/backbone.paginator.min", + "tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min", + "jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min", + "xmodule": "/xmodule/xmodule", + "xblock": "coffee/src/xblock", + "utility": "js/src/utility", + "accessibility": "js/src/accessibility_tools", + "draggabilly": "js/vendor/draggabilly.pkgd", + "URI": "js/vendor/URI.min", + "ieshim": "js/src/ie_shim", + "tooltip_manager": "js/src/tooltip_manager", + + // Files needed for Annotations feature + "annotator": "js/vendor/ova/annotator-full", + "annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth", + "video.dev": "js/vendor/ova/video.dev", + "vjs.youtube": 'js/vendor/ova/vjs.youtube', + "rangeslider": 'js/vendor/ova/rangeslider', + "share-annotator": 'js/vendor/ova/share-annotator', + "richText-annotator": 'js/vendor/ova/richText-annotator', + "reply-annotator": 'js/vendor/ova/reply-annotator', + "grouping-annotator": 'js/vendor/ova/grouping-annotator', + "tags-annotator": 'js/vendor/ova/tags-annotator', + "diacritic-annotator": 'js/vendor/ova/diacritic-annotator', + "flagging-annotator": 'js/vendor/ova/flagging-annotator', + "jquery-Watch": 'js/vendor/ova/jquery-Watch', + "openseadragon": 'js/vendor/ova/openseadragon', + "osda": 'js/vendor/ova/OpenSeaDragonAnnotation', + "ova": 'js/vendor/ova/ova', + "catch": 'js/vendor/ova/catch/js/catch', + "handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2', + // end of Annotation tool files + + // externally hosted files + "tender": [ + "//edxedge.tenderapp.com/tender_widget", + // if tender fails to load, fallback on a local file + // so that require doesn't fall over + "js/src/tender_fallback" + ], + "mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured", + "youtube": [ + // youtube URL does not end in ".js". We add "?noext" to the path so + // that require.js adds the ".js" to the query component of the URL, + // and leaves the path component intact. + "//www.youtube.com/player_api?noext", + // if youtube fails to load, fallback on a local file + // so that require doesn't fall over + "js/src/youtube_fallback" + ] + }, + shim: { + "gettext": { + exports: "gettext" + }, + "date": { + exports: "Date" + }, + "jquery.ui": { + deps: ["jquery"], + exports: "jQuery.ui" + }, + "jquery.form": { + deps: ["jquery"], + exports: "jQuery.fn.ajaxForm" + }, + "jquery.markitup": { + deps: ["jquery"], + exports: "jQuery.fn.markitup" + }, + "jquery.leanmodal": { + deps: ["jquery"], + exports: "jQuery.fn.leanModal" + }, + "jquery.ajaxQueue": { + deps: ["jquery"], + exports: "jQuery.fn.ajaxQueue" + }, + "jquery.smoothScroll": { + deps: ["jquery"], + exports: "jQuery.fn.smoothScroll" + }, + "jquery.cookie": { + deps: ["jquery"], + exports: "jQuery.fn.cookie" + }, + "jquery.qtip": { + deps: ["jquery"], + exports: "jQuery.fn.qtip" + }, + "jquery.scrollTo": { + deps: ["jquery"], + exports: "jQuery.fn.scrollTo", + }, + "jquery.flot": { + deps: ["jquery"], + exports: "jQuery.fn.plot" + }, + "jquery.fileupload": { + deps: ["jquery.iframe-transport"], + exports: "jQuery.fn.fileupload" + }, + "jquery.inputnumber": { + deps: ["jquery"], + exports: "jQuery.fn.inputNumber" + }, + "jquery.tinymce": { + deps: ["jquery", "tinymce"], + exports: "jQuery.fn.tinymce" + }, + "datepair": { + deps: ["jquery.ui", "jquery.timepicker"] + }, + "underscore": { + exports: "_" + }, + "backbone": { + deps: ["underscore", "jquery"], + exports: "Backbone" + }, + "backbone.associations": { + deps: ["backbone"], + exports: "Backbone.Associations" + }, + "backbone.paginator": { + deps: ["backbone"], + exports: "Backbone.Paginator" + }, + "tender": { + exports: 'Tender' + }, + "youtube": { + exports: "YT" + }, + "codemirror": { + exports: "CodeMirror" + }, + "codemirror/stex": { + deps: ["codemirror"] + }, + "tinymce": { + exports: "tinymce" + }, + "mathjax": { + exports: "MathJax", + init: function() { + MathJax.Hub.Config({ + tex2jax: { + inlineMath: [ + ["\\(","\\)"], + ['[mathjaxinline]','[/mathjaxinline]'] + ], + displayMath: [ + ["\\[","\\]"], + ['[mathjax]','[/mathjax]'] + ] + } + }); + MathJax.Hub.Configured(); + } + }, + "URI": { + exports: "URI" + }, + "tooltip_manager": { + deps: ["jquery", "underscore"] + }, + "jquery.immediateDescendents": { + deps: ["jquery"] + }, + "xblock/core": { + exports: "XBlock", + deps: ["jquery", "jquery.immediateDescendents"] + }, + "xblock/runtime.v1": { + exports: "XBlock", + deps: ["xblock/core"] + }, + + "coffee/src/main": { + deps: ["coffee/src/ajax_prefix"] + }, + "coffee/src/logger": { + exports: "Logger", + deps: ["coffee/src/ajax_prefix"] + }, + + // the following are all needed for annotation tools + "video.dev": { + exports:"videojs" + }, + "vjs.youtube": { + deps: ["video.dev"] + }, + "rangeslider": { + deps: ["video.dev"] + }, + "annotator": { + exports: "Annotator" + }, + "annotator-harvardx":{ + deps: ["annotator"] + }, + "share-annotator": { + deps: ["annotator"] + }, + "richText-annotator": { + deps: ["annotator", "tinymce"] + }, + "reply-annotator": { + deps: ["annotator"] + }, + "tags-annotator": { + deps: ["annotator"] + }, + "diacritic-annotator": { + deps: ["annotator"] + }, + "flagging-annotator": { + deps: ["annotator"] + }, + "grouping-annotator": { + deps: ["annotator"] + }, + "ova":{ + exports: "ova", + deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "jquery-Watch", "catch", "handlebars", "URI"] + }, + "osda":{ + exports: "osda", + deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars", "URI"] + }, + // end of annotation tool files + } +}); diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index ab36424574..7e886b7241 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -17,18 +17,10 @@ % endfor -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/asset_index"], function (AssetIndexFactory) { + AssetIndexFactory("${asset_callback_url}"); + }); <%block name="content"> @@ -72,9 +64,9 @@

${_("Using File URLs")}

- +

${_("Use the {em_start}Embed URL{em_end} value to link to the file or image from a component, a course update, or a course handout.").format(em_start='', em_end="")}

- +

${_("Use the {em_start}External URL{em_end} value to reference the file or image only from outside of your course.").format(em_start='', em_end="")}

${_("Click in the Embed URL or External URL column to select the value, then copy it.")}

diff --git a/cms/templates/base.html b/cms/templates/base.html index 97e28b3c0f..ecd68e593a 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -46,305 +46,17 @@ ${_("Skip to this view's content")} + ## js templates - - % if context_course: - - % endif -
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %> @@ -359,12 +71,8 @@
% if user.is_authenticated(): - - <%include file="widgets/sock.html" args="online_help_token=online_help_token" /> + <%include file="widgets/sock.html" args="online_help_token=online_help_token" /> % endif - <%include file="widgets/footer.html" /> <%include file="widgets/tender.html" /> @@ -373,6 +81,28 @@
<%block name="jsextra"> + diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html index 71f60d5874..75530df5ae 100644 --- a/cms/templates/checklists.html +++ b/cms/templates/checklists.html @@ -17,20 +17,10 @@ % endfor -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/checklists"], function (ChecklistsFactory) { + ChecklistsFactory("${handler_url}"); + }); diff --git a/cms/templates/container.html b/cms/templates/container.html index 1de6dcff75..4a6e0eaae2 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -34,28 +34,13 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal", -<%block name="jsextra"> - - +<%block name="requirejs"> + require(["js/factories/container"], function(ContainerFactory) { + ContainerFactory( + ${component_templates | n}, ${json.dumps(xblock_info) | n}, + "${action}", ${json.dumps(is_unit_page)} + ); + }); <%block name="content"> diff --git a/cms/templates/course-create-rerun.html b/cms/templates/course-create-rerun.html index cfa4260b17..bee893a5c0 100644 --- a/cms/templates/course-create-rerun.html +++ b/cms/templates/course-create-rerun.html @@ -8,15 +8,14 @@ <%block name="bodyclass">is-signedin view-course-create view-course-create-rerun <%block name="jsextra"> - - - + + +<%block name="requirejs"> + require(["js/factories/course_create_rerun"], function (CourseCreateRerunFactory) { + CourseCreateRerunFactory(); + }); <%block name="content"> diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index 71e3149566..ccb4bcdd0f 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -19,30 +19,10 @@ % endfor -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/course_info"], function(CourseInfoFactory) { + CourseInfoFactory("${updates_url}", "${handouts_locator | escapejs}", "${base_asset_url}"); + }); <%block name="content"> diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 5b8db60705..1fe5648c04 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -12,19 +12,10 @@ from contentstore.utils import reverse_usage_url <%namespace name='static' file='static_content.html'/> -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/outline"], function (OutlineFactory) { + OutlineFactory(${json.dumps(course_structure) | n}, ${json.dumps(initial_state) | n}); + }); <%block name="header_extras"> diff --git a/cms/templates/edit-tabs.html b/cms/templates/edit-tabs.html index ff3abca634..d36307d073 100644 --- a/cms/templates/edit-tabs.html +++ b/cms/templates/edit-tabs.html @@ -18,25 +18,10 @@ % endfor -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/edit_tabs"], function (EditTabsFactory) { + EditTabsFactory("${context_course.location | escapejs}", "${reverse('contentstore.views.tabs_handler', kwargs={'course_key_string': context_course.id})}"); + }); <%block name="content"> diff --git a/cms/templates/export.html b/cms/templates/export.html index e41bb299d6..0d8f783f2b 100644 --- a/cms/templates/export.html +++ b/cms/templates/export.html @@ -9,71 +9,17 @@ <%block name="title">${_("Course Export")} <%block name="bodyclass">is-signedin course tools view-export -<%block name="jsextra"> - % if in_err: - - %endif + require(["js/factories/export"], function(ExportFactory) { + ExportFactory(hasUnit, editUnitUrl, courseHomeUrl, errMsg); + }); +%endif <%block name="content"> diff --git a/cms/templates/group_configurations.html b/cms/templates/group_configurations.html index 7ef51bedd5..2cdf6ed7ee 100644 --- a/cms/templates/group_configurations.html +++ b/cms/templates/group_configurations.html @@ -18,22 +18,12 @@ % endfor -<%block name="jsextra"> - +<%block name="requirejs"> +% if configurations is not None: + require(["js/factories/group_configurations"], function(GroupConfigurationsFactory) { + GroupConfigurationsFactory(${json.dumps(configurations)}, "${group_configuration_url}", "${course_outline_url}"); + }); +% endif <%block name="content"> diff --git a/cms/templates/import.html b/cms/templates/import.html index 998f0fc8da..2460b74ab9 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -147,127 +147,8 @@ -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/import"], function(ImportFactory) { + ImportFactory("${import_status_url}"); + }); diff --git a/cms/templates/index.html b/cms/templates/index.html index aabe52c799..91847cfab0 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -5,31 +5,10 @@ <%block name="title">${_("My Courses")} <%block name="bodyclass">is-signedin index view-dashboard -<%block name="jsextra"> - +<%block name="requirejs"> + require(["js/factories/index"], function (IndexFactory) { + IndexFactory(); + }); <%block name="content"> diff --git a/cms/templates/login.html b/cms/templates/login.html index 82988ebd75..7034cfad48 100644 --- a/cms/templates/login.html +++ b/cms/templates/login.html @@ -57,42 +57,8 @@ from django.utils.translation import ugettext as _ -<%block name="jsextra"> - diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 22f82192ab..982d84cf94 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -161,216 +161,8 @@ -<%block name="jsextra"> - diff --git a/cms/templates/register.html b/cms/templates/register.html index 9986f5ba57..dd1e72c5ca 100644 --- a/cms/templates/register.html +++ b/cms/templates/register.html @@ -99,35 +99,8 @@ -<%block name="jsextra"> - diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 905ff06601..b9026083e6 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -25,30 +25,13 @@ window.CMS = window.CMS || {}; CMS.URL = CMS.URL || {}; CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; - -require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/settings/main"], - function(doc, $, CourseDetailsModel, MainView) { - // highlighting labels when fields are focused in - $("form :input").focus(function() { - $("label[for='" + this.id + "']").addClass("is-focused"); - }).blur(function() { - $("label").removeClass("is-focused"); - }); - var model = new CourseDetailsModel(); - model.urlRoot = '${details_url}'; - model.fetch({ - success: function(model) { - var editor = new MainView({ - el: $('.settings-details'), - model: model - }); - editor.render(); - }, - reset: true - }); -}); +<%block name="requirejs"> + require(["js/factories/settings"], function(SettingsFactory) { + SettingsFactory("${details_url}"); + }); + <%block name="content">
diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index ab530e88a4..7a0bca3c16 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -9,53 +9,18 @@ <%block name="title">${_("Advanced Settings")} <%block name="bodyclass">is-signedin course advanced view-settings -<%block name="jsextra"> +<%block name="header_extras"> % for template_name in ["advanced_entry", "basic-modal", "modal-button", "validation-error-modal"]: % endfor + - +<%block name="requirejs"> + require(["js/factories/settings_advanced"], function(SettingsAdvancedFactory) { + SettingsAdvancedFactory(${advanced_dict | n}, "${advanced_settings_url}"); + }); <%block name="content"> diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index 9c26972bb4..7d309b0f55 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -19,24 +19,11 @@ <%block name="jsextra"> - <%block name="content"> diff --git a/cms/templates/textbooks.html b/cms/templates/textbooks.html index 840de4e289..2a28b03c4b 100644 --- a/cms/templates/textbooks.html +++ b/cms/templates/textbooks.html @@ -22,26 +22,13 @@ CMS.URL = CMS.URL || {}; CMS.URL.UPLOAD_ASSET = "${upload_asset_url}" CMS.URL.TEXTBOOKS = "${textbook_url}" CMS.URL.LMS_BASE = "${settings.LMS_BASE}" -require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"], - function(Section, TextbookCollection, ListTextbooksView) { - var textbooks = new TextbookCollection(${json.dumps(textbooks)}, {parse: true}); - var tbView = new ListTextbooksView({collection: textbooks}); - - $(function() { - $(".content-primary").append(tbView.render().el); - $(".nav-actions .new-button").click(function(e) { - tbView.addOne(e); - }) - $(window).on("beforeunload", function() { - var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); - if(dirty) { - return "${_('You have unsaved changes. Do you really want to leave this page?')}"; - } - }) - }) -}); +<%block name="requirejs"> + require(["js/factories/textbooks"], function(TextbooksFactory) { + TextbooksFactory(${json.dumps(textbooks)}); + }); + <%block name="content">
diff --git a/common/static/js/vendor/date.js b/common/static/js/vendor/date.js index 3cb003fa37..0781591518 100644 --- a/common/static/js/vendor/date.js +++ b/common/static/js/vendor/date.js @@ -3,7 +3,7 @@ * @author: Coolite Inc. http://www.coolite.com/ * @date: 2008-05-13 * @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved. - * @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. * @website: http://www.datejs.com/ */ Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]}; @@ -44,7 +44,7 @@ f(this.getUTCDate())+'T'+ f(this.getUTCHours())+':'+ f(this.getUTCMinutes())+':'+ f(this.getUTCSeconds())+'Z"';};} -$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}} +if(typeof $P._toString === 'undefined'){$P._toString=$P.toString;}$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}} var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");} x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}()); (function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());} diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index 4c7f6e87c1..a4614f8b10 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -2,6 +2,7 @@ Course Advanced Settings page """ +from bok_choy.promise import EmptyPromise from .course_page import CoursePage from .utils import press_the_notification_button, type_in_codemirror, get_codemirror_value @@ -13,6 +14,7 @@ MODAL_SELECTOR = ".validation-error-modal-content" ERROR_ITEM_NAME_SELECTOR = ".error-item-title strong" ERROR_ITEM_CONTENT_SELECTOR = ".error-item-message" + class AdvancedSettingsPage(CoursePage): """ Course Advanced Settings page. @@ -21,6 +23,10 @@ class AdvancedSettingsPage(CoursePage): url_path = "settings/advanced" def is_browser_on_page(self): + def _is_finished_loading(): + return len(self.q(css='.course-advanced-policy-list-item')) > 0 + + EmptyPromise(_is_finished_loading, 'Finished rendering the advanced policy items.').fulfill() return self.q(css='body.advanced').present def wait_for_modal_load(self): diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index 4c3f17d2f0..a94f50ba6f 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -4,6 +4,7 @@ Utility methods useful for Studio page tests. from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys from bok_choy.promise import EmptyPromise +from bok_choy.javascript import js_defined from ...tests.helpers import disable_animations @@ -50,6 +51,7 @@ def wait_for_notification(page): EmptyPromise(_is_saving_done, 'Notification should have been hidden.', timeout=60).fulfill() +@js_defined('window.jQuery') def press_the_notification_button(page, name): # Because the notification uses a CSS transition, # Selenium will always report it as being visible. @@ -101,6 +103,7 @@ def add_advanced_component(page, menu_index, name): click_css(page, component_css, 0) +@js_defined('window.jQuery') def type_in_codemirror(page, index, text, find_prefix="$"): script = """ var cm = {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror; @@ -110,6 +113,7 @@ def type_in_codemirror(page, index, text, find_prefix="$"): page.browser.execute_script(script, str(text)) +@js_defined('window.jQuery') def get_codemirror_value(page, index=0, find_prefix="$"): return page.browser.execute_script( """ diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 5919c38b97..d75f99fed7 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -83,6 +83,7 @@ sympy==0.7.1 xmltodict==0.4.1 django-ratelimit-backend==0.6 unicodecsv==0.9.4 +django-require==1.0.6 # Used for development operation watchdog==0.7.1