Commit 5709ff34 by Don Mitchell

Refactored videos to a much simpler impl b/c about videos don't have

speeds and don't use the video/default.yaml format.
parent b74c5429
...@@ -8,6 +8,8 @@ from contentstore.utils import get_modulestore ...@@ -8,6 +8,8 @@ from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date from util.converters import jsdate_to_time, time_to_date
from cms.djangoapps.models.settings import course_grading from cms.djangoapps.models.settings import course_grading
from cms.djangoapps.contentstore.utils import update_item from cms.djangoapps.contentstore.utils import update_item
import re
class CourseDetails: class CourseDetails:
def __init__(self, location): def __init__(self, location):
...@@ -58,7 +60,8 @@ class CourseDetails: ...@@ -58,7 +60,8 @@ class CourseDetails:
temploc = temploc._replace(name='video') temploc = temploc._replace(name='video')
try: try:
course.intro_video = get_modulestore(temploc).get_item(temploc).definition['data'] raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError: except ItemNotFoundError:
pass pass
...@@ -127,12 +130,43 @@ class CourseDetails: ...@@ -127,12 +130,43 @@ class CourseDetails:
update_item(temploc, jsondict['effort']) update_item(temploc, jsondict['effort'])
temploc = temploc._replace(name='video') temploc = temploc._replace(name='video')
update_item(temploc, jsondict['intro_video']) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly # it persisted correctly
return CourseDetails.fetch(course_location) return CourseDetails.fetch(course_location)
@staticmethod
def parse_video_tag(raw_video):
"""
Because the client really only wants the author to specify the youtube key, that's all we send to and get from the client.
The problem is that the db stores the html markup as well (which, of course, makes any sitewide changes to how we do videos
next to impossible.)
"""
if not raw_video:
return None
keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
if keystring_matcher is None:
keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video)
if keystring_matcher:
return keystring_matcher.group(0)
else:
# TODO should this be None or the raw_video? It would be good to at least log this
return None
@staticmethod
def recompose_video_tag(video_key):
# TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing
# the right thing
result = '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + \
video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
return result
# TODO move to a more general util? Is there a better way to do the isinstance model check? # TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder): class CourseSettingsEncoder(json.JSONEncoder):
......
...@@ -50,19 +50,10 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -50,19 +50,10 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
errors.enrollment_end = "The enrollment end date cannot be after the course end date."; errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
} }
if (newattrs.intro_video && newattrs.intro_video != this.get('intro_video')) { if (newattrs.intro_video && newattrs.intro_video != this.get('intro_video')) {
var videos = this.parse_videosource(newattrs.intro_video); if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
var vid_errors = new Array(); errors.intro_video = "Key should only contain letters, numbers, _, or -";
var cachethis = this;
for (var i=0; i<videos.length; i++) {
// doesn't call parseFloat or Number b/c they stop on first non parsable and return what they have
if (!isFinite(videos[i].speed)) vid_errors.push(videos[i].speed + " is not a valid speed.");
else if (!videos[i].key) vid_errors.push(videos[i].speed + " does not have a video id");
// can't use get from client to test if video exists b/c of CORS (crossbrowser get not allowed)
// GET "http://gdata.youtube.com/feeds/api/videos/" + videokey
}
if (!_.isEmpty(vid_errors)) {
errors.intro_video = vid_errors.join(' ');
} }
// TODO check if key points to a real video using google's youtube api
} }
if (!_.isEmpty(errors)) return errors; if (!_.isEmpty(errors)) return errors;
// NOTE don't return empty errors as that will be interpreted as an error state // NOTE don't return empty errors as that will be interpreted as an error state
...@@ -73,100 +64,20 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -73,100 +64,20 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details'; return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
}, },
_videoprefix : /\s*<video\s*youtube="/g, _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
// the below is lax to enable validation
_videospeedparse : /[^:]*/g, // /\d+\.?\d*(?=:)/g,
_videokeyparse : /([^,\/>]+)/g,
_videonosuffix : /[^"\/>]+/g,
_getNextMatch : function (regex, string, cursor) {
regex.lastIndex = cursor;
var result = regex.exec(string);
if (_.isArray(result)) return result[0];
else return result;
},
// the whole string for editing (put in edit box)
getVideoSource: function() {
if (this.get('intro_video')) {
var cursor = 0;
var videostring = this.get('intro_video');
this._getNextMatch(this._videoprefix, videostring, cursor);
cursor = this._videoprefix.lastIndex;
return this._getNextMatch(this._videonosuffix, videostring, cursor);
}
else return "";
},
// the source closest to 1.0 speed
videosourceSample: function() {
if (this.get('intro_video')) {
var cursor = 0;
var videostring = this.get('intro_video');
this._getNextMatch(this._videoprefix, videostring, cursor);
cursor = this._videoprefix.lastIndex;
// parse from [speed:id,/s?]* to find 1.0 or take first
var parsedspeed = this._getNextMatch(this._videospeedparse, videostring, cursor);
var bestkey;
if (parsedspeed) {
cursor = this._videospeedparse.lastIndex + 1;
var bestspeed = Number(parsedspeed);
bestkey = this._getNextMatch(this._videokeyparse, videostring, cursor);
cursor = this._videokeyparse.lastIndex + 1;
while (cursor < videostring.length && bestspeed != 1.0) {
parsedspeed = this._getNextMatch(this._videospeedparse, videostring, cursor);
if (parsedspeed) cursor = this._videospeedparse.lastIndex + 1;
else break;
if (Math.abs(Number(parsedspeed) - 1.0) < Math.abs(bestspeed - 1.0)) {
bestspeed = Number(parsedspeed);
bestkey = this._getNextMatch(this._videokeyparse, videostring, cursor);
}
else this._getNextMatch(this._videokeyparse, videostring, cursor);
if (this._videokeyparse.lastIndex > cursor) cursor = this._videokeyparse.lastIndex + 1;
else cursor++;
}
}
else {
bestkey = this._getNextMatch(this._videokeyparse, videostring, cursor);
}
if (bestkey) {
// WTF? for some reason bestkey is an array [key, key] (same one repeated)
if (_.isArray(bestkey)) bestkey = bestkey[0];
return "http://www.youtube.com/embed/" + bestkey;
}
else return "";
}
},
parse_videosource: function(videostring) {
// used to validate before set so cannot get from model attr. Returns [{ speed: fff, key: sss }]
var cursor = 0;
this._getNextMatch(this._videoprefix, videostring, cursor);
cursor = this._videoprefix.lastIndex;
videostring = this._getNextMatch(this._videonosuffix, videostring, cursor);
cursor = 0;
// parsed to "fff:kkk,fff:kkk"
var result = new Array();
if (!videostring || videostring.length == 0) return result;
while (cursor < videostring.length) {
var speed = this._getNextMatch(this._videospeedparse, videostring, cursor);
if (speed) cursor = this._videospeedparse.lastIndex + 1;
else return result;
var key = this._getNextMatch(this._videokeyparse, videostring, cursor);
if (key) cursor = this._videokeyparse.lastIndex + 1;
// See the WTF above
if (_.isArray(key)) key = key[0];
result.push({speed: speed, key: key});
}
return result;
},
save_videosource: function(newsource) { save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string // newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1 // returns the videosource for the preview which iss the key whose speed is closest to 1
if (newsource == null) this.save({'intro_video': null}); if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string // TODO remove all whitespace w/in string
else { else {
var newVal = ((this._getNextMatch(this._videoprefix, newsource, 0)) ? newsource : '<video youtube="' + newsource + '"/>'); if (this.get('intro_video') != newsource) this.save('intro_video', newsource);
if (this.get('intro_video') != newVal) this.save('intro_video', newVal);
} }
return this.videosourceSample(); return this.videosourceSample();
},
videosourceSample : function() {
if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
else return "";
} }
}); });
...@@ -193,7 +193,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -193,7 +193,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample()); this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
if (this.model.has('intro_video')) { if (this.model.has('intro_video')) {
this.$el.find('.remove-course-introduction-video').show(); this.$el.find('.remove-course-introduction-video').show();
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.getVideoSource()); this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
} }
else this.$el.find('.remove-course-introduction-video').hide(); else this.$el.find('.remove-course-introduction-video').hide();
...@@ -261,6 +261,12 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -261,6 +261,12 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.clearValidationErrors(); this.clearValidationErrors();
var previewsource = this.model.save_videosource($(event.currentTarget).val()); var previewsource = this.model.save_videosource($(event.currentTarget).val());
this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource); this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
if (this.model.has('intro_video')) {
this.$el.find('.remove-course-introduction-video').show();
}
else {
this.$el.find('.remove-course-introduction-video').hide();
}
break; break;
default: default:
...@@ -282,6 +288,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -282,6 +288,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.model.save_videosource(null); this.model.save_videosource(null);
this.$el.find(".current-course-introduction-video iframe").attr("src", ""); this.$el.find(".current-course-introduction-video iframe").attr("src", "");
this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(""); this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
this.$el.find('.remove-course-introduction-video').hide();
} }
}, },
codeMirrors : {}, codeMirrors : {},
......
...@@ -230,7 +230,7 @@ from contentstore import utils ...@@ -230,7 +230,7 @@ from contentstore import utils
</div> </div>
<div class="input"> <div class="input">
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="speed:id,speed:id" autocomplete="off"> <input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="id" autocomplete="off">
<span class="tip tip-stacked">Video restrictions go here</span> <span class="tip tip-stacked">Video restrictions go here</span>
</div> </div>
</div> </div>
......
...@@ -61,16 +61,18 @@ category = ${category | h} ...@@ -61,16 +61,18 @@ category = ${category | h}
<script type="text/javascript"> <script type="text/javascript">
// assumes courseware.html's loaded this method. // assumes courseware.html's loaded this method.
setup_debug('${element_id}', % if staff_access:
%if edit_link: setup_debug('${element_id}',
'${edit_link}', %if edit_link:
%else: '${edit_link}',
null, %else:
%endif null,
{ %endif
'location': '${location}', {
'xqa_key': '${xqa_key}', 'location': '${location}',
'category': '${category}', 'xqa_key': '${xqa_key}',
'user': '${user}' 'category': '${category}',
}); 'user': '${user}'
});
% endif
</script> </script>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment