Merge pull request #12863 from edx/rc/2016-06-28
Release Candidate rc/2016-06-28
This commit is contained in:
@@ -26,6 +26,8 @@ omit =
|
||||
openedx/core/djangoapps/*/migrations/*
|
||||
openedx/core/djangoapps/debug/*
|
||||
|
||||
concurrency=multiprocessing
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "<Component>" "Advanced Problem" component
|
||||
# Then I see a "<Component>" 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 |
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
} ())
|
||||
}())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
299
cms/static/cms/js/spec/main.js
Normal file
299
cms/static/cms/js/spec/main.js
Normal file
@@ -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
|
||||
218
cms/static/cms/js/spec/main_squire.js
Normal file
218
cms/static/cms/js/spec/main_squire.js
Normal file
@@ -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
|
||||
@@ -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();
|
||||
188
cms/static/cms/js/xblock/cms.runtime.v1.js
Normal file
188
cms/static/cms/js/xblock/cms.runtime.v1.js
Normal file
@@ -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;
|
||||
});
|
||||
@@ -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()
|
||||
@@ -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", ->
|
||||
|
||||
@@ -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()
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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", ->
|
||||
|
||||
@@ -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 """
|
||||
<ul>
|
||||
<li class="component" id="stub-id" data-locator="stub-id">
|
||||
<div class="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
|
||||
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
|
||||
</div>
|
||||
<span class="drag-handle action"></span>
|
||||
<section class="xblock xblock-student_view xmodule_display xmodule_stub" data-type="StubModule">
|
||||
<div id="stub-module-content"/>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
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: '<div>Response html</div>'
|
||||
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("<style type='text/css'>inline-css</style>")
|
||||
|
||||
it "loads css urls from fragments", ->
|
||||
expect($('head').append).toHaveBeenCalledWith("<link rel='stylesheet' href='css-url' type='text/css'>")
|
||||
|
||||
it "loads inline js from fragments", ->
|
||||
expect($('head').append).toHaveBeenCalledWith("<script>inline-js</script>")
|
||||
|
||||
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: '<div>Response html 2</div>'
|
||||
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))
|
||||
@@ -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($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new Textbook({name: "Life Sciences", id: "0life-sciences"})
|
||||
spyOn(@model, "destroy").and.callThrough()
|
||||
@collection = new TextbookSet([@model])
|
||||
@view = new ShowTextbook({model: @model})
|
||||
|
||||
@promptSpies = jasmine.stealth.spyOnConstructor(Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.and.returnValue(@promptSpies)
|
||||
window.course = new Course({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
|
||||
afterEach ->
|
||||
delete window.course
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("Life Sciences")
|
||||
|
||||
it "should set the 'editing' property on the model when the edit button is clicked", ->
|
||||
@view.render().$(".edit").click()
|
||||
expect(@model.get("editing")).toBeTruthy()
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".delete").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/)
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
it "should show chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', false)
|
||||
@view.render().$(".show-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeTruthy()
|
||||
|
||||
it "should hide chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', true)
|
||||
@view.render().$(".hide-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeFalsy()
|
||||
|
||||
describe "AJAX", ->
|
||||
beforeEach ->
|
||||
@savingSpies = jasmine.stealth.spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"])
|
||||
@savingSpies.show.and.returnValue(@savingSpies)
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
requests = AjaxHelpers["requests"](this)
|
||||
|
||||
@view.render().$(".delete").click()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(savingOptions.title).toMatch(/Deleting/)
|
||||
# return a success response
|
||||
requests[0].respond(204)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
describe "EditTextbook", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('edit-textbook.underscore')
|
||||
describe "ShowTextbook", ->
|
||||
tpl = readFixtures('show-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
setFixtures($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new Textbook({name: "Life Sciences", editing: true})
|
||||
spyOn(@model, 'save')
|
||||
@collection = new TextbookSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditTextbook({model: @model})
|
||||
spyOn(@view, 'render').and.callThrough()
|
||||
@model = new Textbook({name: "Life Sciences", id: "0life-sciences"})
|
||||
spyOn(@model, "destroy").and.callThrough()
|
||||
@collection = new TextbookSet([@model])
|
||||
@view = new ShowTextbook({model: @model})
|
||||
|
||||
it "should render properly", ->
|
||||
@promptSpies = jasmine.stealth.spyOnConstructor(Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.and.returnValue(@promptSpies)
|
||||
window.course = new Course({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
|
||||
afterEach ->
|
||||
delete window.course
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("Life Sciences")
|
||||
|
||||
it "should set the 'editing' property on the model when the edit button is clicked", ->
|
||||
@view.render().$(".edit").click()
|
||||
expect(@model.get("editing")).toBeTruthy()
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".delete").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/)
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
it "should show chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', false)
|
||||
@view.render().$(".show-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeTruthy()
|
||||
|
||||
it "should hide chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', true)
|
||||
@view.render().$(".hide-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeFalsy()
|
||||
|
||||
describe "AJAX", ->
|
||||
beforeEach ->
|
||||
@savingSpies = jasmine.stealth.spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"])
|
||||
@savingSpies.show.and.returnValue(@savingSpies)
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
requests = AjaxHelpers["requests"](this)
|
||||
|
||||
@view.render().$(".delete").click()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(savingOptions.title).toMatch(/Deleting/)
|
||||
# return a success response
|
||||
requests[0].respond(204)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
describe "EditTextbook", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('edit-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new Textbook({name: "Life Sciences", editing: true})
|
||||
spyOn(@model, 'save')
|
||||
@collection = new TextbookSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditTextbook({model: @model})
|
||||
spyOn(@view, 'render').and.callThrough()
|
||||
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$("input[name=textbook-name]").val()).toEqual("Life Sciences")
|
||||
|
||||
it "should allow you to create new empty chapters", ->
|
||||
@view.render()
|
||||
numChapters = @model.get("chapters").length
|
||||
@view.$(".action-add-chapter").click()
|
||||
expect(@model.get("chapters").length).toEqual(numChapters+1)
|
||||
expect(@model.get("chapters").last().isEmpty()).toBeTruthy()
|
||||
|
||||
it "should save properly", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("wallflower")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.get("name")).toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("name")).toEqual("wallflower")
|
||||
expect(chapter.get("asset_path")).toEqual("foobar")
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "should not save on invalid", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "does not save on cancel", ->
|
||||
@model.get("chapters").add([{name: "a", asset_path: "b"}])
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$(".action-cancel").click()
|
||||
expect(@model.get("name")).not.toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar")
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "should be possible to correct validation errors", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeFalsy()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "removes all empty chapters on cancel if the model has a non-empty chapter", ->
|
||||
chapters = @model.get("chapters")
|
||||
chapters.at(0).set("name", "non-empty")
|
||||
@model.setOriginalAttributes()
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.first().get('name')).toEqual("non-empty")
|
||||
|
||||
it "removes all empty chapters on cancel except one if the model has no non-empty chapters", ->
|
||||
chapters = @model.get("chapters")
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
|
||||
describe "ListTextbooks", ->
|
||||
noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
editTextbooktpl = readFixtures('edit-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
appendSetFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
appendSetFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(editTextbooktpl))
|
||||
@collection = new TextbookSet
|
||||
@view = new ListTextbooks({collection: @collection})
|
||||
@view.render()
|
||||
expect(@view.$("input[name=textbook-name]").val()).toEqual("Life Sciences")
|
||||
|
||||
it "should allow you to create new empty chapters", ->
|
||||
@view.render()
|
||||
numChapters = @model.get("chapters").length
|
||||
@view.$(".action-add-chapter").click()
|
||||
expect(@model.get("chapters").length).toEqual(numChapters+1)
|
||||
expect(@model.get("chapters").last().isEmpty()).toBeTruthy()
|
||||
it "should scroll to newly added textbook", ->
|
||||
spyOn(ViewUtils, 'setScrollOffset')
|
||||
@view.$(".new-button").click()
|
||||
$sectionEl = @view.$el.find('section:last')
|
||||
expect($sectionEl.length).toEqual(1)
|
||||
expect(ViewUtils.setScrollOffset).toHaveBeenCalledWith($sectionEl, 0)
|
||||
|
||||
it "should save properly", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("wallflower")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.get("name")).toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("name")).toEqual("wallflower")
|
||||
expect(chapter.get("asset_path")).toEqual("foobar")
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "should not save on invalid", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "does not save on cancel", ->
|
||||
@model.get("chapters").add([{name: "a", asset_path: "b"}])
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$(".action-cancel").click()
|
||||
expect(@model.get("name")).not.toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar")
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "should be possible to correct validation errors", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeFalsy()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "removes all empty chapters on cancel if the model has a non-empty chapter", ->
|
||||
chapters = @model.get("chapters")
|
||||
chapters.at(0).set("name", "non-empty")
|
||||
@model.setOriginalAttributes()
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.first().get('name')).toEqual("non-empty")
|
||||
|
||||
it "removes all empty chapters on cancel except one if the model has no non-empty chapters", ->
|
||||
chapters = @model.get("chapters")
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
|
||||
describe "ListTextbooks", ->
|
||||
noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
editTextbooktpl = readFixtures('edit-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
appendSetFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
appendSetFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(editTextbooktpl))
|
||||
@collection = new TextbookSet
|
||||
@view = new ListTextbooks({collection: @collection})
|
||||
@view.render()
|
||||
|
||||
it "should scroll to newly added textbook", ->
|
||||
spyOn(ViewUtils, 'setScrollOffset')
|
||||
@view.$(".new-button").click()
|
||||
$sectionEl = @view.$el.find('section:last')
|
||||
expect($sectionEl.length).toEqual(1)
|
||||
expect(ViewUtils.setScrollOffset).toHaveBeenCalledWith($sectionEl, 0)
|
||||
|
||||
it "should focus first input element of newly added textbook", ->
|
||||
spyOn(jQuery.fn, 'focus').and.callThrough()
|
||||
jasmine.addMatchers
|
||||
toHaveBeenCalledOnJQueryObject: () ->
|
||||
return {
|
||||
compare: (actual, expected) ->
|
||||
return {
|
||||
pass: actual.calls && actual.calls.mostRecent() &&
|
||||
actual.calls.mostRecent().object[0] == expected[0]
|
||||
}
|
||||
it "should focus first input element of newly added textbook", ->
|
||||
spyOn(jQuery.fn, 'focus').and.callThrough()
|
||||
jasmine.addMatchers
|
||||
toHaveBeenCalledOnJQueryObject: () ->
|
||||
return {
|
||||
compare: (actual, expected) ->
|
||||
return {
|
||||
pass: actual.calls && actual.calls.mostRecent() &&
|
||||
actual.calls.mostRecent().object[0] == expected[0]
|
||||
}
|
||||
}
|
||||
@view.$(".new-button").click()
|
||||
$inputEl = @view.$el.find('section:last input:first')
|
||||
expect($inputEl.length).toEqual(1)
|
||||
# testing for element focused seems to be tricky
|
||||
# (see http://stackoverflow.com/questions/967096)
|
||||
# and the following doesn't seem to work
|
||||
# expect($inputEl).toBeFocused()
|
||||
# expect($inputEl.find(':focus').length).toEqual(1)
|
||||
expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject($inputEl)
|
||||
@view.$(".new-button").click()
|
||||
$inputEl = @view.$el.find('section:last input:first')
|
||||
expect($inputEl.length).toEqual(1)
|
||||
# testing for element focused seems to be tricky
|
||||
# (see http://stackoverflow.com/questions/967096)
|
||||
# and the following doesn't seem to work
|
||||
# expect($inputEl).toBeFocused()
|
||||
# expect($inputEl.find(':focus').length).toEqual(1)
|
||||
expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject($inputEl)
|
||||
|
||||
# describe "ListTextbooks", ->
|
||||
# noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
#
|
||||
# beforeEach ->
|
||||
# setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
# @showSpies = spyOnConstructor("ShowTextbook", ["render"])
|
||||
# @showSpies.render.and.returnValue(@showSpies) # equivalent of `return this`
|
||||
# showEl = $("<li>")
|
||||
# @showSpies.$el = showEl
|
||||
# @showSpies.el = showEl.get(0)
|
||||
# @editSpies = spyOnConstructor("EditTextbook", ["render"])
|
||||
# editEl = $("<li>")
|
||||
# @editSpies.render.and.returnValue(@editSpies)
|
||||
# @editSpies.$el = editEl
|
||||
# @editSpies.el= editEl.get(0)
|
||||
#
|
||||
# @collection = new TextbookSet
|
||||
# @view = new ListTextbooks({collection: @collection})
|
||||
# @view.render()
|
||||
#
|
||||
# it "should render the empty template if there are no textbooks", ->
|
||||
# expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
# expect(@view.$el).toContain(".new-button")
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
# # add three empty textbooks to the collection
|
||||
# @collection.add([{}, {}, {}])
|
||||
# # reset spies due to re-rendering on collection modification
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@view.$el).not.toContainText(
|
||||
# "You haven't added any textbooks to this course yet")
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render an EditTextbook view for a textbook being edited", ->
|
||||
# # add three empty textbooks to the collection: the first and third
|
||||
# # should be shown, and the second should be edited
|
||||
# @collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
# editing = @collection.at(1)
|
||||
# expect(editing.get("editing")).toBeTruthy()
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
# expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
# expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
#
|
||||
# it "should add a new textbook when the new-button is clicked", ->
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # test
|
||||
# @view.$(".new-button").click()
|
||||
#
|
||||
# expect(@collection.length).toEqual(1)
|
||||
# expect(@view.$el).toContain(@editSpies.$el)
|
||||
# expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
# describe "ListTextbooks", ->
|
||||
# noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
#
|
||||
# beforeEach ->
|
||||
# setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
# @showSpies = spyOnConstructor("ShowTextbook", ["render"])
|
||||
# @showSpies.render.and.returnValue(@showSpies) # equivalent of `return this`
|
||||
# showEl = $("<li>")
|
||||
# @showSpies.$el = showEl
|
||||
# @showSpies.el = showEl.get(0)
|
||||
# @editSpies = spyOnConstructor("EditTextbook", ["render"])
|
||||
# editEl = $("<li>")
|
||||
# @editSpies.render.and.returnValue(@editSpies)
|
||||
# @editSpies.$el = editEl
|
||||
# @editSpies.el= editEl.get(0)
|
||||
#
|
||||
# @collection = new TextbookSet
|
||||
# @view = new ListTextbooks({collection: @collection})
|
||||
# @view.render()
|
||||
#
|
||||
# it "should render the empty template if there are no textbooks", ->
|
||||
# expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
# expect(@view.$el).toContain(".new-button")
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
# # add three empty textbooks to the collection
|
||||
# @collection.add([{}, {}, {}])
|
||||
# # reset spies due to re-rendering on collection modification
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@view.$el).not.toContainText(
|
||||
# "You haven't added any textbooks to this course yet")
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render an EditTextbook view for a textbook being edited", ->
|
||||
# # add three empty textbooks to the collection: the first and third
|
||||
# # should be shown, and the second should be edited
|
||||
# @collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
# editing = @collection.at(1)
|
||||
# expect(editing.get("editing")).toBeTruthy()
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
# expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
# expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
#
|
||||
# it "should add a new textbook when the new-button is clicked", ->
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # test
|
||||
# @view.$(".new-button").click()
|
||||
#
|
||||
# expect(@collection.length).toEqual(1)
|
||||
# expect(@view.$el).toContain(@editSpies.$el)
|
||||
# expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
|
||||
|
||||
describe "EditChapter", ->
|
||||
beforeEach ->
|
||||
modal_helpers.installModalTemplates()
|
||||
@model = new Chapter
|
||||
name: "Chapter 1"
|
||||
asset_path: "/ch1.pdf"
|
||||
@collection = new ChapterSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditChapter({model: @model})
|
||||
spyOn(@view, "remove").and.callThrough()
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
window.course = new Course({name: "abcde"})
|
||||
describe "EditChapter", ->
|
||||
beforeEach ->
|
||||
modal_helpers.installModalTemplates()
|
||||
@model = new Chapter
|
||||
name: "Chapter 1"
|
||||
asset_path: "/ch1.pdf"
|
||||
@collection = new ChapterSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditChapter({model: @model})
|
||||
spyOn(@view, "remove").and.callThrough()
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
window.course = new Course({name: "abcde"})
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
delete window.course
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
delete window.course
|
||||
|
||||
it "can render", ->
|
||||
@view.render()
|
||||
expect(@view.$("input.chapter-name").val()).toEqual("Chapter 1")
|
||||
expect(@view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf")
|
||||
it "can render", ->
|
||||
@view.render()
|
||||
expect(@view.$("input.chapter-name").val()).toEqual("Chapter 1")
|
||||
expect(@view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf")
|
||||
|
||||
it "can delete itself", ->
|
||||
@view.render().$(".action-close").click()
|
||||
expect(@collection.length).toEqual(0)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
it "can delete itself", ->
|
||||
@view.render().$(".action-close").click()
|
||||
expect(@collection.length).toEqual(0)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
|
||||
# it "can open an upload dialog", ->
|
||||
# uploadSpies = spyOnConstructor("UploadDialog", ["show", "el"])
|
||||
# uploadSpies.show.and.returnValue(uploadSpies)
|
||||
#
|
||||
# @view.render().$(".action-upload").click()
|
||||
# ctorOptions = uploadSpies.constructor.calls.mostRecent().args[0]
|
||||
# expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
# expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
# expect(uploadSpies.show).toHaveBeenCalled()
|
||||
# it "can open an upload dialog", ->
|
||||
# uploadSpies = spyOnConstructor("UploadDialog", ["show", "el"])
|
||||
# uploadSpies.show.and.returnValue(uploadSpies)
|
||||
#
|
||||
# @view.render().$(".action-upload").click()
|
||||
# ctorOptions = uploadSpies.constructor.calls.mostRecent().args[0]
|
||||
# expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
# expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
# expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
# Disabling because this test does not close the modal dialog. This can cause
|
||||
# tests that run after it to fail (see STUD-1963).
|
||||
xit "saves content when opening upload dialog", ->
|
||||
@view.render()
|
||||
@view.$("input.chapter-name").val("rainbows")
|
||||
@view.$("input.chapter-asset-path").val("unicorns")
|
||||
@view.$(".action-upload").click()
|
||||
expect(@model.get("name")).toEqual("rainbows")
|
||||
expect(@model.get("asset_path")).toEqual("unicorns")
|
||||
# Disabling because this test does not close the modal dialog. This can cause
|
||||
# tests that run after it to fail (see STUD-1963).
|
||||
xit "saves content when opening upload dialog", ->
|
||||
@view.render()
|
||||
@view.$("input.chapter-name").val("rainbows")
|
||||
@view.$("input.chapter-asset-path").val("unicorns")
|
||||
@view.$(".action-upload").click()
|
||||
expect(@model.get("name")).toEqual("rainbows")
|
||||
expect(@model.get("asset_path")).toEqual("unicorns")
|
||||
|
||||
@@ -1,133 +1,135 @@
|
||||
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/modal_helpers"], (FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) ->
|
||||
define ["js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/modal_helpers"],
|
||||
(FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) ->
|
||||
|
||||
describe "UploadDialog", ->
|
||||
tpl = readFixtures("upload-dialog.underscore")
|
||||
describe "UploadDialog", ->
|
||||
tpl = readFixtures("upload-dialog.underscore")
|
||||
|
||||
beforeEach ->
|
||||
modal_helpers.installModalTemplates()
|
||||
appendSetFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
@model = new FileUpload(
|
||||
mimeTypes: ['application/pdf']
|
||||
)
|
||||
@dialogResponse = dialogResponse = []
|
||||
@mockFiles = []
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
modal_helpers.cancelModalIfShowing()
|
||||
|
||||
createTestView = (test) ->
|
||||
view = new UploadDialog(
|
||||
model: test.model,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
onSuccess: (response) =>
|
||||
test.dialogResponse.push(response.response)
|
||||
)
|
||||
spyOn(view, 'remove').and.callThrough()
|
||||
|
||||
# create mock file input, so that we aren't subject to browser restrictions
|
||||
mockFileInput = jasmine.createSpy('mockFileInput')
|
||||
mockFileInput.files = test.mockFiles
|
||||
jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith'])
|
||||
jqMockFileInput.get.and.returnValue(mockFileInput)
|
||||
originalView$ = view.$
|
||||
spyOn(view, "$").and.callFake (selector) ->
|
||||
if selector == "input[type=file]"
|
||||
jqMockFileInput
|
||||
else
|
||||
originalView$.apply(this, arguments)
|
||||
@lastView = view
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render without a file selected", ->
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render with a PDF selected", ->
|
||||
view = createTestView(this)
|
||||
file = {name: "fake.pdf", "type": "application/pdf"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).not.toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type selected", ->
|
||||
view = createTestView(this)
|
||||
file = {name: "fake.png", "type": "image/png"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type after a correct file type selected", ->
|
||||
view = createTestView(this)
|
||||
correctFile = {name: "fake.pdf", "type": "application/pdf"}
|
||||
inCorrectFile = {name: "fake.png", "type": "image/png"}
|
||||
event = {}
|
||||
view.render()
|
||||
|
||||
event.target = {"files": [correctFile]}
|
||||
view.selectFile(event)
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).not.toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
realMethod = @model.set
|
||||
spyOn(@model, "set").and.callFake (data) ->
|
||||
if data.selectedFile != undefined
|
||||
this.attributes.selectedFile = data.selectedFile
|
||||
this.changed = {}
|
||||
else
|
||||
realMethod.apply(this, arguments)
|
||||
|
||||
event.target = {"files": [inCorrectFile]}
|
||||
view.selectFile(event)
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
describe "Uploads", ->
|
||||
beforeEach ->
|
||||
@clock = sinon.useFakeTimers()
|
||||
modal_helpers.installModalTemplates()
|
||||
appendSetFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
@model = new FileUpload(
|
||||
mimeTypes: ['application/pdf']
|
||||
)
|
||||
@dialogResponse = dialogResponse = []
|
||||
@mockFiles = []
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
modal_helpers.cancelModalIfShowing()
|
||||
@clock.restore()
|
||||
|
||||
it "can upload correctly", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
expect(@model.get("uploading")).toBeTruthy()
|
||||
AjaxHelpers.expectRequest(requests, "POST", "/upload")
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"})
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
expect(@model.get("finished")).toBeTruthy()
|
||||
expect(@dialogResponse.pop()).toEqual("dummy_response")
|
||||
createTestView = (test) ->
|
||||
view = new UploadDialog(
|
||||
model: test.model,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
onSuccess: (response) =>
|
||||
test.dialogResponse.push(response.response)
|
||||
)
|
||||
spyOn(view, 'remove').and.callThrough()
|
||||
|
||||
it "can handle upload errors", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
AjaxHelpers.respondWithError(requests)
|
||||
expect(@model.get("title")).toMatch(/error/)
|
||||
expect(view.remove).not.toHaveBeenCalled()
|
||||
# create mock file input, so that we aren't subject to browser restrictions
|
||||
mockFileInput = jasmine.createSpy('mockFileInput')
|
||||
mockFileInput.files = test.mockFiles
|
||||
jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith'])
|
||||
jqMockFileInput.get.and.returnValue(mockFileInput)
|
||||
originalView$ = view.$
|
||||
spyOn(view, "$").and.callFake (selector) ->
|
||||
if selector == "input[type=file]"
|
||||
jqMockFileInput
|
||||
else
|
||||
originalView$.apply(this, arguments)
|
||||
@lastView = view
|
||||
|
||||
it "removes itself after two seconds on successful upload", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"})
|
||||
expect(modal_helpers.isShowingModal(view)).toBeTruthy();
|
||||
@clock.tick(2001)
|
||||
expect(modal_helpers.isShowingModal(view)).toBeFalsy();
|
||||
describe "Basic", ->
|
||||
it "should render without a file selected", ->
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render with a PDF selected", ->
|
||||
view = createTestView(this)
|
||||
file = {name: "fake.pdf", "type": "application/pdf"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).not.toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type selected", ->
|
||||
view = createTestView(this)
|
||||
file = {name: "fake.png", "type": "image/png"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type after a correct file type selected", ->
|
||||
view = createTestView(this)
|
||||
correctFile = {name: "fake.pdf", "type": "application/pdf"}
|
||||
inCorrectFile = {name: "fake.png", "type": "image/png"}
|
||||
event = {}
|
||||
view.render()
|
||||
|
||||
event.target = {"files": [correctFile]}
|
||||
view.selectFile(event)
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).not.toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
realMethod = @model.set
|
||||
spyOn(@model, "set").and.callFake (data) ->
|
||||
if data.selectedFile != undefined
|
||||
this.attributes.selectedFile = data.selectedFile
|
||||
this.changed = {}
|
||||
else
|
||||
realMethod.apply(this, arguments)
|
||||
|
||||
event.target = {"files": [inCorrectFile]}
|
||||
view.selectFile(event)
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
describe "Uploads", ->
|
||||
beforeEach ->
|
||||
@clock = sinon.useFakeTimers()
|
||||
|
||||
afterEach ->
|
||||
modal_helpers.cancelModalIfShowing()
|
||||
@clock.restore()
|
||||
|
||||
it "can upload correctly", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
expect(@model.get("uploading")).toBeTruthy()
|
||||
AjaxHelpers.expectRequest(requests, "POST", "/upload")
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"})
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
expect(@model.get("finished")).toBeTruthy()
|
||||
expect(@dialogResponse.pop()).toEqual("dummy_response")
|
||||
|
||||
it "can handle upload errors", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
AjaxHelpers.respondWithError(requests)
|
||||
expect(@model.get("title")).toMatch(/error/)
|
||||
expect(view.remove).not.toHaveBeenCalled()
|
||||
|
||||
it "removes itself after two seconds on successful upload", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"})
|
||||
expect(modal_helpers.isShowingModal(view)).toBeTruthy();
|
||||
@clock.tick(2001)
|
||||
expect(modal_helpers.isShowingModal(view)).toBeFalsy();
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
|
||||
"js/views/xblock", "js/views/modals/edit_xblock"],
|
||||
($, _, gettext, XBlock, XBlockView, EditXBlockModal) ->
|
||||
class ModuleEdit extends XBlockView
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
|
||||
events:
|
||||
"click .edit-button": 'clickEditButton'
|
||||
"click .delete-button": 'onDelete'
|
||||
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@render()
|
||||
|
||||
loadDisplay: ->
|
||||
# Not all components render an inline student view, e.g. child containers which
|
||||
# instead render a link to a separate container page.
|
||||
xblockElement = @$el.find('.xblock-student_view')
|
||||
if xblockElement.length > 0
|
||||
XBlock.initializeBlock(xblockElement)
|
||||
|
||||
createItem: (parent, payload, callback=->) ->
|
||||
payload.parent_locator = parent
|
||||
$.postJSON(
|
||||
@model.urlRoot + '/'
|
||||
payload
|
||||
(data) =>
|
||||
@model.set(id: data.locator)
|
||||
@$el.data('locator', data.locator)
|
||||
@$el.data('courseKey', data.courseKey)
|
||||
@render()
|
||||
).success(callback)
|
||||
|
||||
loadView: (viewName, target, callback) ->
|
||||
if @model.id
|
||||
$.ajax(
|
||||
url: "#{decodeURIComponent(@model.url())}/#{viewName}"
|
||||
type: 'GET'
|
||||
cache: false
|
||||
headers:
|
||||
Accept: 'application/json'
|
||||
success: (fragment) =>
|
||||
@renderXBlockFragment(fragment, target).done(callback)
|
||||
)
|
||||
|
||||
render: -> @loadView('student_view', @$el, =>
|
||||
@loadDisplay()
|
||||
@delegateEvents()
|
||||
)
|
||||
|
||||
clickEditButton: (event) ->
|
||||
event.preventDefault()
|
||||
modal = new EditXBlockModal();
|
||||
modal.edit(this.$el, self.model, { refresh: _.bind(@render, this) })
|
||||
@@ -1,133 +0,0 @@
|
||||
define ["underscore", "jquery", "jquery.ui", "backbone", "common/js/components/views/feedback_prompt",
|
||||
"common/js/components/views/feedback_notification", "coffee/src/views/module_edit", "js/models/module_info", "js/utils/module"],
|
||||
(_, $, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) ->
|
||||
class TabsEdit extends Backbone.View
|
||||
|
||||
initialize: (options) =>
|
||||
@$('.component').each((idx, element) =>
|
||||
model = new ModuleModel({
|
||||
id: $(element).data('locator')
|
||||
})
|
||||
|
||||
new ModuleEditView(
|
||||
el: element,
|
||||
onDelete: @deleteTab,
|
||||
model: model
|
||||
)
|
||||
)
|
||||
@options = _.extend({}, options)
|
||||
@options.mast.find('.new-tab').on('click', @addNewTab)
|
||||
$('.add-pages .new-tab').on('click', @addNewTab)
|
||||
$('.toggle-checkbox').on('click', @toggleVisibilityOfTab)
|
||||
@$('.course-nav-list').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: @tabMoved
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .is-movable'
|
||||
)
|
||||
|
||||
toggleVisibilityOfTab: (event, ui) =>
|
||||
checkbox_element = event.target
|
||||
tab_element = $(checkbox_element).parents(".course-tab")[0]
|
||||
|
||||
saving = new NotificationView.Mini({title: gettext("Saving")})
|
||||
saving.show()
|
||||
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: @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(=> saving.hide())
|
||||
|
||||
tabMoved: (event, ui) =>
|
||||
tabs = []
|
||||
@$('.course-tab').each((idx, element) =>
|
||||
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()
|
||||
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: @model.url(),
|
||||
data: JSON.stringify({
|
||||
tabs : tabs
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).success(=> saving.hide())
|
||||
|
||||
addNewTab: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
editor = new ModuleEditView(
|
||||
onDelete: @deleteTab
|
||||
model: new ModuleModel()
|
||||
)
|
||||
|
||||
$('.new-component-item').before(editor.$el)
|
||||
editor.$el.addClass('course-tab is-movable')
|
||||
editor.$el.addClass('new')
|
||||
setTimeout(=>
|
||||
editor.$el.removeClass('new')
|
||||
, 1000)
|
||||
$('html, body').animate {scrollTop: $('.new-component-item').offset().top}, 500
|
||||
|
||||
|
||||
editor.createItem(
|
||||
@model.get('id'),
|
||||
{category: 'static_tab'}
|
||||
)
|
||||
|
||||
analytics.track "Added Page",
|
||||
course: course_location_analytics
|
||||
|
||||
deleteTab: (event) =>
|
||||
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: (view) ->
|
||||
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()
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: ModuleUtils.getUpdateUrl($component.data('locator'))
|
||||
}).success(=>
|
||||
$component.remove()
|
||||
deleting.hide()
|
||||
)
|
||||
secondary: [
|
||||
text: gettext('Cancel')
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
]
|
||||
confirm.show()
|
||||
@@ -1,96 +0,0 @@
|
||||
define [
|
||||
"jquery", "backbone", "xblock/runtime.v1", "URI", "gettext",
|
||||
"js/utils/modal", "common/js/components/views/feedback_notification"
|
||||
], ($, Backbone, XBlock, URI, gettext, ModalUtils, NotificationView) ->
|
||||
|
||||
@BaseRuntime = {}
|
||||
|
||||
class BaseRuntime.v1 extends XBlock.Runtime.v1
|
||||
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
|
||||
uri = URI(@handlerPrefix).segment($(element).data('usage-id'))
|
||||
.segment('handler')
|
||||
.segment(handlerName)
|
||||
if suffix? then uri.segment(suffix)
|
||||
if query? then uri.search(query)
|
||||
uri.toString()
|
||||
|
||||
constructor: () ->
|
||||
super()
|
||||
@dispatcher = _.clone(Backbone.Events)
|
||||
@listenTo('save', @_handleSave)
|
||||
@listenTo('cancel', @_handleCancel)
|
||||
@listenTo('error', @_handleError)
|
||||
@listenTo('modal-shown', (data) ->
|
||||
@modal = data)
|
||||
@listenTo('modal-hidden', () ->
|
||||
@modal = null)
|
||||
@listenTo('page-shown', (data) ->
|
||||
@page = data)
|
||||
|
||||
# Notify the Studio client-side runtime of an event so that it can update the UI in a consistent way.
|
||||
notify: (name, data) ->
|
||||
@dispatcher.trigger(name, data)
|
||||
|
||||
# Listen to a Studio event and invoke the specified callback when it is triggered.
|
||||
listenTo: (name, callback) ->
|
||||
@dispatcher.bind(name, callback, this)
|
||||
|
||||
# Refresh the view for the xblock represented by the specified element.
|
||||
refreshXBlock: (element) ->
|
||||
if @page
|
||||
@page.refreshXBlock(element)
|
||||
|
||||
_handleError: (data) ->
|
||||
message = data.message || data.msg
|
||||
if message
|
||||
# TODO: remove 'Open Assessment' specific default title
|
||||
title = data.title || gettext("OpenAssessment Save Error")
|
||||
@alert = new NotificationView.Error
|
||||
title: title
|
||||
message: message
|
||||
closeIcon: false
|
||||
shown: false
|
||||
@alert.show()
|
||||
|
||||
_handleSave: (data) ->
|
||||
# Starting to save, so show a notification
|
||||
if data.state == 'start'
|
||||
message = data.message || gettext('Saving')
|
||||
@notification = new NotificationView.Mini
|
||||
title: message
|
||||
@notification.show()
|
||||
|
||||
# Finished saving, so hide the notification and refresh appropriately
|
||||
else if data.state == 'end'
|
||||
@_hideAlerts()
|
||||
|
||||
# Notify the modal that the save has completed so that it can hide itself
|
||||
# and then refresh the xblock.
|
||||
if @modal and @modal.onSave
|
||||
@modal.onSave()
|
||||
# ... else ask it to refresh the newly saved xblock
|
||||
else if data.element
|
||||
@refreshXBlock(data.element)
|
||||
|
||||
@notification.hide()
|
||||
|
||||
_handleCancel: () ->
|
||||
@_hideAlerts()
|
||||
if @modal
|
||||
@modal.cancel()
|
||||
@notify('modal-hidden')
|
||||
|
||||
_hideAlerts: () ->
|
||||
# Hide any alerts that are being shown
|
||||
if @alert && @alert.options.shown
|
||||
@alert.hide()
|
||||
|
||||
@PreviewRuntime = {}
|
||||
|
||||
class PreviewRuntime.v1 extends BaseRuntime.v1
|
||||
handlerPrefix: '/preview/xblock'
|
||||
|
||||
@StudioRuntime = {}
|
||||
|
||||
class StudioRuntime.v1 extends BaseRuntime.v1
|
||||
handlerPrefix: '/xblock'
|
||||
@@ -8,7 +8,7 @@ define([ // jshint ignore:line
|
||||
'js/certificates/views/certificate_details',
|
||||
'js/certificates/views/certificate_preview',
|
||||
'common/js/components/views/feedback_notification',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'common/js/spec_helpers/view_helpers',
|
||||
'js/spec_helpers/validation_helpers',
|
||||
|
||||
@@ -8,7 +8,7 @@ define([ // jshint ignore:line
|
||||
'js/certificates/collections/certificates',
|
||||
'js/certificates/views/certificate_editor',
|
||||
'common/js/components/views/feedback_notification',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'common/js/spec_helpers/view_helpers',
|
||||
'js/spec_helpers/validation_helpers',
|
||||
|
||||
@@ -7,7 +7,7 @@ define([ // jshint ignore:line
|
||||
'js/certificates/views/certificate_preview',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'common/js/spec_helpers/view_helpers',
|
||||
'common/js/spec_helpers/ajax_helpers'
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
|
||||
],
|
||||
function(_, $, Course, CertificatePreview, TemplateHelpers, ViewHelpers, AjaxHelpers) {
|
||||
'use strict';
|
||||
|
||||
@@ -11,7 +11,7 @@ define([ // jshint ignore:line
|
||||
'js/certificates/views/certificates_list',
|
||||
'js/certificates/views/certificate_preview',
|
||||
'common/js/components/views/feedback_notification',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'js/certificates/spec/custom_matchers'
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
define([
|
||||
'js/models/explicit_url', 'coffee/src/views/tabs', 'xmodule', 'coffee/src/main', 'xblock/cms.runtime.v1'
|
||||
'js/models/explicit_url', 'js/views/tabs', 'xmodule', 'coffee/src/main', 'xblock/cms.runtime.v1'
|
||||
], function (TabsModel, TabsEditView, xmoduleLoader) {
|
||||
'use strict';
|
||||
return function (courseLocation, explicitUrl) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
define(["js/utils/drag_and_drop", "common/js/components/views/feedback_notification", "common/js/spec_helpers/ajax_helpers", "jquery", "underscore"],
|
||||
define(["js/utils/drag_and_drop", "common/js/components/views/feedback_notification",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "jquery", "underscore"],
|
||||
function (ContentDragger, Notification, AjaxHelpers, $, _) {
|
||||
describe("Overview drag and drop functionality", function () {
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'js/views/video/transcripts/utils',
|
||||
'js/views/video/transcripts/metadata_videolist', 'js/models/metadata',
|
||||
'js/views/abstract_editor',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'xmodule'
|
||||
],
|
||||
function ($, _, Utils, VideoList, MetadataModel, AbstractEditor, AjaxHelpers) {
|
||||
function ($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
'use strict';
|
||||
describe('CMS.Views.Metadata.VideoList', function () {
|
||||
var videoListEntryTemplate = readFixtures(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", "js/views/assets",
|
||||
define([ "jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "URI", "js/views/assets",
|
||||
"js/collections/asset", "common/js/spec_helpers/view_helpers"],
|
||||
function ($, AjaxHelpers, URI, AssetsView, AssetCollection, ViewHelpers) {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define([ "jquery", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
|
||||
define([ "jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/edit_helpers",
|
||||
"js/views/container", "js/models/xblock_info", "jquery.simulate",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function ($, AjaxHelpers, EditHelpers, ContainerView, XBlockInfo) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
define([
|
||||
'underscore', 'js/models/course', 'js/models/group_configuration', 'js/models/group',
|
||||
'js/collections/group_configuration', 'js/collections/group',
|
||||
'js/views/group_configuration_details', 'js/views/group_configurations_list', 'js/views/group_configuration_editor',
|
||||
'js/views/group_configuration_item', 'js/views/experiment_group_edit', 'js/views/content_group_list',
|
||||
'js/views/content_group_details', 'js/views/content_group_editor', 'js/views/content_group_item',
|
||||
'common/js/components/views/feedback_notification', 'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/template_helpers',
|
||||
'common/js/spec_helpers/view_helpers'
|
||||
'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers',
|
||||
'common/js/spec_helpers/view_helpers', 'js/models/course', 'js/models/group_configuration', 'js/models/group',
|
||||
'js/collections/group_configuration', 'js/collections/group', 'js/views/group_configuration_details',
|
||||
'js/views/group_configurations_list', 'js/views/group_configuration_editor', 'js/views/group_configuration_item',
|
||||
'js/views/experiment_group_edit', 'js/views/content_group_list', 'js/views/content_group_details',
|
||||
'js/views/content_group_editor', 'js/views/content_group_item'
|
||||
], function(
|
||||
_, Course, GroupConfigurationModel, GroupModel, GroupConfigurationCollection, GroupCollection,
|
||||
GroupConfigurationDetailsView, GroupConfigurationsListView, GroupConfigurationEditorView,
|
||||
GroupConfigurationItemView, ExperimentGroupEditView, GroupList, ContentGroupDetailsView,
|
||||
ContentGroupEditorView, ContentGroupItemView, Notification, AjaxHelpers, TemplateHelpers, ViewHelpers
|
||||
_, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel,
|
||||
GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView,
|
||||
GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList,
|
||||
ContentGroupDetailsView, ContentGroupEditorView, ContentGroupItemView
|
||||
) {
|
||||
'use strict';
|
||||
var SELECTORS = {
|
||||
@@ -286,7 +285,7 @@ define([
|
||||
it('should hide empty usage appropriately', function() {
|
||||
this.model.set('showGroups', true);
|
||||
this.view.$('.hide-groups').click();
|
||||
assertHideEmptyUsages(this.view)
|
||||
assertHideEmptyUsages(this.view);
|
||||
});
|
||||
|
||||
it('should show non-empty usage appropriately', function() {
|
||||
@@ -298,7 +297,7 @@ define([
|
||||
this.view,
|
||||
'This Group Configuration is used in:',
|
||||
'Cannot delete when in use by an experiment'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should hide non-empty usage appropriately', function() {
|
||||
@@ -922,7 +921,7 @@ define([
|
||||
this.view,
|
||||
'This content group is used in:',
|
||||
'Cannot delete when in use by a unit'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should hide non-empty usage appropriately', function() {
|
||||
@@ -988,7 +987,7 @@ define([
|
||||
notificationSpy = ViewHelpers.createNotificationSpy();
|
||||
this.view.$(SELECTORS.inputName).val('New Content Group');
|
||||
|
||||
ViewHelpers.submitAndVerifyFormError(this.view, requests, notificationSpy)
|
||||
ViewHelpers.submitAndVerifyFormError(this.view, requests, notificationSpy);
|
||||
});
|
||||
|
||||
it('does not save on cancel', function() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
define(['jquery', 'js/factories/login', 'common/js/spec_helpers/ajax_helpers', 'common/js/components/utils/view_utils'],
|
||||
define(['jquery', 'js/factories/login', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/components/utils/view_utils'],
|
||||
function($, LoginFactory, AjaxHelpers, ViewUtils) {
|
||||
'use strict';
|
||||
describe("Studio Login Page", function() {
|
||||
|
||||
@@ -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/modals/edit_xblock", "js/models/xblock_info"],
|
||||
function ($, _, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) {
|
||||
|
||||
|
||||
265
cms/static/js/spec/views/module_edit_spec.js
Normal file
265
cms/static/js/spec/views/module_edit_spec.js
Normal file
@@ -0,0 +1,265 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'common/js/components/utils/view_utils', 'js/spec_helpers/edit_helpers',
|
||||
'js/views/module_edit', 'js/models/module_info', 'xmodule'],
|
||||
function($, ViewUtils, edit_helpers, ModuleEdit, ModuleModel) {
|
||||
describe('ModuleEdit', function() {
|
||||
beforeEach(function() {
|
||||
this.stubModule = new ModuleModel({
|
||||
id: 'stub-id'
|
||||
});
|
||||
setFixtures('<ul>\n' +
|
||||
'<li class="component" id="stub-id" data-locator="stub-id">\n' +
|
||||
' <div class="component-editor">\n' +
|
||||
' <div class="module-editor">\n' +
|
||||
' ${editor}\n' +
|
||||
' </div>\n' +
|
||||
' <a href="#" class="save-button">Save</a>\n' +
|
||||
' <a href="#" class="cancel-button">Cancel</a>\n' +
|
||||
' </div>\n' +
|
||||
' <div class="component-actions">\n' +
|
||||
' <a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>\n' +
|
||||
' <a href="#" class="delete-button"><span class="delete-icon white">' +
|
||||
'</span>Delete</a>\n' +
|
||||
' </div>\n' +
|
||||
' <span class="drag-handle action"></span>\n' +
|
||||
' <section class="xblock xblock-student_view xmodule_display xmodule_stub"' +
|
||||
' data-type="StubModule">\n' +
|
||||
' <div id="stub-module-content"/>\n' +
|
||||
' </section>\n' +
|
||||
'</li>\n' +
|
||||
'</ul>');
|
||||
edit_helpers.installEditTemplates(true);
|
||||
spyOn($, 'ajax').and.returnValue(this.moduleData);
|
||||
this.moduleEdit = new ModuleEdit({
|
||||
el: $('.component'),
|
||||
model: this.stubModule,
|
||||
onDelete: jasmine.createSpy()
|
||||
});
|
||||
return this.moduleEdit;
|
||||
});
|
||||
describe('class definition', function() {
|
||||
it('sets the correct tagName', function() {
|
||||
return expect(this.moduleEdit.tagName).toEqual('li');
|
||||
});
|
||||
it('sets the correct className', function() {
|
||||
return expect(this.moduleEdit.className).toEqual('component');
|
||||
});
|
||||
});
|
||||
describe('methods', function() {
|
||||
describe('initialize', function() {
|
||||
beforeEach(function() {
|
||||
spyOn(ModuleEdit.prototype, 'render');
|
||||
this.moduleEdit = new ModuleEdit({
|
||||
el: $('.component'),
|
||||
model: this.stubModule,
|
||||
onDelete: jasmine.createSpy()
|
||||
});
|
||||
return this.moduleEdit;
|
||||
});
|
||||
it('renders the module editor', function() {
|
||||
return expect(ModuleEdit.prototype.render).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', function() {
|
||||
beforeEach(function() {
|
||||
spyOn(this.moduleEdit, 'loadDisplay');
|
||||
spyOn(this.moduleEdit, 'delegateEvents');
|
||||
spyOn($.fn, 'append');
|
||||
spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise());
|
||||
window.MockXBlock = function() {
|
||||
return {};
|
||||
};
|
||||
window.loadedXBlockResources = void 0;
|
||||
this.moduleEdit.render();
|
||||
return $.ajax.calls.mostRecent().args[0].success({
|
||||
html: '<div>Response html</div>',
|
||||
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(function() {
|
||||
window.MockXBlock = null;
|
||||
return window.MockXBlock;
|
||||
});
|
||||
it('loads the module preview via ajax on the view element', function() {
|
||||
expect($.ajax).toHaveBeenCalledWith({
|
||||
url: '/xblock/' + this.moduleEdit.model.id + '/student_view',
|
||||
type: 'GET',
|
||||
cache: false,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
success: jasmine.any(Function)
|
||||
});
|
||||
expect($.ajax).not.toHaveBeenCalledWith({
|
||||
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
|
||||
type: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
success: jasmine.any(Function)
|
||||
});
|
||||
expect(this.moduleEdit.loadDisplay).toHaveBeenCalled();
|
||||
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
|
||||
});
|
||||
it('loads the editing view via ajax on demand', function() {
|
||||
var mockXBlockEditorHtml;
|
||||
edit_helpers.installEditTemplates(true);
|
||||
expect($.ajax).not.toHaveBeenCalledWith({
|
||||
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
|
||||
type: 'GET',
|
||||
cache: false,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
success: jasmine.any(Function)
|
||||
});
|
||||
this.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/' + this.moduleEdit.model.id + '/studio_view',
|
||||
type: 'GET',
|
||||
cache: false,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
success: jasmine.any(Function)
|
||||
});
|
||||
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
|
||||
});
|
||||
it('loads inline css from fragments', function() {
|
||||
var args = "<style type='text/css'>inline-css</style>";
|
||||
return expect($('head').append).toHaveBeenCalledWith(args);
|
||||
});
|
||||
it('loads css urls from fragments', function() {
|
||||
var args = "<link rel='stylesheet' href='css-url' type='text/css'>";
|
||||
return expect($('head').append).toHaveBeenCalledWith(args);
|
||||
});
|
||||
it('loads inline js from fragments', function() {
|
||||
return expect($('head').append).toHaveBeenCalledWith('<script>inline-js</script>');
|
||||
});
|
||||
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: '<div>Response html 2</div>',
|
||||
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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
111
cms/static/js/views/module_edit.js
Normal file
111
cms/static/js/views/module_edit.js
Normal file
@@ -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);
|
||||
203
cms/static/js/views/tabs.js
Normal file
203
cms/static/js/views/tabs.js
Normal file
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ var options = {
|
||||
],
|
||||
|
||||
runFiles: [
|
||||
{pattern: 'coffee/spec/main_squire.js', included: true}
|
||||
{pattern: 'cms/js/spec/main_squire.js', included: true}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ body, input, button {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
// we want to hide the outline on the focusable <main> element
|
||||
main {
|
||||
// removing the outline on any element that we make programmatically focusable
|
||||
[tabindex="-1"] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by a unit') %>">
|
||||
<button class="delete action-icon is-disabled" aria-disabled="true" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
|
||||
<button class="delete action-icon is-disabled" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by an experiment') %>">
|
||||
<button class="delete action-icon is-disabled" aria-disabled="true" aria-hidden="true" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o"></span></button>
|
||||
<button class="delete action-icon is-disabled" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
0
common/djangoapps/config_models/tests/__init__.py
Normal file
0
common/djangoapps/config_models/tests/__init__.py
Normal file
14
common/djangoapps/config_models/tests/data/data.json
Normal file
14
common/djangoapps/config_models/tests/data/data.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"model": "config_models.ExampleDeserializeConfig",
|
||||
"data": [
|
||||
{
|
||||
"name": "betty",
|
||||
"enabled": true,
|
||||
"int_field": 5
|
||||
},
|
||||
{
|
||||
"name": "fred",
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
69
common/djangoapps/config_models/utils.py
Normal file
69
common/djangoapps/config_models/utils.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -166,7 +166,7 @@ else:
|
||||
%></%def>
|
||||
|
||||
<%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>
|
||||
|
||||
<%def name="get_released_languages()"><%
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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, {}))
|
||||
|
||||
80
common/lib/xmodule/xmodule/block_metadata_utils.py
Normal file
80
common/lib/xmodule/xmodule/block_metadata_utils.py
Normal file
@@ -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:
|
||||
<span class="course-name">${course.display_name_with_default_escaped}</span>
|
||||
|
||||
After:
|
||||
<span class="course-name">${course.display_name_with_default | h}</span>
|
||||
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('>', '>')
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
<span class="course-name">${course.display_name_with_default_escaped}</span>
|
||||
|
||||
After:
|
||||
<span class="course-name">${course.display_name_with_default | h}</span>
|
||||
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.
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -55,8 +55,9 @@
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
<div class="subtitles">
|
||||
<ol class="subtitles-menu"><li></li></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +109,9 @@
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<ol class="subtitles"><li></li></ol>
|
||||
<div class="subtitles">
|
||||
<ol class="subtitles-menu"><li></li></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 [
|
||||
'<li data-speed="', speed, '">',
|
||||
'<button class="control speed-option" tabindex="-1">',
|
||||
speed, 'x',
|
||||
'</button>',
|
||||
'</li>'
|
||||
].join('');
|
||||
return HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<li data-speed="{speed}">',
|
||||
'<button class="control speed-option" tabindex="-1" aria-pressed="false">',
|
||||
'{speed}x',
|
||||
'</button>',
|
||||
'</li>'
|
||||
].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;
|
||||
},
|
||||
|
||||
@@ -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 = [
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<div class="lang menu-container" role="application">',
|
||||
'<p class="sr instructions" id="lang-instructions"></p>',
|
||||
'<button class="control language-menu" aria-disabled="false"',
|
||||
'aria-describedby="lang-instructions" ',
|
||||
'title="',
|
||||
gettext('Open language menu'),
|
||||
'">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('');
|
||||
var langHtml = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<div class="lang menu-container" role="application">',
|
||||
'<p class="sr instructions" id="lang-instructions"></p>',
|
||||
'<button class="control language-menu" aria-disabled="false"',
|
||||
'aria-describedby="lang-instructions" ',
|
||||
'title="{langTitle}">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
{
|
||||
langTitle: gettext('Open language menu')
|
||||
}
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
var template = [
|
||||
'<div class="subtitles" role="region" id="transcript-' + this.state.id + '">',
|
||||
'<h3 id="transcript-label-' + this.state.id + '" class="transcript-title sr"></h3>',
|
||||
'<ol id="transcript-captions" class="subtitles-menu" lang="' + this.state.lang + '"></ol>',
|
||||
'</div>'
|
||||
].join('');
|
||||
var subtitlesHtml = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<div class="subtitles" role="region" id="transcript-{courseId}">',
|
||||
'<h3 id="transcript-label-{courseId}" class="transcript-title sr"></h3>',
|
||||
'<ol id="transcript-captions" class="subtitles-menu" lang="{courseLang}"></ol>',
|
||||
'</div>'
|
||||
].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('<li></li>');
|
||||
HtmlUtils.setHtml(
|
||||
self.subtitlesEl.find('.subtitles-menu'),
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<li>'),
|
||||
gettext('Transcript will be displayed when you start playing the video.'),
|
||||
HtmlUtils.HTML('</li>')
|
||||
)
|
||||
);
|
||||
} 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 = $('<ol class="langs-list menu">'),
|
||||
currentLang = state.getCurrentLanguage();
|
||||
$menu = $('<ol class="langs-list menu">'),
|
||||
currentLang = state.getCurrentLanguage(),
|
||||
$li, $link, linkHtml;
|
||||
|
||||
if (_.keys(languages).length < 2) {
|
||||
// Remove the menu toggle button
|
||||
@@ -661,20 +689,29 @@
|
||||
this.showLanguageMenu = true;
|
||||
|
||||
$.each(languages, function(code, label) {
|
||||
var li = $('<li data-lang-code="' + code + '" />'),
|
||||
link = $('<button class="control control-lang">' + label + '</button>');
|
||||
$li = $('<li />', { 'data-lang-code': code });
|
||||
linkHtml = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<button class="control control-lang">'),
|
||||
label,
|
||||
HtmlUtils.HTML('</button>')
|
||||
);
|
||||
$link = $(linkHtml.toString());
|
||||
|
||||
if (currentLang === code) {
|
||||
li.addClass('is-active');
|
||||
$li.addClass('is-active');
|
||||
$link.attr('aria-pressed', 'true');
|
||||
}
|
||||
|
||||
li.append(link);
|
||||
menu.append(li);
|
||||
$li.append($link);
|
||||
$menu.append($li);
|
||||
});
|
||||
|
||||
HtmlUtils.append(
|
||||
this.languageChooserEl,
|
||||
HtmlUtils.HTML($menu)
|
||||
);
|
||||
|
||||
this.languageChooserEl.append(menu);
|
||||
|
||||
menu.on('click', '.control-lang', function (e) {
|
||||
$menu.on('click', '.control-lang', function (e) {
|
||||
var el = $(e.currentTarget).parent(),
|
||||
state = self.state,
|
||||
langCode = el.data('lang-code');
|
||||
@@ -683,7 +720,11 @@
|
||||
state.lang = langCode;
|
||||
el .addClass('is-active')
|
||||
.siblings('li')
|
||||
.removeClass('is-active');
|
||||
.removeClass('is-active')
|
||||
.find('.control-lang')
|
||||
.attr('aria-pressed', 'false');
|
||||
|
||||
$(e.currentTarget).attr('aria-pressed', 'true');
|
||||
|
||||
state.el.trigger('language_menu:change', [langCode]);
|
||||
self.fetchCaption();
|
||||
@@ -693,6 +734,7 @@
|
||||
|
||||
// update the transcript lang attribute
|
||||
self.subtitlesMenuEl.attr('lang', langCode);
|
||||
self.closeLanguageMenu(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -715,13 +757,18 @@
|
||||
'data-index': index,
|
||||
'data-start': start[index],
|
||||
'tabindex': 0
|
||||
}).html(text);
|
||||
});
|
||||
|
||||
$(liEl).html(text);
|
||||
|
||||
return liEl[0];
|
||||
};
|
||||
|
||||
return AsyncProcess.array(captions, process).done(function (list) {
|
||||
container.append(list);
|
||||
HtmlUtils.append(
|
||||
container,
|
||||
HtmlUtils.HTML(list)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -790,17 +837,40 @@
|
||||
* out of the tabbing order.
|
||||
*
|
||||
*/
|
||||
addPaddings: function () {
|
||||
|
||||
this.subtitlesMenuEl
|
||||
.prepend(
|
||||
$('<li class="spacing"><a href="#transcript-end-' + this.state.id + '" id="transcript-start-' + this.state.id + '" class="transcript-start"></a>') // jshint ignore: line
|
||||
.height(this.topSpacingHeight())
|
||||
)
|
||||
.append(
|
||||
$('<li class="spacing"><a href="#transcript-start-' + this.state.id + '" id="transcript-end-' + this.state.id + '" class="transcript-end"></a>') // jshint ignore: line
|
||||
.height(this.bottomSpacingHeight())
|
||||
addPaddings: function() {
|
||||
var topSpacer = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<li class="spacing" style="height: {height}px">',
|
||||
'<a href="#transcript-end-{id}" id="transcript-start-{id}" class="transcript-start"></a>', // jshint ignore:line
|
||||
'</li>'
|
||||
].join('')),
|
||||
{
|
||||
id: this.state.id,
|
||||
height: this.topSpacingHeight()
|
||||
}
|
||||
);
|
||||
|
||||
var bottomSpacer = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<li class="spacing" style="height: {height}px">',
|
||||
'<a href="#transcript-start-{id}" id="transcript-end-{id}" class="transcript-end"></a>', // jshint ignore:line
|
||||
'</li>'
|
||||
].join('')),
|
||||
{
|
||||
id: this.state.id,
|
||||
height: this.bottomSpacingHeight()
|
||||
}
|
||||
);
|
||||
|
||||
HtmlUtils.prepend(
|
||||
this.subtitlesMenuEl,
|
||||
topSpacer
|
||||
);
|
||||
|
||||
HtmlUtils.append(
|
||||
this.subtitlesMenuEl,
|
||||
bottomSpacer
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -2950,10 +2950,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
output_fields = dict(jsonfields)
|
||||
for field_name, value in output_fields.iteritems():
|
||||
if value:
|
||||
field = xblock_class.fields.get(field_name)
|
||||
if field is None:
|
||||
try:
|
||||
field = xblock_class.fields.get(field_name)
|
||||
except AttributeError:
|
||||
continue
|
||||
elif isinstance(field, Reference):
|
||||
if isinstance(field, Reference):
|
||||
output_fields[field_name] = robust_usage_key(value)
|
||||
elif isinstance(field, ReferenceList):
|
||||
output_fields[field_name] = [robust_usage_key(ele) for ele in value]
|
||||
|
||||
@@ -218,7 +218,7 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin):
|
||||
|
||||
MODULESTORE = functools.partial(mixed_store_config, mkdtemp_clean(), {})
|
||||
CONTENTSTORE = functools.partial(contentstore_config)
|
||||
ENABLED_CACHES = ['mongo_metadata_inheritance', 'loc_cache']
|
||||
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
|
||||
__settings_overrides = []
|
||||
__old_modulestores = []
|
||||
__old_contentstores = []
|
||||
|
||||
@@ -210,13 +210,15 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
self._capture_basic_metrics()
|
||||
|
||||
# Is this sequential part of a timed or proctored exam?
|
||||
masquerading = context.get('specific_masquerade', False)
|
||||
special_exam_html = None
|
||||
if self.is_time_limited:
|
||||
view_html = self._time_limited_student_view(context)
|
||||
special_exam_html = self._time_limited_student_view(context)
|
||||
|
||||
# Do we have an alternate rendering
|
||||
# Do we have an applicable alternate rendering
|
||||
# from the edx_proctoring subsystem?
|
||||
if view_html:
|
||||
fragment.add_content(view_html)
|
||||
if special_exam_html and not masquerading:
|
||||
fragment.add_content(special_exam_html)
|
||||
return fragment
|
||||
|
||||
for child in display_items:
|
||||
@@ -249,6 +251,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'next_url': context.get('next_url'),
|
||||
'prev_url': context.get('prev_url'),
|
||||
'override_hidden_exam': masquerading and special_exam_html is not None,
|
||||
}
|
||||
|
||||
fragment.add_content(self.system.render_template("seq_module.html", params))
|
||||
|
||||
@@ -7,11 +7,13 @@ from unittest import TestCase
|
||||
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.course_metadata_utils import (
|
||||
clean_course_key,
|
||||
url_name_for_course_location,
|
||||
from xmodule.block_metadata_utils import (
|
||||
url_name_for_block,
|
||||
display_name_with_default,
|
||||
display_name_with_default_escaped,
|
||||
)
|
||||
from xmodule.course_metadata_utils import (
|
||||
clean_course_key,
|
||||
number_for_course_location,
|
||||
has_course_started,
|
||||
has_course_ended,
|
||||
@@ -130,9 +132,9 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
"course_MNXXK4TTMUWXMMJ2KVXGS5TFOJZWS5DZLAVUGUZNGIYDGK2ZGIYDSNQ~"
|
||||
),
|
||||
]),
|
||||
FunctionTest(url_name_for_course_location, [
|
||||
TestScenario((self.demo_course.location,), self.demo_course.location.name),
|
||||
TestScenario((self.html_course.location,), self.html_course.location.name),
|
||||
FunctionTest(url_name_for_block, [
|
||||
TestScenario((self.demo_course,), self.demo_course.location.name),
|
||||
TestScenario((self.html_course,), self.html_course.location.name),
|
||||
]),
|
||||
FunctionTest(display_name_with_default_escaped, [
|
||||
# Test course with no display name.
|
||||
|
||||
@@ -27,7 +27,7 @@ from xblock.fields import (
|
||||
|
||||
from xblock.fragment import Fragment
|
||||
from xblock.runtime import Runtime, IdReader, IdGenerator
|
||||
from xmodule import course_metadata_utils
|
||||
from xmodule import block_metadata_utils
|
||||
from xmodule.fields import RelativeTime
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -340,7 +340,7 @@ class XModuleMixin(XModuleFields, XBlock):
|
||||
|
||||
@property
|
||||
def url_name(self):
|
||||
return course_metadata_utils.url_name_for_course_location(self.location)
|
||||
return block_metadata_utils.url_name_for_block(self)
|
||||
|
||||
@property
|
||||
def display_name_with_default(self):
|
||||
@@ -348,7 +348,7 @@ class XModuleMixin(XModuleFields, XBlock):
|
||||
Return a display name for the module: use display_name if defined in
|
||||
metadata, otherwise convert the url name.
|
||||
"""
|
||||
return course_metadata_utils.display_name_with_default(self)
|
||||
return block_metadata_utils.display_name_with_default(self)
|
||||
|
||||
@property
|
||||
def display_name_with_default_escaped(self):
|
||||
@@ -363,7 +363,7 @@ class XModuleMixin(XModuleFields, XBlock):
|
||||
migrate and test switching to display_name_with_default, which is no
|
||||
longer escaped.
|
||||
"""
|
||||
return course_metadata_utils.display_name_with_default_escaped(self)
|
||||
return block_metadata_utils.display_name_with_default_escaped(self)
|
||||
|
||||
@property
|
||||
def tooltip_title(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user