Commit 20553b00 by Brian Talbot

resolving merge conflict with master

parents 9feb8100 bfcc0559
......@@ -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)
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>
<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",
]
}
)
& {
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;
}
}
}
}
}
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):
......
*.js
# Please do not ignore *.js files. Some xmodules are written in JS.
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
class @SubviewAlpha
constructor: (options) ->
$.each options, (key, value) =>
@[key] = value
@initialize()
@render()
@bind()
$: (selector) ->
$(selector, @el)
initialize: ->
render: ->
bind: ->
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;
}());
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()
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')
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()
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)}")
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)
\ No newline at end of file
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")
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,9 +31,16 @@ 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)
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=''):
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
......
......@@ -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)
---
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__)
......
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,16 +38,19 @@
//Get answer from protex and store it into the hidden input field
//when Check button is clicked
var fold_button = $("#fold-button");
fold_button.on('click', function(){
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 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
// and protexCheckAnswer have been appended to global scope -- this
......
......@@ -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:
......
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()
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"}]))
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
......
This is a library for edx4edx, allowing users to practice writing problems.
#!/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('<', '&#60;')
msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<', '&#60;')
ret = {'ok': is_ok,
'msg': msg + endmsg,
}
return ret
'''
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('<', '&lt;')
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;
}
}
\ No newline at end of file
......@@ -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">
......
<%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>
<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>
\ No newline at end of file
<!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>
<%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>
% 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
......@@ -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:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment