diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js index 4679d4c0c6b0..48d380027ee6 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js @@ -127,6 +127,20 @@ import * as Time from 'time.js'; }); }); + it('data contains video completely watched flag, async is true', function() { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: undefined, + data: { + is_complete: true + }, + ajaxData: { + is_complete: true + } + }); + }); + function itSpec(value) { state.config.saveStateEnabled = true; var asyncVal = value.asyncVal, diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 3894a28632c3..7d888e2382a7 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -224,6 +224,9 @@ function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _, Time) { if (state.isTouch) { dfd.resolve(); } + if (state.config.enableNextOnCompletion === true && state.config.isComplete === false) { + $('.sequence-nav-button.button-next').prop('disabled', true); + } } function _updateVcrAndRegion(state, isYoutube) { @@ -511,8 +514,6 @@ function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _, Time) { function onEnded() { var time = this.videoPlayer.duration(); - - this.trigger('videoProgressSlider.notifyThroughHandleEnd', { end: true }); diff --git a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js index bf72cf32efad..27ecb70cfc37 100644 --- a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js @@ -76,7 +76,6 @@ function() { // have to do repeated jQuery element selects. function _renderElements(state) { state.videoProgressSlider.el = $(template); - state.el.find('.video-controls').prepend(state.videoProgressSlider.el); state.videoProgressSlider.buildSlider(); _buildHandle(state); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_completion.js b/common/lib/xmodule/xmodule/js/src/video/09_completion.js index 97378e92ef17..c2eb136faa4f 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_completion.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_completion.js @@ -102,6 +102,7 @@ /** Handler to call when a timeupdate event is triggered */ handleTimeUpdate: function(currentTime) { + var duration; if (this.isComplete) { return; @@ -132,6 +133,9 @@ var errmsg; this.isComplete = true; this.lastSentTime = currentTime; + if (self.state.config.enableNextOnCompletion === true && self.state.config.isComplete === false) { + $('.sequence-nav-button.button-next').prop('disabled', false); + } if (this.state.config.publishCompletionUrl) { $.ajax({ type: 'POST', @@ -142,6 +146,7 @@ success: function() { self.state.el.off('timeupdate.completion'); self.state.el.off('ended.completion'); + self.state.videoSaveStatePlugin.onVideoComplete(); }, error: function(xhr) { /* eslint-disable no-console */ diff --git a/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js index 726d07043d5f..890450e6c713 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js @@ -16,7 +16,7 @@ } _.bindAll(this, 'onSpeedChange', 'onAutoAdvanceChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload', - 'onYoutubeAvailability', 'onLanguageChange', 'destroy'); + 'onYoutubeAvailability', 'onLanguageChange', 'destroy','onVideoComplete'); this.state = state; this.options = _.extend({events: []}, options); this.state.videoSaveStatePlugin = this; @@ -72,6 +72,10 @@ this.state.storage.setItem('general_speed', newSpeed); }, + onVideoComplete: function(event) { + this.saveState(true, { is_complete: true }); + }, + onAutoAdvanceChange: function(event, enabled) { this.saveState(true, {auto_advance: enabled}); this.state.storage.setItem('auto_advance', enabled); diff --git a/common/lib/xmodule/xmodule/js/src/video/10_main.js b/common/lib/xmodule/xmodule/js/src/video/10_main.js index 25c987e61cbe..b75afecc1bcf 100644 --- a/common/lib/xmodule/xmodule/js/src/video/10_main.js +++ b/common/lib/xmodule/xmodule/js/src/video/10_main.js @@ -87,12 +87,15 @@ storage = VideoStorage('VideoState', id), bumperMetadata = el.data('bumper-metadata'), autoAdvanceEnabled = el.data('autoadvance-enabled') === 'True', + metadata = el.data('metadata'), mainVideoModules = [ FocusGrabber, VideoControl, VideoPlayPlaceholder, - VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl, + VideoPlayPauseControl, VideoVolumeControl, VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands, VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler - ].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []), + ].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : [], + metadata.enableProgressSlider ? [VideoProgressSlider] : [], + metadata.enableSpeedControl ? [VideoSpeedControl] : []), bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl, VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoEventsBumperPlugin, VideoCompletionHandler], diff --git a/common/lib/xmodule/xmodule/video_module/video_handlers.py b/common/lib/xmodule/xmodule/video_module/video_handlers.py index 1d7dd008bc7a..1225e5528e6b 100644 --- a/common/lib/xmodule/xmodule/video_module/video_handlers.py +++ b/common/lib/xmodule/xmodule/video_module/video_handlers.py @@ -67,7 +67,7 @@ def handle_ajax(self, dispatch, data): accepted_keys = [ 'speed', 'auto_advance', 'saved_video_position', 'transcript_language', 'transcript_download_format', 'youtube_is_available', - 'bumper_last_view_date', 'bumper_do_not_show_again' + 'bumper_last_view_date', 'bumper_do_not_show_again','is_complete' ] conversions = { @@ -77,6 +77,7 @@ def handle_ajax(self, dispatch, data): 'youtube_is_available': json.loads, 'bumper_last_view_date': to_boolean, 'bumper_do_not_show_again': to_boolean, + 'is_complete': to_boolean, } if dispatch == 'save_user_state': diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 7e12761a7300..a737dbebaaa4 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -449,8 +449,12 @@ def get_html(self, view=STUDENT_VIEW): if getattr(self.runtime, 'suppports_state_for_anonymous_users', False) else '' ), 'ytTestTimeout': settings.YOUTUBE['TEST_TIMEOUT'], - } + 'enableProgressSlider': self.enable_progress_slider, + 'enableSpeedControl': self.enable_speed_control, + 'enableNextOnCompletion': self.enable_next_on_completion, + 'isComplete': self.is_complete, + } bumperize(self) context = { diff --git a/common/lib/xmodule/xmodule/video_module/video_xfields.py b/common/lib/xmodule/xmodule/video_module/video_xfields.py index 60f257130bba..86cb4d28956d 100644 --- a/common/lib/xmodule/xmodule/video_module/video_xfields.py +++ b/common/lib/xmodule/xmodule/video_module/video_xfields.py @@ -206,3 +206,32 @@ class VideoFields(object): scope=Scope.preferences, default=False, ) + enable_progress_slider = Boolean( + help=_( + "Specify whether progress bar is enabled or disabled" + ), + display_name=_("Enable Progress Slider"), + scope=Scope.settings, + default=True + ) + enable_speed_control = Boolean( + help=_( + "Specify whether Speed Controls are enabled or disabled" + ), + display_name=_("Enable Speed Controls"), + scope=Scope.settings, + default=True + ) + enable_next_on_completion = Boolean( + help=_( + "Enable Next Button on completion" + ), + display_name=_("Enable Next Button on Completion"), + scope=Scope.settings, + default=False + ) + is_complete = Boolean( + help=_("Is video completely watched?"), + scope=Scope.user_state, + default=False + ) diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index 1df5546745b2..eb16d10306af 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -189,13 +189,13 @@ def test_handle_ajax_for_speed_with_nan(self): self.assertEqual(self.item_descriptor.global_speed, 1.0) def test_handle_ajax(self): - data = [ {u'speed': 2.0}, {u'saved_video_position': "00:00:10"}, {u'transcript_language': 'uk'}, {u'bumper_do_not_show_again': True}, {u'bumper_last_view_date': True}, + {u'is_complete': False}, {u'demoo�': 'sample'} ] for sample in data: @@ -222,6 +222,10 @@ def test_handle_ajax(self): self.item_descriptor.handle_ajax('save_user_state', {'bumper_do_not_show_again': True}) self.assertEqual(self.item_descriptor.bumper_do_not_show_again, True) + self.assertEqual(self.item_descriptor.is_complete, False) + self.item_descriptor.handle_ajax('save_user_state', {'is_complete': True}) + self.assertEqual(self.item_descriptor.is_complete, True) + with freezegun.freeze_time(now()): self.assertEqual(self.item_descriptor.bumper_last_view_date, None) self.item_descriptor.handle_ajax('save_user_state', {'bumper_last_view_date': True}) diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 7f6955c6b635..c01f006cc483 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -123,6 +123,10 @@ def test_video_constructor(self): 'completionPercentage': 0.95, 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), 'prioritizeHls': False, + 'enableProgressSlider': True, + 'enableSpeedControl': True, + 'enableNextOnCompletion': False, + 'isComplete': False, })), 'track': None, 'transcript_download_format': u'srt', @@ -132,7 +136,7 @@ def test_video_constructor(self): ], 'poster': 'null', } - + self.assertEqual( get_context_dict_from_string(context), get_context_dict_from_string( @@ -207,6 +211,10 @@ def test_video_constructor(self): 'completionPercentage': 0.95, 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), 'prioritizeHls': False, + 'enableProgressSlider': True, + 'enableSpeedControl': True, + 'enableNextOnCompletion': False, + 'isComplete': False, })), 'track': None, 'transcript_download_format': u'srt', @@ -271,6 +279,10 @@ def setUp(self): 'completionPercentage': 0.95, 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), 'prioritizeHls': False, + 'enableProgressSlider': True, + 'enableSpeedControl': True, + 'enableNextOnCompletion': False, + 'isComplete': False, }) def get_handler_url(self, handler, suffix): @@ -2250,6 +2262,10 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum 'completionPercentage': 0.95, 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), 'prioritizeHls': False, + 'enableProgressSlider': True, + 'enableSpeedControl': True, + 'enableNextOnCompletion': False, + 'isComplete': False, })), 'track': None, 'transcript_download_format': u'srt', @@ -2330,6 +2346,10 @@ def prepare_expected_context(self, autoadvanceenabled_flag, autoadvance_flag): 'completionPercentage': 0.95, 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), 'prioritizeHls': False, + 'enableProgressSlider': True, + 'enableSpeedControl': True, + 'enableNextOnCompletion': False, + 'isComplete': False, })), 'track': None, 'transcript_download_format': u'srt', @@ -2347,7 +2367,7 @@ def assert_content_matches_expectations(self, autoadvanceenabled_must_be, autoad to the passed context. Helper function to avoid code repetition. """ - + with override_settings(FEATURES=self.FEATURES): content = self.item_descriptor.render(STUDENT_VIEW).content