diff --git a/cms/envs/common.py b/cms/envs/common.py index 7335f43a4f..ee3e78e720 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -380,7 +380,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' EMBARGO_SITE_REDIRECT_URL = None ############################### Pipeline ####################################### -STATICFILES_STORAGE = 'cms.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage' +STATICFILES_STORAGE = 'django_require.staticstorage.OptimizedCachedRequireJsStorage' from rooted_paths import rooted_glob @@ -520,7 +520,7 @@ 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" +REQUIRE_BUILD_PROFILE = "build-studio.js" # The name of the require.js script used by your project, relative to REQUIRE_BASE_URL. REQUIRE_JS = "js/vendor/require.js" diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index f714172955..fb1050acc0 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -30,6 +30,11 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' LMS_BASE = "localhost:8000" FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE +########################### PIPELINE ################################# + +# Skip RequireJS optimizer in development +STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' + ############################# ADVANCED COMPONENTS ############################# # Make it easier to test advanced components in local dev diff --git a/cms/static/build.js b/cms/static/build-studio.js similarity index 99% rename from cms/static/build.js rename to cms/static/build-studio.js index d62cf09701..9d9278e035 100644 --- a/cms/static/build.js +++ b/cms/static/build-studio.js @@ -61,7 +61,7 @@ * 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', + mainConfigFile: 'require-config-studio.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 diff --git a/cms/static/require-config.js b/cms/static/require-config-studio.js similarity index 100% rename from cms/static/require-config.js rename to cms/static/require-config-studio.js diff --git a/cms/templates/base.html b/cms/templates/base.html index 176f9599a1..b7dcb25dc6 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -56,7 +56,7 @@ import json var require = {baseUrl: window.baseUrl}; - + ## js templates ') + return "\n".join(html) diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index 9deb58e7b6..4e457b2636 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -2,6 +2,7 @@ from staticfiles.storage import staticfiles_storage from pipeline_mako import compressed_css, compressed_js from django.utils.translation import get_language_bidi +from require.templatetags.require import require_module %> <%def name='url(file, raw=False)'><% @@ -37,6 +38,10 @@ except: %endif %def> +<%def name='require_module(module)'> + ${require_module(module)} +%def> + <%def name="include(path)"><% from django.template.loaders.filesystem import _loader source, template_path = _loader.load_template_source(path) diff --git a/cms/lib/django_require/__init__.py b/common/djangoapps/pipeline_mako/tests/__init__.py similarity index 100% rename from cms/lib/django_require/__init__.py rename to common/djangoapps/pipeline_mako/tests/__init__.py diff --git a/common/djangoapps/pipeline_mako/tests/test_render.py b/common/djangoapps/pipeline_mako/tests/test_render.py new file mode 100644 index 0000000000..e66b215ce8 --- /dev/null +++ b/common/djangoapps/pipeline_mako/tests/test_render.py @@ -0,0 +1,29 @@ +"""Tests for rendering functions in the mako pipeline. """ + +from django.test import TestCase +from pipeline_mako import render_require_js_path_overrides + + +class RequireJSPathOverridesTest(TestCase): + """Test RequireJS path overrides. """ + + OVERRIDES = { + 'jquery': 'js/vendor/jquery.min.js', + 'backbone': 'js/vendor/backbone-min.js', + 'text': 'js/vendor/text.js' + } + + OVERRIDES_JS = ( + "" + ) + + def test_requirejs_path_overrides(self): + result = render_require_js_path_overrides(self.OVERRIDES) + self.assertEqual(result, self.OVERRIDES_JS) diff --git a/common/lib/django_require/__init__.py b/common/lib/django_require/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/lib/django_require/staticstorage.py b/common/lib/django_require/staticstorage.py similarity index 100% rename from cms/lib/django_require/staticstorage.py rename to common/lib/django_require/staticstorage.py diff --git a/common/static/js/vendor/text.js b/common/static/js/vendor/text.js new file mode 100644 index 0000000000..17921b6e5e --- /dev/null +++ b/common/static/js/vendor/text.js @@ -0,0 +1,390 @@ +/** + * @license RequireJS text 2.0.12 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/requirejs/text for details + */ +/*jslint regexp: true */ +/*global require, XMLHttpRequest, ActiveXObject, + define, window, process, Packages, + java, location, Components, FileUtils */ + +define(['module'], function (module) { + 'use strict'; + + var text, fs, Cc, Ci, xpcIsWindows, + progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], + xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, + bodyRegExp = /
]*>\s*([\s\S]+)\s*<\/body>/im, + hasLocation = typeof location !== 'undefined' && location.href, + defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), + defaultHostName = hasLocation && location.hostname, + defaultPort = hasLocation && (location.port || undefined), + buildMap = {}, + masterConfig = (module.config && module.config()) || {}; + + text = { + version: '2.0.12', + + strip: function (content) { + //Strips declarations so that external SVG and XML + //documents can be added to a document without worry. Also, if the string + //is an HTML document, only the part inside the body tag is returned. + if (content) { + content = content.replace(xmlRegExp, ""); + var matches = content.match(bodyRegExp); + if (matches) { + content = matches[1]; + } + } else { + content = ""; + } + return content; + }, + + jsEscape: function (content) { + return content.replace(/(['\\])/g, '\\$1') + .replace(/[\f]/g, "\\f") + .replace(/[\b]/g, "\\b") + .replace(/[\n]/g, "\\n") + .replace(/[\t]/g, "\\t") + .replace(/[\r]/g, "\\r") + .replace(/[\u2028]/g, "\\u2028") + .replace(/[\u2029]/g, "\\u2029"); + }, + + createXhr: masterConfig.createXhr || function () { + //Would love to dump the ActiveX crap in here. Need IE 6 to die first. + var xhr, i, progId; + if (typeof XMLHttpRequest !== "undefined") { + return new XMLHttpRequest(); + } else if (typeof ActiveXObject !== "undefined") { + for (i = 0; i < 3; i += 1) { + progId = progIds[i]; + try { + xhr = new ActiveXObject(progId); + } catch (e) {} + + if (xhr) { + progIds = [progId]; // so faster next time + break; + } + } + } + + return xhr; + }, + + /** + * Parses a resource name into its component parts. Resource names + * look like: module/name.ext!strip, where the !strip part is + * optional. + * @param {String} name the resource name + * @returns {Object} with properties "moduleName", "ext" and "strip" + * where strip is a boolean. + */ + parseName: function (name) { + var modName, ext, temp, + strip = false, + index = name.indexOf("."), + isRelative = name.indexOf('./') === 0 || + name.indexOf('../') === 0; + + if (index !== -1 && (!isRelative || index > 1)) { + modName = name.substring(0, index); + ext = name.substring(index + 1, name.length); + } else { + modName = name; + } + + temp = ext || modName; + index = temp.indexOf("!"); + if (index !== -1) { + //Pull off the strip arg. + strip = temp.substring(index + 1) === "strip"; + temp = temp.substring(0, index); + if (ext) { + ext = temp; + } else { + modName = temp; + } + } + + return { + moduleName: modName, + ext: ext, + strip: strip + }; + }, + + xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, + + /** + * Is an URL on another domain. Only works for browser use, returns + * false in non-browser environments. Only used to know if an + * optimized .js version of a text resource should be loaded + * instead. + * @param {String} url + * @returns Boolean + */ + useXhr: function (url, protocol, hostname, port) { + var uProtocol, uHostName, uPort, + match = text.xdRegExp.exec(url); + if (!match) { + return true; + } + uProtocol = match[2]; + uHostName = match[3]; + + uHostName = uHostName.split(':'); + uPort = uHostName[1]; + uHostName = uHostName[0]; + + return (!uProtocol || uProtocol === protocol) && + (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && + ((!uPort && !uHostName) || uPort === port); + }, + + finishLoad: function (name, strip, content, onLoad) { + content = strip ? text.strip(content) : content; + if (masterConfig.isBuild) { + buildMap[name] = content; + } + onLoad(content); + }, + + load: function (name, req, onLoad, config) { + //Name has format: some.module.filext!strip + //The strip part is optional. + //if strip is present, then that means only get the string contents + //inside a body tag in an HTML string. For XML/SVG content it means + //removing the declarations so the content can be inserted + //into the current doc without problems. + + // Do not bother with the work if a build and text will + // not be inlined. + if (config && config.isBuild && !config.inlineText) { + onLoad(); + return; + } + + masterConfig.isBuild = config && config.isBuild; + + var parsed = text.parseName(name), + nonStripName = parsed.moduleName + + (parsed.ext ? '.' + parsed.ext : ''), + url = req.toUrl(nonStripName), + useXhr = (masterConfig.useXhr) || + text.useXhr; + + // Do not load if it is an empty: url + if (url.indexOf('empty:') === 0) { + onLoad(); + return; + } + + //Load the text. Use XHR if possible and in a browser. + if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { + text.get(url, function (content) { + text.finishLoad(name, parsed.strip, content, onLoad); + }, function (err) { + if (onLoad.error) { + onLoad.error(err); + } + }); + } else { + //Need to fetch the resource across domains. Assume + //the resource has been optimized into a JS module. Fetch + //by the module name + extension, but do not include the + //!strip part to avoid file system issues. + req([nonStripName], function (content) { + text.finishLoad(parsed.moduleName + '.' + parsed.ext, + parsed.strip, content, onLoad); + }); + } + }, + + write: function (pluginName, moduleName, write, config) { + if (buildMap.hasOwnProperty(moduleName)) { + var content = text.jsEscape(buildMap[moduleName]); + write.asModule(pluginName + "!" + moduleName, + "define(function () { return '" + + content + + "';});\n"); + } + }, + + writeFile: function (pluginName, moduleName, req, write, config) { + var parsed = text.parseName(moduleName), + extPart = parsed.ext ? '.' + parsed.ext : '', + nonStripName = parsed.moduleName + extPart, + //Use a '.js' file name so that it indicates it is a + //script that can be loaded across domains. + fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; + + //Leverage own load() method to load plugin value, but only + //write out values that do not have the strip argument, + //to avoid any potential issues with ! in file names. + text.load(nonStripName, req, function (value) { + //Use own write() method to construct full module value. + //But need to create shell that translates writeFile's + //write() to the right interface. + var textWrite = function (contents) { + return write(fileName, contents); + }; + textWrite.asModule = function (moduleName, contents) { + return write.asModule(moduleName, fileName, contents); + }; + + text.write(pluginName, nonStripName, textWrite, config); + }, config); + } + }; + + if (masterConfig.env === 'node' || (!masterConfig.env && + typeof process !== "undefined" && + process.versions && + !!process.versions.node && + !process.versions['node-webkit'])) { + //Using special require.nodeRequire, something added by r.js. + fs = require.nodeRequire('fs'); + + text.get = function (url, callback, errback) { + try { + var file = fs.readFileSync(url, 'utf8'); + //Remove BOM (Byte Mark Order) from utf8 files if it is there. + if (file.indexOf('\uFEFF') === 0) { + file = file.substring(1); + } + callback(file); + } catch (e) { + if (errback) { + errback(e); + } + } + }; + } else if (masterConfig.env === 'xhr' || (!masterConfig.env && + text.createXhr())) { + text.get = function (url, callback, errback, headers) { + var xhr = text.createXhr(), header; + xhr.open('GET', url, true); + + //Allow plugins direct access to xhr headers + if (headers) { + for (header in headers) { + if (headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header.toLowerCase(), headers[header]); + } + } + } + + //Allow overrides specified in config + if (masterConfig.onXhr) { + masterConfig.onXhr(xhr, url); + } + + xhr.onreadystatechange = function (evt) { + var status, err; + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status || 0; + if (status > 399 && status < 600) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + if (errback) { + errback(err); + } + } else { + callback(xhr.responseText); + } + + if (masterConfig.onXhrComplete) { + masterConfig.onXhrComplete(xhr, url); + } + } + }; + xhr.send(null); + }; + } else if (masterConfig.env === 'rhino' || (!masterConfig.env && + typeof Packages !== 'undefined' && typeof java !== 'undefined')) { + //Why Java, why is this so awkward? + text.get = function (url, callback) { + var stringBuffer, line, + encoding = "utf-8", + file = new java.io.File(url), + lineSeparator = java.lang.System.getProperty("line.separator"), + input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), + content = ''; + try { + stringBuffer = new java.lang.StringBuffer(); + line = input.readLine(); + + // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 + // http://www.unicode.org/faq/utf_bom.html + + // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 + if (line && line.length() && line.charAt(0) === 0xfeff) { + // Eat the BOM, since we've already found the encoding on this file, + // and we plan to concatenating this buffer with others; the BOM should + // only appear at the top of a file. + line = line.substring(1); + } + + if (line !== null) { + stringBuffer.append(line); + } + + while ((line = input.readLine()) !== null) { + stringBuffer.append(lineSeparator); + stringBuffer.append(line); + } + //Make sure we return a JavaScript string and not a Java string. + content = String(stringBuffer.toString()); //String + } finally { + input.close(); + } + callback(content); + }; + } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && + typeof Components !== 'undefined' && Components.classes && + Components.interfaces)) { + //Avert your gaze! + Cc = Components.classes; + Ci = Components.interfaces; + Components.utils['import']('resource://gre/modules/FileUtils.jsm'); + xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); + + text.get = function (url, callback) { + var inStream, convertStream, fileObj, + readData = {}; + + if (xpcIsWindows) { + url = url.replace(/\//g, '\\'); + } + + fileObj = new FileUtils.File(url); + + //XPCOM, you so crazy + try { + inStream = Cc['@mozilla.org/network/file-input-stream;1'] + .createInstance(Ci.nsIFileInputStream); + inStream.init(fileObj, 1, 0, false); + + convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] + .createInstance(Ci.nsIConverterInputStream); + convertStream.init(inStream, "utf-8", inStream.available(), + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + convertStream.readString(inStream.available(), readData); + convertStream.close(); + inStream.close(); + callback(readData.value); + } catch (e) { + throw new Error((fileObj && fileObj.path || '') + ': ' + e); + } + }; + } + return text; +}); diff --git a/lms/envs/common.py b/lms/envs/common.py index f609758535..ec625ca659 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1029,7 +1029,7 @@ X_FRAME_OPTIONS = 'ALLOW' ############################### Pipeline ####################################### -STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' +STATICFILES_STORAGE = 'django_require.staticstorage.OptimizedCachedRequireJsStorage' from rooted_paths import rooted_glob @@ -1365,6 +1365,56 @@ PIPELINE_UGLIFYJS_BINARY = 'node_modules/.bin/uglifyjs' # Setting that will only affect the edX version of django-pipeline until our changes are merged upstream PIPELINE_COMPILE_INPLACE = True + +################################# 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-lms.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" + +# In production, the Django pipeline appends a file hash to JavaScript file names. +# This makes it difficult for RequireJS to load its requirements, since module names +# specified in JavaScript code do not include the hash. +# For this reason, we calculate the actual path including the hash on the server +# when rendering the page. We then override the default paths provided to RequireJS +# so it can resolve the module name to the correct URL. +# +# If you want to load JavaScript dependencies using RequireJS +# but you don't want to include those dependencies in the JS bundle for the page, +# then you need to add the module and URL path to this dictionary. +REQUIRE_JS_PATH_OVERRIDES = { + 'jquery': 'js/vendor/jquery.min.js', + 'jquery.cookie': 'js/vendor/jquery.cookie.js', + 'underscore': 'js/vendor/underscore-min.js', + 'underscore.string': 'js/vendor/underscore.string.min.js', + 'backbone': 'js/vendor/backbone-min.js', + 'text': 'js/vendor/text.js' +} + + ################################# CELERY ###################################### # Message configuration diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index f9d4012ceb..54cd19a700 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -76,6 +76,9 @@ DEBUG_TOOLBAR_CONFIG = { PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) +# Skip RequireJS optimizer in development +STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' + ########################### VERIFIED CERTIFICATES ################################# FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True @@ -112,6 +115,10 @@ FEATURES['MILESTONES_APP'] = True ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True +################################# DJANGO-REQUIRE ############################### + +# Whether to run django-require in debug mode. +REQUIRE_DEBUG = DEBUG ##################################################################### # See if the developer has any local overrides. diff --git a/lms/static/build-lms.js b/lms/static/build-lms.js new file mode 100644 index 0000000000..a0adb2b5a7 --- /dev/null +++ b/lms/static/build-lms.js @@ -0,0 +1,112 @@ +(function () { + 'use strict'; + + 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: [], + + /** + * 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-lms.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:' + }, + + /** + * 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: {}, + + /** + * 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: 0 + }; +} ()) diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index 5dfa944db2..673687d503 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -58,6 +58,7 @@ lib_paths: - xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min.js - xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/vendor/date.js + - xmodule_js/common_static/js/vendor/text.js # Paths to source JavaScript files src_paths: diff --git a/lms/static/require-config-lms.js b/lms/static/require-config-lms.js index f515351e19..14e8aabbf9 100644 --- a/lms/static/require-config-lms.js +++ b/lms/static/require-config-lms.js @@ -39,7 +39,6 @@ } else { paths.tinymce = "js/vendor/tinymce/js/tinymce/jquery.tinymce.min"; } - config = { // NOTE: baseUrl has been previously set in lms/static/templates/main.html waitSeconds: 60, @@ -47,7 +46,18 @@ "annotator_1.2.9": "js/vendor/edxnotes/annotator-full.min", "date": "js/vendor/date", "backbone": "js/vendor/backbone-min", + "gettext": "/i18n", + "jquery": "js/vendor/jquery.min", + "jquery.cookie": "js/vendor/jquery.cookie", + "jquery.url": "js/vendor/url.min", + "text": "js/vendor/text", + "underscore": "js/vendor/underscore-min", "underscore.string": "js/vendor/underscore.string.min", + + // This module defines some global functions. + // TODO: replace these with RequireJS-compatible modules + "utility": "js/src/utility", + // Files needed by OVA "annotator": "js/vendor/ova/annotator-full", "annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth", @@ -80,6 +90,14 @@ "jquery": { exports: "$" }, + "jquery.cookie": { + deps: ["jquery"], + exports: "jQuery.fn.cookie" + }, + "jquery.url": { + deps: ["jquery"], + exports: "jQuery.url" + }, "underscore": { exports: "_" }, @@ -87,6 +105,9 @@ deps: ["underscore", "jquery"], exports: "Backbone" }, + "gettext": { + exports: "gettext" + }, "logger": { exports: "Logger" }, diff --git a/lms/templates/main.html b/lms/templates/main.html index d7b4f3ef83..dafd5c77dc 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -14,9 +14,12 @@ % if responsive: % endif -<%! from django.utils.translation import ugettext as _ %> -<%! from microsite_configuration import microsite %> -<%! from microsite_configuration import page_title_breadcrumbs %> +<%! +from django.utils.translation import ugettext as _ +from pipeline_mako import render_require_js_path_overrides +from microsite_configuration import microsite +from microsite_configuration import page_title_breadcrumbs +%> <%namespace name='static' file='static_content.html'/> <%! from django.utils.http import urlquote_plus %> @@ -58,7 +61,13 @@ })(this); - + ## RequireJS-enabled pages should include the "i18n" module as a requirement. + ## If RequireJS is not enabled, then we will need to load i18n explicitly + ## so that JavaScript on the page can use internationalization functions. + % if not enable_require_js: + + % endif + @@ -67,28 +76,16 @@ <%static:css group='style-app-extend1'/> <%static:css group='style-app-extend2'/> - % if disable_courseware_js: - <%static:js group='base_vendor'/> + % if enable_require_js: + + ${render_require_js_path_overrides(settings.REQUIRE_JS_PATH_OVERRIDES)} % else: - <%static:js group='main_vendor'/> - % endif - - - + % endif <%block name="headextra"/> @@ -149,7 +146,7 @@ <%block name="footer"> - ## Can be overridden by child templates wanting to hide the footer. + ## Can be overridden by child templates wanting to hide the footer. <% if theme_enabled() and not is_microsite(): footer_file = 'theme-footer.html' @@ -163,7 +160,8 @@ - % if not disable_courseware_js: + + % if not enable_require_js and not disable_courseware_js: <%static:js group='application'/> <%static:js group='module-js'/> % endif diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html index 2714920b77..5164a26cff 100644 --- a/lms/templates/navigation-edx.html +++ b/lms/templates/navigation-edx.html @@ -142,7 +142,9 @@ site_status_msg = get_site_status_msg(course_id) % endif -%if not user.is_authenticated(): +## The forgot password modal JavaScript assumes that JQuery is loaded, +## which is not necessarily the case when using RequireJS. +%if not enable_require_js and not user.is_authenticated(): <%include file="forgot_password_modal.html" /> %endif diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 945291ae2e..9af52223ba 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -141,7 +141,9 @@ site_status_msg = get_site_status_msg(course_id) % endif -%if not user.is_authenticated(): +## The forgot password modal JavaScript assumes that JQuery is loaded, +## which is not necessarily the case when using RequireJS. +%if not enable_require_js and not user.is_authenticated(): <%include file="forgot_password_modal.html" /> %endif diff --git a/lms/urls.py b/lms/urls.py index 3bf18ff8d3..b8de5f3124 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -104,7 +104,7 @@ js_info_dict = { urlpatterns += ( # Serve catalog of localized strings to be rendered by Javascript - url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), + url(r'^i18n.js$', 'django.views.i18n.javascript_catalog', js_info_dict), ) # sysadmin dashboard, to see what courses are loaded, to delete & load courses diff --git a/pavelib/assets.py b/pavelib/assets.py index 054c70adcb..a2fe99b780 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -165,7 +165,13 @@ def collect_assets(systems, settings): `settings` is the Django settings module to use. """ for sys in systems: - sh(django_cmd(sys, settings, "collectstatic --noinput > /dev/null")) + options = "--noinput" + # Force clear the static assets directory on LMS. This shouldn't be necessary, but there + # were repeatable scenarios where RequireJS optimized files were not installed once they + # were generated if an older version was found. + if sys == 'lms': + options += " --clear" + sh(django_cmd(sys, settings, "collectstatic {options} > /dev/null".format(options=options))) @task diff --git a/test_root/staticfiles/.gitkeep b/test_root/staticfiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2