Commit 5475e3ed by J. Cliff Dyer

Define custom completion for VideoModule

Update the VideoModule to publish a completion event when the player
reaches 95% complete, and submit a BlockCompletion when that event
occurs.

OC-3091
MCKIN-5897
parent 2cadbbad
(function(define) {
'use strict';
define('video/99_completion.js', [], function() {
/**
* Play/pause control module.
* @exports video/09_play_pause_control.js
* @constructor
* @param {Object} state The object containing the state of the video
* @return {jquery Promise}
*/
var CompletionListener = function(state) {
if (!(this instanceof CompletionListener)) {
return new CompletionListener(state);
}
_.bindAll(this, 'play', 'pause', 'onClick', 'destroy');
this.state = state;
this.state.completionListener = this;
this.initialize();
return $.Deferred().resolve().promise();
};
CompletionListener.prototype = {
destroy: function() {
this.el.remove();
this.state.el.off('destroy', this.destroy);
delete this.state.videoPlayPauseControl;
},
/** Initializes the module. */
initialize: function() {
this.complete = False;
this.bindHandlers();
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.state.el.on({
'timeupdate': this.checkCompletion,
});
},
/** Event handler to check if the video is complete, and submit a completion if it is */
checkCompletion: function(currentTime) {
// Need to access runtime for this.
if (this.complete === false && currentTime > this.state.completeAfter) {
this.complete = true;
if (this.state.config.publishCompletionUrl) {
$.ajax({
type: 'POST',
url: this.state.config.publishCompletionUrl,
data: JSON.stringify({
completion: 1.0
})
});
} else {
console.warn("publishCompletionUrl not defined");
}
}
}
};
return CompletionListener;
});
}(RequireJS.define));
""" """
Utils for video bumper Utils for video bumper
""" """
from collections import OrderedDict
import copy import copy
import json import json
import pytz
import logging import logging
from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
import pytz
from .video_utils import set_query_parameter from .video_utils import set_query_parameter
...@@ -137,6 +137,9 @@ def bumper_metadata(video, sources): ...@@ -137,6 +137,9 @@ def bumper_metadata(video, sources):
'transcriptAvailableTranslationsUrl': set_query_parameter( 'transcriptAvailableTranslationsUrl': set_query_parameter(
video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1 video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1
), ),
'publishCompletionUrl': set_query_parameter(
video.runtime.handler_url(video, 'publish_completion', None).rstrip('/?'), 'is_bumper', 1
),
}) })
return metadata return metadata
...@@ -13,6 +13,7 @@ from datetime import datetime ...@@ -13,6 +13,7 @@ from datetime import datetime
from webob import Response from webob import Response
from xblock.core import XBlock from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.fields import RelativeTime from xmodule.fields import RelativeTime
...@@ -201,6 +202,28 @@ class VideoStudentViewHandlers(object): ...@@ -201,6 +202,28 @@ class VideoStudentViewHandlers(object):
) )
return response return response
@XBlock.json_handler
def publish_completion(self, data, dispatch): # pylint: disable=unused-argument
"""
Entry point for completion for student_view.
Parameters:
data: JSON dict:
key: "completion"
value: float in range [0.0, 1.0]
dispatch: Ignored.
Return value: JSON response (200 on success, 400 for malformed data)
"""
completion_service = self.runtime.service('completion')
if not completion_service or not completion_service.is_completion_enabled():
raise JsonHandlerError(404, u"Completion service not available")
if not 0.0 <= data['completion'] <= 1.0:
message = u"Invalid completion value {}. Must be in range [0.0, 1.0]"
raise JsonHandlerError(400, message.format(data['completion']))
self.runtime.publish(self, "completion", data)
return {"result": "ok"}
@XBlock.handler @XBlock.handler
def transcript(self, request, dispatch): def transcript(self, request, dispatch):
""" """
...@@ -281,6 +304,8 @@ class VideoStudentViewHandlers(object): ...@@ -281,6 +304,8 @@ class VideoStudentViewHandlers(object):
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript( transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(
transcripts, transcript_format=self.transcript_download_format, lang=lang transcripts, transcript_format=self.transcript_download_format, lang=lang
) )
except (KeyError, UnicodeDecodeError):
return Response(status=404)
except (ValueError, NotFoundError): except (ValueError, NotFoundError):
response = Response(status=404) response = Response(status=404)
# Check for transcripts in edx-val as a last resort if corresponding feature is enabled. # Check for transcripts in edx-val as a last resort if corresponding feature is enabled.
...@@ -318,8 +343,6 @@ class VideoStudentViewHandlers(object): ...@@ -318,8 +343,6 @@ class VideoStudentViewHandlers(object):
response.content_type = Transcript.mime_types[self.transcript_download_format] response.content_type = Transcript.mime_types[self.transcript_download_format]
return response return response
except (KeyError, UnicodeDecodeError):
return Response(status=404)
else: else:
response = Response( response = Response(
transcript_content, transcript_content,
......
...@@ -108,6 +108,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -108,6 +108,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/> <source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
</video> </video>
""" """
has_custom_completion = True
completion_method = u'completable'
video_time = 0 video_time = 0
icon_class = 'video' icon_class = 'video'
...@@ -150,7 +153,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -150,7 +153,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
resource_string(module, 'js/src/video/09_poster.js'), resource_string(module, 'js/src/video/09_poster.js'),
resource_string(module, 'js/src/video/095_video_context_menu.js'), resource_string(module, 'js/src/video/095_video_context_menu.js'),
resource_string(module, 'js/src/video/10_commands.js'), resource_string(module, 'js/src/video/10_commands.js'),
resource_string(module, 'js/src/video/10_main.js') resource_string(module, 'js/src/video/10_main.js'),
] ]
} }
css = {'scss': [ css = {'scss': [
...@@ -336,6 +339,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -336,6 +339,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
'savedVideoPosition': self.saved_video_position.total_seconds(), 'savedVideoPosition': self.saved_video_position.total_seconds(),
'start': self.start_time.total_seconds(), 'start': self.start_time.total_seconds(),
'end': self.end_time.total_seconds(), 'end': self.end_time.total_seconds(),
'completeAfter': self.start_time.total_seconds() + settings.COMPLETION_VIDEO_COMPLETE_PERCENTAGE * (self.end_time.total_seconds() - self.start_time.total_seconds()),
'transcriptLanguage': transcript_language, 'transcriptLanguage': transcript_language,
'transcriptLanguages': sorted_languages, 'transcriptLanguages': sorted_languages,
'ytTestTimeout': settings.YOUTUBE['TEST_TIMEOUT'], 'ytTestTimeout': settings.YOUTUBE['TEST_TIMEOUT'],
...@@ -349,6 +353,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -349,6 +353,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
'transcriptAvailableTranslationsUrl': self.runtime.handler_url( 'transcriptAvailableTranslationsUrl': self.runtime.handler_url(
self, 'transcript', 'available_translations' self, 'transcript', 'available_translations'
).rstrip('/?'), ).rstrip('/?'),
'publishCompletionUrl': self.runtime.handler_url(self, 'publish_completion', None).rstrip('/?'),
## For now, the option "data-autohide-html5" is hard coded. This option ## For now, the option "data-autohide-html5" is hard coded. This option
## either enables or disables autohiding of controls and captions on mouse ## either enables or disables autohiding of controls and captions on mouse
......
...@@ -3358,3 +3358,8 @@ if not EDX_PLATFORM_REVISION: ...@@ -3358,3 +3358,8 @@ if not EDX_PLATFORM_REVISION:
except TypeError: except TypeError:
# Not a git repository # Not a git repository
EDX_PLATFORM_REVISION = 'unknown' EDX_PLATFORM_REVISION = 'unknown'
# NEW
COMPLETION_VIDEO_COMPLETE_PERCENTAGE = 0.95
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