From a4539d6ac0f61179e7772553ec1fc601e83713d0 Mon Sep 17 00:00:00 2001 From: NomanShafi Date: Thu, 27 May 2021 11:32:44 +0500 Subject: [PATCH 1/2] Add speed controls, progress bar and next button feature --- .../video/video_save_state_plugin_spec.js | 14 +++++++++ .../xmodule/js/src/video/03_video_player.js | 5 +++- .../xmodule/js/src/video/09_completion.js | 4 +++ .../js/src/video/09_save_state_plugin.js | 6 +++- .../xmodule/xmodule/js/src/video/10_main.js | 7 +++-- .../xmodule/video_module/video_handlers.py | 3 +- .../xmodule/video_module/video_module.py | 6 +++- .../xmodule/video_module/video_xfields.py | 29 +++++++++++++++++++ .../courseware/tests/test_video_handlers.py | 6 +++- .../courseware/tests/test_video_mongo.py | 22 ++++++++++++-- 10 files changed, 93 insertions(+), 9 deletions(-) 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..aef4f6308eea 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) { @@ -512,7 +515,7 @@ 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/09_completion.js b/common/lib/xmodule/xmodule/js/src/video/09_completion.js index 97378e92ef17..e51d1481417a 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_completion.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_completion.js @@ -132,6 +132,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 +145,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..93dca5b8d6eb 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..fe600ca8c1fc 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,6 @@ def test_video_constructor(self): ], 'poster': 'null', } - self.assertEqual( get_context_dict_from_string(context), get_context_dict_from_string( @@ -207,6 +210,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 +278,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 +2261,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 +2345,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 +2366,6 @@ 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 From 8d2c2d641bfe40e80a7745ce6f045e7194e8f1e6 Mon Sep 17 00:00:00 2001 From: tasawernawaz Date: Thu, 10 Jun 2021 16:30:54 +0500 Subject: [PATCH 2/2] revert empty line changes --- common/lib/xmodule/xmodule/js/src/video/03_video_player.js | 2 +- lms/djangoapps/courseware/tests/test_video_handlers.py | 3 ++- lms/djangoapps/courseware/tests/test_video_mongo.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) 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 aef4f6308eea..71aaff1e9b0b 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 @@ -515,7 +515,7 @@ function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _, Time) { function onEnded() { var time = this.videoPlayer.duration(); - + this.trigger('videoProgressSlider.notifyThroughHandleEnd', { end: true }); diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index 93dca5b8d6eb..79fbb1983e70 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -189,6 +189,7 @@ 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"}, @@ -225,7 +226,7 @@ def test_handle_ajax(self): 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 fe600ca8c1fc..21e24cd4e88c 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -136,6 +136,7 @@ def test_video_constructor(self): ], 'poster': 'null', } + self.assertEqual( get_context_dict_from_string(context), get_context_dict_from_string( @@ -2366,6 +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