Merge remote-tracking branch 'origin/feature/cale/cms-master' into
feature/dhm/cms-settings Conflicts: cms/djangoapps/contentstore/course_info_model.py cms/djangoapps/contentstore/views.py cms/static/js/models/course_info.js cms/static/js/template_loader.js cms/static/js/views/course_info_edit.js cms/templates/base.html cms/templates/course_info.html cms/urls.py
This commit is contained in:
1
common/lib/.gitignore
vendored
Normal file
1
common/lib/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*/jasmine_test_runner.html
|
||||
@@ -3,6 +3,7 @@ import platform
|
||||
import sys
|
||||
from logging.handlers import SysLogHandler
|
||||
|
||||
LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
|
||||
def get_logger_config(log_dir,
|
||||
logging_env="no_env",
|
||||
@@ -11,7 +12,8 @@ def get_logger_config(log_dir,
|
||||
dev_env=False,
|
||||
syslog_addr=None,
|
||||
debug=False,
|
||||
local_loglevel='INFO'):
|
||||
local_loglevel='INFO',
|
||||
console_loglevel=None):
|
||||
|
||||
"""
|
||||
|
||||
@@ -30,9 +32,12 @@ def get_logger_config(log_dir,
|
||||
"""
|
||||
|
||||
# Revert to INFO if an invalid string is passed in
|
||||
if local_loglevel not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
if local_loglevel not in LOG_LEVELS:
|
||||
local_loglevel = 'INFO'
|
||||
|
||||
if console_loglevel is None or console_loglevel not in LOG_LEVELS:
|
||||
console_loglevel = 'DEBUG' if debug else 'INFO'
|
||||
|
||||
hostname = platform.node().split(".")[0]
|
||||
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s "
|
||||
"[{hostname} %(process)d] [%(filename)s:%(lineno)d] "
|
||||
@@ -55,7 +60,7 @@ def get_logger_config(log_dir,
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'DEBUG' if debug else 'INFO',
|
||||
'level': console_loglevel,
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'standard',
|
||||
'stream': sys.stdout,
|
||||
|
||||
44
common/lib/xmodule/jasmine_test_runner.html.erb
Normal file
44
common/lib/xmodule/jasmine_test_runner.html.erb
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Jasmine Test Runner</title>
|
||||
<link rel="stylesheet" type="text/css" href="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.css">
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.js"></script>
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine-html.js"></script>
|
||||
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/lib/console-runner.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/ajax_prefix.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, function() {
|
||||
return "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- SOURCE FILES -->
|
||||
<% for src in js_source %>
|
||||
<script type="text/javascript" src="<%= src %>"></script>
|
||||
<% end %>
|
||||
|
||||
<!-- SPEC FILES -->
|
||||
<% for src in js_specs %>
|
||||
<script type="text/javascript" src="<%= src %>"></script>
|
||||
<% end %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="text/javascript">
|
||||
var console_reporter = new jasmine.ConsoleReporter()
|
||||
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
|
||||
jasmine.getEnv().addReporter(console_reporter);
|
||||
jasmine.getEnv().execute();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -347,7 +347,7 @@ class CapaModule(XModule):
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
|
||||
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'])
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location)
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
@@ -451,7 +451,7 @@ class CapaModule(XModule):
|
||||
new_answers = dict()
|
||||
for answer_id in answers:
|
||||
try:
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'])}
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
|
||||
except TypeError:
|
||||
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
|
||||
new_answer = {answer_id: answers[answer_id]}
|
||||
|
||||
@@ -46,6 +46,11 @@ class StaticContent(object):
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_base_url_path_for_course_assets(loc):
|
||||
if loc is not None:
|
||||
return "/c4x/{org}/{course}/asset".format(**loc.dict())
|
||||
|
||||
@staticmethod
|
||||
def get_id_from_location(location):
|
||||
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import abc
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
@@ -66,17 +65,27 @@ def grader_from_conf(conf):
|
||||
for subgraderconf in conf:
|
||||
subgraderconf = subgraderconf.copy()
|
||||
weight = subgraderconf.pop("weight", 0)
|
||||
# NOTE: 'name' used to exist in SingleSectionGrader. We are deprecating SingleSectionGrader
|
||||
# and converting everything into an AssignmentFormatGrader by adding 'min_count' and
|
||||
# 'drop_count'. AssignmentFormatGrader does not expect 'name', so if it appears
|
||||
# in bad_args, go ahead remove it (this causes no errors). Eventually, SingleSectionGrader
|
||||
# should be completely removed.
|
||||
name = 'name'
|
||||
try:
|
||||
if 'min_count' in subgraderconf:
|
||||
#This is an AssignmentFormatGrader
|
||||
subgrader_class = AssignmentFormatGrader
|
||||
elif 'name' in subgraderconf:
|
||||
elif name in subgraderconf:
|
||||
#This is an SingleSectionGrader
|
||||
subgrader_class = SingleSectionGrader
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
bad_args = invalid_args(subgrader_class.__init__, subgraderconf)
|
||||
# See note above concerning 'name'.
|
||||
if bad_args.issuperset({name}):
|
||||
bad_args = bad_args - {name}
|
||||
del subgraderconf[name]
|
||||
if len(bad_args) > 0:
|
||||
log.warning("Invalid arguments for a subgrader: %s", bad_args)
|
||||
for key in bad_args:
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<section id="problem_1" class="problems-wrapper" data-url="/problem/url/"></section>
|
||||
<section class='xmodule_display xmodule_CapaModule' data-type='Problem'>
|
||||
<section id='problem_1'
|
||||
class='problems-wrapper'
|
||||
data-problem-id='i4x://edX/101/problem/Problem1'
|
||||
data-url='/problem/Problem1'>
|
||||
</section>
|
||||
</section>
|
||||
@@ -8,25 +8,43 @@ describe 'Problem', ->
|
||||
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
|
||||
window.update_schematics = ->
|
||||
|
||||
# Load this function from spec/helper.coffee
|
||||
# Note that if your test fails with a message like:
|
||||
# 'External request attempted for blah, which is not defined.'
|
||||
# this msg is coming from the stubRequests function else clause.
|
||||
jasmine.stubRequests()
|
||||
|
||||
# note that the fixturesPath is set in spec/helper.coffee
|
||||
loadFixtures 'problem.html'
|
||||
|
||||
spyOn Logger, 'log'
|
||||
spyOn($.fn, 'load').andCallFake (url, callback) ->
|
||||
$(@).html readFixtures('problem_content.html')
|
||||
callback()
|
||||
jasmine.stubRequests()
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@problem.el).toBe '#problem_1'
|
||||
it 'set the element from html', ->
|
||||
@problem999 = new Problem ("
|
||||
<section class='xmodule_display xmodule_CapaModule' data-type='Problem'>
|
||||
<section id='problem_999'
|
||||
class='problems-wrapper'
|
||||
data-problem-id='i4x://edX/999/problem/Quiz'
|
||||
data-url='/problem/quiz/'>
|
||||
</section>
|
||||
</section>
|
||||
")
|
||||
expect(@problem999.element_id).toBe 'problem_999'
|
||||
|
||||
it 'set the element from loadFixtures', ->
|
||||
@problem1 = new Problem($('.xmodule_display'))
|
||||
expect(@problem1.element_id).toBe 'problem_1'
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
spyOn window, 'update_schematics'
|
||||
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
|
||||
it 'set mathjax typeset', ->
|
||||
expect(MathJax.Hub.Queue).toHaveBeenCalled()
|
||||
@@ -38,7 +56,7 @@ describe 'Problem', ->
|
||||
expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers
|
||||
|
||||
it 'bind the check button', ->
|
||||
expect($('section.action input.check')).toHandleWith 'click', @problem.check
|
||||
expect($('section.action input.check')).toHandleWith 'click', @problem.check_fd
|
||||
|
||||
it 'bind the reset button', ->
|
||||
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
|
||||
@@ -60,7 +78,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@bind = @problem.bind
|
||||
spyOn @problem, 'bind'
|
||||
|
||||
@@ -86,9 +104,13 @@ describe 'Problem', ->
|
||||
it 're-bind the content', ->
|
||||
expect(@problem.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'check_fd', ->
|
||||
xit 'should have specs written for this functionality', ->
|
||||
expect(false)
|
||||
|
||||
describe 'check', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_check event', ->
|
||||
@@ -98,30 +120,34 @@ describe 'Problem', ->
|
||||
it 'submit the answer for check', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.check()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_check', 'foo=1&bar=2', jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_check',
|
||||
'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
describe 'when the response is correct', ->
|
||||
it 'call render with returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'correct', contents: 'Correct!')
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
callback(success: 'correct', contents: 'Correct!')
|
||||
@problem.check()
|
||||
expect(@problem.el.html()).toEqual 'Correct!'
|
||||
|
||||
describe 'when the response is incorrect', ->
|
||||
it 'call render with returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'incorrect', contents: 'Correct!')
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
callback(success: 'incorrect', contents: 'Incorrect!')
|
||||
@problem.check()
|
||||
expect(@problem.el.html()).toEqual 'Correct!'
|
||||
expect(@problem.el.html()).toEqual 'Incorrect!'
|
||||
|
||||
describe 'when the response is undetermined', ->
|
||||
it 'alert the response', ->
|
||||
spyOn window, 'alert'
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'Number Only!')
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
callback(success: 'Number Only!')
|
||||
@problem.check()
|
||||
expect(window.alert).toHaveBeenCalledWith 'Number Only!'
|
||||
|
||||
describe 'reset', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
|
||||
it 'log the problem_reset event', ->
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
@@ -131,7 +157,8 @@ describe 'Problem', ->
|
||||
it 'POST to the problem reset page', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.reset()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_reset', { id: 1 }, jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset',
|
||||
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function)
|
||||
|
||||
it 'render the returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
@@ -141,7 +168,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'show', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.el.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
|
||||
|
||||
describe 'when the answer has not yet shown', ->
|
||||
@@ -150,12 +177,14 @@ describe 'Problem', ->
|
||||
|
||||
it 'log the problem_show event', ->
|
||||
@problem.show()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_show', problem: 1
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_show',
|
||||
problem: 'i4x://edX/101/problem/Problem1'
|
||||
|
||||
it 'fetch the answers', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.show()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_show', jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_show',
|
||||
jasmine.any(Function)
|
||||
|
||||
it 'show the answers', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
|
||||
@@ -220,7 +249,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'save', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_save event', ->
|
||||
@@ -230,7 +259,8 @@ describe 'Problem', ->
|
||||
it 'POST to save problem', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.save()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_save', 'foo=1&bar=2', jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save',
|
||||
'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
it 'alert to the user', ->
|
||||
spyOn window, 'alert'
|
||||
@@ -240,7 +270,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'refreshMath', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
$('#input_example_1').val 'E=mc^2'
|
||||
@problem.refreshMath target: $('#input_example_1').get(0)
|
||||
|
||||
@@ -250,7 +280,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'updateMathML', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@stubbedJax.root.toMathML.andReturn '<MathML>'
|
||||
|
||||
describe 'when there is no exception', ->
|
||||
@@ -270,7 +300,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'refreshAnswers', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.el.html '''
|
||||
<textarea class="CodeMirror" />
|
||||
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
|
||||
@@ -293,3 +323,6 @@ describe 'Problem', ->
|
||||
it 'serialize all answers', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two"
|
||||
|
||||
|
||||
|
||||
|
||||
76
common/lib/xmodule/xmodule/js/spec/helper.coffee
Normal file
76
common/lib/xmodule/xmodule/js/spec/helper.coffee
Normal file
@@ -0,0 +1,76 @@
|
||||
jasmine.getFixtures().fixturesPath = 'xmodule/js/fixtures'
|
||||
|
||||
jasmine.stubbedMetadata =
|
||||
slowerSpeedYoutubeId:
|
||||
id: 'slowerSpeedYoutubeId'
|
||||
duration: 300
|
||||
normalSpeedYoutubeId:
|
||||
id: 'normalSpeedYoutubeId'
|
||||
duration: 200
|
||||
bogus:
|
||||
duration: 100
|
||||
|
||||
jasmine.stubbedCaption =
|
||||
start: [0, 10000, 20000, 30000]
|
||||
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
|
||||
|
||||
jasmine.stubRequests = ->
|
||||
spyOn($, 'ajax').andCallFake (settings) ->
|
||||
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/
|
||||
settings.success jasmine.stubbedCaption
|
||||
else if settings.url.match /.+\/problem_get$/
|
||||
settings.success html: readFixtures('problem_content.html')
|
||||
else if settings.url == '/calculate' ||
|
||||
settings.url.match(/.+\/goto_position$/) ||
|
||||
settings.url.match(/event$/) ||
|
||||
settings.url.match(/.+\/problem_(check|reset|show|save)$/)
|
||||
# do nothing
|
||||
else
|
||||
throw "External request attempted for #{settings.url}, which is not defined."
|
||||
|
||||
jasmine.stubYoutubePlayer = ->
|
||||
YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
|
||||
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
|
||||
'playVideo', 'pauseVideo', 'seekTo']
|
||||
|
||||
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
|
||||
enableParts = [enableParts] unless $.isArray(enableParts)
|
||||
|
||||
suite = context.suite
|
||||
currentPartName = suite.description while suite = suite.parentSuite
|
||||
enableParts.push currentPartName
|
||||
|
||||
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
|
||||
unless $.inArray(part, enableParts) >= 0
|
||||
spyOn window, part
|
||||
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
YT.Player = undefined
|
||||
context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
|
||||
jasmine.stubYoutubePlayer()
|
||||
if createPlayer
|
||||
return new VideoPlayer(video: context.video)
|
||||
|
||||
spyOn(window, 'onunload')
|
||||
|
||||
# Stub Youtube API
|
||||
window.YT =
|
||||
PlayerState:
|
||||
UNSTARTED: -1
|
||||
ENDED: 0
|
||||
PLAYING: 1
|
||||
PAUSED: 2
|
||||
BUFFERING: 3
|
||||
CUED: 5
|
||||
|
||||
# Stub jQuery.cookie
|
||||
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
|
||||
|
||||
# Stub jQuery.qtip
|
||||
$.fn.qtip = jasmine.createSpy 'jQuery.qtip'
|
||||
|
||||
# Stub jQuery.scrollTo
|
||||
$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo'
|
||||
@@ -3,6 +3,7 @@ class @Video
|
||||
@el = $(element).find('.video')
|
||||
@id = @el.attr('id').replace(/video_/, '')
|
||||
@caption_data_dir = @el.data('caption-data-dir')
|
||||
@caption_asset_path = @el.data('caption-asset-path')
|
||||
@show_captions = @el.data('show-captions') == "true"
|
||||
window.player = null
|
||||
@el = $("#video_#{@id}")
|
||||
|
||||
@@ -10,7 +10,7 @@ class @VideoCaption extends Subview
|
||||
.bind('DOMMouseScroll', @onMovement)
|
||||
|
||||
captionURL: ->
|
||||
"/static/#{@captionDataDir}/subs/#{@youtubeId}.srt.sjson"
|
||||
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
|
||||
|
||||
render: ->
|
||||
# TODO: make it so you can have a video with no captions.
|
||||
|
||||
@@ -31,7 +31,7 @@ class @VideoPlayer extends Subview
|
||||
el: @el
|
||||
youtubeId: @video.youtubeId('1.0')
|
||||
currentSpeed: @currentSpeed()
|
||||
captionDataDir: @video.caption_data_dir
|
||||
captionAssetPath: @video.caption_asset_path
|
||||
unless onTouchBasedDevice()
|
||||
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
|
||||
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
|
||||
|
||||
76
common/lib/xmodule/xmodule/js/src/xmodule.coffee
Normal file
76
common/lib/xmodule/xmodule/js/src/xmodule.coffee
Normal file
@@ -0,0 +1,76 @@
|
||||
@XModule =
|
||||
###
|
||||
Load a single module (either an edit module or a display module)
|
||||
from the supplied element, which should have a data-type attribute
|
||||
specifying the class to load
|
||||
###
|
||||
loadModule: (element) ->
|
||||
moduleType = $(element).data('type')
|
||||
if moduleType == 'None'
|
||||
return
|
||||
|
||||
try
|
||||
module = new window[moduleType](element)
|
||||
if $(element).hasClass('xmodule_edit')
|
||||
$(document).trigger('XModule.loaded.edit', [element, module])
|
||||
|
||||
if $(element).hasClass('xmodule_display')
|
||||
$(document).trigger('XModule.loaded.display', [element, module])
|
||||
|
||||
return module
|
||||
|
||||
catch error
|
||||
console.error "Unable to load #{moduleType}: #{error.message}" if console
|
||||
|
||||
###
|
||||
Load all modules on the page of the specified type.
|
||||
If container is provided, only load modules inside that element
|
||||
Type is one of 'display' or 'edit'
|
||||
###
|
||||
loadModules: (container) ->
|
||||
selector = ".xmodule_edit, .xmodule_display"
|
||||
|
||||
if container?
|
||||
modules = $(container).find(selector)
|
||||
else
|
||||
modules = $(selector)
|
||||
|
||||
modules.each((idx, element) -> XModule.loadModule element)
|
||||
|
||||
class @XModule.Descriptor
|
||||
|
||||
###
|
||||
Register a callback method to be called when the state of this
|
||||
descriptor is updated. The callback will be passed the results
|
||||
of calling the save method on this descriptor.
|
||||
###
|
||||
onUpdate: (callback) ->
|
||||
if ! @callbacks?
|
||||
@callbacks = []
|
||||
|
||||
@callbacks.push(callback)
|
||||
|
||||
###
|
||||
Notify registered callbacks that the state of this descriptor has changed
|
||||
###
|
||||
update: =>
|
||||
data = @save()
|
||||
callback(data) for callback in @callbacks
|
||||
|
||||
###
|
||||
Bind the module to an element. This may be called multiple times,
|
||||
if the element content has changed and so the module needs to be rebound
|
||||
|
||||
@method: constructor
|
||||
@param {html element} the .xmodule_edit section containing all of the descriptor content
|
||||
###
|
||||
constructor: (@element) -> return
|
||||
|
||||
###
|
||||
Return the current state of the descriptor (to be written to the module store)
|
||||
|
||||
@method: save
|
||||
@returns {object} An object containing children and data attributes (both optional).
|
||||
The contents of the attributes will be saved to the server
|
||||
###
|
||||
save: -> return {}
|
||||
@@ -153,7 +153,7 @@ class Location(_LocationBase):
|
||||
def check(val, regexp):
|
||||
if val is not None and regexp.search(val) is not None:
|
||||
log.debug('invalid characters val="%s", list_="%s"' % (val, list_))
|
||||
raise InvalidLocationError(location)
|
||||
raise InvalidLocationError("Invalid characters in '%s'." % (val))
|
||||
|
||||
list_ = list(list_)
|
||||
for val in list_[:4] + [list_[5]]:
|
||||
|
||||
@@ -53,6 +53,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
self.unnamed = defaultdict(int) # category -> num of new url_names for that category
|
||||
self.used_names = defaultdict(set) # category -> set of used url_names
|
||||
self.org, self.course, self.url_name = course_id.split('/')
|
||||
# cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name
|
||||
self.course_id = course_id
|
||||
self.load_error_modules = load_error_modules
|
||||
|
||||
def process_xml(xml):
|
||||
@@ -303,7 +305,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
try:
|
||||
course_descriptor = self.load_course(course_dir, errorlog.tracker)
|
||||
except Exception as e:
|
||||
msg = "Failed to load course '{0}': {1}".format(course_dir, str(e))
|
||||
msg = "ERROR: Failed to load course '{0}': {1}".format(course_dir, str(e))
|
||||
log.exception(msg)
|
||||
errorlog.tracker(msg)
|
||||
|
||||
@@ -337,7 +339,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
with open(policy_path) as f:
|
||||
return json.load(f)
|
||||
except (IOError, ValueError) as err:
|
||||
msg = "Error loading course policy from {0}".format(policy_path)
|
||||
msg = "ERROR: loading course policy from {0}".format(policy_path)
|
||||
tracker(msg)
|
||||
log.warning(msg + " " + str(err))
|
||||
return {}
|
||||
@@ -455,10 +457,18 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
slug = os.path.splitext(os.path.basename(filepath))[0]
|
||||
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
|
||||
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
|
||||
# VS[compat]:
|
||||
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
|
||||
# from the course policy
|
||||
if category == "static_tab":
|
||||
for tab in course_descriptor.tabs or []:
|
||||
if tab.get('url_slug') == slug:
|
||||
module.metadata['display_name'] = tab['name']
|
||||
module.metadata['data_dir'] = course_dir
|
||||
self.modules[course_descriptor.id][module.location] = module
|
||||
except Exception, e:
|
||||
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
|
||||
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
"""
|
||||
|
||||
@@ -11,12 +11,12 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace):
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace, subpath = 'static'):
|
||||
|
||||
remap_dict = {}
|
||||
|
||||
|
||||
# now import all static assets
|
||||
static_dir = course_data_path / 'static/'
|
||||
static_dir = course_data_path / subpath
|
||||
|
||||
for dirname, dirnames, filenames in os.walk(static_dir):
|
||||
for filename in filenames:
|
||||
@@ -24,6 +24,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
|
||||
try:
|
||||
content_path = os.path.join(dirname, filename)
|
||||
fullname_with_subpath = content_path.replace(static_dir, '') # strip away leading path from the name
|
||||
if fullname_with_subpath.startswith('/'):
|
||||
fullname_with_subpath = fullname_with_subpath[1:]
|
||||
content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath)
|
||||
mime_type = mimetypes.guess_type(filename)[0]
|
||||
|
||||
@@ -88,7 +90,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace = None):
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace=None):
|
||||
"""
|
||||
Import the specified xml data_dir into the "store" modulestore,
|
||||
using org and course as the location org and course.
|
||||
@@ -125,8 +127,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
course_location = module.location
|
||||
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location)
|
||||
_namespace_rename, subpath='static')
|
||||
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
@@ -159,6 +164,16 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
|
||||
module.metadata['hide_progress_tab'] = True
|
||||
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
|
||||
@@ -192,7 +207,6 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
store.update_item(module.location, module_data)
|
||||
|
||||
|
||||
if 'children' in module.definition:
|
||||
store.update_children(module.location, module.definition['children'])
|
||||
|
||||
@@ -200,6 +214,100 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(module.own_metadata))
|
||||
|
||||
|
||||
|
||||
return module_store, course_items
|
||||
|
||||
|
||||
def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category):
|
||||
err_cnt = 0
|
||||
|
||||
parents = []
|
||||
# get all modules of parent_category
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
if module.location.category == parent_category:
|
||||
parents.append(module)
|
||||
|
||||
for parent in parents:
|
||||
for child_loc in [Location(child) for child in parent.definition.get('children', [])]:
|
||||
if child_loc.category != expected_child_category:
|
||||
err_cnt += 1
|
||||
print 'ERROR: child {0} of parent {1} was expected to be category of {2} but was {3}'.format(
|
||||
child_loc, parent.location, expected_child_category, child_loc.category)
|
||||
|
||||
return err_cnt
|
||||
|
||||
def validate_data_source_path_existence(path, is_err = True, extra_msg = None):
|
||||
_cnt = 0
|
||||
if not os.path.exists(path):
|
||||
print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if
|
||||
extra_msg is not None else ''))
|
||||
_cnt = 1
|
||||
return _cnt
|
||||
|
||||
def validate_data_source_paths(data_dir, course_dir):
|
||||
# check that there is a '/static/' directory
|
||||
course_path = data_dir / course_dir
|
||||
err_cnt = 0
|
||||
warn_cnt = 0
|
||||
err_cnt += validate_data_source_path_existence(course_path / 'static')
|
||||
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err = False,
|
||||
extra_msg = 'Video captions (if they are used) will not work unless they are static/subs.')
|
||||
return err_cnt, warn_cnt
|
||||
|
||||
|
||||
def perform_xlint(data_dir, course_dirs,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True):
|
||||
err_cnt = 0
|
||||
warn_cnt = 0
|
||||
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
course_dirs=course_dirs,
|
||||
load_error_modules=load_error_modules
|
||||
)
|
||||
|
||||
# check all data source path information
|
||||
for course_dir in course_dirs:
|
||||
_err_cnt, _warn_cnt = validate_data_source_paths(path(data_dir), course_dir)
|
||||
err_cnt += _err_cnt
|
||||
warn_cnt += _warn_cnt
|
||||
|
||||
# first count all errors and warnings as part of the XMLModuleStore import
|
||||
for err_log in module_store._location_errors.itervalues():
|
||||
for err_log_entry in err_log.errors:
|
||||
msg = err_log_entry[0]
|
||||
if msg.startswith('ERROR:'):
|
||||
err_cnt+=1
|
||||
else:
|
||||
warn_cnt+=1
|
||||
|
||||
# then count outright all courses that failed to load at all
|
||||
for err_log in module_store.errored_courses.itervalues():
|
||||
for err_log_entry in err_log.errors:
|
||||
msg = err_log_entry[0]
|
||||
print msg
|
||||
if msg.startswith('ERROR:'):
|
||||
err_cnt+=1
|
||||
else:
|
||||
warn_cnt+=1
|
||||
|
||||
for course_id in module_store.modules.keys():
|
||||
# constrain that courses only have 'chapter' children
|
||||
err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter")
|
||||
# constrain that chapters only have 'sequentials'
|
||||
err_cnt += validate_category_hierarcy(module_store, course_id, "chapter", "sequential")
|
||||
# constrain that sequentials only have 'verticals'
|
||||
err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical")
|
||||
|
||||
print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt)
|
||||
|
||||
if err_cnt > 0:
|
||||
print "This course is not suitable for importing. Please fix courseware according to specifications before importing."
|
||||
elif warn_cnt > 0:
|
||||
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
|
||||
else:
|
||||
print "This course can be imported successfully."
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,8 +127,10 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
for child in xml_object:
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child)).location.url())
|
||||
except:
|
||||
except Exception, e:
|
||||
log.exception("Unable to load child when parsing Sequence. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {'children': children}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
from lxml import etree
|
||||
from mako.template import Template
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
import logging
|
||||
|
||||
class CustomTagModule(XModule):
|
||||
"""
|
||||
@@ -61,7 +61,7 @@ class CustomTagDescriptor(RawDescriptor):
|
||||
# cdodge: look up the template as a module
|
||||
template_loc = self.location._replace(category='custom_tag_template', name=template_name)
|
||||
|
||||
template_module = modulestore().get_item(template_loc)
|
||||
template_module = modulestore().get_instance(system.course_id, template_loc)
|
||||
template_module_data = template_module.definition['data']
|
||||
template = Template(template_module_data)
|
||||
return template.render(**params)
|
||||
|
||||
@@ -6,6 +6,9 @@ from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -93,6 +96,13 @@ class VideoModule(XModule):
|
||||
return self.youtube
|
||||
|
||||
def get_html(self):
|
||||
if isinstance(modulestore(), MongoModuleStore) :
|
||||
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
|
||||
else:
|
||||
# VS[compat]
|
||||
# cdodge: filesystem static content support.
|
||||
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'streams': self.video_list(),
|
||||
'id': self.location.html_id(),
|
||||
@@ -102,6 +112,7 @@ class VideoModule(XModule):
|
||||
'display_name': self.display_name,
|
||||
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
|
||||
'data_dir': self.metadata['data_dir'],
|
||||
'caption_asset_path': caption_asset_path,
|
||||
'show_captions': self.show_captions
|
||||
})
|
||||
|
||||
|
||||
@@ -108,7 +108,20 @@ class HTMLSnippet(object):
|
||||
|
||||
All of these will be loaded onto the page in the CMS
|
||||
"""
|
||||
return cls.js
|
||||
# cdodge: We've moved the xmodule.coffee script from an outside directory into the xmodule area of common
|
||||
# this means we need to make sure that all xmodules include this dependency which had been previously implicitly
|
||||
# fulfilled in a different area of code
|
||||
js = cls.js
|
||||
|
||||
if js is None:
|
||||
js = {}
|
||||
|
||||
if 'coffee' not in js:
|
||||
js['coffee'] = []
|
||||
|
||||
js['coffee'].append(resource_string(__name__, 'js/src/xmodule.coffee'))
|
||||
|
||||
return js
|
||||
|
||||
@classmethod
|
||||
def get_css(cls):
|
||||
|
||||
Reference in New Issue
Block a user