Merge remote-tracking branch 'origin/master' into feature/vik/disable-spell-check
This commit is contained in:
@@ -34,8 +34,10 @@ setup(
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"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",
|
||||
@@ -43,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",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -703,15 +703,15 @@ class CapaDescriptor(RawDescriptor):
|
||||
|
||||
def get_context(self):
|
||||
_context = RawDescriptor.get_context(self)
|
||||
_context.update({'markdown': self.metadata.get('markdown', '')})
|
||||
_context.update({'markdown': self.metadata.get('markdown', ''),
|
||||
'enable_markdown' : 'markdown' in self.metadata})
|
||||
return _context
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove metadata from the editable fields since it has its own editor"""
|
||||
subset = super(CapaDescriptor, self).editable_metadata_fields
|
||||
if 'markdown' in subset:
|
||||
subset.remove('markdown')
|
||||
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
|
||||
subset = [field for field in super(CapaDescriptor,self).editable_metadata_fields
|
||||
if field not in ['markdown', 'empty']]
|
||||
return subset
|
||||
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# utility function to get datetime objects for dates used to
|
||||
# compute the is_new flag and the sorting_score
|
||||
def to_datetime(timestamp):
|
||||
return datetime.fromtimestamp(time.mktime(timestamp))
|
||||
return datetime(*timestamp[:6])
|
||||
|
||||
def get_date(field):
|
||||
timetuple = self._try_parse_time(field)
|
||||
@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
raise ValueError("First appointment date must be before last appointment date")
|
||||
if self.registration_end_date > self.last_eligible_appointment_date:
|
||||
raise ValueError("Registration end date must be before last appointment date")
|
||||
|
||||
self.exam_url = exam_info.get('Exam_URL')
|
||||
|
||||
def _try_parse_time(self, key):
|
||||
"""
|
||||
@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_test_center_exam(self, exam_series_code):
|
||||
exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
|
||||
return exams[0] if len(exams) == 1 else None
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.display_name
|
||||
|
||||
@@ -120,7 +120,7 @@ div.combined-rubric-container {
|
||||
}
|
||||
}
|
||||
|
||||
b.rubric-category {
|
||||
span.rubric-category {
|
||||
font-size: .9em;
|
||||
}
|
||||
padding-bottom: 5px;
|
||||
|
||||
@@ -49,10 +49,18 @@ p {
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
|
||||
span {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: bold;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
|
||||
559
common/lib/xmodule/xmodule/css/videoalpha/display.scss
Normal file
559
common/lib/xmodule/xmodule/css/videoalpha/display.scss
Normal file
@@ -0,0 +1,559 @@
|
||||
& {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
div.video {
|
||||
@include clearfix();
|
||||
background: #f3f3f3;
|
||||
display: block;
|
||||
margin: 0 -12px;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
|
||||
article.video-wrapper {
|
||||
float: left;
|
||||
margin-right: flex-gutter(9);
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
section.video-player {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
position: relative;
|
||||
|
||||
object, iframe {
|
||||
border: none;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
@include clearfix();
|
||||
background: #333;
|
||||
border: 1px solid #000;
|
||||
border-top: 0;
|
||||
color: #ccc;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
div.slider {
|
||||
@include clearfix();
|
||||
background: #c2c2c2;
|
||||
border: 1px solid #000;
|
||||
@include border-radius(0);
|
||||
border-top: 1px solid #000;
|
||||
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
|
||||
height: 7px;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
@include transition(height 2.0s ease-in-out);
|
||||
|
||||
div.ui-widget-header {
|
||||
background: #777;
|
||||
@include box-shadow(inset 0 1px 0 #999);
|
||||
}
|
||||
|
||||
a.ui-slider-handle {
|
||||
background: $pink url(../images/slider-handle.png) center center no-repeat;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($pink, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
margin-left: -7px;
|
||||
top: -4px;
|
||||
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
|
||||
width: 15px;
|
||||
|
||||
&:focus, &:hover {
|
||||
background-color: lighten($pink, 10%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.vcr {
|
||||
@extend .dullify;
|
||||
float: left;
|
||||
list-style: none;
|
||||
margin: 0 lh() 0 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
|
||||
a {
|
||||
border-bottom: none;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 46px;
|
||||
padding: 0 lh(.75);
|
||||
text-indent: -9999px;
|
||||
@include transition(background-color, opacity);
|
||||
width: 14px;
|
||||
background: url('../images/vcr.png') 15px 15px no-repeat;
|
||||
outline: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
height: 46px;
|
||||
background: url('../images/vcr.png') 15px 15px no-repeat;
|
||||
}
|
||||
|
||||
&.play {
|
||||
background-position: 17px -114px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
&.pause {
|
||||
background-position: 16px -50px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.vidtime {
|
||||
padding-left: lh(.75);
|
||||
font-weight: bold;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
padding-left: lh(.75);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.secondary-controls {
|
||||
@extend .dullify;
|
||||
float: right;
|
||||
|
||||
div.speeds {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
&>a {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
}
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
&>a {
|
||||
background: url('../images/closed-arrow.png') 10px center no-repeat;
|
||||
border-left: 1px solid #000;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
@include clearfix();
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-right: 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 116px;
|
||||
outline: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #999;
|
||||
float: left;
|
||||
font-size: em(14);
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
padding: 0 lh(.25) 0 lh(.5);
|
||||
line-height: 46px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p.active {
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
padding: 0 lh(.5) 0 0;
|
||||
line-height: 46px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
// fix for now
|
||||
ol.video_speeds {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
|
||||
li {
|
||||
@include box-shadow( 0 1px 0 #555);
|
||||
border-bottom: 1px solid #000;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
display: block;
|
||||
padding: lh(.5);
|
||||
|
||||
&:hover {
|
||||
background-color: #666;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include box-shadow(none);
|
||||
border-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.volume {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.muted {
|
||||
&>a {
|
||||
background: url('../images/mute.png') 10px center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
background: url('../images/volume.png') 10px center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
@include clearfix();
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 46px;
|
||||
margin-right: 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 30px;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
margin-left: -1px;
|
||||
z-index: 10;
|
||||
|
||||
.volume-slider {
|
||||
height: 100px;
|
||||
border: 0;
|
||||
width: 5px;
|
||||
margin: 14px auto;
|
||||
background: #666;
|
||||
border: 1px solid #000;
|
||||
@include box-shadow(0 1px 0 #333);
|
||||
|
||||
a.ui-slider-handle {
|
||||
background: $pink url(../images/slider-handle.png) center center no-repeat;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($pink, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
left: -6px;
|
||||
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.ui-slider-range {
|
||||
background: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.add-fullscreen {
|
||||
background: url(../images/fullscreen.png) center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
padding: 0 lh(.5);
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.quality_control {
|
||||
background: url(../images/hd.png) center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
padding: 0 lh(.5);
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #F44;
|
||||
color: #0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
a.hide-subtitles {
|
||||
background: url('../images/cc.png') center no-repeat;
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
height: 14px;
|
||||
margin-top: -7px;
|
||||
|
||||
a.ui-slider-handle {
|
||||
@include border-radius(20px);
|
||||
height: 20px;
|
||||
margin-left: -10px;
|
||||
top: -4px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
padding-left: 0;
|
||||
float: left;
|
||||
max-height: 460px;
|
||||
overflow: auto;
|
||||
width: flex-grid(3, 9);
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
border: 0;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
line-height: lh();
|
||||
|
||||
&.current {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.closed {
|
||||
@extend .trans;
|
||||
|
||||
article.video-wrapper {
|
||||
width: flex-grid(9,9);
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
background: rgba(#000, .95);
|
||||
border: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
vertical-align: middle;
|
||||
|
||||
&.closed {
|
||||
ol.subtitles {
|
||||
right: -(flex-grid(4));
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
@include clearfix;
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
article.video-wrapper {
|
||||
width: 100%;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
float: none;
|
||||
}
|
||||
|
||||
object, iframe {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
background: rgba(#000, .8);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
max-width: flex-grid(3);
|
||||
padding: lh();
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
@include transition();
|
||||
|
||||
li {
|
||||
color: #aaa;
|
||||
|
||||
&.current {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
common/lib/xmodule/xmodule/foldit_module.py
Normal file
124
common/lib/xmodule/xmodule/foldit_module.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
from dateutil import parser
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class FolditModule(XModule):
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
# ooh look--I'm lazy, so hardcoding the 7.00x required level.
|
||||
# If we need it generalized, can pull from the xml later
|
||||
self.required_level = 4
|
||||
self.required_sublevel = 5
|
||||
|
||||
def parse_due_date():
|
||||
"""
|
||||
Pull out the date, or None
|
||||
"""
|
||||
s = self.metadata.get("due")
|
||||
if s:
|
||||
return parser.parse(s)
|
||||
else:
|
||||
return None
|
||||
|
||||
self.due_str = self.metadata.get("due", "None")
|
||||
self.due = parse_due_date()
|
||||
|
||||
def is_complete(self):
|
||||
"""
|
||||
Did the user get to the required level before the due date?
|
||||
"""
|
||||
# We normally don't want django dependencies in xmodule. foldit is
|
||||
# special. Import this late to avoid errors with things not yet being
|
||||
# initialized.
|
||||
from foldit.models import PuzzleComplete
|
||||
|
||||
complete = PuzzleComplete.is_level_complete(
|
||||
self.system.anonymous_student_id,
|
||||
self.required_level,
|
||||
self.required_sublevel,
|
||||
self.due)
|
||||
return complete
|
||||
|
||||
def completed_puzzles(self):
|
||||
"""
|
||||
Return a list of puzzles that this user has completed, as an array of
|
||||
dicts:
|
||||
|
||||
[ {'set': int,
|
||||
'subset': int,
|
||||
'created': datetime} ]
|
||||
|
||||
The list is sorted by set, then subset
|
||||
"""
|
||||
from foldit.models import PuzzleComplete
|
||||
|
||||
return sorted(
|
||||
PuzzleComplete.completed_puzzles(self.system.anonymous_student_id),
|
||||
key=lambda d: (d['set'], d['subset']))
|
||||
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Render the html for the module.
|
||||
"""
|
||||
goal_level = '{0}-{1}'.format(
|
||||
self.required_level,
|
||||
self.required_sublevel)
|
||||
|
||||
context = {
|
||||
'due': self.due_str,
|
||||
'success': self.is_complete(),
|
||||
'goal_level': goal_level,
|
||||
'completed': self.completed_puzzles(),
|
||||
}
|
||||
|
||||
return self.system.render_template('foldit.html', context)
|
||||
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
0 / 1 based on whether student has gotten far enough.
|
||||
"""
|
||||
score = 1 if self.is_complete() else 0
|
||||
return {'score': score,
|
||||
'total': self.max_score()}
|
||||
|
||||
def max_score(self):
|
||||
return 1
|
||||
|
||||
|
||||
class FolditDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding open ended response questions to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = FolditModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "foldit"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
# The grade changes without any student interaction with the edx website,
|
||||
# so always need to actually check.
|
||||
always_recalculate_grades = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
For now, don't need anything from the xml
|
||||
"""
|
||||
return {}
|
||||
@@ -3,7 +3,11 @@ from xmodule.raw_module import RawDescriptor
|
||||
|
||||
|
||||
class HiddenModule(XModule):
|
||||
pass
|
||||
def get_html(self):
|
||||
if self.system.user_is_staff:
|
||||
return "ERROR: This module is unknown--students will not see it at all"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class HiddenDescriptor(RawDescriptor):
|
||||
|
||||
@@ -172,6 +172,13 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
elt.set("filename", relname)
|
||||
return elt
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
|
||||
subset = [field for field in super(HtmlDescriptor,self).editable_metadata_fields
|
||||
if field not in ['empty']]
|
||||
return subset
|
||||
|
||||
|
||||
class AboutDescriptor(HtmlDescriptor):
|
||||
"""
|
||||
|
||||
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
*.js
|
||||
|
||||
# Please do not ignore *.js files. Some xmodules are written in JS.
|
||||
|
||||
@@ -4,7 +4,7 @@ class @Rubric
|
||||
# finds the scores for each rubric category
|
||||
@get_score_list: () =>
|
||||
# find the number of categories:
|
||||
num_categories = $('b.rubric-category').length
|
||||
num_categories = $('.rubric-category').length
|
||||
|
||||
score_lst = []
|
||||
# get the score for each one
|
||||
@@ -23,7 +23,7 @@ class @Rubric
|
||||
|
||||
@check_complete: () ->
|
||||
# check to see whether or not any categories have not been scored
|
||||
num_categories = $('b.rubric-category').length
|
||||
num_categories = $('.rubric-category').length
|
||||
for i in [0..(num_categories-1)]
|
||||
score = $("input[name='score-selection-#{i}']:checked").val()
|
||||
if score == undefined
|
||||
|
||||
@@ -107,12 +107,13 @@ class @HTMLEditingDescriptor
|
||||
# In order for isDirty() to return true ONLY if edits have been made after setting the text,
|
||||
# both the startContent must be sync'ed up and the dirty flag set to false.
|
||||
visualEditor.startContent = visualEditor.getContent({format: "raw", no_events: 1});
|
||||
visualEditor.isNotDirty = true
|
||||
@focusVisualEditor(visualEditor)
|
||||
@showingVisualEditor = true
|
||||
|
||||
focusVisualEditor: (visualEditor) =>
|
||||
visualEditor.focus()
|
||||
# Need to mark editor as not dirty both when it is initially created and when we switch back to it.
|
||||
visualEditor.isNotDirty = true
|
||||
if not @$mceToolbar?
|
||||
@$mceToolbar = $(@element).find('table.mceToolbar')
|
||||
|
||||
|
||||
103
common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee
Normal file
103
common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee
Normal file
@@ -0,0 +1,103 @@
|
||||
class @VideoAlpha
|
||||
constructor: (element) ->
|
||||
@el = $(element).find('.video')
|
||||
@id = @el.attr('id').replace(/video_/, '')
|
||||
@start = @el.data('start')
|
||||
@end = @el.data('end')
|
||||
@caption_data_dir = @el.data('caption-data-dir')
|
||||
@caption_asset_path = @el.data('caption-asset-path')
|
||||
@show_captions = @el.data('show-captions').toString() == "true"
|
||||
@el = $("#video_#{@id}")
|
||||
if @parseYoutubeId(@el.data("streams")) is true
|
||||
@videoType = "youtube"
|
||||
@fetchMetadata()
|
||||
@parseSpeed()
|
||||
else
|
||||
@videoType = "html5"
|
||||
@parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source')
|
||||
@speeds = ['0.75', '1.0', '1.25', '1.50']
|
||||
sub = @el.data('sub')
|
||||
if (typeof sub isnt "string") or (sub.length is 0)
|
||||
sub = ""
|
||||
@show_captions = false
|
||||
@videos =
|
||||
"0.75": sub
|
||||
"1.0": sub
|
||||
"1.25": sub
|
||||
"1.5": sub
|
||||
@setSpeed $.cookie('video_speed')
|
||||
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
|
||||
if @show_captions is true
|
||||
@hide_captions = $.cookie('hide_captions') == 'true'
|
||||
else
|
||||
@hide_captions = true
|
||||
$.cookie('hide_captions', @hide_captions, expires: 3650, path: '/')
|
||||
@el.addClass 'closed'
|
||||
if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player))
|
||||
@embed()
|
||||
else
|
||||
if @videoType is "youtube"
|
||||
window.onYouTubePlayerAPIReady = =>
|
||||
@embed()
|
||||
else if @videoType is "html5"
|
||||
window.onHTML5PlayerAPIReady = =>
|
||||
@embed()
|
||||
|
||||
youtubeId: (speed)->
|
||||
@videos[speed || @speed]
|
||||
|
||||
parseYoutubeId: (videos)->
|
||||
return false if (typeof videos isnt "string") or (videos.length is 0)
|
||||
@videos = {}
|
||||
$.each videos.split(/,/), (index, video) =>
|
||||
speed = undefined
|
||||
video = video.split(/:/)
|
||||
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0")
|
||||
@videos[speed] = video[1]
|
||||
true
|
||||
|
||||
parseHtml5Sources: (mp4Source, webmSource, oggSource)->
|
||||
@html5Sources =
|
||||
mp4: null
|
||||
webm: null
|
||||
ogg: null
|
||||
@html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0)
|
||||
@html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0)
|
||||
@html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0)
|
||||
|
||||
parseSpeed: ->
|
||||
@speeds = ($.map @videos, (url, speed) -> speed).sort()
|
||||
@setSpeed $.cookie('video_speed')
|
||||
|
||||
setSpeed: (newSpeed, updateCookie)->
|
||||
if @speeds.indexOf(newSpeed) isnt -1
|
||||
@speed = newSpeed
|
||||
|
||||
if updateCookie isnt false
|
||||
$.cookie "video_speed", "" + newSpeed,
|
||||
expires: 3650
|
||||
path: "/"
|
||||
else
|
||||
@speed = "1.0"
|
||||
|
||||
embed: ->
|
||||
@player = new VideoPlayerAlpha video: this
|
||||
|
||||
fetchMetadata: (url) ->
|
||||
@metadata = {}
|
||||
$.each @videos, (speed, url) =>
|
||||
$.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
|
||||
|
||||
getDuration: ->
|
||||
@metadata[@youtubeId()].duration
|
||||
|
||||
log: (eventName)->
|
||||
logInfo =
|
||||
id: @id
|
||||
code: @youtubeId()
|
||||
currentTime: @player.currentTime
|
||||
speed: @speed
|
||||
if @videoType is "youtube"
|
||||
logInfo.code = @youtubeId()
|
||||
else logInfo.code = "html5" if @videoType is "html5"
|
||||
Logger.log eventName, logInfo
|
||||
@@ -0,0 +1,14 @@
|
||||
class @SubviewAlpha
|
||||
constructor: (options) ->
|
||||
$.each options, (key, value) =>
|
||||
@[key] = value
|
||||
@initialize()
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
initialize: ->
|
||||
render: ->
|
||||
bind: ->
|
||||
@@ -0,0 +1,294 @@
|
||||
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 () {
|
||||
// Because firefox triggers 'canplay' event every time when 'currentTime' property
|
||||
// changes, we must make sure that this block of code runs only once. Otherwise,
|
||||
// this will be an endless loop ('currentTime' property is changed below).
|
||||
//
|
||||
// Chrome is immune to this behavior.
|
||||
if (_this.playerState !== HTML5Video.PlayerState.UNSTARTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
|
||||
if (_this.start > _this.video.duration) {
|
||||
_this.start = 0;
|
||||
}
|
||||
if ((_this.end === null) || (_this.end > _this.video.duration)) {
|
||||
_this.end = _this.video.duration;
|
||||
}
|
||||
_this.video.currentTime = _this.start;
|
||||
|
||||
if ($.isFunction(_this.config.events.onReady) === true) {
|
||||
_this.config.events.onReady(null);
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Register the 'play' event.
|
||||
this.video.addEventListener('play', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'pause' event.
|
||||
this.video.addEventListener('pause', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'ended' event.
|
||||
this.video.addEventListener('ended', function () {
|
||||
_this.playerState = HTML5Video.PlayerState.ENDED;
|
||||
_this.callStateChangeCallback();
|
||||
}, false);
|
||||
|
||||
// Register the 'timeupdate' event. This is the place where we control when the video ends.
|
||||
// If an ending time was specified, then after the video plays through to this spot, pauses, we
|
||||
// must make sure to update the ending time to the end of the video. This way, the user can watch
|
||||
// any parts of it afterwards.
|
||||
this.video.addEventListener('timeupdate', function (data) {
|
||||
if (_this.end < _this.video.currentTime) {
|
||||
// When we call video.pause(), a 'pause' event will be formed, and we will catch it
|
||||
// in another handler (see above).
|
||||
_this.video.pause();
|
||||
_this.end = _this.video.duration;
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Place the <video> element on the page.
|
||||
this.videoEl.appendTo(this.el.find('.video-player div'));
|
||||
}
|
||||
}());
|
||||
|
||||
HTML5Video.PlayerState = {
|
||||
'UNSTARTED': -1,
|
||||
'ENDED': 0,
|
||||
'PLAYING': 1,
|
||||
'PAUSED': 2,
|
||||
'BUFFERING': 3,
|
||||
'CUED': 5
|
||||
};
|
||||
|
||||
return HTML5Video;
|
||||
}());
|
||||
@@ -0,0 +1,152 @@
|
||||
class @VideoCaptionAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@loaded = false
|
||||
|
||||
bind: ->
|
||||
$(window).bind('resize', @resize)
|
||||
@$('.hide-subtitles').click @toggle
|
||||
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
|
||||
.mousemove(@onMovement).bind('mousewheel', @onMovement)
|
||||
.bind('DOMMouseScroll', @onMovement)
|
||||
|
||||
captionURL: ->
|
||||
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
|
||||
|
||||
render: ->
|
||||
# TODO: make it so you can have a video with no captions.
|
||||
#@$('.video-wrapper').after """
|
||||
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
|
||||
# """
|
||||
@$('.video-wrapper').after """
|
||||
<ol class="subtitles"></ol>
|
||||
"""
|
||||
@$('.video-controls .secondary-controls').append """
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
|
||||
"""#"
|
||||
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
|
||||
@fetchCaption()
|
||||
|
||||
fetchCaption: ->
|
||||
$.getWithPrefix @captionURL(), (captions) =>
|
||||
@captions = captions.text
|
||||
@start = captions.start
|
||||
|
||||
@loaded = true
|
||||
|
||||
if onTouchBasedDevice()
|
||||
$('.subtitles li').html "Caption will be displayed when you start playing the video."
|
||||
else
|
||||
@renderCaption()
|
||||
|
||||
renderCaption: ->
|
||||
container = $('<ol>')
|
||||
|
||||
$.each @captions, (index, text) =>
|
||||
container.append $('<li>').html(text).attr
|
||||
'data-index': index
|
||||
'data-start': @start[index]
|
||||
|
||||
@$('.subtitles').html(container.html())
|
||||
@$('.subtitles li[data-index]').click @seekPlayer
|
||||
|
||||
# prepend and append an empty <li> for cosmetic reason
|
||||
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
|
||||
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
|
||||
|
||||
@rendered = true
|
||||
|
||||
search: (time) ->
|
||||
if @loaded
|
||||
min = 0
|
||||
max = @start.length - 1
|
||||
|
||||
while min < max
|
||||
index = Math.ceil((max + min) / 2)
|
||||
if time < @start[index]
|
||||
max = index - 1
|
||||
if time >= @start[index]
|
||||
min = index
|
||||
return min
|
||||
|
||||
play: ->
|
||||
if @loaded
|
||||
@renderCaption() unless @rendered
|
||||
@playing = true
|
||||
|
||||
pause: ->
|
||||
if @loaded
|
||||
@playing = false
|
||||
|
||||
updatePlayTime: (time) ->
|
||||
if @loaded
|
||||
# This 250ms offset is required to match the video speed
|
||||
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
|
||||
newIndex = @search time
|
||||
|
||||
if newIndex != undefined && @currentIndex != newIndex
|
||||
if @currentIndex
|
||||
@$(".subtitles li.current").removeClass('current')
|
||||
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
|
||||
|
||||
@currentIndex = newIndex
|
||||
@scrollCaption()
|
||||
|
||||
resize: =>
|
||||
@$('.subtitles').css maxHeight: @captionHeight()
|
||||
@$('.subtitles .spacing:first').height(@topSpacingHeight())
|
||||
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
|
||||
@scrollCaption()
|
||||
|
||||
onMouseEnter: =>
|
||||
clearTimeout @frozen if @frozen
|
||||
@frozen = setTimeout @onMouseLeave, 10000
|
||||
|
||||
onMovement: =>
|
||||
@onMouseEnter()
|
||||
|
||||
onMouseLeave: =>
|
||||
clearTimeout @frozen if @frozen
|
||||
@frozen = null
|
||||
@scrollCaption() if @playing
|
||||
|
||||
scrollCaption: ->
|
||||
if !@frozen && @$('.subtitles .current:first').length
|
||||
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
|
||||
offset: - @calculateOffset(@$('.subtitles .current:first'))
|
||||
|
||||
seekPlayer: (event) =>
|
||||
event.preventDefault()
|
||||
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
|
||||
$(@).trigger('seek', time)
|
||||
|
||||
calculateOffset: (element) ->
|
||||
@captionHeight() / 2 - element.height() / 2
|
||||
|
||||
topSpacingHeight: ->
|
||||
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
|
||||
|
||||
bottomSpacingHeight: ->
|
||||
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
|
||||
|
||||
toggle: (event) =>
|
||||
event.preventDefault()
|
||||
if @el.hasClass('closed') # Captions are "closed" e.g. turned off
|
||||
@hideCaptions(false)
|
||||
else # Captions are on
|
||||
@hideCaptions(true)
|
||||
|
||||
hideCaptions: (hide_captions) =>
|
||||
if hide_captions
|
||||
@$('.hide-subtitles').attr('title', 'Turn on captions')
|
||||
@el.addClass('closed')
|
||||
else
|
||||
@$('.hide-subtitles').attr('title', 'Turn off captions')
|
||||
@el.removeClass('closed')
|
||||
@scrollCaption()
|
||||
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
|
||||
|
||||
captionHeight: ->
|
||||
if @el.hasClass('fullscreen')
|
||||
$(window).height() - @$('.video-controls').height()
|
||||
else
|
||||
@$('.video-wrapper').height()
|
||||
@@ -0,0 +1,35 @@
|
||||
class @VideoControlAlpha extends SubviewAlpha
|
||||
bind: ->
|
||||
@$('.video_control').click @togglePlayback
|
||||
|
||||
render: ->
|
||||
@el.append """
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control" href="#"></a></li>
|
||||
<li>
|
||||
<div class="vidtime">0:00 / 0:00</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
</div>
|
||||
</div>
|
||||
"""#"
|
||||
|
||||
unless onTouchBasedDevice()
|
||||
@$('.video_control').addClass('play').html('Play')
|
||||
|
||||
play: ->
|
||||
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
|
||||
|
||||
pause: ->
|
||||
@$('.video_control').removeClass('pause').addClass('play').html('Play')
|
||||
|
||||
togglePlayback: (event) =>
|
||||
event.preventDefault()
|
||||
if @$('.video_control').hasClass('play')
|
||||
$(@).trigger('play')
|
||||
else if @$('.video_control').hasClass('pause')
|
||||
$(@).trigger('pause')
|
||||
@@ -0,0 +1,283 @@
|
||||
class @VideoPlayerAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
# If we switch verticals while the video is playing, then HTML content is
|
||||
# removed, but JS code is still executing (setInterval() method), and there will
|
||||
# arise conflicts (no HTML content, but code tries to access it). Therefore
|
||||
# we must pause the player (stop setInterval() method).
|
||||
if (window.OldVideoPlayerAlpha) and (window.OldVideoPlayerAlpha.onPause)
|
||||
window.OldVideoPlayerAlpha.onPause()
|
||||
window.OldVideoPlayerAlpha = this
|
||||
|
||||
if @video.videoType is 'youtube'
|
||||
@PlayerState = YT.PlayerState
|
||||
# Define a missing constant of Youtube API
|
||||
@PlayerState.UNSTARTED = -1
|
||||
else if @video.videoType is 'html5'
|
||||
@PlayerState = HTML5Video.PlayerState
|
||||
|
||||
@currentTime = 0
|
||||
@el = $("#video_#{@video.id}")
|
||||
|
||||
bind: ->
|
||||
$(@control).bind('play', @play)
|
||||
.bind('pause', @pause)
|
||||
if @video.videoType is 'youtube'
|
||||
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
|
||||
if @video.show_captions is true
|
||||
$(@caption).bind('seek', @onSeek)
|
||||
$(@speedControl).bind('speedChange', @onSpeedChange)
|
||||
$(@progressSlider).bind('seek', @onSeek)
|
||||
if @volumeControl
|
||||
$(@volumeControl).bind('volumeChange', @onVolumeChange)
|
||||
$(document).keyup @bindExitFullScreen
|
||||
|
||||
@$('.add-fullscreen').click @toggleFullScreen
|
||||
@addToolTip() unless onTouchBasedDevice()
|
||||
|
||||
bindExitFullScreen: (event) =>
|
||||
if @el.hasClass('fullscreen') && event.keyCode == 27
|
||||
@toggleFullScreen(event)
|
||||
|
||||
render: ->
|
||||
@control = new VideoControlAlpha el: @$('.video-controls')
|
||||
if @video.videoType is 'youtube'
|
||||
@qualityControl = new VideoQualityControlAlpha el: @$('.secondary-controls')
|
||||
if @video.show_captions is true
|
||||
@caption = new VideoCaptionAlpha
|
||||
el: @el
|
||||
youtubeId: @video.youtubeId('1.0')
|
||||
currentSpeed: @currentSpeed()
|
||||
captionAssetPath: @video.caption_asset_path
|
||||
unless onTouchBasedDevice()
|
||||
@volumeControl = new VideoVolumeControlAlpha el: @$('.secondary-controls')
|
||||
@speedControl = new VideoSpeedControlAlpha el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
|
||||
@progressSlider = new VideoProgressSliderAlpha el: @$('.slider')
|
||||
@playerVars =
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
modestbranding: 1
|
||||
html5: 1
|
||||
if @video.start
|
||||
@playerVars.start = @video.start
|
||||
@playerVars.wmode = 'window'
|
||||
if @video.end
|
||||
# work in AS3, not HMLT5. but iframe use AS3
|
||||
@playerVars.end = @video.end
|
||||
if @video.videoType is 'html5'
|
||||
@player = new HTML5Video.Player @video.el,
|
||||
playerVars: @playerVars,
|
||||
videoSources: @video.html5Sources,
|
||||
events:
|
||||
onReady: @onReady
|
||||
onStateChange: @onStateChange
|
||||
else if @video.videoType is 'youtube'
|
||||
prev_player_type = $.cookie('prev_player_type')
|
||||
if prev_player_type == 'html5'
|
||||
youTubeId = @video.videos['1.0']
|
||||
else
|
||||
youTubeId = @video.youtubeId()
|
||||
@player = new YT.Player @video.id,
|
||||
playerVars: @playerVars
|
||||
videoId: youTubeId
|
||||
events:
|
||||
onReady: @onReady
|
||||
onStateChange: @onStateChange
|
||||
onPlaybackQualityChange: @onPlaybackQualityChange
|
||||
if @video.show_captions is true
|
||||
@caption.hideCaptions(@['video'].hide_captions)
|
||||
|
||||
addToolTip: ->
|
||||
@$('.add-fullscreen, .hide-subtitles').qtip
|
||||
position:
|
||||
my: 'top right'
|
||||
at: 'top center'
|
||||
|
||||
onReady: (event) =>
|
||||
if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate @video.speed
|
||||
unless onTouchBasedDevice()
|
||||
$('.video-load-complete:first').data('video').player.play()
|
||||
|
||||
onStateChange: (event) =>
|
||||
_this = this
|
||||
switch event.data
|
||||
when @PlayerState.UNSTARTED
|
||||
# Before the video starts playing, let us see if we are in YouTube player,
|
||||
# and if YouTube is in HTML5 mode. If both cases are true, then we can make
|
||||
# it so that speed switching happens natively.
|
||||
|
||||
if @video.videoType is "youtube"
|
||||
# Because YouTube API does not have a direct method to determine the mode we
|
||||
# are in (Flash or HTML5), we rely on an indirect method. Currently, when in
|
||||
# Flash mode, YouTube player reports that there is only one (1.0) speed
|
||||
# available. When in HTML5 mode, it reports multiple speeds available. We
|
||||
# will use this fact.
|
||||
#
|
||||
# NOTE: It is my strong belief that in the future YouTube Flash player will
|
||||
# not get speed changes. This is a dying technology. So we can safely use
|
||||
# this indirect method to determine player mode.
|
||||
availableSpeeds = @player.getAvailablePlaybackRates()
|
||||
prev_player_type = $.cookie('prev_player_type')
|
||||
if availableSpeeds.length > 1
|
||||
# If the user last accessed the page and watched a movie via YouTube
|
||||
# player, and it was using Flash mode, then we must reset the current
|
||||
# YouTube speed to 1.0 (by loading appropriate video that is encoded at
|
||||
# 1.0 speed).
|
||||
if prev_player_type == 'youtube'
|
||||
$.cookie('prev_player_type', 'html5', expires: 3650, path: '/')
|
||||
@onSpeedChange null, '1.0', false
|
||||
else if prev_player_type != 'html5'
|
||||
$.cookie('prev_player_type', 'html5', expires: 3650, path: '/')
|
||||
|
||||
# Now we must update all the speeds to the ones available via the YouTube
|
||||
# HTML5 API. The default speeds are not exactly the same as reported by
|
||||
# YouTube, so we will remove the default speeds, and populate all the
|
||||
# necessary data with correct available speeds.
|
||||
baseSpeedSubs = @video.videos["1.0"]
|
||||
$.each @video.videos, (index, value) ->
|
||||
delete _this.video.videos[index]
|
||||
@video.speeds = []
|
||||
$.each availableSpeeds, (index, value) ->
|
||||
_this.video.videos[value.toFixed(2).replace(/\.00$/, ".0")] = baseSpeedSubs
|
||||
_this.video.speeds.push value.toFixed(2).replace(/\.00$/, ".0")
|
||||
|
||||
# We must update the Speed Control to reflect the new avialble speeds.
|
||||
@speedControl.reRender @video.speeds, @video.speed
|
||||
|
||||
# Now we set the videoType to 'HTML5'. This works because my HTML5Video
|
||||
# class is fully compatible with YouTube HTML5 API.
|
||||
@video.videoType = 'html5'
|
||||
|
||||
@video.setSpeed $.cookie('video_speed')
|
||||
|
||||
# Change the speed to the required one.
|
||||
@player.setPlaybackRate @video.speed
|
||||
else
|
||||
# We are in YouTube player, and in Flash mode. Check previos mode.
|
||||
if prev_player_type != 'youtube'
|
||||
$.cookie('prev_player_type', 'youtube', expires: 3650, path: '/')
|
||||
|
||||
# We need to set the proper speed when previous mode was not 'youtube'.
|
||||
@onSpeedChange null, $.cookie('video_speed')
|
||||
|
||||
@onUnstarted()
|
||||
when @PlayerState.PLAYING
|
||||
@onPlay()
|
||||
when @PlayerState.PAUSED
|
||||
@onPause()
|
||||
when @PlayerState.ENDED
|
||||
@onEnded()
|
||||
|
||||
onPlaybackQualityChange: (event, value) =>
|
||||
quality = @player.getPlaybackQuality()
|
||||
@qualityControl.onQualityChange(quality)
|
||||
|
||||
handlePlaybackQualityChange: (event, value) =>
|
||||
@player.setPlaybackQuality(value)
|
||||
|
||||
onUnstarted: =>
|
||||
@control.pause()
|
||||
if @video.show_captions is true
|
||||
@caption.pause()
|
||||
|
||||
onPlay: =>
|
||||
@video.log 'play_video'
|
||||
unless @player.interval
|
||||
@player.interval = setInterval(@update, 200)
|
||||
if @video.show_captions is true
|
||||
@caption.play()
|
||||
@control.play()
|
||||
@progressSlider.play()
|
||||
|
||||
onPause: =>
|
||||
@video.log 'pause_video'
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = null
|
||||
if @video.show_captions is true
|
||||
@caption.pause()
|
||||
@control.pause()
|
||||
|
||||
onEnded: =>
|
||||
@control.pause()
|
||||
if @video.show_captions is true
|
||||
@caption.pause()
|
||||
|
||||
onSeek: (event, time) =>
|
||||
@player.seekTo(time, true)
|
||||
if @isPlaying()
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = setInterval(@update, 200)
|
||||
else
|
||||
@currentTime = time
|
||||
@updatePlayTime time
|
||||
|
||||
onSpeedChange: (event, newSpeed, updateCookie) =>
|
||||
if @video.videoType is 'youtube'
|
||||
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
|
||||
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
|
||||
@video.setSpeed newSpeed, updateCookie
|
||||
if @video.videoType is 'youtube'
|
||||
if @video.show_captions is true
|
||||
@caption.currentSpeed = newSpeed
|
||||
if @video.videoType is 'html5'
|
||||
@player.setPlaybackRate newSpeed
|
||||
else if @video.videoType is 'youtube'
|
||||
if @isPlaying()
|
||||
@player.loadVideoById(@video.youtubeId(), @currentTime)
|
||||
else
|
||||
@player.cueVideoById(@video.youtubeId(), @currentTime)
|
||||
if @video.videoType is 'youtube'
|
||||
@updatePlayTime @currentTime
|
||||
|
||||
onVolumeChange: (event, volume) =>
|
||||
@player.setVolume volume
|
||||
|
||||
update: =>
|
||||
if @currentTime = @player.getCurrentTime()
|
||||
@updatePlayTime @currentTime
|
||||
|
||||
updatePlayTime: (time) ->
|
||||
progress = Time.format(time) + ' / ' + Time.format(@duration())
|
||||
@$(".vidtime").html(progress)
|
||||
if @video.show_captions is true
|
||||
@caption.updatePlayTime(time)
|
||||
@progressSlider.updatePlayTime(time, @duration())
|
||||
|
||||
toggleFullScreen: (event) =>
|
||||
event.preventDefault()
|
||||
if @el.hasClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Fill browser')
|
||||
@el.removeClass('fullscreen')
|
||||
else
|
||||
@el.addClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Exit fill browser')
|
||||
if @video.show_captions is true
|
||||
@caption.resize()
|
||||
|
||||
# Delegates
|
||||
play: =>
|
||||
@player.playVideo() if @player.playVideo
|
||||
|
||||
isPlaying: ->
|
||||
@player.getPlayerState() == @PlayerState.PLAYING
|
||||
|
||||
pause: =>
|
||||
@player.pauseVideo() if @player.pauseVideo
|
||||
|
||||
duration: ->
|
||||
duration = @player.getDuration()
|
||||
if isFinite(duration) is false
|
||||
duration = @video.getDuration()
|
||||
duration
|
||||
|
||||
currentSpeed: ->
|
||||
@video.speed
|
||||
|
||||
volume: (value) ->
|
||||
if value?
|
||||
@player.setVolume value
|
||||
else
|
||||
@player.getVolume()
|
||||
@@ -0,0 +1,49 @@
|
||||
class @VideoProgressSliderAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@buildSlider() unless onTouchBasedDevice()
|
||||
|
||||
buildSlider: ->
|
||||
@slider = @el.slider
|
||||
range: 'min'
|
||||
change: @onChange
|
||||
slide: @onSlide
|
||||
stop: @onStop
|
||||
@buildHandle()
|
||||
|
||||
buildHandle: ->
|
||||
@handle = @$('.slider .ui-slider-handle')
|
||||
@handle.qtip
|
||||
content: "#{Time.format(@slider.slider('value'))}"
|
||||
position:
|
||||
my: 'bottom center'
|
||||
at: 'top center'
|
||||
container: @handle
|
||||
hide:
|
||||
delay: 700
|
||||
style:
|
||||
classes: 'ui-tooltip-slider'
|
||||
widget: true
|
||||
|
||||
play: =>
|
||||
@buildSlider() unless @slider
|
||||
|
||||
updatePlayTime: (currentTime, duration) ->
|
||||
if @slider && !@frozen
|
||||
@slider.slider('option', 'max', duration)
|
||||
@slider.slider('value', currentTime)
|
||||
|
||||
onSlide: (event, ui) =>
|
||||
@frozen = true
|
||||
@updateTooltip(ui.value)
|
||||
$(@).trigger('seek', ui.value)
|
||||
|
||||
onChange: (event, ui) =>
|
||||
@updateTooltip(ui.value)
|
||||
|
||||
onStop: (event, ui) =>
|
||||
@frozen = true
|
||||
$(@).trigger('seek', ui.value)
|
||||
setTimeout (=> @frozen = false), 200
|
||||
|
||||
updateTooltip: (value)->
|
||||
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
|
||||
@@ -0,0 +1,26 @@
|
||||
class @VideoQualityControlAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@quality = null;
|
||||
|
||||
bind: ->
|
||||
@$('.quality_control').click @toggleQuality
|
||||
|
||||
render: ->
|
||||
@el.append """
|
||||
<a href="#" class="quality_control" title="HD">HD</a>
|
||||
"""#"
|
||||
|
||||
onQualityChange: (value) ->
|
||||
@quality = value
|
||||
if @quality in ['hd720', 'hd1080', 'highres']
|
||||
@el.addClass('active')
|
||||
else
|
||||
@el.removeClass('active')
|
||||
|
||||
toggleQuality: (event) =>
|
||||
event.preventDefault()
|
||||
if @quality in ['hd720', 'hd1080', 'highres']
|
||||
newQuality = 'large'
|
||||
else
|
||||
newQuality = 'hd720'
|
||||
$(@).trigger('changeQuality', newQuality)
|
||||
@@ -0,0 +1,53 @@
|
||||
class @VideoSpeedControlAlpha extends SubviewAlpha
|
||||
bind: ->
|
||||
@$('.video_speeds a').click @changeVideoSpeed
|
||||
if onTouchBasedDevice()
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).toggleClass('open')
|
||||
else
|
||||
@$('.speeds').mouseenter ->
|
||||
$(this).addClass('open')
|
||||
@$('.speeds').mouseleave ->
|
||||
$(this).removeClass('open')
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).removeClass('open')
|
||||
|
||||
render: ->
|
||||
@el.prepend """
|
||||
<div class="speeds">
|
||||
<a href="#">
|
||||
<h3>Speed</h3>
|
||||
<p class="active"></p>
|
||||
</a>
|
||||
<ol class="video_speeds"></ol>
|
||||
</div>
|
||||
"""
|
||||
$.each @speeds, (index, speed) =>
|
||||
link = $('<a>').attr(href: "#").html("#{speed}x")
|
||||
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
|
||||
@setSpeed @currentSpeed
|
||||
|
||||
reRender: (newSpeeds, currentSpeed) ->
|
||||
@$('.video_speeds').empty()
|
||||
@$('.video_speeds li').removeClass('active')
|
||||
@speeds = newSpeeds
|
||||
$.each @speeds, (index, speed) =>
|
||||
link = $('<a>').attr(href: "#").html("#{speed}x")
|
||||
listItem = $('<li>').attr('data-speed', speed).html(link);
|
||||
listItem.addClass('active') if speed is currentSpeed
|
||||
@$('.video_speeds').prepend listItem
|
||||
@$('.video_speeds a').click @changeVideoSpeed
|
||||
|
||||
changeVideoSpeed: (event) =>
|
||||
event.preventDefault()
|
||||
unless $(event.target).parent().hasClass('active')
|
||||
@currentSpeed = $(event.target).parent().data('speed')
|
||||
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
|
||||
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
|
||||
|
||||
setSpeed: (speed) ->
|
||||
@$('.video_speeds li').removeClass('active')
|
||||
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
|
||||
@$('.speeds p.active').html("#{speed}x")
|
||||
@@ -0,0 +1,40 @@
|
||||
class @VideoVolumeControlAlpha extends SubviewAlpha
|
||||
initialize: ->
|
||||
@currentVolume = 100
|
||||
|
||||
bind: ->
|
||||
@$('.volume').mouseenter ->
|
||||
$(this).addClass('open')
|
||||
@$('.volume').mouseleave ->
|
||||
$(this).removeClass('open')
|
||||
@$('.volume>a').click(@toggleMute)
|
||||
|
||||
render: ->
|
||||
@el.prepend """
|
||||
<div class="volume">
|
||||
<a href="#"></a>
|
||||
<div class="volume-slider-container">
|
||||
<div class="volume-slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""#"
|
||||
@slider = @$('.volume-slider').slider
|
||||
orientation: "vertical"
|
||||
range: "min"
|
||||
min: 0
|
||||
max: 100
|
||||
value: 100
|
||||
change: @onChange
|
||||
slide: @onChange
|
||||
|
||||
onChange: (event, ui) =>
|
||||
@currentVolume = ui.value
|
||||
$(@).trigger 'volumeChange', @currentVolume
|
||||
@$('.volume').toggleClass 'muted', @currentVolume == 0
|
||||
|
||||
toggleMute: =>
|
||||
if @currentVolume > 0
|
||||
@previousVolume = @currentVolume
|
||||
@slider.slider 'option', 'value', 0
|
||||
else
|
||||
@slider.slider 'option', 'value', @previousVolume
|
||||
@@ -73,7 +73,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
# VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check)
|
||||
|
||||
# tags that really need unique names--they store (or should store) state.
|
||||
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence')
|
||||
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'timelimit')
|
||||
|
||||
attr = xml_data.attrib
|
||||
tag = xml_data.tag
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from fs.osfs import OSFS
|
||||
from json import dumps
|
||||
|
||||
|
||||
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
|
||||
@@ -27,6 +28,19 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
# export the course updates
|
||||
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -537,9 +537,6 @@ class CombinedOpenEndedV1Module():
|
||||
rubric_scores = [all_responses[i]['rubric_scores'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['rubric_scores'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()]
|
||||
grader_types = [all_responses[i]['grader_types'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['grader_types'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()]
|
||||
feedback_items = [all_responses[i]['feedback_items'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['feedback_items'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()]
|
||||
log.debug(rubric_scores)
|
||||
log.debug(grader_types)
|
||||
log.debug(feedback_items)
|
||||
rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), rubric_scores,
|
||||
grader_types, feedback_items)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class CombinedOpenEndedRubric(object):
|
||||
success = False
|
||||
try:
|
||||
rubric_categories = self.extract_categories(rubric_xml)
|
||||
if score_list:
|
||||
if score_list and len(score_list)==len(rubric_categories):
|
||||
for i in xrange(0,len(rubric_categories)):
|
||||
category = rubric_categories[i]
|
||||
for j in xrange(0,len(category['options'])):
|
||||
@@ -231,6 +231,14 @@ class CombinedOpenEndedRubric(object):
|
||||
|
||||
@staticmethod
|
||||
def reformat_scores_for_rendering(scores, score_types, feedback_types):
|
||||
"""
|
||||
Takes in a list of rubric scores, the types of those scores, and feedback associated with them
|
||||
Outputs a reformatted list of score tuples (count, rubric category, rubric score, [graders that gave this score], [feedback types])
|
||||
@param scores:
|
||||
@param score_types:
|
||||
@param feedback_types:
|
||||
@return:
|
||||
"""
|
||||
success = False
|
||||
if len(scores)==0:
|
||||
log.error("Score length is 0.")
|
||||
@@ -271,6 +279,13 @@ class CombinedOpenEndedRubric(object):
|
||||
|
||||
@staticmethod
|
||||
def check_for_tuple_matches(tuples, tuple):
|
||||
"""
|
||||
Checks to see if a tuple in a list of tuples is a match for tuple.
|
||||
If not match, creates a new tuple matching tuple.
|
||||
@param tuples: list of tuples
|
||||
@param tuple: tuples to match
|
||||
@return: a new list of tuples, and the index of the tuple that matches tuple
|
||||
"""
|
||||
category = tuple[1]
|
||||
score = tuple[2]
|
||||
tup_ind = -1
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Empty
|
||||
display_name: Blank HTML Page
|
||||
empty: True
|
||||
|
||||
data: |
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
---
|
||||
metadata:
|
||||
display_name: Circuit Schematic
|
||||
display_name: Circuit Schematic Builder
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Custom Grader
|
||||
display_name: Custom Python-Evaluated Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
@@ -8,7 +8,7 @@ metadata:
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
A custom response problem accepts one or more lines of text input from the
|
||||
A custom python-evaluated input problem accepts one or more lines of text input from the
|
||||
student, and evaluates the inputs for correctness based on evaluation using a
|
||||
python script embedded within the problem.
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Empty
|
||||
display_name: Blank Common Problem
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
markdown: ""
|
||||
weight: ""
|
||||
empty: True
|
||||
attempts: ""
|
||||
data: |
|
||||
<problem>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Blank Advanced Problem
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
empty: True
|
||||
data: |
|
||||
<problem>
|
||||
</problem>
|
||||
|
||||
children: []
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Formula Response
|
||||
display_name: Math Expression Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
@@ -8,7 +8,7 @@ metadata:
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
A formula response problem accepts a line of text representing a mathematical expression from the
|
||||
A math expression input problem accepts a line of text representing a mathematical expression from the
|
||||
student, and evaluates the input for equivalence to a mathematical expression provided by the
|
||||
grader. Correctness is based on numerical sampling of the symbolic expressions.
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Image Response
|
||||
display_name: Image Mapped Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
@@ -8,7 +8,7 @@ metadata:
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
An image response problem presents an image for the student. Input is
|
||||
An image mapped input problem presents an image for the student. Input is
|
||||
given by the location of mouse clicks on the image. Correctness of input can be evaluated based on expected dimensions of a rectangle.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -26,10 +26,6 @@ metadata:
|
||||
|
||||
( ) The vegetable peeler
|
||||
|
||||
( ) Android
|
||||
|
||||
( ) The Beatles
|
||||
|
||||
|
||||
[explanation]
|
||||
The release of the iPod allowed consumers to carry their entire music library with them in a
|
||||
@@ -51,8 +47,6 @@ data: |
|
||||
<choice correct="false" name="beatles">Napster</choice>
|
||||
<choice correct="true" name="ipod">The iPod</choice>
|
||||
<choice correct="false" name="peeler">The vegetable peeler</choice>
|
||||
<choice correct="false" name="android">Android</choice>
|
||||
<choice correct="false" name="beatles">The Beatles</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<solution>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Numerical Response
|
||||
display_name: Numerical Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
markdown:
|
||||
"A numerical response problem accepts a line of text input from the
|
||||
"A numerical input problem accepts a line of text input from the
|
||||
student, and evaluates the input for correctness based on its
|
||||
numerical value.
|
||||
|
||||
@@ -45,7 +45,7 @@ metadata:
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
A numerical response problem accepts a line of text input from the
|
||||
A numerical input problem accepts a line of text input from the
|
||||
student, and evaluates the input for correctness based on its
|
||||
numerical value.
|
||||
</p>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Option Response
|
||||
display_name: Dropdown
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
markdown:
|
||||
"OptionResponse gives a limited set of options for students to respond with, and presents those options
|
||||
"Dropdown problems give a limited set of options for students to respond with, and present those options
|
||||
in a format that encourages them to search for a specific answer rather than being immediately presented
|
||||
with options from which to recognize the correct answer.
|
||||
|
||||
@@ -14,30 +14,30 @@ metadata:
|
||||
The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.
|
||||
|
||||
|
||||
Translation between Option Response and __________ is extremely straightforward:
|
||||
Translation between Dropdown and __________ is extremely straightforward:
|
||||
|
||||
[[(Multiple Choice), String Response, Numerical Response, External Response, Image Response]]
|
||||
[[(Multiple Choice), Text Input, Numerical Input, External Response, Image Response]]
|
||||
|
||||
|
||||
[explanation]
|
||||
Multiple Choice also allows students to select from a variety of pre-written responses, although the
|
||||
format makes it easier for students to read very long response options. Optionresponse also differs
|
||||
format makes it easier for students to read very long response options. Dropdowns also differ
|
||||
slightly because students are more likely to think of an answer and then search for it rather than
|
||||
relying purely on recognition to answer the question.
|
||||
[explanation]
|
||||
"
|
||||
data: |
|
||||
<problem>
|
||||
<p>OptionResponse gives a limited set of options for students to respond with, and presents those options
|
||||
<p>Dropdown problems give a limited set of options for students to respond with, and present those options
|
||||
in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.</p>
|
||||
|
||||
<p>
|
||||
The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.
|
||||
</p>
|
||||
<p>Translation between Option Response and __________ is extremely straightforward:
|
||||
<p>Translation between Dropdown and __________ is extremely straightforward:
|
||||
|
||||
<optionresponse>
|
||||
<optioninput options="('Multiple Choice','String Response','Numerical Response','External Response','Image Response')" correct="Multiple Choice"></optioninput>
|
||||
<optioninput options="('Multiple Choice','Text Input','Numerical Input','External Response','Image Response')" correct="Multiple Choice"></optioninput>
|
||||
</optionresponse>
|
||||
</p>
|
||||
<solution>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: String Response
|
||||
display_name: Text Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
|
||||
markdown:
|
||||
"A string response problem accepts a line of text input from the
|
||||
"A text input problem accepts a line of text from the
|
||||
student, and evaluates the input for correctness based on an expected
|
||||
answer within each input box.
|
||||
answer.
|
||||
|
||||
|
||||
The answer is correct if it matches every character of the expected answer. This can be a problem with
|
||||
@@ -30,9 +30,9 @@ data: |
|
||||
<problem showanswer="always">
|
||||
<p>
|
||||
|
||||
A string response problem accepts a line of text input from the
|
||||
A text input problem accepts a line of text from the
|
||||
student, and evaluates the input for correctness based on an expected
|
||||
answer within each input box.
|
||||
answer.
|
||||
|
||||
The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: default
|
||||
data_dir: a_made_up_name
|
||||
data: |
|
||||
<videoalpha youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
|
||||
children: []
|
||||
@@ -12,6 +12,8 @@ import capa.xqueue_interface as xqueue_interface
|
||||
from datetime import datetime
|
||||
|
||||
from . import test_system
|
||||
|
||||
import test_util_open_ended
|
||||
"""
|
||||
Tests for the various pieces of the CombinedOpenEndedGrading system
|
||||
|
||||
@@ -43,7 +45,9 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
'max_score': max_score,
|
||||
'display_name': 'Name',
|
||||
'accept_file_upload': False,
|
||||
'close_date': None
|
||||
'close_date': None,
|
||||
's3_interface' : "",
|
||||
'open_ended_grading_interface' : {},
|
||||
}
|
||||
definition = Mock()
|
||||
descriptor = Mock()
|
||||
@@ -161,6 +165,8 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'accept_file_upload': False,
|
||||
'rewrite_content_links' : "",
|
||||
'close_date': None,
|
||||
's3_interface' : test_util_open_ended.S3_INTERFACE,
|
||||
'open_ended_grading_interface' : test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
|
||||
}
|
||||
|
||||
oeparam = etree.XML('''
|
||||
@@ -293,6 +299,8 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
'accept_file_upload' : False,
|
||||
'rewrite_content_links' : "",
|
||||
'close_date' : "",
|
||||
's3_interface' : test_util_open_ended.S3_INTERFACE,
|
||||
'open_ended_grading_interface' : test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
|
||||
}
|
||||
|
||||
oeparam = etree.XML('''
|
||||
|
||||
@@ -84,18 +84,21 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
|
||||
location = descriptor.location
|
||||
instance_state = instance_states.get(location.category, None)
|
||||
print "inner_get_module, location.category=%s, inst_state=%s" % (location.category, instance_state)
|
||||
print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
|
||||
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
|
||||
|
||||
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
|
||||
module = inner_get_module(location)
|
||||
|
||||
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
|
||||
return text
|
||||
test_system.replace_urls = replace_urls
|
||||
test_system.get_module = inner_get_module
|
||||
|
||||
module = inner_get_module(location)
|
||||
print "module: ", module
|
||||
print "module definition: ", module.definition
|
||||
print "module children: ", module.get_children()
|
||||
print "module display items (children): ", module.get_display_items()
|
||||
|
||||
html = module.get_html()
|
||||
print "html type: ", type(html)
|
||||
|
||||
@@ -5,9 +5,11 @@ import unittest
|
||||
from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssessmentModule
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
from . import test_system
|
||||
|
||||
import test_util_open_ended
|
||||
|
||||
class SelfAssessmentTest(unittest.TestCase):
|
||||
|
||||
@@ -46,7 +48,9 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
'max_score': 1,
|
||||
'display_name': "Name",
|
||||
'accept_file_upload': False,
|
||||
'close_date': None
|
||||
'close_date': None,
|
||||
's3_interface' : test_util_open_ended.S3_INTERFACE,
|
||||
'open_ended_grading_interface' : test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
|
||||
}
|
||||
|
||||
self.module = SelfAssessmentModule(test_system, self.location,
|
||||
@@ -59,7 +63,7 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
self.assertTrue("This is sample prompt text" in html)
|
||||
|
||||
def test_self_assessment_flow(self):
|
||||
|
||||
raise SkipTest()
|
||||
self.assertEqual(self.module.get_score()['score'], 0)
|
||||
|
||||
self.module.save_answer({'student_answer': "I am an answer"}, test_system)
|
||||
|
||||
14
common/lib/xmodule/xmodule/tests/test_util_open_ended.py
Normal file
14
common/lib/xmodule/xmodule/tests/test_util_open_ended.py
Normal file
@@ -0,0 +1,14 @@
|
||||
OPEN_ENDED_GRADING_INTERFACE = {
|
||||
'url' : 'http://127.0.0.1:3033/',
|
||||
'username' : 'incorrect',
|
||||
'password' : 'incorrect',
|
||||
'staff_grading' : 'staff_grading',
|
||||
'peer_grading' : 'peer_grading',
|
||||
'grading_controller' : 'grading_controller'
|
||||
}
|
||||
|
||||
S3_INTERFACE = {
|
||||
'aws_access_key' : "",
|
||||
'aws_secret_key' : "",
|
||||
"aws_bucket_name" : "",
|
||||
}
|
||||
162
common/lib/xmodule/xmodule/timelimit_module.py
Normal file
162
common/lib/xmodule/xmodule/timelimit_module.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from time import time
|
||||
|
||||
from xmodule.editing_module import XMLEditingDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class TimeLimitModule(XModule):
|
||||
'''
|
||||
Wrapper module which imposes a time constraint for the completion of its child.
|
||||
'''
|
||||
|
||||
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)
|
||||
|
||||
self.rendered = False
|
||||
self.beginning_at = None
|
||||
self.ending_at = None
|
||||
self.accommodation_code = None
|
||||
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
|
||||
if 'beginning_at' in state:
|
||||
self.beginning_at = state['beginning_at']
|
||||
if 'ending_at' in state:
|
||||
self.ending_at = state['ending_at']
|
||||
if 'accommodation_code' in state:
|
||||
self.accommodation_code = state['accommodation_code']
|
||||
|
||||
# For a timed activity, we are only interested here
|
||||
# in time-related accommodations, and these should be disjoint.
|
||||
# (For proctored exams, it is possible to have multiple accommodations
|
||||
# apply to an exam, so they require accommodating a multi-choice.)
|
||||
TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
|
||||
('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
|
||||
('ADD30MIN', 'Extra Time - 30 Minutes'),
|
||||
('DOUBLE', 'Extra Time - Double Time'),
|
||||
('TESTING', 'Extra Time -- Large amount for testing purposes')
|
||||
)
|
||||
|
||||
def _get_accommodated_duration(self, duration):
|
||||
'''
|
||||
Get duration for activity, as adjusted for accommodations.
|
||||
Input and output are expressed in seconds.
|
||||
'''
|
||||
if self.accommodation_code is None or self.accommodation_code == 'NONE':
|
||||
return duration
|
||||
elif self.accommodation_code == 'ADDHALFTIME':
|
||||
# TODO: determine what type to return
|
||||
return int(duration * 1.5)
|
||||
elif self.accommodation_code == 'ADD30MIN':
|
||||
return (duration + (30 * 60))
|
||||
elif self.accommodation_code == 'DOUBLE':
|
||||
return (duration * 2)
|
||||
elif self.accommodation_code == 'TESTING':
|
||||
# when testing, set timer to run for a week at a time.
|
||||
return 3600 * 24 * 7
|
||||
|
||||
@property
|
||||
def has_begun(self):
|
||||
return self.beginning_at is not None
|
||||
|
||||
@property
|
||||
def has_ended(self):
|
||||
if not self.ending_at:
|
||||
return False
|
||||
return self.ending_at < time()
|
||||
|
||||
def begin(self, duration):
|
||||
'''
|
||||
Sets the starting time and ending time for the activity,
|
||||
based on the duration provided (in seconds).
|
||||
'''
|
||||
self.beginning_at = time()
|
||||
modified_duration = self._get_accommodated_duration(duration)
|
||||
self.ending_at = self.beginning_at + modified_duration
|
||||
|
||||
def get_remaining_time_in_ms(self):
|
||||
return int((self.ending_at - time()) * 1000)
|
||||
|
||||
def get_instance_state(self):
|
||||
state = {}
|
||||
if self.beginning_at:
|
||||
state['beginning_at'] = self.beginning_at
|
||||
if self.ending_at:
|
||||
state['ending_at'] = self.ending_at
|
||||
if self.accommodation_code:
|
||||
state['accommodation_code'] = self.accommodation_code
|
||||
return json.dumps(state)
|
||||
|
||||
def get_html(self):
|
||||
self.render()
|
||||
return self.content
|
||||
|
||||
def get_progress(self):
|
||||
''' Return the total progress, adding total done and total available.
|
||||
(assumes that each submodule uses the same "units" for progress.)
|
||||
'''
|
||||
# TODO: Cache progress or children array?
|
||||
children = self.get_children()
|
||||
progresses = [child.get_progress() for child in children]
|
||||
progress = reduce(Progress.add_counts, progresses)
|
||||
return progress
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def render(self):
|
||||
if self.rendered:
|
||||
return
|
||||
# assumes there is one and only one child, so it only renders the first child
|
||||
children = self.get_display_items()
|
||||
if children:
|
||||
child = children[0]
|
||||
self.content = child.get_html()
|
||||
self.rendered = True
|
||||
|
||||
def get_icon_class(self):
|
||||
children = self.get_children()
|
||||
if children:
|
||||
return children[0].get_icon_class()
|
||||
else:
|
||||
return "other"
|
||||
|
||||
class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
|
||||
|
||||
module_class = TimeLimitModule
|
||||
|
||||
# For remembering when a student started, and when they should end
|
||||
stores_state = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
children = []
|
||||
for child in xml_object:
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
|
||||
except Exception as e:
|
||||
log.exception("Unable to load child when parsing TimeLimit wrapper. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {'children': children}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('timelimit')
|
||||
for child in self.get_children():
|
||||
xml_object.append(
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return xml_object
|
||||
|
||||
@@ -4,6 +4,8 @@ import logging
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
@@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
155
common/lib/xmodule/xmodule/videoalpha_module.py
Normal file
155
common/lib/xmodule/xmodule/videoalpha_module.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VideoAlphaModule(XModule):
|
||||
"""
|
||||
XML source example:
|
||||
|
||||
<videoalpha show_captions="true"
|
||||
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
|
||||
url_name="lecture_21_3" display_name="S19V3: Vacancies"
|
||||
>
|
||||
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
|
||||
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
|
||||
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
|
||||
</videoalpha>
|
||||
"""
|
||||
video_time = 0
|
||||
icon_class = 'video'
|
||||
|
||||
js = {
|
||||
'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')],
|
||||
'coffee':
|
||||
[resource_string(__name__, 'js/src/time.coffee'),
|
||||
resource_string(__name__, 'js/src/videoalpha/display.coffee')] +
|
||||
[resource_string(__name__, 'js/src/videoalpha/display/' + filename)
|
||||
for filename
|
||||
in sorted(resource_listdir(__name__, 'js/src/videoalpha/display'))
|
||||
if filename.endswith('.coffee')]}
|
||||
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
|
||||
js_module_name = "VideoAlpha"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
self.youtube_streams = xmltree.get('youtube')
|
||||
self.sub = xmltree.get('sub')
|
||||
self.position = 0
|
||||
self.show_captions = xmltree.get('show_captions', 'true')
|
||||
self.sources = {
|
||||
'main': self._get_source(xmltree),
|
||||
'mp4': self._get_source(xmltree, ['mp4']),
|
||||
'webm': self._get_source(xmltree, ['webm']),
|
||||
'ogv': self._get_source(xmltree, ['ogv']),
|
||||
}
|
||||
self.track = self._get_track(xmltree)
|
||||
self.start_time, self.end_time = self._get_timeframe(xmltree)
|
||||
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
if 'position' in state:
|
||||
self.position = int(float(state['position']))
|
||||
|
||||
def _get_source(self, xmltree, exts=None):
|
||||
"""Find the first valid source, which ends with one of `exts`."""
|
||||
exts = ['mp4', 'ogv', 'avi', 'webm'] if exts is None else exts
|
||||
condition = lambda src: any([src.endswith(ext) for ext in exts])
|
||||
return self._get_first_external(xmltree, 'source', condition)
|
||||
|
||||
def _get_track(self, xmltree):
|
||||
# find the first valid track
|
||||
return self._get_first_external(xmltree, 'track')
|
||||
|
||||
def _get_first_external(self, xmltree, tag, condition=bool):
|
||||
"""Will return the first 'valid' element of the given tag.
|
||||
'valid' means that `condition('src' attribute) == True`
|
||||
"""
|
||||
result = None
|
||||
|
||||
for element in xmltree.findall(tag):
|
||||
src = element.get('src')
|
||||
if condition(src):
|
||||
result = src
|
||||
break
|
||||
return result
|
||||
|
||||
def _get_timeframe(self, xmltree):
|
||||
""" Converts 'from' and 'to' parameters in video tag to seconds.
|
||||
If there are no parameters, returns empty string. """
|
||||
|
||||
def parse_time(s):
|
||||
"""Converts s in '12:34:45' format to seconds. If s is
|
||||
None, returns empty string"""
|
||||
if s is None:
|
||||
return ''
|
||||
else:
|
||||
x = time.strptime(s, '%H:%M:%S')
|
||||
return datetime.timedelta(hours=x.tm_hour,
|
||||
minutes=x.tm_min,
|
||||
seconds=x.tm_sec).total_seconds()
|
||||
|
||||
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""Handle ajax calls to this video.
|
||||
TODO (vshnayder): This is not being called right now, so the
|
||||
position is not being saved.
|
||||
"""
|
||||
log.debug(u"GET {0}".format(get))
|
||||
log.debug(u"DISPATCH {0}".format(dispatch))
|
||||
if dispatch == 'goto_position':
|
||||
self.position = int(float(get['position']))
|
||||
log.info(u"NEW POSITION {0}".format(self.position))
|
||||
return json.dumps({'success': True})
|
||||
raise Http404()
|
||||
|
||||
def get_instance_state(self):
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
def get_html(self):
|
||||
if isinstance(modulestore(), MongoModuleStore):
|
||||
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
|
||||
else:
|
||||
# VS[compat]
|
||||
# cdodge: filesystem static content support.
|
||||
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
|
||||
|
||||
return self.system.render_template('videoalpha.html', {
|
||||
'youtube_streams': self.youtube_streams,
|
||||
'id': self.location.html_id(),
|
||||
'sub': self.sub,
|
||||
'sources': self.sources,
|
||||
'track': self.track,
|
||||
'display_name': self.display_name,
|
||||
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
|
||||
'data_dir': self.metadata['data_dir'],
|
||||
'caption_asset_path': caption_asset_path,
|
||||
'show_captions': self.show_captions,
|
||||
'start': self.start_time,
|
||||
'end': self.end_time
|
||||
})
|
||||
|
||||
|
||||
class VideoAlphaDescriptor(RawDescriptor):
|
||||
module_class = VideoAlphaModule
|
||||
stores_state = True
|
||||
template_dir_name = "videoalpha"
|
||||
@@ -515,6 +515,16 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
self._child_instances = None
|
||||
self._inherited_metadata = set()
|
||||
|
||||
|
||||
# Class level variable
|
||||
always_recalculate_grades = False
|
||||
"""
|
||||
Return whether this descriptor always requires recalculation of grades, for
|
||||
example if the score can change via an extrnal service, not just when the
|
||||
student interacts with the module on the page. A specific example is
|
||||
FoldIt, which posts grade-changing updates through a separate API.
|
||||
"""
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
'''
|
||||
|
||||
Reference in New Issue
Block a user