Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
82ad9295
Unverified
Commit
82ad9295
authored
Dec 19, 2017
by
Cliff Dyer
Committed by
GitHub
Dec 19, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16569 from open-craft/cliff/video-completion
Define custom completion for VideoModule
parents
2c4a5207
1fc43bf6
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
407 additions
and
95 deletions
+407
-95
cms/envs/aws.py
+9
-0
cms/envs/common.py
+7
-0
common/lib/xmodule/xmodule/js/spec/video/completion_spec.js
+60
-0
common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
+1
-1
common/lib/xmodule/xmodule/js/src/video/09_completion.js
+178
-0
common/lib/xmodule/xmodule/js/src/video/10_main.js
+14
-11
common/lib/xmodule/xmodule/video_module/bumper_utils.py
+5
-2
common/lib/xmodule/xmodule/video_module/video_handlers.py
+30
-2
common/lib/xmodule/xmodule/video_module/video_module.py
+30
-16
lms/djangoapps/courseware/tests/test_video_handlers.py
+8
-0
lms/djangoapps/courseware/tests/test_video_mongo.py
+50
-63
lms/envs/aws.py
+9
-0
lms/envs/common.py
+6
-0
No files found.
cms/envs/aws.py
View file @
82ad9295
...
...
@@ -552,6 +552,15 @@ PARENTAL_CONSENT_AGE_LIMIT = ENV_TOKENS.get(
# Allow extra middleware classes to be added to the app through configuration.
MIDDLEWARE_CLASSES
.
extend
(
ENV_TOKENS
.
get
(
'EXTRA_MIDDLEWARE_CLASSES'
,
[]))
########################## Settings for Completion API #####################
# Once a user has watched this percentage of a video, mark it as complete:
# (0.0 = 0%, 1.0 = 100%)
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
=
ENV_TOKENS
.
get
(
'COMPLETION_VIDEO_COMPLETE_PERCENTAGE'
,
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
,
)
########################## Derive Any Derived Settings #######################
derive_settings
(
__name__
)
cms/envs/common.py
View file @
82ad9295
...
...
@@ -1493,3 +1493,10 @@ ZENDESK_USER = None
ZENDESK_API_KEY
=
None
ZENDESK_OAUTH_ACCESS_TOKEN
=
None
ZENDESK_CUSTOM_FIELDS
=
{}
############## Settings for Completion API #########################
# Once a user has watched this percentage of a video, mark it as complete:
# (0.0 = 0%, 1.0 = 100%)
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
=
0.95
common/lib/xmodule/xmodule/js/spec/video/completion_spec.js
0 → 100644
View file @
82ad9295
(
function
()
{
'use strict'
;
describe
(
'VideoPlayer completion'
,
function
()
{
var
state
,
oldOTBD
;
beforeEach
(
function
()
{
oldOTBD
=
window
.
onTouchBasedDevice
;
window
.
onTouchBasedDevice
=
jasmine
.
createSpy
(
'onTouchBasedDevice'
)
.
and
.
returnValue
(
null
);
state
=
jasmine
.
initializePlayer
({
recordedYoutubeIsAvailable
:
true
,
completionEnabled
:
true
,
publishCompletionUrl
:
'https://example.com/publish_completion_url'
});
state
.
completionHandler
.
completeAfterTime
=
20
;
});
afterEach
(
function
()
{
$
(
'source'
).
remove
();
window
.
onTouchBasedDevice
=
oldOTBD
;
state
.
storage
.
clear
();
if
(
state
.
videoPlayer
)
{
state
.
videoPlayer
.
destroy
();
}
});
it
(
'calls the completion api when marking an object complete'
,
function
()
{
state
.
completionHandler
.
markCompletion
(
Date
.
now
());
expect
(
$
.
ajax
).
toHaveBeenCalledWith
({
url
:
state
.
config
.
publishCompletionUrl
,
type
:
'POST'
,
contentType
:
'application/json'
,
dataType
:
'json'
,
data
:
JSON
.
stringify
({
completion
:
1.0
}),
success
:
jasmine
.
any
(
Function
),
error
:
jasmine
.
any
(
Function
)
});
expect
(
state
.
completionHandler
.
isComplete
).
toEqual
(
true
);
});
it
(
'calls the completion api on the LMS when the time updates'
,
function
()
{
spyOn
(
state
.
completionHandler
,
'markCompletion'
).
and
.
callThrough
();
state
.
el
.
trigger
(
'timeupdate'
,
24.0
);
expect
(
state
.
completionHandler
.
markCompletion
).
toHaveBeenCalled
();
state
.
completionHandler
.
markCompletion
.
calls
.
reset
();
// But the handler is not called again after the block is completed.
state
.
el
.
trigger
(
'timeupdate'
,
30.0
);
expect
(
state
.
completionHandler
.
markCompletion
).
not
.
toHaveBeenCalled
();
});
it
(
'calls the completion api on the LMS when the video ends'
,
function
()
{
spyOn
(
state
.
completionHandler
,
'markCompletion'
).
and
.
callThrough
();
state
.
el
.
trigger
(
'ended'
);
expect
(
state
.
completionHandler
.
markCompletion
).
toHaveBeenCalled
();
});
});
}).
call
(
this
);
common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
View file @
82ad9295
...
...
@@ -219,7 +219,7 @@
}).
done
(
done
);
});
it
(
'set new inc
c
orrect values'
,
function
()
{
it
(
'set new incorrect values'
,
function
()
{
var
seek
=
state
.
videoPlayer
.
player
.
video
.
currentTime
;
state
.
videoPlayer
.
player
.
seekTo
(
-
50
);
expect
(
state
.
videoPlayer
.
player
.
getCurrentTime
()).
toBe
(
seek
);
...
...
common/lib/xmodule/xmodule/js/src/video/09_completion.js
0 → 100644
View file @
82ad9295
(
function
(
define
)
{
'use strict'
;
/**
* Completion handler
* @exports video/09_completion.js
* @constructor
* @param {Object} state The object containing the state of the video
* @return {jquery Promise}
*/
define
(
'video/09_completion.js'
,
[],
function
()
{
var
VideoCompletionHandler
=
function
(
state
)
{
if
(
!
(
this
instanceof
VideoCompletionHandler
))
{
return
new
VideoCompletionHandler
(
state
);
}
this
.
state
=
state
;
this
.
state
.
completionHandler
=
this
;
this
.
initialize
();
return
$
.
Deferred
().
resolve
().
promise
();
};
VideoCompletionHandler
.
prototype
=
{
/** Tears down the VideoCompletionHandler.
*
* * Removes backreferences from this.state to this.
* * Turns off signal handlers.
*/
destroy
:
function
()
{
this
.
el
.
remove
();
this
.
el
.
off
(
'timeupdate.completion'
);
this
.
el
.
off
(
'ended.completion'
);
delete
this
.
state
.
completionHandler
;
},
/** Initializes the VideoCompletionHandler.
*
* This sets all the instance variables needed to perform
* completion calculations.
*/
initialize
:
function
()
{
// Attributes with "Time" in the name refer to the number of seconds since
// the beginning of the video, except for lastSentTime, which refers to a
// timestamp in seconds since the Unix epoch.
this
.
lastSentTime
=
undefined
;
this
.
isComplete
=
false
;
this
.
completionPercentage
=
this
.
state
.
config
.
completionPercentage
;
this
.
startTime
=
this
.
state
.
config
.
startTime
;
this
.
endTime
=
this
.
state
.
config
.
endTime
;
this
.
isEnabled
=
this
.
state
.
config
.
completionEnabled
;
if
(
this
.
endTime
)
{
this
.
completeAfterTime
=
this
.
calculateCompleteAfterTime
(
this
.
startTime
,
this
.
endTime
);
}
if
(
this
.
isEnabled
)
{
this
.
bindHandlers
();
}
},
/** Bind event handler callbacks.
*
* When ended is triggered, mark the video complete
* unconditionally.
*
* When timeupdate is triggered, check to see if the user has
* passed the completeAfterTime in the video, and if so, mark the
* video complete.
*
* When destroy is triggered, clean up outstanding resources.
*/
bindHandlers
:
function
()
{
var
self
=
this
;
/** Event handler to check if the video is complete, and submit
* a completion if it is.
*
* If the timeupdate handler doesn't fire after the required
* percentage, this will catch any fully complete videos.
*/
this
.
state
.
el
.
on
(
'ended.completion'
,
function
()
{
self
.
handleEnded
();
});
/** Event handler to check video progress, and mark complete if
* greater than completionPercentage
*/
this
.
state
.
el
.
on
(
'timeupdate.completion'
,
function
(
ev
,
currentTime
)
{
self
.
handleTimeUpdate
(
currentTime
);
});
/** Event handler to clean up resources when the video player
* is destroyed.
*/
this
.
state
.
el
.
off
(
'destroy'
,
this
.
destroy
);
},
/** Handler to call when the ended event is triggered */
handleEnded
:
function
()
{
if
(
this
.
isComplete
)
{
return
;
}
this
.
markCompletion
();
},
/** Handler to call when a timeupdate event is triggered */
handleTimeUpdate
:
function
(
currentTime
)
{
var
duration
;
if
(
this
.
isComplete
)
{
return
;
}
if
(
this
.
lastSentTime
!==
undefined
&&
currentTime
-
this
.
lastSentTime
<
this
.
repostDelaySeconds
())
{
// Throttle attempts to submit in case of network issues
return
;
}
if
(
this
.
completeAfterTime
===
undefined
)
{
// Duration is not available at initialization time
duration
=
this
.
state
.
videoPlayer
.
duration
();
if
(
!
duration
)
{
// duration is not yet set. Wait for another event,
// or fall back to 'ended' handler.
return
;
}
this
.
completeAfterTime
=
this
.
calculateCompleteAfterTime
(
this
.
startTime
,
duration
);
}
if
(
currentTime
>
this
.
completeAfterTime
)
{
this
.
markCompletion
(
currentTime
);
}
},
/** Submit completion to the LMS */
markCompletion
:
function
(
currentTime
)
{
var
self
=
this
;
var
errmsg
;
this
.
isComplete
=
true
;
this
.
lastSentTime
=
currentTime
;
if
(
this
.
state
.
config
.
publishCompletionUrl
)
{
$
.
ajax
({
type
:
'POST'
,
url
:
this
.
state
.
config
.
publishCompletionUrl
,
contentType
:
'application/json'
,
dataType
:
'json'
,
data
:
JSON
.
stringify
({
completion
:
1.0
}),
success
:
function
()
{
self
.
state
.
el
.
off
(
'timeupdate.completion'
);
self
.
state
.
el
.
off
(
'ended.completion'
);
},
error
:
function
(
xhr
)
{
/* eslint-disable no-console */
self
.
complete
=
false
;
errmsg
=
'Failed to submit completion'
;
if
(
xhr
.
responseJSON
!==
undefined
)
{
errmsg
+=
': '
+
xhr
.
responseJSON
.
error
;
}
console
.
warn
(
errmsg
);
/* eslint-enable no-console */
}
});
}
else
{
/* eslint-disable no-console */
console
.
warn
(
'publishCompletionUrl not defined'
);
/* eslint-enable no-console */
}
},
/** Determine what point in the video (in seconds from the
* beginning) counts as complete.
*/
calculateCompleteAfterTime
:
function
(
startTime
,
endTime
)
{
return
startTime
+
(
endTime
-
startTime
)
*
this
.
completionPercentage
;
},
/** How many seconds to wait after a POST fails to try again. */
repostDelaySeconds
:
function
()
{
return
3.0
;
}
};
return
VideoCompletionHandler
;
});
}(
RequireJS
.
define
));
common/lib/xmodule/xmodule/js/src/video/10_main.js
View file @
82ad9295
/* globals _ */
(
function
(
require
,
$
)
{
'use strict'
;
// In the case when the Video constructor will be called before RequireJS finishes loading all of the Video
...
...
@@ -15,9 +16,9 @@
// If mock function was called with second parameter set to truthy value, we invoke the real `window.Video`
// on all the stored elements so far.
if
(
processTempCallStack
)
{
$
.
each
(
tempCallStack
,
function
(
index
,
el
ement
)
{
$
.
each
(
tempCallStack
,
function
(
index
,
el
)
{
// By now, `window.Video` is the real constructor.
window
.
Video
(
el
ement
);
window
.
Video
(
el
);
});
return
;
...
...
@@ -54,6 +55,7 @@
'video/09_events_plugin.js'
,
'video/09_events_bumper_plugin.js'
,
'video/09_poster.js'
,
'video/09_completion.js'
,
'video/10_commands.js'
,
'video/095_video_context_menu.js'
],
...
...
@@ -61,8 +63,8 @@
VideoStorage
,
initialize
,
FocusGrabber
,
VideoAccessibleMenu
,
VideoControl
,
VideoFullScreen
,
VideoQualityControl
,
VideoProgressSlider
,
VideoVolumeControl
,
VideoSpeedControl
,
VideoCaption
,
VideoPlayPlaceholder
,
VideoPlayPauseControl
,
VideoPlaySkipControl
,
VideoSkipControl
,
VideoBumper
,
VideoSaveStatePlugin
,
VideoEventsPlugin
,
VideoEventsBumperPlugin
,
VideoPoster
,
VideoCommands
,
VideoContextMenu
VideoSaveStatePlugin
,
VideoEventsPlugin
,
VideoEventsBumperPlugin
,
VideoPoster
,
VideoCo
mpletionHandler
,
VideoCommands
,
VideoCo
ntextMenu
)
{
var
youtubeXhr
=
null
,
oldVideo
=
window
.
Video
;
...
...
@@ -75,9 +77,10 @@
mainVideoModules
=
[
FocusGrabber
,
VideoControl
,
VideoPlayPlaceholder
,
VideoPlayPauseControl
,
VideoProgressSlider
,
VideoSpeedControl
,
VideoVolumeControl
,
VideoQualityControl
,
VideoFullScreen
,
VideoCaption
,
VideoCommands
,
VideoContextMenu
,
VideoSaveStatePlugin
,
VideoEventsPlugin
],
VideoSaveStatePlugin
,
VideoEventsPlugin
,
VideoCompletionHandler
],
bumperVideoModules
=
[
VideoControl
,
VideoPlaySkipControl
,
VideoSkipControl
,
VideoVolumeControl
,
VideoCaption
,
VideoCommands
,
VideoSaveStatePlugin
,
VideoEventsBumperPlugin
],
VideoVolumeControl
,
VideoCaption
,
VideoCommands
,
VideoSaveStatePlugin
,
VideoEventsBumperPlugin
,
VideoCompletionHandler
],
state
=
{
el
:
el
,
id
:
id
,
...
...
@@ -104,10 +107,10 @@
return
bumperState
;
};
var
player
=
function
(
s
tate
)
{
var
player
=
function
(
innerS
tate
)
{
return
function
()
{
_
.
extend
(
s
tate
.
metadata
,
{
autoplay
:
true
,
focusFirstControl
:
true
});
initialize
(
s
tate
,
element
);
_
.
extend
(
innerS
tate
.
metadata
,
{
autoplay
:
true
,
focusFirstControl
:
true
});
initialize
(
innerS
tate
,
element
);
};
};
...
...
@@ -120,8 +123,8 @@
new
VideoPoster
(
el
,
{
poster
:
el
.
data
(
'poster'
),
onClick
:
_
.
once
(
function
()
{
var
mainVideoPlayer
=
player
(
state
)
,
bumper
,
bumperState
;
var
mainVideoPlayer
=
player
(
state
)
;
var
bumper
,
bumperState
;
if
(
storage
.
getItem
(
'isBumperShown'
))
{
mainVideoPlayer
();
}
else
{
...
...
common/lib/xmodule/xmodule/video_module/bumper_utils.py
View file @
82ad9295
"""
Utils for video bumper
"""
from
collections
import
OrderedDict
import
copy
import
json
import
pytz
import
logging
from
collections
import
OrderedDict
from
datetime
import
datetime
,
timedelta
from
django.conf
import
settings
import
pytz
from
.video_utils
import
set_query_parameter
...
...
@@ -137,6 +137,9 @@ def bumper_metadata(video, sources):
'transcriptAvailableTranslationsUrl'
:
set_query_parameter
(
video
.
runtime
.
handler_url
(
video
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'is_bumper'
,
1
),
'publishCompletionUrl'
:
set_query_parameter
(
video
.
runtime
.
handler_url
(
video
,
'publish_completion'
,
''
)
.
rstrip
(
'?'
),
'is_bumper'
,
1
),
})
return
metadata
common/lib/xmodule/xmodule/video_module/video_handlers.py
View file @
82ad9295
...
...
@@ -13,6 +13,7 @@ from datetime import datetime
from
webob
import
Response
from
xblock.core
import
XBlock
from
xblock.exceptions
import
JsonHandlerError
from
xmodule.exceptions
import
NotFoundError
from
xmodule.fields
import
RelativeTime
...
...
@@ -202,6 +203,33 @@ class VideoStudentViewHandlers(object):
)
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
(
self
,
'completion'
)
if
completion_service
is
None
:
raise
JsonHandlerError
(
500
,
u"No completion service found"
)
elif
not
completion_service
.
completion_tracking_enabled
():
raise
JsonHandlerError
(
404
,
u"Completion tracking is not enabled and API calls are unexpected"
)
if
not
isinstance
(
data
[
'completion'
],
(
int
,
float
)):
message
=
u"Invalid completion value {}. Must be a float in range [0.0, 1.0]"
raise
JsonHandlerError
(
400
,
message
.
format
(
data
[
'completion'
]))
elif
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
def
transcript
(
self
,
request
,
dispatch
):
"""
...
...
@@ -282,6 +310,8 @@ class VideoStudentViewHandlers(object):
transcript_content
,
transcript_filename
,
transcript_mime_type
=
self
.
get_transcript
(
transcripts
,
transcript_format
=
self
.
transcript_download_format
,
lang
=
lang
)
except
(
KeyError
,
UnicodeDecodeError
):
return
Response
(
status
=
404
)
except
(
ValueError
,
NotFoundError
):
response
=
Response
(
status
=
404
)
# Check for transcripts in edx-val as a last resort if corresponding feature is enabled.
...
...
@@ -319,8 +349,6 @@ class VideoStudentViewHandlers(object):
response
.
content_type
=
Transcript
.
mime_types
[
self
.
transcript_download_format
]
return
response
except
(
KeyError
,
UnicodeDecodeError
):
return
Response
(
status
=
404
)
else
:
response
=
Response
(
transcript_content
,
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
82ad9295
...
...
@@ -27,6 +27,7 @@ from opaque_keys.edx.locator import AssetLocator
from
openedx.core.djangoapps.video_config.models
import
HLSPlaybackEnabledFlag
from
openedx.core.lib.cache_utils
import
memoize_in_request_cache
from
openedx.core.lib.license
import
LicenseMixin
from
xblock.completable
import
XBlockCompletionMode
from
xblock.core
import
XBlock
from
xblock.fields
import
ScopeIds
from
xblock.runtime
import
KvsFieldData
...
...
@@ -97,7 +98,7 @@ log = logging.getLogger(__name__)
_
=
lambda
text
:
text
@XBlock.wants
(
'settings'
)
@XBlock.wants
(
'settings'
,
'completion'
)
class
VideoModule
(
VideoFields
,
VideoTranscriptsMixin
,
VideoStudentViewHandlers
,
XModule
,
LicenseMixin
):
"""
XML source example:
...
...
@@ -110,6 +111,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
</video>
"""
has_custom_completion
=
True
completion_mode
=
XBlockCompletionMode
.
COMPLETABLE
video_time
=
0
icon_class
=
'video'
...
...
@@ -150,9 +154,10 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
resource_string
(
module
,
'js/src/video/09_events_plugin.js'
),
resource_string
(
module
,
'js/src/video/09_events_bumper_plugin.js'
),
resource_string
(
module
,
'js/src/video/09_poster.js'
),
resource_string
(
module
,
'js/src/video/09_completion.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_main.js'
)
resource_string
(
module
,
'js/src/video/10_main.js'
)
,
]
}
css
=
{
'scss'
:
[
...
...
@@ -327,6 +332,12 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
edx_video_id
=
self
.
edx_video_id
.
strip
()
)
completion_service
=
self
.
runtime
.
service
(
self
,
'completion'
)
if
completion_service
:
completion_enabled
=
completion_service
.
completion_tracking_enabled
()
else
:
completion_enabled
=
False
metadata
=
{
'saveStateUrl'
:
self
.
system
.
ajax_url
+
'/save_user_state'
,
'autoplay'
:
settings
.
FEATURES
.
get
(
'AUTOPLAY_VIDEOS'
,
False
),
...
...
@@ -345,6 +356,8 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
'savedVideoPosition'
:
self
.
saved_video_position
.
total_seconds
(),
'start'
:
self
.
start_time
.
total_seconds
(),
'end'
:
self
.
end_time
.
total_seconds
(),
'completionEnabled'
:
completion_enabled
,
'completionPercentage'
:
settings
.
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
,
'transcriptLanguage'
:
transcript_language
,
'transcriptLanguages'
:
sorted_languages
,
'ytTestTimeout'
:
settings
.
YOUTUBE
[
'TEST_TIMEOUT'
],
...
...
@@ -358,18 +371,19 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
'transcriptAvailableTranslationsUrl'
:
self
.
runtime
.
handler_url
(
self
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
## For now, the option "data-autohide-html5" is hard coded. This option
## either enables or disables autohiding of controls and captions on mouse
## inactivity. If set to true, controls and captions will autohide for
## HTML5 sources (non-YouTube) after a period of mouse inactivity over the
## whole video. When the mouse moves (or a key is pressed while any part of
## the video player is focused), the captions and controls will be shown
## once again.
##
## There is no option in the "Advanced Editor" to set this option. However,
## this option will have an effect if changed to "True". The code on
## front-end exists.
'publishCompletionUrl'
:
self
.
runtime
.
handler_url
(
self
,
'publish_completion'
,
''
)
.
rstrip
(
'?'
),
# For now, the option "data-autohide-html5" is hard coded. This option
# either enables or disables autohiding of controls and captions on mouse
# inactivity. If set to true, controls and captions will autohide for
# HTML5 sources (non-YouTube) after a period of mouse inactivity over the
# whole video. When the mouse moves (or a key is pressed while any part of
# the video player is focused), the captions and controls will be shown
# once again.
#
# There is no option in the "Advanced Editor" to set this option. However,
# this option will have an effect if changed to "True". The code on
# front-end exists.
'autohideHtml5'
:
False
,
# This is the server's guess at whether youtube is available for
...
...
@@ -399,8 +413,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
return
self
.
system
.
render_template
(
'video.html'
,
context
)
@XBlock.wants
(
"request_cache"
)
@XBlock.wants
(
"settings"
)
@XBlock.wants
(
"request_cache"
,
"settings"
,
"completion"
)
class
VideoDescriptor
(
VideoFields
,
VideoTranscriptsMixin
,
VideoStudioViewHandlers
,
TabsEditingDescriptor
,
EmptyDataRawDescriptor
,
LicenseMixin
):
"""
...
...
@@ -408,6 +421,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
"""
module_class
=
VideoModule
transcript
=
module_attr
(
'transcript'
)
publish_completion
=
module_attr
(
'publish_completion'
)
show_in_read_only_mode
=
True
...
...
lms/djangoapps/courseware/tests/test_video_handlers.py
View file @
82ad9295
...
...
@@ -183,6 +183,14 @@ class TestVideo(BaseTestXmodule):
response
=
self
.
item_descriptor
.
handle_ajax
(
'save_user_state'
,
{
u'demoo�'
:
"sample"
})
self
.
assertEqual
(
json
.
loads
(
response
)[
'success'
],
True
)
def
get_handler_url
(
self
,
handler
,
suffix
):
"""
Return the URL for the specified handler on self.item_descriptor.
"""
return
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
handler
,
suffix
)
.
rstrip
(
'/?'
)
def
tearDown
(
self
):
_clear_assets
(
self
.
item_descriptor
.
location
)
super
(
TestVideo
,
self
)
.
tearDown
()
...
...
lms/djangoapps/courseware/tests/test_video_mongo.py
View file @
82ad9295
...
...
@@ -84,14 +84,13 @@ class TestVideoYouTube(TestVideo):
'ytApiUrl'
:
'https://www.youtube.com/iframe_api'
,
'ytMetadataUrl'
:
'https://www.googleapis.com/youtube/v3/videos/'
,
'ytKey'
:
None
,
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'autohideHtml5'
:
False
,
'recordedYoutubeIsAvailable'
:
True
,
'completionEnabled'
:
False
,
'completionPercentage'
:
0.95
,
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
})),
'track'
:
None
,
'transcript_download_format'
:
u'srt'
,
...
...
@@ -165,14 +164,13 @@ class TestVideoNonYouTube(TestVideo):
'ytApiUrl'
:
'https://www.youtube.com/iframe_api'
,
'ytMetadataUrl'
:
'https://www.googleapis.com/youtube/v3/videos/'
,
'ytKey'
:
None
,
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'autohideHtml5'
:
False
,
'recordedYoutubeIsAvailable'
:
True
,
'completionEnabled'
:
False
,
'completionPercentage'
:
0.95
,
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
})),
'track'
:
None
,
'transcript_download_format'
:
u'srt'
,
...
...
@@ -223,16 +221,24 @@ class TestGetHtmlMethod(BaseTestXmodule):
'ytApiUrl'
:
'https://www.youtube.com/iframe_api'
,
'ytMetadataUrl'
:
'https://www.googleapis.com/youtube/v3/videos/'
,
'ytKey'
:
None
,
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'autohideHtml5'
:
False
,
'recordedYoutubeIsAvailable'
:
True
,
'completionEnabled'
:
False
,
'completionPercentage'
:
0.95
,
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
})
def
get_handler_url
(
self
,
handler
,
suffix
):
"""
Return the URL for the specified handler on the block represented by
self.item_descriptor.
"""
return
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
handler
,
suffix
)
.
rstrip
(
'/?'
)
def
test_get_html_track
(
self
):
SOURCE_XML
=
"""
<video show_captions="true"
...
...
@@ -318,20 +324,15 @@ class TestGetHtmlMethod(BaseTestXmodule):
)
self
.
initialize_module
(
data
=
DATA
)
track_url
=
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'download'
)
.
rstrip
(
'/?'
)
track_url
=
self
.
get_handler_url
(
'transcript'
,
'download'
)
context
=
self
.
item_descriptor
.
render
(
STUDENT_VIEW
)
.
content
metadata
.
update
({
'transcriptLanguages'
:
{
"en"
:
"English"
}
if
not
data
[
'transcripts'
]
else
{
"uk"
:
u'Українська'
},
'transcriptLanguage'
:
u'en'
if
not
data
[
'transcripts'
]
or
data
.
get
(
'sub'
)
else
u'uk'
,
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
'saveStateUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
+
'/save_user_state'
,
'sub'
:
data
[
'sub'
],
})
...
...
@@ -441,12 +442,9 @@ class TestGetHtmlMethod(BaseTestXmodule):
expected_context
=
dict
(
initial_context
)
expected_context
[
'metadata'
]
.
update
({
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
'saveStateUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
+
'/save_user_state'
,
'sources'
:
data
[
'result'
]
.
get
(
'sources'
,
[]),
})
...
...
@@ -581,12 +579,9 @@ class TestGetHtmlMethod(BaseTestXmodule):
expected_context
=
dict
(
initial_context
)
expected_context
[
'metadata'
]
.
update
({
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
'saveStateUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
+
'/save_user_state'
,
'sources'
:
data
[
'result'
][
'sources'
],
})
...
...
@@ -742,12 +737,9 @@ class TestGetHtmlMethod(BaseTestXmodule):
# expected_context, expected context to be returned by get_html
expected_context
=
dict
(
initial_context
)
expected_context
[
'metadata'
]
.
update
({
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
'saveStateUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
+
'/save_user_state'
,
'sources'
:
data
[
'result'
][
'sources'
],
})
...
...
@@ -854,12 +846,9 @@ class TestGetHtmlMethod(BaseTestXmodule):
context
=
self
.
item_descriptor
.
render
(
'student_view'
)
.
content
expected_context
=
dict
(
initial_context
)
expected_context
[
'metadata'
]
.
update
({
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
'saveStateUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
+
'/save_user_state'
,
'sources'
:
data
[
'result'
]
.
get
(
'sources'
,
[]),
})
...
...
@@ -1774,14 +1763,13 @@ class TestVideoWithBumper(TestVideo):
'transcriptLanguage'
:
'en'
,
'transcriptLanguages'
:
{
'en'
:
'English'
},
'transcriptTranslationUrl'
:
video_utils
.
set_query_parameter
(
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'is_bumper'
,
1
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'is_bumper'
,
1
),
'transcriptAvailableTranslationsUrl'
:
video_utils
.
set_query_parameter
(
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'is_bumper'
,
1
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'is_bumper'
,
1
),
"publishCompletionUrl"
:
video_utils
.
set_query_parameter
(
self
.
get_handler_url
(
'publish_completion'
,
''
),
'is_bumper'
,
1
),
})),
'cdn_eval'
:
False
,
...
...
@@ -1811,14 +1799,13 @@ class TestVideoWithBumper(TestVideo):
'ytApiUrl'
:
'https://www.youtube.com/iframe_api'
,
'ytMetadataUrl'
:
'https://www.googleapis.com/youtube/v3/videos/'
,
'ytKey'
:
None
,
'transcriptTranslationUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'translation/__lang__'
)
.
rstrip
(
'/?'
),
'transcriptAvailableTranslationsUrl'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_descriptor
,
'transcript'
,
'available_translations'
)
.
rstrip
(
'/?'
),
'transcriptTranslationUrl'
:
self
.
get_handler_url
(
'transcript'
,
'translation/__lang__'
),
'transcriptAvailableTranslationsUrl'
:
self
.
get_handler_url
(
'transcript'
,
'available_translations'
),
'autohideHtml5'
:
False
,
'recordedYoutubeIsAvailable'
:
True
,
'completionEnabled'
:
False
,
'completionPercentage'
:
0.95
,
'publishCompletionUrl'
:
self
.
get_handler_url
(
'publish_completion'
,
''
),
})),
'track'
:
None
,
'transcript_download_format'
:
u'srt'
,
...
...
lms/envs/aws.py
View file @
82ad9295
...
...
@@ -1088,6 +1088,15 @@ EDX_PLATFORM_REVISION = ENV_TOKENS.get('EDX_PLATFORM_REVISION', EDX_PLATFORM_REV
# Allow extra middleware classes to be added to the app through configuration.
MIDDLEWARE_CLASSES
.
extend
(
ENV_TOKENS
.
get
(
'EXTRA_MIDDLEWARE_CLASSES'
,
[]))
########################## Settings for Completion API #####################
# Once a user has watched this percentage of a video, mark it as complete:
# (0.0 = 0%, 1.0 = 100%)
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
=
ENV_TOKENS
.
get
(
'COMPLETION_VIDEO_COMPLETE_PERCENTAGE'
,
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
,
)
########################## Derive Any Derived Settings #######################
derive_settings
(
__name__
)
lms/envs/common.py
View file @
82ad9295
...
...
@@ -3456,3 +3456,9 @@ ACE_ROUTING_KEY = LOW_PRIORITY_QUEUE
# Initialize to 'unknown', but read from JSON in aws.py
EDX_PLATFORM_REVISION
=
'unknown'
############## Settings for Completion API #########################
# Once a user has watched this percentage of a video, mark it as complete:
# (0.0 = 0%, 1.0 = 100%)
COMPLETION_VIDEO_COMPLETE_PERCENTAGE
=
0.95
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment