resolving merge conflict with master
This commit is contained in:
@@ -27,10 +27,12 @@ from xmodule.contentstore.django import contentstore
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.templates import update_templates
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
@@ -212,12 +214,21 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
|
||||
self.assertTrue(fs.exists('grading_policy.json'))
|
||||
|
||||
course = ms.get_item(location)
|
||||
# compare what's on disk compared to what we have in our course
|
||||
with fs.open('grading_policy.json','r') as grading_policy:
|
||||
on_disk = loads(grading_policy.read())
|
||||
course = ms.get_item(location)
|
||||
on_disk = loads(grading_policy.read())
|
||||
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
|
||||
|
||||
#check for policy.json
|
||||
self.assertTrue(fs.exists('policy.json'))
|
||||
|
||||
# compare what's on disk to what we have in the course module
|
||||
with fs.open('policy.json','r') as course_policy:
|
||||
on_disk = loads(course_policy.read())
|
||||
self.assertIn('course/6.002_Spring_2012', on_disk)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
|
||||
|
||||
# remove old course
|
||||
delete_course(ms, cs, location)
|
||||
|
||||
@@ -409,3 +420,32 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
self.assertIn('markdown', context, "markdown is missing from context")
|
||||
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
|
||||
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
|
||||
|
||||
|
||||
class TemplateTestCase(ModuleStoreTestCase):
|
||||
|
||||
def test_template_cleanup(self):
|
||||
ms = modulestore('direct')
|
||||
|
||||
# insert a bogus template in the store
|
||||
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Empty')
|
||||
|
||||
ms.clone_item(source_template_location, bogus_template_location)
|
||||
|
||||
verify_create = ms.get_item(bogus_template_location)
|
||||
self.assertIsNotNone(verify_create)
|
||||
|
||||
# now run cleanup
|
||||
update_templates()
|
||||
|
||||
# now try to find dangling template, it should not be in DB any longer
|
||||
asserted = False
|
||||
try:
|
||||
verify_create = ms.get_item(bogus_template_location)
|
||||
except ItemNotFoundError:
|
||||
asserted = True
|
||||
|
||||
self.assertTrue(asserted)
|
||||
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class CourseGradingTest(CourseTestCase):
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
||||
|
||||
test_grader.grace_period = {'hours' : '4'}
|
||||
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
|
||||
class AuthTestCase(ContentStoreTestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
|
||||
@@ -155,7 +155,8 @@ class CourseGradingModel(object):
|
||||
if 'grace_period' in graceperiodjson:
|
||||
graceperiodjson = graceperiodjson['grace_period']
|
||||
|
||||
grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()])
|
||||
# lms requires these to be in a fixed order
|
||||
grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.metadata['graceperiod'] = grace_rep
|
||||
@@ -234,10 +235,10 @@ class CourseGradingModel(object):
|
||||
|
||||
@staticmethod
|
||||
def convert_set_grace_period(descriptor):
|
||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
|
||||
rawgrace = descriptor.metadata.get('graceperiod', None)
|
||||
if rawgrace:
|
||||
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
|
||||
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
|
||||
return parsedgrace
|
||||
else: return None
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
}
|
||||
var newVal = new Date(date.getTime() + time * 1000);
|
||||
if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
|
||||
cacheModel.save(fieldName, newVal);
|
||||
cacheModel.save(fieldName, newVal, { error: CMS.ServerError});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -632,8 +632,14 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
|
||||
self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml]
|
||||
cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id'))
|
||||
|
||||
# contextualize correct attribute and then select ones for which
|
||||
# correct = "true"
|
||||
self.correct_choices = [
|
||||
contextualize_text(choice.get('name'), self.context)
|
||||
for choice in cxml
|
||||
if contextualize_text(choice.get('correct'), self.context) == "true"]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
},
|
||||
smartIndent: false
|
||||
});
|
||||
$("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))});
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<section id="designprotein2dinput_${id}" class="designprotein2dinput">
|
||||
<div class="script_placeholder" data-src="/static/js/capa/protex/protex.nocache.js"/>
|
||||
<div class="script_placeholder" data-src="/static/js/capa/protex/protex.nocache.js?raw"/>
|
||||
<div class="script_placeholder" data-src="${applet_loader}"/>
|
||||
|
||||
% if status == 'unsubmitted':
|
||||
|
||||
@@ -37,6 +37,7 @@ setup(
|
||||
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
|
||||
"vertical = xmodule.vertical_module:VerticalDescriptor",
|
||||
"video = xmodule.video_module:VideoDescriptor",
|
||||
"videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor",
|
||||
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"videosequence = xmodule.seq_module:SequenceDescriptor",
|
||||
"discussion = xmodule.discussion_module:DiscussionDescriptor",
|
||||
@@ -44,7 +45,8 @@ setup(
|
||||
"static_tab = xmodule.html_module:StaticTabDescriptor",
|
||||
"custom_tag_template = xmodule.raw_module:RawDescriptor",
|
||||
"about = xmodule.html_module:AboutDescriptor",
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor"
|
||||
]
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
559
common/lib/xmodule/xmodule/css/videoalpha/display.scss
Normal file
559
common/lib/xmodule/xmodule/css/videoalpha/display.scss
Normal file
@@ -0,0 +1,559 @@
|
||||
& {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
div.video {
|
||||
@include clearfix();
|
||||
background: #f3f3f3;
|
||||
display: block;
|
||||
margin: 0 -12px;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
|
||||
article.video-wrapper {
|
||||
float: left;
|
||||
margin-right: flex-gutter(9);
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
section.video-player {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
position: relative;
|
||||
|
||||
object, iframe {
|
||||
border: none;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
@include clearfix();
|
||||
background: #333;
|
||||
border: 1px solid #000;
|
||||
border-top: 0;
|
||||
color: #ccc;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
div.slider {
|
||||
@include clearfix();
|
||||
background: #c2c2c2;
|
||||
border: 1px solid #000;
|
||||
@include border-radius(0);
|
||||
border-top: 1px solid #000;
|
||||
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
|
||||
height: 7px;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
@include transition(height 2.0s ease-in-out);
|
||||
|
||||
div.ui-widget-header {
|
||||
background: #777;
|
||||
@include box-shadow(inset 0 1px 0 #999);
|
||||
}
|
||||
|
||||
a.ui-slider-handle {
|
||||
background: $pink url(../images/slider-handle.png) center center no-repeat;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($pink, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
margin-left: -7px;
|
||||
top: -4px;
|
||||
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
|
||||
width: 15px;
|
||||
|
||||
&:focus, &:hover {
|
||||
background-color: lighten($pink, 10%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.vcr {
|
||||
@extend .dullify;
|
||||
float: left;
|
||||
list-style: none;
|
||||
margin: 0 lh() 0 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
|
||||
a {
|
||||
border-bottom: none;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 46px;
|
||||
padding: 0 lh(.75);
|
||||
text-indent: -9999px;
|
||||
@include transition(background-color, opacity);
|
||||
width: 14px;
|
||||
background: url('../images/vcr.png') 15px 15px no-repeat;
|
||||
outline: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
height: 46px;
|
||||
background: url('../images/vcr.png') 15px 15px no-repeat;
|
||||
}
|
||||
|
||||
&.play {
|
||||
background-position: 17px -114px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
&.pause {
|
||||
background-position: 16px -50px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.vidtime {
|
||||
padding-left: lh(.75);
|
||||
font-weight: bold;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
padding-left: lh(.75);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.secondary-controls {
|
||||
@extend .dullify;
|
||||
float: right;
|
||||
|
||||
div.speeds {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
&>a {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
}
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
&>a {
|
||||
background: url('../images/closed-arrow.png') 10px center no-repeat;
|
||||
border-left: 1px solid #000;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
@include clearfix();
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-right: 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 116px;
|
||||
outline: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #999;
|
||||
float: left;
|
||||
font-size: em(14);
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
padding: 0 lh(.25) 0 lh(.5);
|
||||
line-height: 46px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p.active {
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
padding: 0 lh(.5) 0 0;
|
||||
line-height: 46px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
// fix for now
|
||||
ol.video_speeds {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
|
||||
li {
|
||||
@include box-shadow( 0 1px 0 #555);
|
||||
border-bottom: 1px solid #000;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
display: block;
|
||||
padding: lh(.5);
|
||||
|
||||
&:hover {
|
||||
background-color: #666;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include box-shadow(none);
|
||||
border-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.volume {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.muted {
|
||||
&>a {
|
||||
background: url('../images/mute.png') 10px center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
background: url('../images/volume.png') 10px center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
@include clearfix();
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 46px;
|
||||
margin-right: 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 30px;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
margin-left: -1px;
|
||||
z-index: 10;
|
||||
|
||||
.volume-slider {
|
||||
height: 100px;
|
||||
border: 0;
|
||||
width: 5px;
|
||||
margin: 14px auto;
|
||||
background: #666;
|
||||
border: 1px solid #000;
|
||||
@include box-shadow(0 1px 0 #333);
|
||||
|
||||
a.ui-slider-handle {
|
||||
background: $pink url(../images/slider-handle.png) center center no-repeat;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($pink, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
left: -6px;
|
||||
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.ui-slider-range {
|
||||
background: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.add-fullscreen {
|
||||
background: url(../images/fullscreen.png) center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
padding: 0 lh(.5);
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.quality_control {
|
||||
background: url(../images/hd.png) center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
padding: 0 lh(.5);
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #F44;
|
||||
color: #0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
a.hide-subtitles {
|
||||
background: url('../images/cc.png') center no-repeat;
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
height: 14px;
|
||||
margin-top: -7px;
|
||||
|
||||
a.ui-slider-handle {
|
||||
@include border-radius(20px);
|
||||
height: 20px;
|
||||
margin-left: -10px;
|
||||
top: -4px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
padding-left: 0;
|
||||
float: left;
|
||||
max-height: 460px;
|
||||
overflow: auto;
|
||||
width: flex-grid(3, 9);
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
border: 0;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
line-height: lh();
|
||||
|
||||
&.current {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.closed {
|
||||
@extend .trans;
|
||||
|
||||
article.video-wrapper {
|
||||
width: flex-grid(9,9);
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
background: rgba(#000, .95);
|
||||
border: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
vertical-align: middle;
|
||||
|
||||
&.closed {
|
||||
ol.subtitles {
|
||||
right: -(flex-grid(4));
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
@include clearfix;
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
article.video-wrapper {
|
||||
width: 100%;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
float: none;
|
||||
}
|
||||
|
||||
object, iframe {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
background: rgba(#000, .8);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
max-width: flex-grid(3);
|
||||
padding: lh();
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
@include transition();
|
||||
|
||||
li {
|
||||
color: #aaa;
|
||||
|
||||
&.current {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
common/lib/xmodule/xmodule/foldit_module.py
Normal file
124
common/lib/xmodule/xmodule/foldit_module.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
from dateutil import parser
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class FolditModule(XModule):
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
# ooh look--I'm lazy, so hardcoding the 7.00x required level.
|
||||
# If we need it generalized, can pull from the xml later
|
||||
self.required_level = 4
|
||||
self.required_sublevel = 5
|
||||
|
||||
def parse_due_date():
|
||||
"""
|
||||
Pull out the date, or None
|
||||
"""
|
||||
s = self.metadata.get("due")
|
||||
if s:
|
||||
return parser.parse(s)
|
||||
else:
|
||||
return None
|
||||
|
||||
self.due_str = self.metadata.get("due", "None")
|
||||
self.due = parse_due_date()
|
||||
|
||||
def is_complete(self):
|
||||
"""
|
||||
Did the user get to the required level before the due date?
|
||||
"""
|
||||
# We normally don't want django dependencies in xmodule. foldit is
|
||||
# special. Import this late to avoid errors with things not yet being
|
||||
# initialized.
|
||||
from foldit.models import PuzzleComplete
|
||||
|
||||
complete = PuzzleComplete.is_level_complete(
|
||||
self.system.anonymous_student_id,
|
||||
self.required_level,
|
||||
self.required_sublevel,
|
||||
self.due)
|
||||
return complete
|
||||
|
||||
def completed_puzzles(self):
|
||||
"""
|
||||
Return a list of puzzles that this user has completed, as an array of
|
||||
dicts:
|
||||
|
||||
[ {'set': int,
|
||||
'subset': int,
|
||||
'created': datetime} ]
|
||||
|
||||
The list is sorted by set, then subset
|
||||
"""
|
||||
from foldit.models import PuzzleComplete
|
||||
|
||||
return sorted(
|
||||
PuzzleComplete.completed_puzzles(self.system.anonymous_student_id),
|
||||
key=lambda d: (d['set'], d['subset']))
|
||||
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Render the html for the module.
|
||||
"""
|
||||
goal_level = '{0}-{1}'.format(
|
||||
self.required_level,
|
||||
self.required_sublevel)
|
||||
|
||||
context = {
|
||||
'due': self.due_str,
|
||||
'success': self.is_complete(),
|
||||
'goal_level': goal_level,
|
||||
'completed': self.completed_puzzles(),
|
||||
}
|
||||
|
||||
return self.system.render_template('foldit.html', context)
|
||||
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
0 / 1 based on whether student has gotten far enough.
|
||||
"""
|
||||
score = 1 if self.is_complete() else 0
|
||||
return {'score': score,
|
||||
'total': self.max_score()}
|
||||
|
||||
def max_score(self):
|
||||
return 1
|
||||
|
||||
|
||||
class FolditDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding open ended response questions to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = FolditModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "foldit"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
# The grade changes without any student interaction with the edx website,
|
||||
# so always need to actually check.
|
||||
always_recalculate_grades = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
For now, don't need anything from the xml
|
||||
"""
|
||||
return {}
|
||||
@@ -3,7 +3,11 @@ from xmodule.raw_module import RawDescriptor
|
||||
|
||||
|
||||
class HiddenModule(XModule):
|
||||
pass
|
||||
def get_html(self):
|
||||
if self.system.user_is_staff:
|
||||
return "ERROR: This module is unknown--students will not see it at all"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class HiddenDescriptor(RawDescriptor):
|
||||
|
||||
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
*.js
|
||||
|
||||
# Please do not ignore *.js files. Some xmodules are written in JS.
|
||||
|
||||
103
common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee
Normal file
103
common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee
Normal file
@@ -0,0 +1,103 @@
|
||||
class @VideoAlpha
|
||||
constructor: (element) ->
|
||||
@el = $(element).find('.video')
|
||||
@id = @el.attr('id').replace(/video_/, '')
|
||||
@start = @el.data('start')
|
||||
@end = @el.data('end')
|
||||
@caption_data_dir = @el.data('caption-data-dir')
|
||||
@caption_asset_path = @el.data('caption-asset-path')
|
||||
@show_captions = @el.data('show-captions').toString() == "true"
|
||||
@el = $("#video_#{@id}")
|
||||
if @parseYoutubeId(@el.data("streams")) is true
|
||||
@videoType = "youtube"
|
||||
@fetchMetadata()
|
||||
@parseSpeed()
|
||||
else
|
||||
@videoType = "html5"
|
||||
@parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source')
|
||||
@speeds = ['0.75', '1.0', '1.25', '1.50']
|
||||
sub = @el.data('sub')
|
||||
if (typeof sub isnt "string") or (sub.length is 0)
|
||||
sub = ""
|
||||
@show_captions = false
|
||||
@videos =
|
||||
"0.75": sub
|
||||
"1.0": sub
|
||||
"1.25": sub
|
||||
"1.5": sub
|
||||
@setSpeed $.cookie('video_speed')
|
||||
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
|
||||
if @show_captions is true
|
||||
@hide_captions = $.cookie('hide_captions') == 'true'
|
||||
else
|
||||
@hide_captions = true
|
||||
$.cookie('hide_captions', @hide_captions, expires: 3650, path: '/')
|
||||
@el.addClass 'closed'
|
||||
if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player))
|
||||
@embed()
|
||||
else
|
||||
if @videoType is "youtube"
|
||||
window.onYouTubePlayerAPIReady = =>
|
||||
@embed()
|
||||
else if @videoType is "html5"
|
||||
window.onHTML5PlayerAPIReady = =>
|
||||
@embed()
|
||||
|
||||
youtubeId: (speed)->
|
||||
@videos[speed || @speed]
|
||||
|
||||
parseYoutubeId: (videos)->
|
||||
return false if (typeof videos isnt "string") or (videos.length is 0)
|
||||
@videos = {}
|
||||
$.each videos.split(/,/), (index, video) =>
|
||||
speed = undefined
|
||||
video = video.split(/:/)
|
||||
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0")
|
||||
@videos[speed] = video[1]
|
||||
true
|
||||
|
||||
parseHtml5Sources: (mp4Source, webmSource, oggSource)->
|
||||
@html5Sources =
|
||||
mp4: null
|
||||
webm: null
|
||||
ogg: null
|
||||
@html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0)
|
||||
@html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0)
|
||||
@html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0)
|
||||
|
||||
parseSpeed: ->
|
||||
@speeds = ($.map @videos, (url, speed) -> speed).sort()
|
||||
@setSpeed $.cookie('video_speed')
|
||||
|
||||
setSpeed: (newSpeed, updateCookie)->
|
||||
if @speeds.indexOf(newSpeed) isnt -1
|
||||
@speed = newSpeed
|
||||
|
||||
if updateCookie isnt false
|
||||
$.cookie "video_speed", "" + newSpeed,
|
||||
expires: 3650
|
||||
path: "/"
|
||||
else
|
||||
@speed = "1.0"
|
||||
|
||||
embed: ->
|
||||
@player = new VideoPlayerAlpha video: this
|
||||
|
||||
fetchMetadata: (url) ->
|
||||
@metadata = {}
|
||||
$.each @videos, (speed, url) =>
|
||||
$.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
|
||||
|
||||
getDuration: ->
|
||||
@metadata[@youtubeId()].duration
|
||||
|
||||
log: (eventName)->
|
||||
logInfo =
|
||||
id: @id
|
||||
code: @youtubeId()
|
||||
currentTime: @player.currentTime
|
||||
speed: @speed
|
||||
if @videoType is "youtube"
|
||||
logInfo.code = @youtubeId()
|
||||
else logInfo.code = "html5" if @videoType is "html5"
|
||||
Logger.log eventName, logInfo
|
||||
@@ -0,0 +1,14 @@
|
||||
class @SubviewAlpha
|
||||
constructor: (options) ->
|
||||
$.each options, (key, value) =>
|
||||
@[key] = value
|
||||
@initialize()
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
initialize: ->
|
||||
render: ->
|
||||
bind: ->
|
||||
@@ -0,0 +1,285 @@
|
||||
this.HTML5Video = (function () {
|
||||
var HTML5Video;
|
||||
|
||||
HTML5Video = {};
|
||||
|
||||
HTML5Video.Player = (function () {
|
||||
Player.prototype.callStateChangeCallback = function () {
|
||||
if ($.isFunction(this.config.events.onStateChange) === true) {
|
||||
this.config.events.onStateChange({
|
||||
'data': this.playerState
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.pauseVideo = function () {
|
||||
this.video.pause();
|
||||
};
|
||||
|
||||
Player.prototype.seekTo = function (value) {
|
||||
if ((typeof value === 'number') && (value <= this.video.duration) && (value >= 0)) {
|
||||
this.start = 0;
|
||||
this.end = this.video.duration;
|
||||
|
||||
this.video.currentTime = value;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.setVolume = function (value) {
|
||||
if ((typeof value === 'number') && (value <= 100) && (value >= 0)) {
|
||||
this.video.volume = value * 0.01;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.getCurrentTime = function () {
|
||||
return this.video.currentTime;
|
||||
};
|
||||
|
||||
Player.prototype.playVideo = function () {
|
||||
this.video.play();
|
||||
};
|
||||
|
||||
Player.prototype.getPlayerState = function () {
|
||||
return this.playerState;
|
||||
};
|
||||
|
||||
Player.prototype.getVolume = function () {
|
||||
return this.video.volume;
|
||||
};
|
||||
|
||||
Player.prototype.getDuration = function () {
|
||||
if (isFinite(this.video.duration) === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.video.duration;
|
||||
};
|
||||
|
||||
Player.prototype.setPlaybackRate = function (value) {
|
||||
var newSpeed;
|
||||
|
||||
newSpeed = parseFloat(value);
|
||||
|
||||
if (isFinite(newSpeed) === true) {
|
||||
this.video.playbackRate = value;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.getAvailablePlaybackRates = function () {
|
||||
return [0.75, 1.0, 1.25, 1.5];
|
||||
};
|
||||
|
||||
return Player;
|
||||
|
||||
/*
|
||||
* Constructor function for HTML5 Video player.
|
||||
*
|
||||
* @el - A DOM element where the HTML5 player will be inserted (as returned by jQuery(selector) function),
|
||||
* or a selector string which will be used to select an element. This is a required parameter.
|
||||
*
|
||||
* @config - An object whose properties will be used as configuration options for the HTML5 video
|
||||
* player. This is an optional parameter. In the case if this parameter is missing, or some of the config
|
||||
* object's properties are missing, defaults will be used. The available options (and their defaults) are as
|
||||
* follows:
|
||||
*
|
||||
* config = {
|
||||
*
|
||||
* 'videoSources': {}, // An object with properties being video sources. The property name is the
|
||||
* // video format of the source. Supported video formats are: 'mp4', 'webm', and
|
||||
* // 'ogg'.
|
||||
*
|
||||
* 'playerVars': { // Object's properties identify player parameters.
|
||||
* 'start': 0, // Possible values: positive integer. Position from which to start playing the
|
||||
* // video. Measured in seconds. If value is non-numeric, or 'start' property is
|
||||
* // not specified, the video will start playing from the beginning.
|
||||
*
|
||||
* 'end': null // Possible values: positive integer. Position when to stop playing the
|
||||
* // video. Measured in seconds. If value is null, or 'end' property is not
|
||||
* // specified, the video will end playing at the end.
|
||||
*
|
||||
* },
|
||||
*
|
||||
* 'events': { // Object's properties identify the events that the API fires, and the
|
||||
* // functions (event listeners) that the API will call when those events occur.
|
||||
* // If value is null, or property is not specified, then no callback will be
|
||||
* // called for that event.
|
||||
*
|
||||
* 'onReady': null,
|
||||
* 'onStateChange': null
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function Player(el, config) {
|
||||
var sourceStr, _this;
|
||||
|
||||
// If el is string, we assume it is an ID of a DOM element. Get the element, and check that the ID
|
||||
// really belongs to an element. If we didn't get a DOM element, return. At this stage, nothing will
|
||||
// break because other parts of the video player are waiting for 'onReady' callback to be called.
|
||||
if (typeof el === 'string') {
|
||||
this.el = $(el);
|
||||
|
||||
if (this.el.length === 0) {
|
||||
return;
|
||||
}
|
||||
} else if (el instanceof jQuery) {
|
||||
this.el = el;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// A simple test to see that the 'config' is a normal object.
|
||||
if ($.isPlainObject(config) === true) {
|
||||
this.config = config;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// We should have at least one video source. Otherwise there is no point to continue.
|
||||
if (config.hasOwnProperty('videoSources') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// From the start, all sources are empty. We will populate this object below.
|
||||
sourceStr = {
|
||||
'mp4': ' ',
|
||||
'webm': ' ',
|
||||
'ogg': ' '
|
||||
};
|
||||
|
||||
// Will be used in inner functions to point to the current object.
|
||||
_this = this;
|
||||
|
||||
// Create HTML markup for individual sources of the HTML5 <video> element.
|
||||
$.each(sourceStr, function (videoType, videoSource) {
|
||||
if (
|
||||
(_this.config.videoSources.hasOwnProperty(videoType) === true) &&
|
||||
(typeof _this.config.videoSources[videoType] === 'string') &&
|
||||
(_this.config.videoSources[videoType].length > 0)
|
||||
) {
|
||||
sourceStr[videoType] =
|
||||
'<source ' +
|
||||
'src="' + _this.config.videoSources[videoType] + '" ' +
|
||||
'type="video/' + videoType + '" ' +
|
||||
'/> ';
|
||||
}
|
||||
});
|
||||
|
||||
// We should have at least one video source. Otherwise there is no point to continue.
|
||||
if ((sourceStr.mp4 === ' ') && (sourceStr.webm === ' ') && (sourceStr.ogg === ' ')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the starting and ending time for the video.
|
||||
this.start = 0;
|
||||
this.end = null;
|
||||
if (config.hasOwnProperty('playerVars') === true) {
|
||||
this.start = parseFloat(config.playerVars.start);
|
||||
if ((isFinite(this.start) !== true) || (this.start < 0)) {
|
||||
this.start = 0;
|
||||
}
|
||||
|
||||
this.end = parseFloat(config.playerVars.end);
|
||||
if ((isFinite(this.end) !== true) || (this.end < this.start)) {
|
||||
this.end = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create HTML markup for the <video> element, populating it with sources from previous step.
|
||||
this.videoEl = $(
|
||||
'<video style="width: 100%;">' +
|
||||
sourceStr.mp4 +
|
||||
sourceStr.webm +
|
||||
sourceStr.ogg +
|
||||
'</video>'
|
||||
);
|
||||
|
||||
// Get the DOM element (to access the HTML5 video API), and set the player state to UNSTARTED.
|
||||
// The player state is used by other parts of the VideoPlayer to detrermine what the video is
|
||||
// currently doing.
|
||||
this.video = this.videoEl[0];
|
||||
this.playerState = HTML5Video.PlayerState.UNSTARTED;
|
||||
// this.callStateChangeCallback();
|
||||
|
||||
// Attach a 'click' event on the <video> element. It will cause the video to pause/play.
|
||||
this.videoEl.on('click', function (event) {
|
||||
if (_this.playerState === HTML5Video.PlayerState.PAUSED) {
|
||||
_this.video.play();
|
||||
_this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
_this.callStateChangeCallback();
|
||||
} else if (_this.playerState === HTML5Video.PlayerState.PLAYING) {
|
||||
_this.video.pause();
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
_this.callStateChangeCallback();
|
||||
}
|
||||
});
|
||||
|
||||
// When the <video> tag has been processed by the browser, and it is ready for playback,
|
||||
// notify other parts of the VideoPlayer, and initially pause the video.
|
||||
//
|
||||
// Also, at this time we can get the real duration of the video. Update the starting end ending
|
||||
// points of the video. Note that first time, the video will start playing at the specified start time,
|
||||
// and end playing at the specified end time. After it was paused, or when a seek operation happeded,
|
||||
// the starting time and ending time will reset to the beginning and the end of the video respectively.
|
||||
this.video.addEventListener('canplay', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
|
||||
if (_this.start > _this.video.duration) {
|
||||
_this.start = 0;
|
||||
}
|
||||
if ((_this.end === null) || (_this.end > _this.video.duration)) {
|
||||
_this.end = _this.video.duration;
|
||||
}
|
||||
_this.video.currentTime = _this.start;
|
||||
|
||||
if ($.isFunction(_this.config.events.onReady) === true) {
|
||||
_this.config.events.onReady(null);
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Register the 'play' event.
|
||||
this.video.addEventListener('play', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'pause' event.
|
||||
this.video.addEventListener('pause', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'ended' event.
|
||||
this.video.addEventListener('ended', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.ENDED;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'timeupdate' event. This is the place where we control when the video ends.
|
||||
// If an ending time was specified, then after the video plays through to this spot, pauses, we
|
||||
// must make sure to update the ending time to the end of the video. This way, the user can watch
|
||||
// any parts of it afterwards.
|
||||
this.video.addEventListener('timeupdate', function (data) {
|
||||
if (_this.end < _this.video.currentTime) {
|
||||
// When we call video.pause(), a 'pause' event will be formed, and we will catch it
|
||||
// in another handler (see above).
|
||||
_this.video.pause();
|
||||
_this.end = _this.video.duration;
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Place the <video> element on the page.
|
||||
this.videoEl.appendTo(this.el.find('.video-player div'));
|
||||
}
|
||||
}());
|
||||
|
||||
HTML5Video.PlayerState = {
|
||||
'UNSTARTED': -1,
|
||||
'ENDED': 0,
|
||||
'PLAYING': 1,
|
||||
'PAUSED': 2,
|
||||
'BUFFERING': 3,
|
||||
'CUED': 5
|
||||
};
|
||||
|
||||
return HTML5Video;
|
||||
}());
|
||||
@@ -0,0 +1,152 @@
|
||||
class @VideoCaptionAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@loaded = false
|
||||
|
||||
bind: ->
|
||||
$(window).bind('resize', @resize)
|
||||
@$('.hide-subtitles').click @toggle
|
||||
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
|
||||
.mousemove(@onMovement).bind('mousewheel', @onMovement)
|
||||
.bind('DOMMouseScroll', @onMovement)
|
||||
|
||||
captionURL: ->
|
||||
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
|
||||
|
||||
render: ->
|
||||
# TODO: make it so you can have a video with no captions.
|
||||
#@$('.video-wrapper').after """
|
||||
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
|
||||
# """
|
||||
@$('.video-wrapper').after """
|
||||
<ol class="subtitles"></ol>
|
||||
"""
|
||||
@$('.video-controls .secondary-controls').append """
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
|
||||
"""#"
|
||||
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
|
||||
@fetchCaption()
|
||||
|
||||
fetchCaption: ->
|
||||
$.getWithPrefix @captionURL(), (captions) =>
|
||||
@captions = captions.text
|
||||
@start = captions.start
|
||||
|
||||
@loaded = true
|
||||
|
||||
if onTouchBasedDevice()
|
||||
$('.subtitles li').html "Caption will be displayed when you start playing the video."
|
||||
else
|
||||
@renderCaption()
|
||||
|
||||
renderCaption: ->
|
||||
container = $('<ol>')
|
||||
|
||||
$.each @captions, (index, text) =>
|
||||
container.append $('<li>').html(text).attr
|
||||
'data-index': index
|
||||
'data-start': @start[index]
|
||||
|
||||
@$('.subtitles').html(container.html())
|
||||
@$('.subtitles li[data-index]').click @seekPlayer
|
||||
|
||||
# prepend and append an empty <li> for cosmetic reason
|
||||
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
|
||||
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
|
||||
|
||||
@rendered = true
|
||||
|
||||
search: (time) ->
|
||||
if @loaded
|
||||
min = 0
|
||||
max = @start.length - 1
|
||||
|
||||
while min < max
|
||||
index = Math.ceil((max + min) / 2)
|
||||
if time < @start[index]
|
||||
max = index - 1
|
||||
if time >= @start[index]
|
||||
min = index
|
||||
return min
|
||||
|
||||
play: ->
|
||||
if @loaded
|
||||
@renderCaption() unless @rendered
|
||||
@playing = true
|
||||
|
||||
pause: ->
|
||||
if @loaded
|
||||
@playing = false
|
||||
|
||||
updatePlayTime: (time) ->
|
||||
if @loaded
|
||||
# This 250ms offset is required to match the video speed
|
||||
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
|
||||
newIndex = @search time
|
||||
|
||||
if newIndex != undefined && @currentIndex != newIndex
|
||||
if @currentIndex
|
||||
@$(".subtitles li.current").removeClass('current')
|
||||
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
|
||||
|
||||
@currentIndex = newIndex
|
||||
@scrollCaption()
|
||||
|
||||
resize: =>
|
||||
@$('.subtitles').css maxHeight: @captionHeight()
|
||||
@$('.subtitles .spacing:first').height(@topSpacingHeight())
|
||||
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
|
||||
@scrollCaption()
|
||||
|
||||
onMouseEnter: =>
|
||||
clearTimeout @frozen if @frozen
|
||||
@frozen = setTimeout @onMouseLeave, 10000
|
||||
|
||||
onMovement: =>
|
||||
@onMouseEnter()
|
||||
|
||||
onMouseLeave: =>
|
||||
clearTimeout @frozen if @frozen
|
||||
@frozen = null
|
||||
@scrollCaption() if @playing
|
||||
|
||||
scrollCaption: ->
|
||||
if !@frozen && @$('.subtitles .current:first').length
|
||||
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
|
||||
offset: - @calculateOffset(@$('.subtitles .current:first'))
|
||||
|
||||
seekPlayer: (event) =>
|
||||
event.preventDefault()
|
||||
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
|
||||
$(@).trigger('seek', time)
|
||||
|
||||
calculateOffset: (element) ->
|
||||
@captionHeight() / 2 - element.height() / 2
|
||||
|
||||
topSpacingHeight: ->
|
||||
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
|
||||
|
||||
bottomSpacingHeight: ->
|
||||
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
|
||||
|
||||
toggle: (event) =>
|
||||
event.preventDefault()
|
||||
if @el.hasClass('closed') # Captions are "closed" e.g. turned off
|
||||
@hideCaptions(false)
|
||||
else # Captions are on
|
||||
@hideCaptions(true)
|
||||
|
||||
hideCaptions: (hide_captions) =>
|
||||
if hide_captions
|
||||
@$('.hide-subtitles').attr('title', 'Turn on captions')
|
||||
@el.addClass('closed')
|
||||
else
|
||||
@$('.hide-subtitles').attr('title', 'Turn off captions')
|
||||
@el.removeClass('closed')
|
||||
@scrollCaption()
|
||||
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
|
||||
|
||||
captionHeight: ->
|
||||
if @el.hasClass('fullscreen')
|
||||
$(window).height() - @$('.video-controls').height()
|
||||
else
|
||||
@$('.video-wrapper').height()
|
||||
@@ -0,0 +1,35 @@
|
||||
class @VideoControlAlpha extends SubviewAlpha
|
||||
bind: ->
|
||||
@$('.video_control').click @togglePlayback
|
||||
|
||||
render: ->
|
||||
@el.append """
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#"></a></li>
|
||||
<li>
|
||||
<div class="vidtime">0:00 / 0:00</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
</div>
|
||||
</div>
|
||||
"""#"
|
||||
|
||||
unless onTouchBasedDevice()
|
||||
@$('.video_control').addClass('play').html('Play')
|
||||
|
||||
play: ->
|
||||
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
|
||||
|
||||
pause: ->
|
||||
@$('.video_control').removeClass('pause').addClass('play').html('Play')
|
||||
|
||||
togglePlayback: (event) =>
|
||||
event.preventDefault()
|
||||
if @$('.video_control').hasClass('play')
|
||||
$(@).trigger('play')
|
||||
else if @$('.video_control').hasClass('pause')
|
||||
$(@).trigger('pause')
|
||||
@@ -0,0 +1,283 @@
|
||||
class @VideoPlayerAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
# If we switch verticals while the video is playing, then HTML content is
|
||||
# removed, but JS code is still executing (setInterval() method), and there will
|
||||
# arise conflicts (no HTML content, but code tries to access it). Therefore
|
||||
# we must pause the player (stop setInterval() method).
|
||||
if (window.OldVideoPlayerAlpha) and (window.OldVideoPlayerAlpha.onPause)
|
||||
window.OldVideoPlayerAlpha.onPause()
|
||||
window.OldVideoPlayerAlpha = this
|
||||
|
||||
if @video.videoType is 'youtube'
|
||||
@PlayerState = YT.PlayerState
|
||||
# Define a missing constant of Youtube API
|
||||
@PlayerState.UNSTARTED = -1
|
||||
else if @video.videoType is 'html5'
|
||||
@PlayerState = HTML5Video.PlayerState
|
||||
|
||||
@currentTime = 0
|
||||
@el = $("#video_#{@video.id}")
|
||||
|
||||
bind: ->
|
||||
$(@control).bind('play', @play)
|
||||
.bind('pause', @pause)
|
||||
if @video.videoType is 'youtube'
|
||||
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
|
||||
if @video.show_captions is true
|
||||
$(@caption).bind('seek', @onSeek)
|
||||
$(@speedControl).bind('speedChange', @onSpeedChange)
|
||||
$(@progressSlider).bind('seek', @onSeek)
|
||||
if @volumeControl
|
||||
$(@volumeControl).bind('volumeChange', @onVolumeChange)
|
||||
$(document).keyup @bindExitFullScreen
|
||||
|
||||
@$('.add-fullscreen').click @toggleFullScreen
|
||||
@addToolTip() unless onTouchBasedDevice()
|
||||
|
||||
bindExitFullScreen: (event) =>
|
||||
if @el.hasClass('fullscreen') && event.keyCode == 27
|
||||
@toggleFullScreen(event)
|
||||
|
||||
render: ->
|
||||
@control = new VideoControlAlpha el: @$('.video-controls')
|
||||
if @video.videoType is 'youtube'
|
||||
@qualityControl = new VideoQualityControlAlpha el: @$('.secondary-controls')
|
||||
if @video.show_captions is true
|
||||
@caption = new VideoCaptionAlpha
|
||||
el: @el
|
||||
youtubeId: @video.youtubeId('1.0')
|
||||
currentSpeed: @currentSpeed()
|
||||
captionAssetPath: @video.caption_asset_path
|
||||
unless onTouchBasedDevice()
|
||||
@volumeControl = new VideoVolumeControlAlpha el: @$('.secondary-controls')
|
||||
@speedControl = new VideoSpeedControlAlpha el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
|
||||
@progressSlider = new VideoProgressSliderAlpha el: @$('.slider')
|
||||
@playerVars =
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
modestbranding: 1
|
||||
html5: 1
|
||||
if @video.start
|
||||
@playerVars.start = @video.start
|
||||
@playerVars.wmode = 'window'
|
||||
if @video.end
|
||||
# work in AS3, not HMLT5. but iframe use AS3
|
||||
@playerVars.end = @video.end
|
||||
if @video.videoType is 'html5'
|
||||
@player = new HTML5Video.Player @video.el,
|
||||
playerVars: @playerVars,
|
||||
videoSources: @video.html5Sources,
|
||||
events:
|
||||
onReady: @onReady
|
||||
onStateChange: @onStateChange
|
||||
else if @video.videoType is 'youtube'
|
||||
prev_player_type = $.cookie('prev_player_type')
|
||||
if prev_player_type == 'html5'
|
||||
youTubeId = @video.videos['1.0']
|
||||
else
|
||||
youTubeId = @video.youtubeId()
|
||||
@player = new YT.Player @video.id,
|
||||
playerVars: @playerVars
|
||||
videoId: youTubeId
|
||||
events:
|
||||
onReady: @onReady
|
||||
onStateChange: @onStateChange
|
||||
onPlaybackQualityChange: @onPlaybackQualityChange
|
||||
if @video.show_captions is true
|
||||
@caption.hideCaptions(@['video'].hide_captions)
|
||||
|
||||
addToolTip: ->
|
||||
@$('.add-fullscreen, .hide-subtitles').qtip
|
||||
position:
|
||||
my: 'top right'
|
||||
at: 'top center'
|
||||
|
||||
onReady: (event) =>
|
||||
if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate @video.speed
|
||||
unless onTouchBasedDevice()
|
||||
$('.video-load-complete:first').data('video').player.play()
|
||||
|
||||
onStateChange: (event) =>
|
||||
_this = this
|
||||
switch event.data
|
||||
when @PlayerState.UNSTARTED
|
||||
# Before the video starts playing, let us see if we are in YouTube player,
|
||||
# and if YouTube is in HTML5 mode. If both cases are true, then we can make
|
||||
# it so that speed switching happens natively.
|
||||
|
||||
if @video.videoType is "youtube"
|
||||
# Because YouTube API does not have a direct method to determine the mode we
|
||||
# are in (Flash or HTML5), we rely on an indirect method. Currently, when in
|
||||
# Flash mode, YouTube player reports that there is only one (1.0) speed
|
||||
# available. When in HTML5 mode, it reports multiple speeds available. We
|
||||
# will use this fact.
|
||||
#
|
||||
# NOTE: It is my strong belief that in the future YouTube Flash player will
|
||||
# not get speed changes. This is a dying technology. So we can safely use
|
||||
# this indirect method to determine player mode.
|
||||
availableSpeeds = @player.getAvailablePlaybackRates()
|
||||
prev_player_type = $.cookie('prev_player_type')
|
||||
if availableSpeeds.length > 1
|
||||
# If the user last accessed the page and watched a movie via YouTube
|
||||
# player, and it was using Flash mode, then we must reset the current
|
||||
# YouTube speed to 1.0 (by loading appropriate video that is encoded at
|
||||
# 1.0 speed).
|
||||
if prev_player_type == 'youtube'
|
||||
$.cookie('prev_player_type', 'html5', expires: 3650, path: '/')
|
||||
@onSpeedChange null, '1.0', false
|
||||
else if prev_player_type != 'html5'
|
||||
$.cookie('prev_player_type', 'html5', expires: 3650, path: '/')
|
||||
|
||||
# Now we must update all the speeds to the ones available via the YouTube
|
||||
# HTML5 API. The default speeds are not exactly the same as reported by
|
||||
# YouTube, so we will remove the default speeds, and populate all the
|
||||
# necessary data with correct available speeds.
|
||||
baseSpeedSubs = @video.videos["1.0"]
|
||||
$.each @video.videos, (index, value) ->
|
||||
delete _this.video.videos[index]
|
||||
@video.speeds = []
|
||||
$.each availableSpeeds, (index, value) ->
|
||||
_this.video.videos[value.toFixed(2).replace(/\.00$/, ".0")] = baseSpeedSubs
|
||||
_this.video.speeds.push value.toFixed(2).replace(/\.00$/, ".0")
|
||||
|
||||
# We must update the Speed Control to reflect the new avialble speeds.
|
||||
@speedControl.reRender @video.speeds, @video.speed
|
||||
|
||||
# Now we set the videoType to 'HTML5'. This works because my HTML5Video
|
||||
# class is fully compatible with YouTube HTML5 API.
|
||||
@video.videoType = 'html5'
|
||||
|
||||
@video.setSpeed $.cookie('video_speed')
|
||||
|
||||
# Change the speed to the required one.
|
||||
@player.setPlaybackRate @video.speed
|
||||
else
|
||||
# We are in YouTube player, and in Flash mode. Check previos mode.
|
||||
if prev_player_type != 'youtube'
|
||||
$.cookie('prev_player_type', 'youtube', expires: 3650, path: '/')
|
||||
|
||||
# We need to set the proper speed when previous mode was not 'youtube'.
|
||||
@onSpeedChange null, $.cookie('video_speed')
|
||||
|
||||
@onUnstarted()
|
||||
when @PlayerState.PLAYING
|
||||
@onPlay()
|
||||
when @PlayerState.PAUSED
|
||||
@onPause()
|
||||
when @PlayerState.ENDED
|
||||
@onEnded()
|
||||
|
||||
onPlaybackQualityChange: (event, value) =>
|
||||
quality = @player.getPlaybackQuality()
|
||||
@qualityControl.onQualityChange(quality)
|
||||
|
||||
handlePlaybackQualityChange: (event, value) =>
|
||||
@player.setPlaybackQuality(value)
|
||||
|
||||
onUnstarted: =>
|
||||
@control.pause()
|
||||
if @video.show_captions is true
|
||||
@caption.pause()
|
||||
|
||||
onPlay: =>
|
||||
@video.log 'play_video'
|
||||
unless @player.interval
|
||||
@player.interval = setInterval(@update, 200)
|
||||
if @video.show_captions is true
|
||||
@caption.play()
|
||||
@control.play()
|
||||
@progressSlider.play()
|
||||
|
||||
onPause: =>
|
||||
@video.log 'pause_video'
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = null
|
||||
if @video.show_captions is true
|
||||
@caption.pause()
|
||||
@control.pause()
|
||||
|
||||
onEnded: =>
|
||||
@control.pause()
|
||||
if @video.show_captions is true
|
||||
@caption.pause()
|
||||
|
||||
onSeek: (event, time) =>
|
||||
@player.seekTo(time, true)
|
||||
if @isPlaying()
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = setInterval(@update, 200)
|
||||
else
|
||||
@currentTime = time
|
||||
@updatePlayTime time
|
||||
|
||||
onSpeedChange: (event, newSpeed, updateCookie) =>
|
||||
if @video.videoType is 'youtube'
|
||||
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
|
||||
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
|
||||
@video.setSpeed newSpeed, updateCookie
|
||||
if @video.videoType is 'youtube'
|
||||
if @video.show_captions is true
|
||||
@caption.currentSpeed = newSpeed
|
||||
if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate newSpeed
|
||||
else if @video.videoType is 'youtube'
|
||||
if @isPlaying()
|
||||
@player.loadVideoById(@video.youtubeId(), @currentTime)
|
||||
else
|
||||
@player.cueVideoById(@video.youtubeId(), @currentTime)
|
||||
if @video.videoType is 'youtube'
|
||||
@updatePlayTime @currentTime
|
||||
|
||||
onVolumeChange: (event, volume) =>
|
||||
@player.setVolume volume
|
||||
|
||||
update: =>
|
||||
if @currentTime = @player.getCurrentTime()
|
||||
@updatePlayTime @currentTime
|
||||
|
||||
updatePlayTime: (time) ->
|
||||
progress = Time.format(time) + ' / ' + Time.format(@duration())
|
||||
@$(".vidtime").html(progress)
|
||||
if @video.show_captions is true
|
||||
@caption.updatePlayTime(time)
|
||||
@progressSlider.updatePlayTime(time, @duration())
|
||||
|
||||
toggleFullScreen: (event) =>
|
||||
event.preventDefault()
|
||||
if @el.hasClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Fill browser')
|
||||
@el.removeClass('fullscreen')
|
||||
else
|
||||
@el.addClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Exit fill browser')
|
||||
if @video.show_captions is true
|
||||
@caption.resize()
|
||||
|
||||
# Delegates
|
||||
play: =>
|
||||
@player.playVideo() if @player.playVideo
|
||||
|
||||
isPlaying: ->
|
||||
@player.getPlayerState() == @PlayerState.PLAYING
|
||||
|
||||
pause: =>
|
||||
@player.pauseVideo() if @player.pauseVideo
|
||||
|
||||
duration: ->
|
||||
duration = @player.getDuration()
|
||||
if isFinite(duration) is false
|
||||
duration = @video.getDuration()
|
||||
duration
|
||||
|
||||
currentSpeed: ->
|
||||
@video.speed
|
||||
|
||||
volume: (value) ->
|
||||
if value?
|
||||
@player.setVolume value
|
||||
else
|
||||
@player.getVolume()
|
||||
@@ -0,0 +1,49 @@
|
||||
class @VideoProgressSliderAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@buildSlider() unless onTouchBasedDevice()
|
||||
|
||||
buildSlider: ->
|
||||
@slider = @el.slider
|
||||
range: 'min'
|
||||
change: @onChange
|
||||
slide: @onSlide
|
||||
stop: @onStop
|
||||
@buildHandle()
|
||||
|
||||
buildHandle: ->
|
||||
@handle = @$('.slider .ui-slider-handle')
|
||||
@handle.qtip
|
||||
content: "#{Time.format(@slider.slider('value'))}"
|
||||
position:
|
||||
my: 'bottom center'
|
||||
at: 'top center'
|
||||
container: @handle
|
||||
hide:
|
||||
delay: 700
|
||||
style:
|
||||
classes: 'ui-tooltip-slider'
|
||||
widget: true
|
||||
|
||||
play: =>
|
||||
@buildSlider() unless @slider
|
||||
|
||||
updatePlayTime: (currentTime, duration) ->
|
||||
if @slider && !@frozen
|
||||
@slider.slider('option', 'max', duration)
|
||||
@slider.slider('value', currentTime)
|
||||
|
||||
onSlide: (event, ui) =>
|
||||
@frozen = true
|
||||
@updateTooltip(ui.value)
|
||||
$(@).trigger('seek', ui.value)
|
||||
|
||||
onChange: (event, ui) =>
|
||||
@updateTooltip(ui.value)
|
||||
|
||||
onStop: (event, ui) =>
|
||||
@frozen = true
|
||||
$(@).trigger('seek', ui.value)
|
||||
setTimeout (=> @frozen = false), 200
|
||||
|
||||
updateTooltip: (value)->
|
||||
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
|
||||
@@ -0,0 +1,26 @@
|
||||
class @VideoQualityControlAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@quality = null;
|
||||
|
||||
bind: ->
|
||||
@$('.quality_control').click @toggleQuality
|
||||
|
||||
render: ->
|
||||
@el.append """
|
||||
<a href="#" class="quality_control" title="HD">HD</a>
|
||||
"""#"
|
||||
|
||||
onQualityChange: (value) ->
|
||||
@quality = value
|
||||
if @quality in ['hd720', 'hd1080', 'highres']
|
||||
@el.addClass('active')
|
||||
else
|
||||
@el.removeClass('active')
|
||||
|
||||
toggleQuality: (event) =>
|
||||
event.preventDefault()
|
||||
if @quality in ['hd720', 'hd1080', 'highres']
|
||||
newQuality = 'large'
|
||||
else
|
||||
newQuality = 'hd720'
|
||||
$(@).trigger('changeQuality', newQuality)
|
||||
@@ -0,0 +1,53 @@
|
||||
class @VideoSpeedControlAlpha extends SubviewAlpha
|
||||
bind: ->
|
||||
@$('.video_speeds a').click @changeVideoSpeed
|
||||
if onTouchBasedDevice()
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).toggleClass('open')
|
||||
else
|
||||
@$('.speeds').mouseenter ->
|
||||
$(this).addClass('open')
|
||||
@$('.speeds').mouseleave ->
|
||||
$(this).removeClass('open')
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).removeClass('open')
|
||||
|
||||
render: ->
|
||||
@el.prepend """
|
||||
<div class="speeds">
|
||||
<a href="#">
|
||||
<h3>Speed</h3>
|
||||
<p class="active"></p>
|
||||
</a>
|
||||
<ol class="video_speeds"></ol>
|
||||
</div>
|
||||
"""
|
||||
$.each @speeds, (index, speed) =>
|
||||
link = $('<a>').attr(href: "#").html("#{speed}x")
|
||||
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
|
||||
@setSpeed @currentSpeed
|
||||
|
||||
reRender: (newSpeeds, currentSpeed) ->
|
||||
@$('.video_speeds').empty()
|
||||
@$('.video_speeds li').removeClass('active')
|
||||
@speeds = newSpeeds
|
||||
$.each @speeds, (index, speed) =>
|
||||
link = $('<a>').attr(href: "#").html("#{speed}x")
|
||||
listItem = $('<li>').attr('data-speed', speed).html(link);
|
||||
listItem.addClass('active') if speed is currentSpeed
|
||||
@$('.video_speeds').prepend listItem
|
||||
@$('.video_speeds a').click @changeVideoSpeed
|
||||
|
||||
changeVideoSpeed: (event) =>
|
||||
event.preventDefault()
|
||||
unless $(event.target).parent().hasClass('active')
|
||||
@currentSpeed = $(event.target).parent().data('speed')
|
||||
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
|
||||
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
|
||||
|
||||
setSpeed: (speed) ->
|
||||
@$('.video_speeds li').removeClass('active')
|
||||
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
|
||||
@$('.speeds p.active').html("#{speed}x")
|
||||
@@ -0,0 +1,40 @@
|
||||
class @VideoVolumeControlAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@currentVolume = 100
|
||||
|
||||
bind: ->
|
||||
@$('.volume').mouseenter ->
|
||||
$(this).addClass('open')
|
||||
@$('.volume').mouseleave ->
|
||||
$(this).removeClass('open')
|
||||
@$('.volume>a').click(@toggleMute)
|
||||
|
||||
render: ->
|
||||
@el.prepend """
|
||||
<div class="volume">
|
||||
<a href="#"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""#"
|
||||
@slider = @$('.volume-slider').slider
|
||||
orientation: "vertical"
|
||||
range: "min"
|
||||
min: 0
|
||||
max: 100
|
||||
value: 100
|
||||
change: @onChange
|
||||
slide: @onChange
|
||||
|
||||
onChange: (event, ui) =>
|
||||
@currentVolume = ui.value
|
||||
$(@).trigger 'volumeChange', @currentVolume
|
||||
@$('.volume').toggleClass 'muted', @currentVolume == 0
|
||||
|
||||
toggleMute: =>
|
||||
if @currentVolume > 0
|
||||
@previousVolume = @currentVolume
|
||||
@slider.slider 'option', 'value', 0
|
||||
else
|
||||
@slider.slider 'option', 'value', @previousVolume
|
||||
@@ -31,8 +31,15 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
# export the grading policy
|
||||
policies_dir = export_fs.makeopendir('policies')
|
||||
course_run_policy_dir = policies_dir.makeopendir(course.location.name)
|
||||
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
|
||||
grading_policy.write(dumps(course.definition['data']['grading_policy']))
|
||||
if 'grading_policy' in course.definition['data']:
|
||||
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
|
||||
grading_policy.write(dumps(course.definition['data']['grading_policy']))
|
||||
|
||||
# export all of the course metadata in policy.json
|
||||
with course_run_policy_dir.open('policy.json', 'w') as course_policy:
|
||||
policy = {}
|
||||
policy = {'course/' + course.location.name: course.metadata}
|
||||
course_policy.write(dumps(policy))
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
|
||||
|
||||
@@ -56,6 +56,10 @@ def update_templates():
|
||||
available from the installed plugins
|
||||
"""
|
||||
|
||||
# cdodge: build up a list of all existing templates. This will be used to determine which
|
||||
# templates have been removed from disk - and thus we need to remove from the DB
|
||||
templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None])
|
||||
|
||||
for category, templates in all_templates().items():
|
||||
for template in templates:
|
||||
if 'display_name' not in template.metadata:
|
||||
@@ -85,3 +89,12 @@ def update_templates():
|
||||
modulestore('direct').update_item(template_location, template.data)
|
||||
modulestore('direct').update_children(template_location, template.children)
|
||||
modulestore('direct').update_metadata(template_location, template.metadata)
|
||||
|
||||
# remove template from list of templates to delete
|
||||
templates_to_delete = [t for t in templates_to_delete if t.location != template_location]
|
||||
|
||||
# now remove all templates which appear to have removed from disk
|
||||
if len(templates_to_delete) > 0:
|
||||
logging.debug('deleting dangling templates = {0}'.format(templates_to_delete))
|
||||
for template in templates_to_delete:
|
||||
modulestore('direct').delete_item(template.location)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: default
|
||||
data_dir: a_made_up_name
|
||||
data: |
|
||||
<videoalpha youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
|
||||
children: []
|
||||
@@ -4,6 +4,8 @@ import logging
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
@@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
155
common/lib/xmodule/xmodule/videoalpha_module.py
Normal file
155
common/lib/xmodule/xmodule/videoalpha_module.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
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
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VideoAlphaModule(XModule):
|
||||
"""
|
||||
XML source example:
|
||||
|
||||
<videoalpha show_captions="true"
|
||||
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
|
||||
url_name="lecture_21_3" display_name="S19V3: Vacancies"
|
||||
>
|
||||
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
|
||||
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
|
||||
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
|
||||
</videoalpha>
|
||||
"""
|
||||
video_time = 0
|
||||
icon_class = 'video'
|
||||
|
||||
js = {
|
||||
'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')],
|
||||
'coffee':
|
||||
[resource_string(__name__, 'js/src/time.coffee'),
|
||||
resource_string(__name__, 'js/src/videoalpha/display.coffee')] +
|
||||
[resource_string(__name__, 'js/src/videoalpha/display/' + filename)
|
||||
for filename
|
||||
in sorted(resource_listdir(__name__, 'js/src/videoalpha/display'))
|
||||
if filename.endswith('.coffee')]}
|
||||
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
|
||||
js_module_name = "VideoAlpha"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
self.youtube_streams = xmltree.get('youtube')
|
||||
self.sub = xmltree.get('sub')
|
||||
self.position = 0
|
||||
self.show_captions = xmltree.get('show_captions', 'true')
|
||||
self.sources = {
|
||||
'main': self._get_source(xmltree),
|
||||
'mp4': self._get_source(xmltree, ['mp4']),
|
||||
'webm': self._get_source(xmltree, ['webm']),
|
||||
'ogv': self._get_source(xmltree, ['ogv']),
|
||||
}
|
||||
self.track = self._get_track(xmltree)
|
||||
self.start_time, self.end_time = self._get_timeframe(xmltree)
|
||||
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
if 'position' in state:
|
||||
self.position = int(float(state['position']))
|
||||
|
||||
def _get_source(self, xmltree, exts=None):
|
||||
"""Find the first valid source, which ends with one of `exts`."""
|
||||
exts = ['mp4', 'ogv', 'avi', 'webm'] if exts is None else exts
|
||||
condition = lambda src: any([src.endswith(ext) for ext in exts])
|
||||
return self._get_first_external(xmltree, 'source', condition)
|
||||
|
||||
def _get_track(self, xmltree):
|
||||
# find the first valid track
|
||||
return self._get_first_external(xmltree, 'track')
|
||||
|
||||
def _get_first_external(self, xmltree, tag, condition=bool):
|
||||
"""Will return the first 'valid' element of the given tag.
|
||||
'valid' means that `condition('src' attribute) == True`
|
||||
"""
|
||||
result = None
|
||||
|
||||
for element in xmltree.findall(tag):
|
||||
src = element.get('src')
|
||||
if condition(src):
|
||||
result = src
|
||||
break
|
||||
return result
|
||||
|
||||
def _get_timeframe(self, xmltree):
|
||||
""" Converts 'from' and 'to' parameters in video tag to seconds.
|
||||
If there are no parameters, returns empty string. """
|
||||
|
||||
def parse_time(s):
|
||||
"""Converts s in '12:34:45' format to seconds. If s is
|
||||
None, returns empty string"""
|
||||
if s is None:
|
||||
return ''
|
||||
else:
|
||||
x = time.strptime(s, '%H:%M:%S')
|
||||
return datetime.timedelta(hours=x.tm_hour,
|
||||
minutes=x.tm_min,
|
||||
seconds=x.tm_sec).total_seconds()
|
||||
|
||||
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""Handle ajax calls to this video.
|
||||
TODO (vshnayder): This is not being called right now, so the
|
||||
position is not being saved.
|
||||
"""
|
||||
log.debug(u"GET {0}".format(get))
|
||||
log.debug(u"DISPATCH {0}".format(dispatch))
|
||||
if dispatch == 'goto_position':
|
||||
self.position = int(float(get['position']))
|
||||
log.info(u"NEW POSITION {0}".format(self.position))
|
||||
return json.dumps({'success': True})
|
||||
raise Http404()
|
||||
|
||||
def get_instance_state(self):
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
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('videoalpha.html', {
|
||||
'youtube_streams': self.youtube_streams,
|
||||
'id': self.location.html_id(),
|
||||
'sub': self.sub,
|
||||
'sources': self.sources,
|
||||
'track': self.track,
|
||||
'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,
|
||||
'start': self.start_time,
|
||||
'end': self.end_time
|
||||
})
|
||||
|
||||
|
||||
class VideoAlphaDescriptor(RawDescriptor):
|
||||
module_class = VideoAlphaModule
|
||||
stores_state = True
|
||||
template_dir_name = "videoalpha"
|
||||
@@ -515,6 +515,16 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
self._child_instances = None
|
||||
self._inherited_metadata = set()
|
||||
|
||||
|
||||
# Class level variable
|
||||
always_recalculate_grades = False
|
||||
"""
|
||||
Return whether this descriptor always requires recalculation of grades, for
|
||||
example if the score can change via an extrnal service, not just when the
|
||||
student interacts with the module on the page. A specific example is
|
||||
FoldIt, which posts grade-changing updates through a separate API.
|
||||
"""
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
'''
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
// It calls protexIsReady with a deferred command when it has finished
|
||||
// initialization and has drawn itself
|
||||
|
||||
function updateProtexField() {
|
||||
var problem = $('#protex_container').parents('.problem');
|
||||
var input_field = problem.find('input[type=hidden]');
|
||||
var protex_answer = protexCheckAnswer();
|
||||
var value = {protex_answer: protex_answer};
|
||||
//console.log(JSON.stringify(value));
|
||||
input_field.val(JSON.stringify(value));
|
||||
}
|
||||
|
||||
protexIsReady = function() {
|
||||
//Load target shape
|
||||
var target_shape = $('#target_shape').val();
|
||||
@@ -29,15 +38,18 @@
|
||||
|
||||
//Get answer from protex and store it into the hidden input field
|
||||
//when Check button is clicked
|
||||
var problem = $('#protex_container').parents('.problem');
|
||||
var check_button = problem.find('input.check');
|
||||
var input_field = problem.find('input[type=hidden]');
|
||||
check_button.on('click', function() {
|
||||
var fold_button = $("#fold-button");
|
||||
fold_button.on('click', function(){
|
||||
var problem = $('#protex_container').parents('.problem');
|
||||
var input_field = problem.find('input[type=hidden]');
|
||||
var protex_answer = protexCheckAnswer();
|
||||
var value = {protex_answer: protex_answer};
|
||||
//console.log(JSON.stringify(value));
|
||||
input_field.val(JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
updateProtexField();
|
||||
};
|
||||
|
||||
|
||||
/*function initializeProtex() {
|
||||
//Check to see if the two exported GWT functions protexSetTargetShape
|
||||
|
||||
@@ -343,6 +343,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
|
||||
Can return None if user doesn't have access, or if something else went wrong.
|
||||
cache: A StudentModuleCache
|
||||
"""
|
||||
if problem_descriptor.always_recalculate_grades:
|
||||
problem = module_creator(problem_descriptor)
|
||||
d = problem.get_score()
|
||||
if d is not None:
|
||||
return (d['score'], d['total'])
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
|
||||
# These are not problems, and do not have a score
|
||||
return (None, None)
|
||||
|
||||
@@ -33,7 +33,7 @@ from xmodule_modifiers import replace_course_urls, replace_static_urls, add_hist
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from statsd import statsd
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
|
||||
@@ -280,6 +280,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
|
||||
# Make an error module
|
||||
return err_descriptor.xmodule_constructor(system)(None, None)
|
||||
|
||||
system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id))
|
||||
_get_html = module.get_html
|
||||
|
||||
if wrap_xmodule_display == True:
|
||||
|
||||
0
lms/djangoapps/foldit/__init__.py
Normal file
0
lms/djangoapps/foldit/__init__.py
Normal file
95
lms/djangoapps/foldit/models.py
Normal file
95
lms/djangoapps/foldit/models.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Score(models.Model):
|
||||
"""
|
||||
This model stores the scores of different users on FoldIt problems.
|
||||
"""
|
||||
user = models.ForeignKey(User, db_index=True,
|
||||
related_name='foldit_scores')
|
||||
|
||||
# The XModule that wants to access this doesn't have access to the real
|
||||
# userid. Save the anonymized version so we can look up by that.
|
||||
unique_user_id = models.CharField(max_length=50, db_index=True)
|
||||
puzzle_id = models.IntegerField()
|
||||
best_score = models.FloatField(db_index=True)
|
||||
current_score = models.FloatField(db_index=True)
|
||||
score_version = models.IntegerField()
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class PuzzleComplete(models.Model):
|
||||
"""
|
||||
This keeps track of the sets of puzzles completed by each user.
|
||||
|
||||
e.g. PuzzleID 1234, set 1, subset 3. (Sets and subsets correspond to levels
|
||||
in the intro puzzles)
|
||||
"""
|
||||
class Meta:
|
||||
# there should only be one puzzle complete entry for any particular
|
||||
# puzzle for any user
|
||||
unique_together = ('user', 'puzzle_id', 'puzzle_set', 'puzzle_subset')
|
||||
ordering = ['puzzle_id']
|
||||
|
||||
user = models.ForeignKey(User, db_index=True,
|
||||
related_name='foldit_puzzles_complete')
|
||||
|
||||
# The XModule that wants to access this doesn't have access to the real
|
||||
# userid. Save the anonymized version so we can look up by that.
|
||||
unique_user_id = models.CharField(max_length=50, db_index=True)
|
||||
puzzle_id = models.IntegerField()
|
||||
puzzle_set = models.IntegerField(db_index=True)
|
||||
puzzle_subset = models.IntegerField(db_index=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return "PuzzleComplete({0}, id={1}, set={2}, subset={3}, created={4})".format(
|
||||
self.user.username, self.puzzle_id,
|
||||
self.puzzle_set, self.puzzle_subset,
|
||||
self.created)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def completed_puzzles(anonymous_user_id):
|
||||
"""
|
||||
Return a list of puzzles that this user has completed, as an array of
|
||||
dicts:
|
||||
|
||||
[ {'set': int,
|
||||
'subset': int,
|
||||
'created': datetime} ]
|
||||
"""
|
||||
complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id)
|
||||
return [{'set': c.puzzle_set,
|
||||
'subset': c.puzzle_subset,
|
||||
'created': c.created} for c in complete]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_level_complete(anonymous_user_id, level, sub_level, due=None):
|
||||
"""
|
||||
Return True if this user completed level--sub_level by due.
|
||||
|
||||
Users see levels as e.g. 4-5.
|
||||
|
||||
Args:
|
||||
level: int
|
||||
sub_level: int
|
||||
due (optional): If specified, a datetime. Ignored if None.
|
||||
"""
|
||||
complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id,
|
||||
puzzle_set=level,
|
||||
puzzle_subset=sub_level)
|
||||
if due is not None:
|
||||
complete = complete.filter(created__lte=due)
|
||||
|
||||
return complete.exists()
|
||||
|
||||
263
lms/djangoapps/foldit/tests.py
Normal file
263
lms/djangoapps/foldit/tests.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from foldit.views import foldit_ops, verify_code
|
||||
from foldit.models import PuzzleComplete
|
||||
from student.models import UserProfile, unique_id_for_user
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FolditTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.url = reverse('foldit_ops')
|
||||
|
||||
pwd = 'abc'
|
||||
self.user = User.objects.create_user('testuser', 'test@test.com', pwd)
|
||||
self.unique_user_id = unique_id_for_user(self.user)
|
||||
now = datetime.now()
|
||||
self.tomorrow = now + timedelta(days=1)
|
||||
self.yesterday = now - timedelta(days=1)
|
||||
|
||||
UserProfile.objects.create(user=self.user)
|
||||
|
||||
def make_request(self, post_data):
|
||||
request = self.factory.post(self.url, post_data)
|
||||
request.user = self.user
|
||||
return request
|
||||
|
||||
def test_SetPlayerPuzzleScores(self):
|
||||
|
||||
scores = [ {"PuzzleID": 994391,
|
||||
"ScoreType": "score",
|
||||
"BestScore": 0.078034,
|
||||
"CurrentScore":0.080035,
|
||||
"ScoreVersion":23}]
|
||||
scores_str = json.dumps(scores)
|
||||
|
||||
verify = {"Verify": verify_code(self.user.email, scores_str),
|
||||
"VerifyMethod":"FoldItVerify"}
|
||||
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
|
||||
'SetPlayerPuzzleScores': scores_str}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertEqual(response.content, json.dumps(
|
||||
[{"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [{
|
||||
"PuzzleID": 994391,
|
||||
"Status": "Success"}]}]))
|
||||
|
||||
|
||||
def test_SetPlayerPuzzleScores_many(self):
|
||||
|
||||
scores = [ {"PuzzleID": 994391,
|
||||
"ScoreType": "score",
|
||||
"BestScore": 0.078034,
|
||||
"CurrentScore":0.080035,
|
||||
"ScoreVersion":23},
|
||||
|
||||
{"PuzzleID": 994392,
|
||||
"ScoreType": "score",
|
||||
"BestScore": 0.078000,
|
||||
"CurrentScore":0.080011,
|
||||
"ScoreVersion":23}]
|
||||
|
||||
scores_str = json.dumps(scores)
|
||||
|
||||
verify = {"Verify": verify_code(self.user.email, scores_str),
|
||||
"VerifyMethod":"FoldItVerify"}
|
||||
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
|
||||
'SetPlayerPuzzleScores': scores_str}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertEqual(response.content, json.dumps(
|
||||
[{"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [{
|
||||
"PuzzleID": 994391,
|
||||
"Status": "Success"},
|
||||
|
||||
{"PuzzleID": 994392,
|
||||
"Status": "Success"}]}]))
|
||||
|
||||
|
||||
|
||||
def test_SetPlayerPuzzleScores_error(self):
|
||||
|
||||
scores = [ {"PuzzleID": 994391,
|
||||
"ScoreType": "score",
|
||||
"BestScore": 0.078034,
|
||||
"CurrentScore":0.080035,
|
||||
"ScoreVersion":23}]
|
||||
validation_str = json.dumps(scores)
|
||||
|
||||
verify = {"Verify": verify_code(self.user.email, validation_str),
|
||||
"VerifyMethod":"FoldItVerify"}
|
||||
|
||||
# change the real string -- should get an error
|
||||
scores[0]['ScoreVersion'] = 22
|
||||
scores_str = json.dumps(scores)
|
||||
|
||||
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
|
||||
'SetPlayerPuzzleScores': scores_str}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response_data = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
json.dumps([{
|
||||
"OperationID": "SetPlayerPuzzleScores",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"}]))
|
||||
|
||||
|
||||
def make_puzzles_complete_request(self, puzzles):
|
||||
"""
|
||||
Make a puzzles complete request, given an array of
|
||||
puzzles. E.g.
|
||||
|
||||
[ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
|
||||
"""
|
||||
puzzles_str = json.dumps(puzzles)
|
||||
|
||||
verify = {"Verify": verify_code(self.user.email, puzzles_str),
|
||||
"VerifyMethod":"FoldItVerify"}
|
||||
|
||||
data = {'SetPuzzlesCompleteVerify': json.dumps(verify),
|
||||
'SetPuzzlesComplete': puzzles_str}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def set_puzzle_complete_response(values):
|
||||
return json.dumps([{"OperationID":"SetPuzzlesComplete",
|
||||
"Value": values}])
|
||||
|
||||
|
||||
def test_SetPlayerPuzzlesComplete(self):
|
||||
|
||||
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 53524]))
|
||||
|
||||
|
||||
|
||||
def test_SetPlayerPuzzlesComplete_multiple(self):
|
||||
"""Check that state is stored properly"""
|
||||
|
||||
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 53524]))
|
||||
|
||||
puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3},
|
||||
{"PuzzleID": 15, "Set": 1, "SubSet": 1} ]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 14, 15, 53524]))
|
||||
|
||||
|
||||
|
||||
def test_SetPlayerPuzzlesComplete_level_complete(self):
|
||||
"""Check that the level complete function works"""
|
||||
|
||||
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 53524]))
|
||||
|
||||
puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3},
|
||||
{"PuzzleID": 15, "Set": 1, "SubSet": 1} ]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 14, 15, 53524]))
|
||||
|
||||
is_complete = partial(
|
||||
PuzzleComplete.is_level_complete, self.unique_user_id)
|
||||
|
||||
self.assertTrue(is_complete(1, 1))
|
||||
self.assertTrue(is_complete(1, 3))
|
||||
self.assertTrue(is_complete(1, 2))
|
||||
self.assertFalse(is_complete(4, 5))
|
||||
|
||||
puzzles = [ {"PuzzleID": 74, "Set": 4, "SubSet": 5} ]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertTrue(is_complete(4, 5))
|
||||
|
||||
# Now check due dates
|
||||
|
||||
self.assertTrue(is_complete(1, 1, due=self.tomorrow))
|
||||
self.assertFalse(is_complete(1, 1, due=self.yesterday))
|
||||
|
||||
|
||||
|
||||
def test_SetPlayerPuzzlesComplete_error(self):
|
||||
|
||||
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
|
||||
|
||||
puzzles_str = json.dumps(puzzles)
|
||||
|
||||
verify = {"Verify": verify_code(self.user.email, puzzles_str + "x"),
|
||||
"VerifyMethod":"FoldItVerify"}
|
||||
|
||||
data = {'SetPuzzlesCompleteVerify': json.dumps(verify),
|
||||
'SetPuzzlesComplete': puzzles_str}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response_data = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
json.dumps([{
|
||||
"OperationID": "SetPuzzlesComplete",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"}]))
|
||||
137
lms/djangoapps/foldit/views.py
Normal file
137
lms/djangoapps/foldit/views.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from foldit.models import Score, PuzzleComplete
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def foldit_ops(request):
|
||||
"""
|
||||
Endpoint view for foldit operations.
|
||||
"""
|
||||
responses = []
|
||||
if "SetPlayerPuzzleScores" in request.POST:
|
||||
puzzle_scores_json = request.POST.get("SetPlayerPuzzleScores")
|
||||
pz_verify_json = request.POST.get("SetPlayerPuzzleScoresVerify")
|
||||
log.debug("SetPlayerPuzzleScores message: puzzle scores: %r",
|
||||
puzzle_scores_json)
|
||||
|
||||
puzzle_score_verify = json.loads(pz_verify_json)
|
||||
if not verifies_ok(request.user.email,
|
||||
puzzle_scores_json, puzzle_score_verify):
|
||||
responses.append({"OperationID": "SetPlayerPuzzleScores",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"})
|
||||
log.warning("Verification of SetPlayerPuzzleScores failed:" +
|
||||
"user %s, scores json %r, verify %r",
|
||||
request.user, puzzle_scores_json, pz_verify_json)
|
||||
else:
|
||||
puzzle_scores = json.loads(puzzle_scores_json)
|
||||
responses.append(save_scores(request.user, puzzle_scores))
|
||||
|
||||
if "SetPuzzlesComplete" in request.POST:
|
||||
puzzles_complete_json = request.POST.get("SetPuzzlesComplete")
|
||||
pc_verify_json = request.POST.get("SetPuzzlesCompleteVerify")
|
||||
|
||||
log.debug("SetPuzzlesComplete message: %r",
|
||||
puzzles_complete_json)
|
||||
|
||||
puzzles_complete_verify = json.loads(pc_verify_json)
|
||||
|
||||
if not verifies_ok(request.user.email,
|
||||
puzzles_complete_json, puzzles_complete_verify):
|
||||
responses.append({"OperationID": "SetPuzzlesComplete",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"})
|
||||
log.warning("Verification of SetPuzzlesComplete failed:" +
|
||||
" user %s, puzzles json %r, verify %r",
|
||||
request.user, puzzles_complete_json, pc_verify_json)
|
||||
else:
|
||||
puzzles_complete = json.loads(puzzles_complete_json)
|
||||
responses.append(save_complete(request.user, puzzles_complete))
|
||||
|
||||
return HttpResponse(json.dumps(responses))
|
||||
|
||||
|
||||
def verify_code(email, val):
|
||||
"""
|
||||
Given the email and passed in value (str), return the expected
|
||||
verification code.
|
||||
"""
|
||||
# TODO: is this the right string?
|
||||
verification_string = email.lower() + '|' + val
|
||||
return hashlib.md5(verification_string).hexdigest()
|
||||
|
||||
|
||||
def verifies_ok(email, val, verification):
|
||||
"""
|
||||
Check that the hash_str matches the expected hash of val.
|
||||
|
||||
Returns True if verification ok, False otherwise
|
||||
"""
|
||||
if verification.get("VerifyMethod") != "FoldItVerify":
|
||||
log.debug("VerificationMethod in %r isn't FoldItVerify", verification)
|
||||
return False
|
||||
hash_str = verification.get("Verify")
|
||||
|
||||
return verify_code(email, val) == hash_str
|
||||
|
||||
|
||||
def save_scores(user, puzzle_scores):
|
||||
score_responses = []
|
||||
for score in puzzle_scores:
|
||||
log.debug("score: %s", score)
|
||||
# expected keys ScoreType, PuzzleID (int),
|
||||
# BestScore (energy), CurrentScore (Energy), ScoreVersion (int)
|
||||
|
||||
puzzle_id = score['PuzzleID']
|
||||
|
||||
# TODO: save the score
|
||||
|
||||
# SetPlayerPuzzleScoreResponse object
|
||||
score_responses.append({'PuzzleID': puzzle_id,
|
||||
'Status': 'Success'})
|
||||
|
||||
return {"OperationID": "SetPlayerPuzzleScores", "Value": score_responses}
|
||||
|
||||
|
||||
def save_complete(user, puzzles_complete):
|
||||
"""
|
||||
Returned list of PuzzleIDs should be in sorted order (I don't think client
|
||||
cares, but tests do)
|
||||
"""
|
||||
for complete in puzzles_complete:
|
||||
log.debug("Puzzle complete: %s", complete)
|
||||
puzzle_id = complete['PuzzleID']
|
||||
puzzle_set = complete['Set']
|
||||
puzzle_subset = complete['SubSet']
|
||||
|
||||
# create if not there
|
||||
PuzzleComplete.objects.get_or_create(
|
||||
user=user,
|
||||
unique_user_id=unique_id_for_user(user),
|
||||
puzzle_id=puzzle_id,
|
||||
puzzle_set=puzzle_set,
|
||||
puzzle_subset=puzzle_subset)
|
||||
|
||||
# List of all puzzle ids of intro-level puzzles completed ever, including on this
|
||||
# request
|
||||
# TODO: this is just in this request...
|
||||
|
||||
complete_responses = list(pc.puzzle_id
|
||||
for pc in PuzzleComplete.objects.filter(user=user))
|
||||
|
||||
return {"OperationID": "SetPuzzlesComplete", "Value": complete_responses}
|
||||
@@ -200,7 +200,6 @@ COURSE_TITLE = "Circuits and Electronics"
|
||||
### Dark code. Should be enabled in local settings for devel.
|
||||
|
||||
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
|
||||
QUICKEDIT = False
|
||||
|
||||
WIKI_ENABLED = False
|
||||
|
||||
@@ -576,6 +575,9 @@ INSTALLED_APPS = (
|
||||
'wiki.plugins.notifications',
|
||||
'course_wiki.plugins.markdownedx',
|
||||
|
||||
# foldit integration
|
||||
'foldit',
|
||||
|
||||
# For testing
|
||||
'django.contrib.admin', # only used in DEBUG mode
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
|
||||
|
||||
DEBUG = True
|
||||
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
|
||||
QUICKEDIT = True
|
||||
|
||||
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT]
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ COURSE_TITLE = "edx4edx: edX Author Course"
|
||||
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
|
||||
|
||||
### Dark code. Should be enabled in local settings for devel.
|
||||
QUICKEDIT = True
|
||||
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
|
||||
###
|
||||
PIPELINE_CSS_COMPRESSOR = None
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This is a library for edx4edx, allowing users to practice writing problems.
|
||||
@@ -1 +0,0 @@
|
||||
from check import *
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from random import choice
|
||||
import string
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
import capa.capa_problem as lcp
|
||||
from dogfood.views import update_problem
|
||||
|
||||
|
||||
def GenID(length=8, chars=string.letters + string.digits):
|
||||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
randomid = GenID()
|
||||
|
||||
|
||||
def check_problem_code(ans, the_lcp, correct_answers, false_answers):
|
||||
"""
|
||||
ans = student's answer
|
||||
the_lcp = LoncapaProblem instance
|
||||
|
||||
returns dict {'ok':is_ok,'msg': message with iframe}
|
||||
"""
|
||||
pfn = "dog%s" % randomid
|
||||
pfn += the_lcp.problem_id.replace('filename', '') # add problem ID to dogfood problem name
|
||||
update_problem(pfn, ans, filestore=the_lcp.system.filestore)
|
||||
msg = '<hr width="100%"/>'
|
||||
msg += '<iframe src="%s/dogfood/filename%s" width="95%%" height="400" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL, pfn)
|
||||
msg += '<hr width="100%"/>'
|
||||
|
||||
endmsg = """<p><font size="-1" color="purple">Note: if the code text box disappears after clicking on "Check",
|
||||
please type something in the box to make it refresh properly. This is a
|
||||
bug with Chrome; it does not happen with Firefox. It is being fixed.
|
||||
</font></p>"""
|
||||
|
||||
is_ok = True
|
||||
if (not correct_answers) or (not false_answers):
|
||||
ret = {'ok': is_ok,
|
||||
'msg': msg + endmsg,
|
||||
}
|
||||
return ret
|
||||
|
||||
try:
|
||||
# check correctness
|
||||
fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn)
|
||||
test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system)
|
||||
|
||||
if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1') == 'correct'):
|
||||
is_ok = False
|
||||
if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1') == 'correct'):
|
||||
is_ok = False
|
||||
except Exception, err:
|
||||
is_ok = False
|
||||
msg += "<p>Error: %s</p>" % str(err).replace('<', '<')
|
||||
msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<', '<')
|
||||
|
||||
ret = {'ok': is_ok,
|
||||
'msg': msg + endmsg,
|
||||
}
|
||||
return ret
|
||||
@@ -1,325 +0,0 @@
|
||||
'''
|
||||
dogfood.py
|
||||
|
||||
For using mitx / edX / i4x in checking itself.
|
||||
|
||||
df_capa_problem: accepts an XML file for a problem, and renders it.
|
||||
'''
|
||||
import logging
|
||||
import datetime
|
||||
import re
|
||||
import os # FIXME - use OSFS instead
|
||||
|
||||
from fs.osfs import OSFS
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.mail import send_mail
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
import track.views
|
||||
from lxml import etree
|
||||
|
||||
from courseware.module_render import make_track_function, ModuleSystem, get_module
|
||||
from courseware.models import StudentModule
|
||||
from multicourse import multicourse_settings
|
||||
from student.models import UserProfile
|
||||
from util.cache import cache
|
||||
from util.views import accepts
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
#import courseware.modules
|
||||
import xmodule
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True))
|
||||
|
||||
DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings
|
||||
|
||||
|
||||
def update_problem(pfn, pxml, coursename=None, overwrite=True, filestore=None):
|
||||
'''
|
||||
update problem with filename pfn, and content (xml) pxml.
|
||||
'''
|
||||
if not filestore:
|
||||
if not coursename: coursename = DOGFOOD_COURSENAME
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
pfn2 = settings.DATA_DIR + xp + 'problems/%s.xml' % pfn
|
||||
fp = open(pfn2, 'w')
|
||||
else:
|
||||
pfn2 = 'problems/%s.xml' % pfn
|
||||
fp = filestore.open(pfn2, 'w')
|
||||
log.debug('[dogfood.update_problem] pfn2=%s' % pfn2)
|
||||
|
||||
if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False
|
||||
pxmls = pxml if type(pxml) in [str, unicode] else etree.tostring(pxml, pretty_print=True)
|
||||
fp.write(pxmls)
|
||||
fp.close()
|
||||
|
||||
|
||||
def df_capa_problem(request, id=None):
|
||||
'''
|
||||
dogfood capa problem.
|
||||
|
||||
Accepts XML for a problem, inserts it into the dogfood course.xml.
|
||||
Returns rendered problem.
|
||||
'''
|
||||
# "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
|
||||
|
||||
if settings.DEBUG:
|
||||
log.debug('[lib.dogfood.df_capa_problem] id=%s' % id)
|
||||
|
||||
if not 'coursename' in request.session:
|
||||
coursename = DOGFOOD_COURSENAME
|
||||
else:
|
||||
coursename = request.session['coursename']
|
||||
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
module = 'problem'
|
||||
|
||||
try:
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
except Exception, err:
|
||||
log.error("[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err)
|
||||
xml = None
|
||||
|
||||
# if problem of given ID does not exist, then create it
|
||||
# do this only if course.xml has a section named "DogfoodProblems"
|
||||
if not xml:
|
||||
m = re.match('filename([A-Za-z0-9_]+)$', id) # extract problem filename from ID given
|
||||
if not m:
|
||||
raise Exception, '[lib.dogfood.df_capa_problem] Illegal problem id %s' % id
|
||||
pfn = m.group(1)
|
||||
log.debug('[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn)
|
||||
|
||||
# add problem to course.xml
|
||||
fn = settings.DATA_DIR + xp + 'course.xml'
|
||||
xml = etree.parse(fn)
|
||||
seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure!
|
||||
if seq == None:
|
||||
raise Exception, "[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!"
|
||||
newprob = etree.Element('problem')
|
||||
newprob.set('type', 'lecture')
|
||||
newprob.set('showanswer', 'attempted')
|
||||
newprob.set('rerandomize', 'never')
|
||||
newprob.set('title', pfn)
|
||||
newprob.set('filename', pfn)
|
||||
newprob.set('name', pfn)
|
||||
seq.append(newprob)
|
||||
fp = open(fn, 'w')
|
||||
fp.write(etree.tostring(xml, pretty_print=True)) # write new XML
|
||||
fp.close()
|
||||
|
||||
# now create new problem file
|
||||
# update_problem(pfn,'<problem>\n<text>\nThis is a new problem\n</text>\n</problem>\n',coursename,overwrite=False)
|
||||
|
||||
# reset cache entry
|
||||
user = request.user
|
||||
groups = content_parser.user_groups(user)
|
||||
options = {'dev_content': settings.DEV_CONTENT,
|
||||
'groups': groups}
|
||||
filename = xp + 'course.xml'
|
||||
cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
|
||||
log.debug('[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key)
|
||||
#cache.delete(cache_key)
|
||||
tree = content_parser.course_xml_process(xml) # add ID tags
|
||||
cache.set(cache_key, etree.tostring(tree), 60)
|
||||
# settings.DEFAULT_GROUPS.append('dev') # force content_parser.course_file to not use cache
|
||||
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
if not xml:
|
||||
log.debug("[lib.dogfood.df_capa_problem] problem xml not found!")
|
||||
|
||||
# add problem ID to list so that is_staff check can be bypassed
|
||||
request.session['dogfood_id'] = id
|
||||
|
||||
# hand over to quickedit to do the rest
|
||||
return quickedit(request, id=id, qetemplate='dogfood.html', coursename=coursename)
|
||||
|
||||
|
||||
def quickedit(request, id=None, qetemplate='quickedit.html', coursename=None):
|
||||
'''
|
||||
quick-edit capa problem.
|
||||
|
||||
Maybe this should be moved into capa/views.py
|
||||
Or this should take a "module" argument, and the quickedit moved into capa_module.
|
||||
|
||||
id is passed in from url resolution
|
||||
qetemplate is used by dogfood.views.dj_capa_problem, to override normal template
|
||||
'''
|
||||
print "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
|
||||
print "In deployed use, this will only edit on one server"
|
||||
print "We need a setting to disable for production where there is"
|
||||
print "a load balanacer"
|
||||
|
||||
if not request.user.is_staff:
|
||||
if not ('dogfood_id' in request.session and request.session['dogfood_id'] == id):
|
||||
return redirect('/')
|
||||
|
||||
if id == 'course.xml':
|
||||
return quickedit_git_reload(request)
|
||||
|
||||
# get coursename if stored
|
||||
if not coursename:
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
def get_lcp(coursename, id):
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
# create empty student state for this problem, if not previously existing
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_id=id)
|
||||
student_module_cache = list(s) if s is not None else []
|
||||
#if len(s) == 0 or s is None:
|
||||
# smod=StudentModule(student=request.user,
|
||||
# module_type = 'problem',
|
||||
# module_id=id,
|
||||
# state=instance.get_state())
|
||||
# smod.save()
|
||||
# student_module_cache = [smod]
|
||||
module = 'problem'
|
||||
module_xml = etree.XML(content_parser.module_xml(request.user, module, 'id', id, coursename))
|
||||
module_id = module_xml.get('id')
|
||||
log.debug("module_id = %s" % module_id)
|
||||
(instance, smod, module_type) = get_module(request.user, request, module_xml, student_module_cache, position=None)
|
||||
log.debug('[dogfood.views] instance=%s' % instance)
|
||||
lcp = instance.lcp
|
||||
log.debug('[dogfood.views] lcp=%s' % lcp)
|
||||
pxml = lcp.tree
|
||||
pxmls = etree.tostring(pxml, pretty_print=True)
|
||||
return instance, pxmls
|
||||
|
||||
def old_get_lcp(coursename, id):
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
module = 'problem'
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/' + id + '/'
|
||||
|
||||
# Create the module (instance of capa_module.Module)
|
||||
system = ModuleSystem(track_function=make_track_function(request),
|
||||
render_function=None,
|
||||
render_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
filestore=OSFS(settings.DATA_DIR + xp),
|
||||
)
|
||||
instance = xmodule.get_module_class(module)(system,
|
||||
xml,
|
||||
id,
|
||||
state=None)
|
||||
log.info('ajax_url = ' + instance.ajax_url)
|
||||
|
||||
# create empty student state for this problem, if not previously existing
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_state_key=id)
|
||||
if len(s) == 0 or s is None:
|
||||
smod = StudentModule(student=request.user,
|
||||
module_type='problem',
|
||||
module_state_key=id,
|
||||
state=instance.get_instance_state())
|
||||
smod.save()
|
||||
|
||||
lcp = instance.lcp
|
||||
pxml = lcp.tree
|
||||
pxmls = etree.tostring(pxml, pretty_print=True)
|
||||
|
||||
return instance, pxmls
|
||||
|
||||
instance, pxmls = get_lcp(coursename, id)
|
||||
|
||||
# if there was a POST, then process it
|
||||
msg = ''
|
||||
if 'qesubmit' in request.POST:
|
||||
action = request.POST['qesubmit']
|
||||
if "Revert" in action:
|
||||
msg = "Reverted to original"
|
||||
elif action == 'Change Problem':
|
||||
key = 'quickedit_%s' % id
|
||||
if not key in request.POST:
|
||||
msg = "oops, missing code key=%s" % key
|
||||
else:
|
||||
newcode = request.POST[key]
|
||||
|
||||
# see if code changed
|
||||
if str(newcode) == str(pxmls) or '<?xml version="1.0"?>\n' + str(newcode) == str(pxmls):
|
||||
msg = "No changes"
|
||||
else:
|
||||
# check new code
|
||||
isok = False
|
||||
try:
|
||||
newxml = etree.fromstring(newcode)
|
||||
isok = True
|
||||
except Exception, err:
|
||||
msg = "Failed to change problem: XML error \"<font color=red>%s</font>\"" % err
|
||||
|
||||
if isok:
|
||||
filename = instance.lcp.fileobject.name
|
||||
fp = open(filename, 'w') # TODO - replace with filestore call?
|
||||
fp.write(newcode)
|
||||
fp.close()
|
||||
msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename
|
||||
instance, pxmls = get_lcp(coursename, id)
|
||||
|
||||
lcp = instance.lcp
|
||||
|
||||
# get the rendered problem HTML
|
||||
phtml = instance.get_html()
|
||||
# phtml = instance.get_problem_html()
|
||||
|
||||
context = {'id': id,
|
||||
'msg': msg,
|
||||
'lcp': lcp,
|
||||
'filename': lcp.fileobject.name,
|
||||
'pxmls': pxmls,
|
||||
'phtml': phtml,
|
||||
"destroy_js": '',
|
||||
'init_js': '',
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
}
|
||||
|
||||
result = render_to_response(qetemplate, context)
|
||||
return result
|
||||
|
||||
|
||||
def quickedit_git_reload(request):
|
||||
'''
|
||||
reload course.xml and all courseware files for this course, from the git repo.
|
||||
assumes the git repo has already been setup.
|
||||
staff only.
|
||||
'''
|
||||
if not request.user.is_staff:
|
||||
return redirect('/')
|
||||
|
||||
# get coursename if stored
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
|
||||
msg = ""
|
||||
if 'cancel' in request.POST:
|
||||
return redirect("/courseware")
|
||||
|
||||
if 'gitupdate' in request.POST:
|
||||
import os # FIXME - put at top?
|
||||
#cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/',''))
|
||||
cmd = "cd ../data%s; ./GITRELOAD '%s'" % (xp, xp.replace('/', ''))
|
||||
msg += '<p>cmd: %s</p>' % cmd
|
||||
ret = os.popen(cmd).read()
|
||||
msg += '<p><pre>%s</pre></p>' % ret.replace('<', '<')
|
||||
msg += "<p>git update done!</p>"
|
||||
|
||||
context = {'id': id,
|
||||
'msg': msg,
|
||||
'coursename': coursename,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
}
|
||||
|
||||
result = render_to_response("gitupdate.html", context)
|
||||
return result
|
||||
@@ -248,3 +248,17 @@ section.self-assessment {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
section.foldit {
|
||||
table {
|
||||
margin-top: 10px;
|
||||
}
|
||||
th {
|
||||
text-align: center;
|
||||
}
|
||||
td {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ header.global {
|
||||
margin-right: 5px;
|
||||
|
||||
> a {
|
||||
@include background-image(linear-gradient(-90deg, #fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%));
|
||||
@include background-image(linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%));
|
||||
border: 1px solid transparent;
|
||||
border-color: rgb(200,200,200);
|
||||
@include border-radius(3px);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
|
||||
<script type="text/javascript">
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
document.location.protocol + '//www.youtube.com/iframe_api">\x3C/script>');
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -61,7 +61,7 @@
|
||||
</script>
|
||||
|
||||
% if timer_expiration_duration:
|
||||
<script type="text/javascript">
|
||||
<script type="text/javascript">
|
||||
var timer = {
|
||||
timer_inst : null,
|
||||
end_time : null,
|
||||
@@ -79,8 +79,8 @@
|
||||
remaining_secs = remaining_secs % 3600;
|
||||
var minutes = pretty_time_string(Math.floor(remaining_secs / 60));
|
||||
remaining_secs = remaining_secs % 60;
|
||||
var seconds = pretty_time_string(Math.floor(remaining_secs));
|
||||
|
||||
var seconds = pretty_time_string(Math.floor(remaining_secs));
|
||||
|
||||
var remainingTimeString = hours + ":" + minutes + ":" + seconds;
|
||||
return remainingTimeString;
|
||||
},
|
||||
@@ -100,11 +100,11 @@
|
||||
end : function(self) {
|
||||
clearInterval(self.timer_inst);
|
||||
// redirect to specified URL:
|
||||
window.location = "${time_expired_redirect_url}";
|
||||
window.location = "${time_expired_redirect_url}";
|
||||
}
|
||||
}
|
||||
// start timer right away:
|
||||
timer.start();
|
||||
}
|
||||
// start timer right away:
|
||||
timer.start();
|
||||
</script>
|
||||
% endif
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
## -----------------------------------------------------------------------------
|
||||
## Template for lib.dogfood.views.dj_capa_problem
|
||||
##
|
||||
## Used for viewing assesment problems in "dogfood" self-evaluation mode
|
||||
## -----------------------------------------------------------------------------
|
||||
<head>
|
||||
<link rel="stylesheet" href="${static.url('css/vendor/jquery.treeview.css')}" type="text/css" media="all" />
|
||||
## <link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
|
||||
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
|
||||
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
## <%static:css group='application'/>
|
||||
% endif
|
||||
|
||||
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
|
||||
% endif
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/jquery.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery-ui.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script>
|
||||
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:js group='application'/>
|
||||
% endif
|
||||
|
||||
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
% for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]:
|
||||
<script type="text/javascript" src="${jsfn}"></script>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
## codemirror
|
||||
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
|
||||
|
||||
## alternate codemirror
|
||||
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/lib/codemirror.js"></script>
|
||||
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/xml/xml.js"></script>
|
||||
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/python/python.js"></script>
|
||||
|
||||
## image input: for clicking on images (see imageinput.html)
|
||||
<script type="text/javascript" src="/static/js/imageinput.js"></script>
|
||||
|
||||
|
||||
<%include file="mathjax_include.html" />
|
||||
|
||||
</head>
|
||||
<body class="courseware">
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="/static/js/html5shiv.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<div class="courseware"></div>
|
||||
|
||||
## -----------------------------------------------------------------------------
|
||||
## information
|
||||
|
||||
## <hr width="100%">
|
||||
## <h2>Rendition of your problem code</h2>
|
||||
## <hr width="100%">
|
||||
|
||||
## -----------------------------------------------------------------------------
|
||||
## rendered problem display
|
||||
|
||||
<script>
|
||||
${init_js}
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
.problem-header {display:none;}
|
||||
.staff {display:none;}
|
||||
.correct { display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
background: url("/static/images/correct-icon.png") center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px; }
|
||||
.incorrect{
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
background: url("/static/images/incorrect-icon.png") center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 6px; }
|
||||
.unanswered {
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
background: url("/static/images/unanswered-icon.png") center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 14px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
|
||||
<section class="course-content">
|
||||
<form>
|
||||
${phtml}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script>
|
||||
## <script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
|
||||
|
||||
## image input: for clicking on images (see imageinput.html)
|
||||
<script type="text/javascript" src="/static/js/imageinput.js"></script>
|
||||
|
||||
<script type="text/javascript" >
|
||||
var codemirror_set= {}; // associative array of codemirror objects
|
||||
</script>
|
||||
|
||||
<%block name="js_extra"/>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
28
lms/templates/foldit.html
Normal file
28
lms/templates/foldit.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<section class="foldit">
|
||||
<p><strong>Due:</strong> ${due}
|
||||
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
% if success:
|
||||
You have successfully gotten to level ${goal_level}.
|
||||
% else:
|
||||
You have not yet gotten to level ${goal_level}.
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<h3>Completed puzzles</h3>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>Submitted</th>
|
||||
</tr>
|
||||
% for puzzle in completed:
|
||||
<tr>
|
||||
<td>${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}</td>
|
||||
<td>${puzzle['created'].strftime('%Y-%m-%d %H:%M')}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
|
||||
</section>
|
||||
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
|
||||
<html> <head>
|
||||
<title>edX gitupdate</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<hr>
|
||||
<h1>edX gitupdate</h1>
|
||||
<hr>
|
||||
|
||||
<h2>Coursename: ${coursename}</h2>
|
||||
|
||||
% if msg:
|
||||
|
||||
${msg}
|
||||
|
||||
% else:
|
||||
<p>
|
||||
Do you REALLY want to overwrite all the course.xml + problems + html
|
||||
files with version from the main git repository?
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="submit" value="Do git update" name="gitupdate" />
|
||||
## <input type="submit" value="Cancel" name="cancel" />
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf}"/>
|
||||
</form>
|
||||
% endif
|
||||
|
||||
<p><a href="${MITX_ROOT_URL}/courseware/">Return to site</a></p>
|
||||
|
||||
</body> </html>
|
||||
@@ -1,180 +0,0 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
## -----------------------------------------------------------------------------
|
||||
## Template for courseware.views.quickedit
|
||||
##
|
||||
## Used for quick-edit link present when viewing capa-format assesment problems.
|
||||
## -----------------------------------------------------------------------------
|
||||
<head>
|
||||
<link rel="stylesheet" href="${static.url('css/vendor/jquery.treeview.css')}" type="text/css" media="all" />
|
||||
## <link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
|
||||
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
|
||||
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:css group='application'/>
|
||||
% endif
|
||||
|
||||
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
|
||||
% endif
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/jquery.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery-ui.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script>
|
||||
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:js group='application'/>
|
||||
% endif
|
||||
|
||||
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
% for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]:
|
||||
<script type="text/javascript" src="${jsfn}"></script>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
## codemirror
|
||||
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
|
||||
|
||||
## alternate codemirror
|
||||
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/lib/codemirror.js"></script>
|
||||
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/xml/xml.js"></script>
|
||||
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/python/python.js"></script>
|
||||
|
||||
## image input: for clicking on images (see imageinput.html)
|
||||
<script type="text/javascript" src="/static/js/imageinput.js"></script>
|
||||
|
||||
## <script type="text/javascript">
|
||||
## var codemirror_set = {}; // track all codemirror textareas, so they can be refreshed on page changes
|
||||
## </script>
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="${static.url('js/html5shiv.js')}"></script>
|
||||
<![endif]-->
|
||||
|
||||
<%block name="headextra"/>
|
||||
|
||||
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
|
||||
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
|
||||
MathJax extension libraries -->
|
||||
<%include file="mathjax_include.html" />
|
||||
|
||||
</head>
|
||||
<body class="courseware" style="text-align:left;" >
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
.CodeMirror {border-style: solid;
|
||||
border-width: 1px;}
|
||||
.CodeMirror-scroll {
|
||||
height: 500;
|
||||
width: 100%
|
||||
}
|
||||
</style>
|
||||
|
||||
## -----------------------------------------------------------------------------
|
||||
## information and i4x PSL code
|
||||
|
||||
<hr width="100%">
|
||||
<h2>QuickEdit</h2>
|
||||
<hr width="100%">
|
||||
<ul>
|
||||
<li>File = ${filename}</li>
|
||||
<li>ID = ${id}</li>
|
||||
</ul>
|
||||
|
||||
<form method="post">
|
||||
<textarea rows="40" cols="160" name="quickedit_${id}" id="quickedit_${id}">${pxmls|h}</textarea>
|
||||
<br/>
|
||||
<input type="submit" value="Change Problem" name="qesubmit" />
|
||||
<input type="submit" value="Revert to original" name="qesubmit" />
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf}"/>
|
||||
</form>
|
||||
|
||||
<span>${msg|n}</span>
|
||||
|
||||
## -----------------------------------------------------------------------------
|
||||
## rendered problem display
|
||||
|
||||
<script>
|
||||
// height: auto;
|
||||
// overflow-y: hidden;
|
||||
// overflow-x: auto;
|
||||
|
||||
$(function(){
|
||||
var cm = CodeMirror.fromTextArea(document.getElementById("quickedit_${id}"),
|
||||
{ 'mode': {name: "xml", alignCDATA: true},
|
||||
lineNumbers: true
|
||||
});
|
||||
|
||||
// $('.my-wymeditor').wymeditor();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<hr width="100%">
|
||||
|
||||
<script>
|
||||
${init_js}
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
.staff {display:none;}
|
||||
.correct { display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
background: url("/static/images/correct-icon.png") center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px; }
|
||||
.incorrect{
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
background: url("/static/images/incorrect-icon.png") center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 6px; }
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
|
||||
<section class="course-content">
|
||||
<div id="seq_content">
|
||||
<form>
|
||||
${phtml}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script>
|
||||
## <script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
|
||||
|
||||
<script type="text/javascript" >
|
||||
var codemirror_set= {}; // associative array of codemirror objects
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/jquery.scrollTo-1.4.2-min.js')}"></script>
|
||||
|
||||
<%block name="js_extra"/>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
43
lms/templates/videoalpha.html
Normal file
43
lms/templates/videoalpha.html
Normal file
@@ -0,0 +1,43 @@
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<h2> ${display_name} </h2>
|
||||
% endif
|
||||
|
||||
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
<div id="stub_out_video_for_testing"></div>
|
||||
%else:
|
||||
<div
|
||||
id="video_${id}"
|
||||
class="video"
|
||||
data-streams="${youtube_streams}"
|
||||
${'data-sub="{}"'.format(sub) if sub else ''}
|
||||
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
|
||||
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
|
||||
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
|
||||
data-caption-data-dir="${data_dir}"
|
||||
data-show-captions="${show_captions}"
|
||||
data-start="${start}"
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
% if sources.get('main'):
|
||||
<div class="video-sources">
|
||||
<p>Download video <a href="${sources.get('main')}">here</a>.</p>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if track:
|
||||
<div class="video-tracks">
|
||||
<p>Download subtitles <a href="${track}">here</a>.</p>
|
||||
</div>
|
||||
% endif
|
||||
10
lms/urls.py
10
lms/urls.py
@@ -320,10 +320,6 @@ if settings.COURSEWARE_ENABLED:
|
||||
'courseware.views.static_tab', name="static_tab"),
|
||||
)
|
||||
|
||||
if settings.QUICKEDIT:
|
||||
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
|
||||
urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),)
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
@@ -361,6 +357,12 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
||||
url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'),
|
||||
)
|
||||
|
||||
# FoldIt views
|
||||
urlpatterns += (
|
||||
# The path is hardcoded into their app...
|
||||
url(r'^comm/foldit_ops', 'foldit.views.foldit_ops', name="foldit_ops"),
|
||||
)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
Reference in New Issue
Block a user