diff --git a/.coveragerc b/.coveragerc index cb75a6a94e..de7da357c1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -26,6 +26,8 @@ omit = openedx/core/djangoapps/*/migrations/* openedx/core/djangoapps/debug/* +concurrency=multiprocessing + [report] ignore_errors = True diff --git a/.jshintignore b/.jshintignore index 26e4c15e1e..0065f2da20 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,4 +1,6 @@ **/vendor +cms/static/cms/js/build.js +cms/static/cms/js/spec/main.js cms/static/js/i18n/**/*.js lms/static/js/i18n/**/*.js lms/static/lms/js/build.js diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature deleted file mode 100644 index cd34107f39..0000000000 --- a/cms/djangoapps/contentstore/features/component.feature +++ /dev/null @@ -1,61 +0,0 @@ -@shard_1 -Feature: CMS.Component Adding - As a course author, I want to be able to add a wide variety of components - - Scenario: I can add HTML components - Given I am in Studio editing a new unit - When I add this type of HTML component: - | Component | - | Text | - | Announcement | - | Zooming Image Tool | - | Raw HTML | - Then I see HTML components in this order: - | Component | - | Text | - | Announcement | - | Zooming Image Tool | - | Raw HTML | - - Scenario: I can add Latex HTML components - Given I am in Studio editing a new unit - Given I have enabled latex compiler - When I add this type of HTML component: - | Component | - | E-text Written in LaTeX | - Then I see HTML components in this order: - | Component | - | E-text Written in LaTeX | - - Scenario: I can add Common Problem components - Given I am in Studio editing a new unit - When I add this type of Problem component: - | Component | - | Blank Common Problem | - | Checkboxes | - | Dropdown | - | Multiple Choice | - | Numerical Input | - | Text Input | - Then I see Problem components in this order: - | Component | - | Blank Common Problem | - | Checkboxes | - | Dropdown | - | Multiple Choice | - | Numerical Input | - | Text Input | - -# Disabled 1/21/14 due to flakiness seen in master -# Scenario: I can add Advanced Latex Problem components -# Given I am in Studio editing a new unit -# Given I have enabled latex compiler -# When I add a "" "Advanced Problem" component -# Then I see a "" Problem component -# # Flush out the database before the next example executes -# And I reset the database - -# Examples: -# | Component | -# | Problem Written in LaTeX | -# | Problem with Adaptive Hint in Latex | diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 2bb272bcfe..0f87ced3f1 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1000,7 +1000,7 @@ class MiscCourseTests(ContentStoreTestCase): 3) computing thumbnail location of asset 4) deleting the asset from the course """ - asset_key = self.course.id.make_asset_key('asset', 'sample_static.txt') + asset_key = self.course.id.make_asset_key('asset', 'sample_static.html') content = StaticContent( asset_key, "Fake asset", "application/text", "test", ) @@ -1072,7 +1072,7 @@ class MiscCourseTests(ContentStoreTestCase): draft content is also deleted """ # add an asset - asset_key = self.course.id.make_asset_key('asset', 'sample_static.txt') + asset_key = self.course.id.make_asset_key('asset', 'sample_static.html') content = StaticContent( asset_key, "Fake asset", "application/text", "test", ) diff --git a/cms/djangoapps/contentstore/tests/test_orphan.py b/cms/djangoapps/contentstore/tests/test_orphan.py index 519276cbc2..5d089bdd8f 100644 --- a/cms/djangoapps/contentstore/tests/test_orphan.py +++ b/cms/djangoapps/contentstore/tests/test_orphan.py @@ -101,7 +101,7 @@ class TestOrphan(TestOrphanBase): @ddt.data( (ModuleStoreEnum.Type.split, 9, 6), - (ModuleStoreEnum.Type.mongo, 30, 13), + (ModuleStoreEnum.Type.mongo, 34, 13), ) @ddt.unpack def test_delete_orphans(self, default_store, max_mongo_calls, min_mongo_calls): diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 2026295c9e..af5e7b8e52 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -121,7 +121,7 @@ class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase): SEQUENTIAL = 'vertical_sequential' DRAFT_HTML = 'draft_html' DRAFT_VIDEO = 'draft_video' - LOCKED_ASSET_KEY = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.txt') + LOCKED_ASSET_KEY = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.html') def import_and_populate_course(self): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_assets.py b/cms/djangoapps/contentstore/views/tests/test_assets.py index 78ce5b4050..797b76be59 100644 --- a/cms/djangoapps/contentstore/views/tests/test_assets.py +++ b/cms/djangoapps/contentstore/views/tests/test_assets.py @@ -126,7 +126,7 @@ class BasicAssetsTestCase(AssetsTestCase): ) course = module_store.get_course(course_id) - filename = 'sample_static.txt' + filename = 'sample_static.html' html_src_attribute = '"/static/{}"'.format(filename) asset_url = replace_static_urls(html_src_attribute, course_id=course.id) url = asset_url.replace('"', '') @@ -379,7 +379,7 @@ class LockAssetTestCase(AssetsTestCase): """ def verify_asset_locked_state(locked): """ Helper method to verify lock state in the contentstore """ - asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt') + asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.html') content = contentstore().find(asset_location) self.assertEqual(content.locked, locked) @@ -387,14 +387,14 @@ class LockAssetTestCase(AssetsTestCase): """ Helper method for posting asset update. """ content_type = 'application/txt' upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) - asset_location = course.id.make_asset_key('asset', 'sample_static.txt') + asset_location = course.id.make_asset_key('asset', 'sample_static.html') url = reverse_course_url('assets_handler', course.id, kwargs={'asset_key_string': unicode(asset_location)}) resp = self.client.post( url, # pylint: disable=protected-access json.dumps(assets._get_asset_json( - "sample_static.txt", content_type, upload_date, asset_location, None, lock)), + "sample_static.html", content_type, upload_date, asset_location, None, lock)), "application/json" ) diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index 0c047f8ba1..ec34567f9f 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -44,6 +44,9 @@ class StatusDisplayStrings(object): _COMPLETE = ugettext_noop("Ready") # Translators: This is the status for a video that the servers have failed to process _FAILED = ugettext_noop("Failed") + # Translators: This is the status for a video which has failed + # due to being flagged as a duplicate by an external or internal CMS + _DUPLICATE = ugettext_noop("Failed Duplicate") # Translators: This is the status for a video for which an invalid # processing token was provided in the course settings _INVALID_TOKEN = ugettext_noop("Invalid Token") @@ -61,6 +64,7 @@ class StatusDisplayStrings(object): "file_complete": _COMPLETE, "file_corrupt": _FAILED, "pipeline_error": _FAILED, + "duplicate": _DUPLICATE, "invalid_token": _INVALID_TOKEN, "imported": _IMPORTED, } @@ -311,9 +315,9 @@ def videos_post(course, request): edx_video_id = unicode(uuid4()) key = storage_service_key(bucket, file_name=edx_video_id) for metadata_name, value in [ - ("course_video_upload_token", course_video_upload_token), - ("client_video_id", file_name), - ("course_key", unicode(course.id)), + ("course_video_upload_token", course_video_upload_token), + ("client_video_id", file_name), + ("course_key", unicode(course.id)), ]: key.set_metadata(metadata_name, value) upload_url = key.generate_url( diff --git a/cms/envs/common.py b/cms/envs/common.py index a3204be72f..0d210366d3 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -620,7 +620,7 @@ PIPELINE_JS = { 'source_filenames': ( rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js') + rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js') + - rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/discussion/*.js') + rooted_glob(COMMON_ROOT / 'static/', 'common/js/discussion/*.js') ), 'output_filename': 'js/cms-modules.js', 'test_order': 1 diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js index 65e78c3e62..f6a3c1534c 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -1,8 +1,8 @@ -(function () { +(function() { 'use strict'; var commonLibrariesPath = 'common/js/common_libraries'; - var getModule = function (moduleName, excludeCommonDeps) { + var getModule = function(moduleName, excludeCommonDeps) { var module = { name: moduleName }; @@ -14,7 +14,7 @@ return module; }; - var getModulesList = function (modules) { + var getModulesList = function(modules) { var result = [getModule(commonLibrariesPath)]; return result.concat(modules.map(function (moduleName) { return getModule(moduleName, true); @@ -92,7 +92,7 @@ /** * Stub out requireJS text in the optimized file, but leave available for non-optimized development use. */ - stubModules: ["text"], + stubModules: ['text'], /** * If shim config is used in the app during runtime, duplicate the config @@ -170,4 +170,4 @@ */ logLevel: 1 }; -} ()) +}()) diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index 70e8d61e0b..7cd1f1caf1 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -1,5 +1,6 @@ -;(function (require, define) { +;(function(require, define) { 'use strict'; + if (window) { // MathJax Fast Preview was introduced in 2.5. However, it // causes undesirable flashing/font size changes when @@ -16,300 +17,300 @@ // needs to be served. To handle this, we load the correct file in the // rendered template and then use this to ensure that RequireJS knows // how to find it. - define("gettext", function () { return window.gettext; }); + define('gettext', function() { return window.gettext; }); } require.config({ // NOTE: baseUrl has been previously set in cms/static/templates/base.html waitSeconds: 60, paths: { - "domReady": "js/vendor/domReady", - "mustache": "js/vendor/mustache", - "codemirror": "js/vendor/codemirror-compressed", - "codemirror/stex": "js/vendor/CodeMirror/stex", - "jquery": "common/js/vendor/jquery", - "jquery-migrate": "common/js/vendor/jquery-migrate", - "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", - "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": "common/js/vendor/jquery.scrollTo", - "jquery.flot": "js/vendor/flot/jquery.flot.min", - "jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload", - "jquery.fileupload-process": "js/vendor/jQuery-File-Upload/js/jquery.fileupload-process", - "jquery.fileupload-validate": "js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate", - "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", - "moment": "js/vendor/moment.min", - "moment-with-locales": "js/vendor/moment-with-locales.min", - "text": 'js/vendor/requirejs/text', - "underscore": "common/js/vendor/underscore", - "underscore.string": "common/js/vendor/underscore.string", - "backbone": "common/js/vendor/backbone", - "backbone-relational" : "js/vendor/backbone-relational.min", - "backbone.associations": "js/vendor/backbone-associations-min", - "backbone.paginator": "common/js/vendor/backbone.paginator", - "tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min", - "jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min", - "xmodule": "/xmodule/xmodule", - "xblock/core": "js/xblock/core", - "xblock": "coffee/src/xblock", - "utility": "js/src/utility", - "accessibility": "js/src/accessibility_tools", - "URI": "js/vendor/URI.min", - "ieshim": "js/src/ie_shim", - "tooltip_manager": "js/src/tooltip_manager", - "modernizr": "edx-pattern-library/js/modernizr-custom", - "afontgarde": "edx-pattern-library/js/afontgarde", - "edxicons": "edx-pattern-library/js/edx-icons", - "draggabilly": "js/vendor/draggabilly", + 'domReady': 'js/vendor/domReady', + 'mustache': 'js/vendor/mustache', + 'codemirror': 'js/vendor/codemirror-compressed', + 'codemirror/stex': 'js/vendor/CodeMirror/stex', + 'jquery': 'common/js/vendor/jquery', + 'jquery-migrate': 'common/js/vendor/jquery-migrate', + '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', + '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': 'common/js/vendor/jquery.scrollTo', + 'jquery.flot': 'js/vendor/flot/jquery.flot.min', + 'jquery.fileupload': 'js/vendor/jQuery-File-Upload/js/jquery.fileupload', + 'jquery.fileupload-process': 'js/vendor/jQuery-File-Upload/js/jquery.fileupload-process', + 'jquery.fileupload-validate': 'js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate', + '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', + 'moment': 'js/vendor/moment.min', + 'moment-with-locales': 'js/vendor/moment-with-locales.min', + 'text': 'js/vendor/requirejs/text', + 'underscore': 'common/js/vendor/underscore', + 'underscore.string': 'common/js/vendor/underscore.string', + 'backbone': 'common/js/vendor/backbone', + 'backbone-relational': 'js/vendor/backbone-relational.min', + 'backbone.associations': 'js/vendor/backbone-associations-min', + 'backbone.paginator': 'common/js/vendor/backbone.paginator', + 'tinymce': 'js/vendor/tinymce/js/tinymce/tinymce.full.min', + 'jquery.tinymce': 'js/vendor/tinymce/js/tinymce/jquery.tinymce.min', + 'xmodule': '/xmodule/xmodule', + 'xblock/cms.runtime.v1': 'cms/js/xblock/cms.runtime.v1', + 'xblock': 'common/js/xblock', + 'utility': 'js/src/utility', + 'accessibility': 'js/src/accessibility_tools', + 'URI': 'js/vendor/URI.min', + 'ieshim': 'js/src/ie_shim', + 'tooltip_manager': 'js/src/tooltip_manager', + 'modernizr': 'edx-pattern-library/js/modernizr-custom', + 'afontgarde': 'edx-pattern-library/js/afontgarde', + 'edxicons': 'edx-pattern-library/js/edx-icons', + 'draggabilly': 'js/vendor/draggabilly', // 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', - "lang_edx": "js/src/lang_edx", + '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', + 'lang_edx': 'js/src/lang_edx', // end of Annotation tool files // externally hosted files - "mathjax": "//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured", // jshint ignore:line - "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, + 'mathjax': '//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // jshint ignore:line + '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", + '//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" + 'js/src/youtube_fallback' ] }, shim: { - "gettext": { - exports: "gettext" + 'gettext': { + exports: 'gettext' }, - "date": { - exports: "Date" + 'date': { + exports: 'Date' }, - "jquery-migrate": ['jquery'], - "jquery.ui": { - deps: ["jquery"], - exports: "jQuery.ui" + 'jquery-migrate': ['jquery'], + 'jquery.ui': { + deps: ['jquery'], + exports: 'jQuery.ui' }, - "jquery.form": { - deps: ["jquery"], - exports: "jQuery.fn.ajaxForm" + 'jquery.form': { + deps: ['jquery'], + exports: 'jQuery.fn.ajaxForm' }, - "jquery.markitup": { - deps: ["jquery"], - exports: "jQuery.fn.markitup" + 'jquery.markitup': { + deps: ['jquery'], + exports: 'jQuery.fn.markitup' }, - "jquery.leanmodal": { - deps: ["jquery"], - exports: "jQuery.fn.leanModal" + 'jquery.leanmodal': { + deps: ['jquery'], + exports: 'jQuery.fn.leanModal' }, - "jquery.ajaxQueue": { - deps: ["jquery"], - exports: "jQuery.fn.ajaxQueue" + 'jquery.ajaxQueue': { + deps: ['jquery'], + exports: 'jQuery.fn.ajaxQueue' }, - "jquery.smoothScroll": { - deps: ["jquery"], - exports: "jQuery.fn.smoothScroll" + 'jquery.smoothScroll': { + deps: ['jquery'], + exports: 'jQuery.fn.smoothScroll' }, - "jquery.cookie": { - deps: ["jquery"], - exports: "jQuery.fn.cookie" + 'jquery.cookie': { + deps: ['jquery'], + exports: 'jQuery.fn.cookie' }, - "jquery.qtip": { - deps: ["jquery"], - exports: "jQuery.fn.qtip" + 'jquery.qtip': { + deps: ['jquery'], + exports: 'jQuery.fn.qtip' }, - "jquery.scrollTo": { - deps: ["jquery"], - exports: "jQuery.fn.scrollTo" + 'jquery.scrollTo': { + deps: ['jquery'], + exports: 'jQuery.fn.scrollTo' }, - "jquery.flot": { - deps: ["jquery"], - exports: "jQuery.fn.plot" + 'jquery.flot': { + deps: ['jquery'], + exports: 'jQuery.fn.plot' }, - "jquery.fileupload": { - deps: ["jquery.ui", "jquery.iframe-transport"], - exports: "jQuery.fn.fileupload" + 'jquery.fileupload': { + deps: ['jquery.ui', 'jquery.iframe-transport'], + exports: 'jQuery.fn.fileupload' }, - "jquery.fileupload-process": { - deps: ["jquery.fileupload"] + 'jquery.fileupload-process': { + deps: ['jquery.fileupload'] }, - "jquery.fileupload-validate": { - deps: ["jquery.fileupload"] + 'jquery.fileupload-validate': { + deps: ['jquery.fileupload'] }, - "jquery.inputnumber": { - deps: ["jquery"], - exports: "jQuery.fn.inputNumber" + 'jquery.inputnumber': { + deps: ['jquery'], + exports: 'jQuery.fn.inputNumber' }, - "jquery.tinymce": { - deps: ["jquery", "tinymce"], - exports: "jQuery.fn.tinymce" + 'jquery.tinymce': { + deps: ['jquery', 'tinymce'], + exports: 'jQuery.fn.tinymce' }, - "datepair": { - deps: ["jquery.ui", "jquery.timepicker"] + 'datepair': { + deps: ['jquery.ui', 'jquery.timepicker'] }, - "underscore": { - exports: "_" + 'underscore': { + exports: '_' }, - "backbone": { - deps: ["underscore", "jquery"], - exports: "Backbone" + 'backbone': { + deps: ['underscore', 'jquery'], + exports: 'Backbone' }, - "backbone.associations": { - deps: ["backbone"], - exports: "Backbone.Associations" + 'backbone.associations': { + deps: ['backbone'], + exports: 'Backbone.Associations' }, - "backbone.paginator": { - deps: ["backbone"], - exports: "Backbone.PageableCollection" + 'backbone.paginator': { + deps: ['backbone'], + exports: 'Backbone.PageableCollection' }, - "youtube": { - exports: "YT" + 'youtube': { + exports: 'YT' }, - "codemirror": { - exports: "CodeMirror" + 'codemirror': { + exports: 'CodeMirror' }, - "codemirror/stex": { - deps: ["codemirror"] + 'codemirror/stex': { + deps: ['codemirror'] }, - "tinymce": { - exports: "tinymce" + 'tinymce': { + exports: 'tinymce' }, - "lang_edx": { - deps: ["jquery"] + 'lang_edx': { + deps: ['jquery'] }, - "mathjax": { - exports: "MathJax", + 'mathjax': { + exports: 'MathJax', init: function() { window.MathJax.Hub.Config({ tex2jax: { inlineMath: [ - ["\\(","\\)"], + ['\\(','\\)'], ['[mathjaxinline]','[/mathjaxinline]'] ], displayMath: [ - ["\\[","\\]"], + ['\\[','\\]'], ['[mathjax]','[/mathjax]'] ] } - }); - // In order to eliminate all flashing during interactive - // preview, it is necessary to set processSectionDelay to 0 - // (remove delay between input and output phases). This - // effectively disables fast preview, regardless of - // the fast preview setting as shown in the context menu. - window.MathJax.Hub.processSectionDelay = 0; - window.MathJax.Hub.Configured(); + }); + // In order to eliminate all flashing during interactive + // preview, it is necessary to set processSectionDelay to 0 + // (remove delay between input and output phases). This + // effectively disables fast preview, regardless of + // the fast preview setting as shown in the context menu. + window.MathJax.Hub.processSectionDelay = 0; + window.MathJax.Hub.Configured(); } }, - "URI": { - exports: "URI" + 'URI': { + exports: 'URI' }, - "tooltip_manager": { - deps: ["jquery", "underscore"] + 'tooltip_manager': { + deps: ['jquery', 'underscore'] }, - "jquery.immediateDescendents": { - deps: ["jquery"] + 'jquery.immediateDescendents': { + deps: ['jquery'] }, - "xblock/core": { - exports: "XBlock", - deps: ["jquery", "jquery.immediateDescendents"] + 'xblock/core': { + exports: 'XBlock', + deps: ['jquery', 'jquery.immediateDescendents'] }, - "xblock/runtime.v1": { - exports: "XBlock", - deps: ["xblock/core"] + 'xblock/runtime.v1': { + exports: 'XBlock', + deps: ['xblock/core'] }, - "coffee/src/main": { - deps: ["coffee/src/ajax_prefix"] + 'coffee/src/main': { + deps: ['coffee/src/ajax_prefix'] }, - "js/src/logger": { - exports: "Logger", - deps: ["coffee/src/ajax_prefix"] + 'js/src/logger': { + exports: 'Logger', + deps: ['coffee/src/ajax_prefix'] }, - "modernizr": { - exports: "Modernizr" + 'modernizr': { + exports: 'Modernizr' }, - "afontgarde": { - exports: "AFontGarde" + 'afontgarde': { + exports: 'AFontGarde' }, // the following are all needed for annotation tools - "video.dev": { - exports:"videojs" + 'video.dev': { + exports: 'videojs' }, - "vjs.youtube": { - deps: ["video.dev"] + 'vjs.youtube': { + deps: ['video.dev'] }, - "rangeslider": { - deps: ["video.dev"] + 'rangeslider': { + deps: ['video.dev'] }, - "annotator": { - exports: "Annotator" + 'annotator': { + exports: 'Annotator' }, - "annotator-harvardx":{ - deps: ["annotator"] + 'annotator-harvardx': { + deps: ['annotator'] }, - "share-annotator": { - deps: ["annotator"] + 'share-annotator': { + deps: ['annotator'] }, - "richText-annotator": { - deps: ["annotator", "tinymce"] + 'richText-annotator': { + deps: ['annotator', 'tinymce'] }, - "reply-annotator": { - deps: ["annotator"] + 'reply-annotator': { + deps: ['annotator'] }, - "tags-annotator": { - deps: ["annotator"] + 'tags-annotator': { + deps: ['annotator'] }, - "diacritic-annotator": { - deps: ["annotator"] + 'diacritic-annotator': { + deps: ['annotator'] }, - "flagging-annotator": { - deps: ["annotator"] + 'flagging-annotator': { + deps: ['annotator'] }, - "grouping-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"] + '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"] + '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/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js new file mode 100644 index 0000000000..db82630e76 --- /dev/null +++ b/cms/static/cms/js/spec/main.js @@ -0,0 +1,299 @@ +(function(requirejs, requireSerial) { + 'use strict'; + + var i, specHelpers, testFiles; + + requirejs.config({ + baseUrl: '/base/', + paths: { + 'gettext': 'xmodule_js/common_static/js/test/i18n', + 'mustache': 'xmodule_js/common_static/js/vendor/mustache', + 'codemirror': 'xmodule_js/common_static/js/vendor/CodeMirror/codemirror', + 'jquery': 'xmodule_js/common_static/common/js/vendor/jquery', + 'jquery-migrate': 'xmodule_js/common_static/common/js/vendor/jquery-migrate', + 'jquery.ui': 'xmodule_js/common_static/js/vendor/jquery-ui.min', + 'jquery.form': 'xmodule_js/common_static/js/vendor/jquery.form', + 'jquery.markitup': 'xmodule_js/common_static/js/vendor/markitup/jquery.markitup', + 'jquery.leanModal': 'xmodule_js/common_static/js/vendor/jquery.leanModal', + 'jquery.ajaxQueue': 'xmodule_js/common_static/js/vendor/jquery.ajaxQueue', + 'jquery.smoothScroll': 'xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min', + 'jquery.scrollTo': 'common/js/vendor/jquery.scrollTo', + 'jquery.timepicker': 'xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker', + 'jquery.cookie': 'xmodule_js/common_static/js/vendor/jquery.cookie', + 'jquery.qtip': 'xmodule_js/common_static/js/vendor/jquery.qtip.min', + 'jquery.fileupload': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload', + 'jquery.fileupload-process': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process', // jshint ignore:line + 'jquery.fileupload-validate': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate', // jshint ignore:line + 'jquery.iframe-transport': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport', // jshint ignore:line + 'jquery.inputnumber': 'xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill', + 'jquery.immediateDescendents': 'xmodule_js/common_static/coffee/src/jquery.immediateDescendents', + 'jquery.simulate': 'xmodule_js/common_static/js/vendor/jquery.simulate', + 'datepair': 'xmodule_js/common_static/js/vendor/timepicker/datepair', + 'date': 'xmodule_js/common_static/js/vendor/date', + 'moment': 'xmodule_js/common_static/js/vendor/moment.min', + 'moment-with-locales': 'xmodule_js/common_static/js/vendor/moment-with-locales.min', + 'text': 'xmodule_js/common_static/js/vendor/requirejs/text', + 'underscore': 'common/js/vendor/underscore', + 'underscore.string': 'common/js/vendor/underscore.string', + 'backbone': 'common/js/vendor/backbone', + 'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min', + 'backbone.paginator': 'common/js/vendor/backbone.paginator', + 'backbone-relational': 'xmodule_js/common_static/js/vendor/backbone-relational.min', + 'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min', + 'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce', + 'xmodule': 'xmodule_js/src/xmodule', + 'xblock/cms.runtime.v1': 'cms/js/xblock/cms.runtime.v1', + 'xblock': 'common/js/xblock', + 'utility': 'xmodule_js/common_static/js/src/utility', + 'accessibility': 'xmodule_js/common_static/js/src/accessibility_tools', + 'sinon': 'xmodule_js/common_static/js/vendor/sinon-1.17.0', + 'squire': 'xmodule_js/common_static/js/vendor/Squire', + 'jasmine-imagediff': 'xmodule_js/common_static/js/vendor/jasmine-imagediff', + 'draggabilly': 'xmodule_js/common_static/js/vendor/draggabilly', + 'domReady': 'xmodule_js/common_static/js/vendor/domReady', + 'URI': 'xmodule_js/common_static/js/vendor/URI.min', + 'mock-ajax': 'xmodule_js/common_static/js/vendor/mock-ajax', + 'modernizr': 'edx-pattern-library/js/modernizr-custom', + 'afontgarde': 'edx-pattern-library/js/afontgarde', + 'edxicons': 'edx-pattern-library/js/edx-icons', + 'mathjax': '//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // jshint ignore:line + 'youtube': '//www.youtube.com/player_api?noext', + 'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix', + 'js/spec/test_utils': 'js/spec/test_utils' + }, + shim: { + 'gettext': { + exports: 'gettext' + }, + 'date': { + exports: 'Date' + }, + 'jquery-migrate': ['jquery'], + '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.smoothScroll': { + deps: ['jquery'], + exports: 'jQuery.fn.smoothScroll' + }, + 'jquery.ajaxQueue': { + deps: ['jquery'], + exports: 'jQuery.fn.ajaxQueue' + }, + 'jquery.scrollTo': { + deps: ['jquery'], + exports: 'jQuery.fn.scrollTo' + }, + 'jquery.cookie': { + deps: ['jquery'], + exports: 'jQuery.fn.cookie' + }, + 'jquery.qtip': { + deps: ['jquery'], + exports: 'jQuery.fn.qtip' + }, + 'jquery.fileupload': { + deps: ['jquery.ui', 'jquery.iframe-transport'], + exports: 'jQuery.fn.fileupload' + }, + 'jquery.fileupload-process': { + deps: ['jquery.fileupload'] + }, + 'jquery.fileupload-validate': { + deps: ['jquery.fileupload'] + }, + 'jquery.inputnumber': { + deps: ['jquery'], + exports: 'jQuery.fn.inputNumber' + }, + 'jquery.simulate': { + deps: ['jquery'], + exports: 'jQuery.fn.simulate' + }, + '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.PageableCollection' + }, + 'backbone-relational': { + deps: ['backbone'] + }, + 'youtube': { + exports: 'YT' + }, + 'codemirror': { + exports: 'CodeMirror' + }, + 'tinymce': { + exports: 'tinymce' + }, + 'mathjax': { + exports: 'MathJax', + init: function() { + window.MathJax.Hub.Config({ + tex2jax: { + inlineMath: [['\\(', '\\)'], ['[mathjaxinline]', '[/mathjaxinline]']], + displayMath: [['\\[', '\\]'], ['[mathjax]', '[/mathjax]']] + } + }); + return window.MathJax.Hub.Configured(); + } + }, + 'URI': { + exports: 'URI' + }, + 'xmodule': { + exports: 'XModule' + }, + 'sinon': { + exports: 'sinon' + }, + 'jasmine-imagediff': {}, + 'common/js/spec_helpers/jasmine-extensions': { + deps: ['jquery'] + }, + 'common/js/spec_helpers/jasmine-stealth': { + deps: ['underscore', 'underscore.string'] + }, + 'common/js/spec_helpers/jasmine-waituntil': { + deps: ['jquery'] + }, + 'xblock/core': { + exports: 'XBlock', + deps: ['jquery', 'jquery.immediateDescendents'] + }, + 'xblock/runtime.v1': { + exports: 'XBlock', + deps: ['xblock/core'] + }, + 'mock-ajax': { + deps: ['jquery'] + }, + 'coffee/src/main': { + deps: ['coffee/src/ajax_prefix'] + }, + 'coffee/src/ajax_prefix': { + deps: ['jquery'] + }, + 'modernizr': { + exports: 'Modernizr' + }, + 'afontgarde': { + exports: 'AFontGarde' + } + } + }); + + jasmine.getFixtures().fixturesPath += 'coffee/fixtures'; + + testFiles = [ + 'cms/js/spec/xblock/cms.runtime.v1_spec', + 'coffee/spec/main_spec', + 'coffee/spec/models/course_spec', + 'coffee/spec/models/metadata_spec', + 'coffee/spec/models/section_spec', + 'coffee/spec/models/settings_course_grader_spec', + 'coffee/spec/models/settings_grading_spec', + 'coffee/spec/models/textbook_spec', + 'coffee/spec/models/upload_spec', + 'coffee/spec/views/course_info_spec', + 'coffee/spec/views/metadata_edit_spec', + 'coffee/spec/views/textbook_spec', + 'coffee/spec/views/upload_spec', + 'js/spec/video/transcripts/utils_spec', + 'js/spec/video/transcripts/editor_spec', + 'js/spec/video/transcripts/videolist_spec', + 'js/spec/video/transcripts/message_manager_spec', + 'js/spec/video/transcripts/file_uploader_spec', + 'js/spec/models/component_template_spec', + 'js/spec/models/explicit_url_spec', + 'js/spec/models/xblock_info_spec', + 'js/spec/models/xblock_validation_spec', + 'js/spec/models/license_spec', + 'js/spec/utils/drag_and_drop_spec', + 'js/spec/utils/handle_iframe_binding_spec', + 'js/spec/utils/module_spec', + 'js/spec/views/active_video_upload_list_spec', + 'js/spec/views/previous_video_upload_spec', + 'js/spec/views/previous_video_upload_list_spec', + 'js/spec/views/assets_spec', + 'js/spec/views/baseview_spec', + 'js/spec/views/container_spec', + 'js/spec/views/module_edit_spec', + 'js/spec/views/paged_container_spec', + 'js/spec/views/group_configuration_spec', + 'js/spec/views/unit_outline_spec', + 'js/spec/views/xblock_spec', + 'js/spec/views/xblock_editor_spec', + 'js/spec/views/xblock_string_field_editor_spec', + 'js/spec/views/xblock_validation_spec', + 'js/spec/views/license_spec', + 'js/spec/views/paging_spec', + 'js/spec/views/login_studio_spec', + 'js/spec/views/pages/container_spec', + 'js/spec/views/pages/container_subviews_spec', + 'js/spec/views/pages/group_configurations_spec', + 'js/spec/views/pages/course_outline_spec', + 'js/spec/views/pages/course_rerun_spec', + 'js/spec/views/pages/index_spec', + 'js/spec/views/pages/library_users_spec', + 'js/spec/views/modals/base_modal_spec', + 'js/spec/views/modals/edit_xblock_spec', + 'js/spec/views/modals/validation_error_modal_spec', + 'js/spec/views/settings/main_spec', + 'js/spec/factories/xblock_validation_spec', + 'js/certificates/spec/models/certificate_spec', + 'js/certificates/spec/views/certificate_details_spec', + 'js/certificates/spec/views/certificate_editor_spec', + 'js/certificates/spec/views/certificates_list_spec', + 'js/certificates/spec/views/certificate_preview_spec' + ]; + + i = 0; + + while (i < testFiles.length) { + testFiles[i] = '/base/' + testFiles[i] + '.js'; + i++; + } + + specHelpers = [ + 'common/js/spec_helpers/jasmine-extensions', + 'common/js/spec_helpers/jasmine-stealth', + 'common/js/spec_helpers/jasmine-waituntil' + ]; + + requireSerial(specHelpers.concat(testFiles), function() { + return window.__karma__.start(); + }); + +}).call(this, requirejs, requireSerial); // jshint ignore:line diff --git a/cms/static/cms/js/spec/main_squire.js b/cms/static/cms/js/spec/main_squire.js new file mode 100644 index 0000000000..8e65cef81b --- /dev/null +++ b/cms/static/cms/js/spec/main_squire.js @@ -0,0 +1,218 @@ +(function(requirejs, requireSerial) { + 'use strict'; + + var i, specHelpers, testFiles; + + requirejs.config({ + baseUrl: '/base/', + paths: { + 'gettext': 'xmodule_js/common_static/js/test/i18n', + 'mustache': 'xmodule_js/common_static/js/vendor/mustache', + 'codemirror': 'xmodule_js/common_static/js/vendor/CodeMirror/codemirror', + 'jquery': 'common/js/vendor/jquery', + 'jquery-migrate': 'common/js/vendor/jquery-migrate', + 'jquery.ui': 'xmodule_js/common_static/js/vendor/jquery-ui.min', + 'jquery.form': 'xmodule_js/common_static/js/vendor/jquery.form', + 'jquery.markitup': 'xmodule_js/common_static/js/vendor/markitup/jquery.markitup', + 'jquery.leanModal': 'xmodule_js/common_static/js/vendor/jquery.leanModal', + 'jquery.smoothScroll': 'xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min', + 'jquery.scrollTo': 'common/js/vendor/jquery.scrollTo', + 'jquery.timepicker': 'xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker', + 'jquery.cookie': 'xmodule_js/common_static/js/vendor/jquery.cookie', + 'jquery.qtip': 'xmodule_js/common_static/js/vendor/jquery.qtip.min', + 'jquery.fileupload': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload', + 'jquery.fileupload-process': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process', // jshint ignore:line + 'jquery.fileupload-validate': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate', // jshint ignore:line + 'jquery.iframe-transport': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport', // jshint ignore:line + 'jquery.inputnumber': 'xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill', + 'jquery.immediateDescendents': 'xmodule_js/common_static/coffee/src/jquery.immediateDescendents', + 'datepair': 'xmodule_js/common_static/js/vendor/timepicker/datepair', + 'date': 'xmodule_js/common_static/js/vendor/date', + 'text': 'xmodule_js/common_static/js/vendor/requirejs/text', + 'underscore': 'common/js/vendor/underscore', + 'underscore.string': 'common/js/vendor/underscore.string', + 'backbone': 'common/js/vendor/backbone', + 'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min', + 'backbone.paginator': 'common/js/vendor/backbone.paginator', + 'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min', + 'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce', + 'xmodule': 'xmodule_js/src/xmodule', + 'xblock/cms.runtime.v1': 'cms/js/xblock/cms.runtime.v1', + 'xblock': 'common/js/xblock', + 'utility': 'xmodule_js/common_static/js/src/utility', + 'sinon': 'xmodule_js/common_static/js/vendor/sinon-1.17.0', + 'squire': 'xmodule_js/common_static/js/vendor/Squire', + 'modernizr': 'edx-pattern-library/js/modernizr-custom', + 'afontgarde': 'edx-pattern-library/js/afontgarde', + 'edxicons': 'edx-pattern-library/js/edx-icons', + 'draggabilly': 'xmodule_js/common_static/js/vendor/draggabilly', + 'domReady': 'xmodule_js/common_static/js/vendor/domReady', + 'URI': 'xmodule_js/common_static/js/vendor/URI.min', + 'mathjax': '//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // jshint ignore:line + 'youtube': '//www.youtube.com/player_api?noext', + 'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix' + }, + 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.smoothScroll': { + deps: ['jquery'], + exports: 'jQuery.fn.smoothScroll' + }, + 'jquery.scrollTo': { + deps: ['jquery'], + exports: 'jQuery.fn.scrollTo' + }, + 'jquery.cookie': { + deps: ['jquery'], + exports: 'jQuery.fn.cookie' + }, + 'jquery.qtip': { + deps: ['jquery'], + exports: 'jQuery.fn.qtip' + }, + 'jquery.fileupload': { + deps: ['jquery.ui', 'jquery.iframe-transport'], + exports: 'jQuery.fn.fileupload' + }, + 'jquery.fileupload-process': { + deps: ['jquery.fileupload'] + }, + 'jquery.fileupload-validate': { + deps: ['jquery.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.PageableCollection' + }, + 'youtube': { + exports: 'YT' + }, + 'codemirror': { + exports: 'CodeMirror' + }, + 'tinymce': { + exports: 'tinymce' + }, + 'mathjax': { + exports: 'MathJax', + init: function() { + window.MathJax.Hub.Config({ + tex2jax: { + inlineMath: [['\\(', '\\)'], ['[mathjaxinline]', '[/mathjaxinline]']], + displayMath: [['\\[', '\\]'], ['[mathjax]', '[/mathjax]']] + } + }); + window.MathJax.Hub.Configured(); + } + }, + 'URI': { + exports: 'URI' + }, + 'xmodule': { + exports: 'XModule' + }, + 'sinon': { + exports: 'sinon' + }, + 'common/js/spec_helpers/jasmine-extensions': { + deps: ['jquery'] + }, + 'common/js/spec_helpers/jasmine-stealth': { + deps: ['underscore', 'underscore.string'] + }, + 'common/js/spec_helpers/jasmine-waituntil': { + 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/ajax_prefix': { + deps: ['jquery'] + }, + 'modernizr': { + exports: 'Modernizr' + }, + 'afontgarde': { + exports: 'AFontGarde' + } + } + }); + + jasmine.getFixtures().fixturesPath += 'coffee/fixtures'; + + testFiles = [ + 'coffee/spec/views/assets_spec', + 'js/spec/video/translations_editor_spec', + 'js/spec/video/file_uploader_editor_spec', + 'js/spec/models/group_configuration_spec' + ]; + + i = 0; + + while (i < testFiles.length) { + testFiles[i] = '/base/' + testFiles[i] + '.js'; + i++; + } + + specHelpers = [ + 'common/js/spec_helpers/jasmine-extensions', + 'common/js/spec_helpers/jasmine-stealth', + 'common/js/spec_helpers/jasmine-waituntil' + ]; + + requireSerial(specHelpers.concat(testFiles), function() { + return window.__karma__.start(); + }); + +}).call(this, requirejs, requireSerial); // jshint ignore:line diff --git a/cms/static/js/spec/xblock/cms.runtime.v1_spec.js b/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js similarity index 79% rename from cms/static/js/spec/xblock/cms.runtime.v1_spec.js rename to cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js index c508c21548..893fe6827a 100644 --- a/cms/static/js/spec/xblock/cms.runtime.v1_spec.js +++ b/cms/static/cms/js/spec/xblock/cms.runtime.v1_spec.js @@ -1,10 +1,11 @@ -define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cms.runtime.v1"], - function (EditHelpers, BaseModal) { +define(['js/spec_helpers/edit_helpers', 'js/views/modals/base_modal', 'xblock/cms.runtime.v1'], + function(EditHelpers, BaseModal) { + 'use strict'; - describe("Studio Runtime v1", function() { + describe('Studio Runtime v1', function() { var runtime; - beforeEach(function () { + beforeEach(function() { EditHelpers.installEditTemplates(); runtime = new window.StudioRuntime.v1(); }); @@ -20,7 +21,7 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm }); it('shows save notifications', function() { - var title = "Mock saving...", + var title = 'Mock saving...', notificationSpy = EditHelpers.createNotificationSpy(); runtime.notify('save', { state: 'start', @@ -34,9 +35,9 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm }); it('shows error messages', function() { - var title = "Mock Error", - message = "This is a mock error.", - notificationSpy = EditHelpers.createNotificationSpy("Error"); + var title = 'Mock Error', + message = 'This is a mock error.', + notificationSpy = EditHelpers.createNotificationSpy('Error'); runtime.notify('error', { title: title, message: message @@ -44,7 +45,7 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm EditHelpers.verifyNotificationShowing(notificationSpy, title); }); - describe("Modal Dialogs", function() { + describe('Modal Dialogs', function() { var MockModal, modal, showMockModal; MockModal = BaseModal.extend({ @@ -55,12 +56,12 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm showMockModal = function() { modal = new MockModal({ - title: "Mock Modal" + title: 'Mock Modal' }); modal.show(); }; - beforeEach(function () { + beforeEach(function() { EditHelpers.installEditTemplates(); }); @@ -68,7 +69,7 @@ define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cm EditHelpers.hideModalIfShowing(modal); }); - it('cancels a modal dialog', function () { + it('cancels a modal dialog', function() { showMockModal(); runtime.notify('modal-shown', modal); expect(EditHelpers.isShowingModal(modal)).toBeTruthy(); diff --git a/cms/static/cms/js/xblock/cms.runtime.v1.js b/cms/static/cms/js/xblock/cms.runtime.v1.js new file mode 100644 index 0000000000..6c5e73aaf3 --- /dev/null +++ b/cms/static/cms/js/xblock/cms.runtime.v1.js @@ -0,0 +1,188 @@ +define(['jquery', 'backbone', 'xblock/runtime.v1', 'URI', 'gettext', 'js/utils/modal', + 'common/js/components/views/feedback_notification'], + function($, Backbone, XBlock, URI, gettext, ModalUtils, NotificationView) { + 'use strict'; + + var __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { + var key; + for (key in parent) { + if (__hasProp.call(parent, key)) { + child[key] = parent[key]; + } + } + function Ctor() { + this.constructor = child; + } + Ctor.prototype = parent.prototype; + child.prototype = new Ctor(); + child.__super__ = parent.prototype; + return child; + }, + BaseRuntime = {}, + PreviewRuntime = {}, + StudioRuntime = {}; + + BaseRuntime.v1 = (function(_super) { + + __extends(v1, _super); + + v1.prototype.handlerUrl = function(element, handlerName, suffix, query) { + var uri; + uri = URI(this.handlerPrefix) + .segment($(element).data('usage-id')) + .segment('handler') + .segment(handlerName); + if (suffix !== null) { + uri.segment(suffix); + } + if (query !== null) { + uri.search(query); + } + return uri.toString(); + }; + + function v1() { + v1.__super__.constructor.call(this); + this.dispatcher = _.clone(Backbone.Events); + this.listenTo('save', this._handleSave); + this.listenTo('cancel', this._handleCancel); + this.listenTo('error', this._handleError); + this.listenTo('modal-shown', function(data) { + this.modal = data; + }); + this.listenTo('modal-hidden', function() { + this.modal = null; + }); + this.listenTo('page-shown', function(data) { + this.page = data; + }); + } + + /** + * Notify the Studio client-side runtime of an event so that it + * can update the UI in a consistent way. + * + * @param {string} name The name of the event. + * @param {object} data A JSON representation of the data to be included with the event. + */ + v1.prototype.notify = function(name, data) { + this.dispatcher.trigger(name, data); + }; + + /** + * Listen to a Studio event and invoke the specified callback when it is triggered. + * + * @param {string} name The name of the event. + * @param {function} callback The callback to be invoked. + */ + v1.prototype.listenTo = function(name, callback) { + this.dispatcher.bind(name, callback, this); + }; + + /** + * Refresh the view for the xblock represented by the specified element. + * + * @param {element} element The element representing the XBlock. + */ + v1.prototype.refreshXBlock = function(element) { + if (this.page) { + this.page.refreshXBlock(element); + } + }; + + v1.prototype._handleError = function(data) { + var message, title; + message = data.message || data.msg; + if (message) { + // TODO: remove 'Open Assessment' specific default title + title = data.title || gettext('OpenAssessment Save Error'); + this.alert = new NotificationView.Error({ + title: title, + message: message, + closeIcon: false, + shown: false + }); + this.alert.show(); + } + }; + + v1.prototype._handleSave = function(data) { + var message; + // Starting to save, so show a notification + if (data.state === 'start') { + message = data.message || gettext('Saving'); + this.notification = new NotificationView.Mini({ + title: message + }); + this.notification.show(); + } else if (data.state === 'end') { + // Finished saving, so hide the notification and refresh appropriately + this._hideAlerts(); + + if (this.modal && this.modal.onSave) { + // Notify the modal that the save has completed so that it can hide itself + // and then refresh the xblock. + this.modal.onSave(); + } else if (data.element) { + // ... else ask it to refresh the newly saved xblock + this.refreshXBlock(data.element); + } + this.notification.hide(); + } + }; + + v1.prototype._handleCancel = function() { + this._hideAlerts(); + if (this.modal) { + this.modal.cancel(); + this.notify('modal-hidden'); + } + }; + + /** + * Hide any alerts that are being shown. + */ + v1.prototype._hideAlerts = function() { + if (this.alert && this.alert.options.shown) { + this.alert.hide(); + } + }; + + return v1; + + })(XBlock.Runtime.v1); + + PreviewRuntime.v1 = (function(_super) { + + __extends(v1, _super); + + function v1() { + return v1.__super__.constructor.apply(this, arguments); + } + + v1.prototype.handlerPrefix = '/preview/xblock'; + + return v1; + + })(BaseRuntime.v1); + + StudioRuntime.v1 = (function(_super) { + + __extends(v1, _super); + + function v1() { + return v1.__super__.constructor.apply(this, arguments); + } + + v1.prototype.handlerPrefix = '/xblock'; + + return v1; + + })(BaseRuntime.v1); + + // Install the runtime's into the global namespace + window.BaseRuntime = BaseRuntime; + window.PreviewRuntime = PreviewRuntime; + window.StudioRuntime = StudioRuntime; + }); diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee deleted file mode 100644 index 3685cf47ac..0000000000 --- a/cms/static/coffee/spec/main.coffee +++ /dev/null @@ -1,299 +0,0 @@ -requirejs.config({ - baseUrl: '/base/', - paths: { - "gettext": "xmodule_js/common_static/js/test/i18n", - "mustache": "xmodule_js/common_static/js/vendor/mustache", - "codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror", - "jquery": "xmodule_js/common_static/common/js/vendor/jquery", - "jquery-migrate": "xmodule_js/common_static/common/js/vendor/jquery-migrate", - "jquery.ui": "xmodule_js/common_static/js/vendor/jquery-ui.min", - "jquery.form": "xmodule_js/common_static/js/vendor/jquery.form", - "jquery.markitup": "xmodule_js/common_static/js/vendor/markitup/jquery.markitup", - "jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal", - "jquery.ajaxQueue": "xmodule_js/common_static/js/vendor/jquery.ajaxQueue", - "jquery.smoothScroll": "xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min", - "jquery.scrollTo": "common/js/vendor/jquery.scrollTo", - "jquery.timepicker": "xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker", - "jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie", - "jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min", - "jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload", - "jquery.fileupload-process": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process", - "jquery.fileupload-validate": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate", - "jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", - "jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill", - "jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents", - "jquery.simulate": "xmodule_js/common_static/js/vendor/jquery.simulate", - "datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair", - "date": "xmodule_js/common_static/js/vendor/date", - "moment": "xmodule_js/common_static/js/vendor/moment.min", - "moment-with-locales": "xmodule_js/common_static/js/vendor/moment-with-locales.min", - "text": "xmodule_js/common_static/js/vendor/requirejs/text", - "underscore": "common/js/vendor/underscore", - "underscore.string": "common/js/vendor/underscore.string", - "backbone": "common/js/vendor/backbone", - "backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min", - "backbone.paginator": "common/js/vendor/backbone.paginator", - "backbone-relational": "xmodule_js/common_static/js/vendor/backbone-relational.min", - "tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min", - "jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce", - "xmodule": "xmodule_js/src/xmodule", - "xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1", - "xblock/core": "xmodule_js/common_static/js/xblock/core", - "xblock": "xmodule_js/common_static/coffee/src/xblock", - "utility": "xmodule_js/common_static/js/src/utility", - "accessibility": "xmodule_js/common_static/js/src/accessibility_tools", - "sinon": "xmodule_js/common_static/js/vendor/sinon-1.17.0", - "squire": "xmodule_js/common_static/js/vendor/Squire", - "jasmine-imagediff": "xmodule_js/common_static/js/vendor/jasmine-imagediff", - "draggabilly": "xmodule_js/common_static/js/vendor/draggabilly", - "domReady": "xmodule_js/common_static/js/vendor/domReady", - "URI": "xmodule_js/common_static/js/vendor/URI.min", - "mock-ajax": "xmodule_js/common_static/js/vendor/mock-ajax", - "modernizr": "edx-pattern-library/js/modernizr-custom", - "afontgarde": "edx-pattern-library/js/afontgarde", - "edxicons": "edx-pattern-library/js/edx-icons", - - "mathjax": "//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured", - "youtube": "//www.youtube.com/player_api?noext", - - "coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix", - "js/spec/test_utils": "js/spec/test_utils", - } - shim: { - "gettext": { - exports: "gettext" - }, - "date": { - exports: "Date" - }, - "jquery-migrate": ['jquery'], - "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.smoothScroll": { - deps: ["jquery"], - exports: "jQuery.fn.smoothScroll" - }, - "jquery.ajaxQueue": { - deps: ["jquery"], - exports: "jQuery.fn.ajaxQueue" - }, - "jquery.scrollTo": { - deps: ["jquery"], - exports: "jQuery.fn.scrollTo" - }, - "jquery.cookie": { - deps: ["jquery"], - exports: "jQuery.fn.cookie" - }, - "jquery.qtip": { - deps: ["jquery"], - exports: "jQuery.fn.qtip" - }, - "jquery.fileupload": { - deps: ["jquery.ui", "jquery.iframe-transport"], - exports: "jQuery.fn.fileupload" - }, - "jquery.fileupload-process": { - deps: ["jquery.fileupload"] - }, - "jquery.fileupload-validate": { - deps: ["jquery.fileupload"] - }, - "jquery.inputnumber": { - deps: ["jquery"], - exports: "jQuery.fn.inputNumber" - }, - "jquery.simulate": { - deps: ["jquery"], - exports: "jQuery.fn.simulate" - }, - "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.PageableCollection" - }, - "backbone-relational": { - deps: ["backbone"], - }, - "youtube": { - exports: "YT" - }, - "codemirror": { - exports: "CodeMirror" - }, - "tinymce": { - exports: "tinymce" - }, - "mathjax": { - exports: "MathJax", - init: -> - MathJax.Hub.Config - tex2jax: - inlineMath: [ - ["\\(", "\\)"], - ['[mathjaxinline]', '[/mathjaxinline]'] - ] - displayMath: [ - ["\\[", "\\]"], - ['[mathjax]', '[/mathjax]'] - ] - MathJax.Hub.Configured() - }, - "URI": { - exports: "URI" - }, - "xmodule": { - exports: "XModule" - }, - "sinon": { - exports: "sinon" - }, - "jasmine-imagediff": {}, - "common/js/spec_helpers/jasmine-extensions": { - deps: ["jquery"] - }, - "common/js/spec_helpers/jasmine-stealth": { - deps: ["underscore", "underscore.string"] - }, - "common/js/spec_helpers/jasmine-waituntil": { - deps: ["jquery"] - }, - "xblock/core": { - exports: "XBlock", - deps: ["jquery", "jquery.immediateDescendents"] - }, - "xblock/runtime.v1": { - exports: "XBlock", - deps: ["xblock/core"] - }, - "mock-ajax": { - deps: ["jquery"] - } - - "coffee/src/main": { - deps: ["coffee/src/ajax_prefix"] - }, - "coffee/src/ajax_prefix": { - deps: ["jquery"] - }, - "modernizr": { - exports: "Modernizr" - }, - "afontgarde": { - exports: "AFontGarde" - } - } -}); - -jasmine.getFixtures().fixturesPath += 'coffee/fixtures' - -testFiles = [ - "coffee/spec/main_spec", - "coffee/spec/models/course_spec", - "coffee/spec/models/metadata_spec", - "coffee/spec/models/section_spec", - "coffee/spec/models/settings_course_grader_spec", - "coffee/spec/models/settings_grading_spec", - "coffee/spec/models/textbook_spec", - "coffee/spec/models/upload_spec", - "coffee/spec/views/course_info_spec", - "coffee/spec/views/metadata_edit_spec", - "coffee/spec/views/module_edit_spec", - "coffee/spec/views/textbook_spec", - "coffee/spec/views/upload_spec", - "js/spec/video/transcripts/utils_spec", - "js/spec/video/transcripts/editor_spec", - "js/spec/video/transcripts/videolist_spec", - "js/spec/video/transcripts/message_manager_spec", - "js/spec/video/transcripts/file_uploader_spec", - "js/spec/models/component_template_spec", - "js/spec/models/explicit_url_spec", - "js/spec/models/xblock_info_spec", - "js/spec/models/xblock_validation_spec", - "js/spec/models/license_spec", - "js/spec/utils/drag_and_drop_spec", - "js/spec/utils/handle_iframe_binding_spec", - "js/spec/utils/module_spec", - "js/spec/views/active_video_upload_list_spec", - "js/spec/views/previous_video_upload_spec", - "js/spec/views/previous_video_upload_list_spec", - "js/spec/views/assets_spec", - "js/spec/views/baseview_spec", - "js/spec/views/container_spec", - "js/spec/views/paged_container_spec", - "js/spec/views/group_configuration_spec", - "js/spec/views/unit_outline_spec", - "js/spec/views/xblock_spec", - "js/spec/views/xblock_editor_spec", - "js/spec/views/xblock_string_field_editor_spec", - "js/spec/views/xblock_validation_spec", - "js/spec/views/license_spec", - "js/spec/views/paging_spec", - "js/spec/views/login_studio_spec", - "js/spec/views/pages/container_spec", - "js/spec/views/pages/container_subviews_spec", - "js/spec/views/pages/group_configurations_spec", - "js/spec/views/pages/course_outline_spec", - "js/spec/views/pages/course_rerun_spec", - "js/spec/views/pages/index_spec", - "js/spec/views/pages/library_users_spec", - "js/spec/views/modals/base_modal_spec", - "js/spec/views/modals/edit_xblock_spec", - "js/spec/views/modals/validation_error_modal_spec", - "js/spec/views/settings/main_spec", - "js/spec/factories/xblock_validation_spec", - "js/spec/xblock/cms.runtime.v1_spec", - "js/certificates/spec/models/certificate_spec", - "js/certificates/spec/views/certificate_details_spec", - "js/certificates/spec/views/certificate_editor_spec", - "js/certificates/spec/views/certificates_list_spec", - "js/certificates/spec/views/certificate_preview_spec" -] - -i = 0 -while i < testFiles.length - testFiles[i] = '/base/' + testFiles[i] + '.js' - i++ - -specHelpers = [ - 'common/js/spec_helpers/jasmine-extensions', - 'common/js/spec_helpers/jasmine-stealth', - 'common/js/spec_helpers/jasmine-waituntil' -] - -# Jasmine has a global stack for creating a tree of specs. We need to load -# spec files one by one, otherwise some end up getting nested under others. -requireSerial specHelpers.concat(testFiles), -> -# start test run, once Require.js is done - window.__karma__.start() diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee index ed6b6b6d6e..3875df0de5 100644 --- a/cms/static/coffee/spec/main_spec.coffee +++ b/cms/static/coffee/spec/main_spec.coffee @@ -1,4 +1,4 @@ -require ["jquery", "backbone", "coffee/src/main", "common/js/spec_helpers/ajax_helpers", "jquery.cookie"], +require ["jquery", "backbone", "coffee/src/main", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "jquery.cookie"], ($, Backbone, main, AjaxHelpers) -> describe "CMS", -> it "should initialize URL", -> diff --git a/cms/static/coffee/spec/main_squire.coffee b/cms/static/coffee/spec/main_squire.coffee deleted file mode 100644 index 3743e9602d..0000000000 --- a/cms/static/coffee/spec/main_squire.coffee +++ /dev/null @@ -1,218 +0,0 @@ -requirejs.config({ - baseUrl: '/base/', - - paths: { - "gettext": "xmodule_js/common_static/js/test/i18n", - "mustache": "xmodule_js/common_static/js/vendor/mustache", - "codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror", - "jquery": "common/js/vendor/jquery", - "jquery-migrate": "common/js/vendor/jquery-migrate", - "jquery.ui": "xmodule_js/common_static/js/vendor/jquery-ui.min", - "jquery.form": "xmodule_js/common_static/js/vendor/jquery.form", - "jquery.markitup": "xmodule_js/common_static/js/vendor/markitup/jquery.markitup", - "jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal", - "jquery.smoothScroll": "xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min", - "jquery.scrollTo": "common/js/vendor/jquery.scrollTo", - "jquery.timepicker": "xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker", - "jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie", - "jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min", - "jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload", - "jquery.fileupload-process": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process", - "jquery.fileupload-validate": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate", - "jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", - "jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill", - "jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents", - "datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair", - "date": "xmodule_js/common_static/js/vendor/date", - "text": "xmodule_js/common_static/js/vendor/requirejs/text", - "underscore": "common/js/vendor/underscore", - "underscore.string": "common/js/vendor/underscore.string", - "backbone": "common/js/vendor/backbone", - "backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min", - "backbone.paginator": "common/js/vendor/backbone.paginator", - "tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min", - "jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce", - "xmodule": "xmodule_js/src/xmodule", - "xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1", - "xblock/core": "xmodule_js/common_static/js/xblock/core", - "xblock": "xmodule_js/common_static/coffee/src/xblock", - "utility": "xmodule_js/common_static/js/src/utility", - "sinon": "xmodule_js/common_static/js/vendor/sinon-1.17.0", - "squire": "xmodule_js/common_static/js/vendor/Squire", - "modernizr": "edx-pattern-library/js/modernizr-custom", - "afontgarde": "edx-pattern-library/js/afontgarde", - "edxicons": "edx-pattern-library/js/edx-icons", - "draggabilly": "xmodule_js/common_static/js/vendor/draggabilly", - "domReady": "xmodule_js/common_static/js/vendor/domReady", - "URI": "xmodule_js/common_static/js/vendor/URI.min", - - "mathjax": "//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured", - "youtube": "//www.youtube.com/player_api?noext", - - "coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix" - } - 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.smoothScroll": { - deps: ["jquery"], - exports: "jQuery.fn.smoothScroll" - }, - "jquery.scrollTo": { - deps: ["jquery"], - exports: "jQuery.fn.scrollTo" - }, - "jquery.cookie": { - deps: ["jquery"], - exports: "jQuery.fn.cookie" - }, - "jquery.qtip": { - deps: ["jquery"], - exports: "jQuery.fn.qtip" - }, - "jquery.fileupload": { - deps: ["jquery.ui", "jquery.iframe-transport"], - exports: "jQuery.fn.fileupload" - }, - "jquery.fileupload-process": { - deps: ["jquery.fileupload"] - }, - "jquery.fileupload-validate": { - deps: ["jquery.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.PageableCollection" - }, - "youtube": { - exports: "YT" - }, - "codemirror": { - exports: "CodeMirror" - }, - "tinymce": { - exports: "tinymce" - }, - "mathjax": { - exports: "MathJax", - init: -> - MathJax.Hub.Config - tex2jax: - inlineMath: [ - ["\\(","\\)"], - ['[mathjaxinline]','[/mathjaxinline]'] - ] - displayMath: [ - ["\\[","\\]"], - ['[mathjax]','[/mathjax]'] - ] - MathJax.Hub.Configured(); - }, - "URI": { - exports: "URI" - }, - "xmodule": { - exports: "XModule" - }, - "sinon": { - exports: "sinon" - }, - "common/js/spec_helpers/jasmine-extensions": { - deps: ["jquery"] - }, - "common/js/spec_helpers/jasmine-stealth": { - deps: ["underscore", "underscore.string"] - }, - "common/js/spec_helpers/jasmine-waituntil": { - 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/ajax_prefix": { - deps: ["jquery"] - }, - "modernizr": { - exports: "Modernizr" - }, - "afontgarde": { - exports: "AFontGarde" - } - } -}); - -jasmine.getFixtures().fixturesPath += 'coffee/fixtures' - -testFiles = [ - 'coffee/spec/views/assets_spec', - 'js/spec/video/translations_editor_spec', - 'js/spec/video/file_uploader_editor_spec', - 'js/spec/models/group_configuration_spec' -] -i = 0 -while i < testFiles.length - testFiles[i] = '/base/' + testFiles[i] + '.js' - i++ - -specHelpers = [ - 'common/js/spec_helpers/jasmine-extensions', - 'common/js/spec_helpers/jasmine-stealth', - 'common/js/spec_helpers/jasmine-waituntil' -] - -# Jasmine has a global stack for creating a tree of specs. We need to load -# spec files one by one, otherwise some end up getting nested under others. -requireSerial specHelpers.concat(testFiles), -> -# start test run, once Require.js is done - window.__karma__.start() diff --git a/cms/static/coffee/spec/models/section_spec.coffee b/cms/static/coffee/spec/models/section_spec.coffee index 8e7438f5ba..8a954b50dc 100644 --- a/cms/static/coffee/spec/models/section_spec.coffee +++ b/cms/static/coffee/spec/models/section_spec.coffee @@ -1,4 +1,4 @@ -define ["js/models/section", "common/js/spec_helpers/ajax_helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) -> +define ["js/models/section", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) -> describe "Section", -> describe "basic", -> beforeEach -> diff --git a/cms/static/coffee/spec/views/assets_spec.coffee b/cms/static/coffee/spec/views/assets_spec.coffee index eea73055c5..e94d166a52 100644 --- a/cms/static/coffee/spec/views/assets_spec.coffee +++ b/cms/static/coffee/spec/views/assets_spec.coffee @@ -1,4 +1,4 @@ -define ["jquery", "common/js/spec_helpers/ajax_helpers", "squire"], +define ["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "squire"], ($, AjaxHelpers, Squire) -> assetLibraryTpl = readFixtures('asset-library.underscore') diff --git a/cms/static/coffee/spec/views/course_info_spec.coffee b/cms/static/coffee/spec/views/course_info_spec.coffee index edcf9bf7a7..6e275ca2f7 100644 --- a/cms/static/coffee/spec/views/course_info_spec.coffee +++ b/cms/static/coffee/spec/views/course_info_spec.coffee @@ -1,4 +1,5 @@ -define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "common/js/spec_helpers/ajax_helpers"], +define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", + "js/collections/course_update", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers"], (CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, AjaxHelpers) -> describe "Course Updates and Handouts", -> diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee deleted file mode 100644 index a631c4ddde..0000000000 --- a/cms/static/coffee/spec/views/module_edit_spec.coffee +++ /dev/null @@ -1,180 +0,0 @@ -define ["jquery", "common/js/components/utils/view_utils", "js/spec_helpers/edit_helpers", - "coffee/src/views/module_edit", "js/models/module_info", "xmodule"], - ($, ViewUtils, edit_helpers, ModuleEdit, ModuleModel) -> - - describe "ModuleEdit", -> - beforeEach -> - @stubModule = new ModuleModel - id: "stub-id" - - setFixtures """ - - """ - edit_helpers.installEditTemplates(true); - spyOn($, 'ajax').and.returnValue(@moduleData) - - @moduleEdit = new ModuleEdit( - el: $(".component") - model: @stubModule - onDelete: jasmine.createSpy() - ) - - describe "class definition", -> - it "sets the correct tagName", -> - expect(@moduleEdit.tagName).toEqual("li") - - it "sets the correct className", -> - expect(@moduleEdit.className).toEqual("component") - - describe "methods", -> - describe "initialize", -> - beforeEach -> - spyOn(ModuleEdit.prototype, 'render') - @moduleEdit = new ModuleEdit( - el: $(".component") - model: @stubModule - onDelete: jasmine.createSpy() - ) - - it "renders the module editor", -> - expect(ModuleEdit.prototype.render).toHaveBeenCalled() - - describe "render", -> - beforeEach -> - spyOn(@moduleEdit, 'loadDisplay') - spyOn(@moduleEdit, 'delegateEvents') - spyOn($.fn, 'append') - spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise()); - - window.MockXBlock = (runtime, element) -> - return { } - - window.loadedXBlockResources = undefined - - @moduleEdit.render() - $.ajax.calls.mostRecent().args[0].success( - html: '
Response html
' - resources: [ - ['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}], - ['hash2', {kind: 'url', mimetype: 'text/css', data: 'css-url'}], - ['hash3', {kind: 'text', mimetype: 'application/javascript', data: 'inline-js'}], - ['hash4', {kind: 'url', mimetype: 'application/javascript', data: 'js-url'}], - ['hash5', {placement: 'head', mimetype: 'text/html', data: 'head-html'}], - ['hash6', {placement: 'not-head', mimetype: 'text/html', data: 'not-head-html'}], - ] - ) - - afterEach -> - window.MockXBlock = null - - it "loads the module preview via ajax on the view element", -> - expect($.ajax).toHaveBeenCalledWith( - url: "/xblock/#{@moduleEdit.model.id}/student_view" - type: "GET" - cache: false - headers: - Accept: 'application/json' - success: jasmine.any(Function) - ) - - expect($.ajax).not.toHaveBeenCalledWith( - url: "/xblock/#{@moduleEdit.model.id}/studio_view" - type: "GET" - headers: - Accept: 'application/json' - success: jasmine.any(Function) - ) - expect(@moduleEdit.loadDisplay).toHaveBeenCalled() - expect(@moduleEdit.delegateEvents).toHaveBeenCalled() - - it "loads the editing view via ajax on demand", -> - edit_helpers.installEditTemplates(true); - expect($.ajax).not.toHaveBeenCalledWith( - url: "/xblock/#{@moduleEdit.model.id}/studio_view" - type: "GET" - cache : false - headers: - Accept: 'application/json' - success: jasmine.any(Function) - ) - - @moduleEdit.clickEditButton({'preventDefault': jasmine.createSpy('event.preventDefault')}) - - mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore') - - $.ajax.calls.mostRecent().args[0].success( - html: mockXBlockEditorHtml - resources: [ - ['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}], - ['hash2', {kind: 'url', mimetype: 'text/css', data: 'css-url'}], - ['hash3', {kind: 'text', mimetype: 'application/javascript', data: 'inline-js'}], - ['hash4', {kind: 'url', mimetype: 'application/javascript', data: 'js-url'}], - ['hash5', {placement: 'head', mimetype: 'text/html', data: 'head-html'}], - ['hash6', {placement: 'not-head', mimetype: 'text/html', data: 'not-head-html'}], - ] - ) - - expect($.ajax).toHaveBeenCalledWith( - url: "/xblock/#{@moduleEdit.model.id}/studio_view" - type: "GET" - cache: false - headers: - Accept: 'application/json' - success: jasmine.any(Function) - ) - expect(@moduleEdit.delegateEvents).toHaveBeenCalled() - - it "loads inline css from fragments", -> - expect($('head').append).toHaveBeenCalledWith("") - - it "loads css urls from fragments", -> - expect($('head').append).toHaveBeenCalledWith("") - - it "loads inline js from fragments", -> - expect($('head').append).toHaveBeenCalledWith("") - - it "loads js urls from fragments", -> - expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith("js-url") - - it "loads head html", -> - expect($('head').append).toHaveBeenCalledWith("head-html") - - it "doesn't load body html", -> - expect($.fn.append).not.toHaveBeenCalledWith('not-head-html') - - it "doesn't reload resources", -> - count = $('head').append.calls.count() - $.ajax.calls.mostRecent().args[0].success( - html: '
Response html 2
' - resources: [ - ['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}], - ] - ) - expect($('head').append.calls.count()).toBe(count) - - describe "loadDisplay", -> - beforeEach -> - spyOn(XBlock, 'initializeBlock') - @moduleEdit.loadDisplay() - - it "loads the .xmodule-display inside the module editor", -> - expect(XBlock.initializeBlock).toHaveBeenCalled() - expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($('.xblock-student_view').get(0)) diff --git a/cms/static/coffee/spec/views/textbook_spec.coffee b/cms/static/coffee/spec/views/textbook_spec.coffee index bc855b621d..b4cbdcbf70 100644 --- a/cms/static/coffee/spec/views/textbook_spec.coffee +++ b/cms/static/coffee/spec/views/textbook_spec.coffee @@ -1,339 +1,341 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course", "js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks", "js/views/edit_chapter", "common/js/components/views/feedback_prompt", - "common/js/components/views/feedback_notification", "common/js/components/utils/view_utils","common/js/spec_helpers/ajax_helpers", + "common/js/components/views/feedback_notification", "common/js/components/utils/view_utils", + "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/modal_helpers"], -(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTextbooks, EditChapter, Prompt, Notification, ViewUtils, AjaxHelpers, modal_helpers) -> + (Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTextbooks, EditChapter, + Prompt, Notification, ViewUtils, AjaxHelpers, modal_helpers) -> - describe "ShowTextbook", -> - tpl = readFixtures('show-textbook.underscore') - - beforeEach -> - setFixtures($("'); + }); + it('loads js urls from fragments', function() { + return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url'); + }); + it('loads head html', function() { + return expect($('head').append).toHaveBeenCalledWith('head-html'); + }); + it("doesn't load body html", function() { + return expect($.fn.append).not.toHaveBeenCalledWith("not-head-html"); + }); + it("doesn't reload resources", function() { + var count; + count = $('head').append.calls.count(); + $.ajax.calls.mostRecent().args[0].success({ + html: '
Response html 2
', + resources: [ + [ + 'hash1', { + kind: 'text', + mimetype: 'text/css', + data: 'inline-css' + } + ] + ] + }); + return expect($('head').append.calls.count()).toBe(count); + }); + }); + describe('loadDisplay', function() { + beforeEach(function() { + spyOn(XBlock, 'initializeBlock'); + return this.moduleEdit.loadDisplay(); + }); + it('loads the .xmodule-display inside the module editor', function() { + expect(XBlock.initializeBlock).toHaveBeenCalled(); + var sel = '.xblock-student_view'; + return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0)); + }); + }); + }); + }); + }); +}).call(this); diff --git a/cms/static/js/spec/views/paged_container_spec.js b/cms/static/js/spec/views/paged_container_spec.js index 653e283fbb..fd2e210bde 100644 --- a/cms/static/js/spec/views/paged_container_spec.js +++ b/cms/static/js/spec/views/paged_container_spec.js @@ -1,4 +1,4 @@ -define(["jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "URI", "js/models/xblock_info", +define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "URI", "js/models/xblock_info", "js/views/paged_container", "js/views/paging_header", "common/js/components/views/paging_footer", "js/views/xblock"], function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter, XBlockView) { diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index b307111536..9d6ec66a11 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -1,4 +1,4 @@ -define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/ajax_helpers", +define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers", "js/views/pages/container", "js/views/pages/paged_container", "js/models/xblock_info", "jquery.simulate"], function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage, XBlockInfo) { diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js index 1f518342cc..22af2fb667 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -1,4 +1,4 @@ -define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/ajax_helpers", +define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers", "common/js/components/views/feedback_prompt", "js/views/pages/container", "js/views/pages/container_subviews", "js/models/xblock_info", "js/views/utils/xblock_utils", diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index 5de589c5cf..efbe55607a 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -1,6 +1,6 @@ -define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/utils/view_utils", "js/views/pages/course_outline", - "js/models/xblock_outline_info", "js/utils/date_utils", "js/spec_helpers/edit_helpers", - "common/js/spec_helpers/template_helpers", 'js/models/course',], +define(["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/components/utils/view_utils", + "js/views/pages/course_outline", "js/models/xblock_outline_info", "js/utils/date_utils", + "js/spec_helpers/edit_helpers", "common/js/spec_helpers/template_helpers", 'js/models/course'], function($, AjaxHelpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils, EditHelpers, TemplateHelpers, Course) { diff --git a/cms/static/js/spec/views/pages/course_rerun_spec.js b/cms/static/js/spec/views/pages/course_rerun_spec.js index f9d62ed2ba..414131b69a 100644 --- a/cms/static/js/spec/views/pages/course_rerun_spec.js +++ b/cms/static/js/spec/views/pages/course_rerun_spec.js @@ -1,5 +1,6 @@ -define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/view_helpers", "js/views/course_rerun", - "js/views/utils/create_course_utils", "common/js/components/utils/view_utils", "jquery.simulate"], +define(["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/spec_helpers/view_helpers", + "js/views/course_rerun", "js/views/utils/create_course_utils", "common/js/components/utils/view_utils", + "jquery.simulate"], function ($, AjaxHelpers, ViewHelpers, CourseRerunUtils, CreateCourseUtilsFactory, ViewUtils) { describe("Create course rerun page", function () { var selectors = { diff --git a/cms/static/js/spec/views/pages/index_spec.js b/cms/static/js/spec/views/pages/index_spec.js index 243aac1024..85aad9575a 100644 --- a/cms/static/js/spec/views/pages/index_spec.js +++ b/cms/static/js/spec/views/pages/index_spec.js @@ -1,4 +1,6 @@ -define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/view_helpers", "js/index", +define(["jquery", + "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", + "common/js/spec_helpers/view_helpers", "js/index", "common/js/components/utils/view_utils"], function ($, AjaxHelpers, ViewHelpers, IndexUtils, ViewUtils) { describe("Course listing page", function () { diff --git a/cms/static/js/spec/views/pages/library_users_spec.js b/cms/static/js/spec/views/pages/library_users_spec.js index 3c6a4cb384..8a02ea5d69 100644 --- a/cms/static/js/spec/views/pages/library_users_spec.js +++ b/cms/static/js/spec/views/pages/library_users_spec.js @@ -1,5 +1,5 @@ define([ - "jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/view_helpers", + "jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/spec_helpers/view_helpers", "js/factories/manage_users_lib", "common/js/components/utils/view_utils" ], function ($, AjaxHelpers, ViewHelpers, ManageUsersFactory, ViewUtils) { diff --git a/cms/static/js/spec/views/paging_spec.js b/cms/static/js/spec/views/paging_spec.js index 8e5c42d37d..ad91f40587 100644 --- a/cms/static/js/spec/views/paging_spec.js +++ b/cms/static/js/spec/views/paging_spec.js @@ -1,11 +1,13 @@ define([ - "jquery", - "URI", - "common/js/spec_helpers/ajax_helpers", - "edx-ui-toolkit/js/pagination/paging-collection", - "js/views/paging", - "js/views/paging_header" -], function ($, URI, AjaxHelpers, PagingCollection, PagingView, PagingHeader) { + "jquery", + "URI", + "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", + "edx-ui-toolkit/js/pagination/paging-collection", + "js/views/paging", + "js/views/paging_header" + ], + function ($, URI, AjaxHelpers, PagingCollection, PagingView, PagingHeader) { + 'use strict'; var createPageableItem = function(index) { var id = 'item_' + index; diff --git a/cms/static/js/spec/views/settings/main_spec.js b/cms/static/js/spec/views/settings/main_spec.js index b9c1818e5d..790f195023 100644 --- a/cms/static/js/spec/views/settings/main_spec.js +++ b/cms/static/js/spec/views/settings/main_spec.js @@ -1,6 +1,6 @@ define([ 'jquery', 'js/models/settings/course_details', 'js/views/settings/main', - 'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/template_helpers', + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers', ], function($, CourseDetailsModel, MainView, AjaxHelpers, TemplateHelpers) { 'use strict'; @@ -125,10 +125,9 @@ define([ //input some invalid values. expect(entrance_exam_min_score.val('101').trigger('input')).toHaveClass("error"); expect(entrance_exam_min_score.val('invalidVal').trigger('input')).toHaveClass("error"); - }); - it('should provide a default value for the minimum score percentage', function(){ + it('should provide a default value for the minimum score percentage', function() { var entrance_exam_min_score = this.view.$(SELECTORS.entrance_exam_min_score); @@ -138,7 +137,7 @@ define([ .toEqual(this.model.defaults.entrance_exam_minimum_score_pct); }); - it('show and hide the grade requirement section when the check box is selected and deselected respectively', function(){ + it('shows and hide the grade requirement section appropriately', function() { var entrance_exam_enabled_field = this.view.$(SELECTORS.entrance_exam_enabled_field); diff --git a/cms/static/js/spec/views/unit_outline_spec.js b/cms/static/js/spec/views/unit_outline_spec.js index 2f5b5ef338..e73db8d91e 100644 --- a/cms/static/js/spec/views/unit_outline_spec.js +++ b/cms/static/js/spec/views/unit_outline_spec.js @@ -1,8 +1,9 @@ -define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/template_helpers", +define(["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/spec_helpers/template_helpers", "common/js/spec_helpers/view_helpers", "common/js/components/utils/view_utils", "js/models/course", "js/views/unit_outline", "js/models/xblock_info"], - function ($, AjaxHelpers, TemplateHelpers, ViewHelpers, ViewUtils, + function($, AjaxHelpers, TemplateHelpers, ViewHelpers, ViewUtils, Course, UnitOutlineView, XBlockInfo) { + 'use strict'; describe("UnitOutlineView", function() { var createUnitOutlineView, createMockXBlockInfo, diff --git a/cms/static/js/spec/views/xblock_editor_spec.js b/cms/static/js/spec/views/xblock_editor_spec.js index 701c71c499..b1fb40452d 100644 --- a/cms/static/js/spec/views/xblock_editor_spec.js +++ b/cms/static/js/spec/views/xblock_editor_spec.js @@ -1,4 +1,4 @@ -define([ "jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/edit_helpers", +define([ "jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/edit_helpers", "js/views/xblock_editor", "js/models/xblock_info"], function ($, _, AjaxHelpers, EditHelpers, XBlockEditorView, XBlockInfo) { diff --git a/cms/static/js/spec/views/xblock_spec.js b/cms/static/js/spec/views/xblock_spec.js index d192517626..23fa1eeadb 100644 --- a/cms/static/js/spec/views/xblock_spec.js +++ b/cms/static/js/spec/views/xblock_spec.js @@ -1,4 +1,4 @@ -define(["jquery", "URI", "common/js/spec_helpers/ajax_helpers", "common/js/components/utils/view_utils", +define(["jquery", "URI", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/components/utils/view_utils", "js/views/xblock", "js/models/xblock_info", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], function ($, URI, AjaxHelpers, ViewUtils, XBlockView, XBlockInfo) { "use strict"; diff --git a/cms/static/js/spec/views/xblock_string_field_editor_spec.js b/cms/static/js/spec/views/xblock_string_field_editor_spec.js index b3fa04a7db..05ded85952 100644 --- a/cms/static/js/spec/views/xblock_string_field_editor_spec.js +++ b/cms/static/js/spec/views/xblock_string_field_editor_spec.js @@ -1,4 +1,4 @@ -define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/template_helpers", +define(["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers", "js/models/xblock_info", "js/views/xblock_string_field_editor"], function ($, AjaxHelpers, TemplateHelpers, EditHelpers, XBlockInfo, XBlockStringFieldEditor) { describe("XBlockStringFieldEditorView", function () { diff --git a/cms/static/js/spec_helpers/edit_helpers.js b/cms/static/js/spec_helpers/edit_helpers.js index fa100d7907..4d46e6af42 100644 --- a/cms/static/js/spec_helpers/edit_helpers.js +++ b/cms/static/js/spec_helpers/edit_helpers.js @@ -1,9 +1,9 @@ /** * Provides helper methods for invoking Studio editors in Jasmine tests. */ -define(["jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/template_helpers", - "js/spec_helpers/modal_helpers", "js/views/modals/edit_xblock", "js/collections/component_template", - "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], +define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", + "common/js/spec_helpers/template_helpers", "js/spec_helpers/modal_helpers", "js/views/modals/edit_xblock", + "js/collections/component_template", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], function($, _, AjaxHelpers, TemplateHelpers, modal_helpers, EditXBlockModal, ComponentTemplates) { var installMockXBlock, uninstallMockXBlock, installMockXModule, uninstallMockXModule, diff --git a/cms/static/js/views/module_edit.js b/cms/static/js/views/module_edit.js new file mode 100644 index 0000000000..64aa7bafff --- /dev/null +++ b/cms/static/js/views/module_edit.js @@ -0,0 +1,111 @@ +(function() { + 'use strict'; + + var __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { + var key; + for (key in parent) { + if (__hasProp.call(parent, key)) { + child[key] = parent[key]; + } + } + function Ctor() { + this.constructor = child; + } + Ctor.prototype = parent.prototype; + child.prototype = new Ctor(); + child.__super__ = parent.prototype; + return child; + }; + + define(['jquery', 'underscore', 'gettext', 'xblock/runtime.v1', 'js/views/xblock', 'js/views/modals/edit_xblock'], + function($, _, gettext, XBlock, XBlockView, EditXBlockModal) { + var ModuleEdit = (function(_super) { + + __extends(ModuleEdit, _super); + + function ModuleEdit() { + return ModuleEdit.__super__.constructor.apply(this, arguments); + } + + ModuleEdit.prototype.tagName = 'li'; + + ModuleEdit.prototype.className = 'component'; + + ModuleEdit.prototype.editorMode = 'editor-mode'; + + ModuleEdit.prototype.events = { + 'click .edit-button': 'clickEditButton', + 'click .delete-button': 'onDelete' + }; + + ModuleEdit.prototype.initialize = function() { + this.onDelete = this.options.onDelete; + return this.render(); + }; + + ModuleEdit.prototype.loadDisplay = function() { + var xblockElement; + xblockElement = this.$el.find('.xblock-student_view'); + if (xblockElement.length > 0) { + return XBlock.initializeBlock(xblockElement); + } + }; + + ModuleEdit.prototype.createItem = function(parent, payload, callback) { + var _this = this; + if (_.isNull(callback)) { + callback = function() {}; + } + payload.parent_locator = parent; + return $.postJSON(this.model.urlRoot + '/', payload, function(data) { + _this.model.set({ + id: data.locator + }); + _this.$el.data('locator', data.locator); + _this.$el.data('courseKey', data.courseKey); + return _this.render(); + }).success(callback); + }; + + ModuleEdit.prototype.loadView = function(viewName, target, callback) { + var _this = this; + if (this.model.id) { + return $.ajax({ + url: '' + (decodeURIComponent(this.model.url())) + '/' + viewName, + type: 'GET', + cache: false, + headers: { + Accept: 'application/json' + }, + success: function(fragment) { + return _this.renderXBlockFragment(fragment, target).done(callback); + } + }); + } + }; + + ModuleEdit.prototype.render = function() { + var _this = this; + return this.loadView('student_view', this.$el, function() { + _this.loadDisplay(); + return _this.delegateEvents(); + }); + }; + + ModuleEdit.prototype.clickEditButton = function(event) { + var modal; + event.preventDefault(); + modal = new EditXBlockModal(); + return modal.edit(this.$el, this.model, { + refresh: _.bind(this.render, this) + }); + }; + + return ModuleEdit; + + })(XBlockView); + return ModuleEdit; + }); + +}).call(this); diff --git a/cms/static/js/views/tabs.js b/cms/static/js/views/tabs.js new file mode 100644 index 0000000000..04ace25d7e --- /dev/null +++ b/cms/static/js/views/tabs.js @@ -0,0 +1,203 @@ +(function(analytics, course_location_analytics) { + 'use strict'; + + var __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { + var key; + for (key in parent) { + if (__hasProp.call(parent, key)) { + child[key] = parent[key]; + } + } + function Ctor() { + this.constructor = child; + } + Ctor.prototype = parent.prototype; + child.prototype = new Ctor(); + child.__super__ = parent.prototype; + return child; + }; + + define(['underscore', 'jquery', 'jquery.ui', 'backbone', 'common/js/components/views/feedback_prompt', + 'common/js/components/views/feedback_notification', 'js/views/module_edit', + 'js/models/module_info', 'js/utils/module'], + function(_, $, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) { + var TabsEdit; + TabsEdit = (function(_super) { + + __extends(TabsEdit, _super); + + function TabsEdit() { + var self = this; + this.deleteTab = function() { + return TabsEdit.prototype.deleteTab.apply(self, arguments); + }; + this.addNewTab = function() { + return TabsEdit.prototype.addNewTab.apply(self, arguments); + }; + this.tabMoved = function() { + return TabsEdit.prototype.tabMoved.apply(self, arguments); + }; + this.toggleVisibilityOfTab = function() { + return TabsEdit.prototype.toggleVisibilityOfTab.apply(self, arguments); + }; + this.initialize = function() { + return TabsEdit.prototype.initialize.apply(self, arguments); + }; + return TabsEdit.__super__.constructor.apply(this, arguments); + } + + TabsEdit.prototype.initialize = function(options) { + var self = this; + this.$('.component').each(function(idx, element) { + var model; + model = new ModuleModel({ + id: $(element).data('locator') + }); + return new ModuleEditView({ + el: element, + onDelete: self.deleteTab, + model: model + }); + }); + this.options = _.extend({}, options); + this.options.mast.find('.new-tab').on('click', this.addNewTab); + $('.add-pages .new-tab').on('click', this.addNewTab); + $('.toggle-checkbox').on('click', this.toggleVisibilityOfTab); + return this.$('.course-nav-list').sortable({ + handle: '.drag-handle', + update: this.tabMoved, + helper: 'clone', + opacity: '0.5', + placeholder: 'component-placeholder', + forcePlaceholderSize: true, + axis: 'y', + items: '> .is-movable' + }); + }; + + TabsEdit.prototype.toggleVisibilityOfTab = function(event) { + var checkbox_element, saving, tab_element; + checkbox_element = event.target; + tab_element = $(checkbox_element).parents('.course-tab')[0]; + saving = new NotificationView.Mini({ + title: gettext('Saving') + }); + saving.show(); + return $.ajax({ + type: 'POST', + url: this.model.url(), + data: JSON.stringify({ + tab_id_locator: { + tab_id: $(tab_element).data('tab-id'), + tab_locator: $(tab_element).data('locator') + }, + is_hidden: $(checkbox_element).is(':checked') + }), + contentType: 'application/json' + }).success(function() { + return saving.hide(); + }); + }; + + TabsEdit.prototype.tabMoved = function() { + var saving, tabs; + tabs = []; + this.$('.course-tab').each(function(idx, element) { + return tabs.push({ + tab_id: $(element).data('tab-id'), + tab_locator: $(element).data('locator') + }); + }); + analytics.track('Reordered Pages', { + course: course_location_analytics + }); + saving = new NotificationView.Mini({ + title: gettext('Saving') + }); + saving.show(); + return $.ajax({ + type: 'POST', + url: this.model.url(), + data: JSON.stringify({ + tabs: tabs + }), + contentType: 'application/json' + }).success(function() { + return saving.hide(); + }); + }; + + TabsEdit.prototype.addNewTab = function(event) { + var editor; + event.preventDefault(); + editor = new ModuleEditView({ + onDelete: this.deleteTab, + model: new ModuleModel() + }); + $('.new-component-item').before(editor.$el); + editor.$el.addClass('course-tab is-movable'); + editor.$el.addClass('new'); + setTimeout(function() { + return editor.$el.removeClass('new'); + }, 1000); + $('html, body').animate({ + scrollTop: $('.new-component-item').offset().top + }, 500); + editor.createItem(this.model.get('id'), { + category: 'static_tab' + }); + return analytics.track('Added Page', { + course: course_location_analytics + }); + }; + + TabsEdit.prototype.deleteTab = function(event) { + var confirm; + confirm = new PromptView.Warning({ + title: gettext('Delete Page Confirmation'), + message: gettext('Are you sure you want to delete this page? This action cannot be undone.'), + actions: { + primary: { + text: gettext('OK'), + click: function(view) { + var $component, deleting; + view.hide(); + $component = $(event.currentTarget).parents('.component'); + analytics.track('Deleted Page', { + course: course_location_analytics, + id: $component.data('locator') + }); + deleting = new NotificationView.Mini({ + title: gettext('Deleting') + }); + deleting.show(); + return $.ajax({ + type: 'DELETE', + url: ModuleUtils.getUpdateUrl($component.data('locator')) + }).success(function() { + $component.remove(); + return deleting.hide(); + }); + } + }, + secondary: [ + { + text: gettext('Cancel'), + click: function(view) { + return view.hide(); + } + } + ] + } + }); + return confirm.show(); + }; + + return TabsEdit; + + })(Backbone.View); + return TabsEdit; + }); + +}).call(this, analytics, course_location_analytics); //jshint ignore:line diff --git a/cms/static/karma_cms.conf.js b/cms/static/karma_cms.conf.js index edfb65d9f4..f2ef478cd3 100644 --- a/cms/static/karma_cms.conf.js +++ b/cms/static/karma_cms.conf.js @@ -21,11 +21,13 @@ var options = { // Make sure the patterns in sourceFiles and specFiles do not match the same file. // Otherwise Istanbul which is used for coverage tracking will cause tests to not run. sourceFiles: [ + {pattern: 'cms/**/!(*spec|djangojs).js'}, {pattern: 'coffee/src/**/!(*spec).js'}, {pattern: 'js/**/!(*spec|djangojs).js'} ], specFiles: [ + {pattern: 'cms/**/*spec.js'}, {pattern: 'coffee/spec/**/*spec.js'}, {pattern: 'js/certificates/spec/**/*spec.js'}, {pattern: 'js/spec/**/*spec.js'} @@ -37,10 +39,10 @@ var options = { ], runFiles: [ - {pattern: 'coffee/spec/main.js', included: true} + {pattern: 'cms/js/spec/main.js', included: true} ] }; -module.exports = function (config) { +module.exports = function(config) { configModule.configure(config, options); }; diff --git a/cms/static/karma_cms_squire.conf.js b/cms/static/karma_cms_squire.conf.js index 093e51bb55..d8bdda0c98 100644 --- a/cms/static/karma_cms_squire.conf.js +++ b/cms/static/karma_cms_squire.conf.js @@ -36,7 +36,7 @@ var options = { ], runFiles: [ - {pattern: 'coffee/spec/main_squire.js', included: true} + {pattern: 'cms/js/spec/main_squire.js', included: true} ] }; diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 8da5903138..3fd284bdea 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -36,8 +36,8 @@ body, input, button { font-family: 'Open Sans', sans-serif; } -// we want to hide the outline on the focusable
element -main { +// removing the outline on any element that we make programmatically focusable +[tabindex="-1"] { outline: none; } diff --git a/cms/templates/js/content-group-details.underscore b/cms/templates/js/content-group-details.underscore index 041439f7e4..5827c0f9c8 100644 --- a/cms/templates/js/content-group-details.underscore +++ b/cms/templates/js/content-group-details.underscore @@ -32,7 +32,7 @@ <% } else { %>
  • - +
  • <% } %> diff --git a/cms/templates/js/group-configuration-details.underscore b/cms/templates/js/group-configuration-details.underscore index d6e989bab8..8310a2a657 100644 --- a/cms/templates/js/group-configuration-details.underscore +++ b/cms/templates/js/group-configuration-details.underscore @@ -50,7 +50,7 @@ <% } else { %>
  • - +
  • <% } %> diff --git a/common/test/data/toy/static/sample_static.txt b/common/djangoapps/config_models/management/__init__.py similarity index 100% rename from common/test/data/toy/static/sample_static.txt rename to common/djangoapps/config_models/management/__init__.py diff --git a/common/djangoapps/config_models/management/commands/__init__.py b/common/djangoapps/config_models/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/config_models/management/commands/populate_model.py b/common/djangoapps/config_models/management/commands/populate_model.py new file mode 100644 index 0000000000..d41ceae58b --- /dev/null +++ b/common/djangoapps/config_models/management/commands/populate_model.py @@ -0,0 +1,72 @@ +""" +Populates a ConfigurationModel by deserializing JSON data contained in a file. +""" +import os +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError +from django.utils.translation import ugettext_lazy as _ + +from config_models.utils import deserialize_json + + +class Command(BaseCommand): + """ + This command will deserialize the JSON data in the supplied file to populate + a ConfigurationModel. Note that this will add new entries to the model, but it + will not delete any entries (ConfigurationModel entries are read-only). + """ + help = """ + Populates a ConfigurationModel by deserializing the supplied JSON. + + JSON should be in a file, with the following format: + + { "model": "config_models.ExampleConfigurationModel", + "data": + [ + { "enabled": True, + "color": "black" + ... + }, + { "enabled": False, + "color": "yellow" + ... + }, + ... + ] + } + + A username corresponding to an existing user must be specified to indicate who + is executing the command. + + $ ... populate_model -f path/to/file.json -u username + """ + + option_list = BaseCommand.option_list + ( + make_option('-f', '--file', + metavar='JSON_FILE', + dest='file', + default=False, + help='JSON file to import ConfigurationModel data'), + make_option('-u', '--username', + metavar='USERNAME', + dest='username', + default=False, + help='username to specify who is executing the command'), + ) + + def handle(self, *args, **options): + if 'file' not in options or not options['file']: + raise CommandError(_("A file containing JSON must be specified.")) + + if 'username' not in options or not options['username']: + raise CommandError(_("A valid username must be specified.")) + + json_file = options['file'] + if not os.path.exists(json_file): + raise CommandError(_("File {0} does not exist").format(json_file)) + + self.stdout.write(_("Importing JSON data from file {0}").format(json_file)) + with open(json_file) as data: + created_entries = deserialize_json(data, options['username']) + self.stdout.write(_("Import complete, {0} new entries created").format(created_entries)) diff --git a/common/djangoapps/config_models/models.py b/common/djangoapps/config_models/models.py index 5528f084cf..ab429c701d 100644 --- a/common/djangoapps/config_models/models.py +++ b/common/djangoapps/config_models/models.py @@ -6,6 +6,9 @@ from django.contrib.auth.models import User from django.core.cache import caches, InvalidCacheBackendError from django.utils.translation import ugettext_lazy as _ +from rest_framework.utils import model_meta + + try: cache = caches['configuration'] # pylint: disable=invalid-name except InvalidCacheBackendError: @@ -176,3 +179,58 @@ class ConfigurationModel(models.Model): values = list(cls.objects.values_list(*key_fields, flat=flat).order_by().distinct()) cache.set(cache_key, values, cls.cache_timeout) return values + + def fields_equal(self, instance, fields_to_ignore=("id", "change_date", "changed_by")): + """ + Compares this instance's fields to the supplied instance to test for equality. + This will ignore any fields in `fields_to_ignore`. + + Note that this method ignores many-to-many fields. + + Args: + instance: the model instance to compare + fields_to_ignore: List of fields that should not be compared for equality. By default + includes `id`, `change_date`, and `changed_by`. + + Returns: True if the checked fields are all equivalent, else False + """ + for field in self._meta.get_fields(): + if not field.many_to_many and field.name not in fields_to_ignore: + if getattr(instance, field.name) != getattr(self, field.name): + return False + + return True + + @classmethod + def equal_to_current(cls, json, fields_to_ignore=("id", "change_date", "changed_by")): + """ + Compares for equality this instance to a model instance constructed from the supplied JSON. + This will ignore any fields in `fields_to_ignore`. + + Note that this method cannot handle fields with many-to-many associations, as those can only + be set on a saved model instance (and saving the model instance will create a new entry). + All many-to-many field entries will be removed before the equality comparison is done. + + Args: + json: json representing an entry to compare + fields_to_ignore: List of fields that should not be compared for equality. By default + includes `id`, `change_date`, and `changed_by`. + + Returns: True if the checked fields are all equivalent, else False + """ + + # Remove many-to-many relationships from json. + # They require an instance to be already saved. + info = model_meta.get_field_info(cls) + for field_name, relation_info in info.relations.items(): + if relation_info.to_many and (field_name in json): + json.pop(field_name) + + new_instance = cls(**json) + key_field_args = tuple(getattr(new_instance, key) for key in cls.KEY_FIELDS) + current = cls.current(*key_field_args) + # If current.id is None, no entry actually existed and the "current" method created it. + if current.id is not None: + return current.fields_equal(new_instance, fields_to_ignore) + + return False diff --git a/common/djangoapps/config_models/tests/__init__.py b/common/djangoapps/config_models/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/config_models/tests/data/data.json b/common/djangoapps/config_models/tests/data/data.json new file mode 100644 index 0000000000..e6977c7d54 --- /dev/null +++ b/common/djangoapps/config_models/tests/data/data.json @@ -0,0 +1,14 @@ +{ + "model": "config_models.ExampleDeserializeConfig", + "data": [ + { + "name": "betty", + "enabled": true, + "int_field": 5 + }, + { + "name": "fred", + "enabled": false + } + ] +} diff --git a/common/djangoapps/config_models/tests/test_model_deserialization.py b/common/djangoapps/config_models/tests/test_model_deserialization.py new file mode 100644 index 0000000000..af1ff81e49 --- /dev/null +++ b/common/djangoapps/config_models/tests/test_model_deserialization.py @@ -0,0 +1,219 @@ +""" +Tests of the populate_model management command and its helper utils.deserialize_json method. +""" + +import textwrap +import os.path + +from django.utils import timezone +from django.utils.six import BytesIO + +from django.contrib.auth.models import User +from django.core.management.base import CommandError +from django.db import models + +from config_models.management.commands import populate_model +from config_models.models import ConfigurationModel +from config_models.utils import deserialize_json +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase + + +class ExampleDeserializeConfig(ConfigurationModel): + """ + Test model for testing deserialization of ``ConfigurationModels`` with keyed configuration. + """ + KEY_FIELDS = ('name',) + + name = models.TextField() + int_field = models.IntegerField(default=10) + + def __unicode__(self): + return "ExampleDeserializeConfig(enabled={}, name={}, int_field={})".format( + self.enabled, self.name, self.int_field + ) + + +class DeserializeJSONTests(CacheIsolationTestCase): + """ + Tests of deserializing the JSON representation of ConfigurationModels. + """ + def setUp(self): + super(DeserializeJSONTests, self).setUp() + self.test_username = 'test_worker' + User.objects.create_user(username=self.test_username) + self.fixture_path = os.path.join(os.path.dirname(__file__), 'data', 'data.json') + + def test_deserialize_models(self): + """ + Tests the "happy path", where 2 instances of the test model should be created. + A valid username is supplied for the operation. + """ + start_date = timezone.now() + with open(self.fixture_path) as data: + entries_created = deserialize_json(data, self.test_username) + self.assertEquals(2, entries_created) + + self.assertEquals(2, ExampleDeserializeConfig.objects.count()) + + betty = ExampleDeserializeConfig.current('betty') + self.assertTrue(betty.enabled) + self.assertEquals(5, betty.int_field) + self.assertGreater(betty.change_date, start_date) + self.assertEquals(self.test_username, betty.changed_by.username) + + fred = ExampleDeserializeConfig.current('fred') + self.assertFalse(fred.enabled) + self.assertEquals(10, fred.int_field) + self.assertGreater(fred.change_date, start_date) + self.assertEquals(self.test_username, fred.changed_by.username) + + def test_existing_entries_not_removed(self): + """ + Any existing configuration model entries are retained + (though they may be come history)-- deserialize_json is purely additive. + """ + ExampleDeserializeConfig(name="fred", enabled=True).save() + ExampleDeserializeConfig(name="barney", int_field=200).save() + + with open(self.fixture_path) as data: + entries_created = deserialize_json(data, self.test_username) + self.assertEquals(2, entries_created) + + self.assertEquals(4, ExampleDeserializeConfig.objects.count()) + self.assertEquals(3, len(ExampleDeserializeConfig.objects.current_set())) + + self.assertEquals(5, ExampleDeserializeConfig.current('betty').int_field) + self.assertEquals(200, ExampleDeserializeConfig.current('barney').int_field) + + # The JSON file changes "enabled" to False for Fred. + fred = ExampleDeserializeConfig.current('fred') + self.assertFalse(fred.enabled) + + def test_duplicate_entries_not_made(self): + """ + If there is no change in an entry (besides changed_by and change_date), + a new entry is not made. + """ + with open(self.fixture_path) as data: + entries_created = deserialize_json(data, self.test_username) + self.assertEquals(2, entries_created) + + with open(self.fixture_path) as data: + entries_created = deserialize_json(data, self.test_username) + self.assertEquals(0, entries_created) + + # Importing twice will still only result in 2 records (second import a no-op). + self.assertEquals(2, ExampleDeserializeConfig.objects.count()) + + # Change Betty. + betty = ExampleDeserializeConfig.current('betty') + betty.int_field = -8 + betty.save() + + self.assertEquals(3, ExampleDeserializeConfig.objects.count()) + self.assertEquals(-8, ExampleDeserializeConfig.current('betty').int_field) + + # Now importing will add a new entry for Betty. + with open(self.fixture_path) as data: + entries_created = deserialize_json(data, self.test_username) + self.assertEquals(1, entries_created) + + self.assertEquals(4, ExampleDeserializeConfig.objects.count()) + self.assertEquals(5, ExampleDeserializeConfig.current('betty').int_field) + + def test_bad_username(self): + """ + Tests the error handling when the specified user does not exist. + """ + test_json = textwrap.dedent(""" + { + "model": "config_models.ExampleDeserializeConfig", + "data": [{"name": "dino"}] + } + """) + with self.assertRaisesRegexp(Exception, "User matching query does not exist"): + deserialize_json(BytesIO(test_json), "unknown_username") + + def test_invalid_json(self): + """ + Tests the error handling when there is invalid JSON. + """ + test_json = textwrap.dedent(""" + { + "model": "config_models.ExampleDeserializeConfig", + "data": [{"name": "dino" + """) + with self.assertRaisesRegexp(Exception, "JSON parse error"): + deserialize_json(BytesIO(test_json), self.test_username) + + def test_invalid_model(self): + """ + Tests the error handling when the configuration model specified does not exist. + """ + test_json = textwrap.dedent(""" + { + "model": "xxx.yyy", + "data":[{"name": "dino"}] + } + """) + with self.assertRaisesRegexp(Exception, "No installed app"): + deserialize_json(BytesIO(test_json), self.test_username) + + +class PopulateModelTestCase(CacheIsolationTestCase): + """ + Tests of populate model management command. + """ + def setUp(self): + super(PopulateModelTestCase, self).setUp() + self.file_path = os.path.join(os.path.dirname(__file__), 'data', 'data.json') + self.test_username = 'test_management_worker' + User.objects.create_user(username=self.test_username) + + def test_run_command(self): + """ + Tests the "happy path", where 2 instances of the test model should be created. + A valid username is supplied for the operation. + """ + _run_command(file=self.file_path, username=self.test_username) + self.assertEquals(2, ExampleDeserializeConfig.objects.count()) + + betty = ExampleDeserializeConfig.current('betty') + self.assertEquals(self.test_username, betty.changed_by.username) + + fred = ExampleDeserializeConfig.current('fred') + self.assertEquals(self.test_username, fred.changed_by.username) + + def test_no_user_specified(self): + """ + Tests that a username must be specified. + """ + with self.assertRaisesRegexp(CommandError, "A valid username must be specified"): + _run_command(file=self.file_path) + + def test_bad_user_specified(self): + """ + Tests that a username must be specified. + """ + with self.assertRaisesRegexp(Exception, "User matching query does not exist"): + _run_command(file=self.file_path, username="does_not_exist") + + def test_no_file_specified(self): + """ + Tests the error handling when no JSON file is supplied. + """ + with self.assertRaisesRegexp(CommandError, "A file containing JSON must be specified"): + _run_command(username=self.test_username) + + def test_bad_file_specified(self): + """ + Tests the error handling when the path to the JSON file is incorrect. + """ + with self.assertRaisesRegexp(CommandError, "File does/not/exist.json does not exist"): + _run_command(file="does/not/exist.json", username=self.test_username) + + +def _run_command(*args, **kwargs): + """Run the management command to deserializer JSON ConfigurationModel data. """ + command = populate_model.Command() + return command.handle(*args, **kwargs) diff --git a/common/djangoapps/config_models/tests.py b/common/djangoapps/config_models/tests/tests.py similarity index 73% rename from common/djangoapps/config_models/tests.py rename to common/djangoapps/config_models/tests/tests.py index 15f954e336..538058c109 100644 --- a/common/djangoapps/config_models/tests.py +++ b/common/djangoapps/config_models/tests/tests.py @@ -25,6 +25,24 @@ class ExampleConfig(ConfigurationModel): string_field = models.TextField() int_field = models.IntegerField(default=10) + def __unicode__(self): + return "ExampleConfig(enabled={}, string_field={}, int_field={})".format( + self.enabled, self.string_field, self.int_field + ) + + +class ManyToManyExampleConfig(ConfigurationModel): + """ + Test model configuration with a many-to-many field. + """ + cache_timeout = 300 + + string_field = models.TextField() + many_user_field = models.ManyToManyField(User, related_name='topic_many_user_field') + + def __unicode__(self): + return "ManyToManyExampleConfig(enabled={}, string_field={})".format(self.enabled, self.string_field) + @patch('config_models.models.cache') class ConfigurationModelTests(TestCase): @@ -40,7 +58,7 @@ class ConfigurationModelTests(TestCase): ExampleConfig(changed_by=self.user).save() mock_cache.delete.assert_called_with(ExampleConfig.cache_key_name()) - def test_cache_key_name(self, _mock_cache): + def test_cache_key_name(self, __): self.assertEquals(ExampleConfig.cache_key_name(), 'configuration/ExampleConfig/current') def test_no_config_empty_cache(self, mock_cache): @@ -103,6 +121,64 @@ class ConfigurationModelTests(TestCase): self.assertEquals(2, ExampleConfig.objects.all().count()) + def test_equality(self, mock_cache): + mock_cache.get.return_value = None + + config = ExampleConfig(changed_by=self.user, string_field='first') + config.save() + + self.assertTrue(ExampleConfig.equal_to_current({"string_field": "first"})) + self.assertTrue(ExampleConfig.equal_to_current({"string_field": "first", "enabled": False})) + self.assertTrue(ExampleConfig.equal_to_current({"string_field": "first", "int_field": 10})) + + self.assertFalse(ExampleConfig.equal_to_current({"string_field": "first", "enabled": True})) + self.assertFalse(ExampleConfig.equal_to_current({"string_field": "first", "int_field": 20})) + self.assertFalse(ExampleConfig.equal_to_current({"string_field": "second"})) + + self.assertFalse(ExampleConfig.equal_to_current({})) + + def test_equality_custom_fields_to_ignore(self, mock_cache): + mock_cache.get.return_value = None + + config = ExampleConfig(changed_by=self.user, string_field='first') + config.save() + + # id, change_date, and changed_by will all be different for a newly created entry + self.assertTrue(ExampleConfig.equal_to_current({"string_field": "first"})) + self.assertFalse( + ExampleConfig.equal_to_current({"string_field": "first"}, fields_to_ignore=("change_date", "changed_by")) + ) + self.assertFalse( + ExampleConfig.equal_to_current({"string_field": "first"}, fields_to_ignore=("id", "changed_by")) + ) + self.assertFalse( + ExampleConfig.equal_to_current({"string_field": "first"}, fields_to_ignore=("change_date", "id")) + ) + + # Test the ability to ignore a different field ("int_field"). + self.assertFalse(ExampleConfig.equal_to_current({"string_field": "first", "int_field": 20})) + self.assertTrue( + ExampleConfig.equal_to_current( + {"string_field": "first", "int_field": 20}, + fields_to_ignore=("id", "change_date", "changed_by", "int_field") + ) + ) + + def test_equality_ignores_many_to_many(self, mock_cache): + mock_cache.get.return_value = None + config = ManyToManyExampleConfig(changed_by=self.user, string_field='first') + config.save() + + second_user = User(username="second_user") + second_user.save() + config.many_user_field.add(second_user) # pylint: disable=no-member + config.save() + + # The many-to-many field is ignored in comparison. + self.assertTrue( + ManyToManyExampleConfig.equal_to_current({"string_field": "first", "many_user_field": "removed"}) + ) + class ExampleKeyedConfig(ConfigurationModel): """ @@ -120,6 +196,11 @@ class ExampleKeyedConfig(ConfigurationModel): string_field = models.TextField() int_field = models.IntegerField(default=10) + def __unicode__(self): + return "ExampleKeyedConfig(enabled={}, left={}, right={}, string_field={}, int_field={})".format( + self.enabled, self.left, self.right, self.string_field, self.int_field + ) + @ddt.ddt @patch('config_models.models.cache') @@ -294,6 +375,45 @@ class KeyedConfigurationModelTests(TestCase): mock_cache.get.return_value = fake_result self.assertEquals(ExampleKeyedConfig.key_values(), fake_result) + def test_equality(self, mock_cache): + mock_cache.get.return_value = None + + config1 = ExampleKeyedConfig(left='left_a', right='right_a', int_field=1, changed_by=self.user) + config1.save() + + config2 = ExampleKeyedConfig(left='left_b', right='right_b', int_field=2, changed_by=self.user, enabled=True) + config2.save() + + config3 = ExampleKeyedConfig(left='left_c', changed_by=self.user) + config3.save() + + self.assertTrue( + ExampleKeyedConfig.equal_to_current({"left": "left_a", "right": "right_a", "int_field": 1}) + ) + self.assertTrue( + ExampleKeyedConfig.equal_to_current({"left": "left_b", "right": "right_b", "int_field": 2, "enabled": True}) + ) + self.assertTrue( + ExampleKeyedConfig.equal_to_current({"left": "left_c"}) + ) + + self.assertFalse( + ExampleKeyedConfig.equal_to_current( + {"left": "left_a", "right": "right_a", "int_field": 1, "string_field": "foo"} + ) + ) + self.assertFalse( + ExampleKeyedConfig.equal_to_current({"left": "left_a", "int_field": 1}) + ) + self.assertFalse( + ExampleKeyedConfig.equal_to_current({"left": "left_b", "right": "right_b", "int_field": 2}) + ) + self.assertFalse( + ExampleKeyedConfig.equal_to_current({"left": "left_c", "int_field": 11}) + ) + + self.assertFalse(ExampleKeyedConfig.equal_to_current({})) + @ddt.ddt class ConfigurationModelAPITests(TestCase): diff --git a/common/djangoapps/config_models/utils.py b/common/djangoapps/config_models/utils.py new file mode 100644 index 0000000000..10a293af4d --- /dev/null +++ b/common/djangoapps/config_models/utils.py @@ -0,0 +1,69 @@ +""" +Utilities for working with ConfigurationModels. +""" +from django.apps import apps +from rest_framework.parsers import JSONParser +from rest_framework.serializers import ModelSerializer +from django.contrib.auth.models import User + + +def get_serializer_class(configuration_model): + """ Returns a ConfigurationModel serializer class for the supplied configuration_model. """ + class AutoConfigModelSerializer(ModelSerializer): + """Serializer class for configuration models.""" + + class Meta(object): + """Meta information for AutoConfigModelSerializer.""" + model = configuration_model + + def create(self, validated_data): + if "changed_by_username" in self.context: + validated_data['changed_by'] = User.objects.get(username=self.context["changed_by_username"]) + return super(AutoConfigModelSerializer, self).create(validated_data) + + return AutoConfigModelSerializer + + +def deserialize_json(stream, username): + """ + Given a stream containing JSON, deserializers the JSON into ConfigurationModel instances. + + The stream is expected to be in the following format: + { "model": "config_models.ExampleConfigurationModel", + "data": + [ + { "enabled": True, + "color": "black" + ... + }, + { "enabled": False, + "color": "yellow" + ... + }, + ... + ] + } + + If the provided stream does not contain valid JSON for the ConfigurationModel specified, + an Exception will be raised. + + Arguments: + stream: The stream of JSON, as described above. + username: The username of the user making the change. This must match an existing user. + + Returns: the number of created entries + """ + parsed_json = JSONParser().parse(stream) + serializer_class = get_serializer_class(apps.get_model(parsed_json["model"])) + list_serializer = serializer_class(data=parsed_json["data"], context={"changed_by_username": username}, many=True) + if list_serializer.is_valid(): + model_class = serializer_class.Meta.model + for data in reversed(list_serializer.validated_data): + if model_class.equal_to_current(data): + list_serializer.validated_data.remove(data) + + entries_created = len(list_serializer.validated_data) + list_serializer.save() + return entries_created + else: + raise Exception(list_serializer.error_messages) diff --git a/common/djangoapps/config_models/views.py b/common/djangoapps/config_models/views.py index 3bd693ec59..c9d584f9cb 100644 --- a/common/djangoapps/config_models/views.py +++ b/common/djangoapps/config_models/views.py @@ -4,9 +4,10 @@ API view to allow manipulation of configuration models. from rest_framework.generics import CreateAPIView, RetrieveAPIView from rest_framework.permissions import DjangoModelPermissions from rest_framework.authentication import SessionAuthentication -from rest_framework.serializers import ModelSerializer from django.db import transaction +from config_models.utils import get_serializer_class + class ReadableOnlyByAuthors(DjangoModelPermissions): """Only allow access by users with `add` permissions on the model.""" @@ -58,13 +59,7 @@ class ConfigurationModelCurrentAPIView(AtomicMixin, CreateAPIView, RetrieveAPIVi def get_serializer_class(self): if self.serializer_class is None: - class AutoConfigModelSerializer(ModelSerializer): - """Serializer class for configuration models.""" - class Meta(object): - """Meta information for AutoConfigModelSerializer.""" - model = self.model - - self.serializer_class = AutoConfigModelSerializer + self.serializer_class = get_serializer_class(self.model) return self.serializer_class diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py index 8b96c6b536..29eed2e048 100644 --- a/common/djangoapps/contentserver/middleware.py +++ b/common/djangoapps/contentserver/middleware.py @@ -3,12 +3,11 @@ Middleware to serve assets. """ import logging - import datetime import newrelic.agent from django.http import ( HttpResponse, HttpResponseNotModified, HttpResponseForbidden, - HttpResponseBadRequest, HttpResponseNotFound) + HttpResponseBadRequest, HttpResponseNotFound, HttpResponsePermanentRedirect) from student.models import CourseEnrollment from contentserver.models import CourseAssetCacheTtlConfig, CdnUserAgentsConfig @@ -30,32 +29,54 @@ HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" class StaticContentServer(object): + """ + Serves course assets to end users. Colloquially referred to as "contentserver." + """ def is_asset_request(self, request): """Determines whether the given request is an asset request""" return ( request.path.startswith('/' + XASSET_LOCATION_TAG + '/') or request.path.startswith('/' + AssetLocator.CANONICAL_NAMESPACE) + or + StaticContent.is_versioned_asset_path(request.path) ) def process_request(self, request): """Process the given request""" + asset_path = request.path + if self.is_asset_request(request): # Make sure we can convert this request into a location. - if AssetLocator.CANONICAL_NAMESPACE in request.path: - request.path = request.path.replace('block/', 'block@', 1) + if AssetLocator.CANONICAL_NAMESPACE in asset_path: + asset_path = asset_path.replace('block/', 'block@', 1) + + # If this is a versioned request, pull out the digest and chop off the prefix. + requested_digest = None + if StaticContent.is_versioned_asset_path(asset_path): + requested_digest, asset_path = StaticContent.parse_versioned_asset_path(asset_path) + + # Make sure we have a valid location value for this asset. try: - loc = StaticContent.get_location_from_path(request.path) + loc = StaticContent.get_location_from_path(asset_path) except (InvalidLocationError, InvalidKeyError): return HttpResponseBadRequest() - # Try and load the asset. - content = None + # Attempt to load the asset to make sure it exists, and grab the asset digest + # if we're able to load it. + actual_digest = None try: content = self.load_asset_from_location(loc) + actual_digest = getattr(content, "content_digest", None) except (ItemNotFoundError, NotFoundError): return HttpResponseNotFound() + # If this was a versioned asset, and the digest doesn't match, redirect + # them to the actual version. + if requested_digest is not None and actual_digest is not None and (actual_digest != requested_digest): + actual_asset_path = StaticContent.add_version_to_asset_path(asset_path, actual_digest) + return HttpResponsePermanentRedirect(actual_asset_path) + # Set the basics for this request. Make sure that the course key for this # asset has a run, which old-style courses do not. Otherwise, this will # explode when the key is serialized to be sent to NR. diff --git a/common/djangoapps/contentserver/test/test_contentserver.py b/common/djangoapps/contentserver/test/test_contentserver.py index c68bc5a63c..52bdf83d2d 100644 --- a/common/djangoapps/contentserver/test/test_contentserver.py +++ b/common/djangoapps/contentserver/test/test_contentserver.py @@ -16,9 +16,13 @@ from django.test.utils import override_settings from mock import patch from xmodule.contentstore.django import contentstore +from xmodule.contentstore.content import StaticContent from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.xml_importer import import_course_from_xml +from xmodule.assetstore.assetmgr import AssetManager +from opaque_keys import InvalidKeyError +from xmodule.modulestore.exceptions import ItemNotFoundError from contentserver.middleware import parse_range_header, HTTP_DATE_FORMAT, StaticContentServer from student.models import CourseEnrollment @@ -28,9 +32,24 @@ log = logging.getLogger(__name__) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex - TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT +FAKE_MD5_HASH = 'ffffffffffffffffffffffffffffffff' + + +def get_versioned_asset_url(asset_path): + """ + Creates a versioned asset URL. + """ + try: + locator = StaticContent.get_location_from_path(asset_path) + content = AssetManager.find(locator, as_stream=True) + return StaticContent.add_version_to_asset_path(asset_path, content.content_digest) + except (InvalidKeyError, ItemNotFoundError): + pass + + return asset_path + @ddt.ddt @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) @@ -54,13 +73,15 @@ class ContentStoreToyCourseTest(SharedModuleStoreTestCase): ) # A locked asset - cls.locked_asset = cls.course_key.make_asset_key('asset', 'sample_static.txt') + cls.locked_asset = cls.course_key.make_asset_key('asset', 'sample_static.html') cls.url_locked = unicode(cls.locked_asset) + cls.url_locked_versioned = get_versioned_asset_url(cls.url_locked) cls.contentstore.set_attr(cls.locked_asset, 'locked', True) # An unlocked asset cls.unlocked_asset = cls.course_key.make_asset_key('asset', 'another_static.txt') cls.url_unlocked = unicode(cls.unlocked_asset) + cls.url_unlocked_versioned = get_versioned_asset_url(cls.url_unlocked) cls.length_unlocked = cls.contentstore.get_attr(cls.unlocked_asset, 'length') def setUp(self): @@ -81,6 +102,37 @@ class ContentStoreToyCourseTest(SharedModuleStoreTestCase): resp = self.client.get(self.url_unlocked) self.assertEqual(resp.status_code, 200) + def test_unlocked_versioned_asset(self): + """ + Test that unlocked assets that are versioned are being served. + """ + self.client.logout() + resp = self.client.get(self.url_unlocked_versioned) + self.assertEqual(resp.status_code, 200) + + def test_unlocked_versioned_asset_with_nonexistent_version(self): + """ + Test that unlocked assets that are versioned, but have a nonexistent version, + are sent back as a 301 redirect which tells the caller the correct URL. + """ + url_unlocked_versioned_old = StaticContent.add_version_to_asset_path(self.url_unlocked, FAKE_MD5_HASH) + + self.client.logout() + resp = self.client.get(url_unlocked_versioned_old) + self.assertEqual(resp.status_code, 301) + self.assertTrue(resp.url.endswith(self.url_unlocked_versioned)) # pylint: disable=no-member + + def test_locked_versioned_asset(self): + """ + Test that locked assets that are versioned are being served. + """ + CourseEnrollment.enroll(self.non_staff_usr, self.course_key) + self.assertTrue(CourseEnrollment.is_enrolled(self.non_staff_usr, self.course_key)) + + self.client.login(username=self.non_staff_usr, password='test') + resp = self.client.get(self.url_locked_versioned) + self.assertEqual(resp.status_code, 200) + def test_locked_asset_not_logged_in(self): """ Test that locked assets behave appropriately in case the user is not diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index 4d623b5390..1f3a73788c 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -166,7 +166,7 @@ else: %> <%def name="show_language_selector()"><% - return settings.FEATURES.get('SHOW_LANGUAGE_SELECTOR', False) + return get_value('SHOW_LANGUAGE_SELECTOR', settings.FEATURES.get('SHOW_LANGUAGE_SELECTOR', False)) %> <%def name="get_released_languages()"><% diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index d98bea1b90..e2a8fe5069 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -13,6 +13,7 @@ from xmodule.contentstore.content import StaticContent from opaque_keys.edx.locator import AssetLocator log = logging.getLogger(__name__) +XBLOCK_STATIC_RESOURCE_PREFIX = '/static/xblock' def _url_replace_regex(prefix): @@ -109,6 +110,13 @@ def process_static_urls(text, replacement_function, data_dir=None): prefix = match.group('prefix') quote = match.group('quote') rest = match.group('rest') + + # Don't rewrite XBlock resource links. Probably wasn't a good idea that /static + # works for actual static assets and for magical course asset URLs.... + full_url = prefix + rest + if full_url.startswith(XBLOCK_STATIC_RESOURCE_PREFIX): + return original + return replacement_function(original, prefix, quote, rest) return re.sub( diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py index bc203a85bc..04a562a771 100644 --- a/common/djangoapps/static_replace/test/test_static_replace.py +++ b/common/djangoapps/static_replace/test/test_static_replace.py @@ -1,12 +1,13 @@ +# -*- coding: utf-8 -*- """Tests for static_replace""" -from urllib import quote_plus - import ddt import re + +from django.utils.http import urlquote, urlencode +from urlparse import urlparse, urlunparse, parse_qsl from PIL import Image from cStringIO import StringIO - from nose.tools import assert_equals, assert_true, assert_false # pylint: disable=no-name-in-module from static_replace import ( replace_static_urls, @@ -16,7 +17,6 @@ from static_replace import ( make_static_urls_absolute ) from mock import patch, Mock - from opaque_keys.edx.locations import SlashSeparatedCourseKey from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore @@ -25,12 +25,29 @@ from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls from xmodule.modulestore.xml import XMLModuleStore +from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.exceptions import NotFoundError +from xmodule.assetstore.assetmgr import AssetManager DATA_DIRECTORY = 'data_dir' COURSE_KEY = SlashSeparatedCourseKey('org', 'course', 'run') STATIC_SOURCE = '"/static/file.png"' +def encode_unicode_characters_in_url(url): + """ + Encodes all Unicode characters to their percent-encoding representation + in both the path portion and query parameter portion of the given URL. + """ + scheme, netloc, path, params, query, fragment = urlparse(url) + query_params = parse_qsl(query) + updated_query_params = [] + for query_name, query_val in query_params: + updated_query_params.append((query_name, urlquote(query_val))) + + return urlunparse((scheme, netloc, urlquote(path, '/:+@'), params, urlencode(query_params), fragment)) + + def test_multi_replace(): course_source = '"/course/file.png"' @@ -179,6 +196,21 @@ def test_regex(): assert_false(re.match(regex, s)) +@patch('static_replace.staticfiles_storage', autospec=True) +@patch('static_replace.modulestore', autospec=True) +def test_static_url_with_xblock_resource(mock_modulestore, mock_storage): + """ + Make sure that for URLs with XBlock resource URL, which start with /static/, + we don't rewrite them. + """ + mock_storage.exists.return_value = False + mock_modulestore.return_value = Mock(MongoModuleStore) + + pre_text = 'EMBED src ="/static/xblock/resources/babys_first.lil_xblock/public/images/pacifier.png"' + post_text = pre_text + assert_equals(post_text, replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY)) + + @ddt.ddt class CanonicalContentTest(SharedModuleStoreTestCase): """ @@ -202,7 +234,7 @@ class CanonicalContentTest(SharedModuleStoreTestCase): cls.courses[prefix] = CourseFactory.create(org='a', course='b', run=prefix) # Create an unlocked image. - unlock_content = cls.create_image(prefix, (32, 32), 'blue', '{}_unlock.png') + unlock_content = cls.create_image(prefix, (32, 32), 'blue', u'{}_ünlöck.png') # Create a locked image. lock_content = cls.create_image(prefix, (32, 32), 'green', '{}_lock.png', locked=True) @@ -212,14 +244,14 @@ class CanonicalContentTest(SharedModuleStoreTestCase): contentstore().generate_thumbnail(lock_content, dimensions=(16, 16)) # Create an unlocked image in a subdirectory. - cls.create_image(prefix, (1, 1), 'red', 'special/{}_unlock.png') + cls.create_image(prefix, (1, 1), 'red', u'special/{}_ünlöck.png') # Create a locked image in a subdirectory. cls.create_image(prefix, (1, 1), 'yellow', 'special/{}_lock.png', locked=True) # Create an unlocked image with funky characters in the name. - cls.create_image(prefix, (1, 1), 'black', 'weird {}_unlock.png') - cls.create_image(prefix, (1, 1), 'black', 'special/weird {}_unlock.png') + cls.create_image(prefix, (1, 1), 'black', u'weird {}_ünlöck.png') + cls.create_image(prefix, (1, 1), 'black', u'special/weird {}_ünlöck.png') # Create an HTML file to test extension exclusion, and create a control file. cls.create_arbitrary_content(prefix, '{}_not_excluded.htm') @@ -227,6 +259,24 @@ class CanonicalContentTest(SharedModuleStoreTestCase): cls.create_arbitrary_content(prefix, 'special/{}_not_excluded.htm') cls.create_arbitrary_content(prefix, 'special/{}_excluded.html') + @classmethod + def get_content_digest_for_asset_path(cls, prefix, path): + """ + Takes an unprocessed asset path, parses it just enough to try and find the + asset it refers to, and returns the content digest of that asset if it exists. + """ + + # Parse the path as if it was potentially a relative URL with query parameters, + # or an absolute URL, etc. Only keep the path because that's all we need. + _, _, relative_path, _, _, _ = urlparse(path) + asset_key = StaticContent.get_asset_key_from_path(cls.courses[prefix].id, relative_path) + + try: + content = AssetManager.find(asset_key, as_stream=True) + return content.content_digest + except (ItemNotFoundError, NotFoundError): + return None + @classmethod def create_image(cls, prefix, dimensions, color, name, locked=False): """ @@ -277,100 +327,100 @@ class CanonicalContentTest(SharedModuleStoreTestCase): @ddt.data( # No leading slash. - (u'', u'{prfx}_unlock.png', u'/{asset}@{prfx}_unlock.png', 1), + (u'', u'{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1), (u'', u'{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'', u'weird {prfx}_unlock.png', u'/{asset}@weird_{prfx}_unlock.png', 1), - (u'', u'{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), + (u'', u'weird {prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'', u'{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), (u'', u'{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1), - (u'dev', u'{prfx}_unlock.png', u'//dev/{asset}@{prfx}_unlock.png', 1), + (u'dev', u'{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1), (u'dev', u'{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'dev', u'weird {prfx}_unlock.png', u'//dev/{asset}@weird_{prfx}_unlock.png', 1), - (u'dev', u'{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), + (u'dev', u'weird {prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'dev', u'{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), (u'dev', u'{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1), # No leading slash with subdirectory. This ensures we properly substitute slashes. - (u'', u'special/{prfx}_unlock.png', u'/{asset}@special_{prfx}_unlock.png', 1), + (u'', u'special/{prfx}_ünlöck.png', u'/{asset}@special_{prfx}_ünlöck.png', 1), (u'', u'special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1), - (u'', u'special/weird {prfx}_unlock.png', u'/{asset}@special_weird_{prfx}_unlock.png', 1), - (u'', u'special/{prfx}_excluded.html', u'/{asset}@special_{prfx}_excluded.html', 1), + (u'', u'special/weird {prfx}_ünlöck.png', u'/{asset}@special_weird_{prfx}_ünlöck.png', 1), + (u'', u'special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1), (u'', u'special/{prfx}_not_excluded.htm', u'/{asset}@special_{prfx}_not_excluded.htm', 1), - (u'dev', u'special/{prfx}_unlock.png', u'//dev/{asset}@special_{prfx}_unlock.png', 1), + (u'dev', u'special/{prfx}_ünlöck.png', u'//dev/{asset}@special_{prfx}_ünlöck.png', 1), (u'dev', u'special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1), - (u'dev', u'special/weird {prfx}_unlock.png', u'//dev/{asset}@special_weird_{prfx}_unlock.png', 1), - (u'dev', u'special/{prfx}_excluded.html', u'/{asset}@special_{prfx}_excluded.html', 1), + (u'dev', u'special/weird {prfx}_ünlöck.png', u'//dev/{asset}@special_weird_{prfx}_ünlöck.png', 1), + (u'dev', u'special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1), (u'dev', u'special/{prfx}_not_excluded.htm', u'//dev/{asset}@special_{prfx}_not_excluded.htm', 1), # Leading slash. - (u'', u'/{prfx}_unlock.png', u'/{asset}@{prfx}_unlock.png', 1), + (u'', u'/{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1), (u'', u'/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'', u'/weird {prfx}_unlock.png', u'/{asset}@weird_{prfx}_unlock.png', 1), - (u'', u'/{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), + (u'', u'/weird {prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'', u'/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), (u'', u'/{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1), - (u'dev', u'/{prfx}_unlock.png', u'//dev/{asset}@{prfx}_unlock.png', 1), + (u'dev', u'/{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1), (u'dev', u'/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'dev', u'/weird {prfx}_unlock.png', u'//dev/{asset}@weird_{prfx}_unlock.png', 1), - (u'dev', u'/{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), + (u'dev', u'/weird {prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), (u'dev', u'/{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1), # Leading slash with subdirectory. This ensures we properly substitute slashes. - (u'', u'/special/{prfx}_unlock.png', u'/{asset}@special_{prfx}_unlock.png', 1), + (u'', u'/special/{prfx}_ünlöck.png', u'/{asset}@special_{prfx}_ünlöck.png', 1), (u'', u'/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1), - (u'', u'/special/weird {prfx}_unlock.png', u'/{asset}@special_weird_{prfx}_unlock.png', 1), - (u'', u'/special/{prfx}_excluded.html', u'/{asset}@special_{prfx}_excluded.html', 1), + (u'', u'/special/weird {prfx}_ünlöck.png', u'/{asset}@special_weird_{prfx}_ünlöck.png', 1), + (u'', u'/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1), (u'', u'/special/{prfx}_not_excluded.htm', u'/{asset}@special_{prfx}_not_excluded.htm', 1), - (u'dev', u'/special/{prfx}_unlock.png', u'//dev/{asset}@special_{prfx}_unlock.png', 1), + (u'dev', u'/special/{prfx}_ünlöck.png', u'//dev/{asset}@special_{prfx}_ünlöck.png', 1), (u'dev', u'/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1), - (u'dev', u'/special/weird {prfx}_unlock.png', u'//dev/{asset}@special_weird_{prfx}_unlock.png', 1), - (u'dev', u'/special/{prfx}_excluded.html', u'/{asset}@special_{prfx}_excluded.html', 1), + (u'dev', u'/special/weird {prfx}_ünlöck.png', u'//dev/{asset}@special_weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1), (u'dev', u'/special/{prfx}_not_excluded.htm', u'//dev/{asset}@special_{prfx}_not_excluded.htm', 1), # Static path. - (u'', u'/static/{prfx}_unlock.png', u'/{asset}@{prfx}_unlock.png', 1), + (u'', u'/static/{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1), (u'', u'/static/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'', u'/static/weird {prfx}_unlock.png', u'/{asset}@weird_{prfx}_unlock.png', 1), - (u'', u'/static/{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), + (u'', u'/static/weird {prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'', u'/static/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), (u'', u'/static/{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1), - (u'dev', u'/static/{prfx}_unlock.png', u'//dev/{asset}@{prfx}_unlock.png', 1), + (u'dev', u'/static/{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1), (u'dev', u'/static/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'dev', u'/static/weird {prfx}_unlock.png', u'//dev/{asset}@weird_{prfx}_unlock.png', 1), - (u'dev', u'/static/{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), + (u'dev', u'/static/weird {prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/static/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), (u'dev', u'/static/{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1), # Static path with subdirectory. This ensures we properly substitute slashes. - (u'', u'/static/special/{prfx}_unlock.png', u'/{asset}@special_{prfx}_unlock.png', 1), + (u'', u'/static/special/{prfx}_ünlöck.png', u'/{asset}@special_{prfx}_ünlöck.png', 1), (u'', u'/static/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1), - (u'', u'/static/special/weird {prfx}_unlock.png', u'/{asset}@special_weird_{prfx}_unlock.png', 1), - (u'', u'/static/special/{prfx}_excluded.html', u'/{asset}@special_{prfx}_excluded.html', 1), + (u'', u'/static/special/weird {prfx}_ünlöck.png', u'/{asset}@special_weird_{prfx}_ünlöck.png', 1), + (u'', u'/static/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1), (u'', u'/static/special/{prfx}_not_excluded.htm', u'/{asset}@special_{prfx}_not_excluded.htm', 1), - (u'dev', u'/static/special/{prfx}_unlock.png', u'//dev/{asset}@special_{prfx}_unlock.png', 1), + (u'dev', u'/static/special/{prfx}_ünlöck.png', u'//dev/{asset}@special_{prfx}_ünlöck.png', 1), (u'dev', u'/static/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1), - (u'dev', u'/static/special/weird {prfx}_unlock.png', u'//dev/{asset}@special_weird_{prfx}_unlock.png', 1), - (u'dev', u'/static/special/{prfx}_excluded.html', u'/{asset}@special_{prfx}_excluded.html', 1), + (u'dev', u'/static/special/weird {prfx}_ünlöck.png', u'//dev/{asset}@special_weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/static/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1), (u'dev', u'/static/special/{prfx}_not_excluded.htm', u'//dev/{asset}@special_{prfx}_not_excluded.htm', 1), # Static path with query parameter. ( u'', - u'/static/{prfx}_unlock.png?foo=/static/{prfx}_lock.png', - u'/{asset}@{prfx}_unlock.png?foo={encoded_asset}{prfx}_lock.png', + u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png', + u'/{asset}@{prfx}_ünlöck.png?foo={encoded_asset}{prfx}_lock.png', 2 ), ( u'', - u'/static/{prfx}_lock.png?foo=/static/{prfx}_unlock.png', - u'/{asset}@{prfx}_lock.png?foo={encoded_asset}{prfx}_unlock.png', + u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png', + u'/{asset}@{prfx}_lock.png?foo={encoded_asset}{prfx}_ünlöck.png', 2 ), ( u'', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html', - u'/{asset}@{prfx}_excluded.html?foo={encoded_asset}{prfx}_excluded.html', + u'/{base_asset}@{prfx}_excluded.html?foo={encoded_base_asset}{prfx}_excluded.html', 2 ), ( u'', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm', - u'/{asset}@{prfx}_excluded.html?foo={encoded_asset}{prfx}_not_excluded.htm', + u'/{base_asset}@{prfx}_excluded.html?foo={encoded_asset}{prfx}_not_excluded.htm', 2 ), ( u'', u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html', - u'/{asset}@{prfx}_not_excluded.htm?foo={encoded_asset}{prfx}_excluded.html', + u'/{asset}@{prfx}_not_excluded.htm?foo={encoded_base_asset}{prfx}_excluded.html', 2 ), ( @@ -381,32 +431,32 @@ class CanonicalContentTest(SharedModuleStoreTestCase): ), ( u'dev', - u'/static/{prfx}_unlock.png?foo=/static/{prfx}_lock.png', - u'//dev/{asset}@{prfx}_unlock.png?foo={encoded_asset}{prfx}_lock.png', + u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png', + u'//dev/{asset}@{prfx}_ünlöck.png?foo={encoded_asset}{prfx}_lock.png', 2 ), ( u'dev', - u'/static/{prfx}_lock.png?foo=/static/{prfx}_unlock.png', - u'/{asset}@{prfx}_lock.png?foo={encoded_base_url}{encoded_asset}{prfx}_unlock.png', + u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png', + u'/{asset}@{prfx}_lock.png?foo={encoded_base_url}{encoded_asset}{prfx}_ünlöck.png', 2 ), ( u'dev', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html', - u'/{asset}@{prfx}_excluded.html?foo={encoded_asset}{prfx}_excluded.html', + u'/{base_asset}@{prfx}_excluded.html?foo={encoded_base_asset}{prfx}_excluded.html', 2 ), ( u'dev', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm', - u'/{asset}@{prfx}_excluded.html?foo={encoded_base_url}{encoded_asset}{prfx}_not_excluded.htm', + u'/{base_asset}@{prfx}_excluded.html?foo={encoded_base_url}{encoded_asset}{prfx}_not_excluded.htm', 2 ), ( u'dev', u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html', - u'//dev/{asset}@{prfx}_not_excluded.htm?foo={encoded_asset}{prfx}_excluded.html', + u'//dev/{asset}@{prfx}_not_excluded.htm?foo={encoded_base_asset}{prfx}_excluded.html', 2 ), ( @@ -416,163 +466,189 @@ class CanonicalContentTest(SharedModuleStoreTestCase): 2 ), # Already asset key. - (u'', u'/{asset}@{prfx}_unlock.png', u'/{asset}@{prfx}_unlock.png', 1), - (u'', u'/{asset}@{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'', u'/{asset}@weird_{prfx}_unlock.png', u'/{asset}@weird_{prfx}_unlock.png', 1), - (u'', u'/{asset}@{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), - (u'', u'/{asset}@{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1), - (u'dev', u'/{asset}@{prfx}_unlock.png', u'//dev/{asset}@{prfx}_unlock.png', 1), - (u'dev', u'/{asset}@{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), - (u'dev', u'/{asset}@weird_{prfx}_unlock.png', u'//dev/{asset}@weird_{prfx}_unlock.png', 1), - (u'dev', u'/{asset}@{prfx}_excluded.html', u'/{asset}@{prfx}_excluded.html', 1), - (u'dev', u'/{asset}@{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1), + (u'', u'/{base_asset}@{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1), + (u'', u'/{base_asset}@{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), + (u'', u'/{base_asset}@weird_{prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'', u'/{base_asset}@{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), + (u'', u'/{base_asset}@{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1), + (u'dev', u'/{base_asset}@{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1), + (u'dev', u'/{base_asset}@{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1), + (u'dev', u'/{base_asset}@weird_{prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/{base_asset}@{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1), + (u'dev', u'/{base_asset}@{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1), # Old, c4x-style path. - (u'', u'/{c4x}/{prfx}_unlock.png', u'/{c4x}/{prfx}_unlock.png', 1), + (u'', u'/{c4x}/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1), (u'', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), (u'', u'/{c4x}/weird_{prfx}_lock.png', u'/{c4x}/weird_{prfx}_lock.png', 1), (u'', u'/{c4x}/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), (u'', u'/{c4x}/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1), - (u'dev', u'/{c4x}/{prfx}_unlock.png', u'/{c4x}/{prfx}_unlock.png', 1), + (u'dev', u'/{c4x}/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1), (u'dev', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'dev', u'/{c4x}/weird_{prfx}_unlock.png', u'/{c4x}/weird_{prfx}_unlock.png', 1), + (u'dev', u'/{c4x}/weird_{prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1), (u'dev', u'/{c4x}/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), (u'dev', u'/{c4x}/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1), # Thumbnails. - (u'', u'/{th_key}@{prfx}_unlock-{th_ext}', u'/{th_key}@{prfx}_unlock-{th_ext}', 1), - (u'', u'/{th_key}@{prfx}_lock-{th_ext}', u'/{th_key}@{prfx}_lock-{th_ext}', 1), - (u'dev', u'/{th_key}@{prfx}_unlock-{th_ext}', u'//dev/{th_key}@{prfx}_unlock-{th_ext}', 1), - (u'dev', u'/{th_key}@{prfx}_lock-{th_ext}', u'//dev/{th_key}@{prfx}_lock-{th_ext}', 1), + (u'', u'/{base_th_key}@{prfx}_ünlöck-{th_ext}', u'/{th_key}@{prfx}_ünlöck-{th_ext}', 1), + (u'', u'/{base_th_key}@{prfx}_lock-{th_ext}', u'/{th_key}@{prfx}_lock-{th_ext}', 1), + (u'dev', u'/{base_th_key}@{prfx}_ünlöck-{th_ext}', u'//dev/{th_key}@{prfx}_ünlöck-{th_ext}', 1), + (u'dev', u'/{base_th_key}@{prfx}_lock-{th_ext}', u'//dev/{th_key}@{prfx}_lock-{th_ext}', 1), ) @ddt.unpack def test_canonical_asset_path_with_new_style_assets(self, base_url, start, expected, mongo_calls): exts = ['.html', '.tm'] - prefix = 'split' - encoded_base_url = quote_plus('//' + base_url) - c4x = 'c4x/a/b/asset' - asset_key = 'asset-v1:a+b+{}+type@asset+block'.format(prefix) - encoded_asset_key = quote_plus('/asset-v1:a+b+{}+type@asset+block@'.format(prefix)) - th_key = 'asset-v1:a+b+{}+type@thumbnail+block'.format(prefix) - th_ext = 'png-16x16.jpg' + prefix = u'split' + encoded_base_url = urlquote(u'//' + base_url) + c4x = u'c4x/a/b/asset' + base_asset_key = u'asset-v1:a+b+{}+type@asset+block'.format(prefix) + adjusted_asset_key = base_asset_key + encoded_asset_key = urlquote(u'/asset-v1:a+b+{}+type@asset+block@'.format(prefix)) + encoded_base_asset_key = encoded_asset_key + base_th_key = u'asset-v1:a+b+{}+type@thumbnail+block'.format(prefix) + adjusted_th_key = base_th_key + th_ext = u'png-16x16.jpg' start = start.format( prfx=prefix, c4x=c4x, - asset=asset_key, + base_asset=base_asset_key, + asset=adjusted_asset_key, encoded_base_url=encoded_base_url, encoded_asset=encoded_asset_key, - th_key=th_key, + base_th_key=base_th_key, + th_key=adjusted_th_key, th_ext=th_ext ) + + # Adjust for content digest. This gets dicey quickly and we have to order our steps: + # - replace format markets because they have curly braces + # - encode Unicode characters to percent-encoded + # - finally shove back in our regex patterns + digest = CanonicalContentTest.get_content_digest_for_asset_path(prefix, start) + if digest: + adjusted_asset_key = u'assets/courseware/MARK/asset-v1:a+b+{}+type@asset+block'.format(prefix) + adjusted_th_key = u'assets/courseware/MARK/asset-v1:a+b+{}+type@thumbnail+block'.format(prefix) + encoded_asset_key = u'/assets/courseware/MARK/asset-v1:a+b+{}+type@asset+block@'.format(prefix) + encoded_asset_key = urlquote(encoded_asset_key) + expected = expected.format( prfx=prefix, c4x=c4x, - asset=asset_key, + base_asset=base_asset_key, + asset=adjusted_asset_key, encoded_base_url=encoded_base_url, encoded_asset=encoded_asset_key, - th_key=th_key, - th_ext=th_ext + base_th_key=base_th_key, + th_key=adjusted_th_key, + th_ext=th_ext, + encoded_base_asset=encoded_base_asset_key, ) + expected = encode_unicode_characters_in_url(expected) + expected = expected.replace('MARK', '[a-f0-9]{32}') + expected = expected.replace('+', r'\+').replace('?', r'\?') + with check_mongo_calls(mongo_calls): asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts) - self.assertEqual(asset_path, expected) + print expected + print asset_path + self.assertIsNotNone(re.match(expected, asset_path)) @ddt.data( # No leading slash. - (u'', u'{prfx}_unlock.png', u'/{c4x}/{prfx}_unlock.png', 1), + (u'', u'{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1), (u'', u'{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'', u'weird {prfx}_unlock.png', u'/{c4x}/weird_{prfx}_unlock.png', 1), - (u'', u'{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'', u'weird {prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'', u'{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'', u'{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1), - (u'dev', u'{prfx}_unlock.png', u'//dev/{c4x}/{prfx}_unlock.png', 1), + (u'dev', u'{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1), (u'dev', u'{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'dev', u'weird {prfx}_unlock.png', u'//dev/{c4x}/weird_{prfx}_unlock.png', 1), - (u'dev', u'{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'dev', u'weird {prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'dev', u'{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'dev', u'{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1), # No leading slash with subdirectory. This ensures we probably substitute slashes. - (u'', u'special/{prfx}_unlock.png', u'/{c4x}/special_{prfx}_unlock.png', 1), + (u'', u'special/{prfx}_ünlöck.png', u'/{c4x}/special_{prfx}_ünlöck.png', 1), (u'', u'special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1), - (u'', u'special/weird {prfx}_unlock.png', u'/{c4x}/special_weird_{prfx}_unlock.png', 1), - (u'', u'special/{prfx}_excluded.html', u'/{c4x}/special_{prfx}_excluded.html', 1), + (u'', u'special/weird {prfx}_ünlöck.png', u'/{c4x}/special_weird_{prfx}_ünlöck.png', 1), + (u'', u'special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1), (u'', u'special/{prfx}_not_excluded.htm', u'/{c4x}/special_{prfx}_not_excluded.htm', 1), - (u'dev', u'special/{prfx}_unlock.png', u'//dev/{c4x}/special_{prfx}_unlock.png', 1), + (u'dev', u'special/{prfx}_ünlöck.png', u'//dev/{c4x}/special_{prfx}_ünlöck.png', 1), (u'dev', u'special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1), - (u'dev', u'special/weird {prfx}_unlock.png', u'//dev/{c4x}/special_weird_{prfx}_unlock.png', 1), - (u'dev', u'special/{prfx}_excluded.html', u'/{c4x}/special_{prfx}_excluded.html', 1), + (u'dev', u'special/weird {prfx}_ünlöck.png', u'//dev/{c4x}/special_weird_{prfx}_ünlöck.png', 1), + (u'dev', u'special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1), (u'dev', u'special/{prfx}_not_excluded.htm', u'//dev/{c4x}/special_{prfx}_not_excluded.htm', 1), # Leading slash. - (u'', u'/{prfx}_unlock.png', u'/{c4x}/{prfx}_unlock.png', 1), + (u'', u'/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1), (u'', u'/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'', u'/weird {prfx}_unlock.png', u'/{c4x}/weird_{prfx}_unlock.png', 1), - (u'', u'/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'', u'/weird {prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'', u'/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'', u'/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1), - (u'dev', u'/{prfx}_unlock.png', u'//dev/{c4x}/{prfx}_unlock.png', 1), + (u'dev', u'/{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1), (u'dev', u'/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'dev', u'/weird {prfx}_unlock.png', u'//dev/{c4x}/weird_{prfx}_unlock.png', 1), - (u'dev', u'/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'dev', u'/weird {prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'dev', u'/{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1), # Leading slash with subdirectory. This ensures we properly substitute slashes. - (u'', u'/special/{prfx}_unlock.png', u'/{c4x}/special_{prfx}_unlock.png', 1), + (u'', u'/special/{prfx}_ünlöck.png', u'/{c4x}/special_{prfx}_ünlöck.png', 1), (u'', u'/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1), - (u'', u'/special/weird {prfx}_unlock.png', u'/{c4x}/special_weird_{prfx}_unlock.png', 1), - (u'', u'/special/{prfx}_excluded.html', u'/{c4x}/special_{prfx}_excluded.html', 1), + (u'', u'/special/weird {prfx}_ünlöck.png', u'/{c4x}/special_weird_{prfx}_ünlöck.png', 1), + (u'', u'/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1), (u'', u'/special/{prfx}_not_excluded.htm', u'/{c4x}/special_{prfx}_not_excluded.htm', 1), - (u'dev', u'/special/{prfx}_unlock.png', u'//dev/{c4x}/special_{prfx}_unlock.png', 1), + (u'dev', u'/special/{prfx}_ünlöck.png', u'//dev/{c4x}/special_{prfx}_ünlöck.png', 1), (u'dev', u'/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1), - (u'dev', u'/special/weird {prfx}_unlock.png', u'//dev/{c4x}/special_weird_{prfx}_unlock.png', 1), - (u'dev', u'/special/{prfx}_excluded.html', u'/{c4x}/special_{prfx}_excluded.html', 1), + (u'dev', u'/special/weird {prfx}_ünlöck.png', u'//dev/{c4x}/special_weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1), (u'dev', u'/special/{prfx}_not_excluded.htm', u'//dev/{c4x}/special_{prfx}_not_excluded.htm', 1), # Static path. - (u'', u'/static/{prfx}_unlock.png', u'/{c4x}/{prfx}_unlock.png', 1), + (u'', u'/static/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1), (u'', u'/static/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'', u'/static/weird {prfx}_unlock.png', u'/{c4x}/weird_{prfx}_unlock.png', 1), - (u'', u'/static/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'', u'/static/weird {prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'', u'/static/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'', u'/static/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1), - (u'dev', u'/static/{prfx}_unlock.png', u'//dev/{c4x}/{prfx}_unlock.png', 1), + (u'dev', u'/static/{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1), (u'dev', u'/static/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'dev', u'/static/weird {prfx}_unlock.png', u'//dev/{c4x}/weird_{prfx}_unlock.png', 1), - (u'dev', u'/static/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'dev', u'/static/weird {prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/static/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'dev', u'/static/{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1), # Static path with subdirectory. This ensures we properly substitute slashes. - (u'', u'/static/special/{prfx}_unlock.png', u'/{c4x}/special_{prfx}_unlock.png', 1), + (u'', u'/static/special/{prfx}_ünlöck.png', u'/{c4x}/special_{prfx}_ünlöck.png', 1), (u'', u'/static/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1), - (u'', u'/static/special/weird {prfx}_unlock.png', u'/{c4x}/special_weird_{prfx}_unlock.png', 1), - (u'', u'/static/special/{prfx}_excluded.html', u'/{c4x}/special_{prfx}_excluded.html', 1), + (u'', u'/static/special/weird {prfx}_ünlöck.png', u'/{c4x}/special_weird_{prfx}_ünlöck.png', 1), + (u'', u'/static/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1), (u'', u'/static/special/{prfx}_not_excluded.htm', u'/{c4x}/special_{prfx}_not_excluded.htm', 1), - (u'dev', u'/static/special/{prfx}_unlock.png', u'//dev/{c4x}/special_{prfx}_unlock.png', 1), + (u'dev', u'/static/special/{prfx}_ünlöck.png', u'//dev/{c4x}/special_{prfx}_ünlöck.png', 1), (u'dev', u'/static/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1), - (u'dev', u'/static/special/weird {prfx}_unlock.png', u'//dev/{c4x}/special_weird_{prfx}_unlock.png', 1), - (u'dev', u'/static/special/{prfx}_excluded.html', u'/{c4x}/special_{prfx}_excluded.html', 1), + (u'dev', u'/static/special/weird {prfx}_ünlöck.png', u'//dev/{c4x}/special_weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/static/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1), (u'dev', u'/static/special/{prfx}_not_excluded.htm', u'//dev/{c4x}/special_{prfx}_not_excluded.htm', 1), # Static path with query parameter. ( u'', - u'/static/{prfx}_unlock.png?foo=/static/{prfx}_lock.png', - u'/{c4x}/{prfx}_unlock.png?foo={encoded_c4x}{prfx}_lock.png', + u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png', + u'/{c4x}/{prfx}_ünlöck.png?foo={encoded_c4x}{prfx}_lock.png', 2 ), ( u'', - u'/static/{prfx}_lock.png?foo=/static/{prfx}_unlock.png', - u'/{c4x}/{prfx}_lock.png?foo={encoded_c4x}{prfx}_unlock.png', + u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png', + u'/{c4x}/{prfx}_lock.png?foo={encoded_c4x}{prfx}_ünlöck.png', 2 ), ( u'', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html', - u'/{c4x}/{prfx}_excluded.html?foo={encoded_c4x}{prfx}_excluded.html', + u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_base_c4x}{prfx}_excluded.html', 2 ), ( u'', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm', - u'/{c4x}/{prfx}_excluded.html?foo={encoded_c4x}{prfx}_not_excluded.htm', + u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_c4x}{prfx}_not_excluded.htm', 2 ), ( u'', u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html', - u'/{c4x}/{prfx}_not_excluded.htm?foo={encoded_c4x}{prfx}_excluded.html', + u'/{c4x}/{prfx}_not_excluded.htm?foo={encoded_base_c4x}{prfx}_excluded.html', 2 ), ( @@ -583,32 +659,32 @@ class CanonicalContentTest(SharedModuleStoreTestCase): ), ( u'dev', - u'/static/{prfx}_unlock.png?foo=/static/{prfx}_lock.png', - u'//dev/{c4x}/{prfx}_unlock.png?foo={encoded_c4x}{prfx}_lock.png', + u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png', + u'//dev/{c4x}/{prfx}_ünlöck.png?foo={encoded_c4x}{prfx}_lock.png', 2 ), ( u'dev', - u'/static/{prfx}_lock.png?foo=/static/{prfx}_unlock.png', - u'/{c4x}/{prfx}_lock.png?foo={encoded_base_url}{encoded_c4x}{prfx}_unlock.png', + u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png', + u'/{c4x}/{prfx}_lock.png?foo={encoded_base_url}{encoded_c4x}{prfx}_ünlöck.png', 2 ), ( u'dev', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html', - u'/{c4x}/{prfx}_excluded.html?foo={encoded_c4x}{prfx}_excluded.html', + u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_base_c4x}{prfx}_excluded.html', 2 ), ( u'dev', u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm', - u'/{c4x}/{prfx}_excluded.html?foo={encoded_base_url}{encoded_c4x}{prfx}_not_excluded.htm', + u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_base_url}{encoded_c4x}{prfx}_not_excluded.htm', 2 ), ( u'dev', u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html', - u'//dev/{c4x}/{prfx}_not_excluded.htm?foo={encoded_c4x}{prfx}_excluded.html', + u'//dev/{c4x}/{prfx}_not_excluded.htm?foo={encoded_base_c4x}{prfx}_excluded.html', 2 ), ( @@ -618,38 +694,58 @@ class CanonicalContentTest(SharedModuleStoreTestCase): 2 ), # Old, c4x-style path. - (u'', u'/{c4x}/{prfx}_unlock.png', u'/{c4x}/{prfx}_unlock.png', 1), + (u'', u'/{c4x}/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1), (u'', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), (u'', u'/{c4x}/weird_{prfx}_lock.png', u'/{c4x}/weird_{prfx}_lock.png', 1), - (u'', u'/{c4x}/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'', u'/{c4x}/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'', u'/{c4x}/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1), - (u'dev', u'/{c4x}/{prfx}_unlock.png', u'//dev/{c4x}/{prfx}_unlock.png', 1), + (u'dev', u'/{c4x}/{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1), (u'dev', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1), - (u'dev', u'/{c4x}/weird_{prfx}_unlock.png', u'//dev/{c4x}/weird_{prfx}_unlock.png', 1), - (u'dev', u'/{c4x}/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1), + (u'dev', u'/{c4x}/weird_{prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1), + (u'dev', u'/{c4x}/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1), (u'dev', u'/{c4x}/{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1), ) @ddt.unpack def test_canonical_asset_path_with_c4x_style_assets(self, base_url, start, expected, mongo_calls): exts = ['.html', '.tm'] prefix = 'old' - c4x_block = 'c4x/a/b/asset' - encoded_c4x_block = quote_plus('/' + c4x_block + '/') - encoded_base_url = quote_plus('//' + base_url) + base_c4x_block = 'c4x/a/b/asset' + adjusted_c4x_block = base_c4x_block + encoded_c4x_block = urlquote('/' + base_c4x_block + '/') + encoded_base_url = urlquote('//' + base_url) + encoded_base_c4x_block = encoded_c4x_block start = start.format( prfx=prefix, encoded_base_url=encoded_base_url, - c4x=c4x_block, - encoded_c4x=encoded_c4x_block - ) - expected = expected.format( - prfx=prefix, - encoded_base_url=encoded_base_url, - c4x=c4x_block, + c4x=base_c4x_block, encoded_c4x=encoded_c4x_block ) + # Adjust for content digest. This gets dicey quickly and we have to order our steps: + # - replace format markets because they have curly braces + # - encode Unicode characters to percent-encoded + # - finally shove back in our regex patterns + digest = CanonicalContentTest.get_content_digest_for_asset_path(prefix, start) + if digest: + adjusted_c4x_block = 'assets/courseware/MARK/c4x/a/b/asset' + encoded_c4x_block = urlquote('/' + adjusted_c4x_block + '/') + + expected = expected.format( + prfx=prefix, + encoded_base_url=encoded_base_url, + base_c4x=base_c4x_block, + c4x=adjusted_c4x_block, + encoded_c4x=encoded_c4x_block, + encoded_base_c4x=encoded_base_c4x_block, + ) + + expected = encode_unicode_characters_in_url(expected) + expected = expected.replace('MARK', '[a-f0-9]{32}') + expected = expected.replace('+', r'\+').replace('?', r'\?') + with check_mongo_calls(mongo_calls): asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts) - self.assertEqual(asset_path, expected) + print expected + print asset_path + self.assertIsNotNone(re.match(expected, asset_path)) diff --git a/common/djangoapps/student/management/commands/manage_user.py b/common/djangoapps/student/management/commands/manage_user.py index dcb299006e..fed69c7771 100644 --- a/common/djangoapps/student/management/commands/manage_user.py +++ b/common/djangoapps/student/management/commands/manage_user.py @@ -23,6 +23,7 @@ class Command(BaseCommand): parser.add_argument('--remove', dest='is_remove', action='store_true') parser.add_argument('--superuser', dest='is_superuser', action='store_true') parser.add_argument('--staff', dest='is_staff', action='store_true') + parser.add_argument('--unusable-password', dest='unusable_password', action='store_true') parser.add_argument('-g', '--groups', nargs='*', default=[]) def _maybe_update(self, user, attribute, new_value): @@ -68,7 +69,7 @@ class Command(BaseCommand): user.delete() @transaction.atomic - def handle(self, username, email, is_remove, is_staff, is_superuser, groups, *args, **options): + def handle(self, username, email, is_remove, is_staff, is_superuser, groups, unusable_password, *args, **options): if is_remove: return self._handle_remove(username, email) @@ -91,6 +92,11 @@ class Command(BaseCommand): self._maybe_update(user, 'is_staff', is_staff) self._maybe_update(user, 'is_superuser', is_superuser) + # Set unusable password if specified + if unusable_password and user.has_usable_password(): + self.stderr.write(_('Setting unusable password for user "{}"').format(user)) + user.set_unusable_password() + # Ensure the user has a profile try: __ = user.profile diff --git a/common/djangoapps/student/management/tests/test_manage_user.py b/common/djangoapps/student/management/tests/test_manage_user.py index 1335ee5b14..3b3186e250 100644 --- a/common/djangoapps/student/management/tests/test_manage_user.py +++ b/common/djangoapps/student/management/tests/test_manage_user.py @@ -48,6 +48,29 @@ class TestManageUserCommand(TestCase): call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--remove') self.assertEqual([], list(User.objects.all())) + def test_unusable_password(self): + """ + Ensure that a user's password is set to an unusable_password. + """ + user = User.objects.create(username=TEST_USERNAME, email=TEST_EMAIL) + self.assertEqual([(TEST_USERNAME, TEST_EMAIL)], [(u.username, u.email) for u in User.objects.all()]) + user.set_password(User.objects.make_random_password()) + user.save() + + # Run once without passing --unusable-password and make sure the password is usable + call_command('manage_user', TEST_USERNAME, TEST_EMAIL) + user = User.objects.get(username=TEST_USERNAME, email=TEST_EMAIL) + self.assertTrue(user.has_usable_password()) + + # Make sure the user now has an unusable_password + call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--unusable-password') + user = User.objects.get(username=TEST_USERNAME, email=TEST_EMAIL) + self.assertFalse(user.has_usable_password()) + + # check idempotency + call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--unusable-password') + self.assertFalse(user.has_usable_password()) + def test_wrong_email(self): """ Ensure that the operation is aborted if the username matches an diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 26a3df4117..018aad8dd8 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -891,7 +891,7 @@ class AnonymousLookupTable(ModuleStoreTestCase): self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False)) -# TODO: Clean up these tests so that they use the ProgramsDataMixin. +# TODO: Clean up these tests so that they use program factories. @attr('shard_3') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.ddt diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 749a68e122..905b84f096 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -59,7 +59,7 @@ REQUIREJS_WAIT = { # Pages re.compile(r'^Pages \|'): [ - 'js/models/explicit_url', 'coffee/src/views/tabs', + 'js/models/explicit_url', 'js/views/tabs', 'xmodule', 'coffee/src/main', 'xblock/cms.runtime.v1' ], } diff --git a/common/djangoapps/third_party_auth/models.py b/common/djangoapps/third_party_auth/models.py index ce76ef20c5..5bd8f22200 100644 --- a/common/djangoapps/third_party_auth/models.py +++ b/common/djangoapps/third_party_auth/models.py @@ -21,6 +21,7 @@ from social.backends.saml import SAMLAuth, SAMLIdentityProvider from .lti import LTIAuthBackend, LTI_PARAMS_KEY from social.exceptions import SocialAuthBaseException from social.utils import module_member +from openedx.core.djangoapps.theming.helpers import get_value as get_themed_value log = logging.getLogger(__name__) @@ -453,7 +454,7 @@ class SAMLConfiguration(ConfigurationModel): other_config = json.loads(self.other_config_str) if name in ("TECHNICAL_CONTACT", "SUPPORT_CONTACT"): contact = { - "givenName": "{} Support".format(settings.PLATFORM_NAME), + "givenName": "{} Support".format(get_themed_value('PLATFORM_NAME', settings.PLATFORM_NAME)), "emailAddress": settings.TECH_SUPPORT_EMAIL } contact.update(other_config.get(name, {})) diff --git a/common/lib/xmodule/xmodule/block_metadata_utils.py b/common/lib/xmodule/xmodule/block_metadata_utils.py new file mode 100644 index 0000000000..3969419597 --- /dev/null +++ b/common/lib/xmodule/xmodule/block_metadata_utils.py @@ -0,0 +1,80 @@ +""" +Simple utility functions that operate on block metadata. + +This is a place to put simple functions that operate on block metadata. It +allows us to share code between the XModuleMixin and CourseOverview and +BlockStructure. +""" + + +def url_name_for_block(block): + """ + Given a block, returns the block's URL name. + + Arguments: + block (XModuleMixin|CourseOverview|BlockStructureBlockData): + Block that is being accessed + """ + return block.location.name + + +def display_name_with_default(block): + """ + Calculates the display name for a block. + + Default to the display_name if it isn't None, else fall back to creating + a name based on the URL. + + Unlike the rest of this module's functions, this function takes an entire + course descriptor/overview as a parameter. This is because a few test cases + (specifically, {Text|Image|Video}AnnotationModuleTestCase.test_student_view) + create scenarios where course.display_name is not None but course.location + is None, which causes calling course.url_name to fail. So, although we'd + like to just pass course.display_name and course.url_name as arguments to + this function, we can't do so without breaking those tests. + + Note: This method no longer escapes as it once did, so the caller must + ensure it is properly escaped where necessary. + + Arguments: + block (XModuleMixin|CourseOverview|BlockStructureBlockData): + Block that is being accessed + """ + return ( + block.display_name if block.display_name is not None + else url_name_for_block(block).replace('_', ' ') + ) + + +def display_name_with_default_escaped(block): + """ + DEPRECATED: use display_name_with_default + + Calculates the display name for a block with some HTML escaping. + This follows the same logic as display_name_with_default, with + the addition of the escaping. + + Here is an example of how to move away from this method in Mako html: + Before: + ${course.display_name_with_default_escaped} + + After: + ${course.display_name_with_default | h} + If the context is Javascript in Mako, you'll need to follow other best practices. + + Note: Switch to display_name_with_default, and ensure the caller + properly escapes where necessary. + + Note: This newly introduced method should not be used. It was only + introduced to enable a quick search/replace and the ability to slowly + migrate and test switching to display_name_with_default, which is no + longer escaped. + + Arguments: + block (XModuleMixin|CourseOverview|BlockStructureBlockData): + Block that is being accessed + """ + # This escaping is incomplete. However, rather than switching this to use + # markupsafe.escape() and fixing issues, better to put that energy toward + # migrating away from this method altogether. + return display_name_with_default(block).replace('<', '<').replace('>', '>') diff --git a/common/lib/xmodule/xmodule/contentstore/content.py b/common/lib/xmodule/xmodule/contentstore/content.py index 8b5ae95ea0..c98663e91c 100644 --- a/common/lib/xmodule/xmodule/contentstore/content.py +++ b/common/lib/xmodule/xmodule/contentstore/content.py @@ -5,16 +5,16 @@ from xmodule.assetstore.assetmgr import AssetManager XASSET_LOCATION_TAG = 'c4x' XASSET_SRCREF_PREFIX = 'xasset:' - XASSET_THUMBNAIL_TAIL_NAME = '.jpg' - STREAM_DATA_CHUNK_SIZE = 1024 +VERSIONED_ASSETS_PREFIX = '/assets/courseware' +VERSIONED_ASSETS_PATTERN = r'/assets/courseware/([a-f0-9]{32})' import os import logging import StringIO from urlparse import urlparse, urlunparse, parse_qsl -from urllib import urlencode +from urllib import urlencode, quote_plus from opaque_keys.edx.locator import AssetLocator from opaque_keys.edx.keys import CourseKey, AssetKey @@ -26,7 +26,7 @@ from PIL import Image class StaticContent(object): def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None, - length=None, locked=False): + length=None, locked=False, content_digest=None): self.location = loc self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed self.content_type = content_type @@ -38,6 +38,7 @@ class StaticContent(object): # cycles self.import_path = import_path self.locked = locked + self.content_digest = content_digest @property def is_thumbnail(self): @@ -145,6 +146,40 @@ class StaticContent(object): # try stripping off the leading slash and try again return AssetKey.from_string(path[1:]) + @staticmethod + def is_versioned_asset_path(path): + """Determines whether the given asset path is versioned.""" + return path.startswith(VERSIONED_ASSETS_PREFIX) + + @staticmethod + def parse_versioned_asset_path(path): + """ + Examines an asset path and breaks it apart if it is versioned, + returning both the asset digest and the unversioned asset path, + which will normally be an AssetKey. + """ + asset_digest = None + asset_path = path + if StaticContent.is_versioned_asset_path(asset_path): + result = re.match(VERSIONED_ASSETS_PATTERN, asset_path) + if result is not None: + asset_digest = result.groups()[0] + asset_path = re.sub(VERSIONED_ASSETS_PATTERN, '', asset_path) + + return (asset_digest, asset_path) + + @staticmethod + def add_version_to_asset_path(path, version): + """ + Adds a prefix to an asset path indicating the asset's version. + """ + + # Don't version an already-versioned path. + if StaticContent.is_versioned_asset_path(path): + return path + + return VERSIONED_ASSETS_PREFIX + '/' + version + path + @staticmethod def get_asset_key_from_path(course_key, path): """ @@ -172,7 +207,16 @@ class StaticContent(object): return StaticContent.compute_location(course_key, path) @staticmethod - def get_canonicalized_asset_path(course_key, path, base_url, excluded_exts): + def is_excluded_asset_type(path, excluded_exts): + """ + Check if this is an allowed file extension to serve. + + Some files aren't served through the CDN in order to avoid same-origin policy/CORS-related issues. + """ + return any(path.lower().endswith(excluded_ext.lower()) for excluded_ext in excluded_exts) + + @staticmethod + def get_canonicalized_asset_path(course_key, path, base_url, excluded_exts, encode=True): """ Returns a fully-qualified path to a piece of static content. @@ -188,25 +232,27 @@ class StaticContent(object): """ # Break down the input path. - _, _, relative_path, params, query_string, fragment = urlparse(path) + _, _, relative_path, params, query_string, _ = urlparse(path) # Convert our path to an asset key if it isn't one already. asset_key = StaticContent.get_asset_key_from_path(course_key, relative_path) # Check the status of the asset to see if this can be served via CDN aka publicly. serve_from_cdn = False + content_digest = None try: content = AssetManager.find(asset_key, as_stream=True) - is_locked = getattr(content, "locked", True) - serve_from_cdn = not is_locked + serve_from_cdn = not getattr(content, "locked", True) + content_digest = getattr(content, "content_digest", None) except (ItemNotFoundError, NotFoundError): # If we can't find the item, just treat it as if it's locked. serve_from_cdn = False - # See if this is an allowed file extension to serve. Some files aren't served through the - # CDN in order to avoid same-origin policy/CORS-related issues. - if any(relative_path.lower().endswith(excluded_ext.lower()) for excluded_ext in excluded_exts): + # Do a generic check to see if anything about this asset disqualifies it from being CDN'd. + is_excluded = False + if StaticContent.is_excluded_asset_type(relative_path, excluded_exts): serve_from_cdn = False + is_excluded = True # Update any query parameter values that have asset paths in them. This is for assets that # require their own after-the-fact values, like a Flash file that needs the path of a config @@ -215,15 +261,29 @@ class StaticContent(object): updated_query_params = [] for query_name, query_val in query_params: if query_val.startswith("/static/"): - new_val = StaticContent.get_canonicalized_asset_path(course_key, query_val, base_url, excluded_exts) + new_val = StaticContent.get_canonicalized_asset_path( + course_key, query_val, base_url, excluded_exts, encode=False) updated_query_params.append((query_name, new_val)) else: - updated_query_params.append((query_name, query_val)) + # Make sure we're encoding Unicode strings down to their byte string + # representation so that `urlencode` can handle it. + updated_query_params.append((query_name, query_val.encode('utf-8'))) serialized_asset_key = StaticContent.serialize_asset_key_with_slash(asset_key) base_url = base_url if serve_from_cdn else '' + asset_path = serialized_asset_key - return urlunparse((None, base_url, serialized_asset_key, params, urlencode(updated_query_params), fragment)) + # If the content has a digest (i.e. md5sum) value specified, create a versioned path to the asset using it. + if not is_excluded and content_digest: + asset_path = StaticContent.add_version_to_asset_path(serialized_asset_key, content_digest) + + # Only encode this if told to. Important so that we don't double encode + # when working with paths that are in query parameters. + asset_path = asset_path.encode('utf-8') + if encode: + asset_path = quote_plus(asset_path, '/:+@') + + return urlunparse((None, base_url.encode('utf-8'), asset_path, params, urlencode(updated_query_params), None)) def stream_data(self): yield self._data @@ -242,10 +302,10 @@ class StaticContent(object): class StaticContentStream(StaticContent): def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None, - length=None, locked=False): + length=None, locked=False, content_digest=None): super(StaticContentStream, self).__init__(loc, name, content_type, None, last_modified_at=last_modified_at, thumbnail_location=thumbnail_location, import_path=import_path, - length=length, locked=locked) + length=length, locked=locked, content_digest=content_digest) self._stream = stream def stream_data(self): @@ -277,7 +337,8 @@ class StaticContentStream(StaticContent): self._stream.seek(0) content = StaticContent(self.location, self.name, self.content_type, self._stream.read(), last_modified_at=self.last_modified_at, thumbnail_location=self.thumbnail_location, - import_path=self.import_path, length=self.length, locked=self.locked) + import_path=self.import_path, length=self.length, locked=self.locked, + content_digest=self.content_digest) return content diff --git a/common/lib/xmodule/xmodule/contentstore/mongo.py b/common/lib/xmodule/xmodule/contentstore/mongo.py index 8fc505e121..1d1f2a0314 100644 --- a/common/lib/xmodule/xmodule/contentstore/mongo.py +++ b/common/lib/xmodule/xmodule/contentstore/mongo.py @@ -128,7 +128,8 @@ class MongoContentStore(ContentStore): location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate, thumbnail_location=thumbnail_location, import_path=getattr(fp, 'import_path', None), - length=fp.length, locked=getattr(fp, 'locked', False) + length=fp.length, locked=getattr(fp, 'locked', False), + content_digest=getattr(fp, 'md5', None), ) else: with self.fs.get(content_id) as fp: @@ -142,7 +143,8 @@ class MongoContentStore(ContentStore): location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate, thumbnail_location=thumbnail_location, import_path=getattr(fp, 'import_path', None), - length=fp.length, locked=getattr(fp, 'locked', False) + length=fp.length, locked=getattr(fp, 'locked', False), + content_digest=getattr(fp, 'md5', None), ) except NoFile: if throw_on_not_found: diff --git a/common/lib/xmodule/xmodule/course_metadata_utils.py b/common/lib/xmodule/xmodule/course_metadata_utils.py index 0a6a3ba73c..0586d10274 100644 --- a/common/lib/xmodule/xmodule/course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/course_metadata_utils.py @@ -32,78 +32,6 @@ def clean_course_key(course_key, padding_char): ) -def url_name_for_course_location(location): - """ - Given a course's usage locator, returns the course's URL name. - - Arguments: - location (BlockUsageLocator): The course's usage locator. - """ - return location.name - - -def display_name_with_default(course): - """ - Calculates the display name for a course. - - Default to the display_name if it isn't None, else fall back to creating - a name based on the URL. - - Unlike the rest of this module's functions, this function takes an entire - course descriptor/overview as a parameter. This is because a few test cases - (specifically, {Text|Image|Video}AnnotationModuleTestCase.test_student_view) - create scenarios where course.display_name is not None but course.location - is None, which causes calling course.url_name to fail. So, although we'd - like to just pass course.display_name and course.url_name as arguments to - this function, we can't do so without breaking those tests. - - Note: This method no longer escapes as it once did, so the caller must - ensure it is properly escaped where necessary. - - Arguments: - course (CourseDescriptor|CourseOverview): descriptor or overview of - said course. - """ - return ( - course.display_name if course.display_name is not None - else course.url_name.replace('_', ' ') - ) - - -def display_name_with_default_escaped(course): - """ - DEPRECATED: use display_name_with_default - - Calculates the display name for a course with some HTML escaping. - This follows the same logic as display_name_with_default, with - the addition of the escaping. - - Here is an example of how to move away from this method in Mako html: - Before: - ${course.display_name_with_default_escaped} - - After: - ${course.display_name_with_default | h} - If the context is Javascript in Mako, you'll need to follow other best practices. - - Note: Switch to display_name_with_default, and ensure the caller - properly escapes where necessary. - - Note: This newly introduced method should not be used. It was only - introduced to enable a quick search/replace and the ability to slowly - migrate and test switching to display_name_with_default, which is no - longer escaped. - - Arguments: - course (CourseDescriptor|CourseOverview): descriptor or overview of - said course. - """ - # This escaping is incomplete. However, rather than switching this to use - # markupsafe.escape() and fixing issues, better to put that energy toward - # migrating away from this method altogether. - return course.display_name_with_default.replace('<', '<').replace('>', '>') - - def number_for_course_location(location): """ Given a course's block usage locator, returns the course's number. diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 361523dcfd..bd9ff594ab 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -11,12 +11,10 @@ from django.utils.timezone import UTC from lazy import lazy from lxml import etree from path import Path as path -from xblock.core import XBlock from xblock.fields import Scope, List, String, Dict, Boolean, Integer, Float from xmodule import course_metadata_utils from xmodule.course_metadata_utils import DEFAULT_START_DATE -from xmodule.exceptions import UndefinedContext from xmodule.graders import grader_from_conf from xmodule.mixin import LicenseMixin from xmodule.seq_module import SequenceDescriptor, SequenceModule @@ -1183,83 +1181,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): """ return course_metadata_utils.sorting_score(self.start, self.advertised_start, self.announcement) - @lazy - def grading_context(self): - """ - This returns a dictionary with keys necessary for quickly grading - a student. They are used by grades.grade() - - The grading context has two keys: - graded_sections - This contains the sections that are graded, as - well as all possible children modules that can affect the - grading. This allows some sections to be skipped if the student - hasn't seen any part of it. - - The format is a dictionary keyed by section-type. The values are - arrays of dictionaries containing - "section_descriptor" : The section descriptor - "xmoduledescriptors" : An array of xmoduledescriptors that - could possibly be in the section, for any student - - all_descriptors - This contains a list of all xmodules that can - effect grading a student. This is used to efficiently fetch - all the xmodule state for a FieldDataCache without walking - the descriptor tree again. - - - """ - # If this descriptor has been bound to a student, return the corresponding - # XModule. If not, just use the descriptor itself - try: - module = getattr(self, '_xmodule', None) - if not module: - module = self - except UndefinedContext: - module = self - - def possibly_scored(usage_key): - """Can this XBlock type can have a score or children?""" - return usage_key.block_type in self.block_types_affecting_grading - - all_descriptors = [] - graded_sections = {} - - def yield_descriptor_descendents(module_descriptor): - for child in module_descriptor.get_children(usage_key_filter=possibly_scored): - yield child - for module_descriptor in yield_descriptor_descendents(child): - yield module_descriptor - - for chapter in self.get_children(): - for section in chapter.get_children(): - if section.graded: - xmoduledescriptors = list(yield_descriptor_descendents(section)) - xmoduledescriptors.append(section) - - # The xmoduledescriptors included here are only the ones that have scores. - section_description = { - 'section_descriptor': section, - 'xmoduledescriptors': [child for child in xmoduledescriptors if child.has_score] - } - - section_format = section.format if section.format is not None else '' - graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description] - - all_descriptors.extend(xmoduledescriptors) - all_descriptors.append(section) - - return {'graded_sections': graded_sections, - 'all_descriptors': all_descriptors, } - - @lazy - def block_types_affecting_grading(self): - """Return all block types that could impact grading (i.e. scored, or having children).""" - return frozenset( - cat for (cat, xblock_class) in XBlock.load_classes() if ( - getattr(xblock_class, 'has_score', False) or getattr(xblock_class, 'has_children', False) - ) - ) - @staticmethod def make_id(org, course, url_name): return '/'.join([org, course, url_name]) diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index bd478ab0a2..060842d788 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -535,6 +535,8 @@ .speed-option, .control-lang { + @include border-left($baseline/10 solid rgb(14, 166, 236)); + font-weight: $font-bold; color: rgb(14, 166, 236); // UXPL primary accent } } diff --git a/common/lib/xmodule/xmodule/graders.py b/common/lib/xmodule/xmodule/graders.py index 64d2d76923..cb47fa2dbb 100644 --- a/common/lib/xmodule/xmodule/graders.py +++ b/common/lib/xmodule/xmodule/graders.py @@ -173,7 +173,7 @@ class WeightedSubsectionsGrader(CourseGrader): All items in section_breakdown for each subgrader will be combined. A grade_breakdown will be composed using the score from each grader. - Note that the sum of the weights is not take into consideration. If the weights add up to + Note that the sum of the weights is not taken into consideration. If the weights add up to a value > 1, the student may end up with a percent > 100%. This allows for sections that are extra credit. """ diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html index d4f5401785..c941c535a6 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -55,8 +55,9 @@ - -
    +
    +
    +
    @@ -108,7 +109,9 @@ -
    +
    +
    +
    diff --git a/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js b/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js index 5ee38962a7..3f72dcb7b0 100644 --- a/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js +++ b/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js @@ -20,12 +20,10 @@ var options = { libraryFilesToInclude: [ {pattern: 'common_static/js/vendor/requirejs/require.js', included: true}, {pattern: 'RequireJS-namespace-undefine.js', included: true}, - {pattern: 'spec/main_requirejs.js', included: true}, {pattern: 'common_static/coffee/src/ajax_prefix.js', included: true}, {pattern: 'common_static/common/js/vendor/underscore.js', included: true}, {pattern: 'common_static/common/js/vendor/backbone.js', included: true}, - {pattern: 'common_static/edx-ui-toolkit/js/utils/global-loader.js', included: true}, {pattern: 'common_static/js/vendor/CodeMirror/codemirror.js', included: true}, {pattern: 'common_static/js/vendor/draggabilly.js'}, {pattern: 'common_static/common/js/vendor/jquery.js', included: true}, @@ -50,11 +48,14 @@ var options = { {pattern: 'common_static/js/vendor/jasmine-imagediff.js', included: true}, {pattern: 'common_static/common/js/spec_helpers/jasmine-waituntil.js', included: true}, {pattern: 'common_static/common/js/spec_helpers/jasmine-extensions.js', included: true}, - {pattern: 'common_static/js/vendor/sinon-1.17.0.js', included: true} + {pattern: 'common_static/js/vendor/sinon-1.17.0.js', included: true}, + + {pattern: 'spec/main_requirejs.js', included: true}, ], libraryFiles: [ - {pattern: 'common_static/edx-pattern-library/js/**/*.js'} + {pattern: 'common_static/edx-pattern-library/js/**/*.js'}, + {pattern: 'common_static/edx-ui-toolkit/js/**/*.js'} ], // Make sure the patterns in sourceFiles and specFiles do not match the same file. diff --git a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js index a57497f8c8..d4692469e5 100644 --- a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js +++ b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js @@ -1,4 +1,36 @@ -(function(requirejs) { +(function(requirejs, define) { + 'use strict'; + // We do not wish to bundle common libraries (that may also be used by non-RequireJS code on the page + // into the optimized files. Therefore load these libraries through script tags and explicitly define them. + // Note that when the optimizer executes this code, window will not be defined. + if (window) { + var defineDependency = function (globalName, name, noShim) { + var getGlobalValue = function(name) { + var globalNamePath = name.split('.'), + result = window, + i; + for (i = 0; i < globalNamePath.length; i++) { + result = result[globalNamePath[i]]; + } + return result; + }, + globalValue = getGlobalValue(globalName); + if (globalValue) { + if (noShim) { + define(name, {}); + } + else { + define(name, [], function() { return globalValue; }); + } + } + else { + console.error("Expected library to be included on page, but not found on window object: " + name); + } + }; + defineDependency("jQuery", "jquery"); + defineDependency("jQuery", "jquery-migrate"); + defineDependency("_", "underscore"); + } requirejs.config({ baseUrl: '/base/', paths: { @@ -6,7 +38,8 @@ "modernizr": "common_static/edx-pattern-library/js/modernizr-custom", "afontgarde": "common_static/edx-pattern-library/js/afontgarde", "edxicons": "common_static/edx-pattern-library/js/edx-icons", - "draggabilly": "common_static/js/vendor/draggabilly" + "draggabilly": "common_static/js/vendor/draggabilly", + 'edx-ui-toolkit': 'common_static/edx-ui-toolkit' }, "moment": { exports: "moment" @@ -18,5 +51,4 @@ exports: "AFontGarde" } }); - -}).call(this, RequireJS.requirejs); +}).call(this, RequireJS.requirejs, RequireJS.define); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js index b8cf3065b1..5d85be2551 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js @@ -266,6 +266,7 @@ expect($('.closed-captions')).toHaveAttrs({ 'lang': 'de' }); + expect(link).toHaveAttr('aria-pressed', 'true'); }); it('when clicking on link with current language', function () { @@ -284,6 +285,7 @@ expect(state.storage.setItem) .not.toHaveBeenCalledWith('language', 'en'); expect($('.langs-list li.is-active').length).toBe(1); + expect(link).toHaveAttr('aria-pressed', 'true'); }); it('open the language toggle on hover', function () { @@ -413,7 +415,7 @@ }); it('show explanation message', function () { - expect($('.subtitles-menu li')).toHaveText( + expect($('.subtitles .subtitles-menu li')).toHaveText( 'Transcript will be displayed when you start playing the video.' ); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js index 2a8b5e8b67..f73b1f3c59 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js @@ -203,16 +203,18 @@ describe('onSpeedChange', function () { beforeEach(function () { state = jasmine.initializePlayer(); - $('li[data-speed="1.0"]').addClass('is-active'); + $('li[data-speed="1.0"]').addClass('is-active').attr('aria-pressed', 'true'); state.videoSpeedControl.setSpeed(0.75); }); it('set the new speed as active', function () { - expect($('.video-speeds li[data-speed="1.0"]')) - .not.toHaveClass('is-active'); - expect($('.video-speeds li[data-speed="0.75"]')) - .toHaveClass('is-active'); - expect($('.speeds .value')).toHaveHtml('0.75x'); + expect($('li[data-speed="1.0"]')).not.toHaveClass('is-active'); + expect($('li[data-speed="1.0"] .speed-option').attr('aria-pressed')).not.toEqual('true'); + + expect($('li[data-speed="0.75"]')).toHaveClass('is-active'); + expect($('li[data-speed="0.75"] .speed-option').attr('aria-pressed')).toEqual('true'); + + expect($('.speeds .speed-button .value')).toHaveHtml('0.75x'); }); }); diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js index 986d9ea7ff..ace6f39114 100644 --- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js @@ -1,9 +1,10 @@ (function (requirejs, require, define) { "use strict"; define( -'video/08_video_speed_control.js', -['video/00_iterator.js'], -function (Iterator) { +'video/08_video_speed_control.js', [ + 'video/00_iterator.js', + 'edx-ui-toolkit/js/utils/html-utils' +], function (Iterator, HtmlUtils) { /** * Video speed control module. * @exports video/08_video_speed_control.js @@ -95,23 +96,38 @@ function (Iterator) { * Creates any necessary DOM elements, attach them, and set their, * initial configuration. * @param {array} speeds List of speeds available for the player. + * @param {string} currentSpeed The current speed set to the player. */ - render: function (speeds) { + render: function (speeds, currentSpeed) { var speedsContainer = this.speedsContainer, reversedSpeeds = speeds.concat().reverse(), speedsList = $.map(reversedSpeeds, function (speed) { - return [ - '
  • ', - '', - '
  • ' - ].join(''); + return HtmlUtils.interpolateHtml( + HtmlUtils.HTML( + [ + '
  • ', + '', + '
  • ' + ].join('') + ), + { + speed: speed + } + ).toString(); }); - speedsContainer.html(speedsList.join('')); + HtmlUtils.setHtml( + speedsContainer, + HtmlUtils.HTML(speedsList) + ); this.speedLinks = new Iterator(speedsContainer.find('.speed-option')); - this.state.el.find('.secondary-controls').prepend(this.el); + HtmlUtils.prepend( + this.state.el.find('.secondary-controls'), + HtmlUtils.HTML(this.el) + ); + this.setActiveSpeed(currentSpeed); }, /** @@ -216,17 +232,38 @@ function (Iterator) { if (speed !== this.currentSpeed || forceUpdate) { this.speedsContainer .find('li') - .removeClass('is-active') - .siblings("li[data-speed='" + speed + "']") - .addClass('is-active'); + .siblings("li[data-speed='" + speed + "']"); - this.speedButton.find('.value').html(speed + 'x'); + this.speedButton.find('.value').text(speed + 'x'); this.currentSpeed = speed; if (!silent) { this.el.trigger('speedchange', [speed, this.state.speed]); } } + + this.resetActiveSpeed(); + this.setActiveSpeed(speed); + }, + + resetActiveSpeed: function() { + var speedOptions = this.speedsContainer.find('li'); + + $(speedOptions).each(function(index, el) { + $(el).removeClass('is-active') + .find('.speed-option') + .attr('aria-pressed', 'false'); + }); + }, + + setActiveSpeed: function(speed) { + var speedOption = this.speedsContainer.find('li[data-speed="' + speed + '"]'); + + speedOption.addClass('is-active') + .find('.speed-option') + .attr('aria-pressed', 'true'); + + this.speedButton.attr('title', gettext('Video speed: ') + speed + 'x'); }, /** @@ -244,10 +281,13 @@ function (Iterator) { * @param {jquery Event} event */ clickLinkHandler: function (event) { - var speed = $(event.currentTarget).parent().data('speed'); - - this.closeMenu(); + var el = $(event.currentTarget).parent(), + speed = $(el).data('speed'); + + this.resetActiveSpeed(); + this.setActiveSpeed(speed); this.state.videoCommands.execute('speed', speed); + this.closeMenu(true); return false; }, diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index b99a1b1128..b251d128bb 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -5,11 +5,12 @@ define('video/09_video_caption.js',[ 'video/00_sjson.js', 'video/00_async_process.js', + 'edx-ui-toolkit/js/utils/html-utils', 'draggabilly', 'modernizr', 'afontgarde', 'edxicons' - ], function (Sjson, AsyncProcess, Draggabilly) { + ], function (Sjson, AsyncProcess, HtmlUtils, Draggabilly) { /** * @desc VideoCaption module exports a function. @@ -80,47 +81,60 @@ renderElements: function () { var languages = this.state.config.transcriptLanguages; - var langTemplate = [ - '
    ', - '', - '', - '', - '
    ' - ].join(''); + var langHtml = HtmlUtils.interpolateHtml( + HtmlUtils.HTML( + [ + '
    ', + '', + '', + '', + '
    ' + ].join(''), + { + langTitle: gettext('Open language menu') + } + ) + + ); - var template = [ - '
    ', - '

    ', - '
      ', - '
      ' - ].join(''); + var subtitlesHtml = HtmlUtils.interpolateHtml( + HtmlUtils.HTML( + [ + '
      ', + '

      ', + '
        ', + '
        ' + ].join('')), + { + courseId: this.state.id, + courseLang: this.state.lang + } + ); this.loaded = false; - this.subtitlesEl = $(template); + this.subtitlesEl = $(HtmlUtils.ensureHtml(subtitlesHtml).toString()); this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu'); - this.container = $(langTemplate); + this.container = $(HtmlUtils.ensureHtml(langHtml).toString()); this.captionControlEl = this.container.find('.toggle-captions'); this.captionDisplayEl = this.state.el.find('.closed-captions'); this.transcriptControlEl = this.container.find('.toggle-transcript'); @@ -542,15 +556,26 @@ } } else { if (state.isTouch) { - self.subtitlesEl.find('.subtitles-menu') - .text(gettext('Transcript will be displayed when you start playing the video.')) // jshint ignore: line - .wrapInner('
      1. '); + HtmlUtils.setHtml( + self.subtitlesEl.find('.subtitles-menu'), + HtmlUtils.joinHtml( + HtmlUtils.HTML('
      2. '), + gettext('Transcript will be displayed when you start playing the video.'), + HtmlUtils.HTML('
      3. ') + ) + ); } else { self.renderCaption(start, captions); } self.hideCaptions(state.hide_captions, false); - self.state.el.find('.video-wrapper').after(self.subtitlesEl); - self.state.el.find('.secondary-controls').append(self.container); + HtmlUtils.append( + self.state.el.find('.video-wrapper').parent(), + HtmlUtils.HTML(self.subtitlesEl) + ); + HtmlUtils.append( + self.state.el.find('.secondary-controls'), + HtmlUtils.HTML(self.container) + ); self.bindHandlers(); } @@ -630,9 +655,11 @@ onResize: function () { this.subtitlesEl .find('.spacing').first() - .height(this.topSpacingHeight()).end() + .height(this.topSpacingHeight()); + + this.subtitlesEl .find('.spacing').last() - .height(this.bottomSpacingHeight()); + .height(this.bottomSpacingHeight()); this.scrollCaption(); this.setSubtitlesHeight(); @@ -649,8 +676,9 @@ renderLanguageMenu: function (languages) { var self = this, state = this.state, - menu = $('