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
538a3d78
Commit
538a3d78
authored
Sep 11, 2017
by
Mushtaq Ali
Committed by
muzaffaryousaf
Oct 16, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add video transcript config model flags - EDUCATOR-1224
parent
2203de4a
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
503 additions
and
114 deletions
+503
-114
cms/djangoapps/contentstore/views/tests/test_videos.py
+113
-32
cms/djangoapps/contentstore/views/videos.py
+18
-7
cms/static/js/factories/videos_index.js
+2
-0
cms/static/js/spec/views/active_video_upload_list_spec.js
+34
-22
cms/static/js/views/active_video_upload_list.js
+5
-0
cms/static/js/views/course_video_settings.js
+18
-0
cms/templates/js/course-video-settings.underscore
+3
-1
cms/templates/videos_index.html
+3
-0
openedx/core/djangoapps/video_config/admin.py
+31
-5
openedx/core/djangoapps/video_config/forms.py
+25
-5
openedx/core/djangoapps/video_config/migrations/0002_coursevideotranscriptenabledflag_videotranscriptenabledflag.py
+46
-0
openedx/core/djangoapps/video_config/models.py
+65
-0
openedx/core/djangoapps/video_config/tests/test_models.py
+140
-42
No files found.
cms/djangoapps/contentstore/views/tests/test_videos.py
View file @
538a3d78
...
...
@@ -554,6 +554,22 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
self
.
assert_video_status
(
url
,
edx_video_id
,
'Failed'
)
@ddt.data
(
True
,
False
)
@patch
(
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled'
)
def
test_video_index_transcript_feature_enablement
(
self
,
is_video_transcript_enabled
,
video_transcript_feature
):
"""
Test that when video transcript is enabled/disabled, correct response is rendered.
"""
video_transcript_feature
.
return_value
=
is_video_transcript_enabled
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that course video button is present in the response if videos transcript feature is enabled.
self
.
assertEqual
(
'<button class="button course-video-settings-button">'
in
response
.
content
,
is_video_transcript_enabled
)
@ddt.ddt
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_VIDEO_UPLOAD_PIPELINE'
:
True
})
...
...
@@ -845,7 +861,10 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
edx_video_id
=
'test1'
video_image_upload_url
=
self
.
get_url_for_course_key
(
self
.
course
.
id
,
{
'edx_video_id'
:
edx_video_id
})
with
make_image_file
(
dimensions
=
(
image_data
.
get
(
'width'
,
settings
.
VIDEO_IMAGE_MIN_WIDTH
),
image_data
.
get
(
'height'
,
settings
.
VIDEO_IMAGE_MIN_HEIGHT
)),
dimensions
=
(
image_data
.
get
(
'width'
,
settings
.
VIDEO_IMAGE_MIN_WIDTH
),
image_data
.
get
(
'height'
,
settings
.
VIDEO_IMAGE_MIN_HEIGHT
)
),
prefix
=
image_data
.
get
(
'prefix'
,
'videoimage'
),
extension
=
image_data
.
get
(
'extension'
,
'.png'
),
force_size
=
image_data
.
get
(
'size'
,
settings
.
VIDEO_IMAGE_SETTINGS
[
'VIDEO_IMAGE_MIN_BYTES'
])
...
...
@@ -858,6 +877,10 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
@ddt.ddt
@patch
(
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled'
,
Mock
(
return_value
=
True
)
)
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_VIDEO_UPLOAD_PIPELINE'
:
True
})
class
TranscriptPreferencesTestCase
(
VideoUploadTestBase
,
CourseTestCase
):
"""
...
...
@@ -867,35 +890,52 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
VIEW_NAME
=
'transcript_preferences_handler'
@ddt.data
(
# Video transcript feature disabled
(
{},
False
,
''
,
404
,
),
# Error cases
(
{},
'Invalid provider.'
True
,
'Invalid provider.'
,
400
),
(
{
'provider'
:
''
},
'Invalid provider.'
True
,
'Invalid provider.'
,
400
),
(
{
'provider'
:
'dummy-provider'
},
'Invalid provider.'
True
,
'Invalid provider.'
,
400
),
(
{
'provider'
:
TranscriptProvider
.
CIELO24
},
'Invalid cielo24 fidelity.'
True
,
'Invalid cielo24 fidelity.'
,
400
),
(
{
'provider'
:
TranscriptProvider
.
CIELO24
,
'cielo24_fidelity'
:
'PROFESSIONAL'
,
},
'Invalid cielo24 turnaround.'
True
,
'Invalid cielo24 turnaround.'
,
400
),
(
{
...
...
@@ -903,7 +943,9 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
'cielo24_fidelity'
:
'PROFESSIONAL'
,
'cielo24_turnaround'
:
'STANDARD'
},
'Invalid languages.'
True
,
'Invalid languages.'
,
400
),
(
{
...
...
@@ -912,20 +954,26 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
'cielo24_turnaround'
:
'STANDARD'
,
'preferred_languages'
:
[
'es'
,
'ur'
]
},
'Invalid languages.'
True
,
'Invalid languages.'
,
400
),
(
{
'provider'
:
TranscriptProvider
.
THREE_PLAY_MEDIA
},
'Invalid 3play turnaround.'
True
,
'Invalid 3play turnaround.'
,
400
),
(
{
'provider'
:
TranscriptProvider
.
THREE_PLAY_MEDIA
,
'three_play_turnaround'
:
'default'
},
'Invalid languages.'
True
,
'Invalid languages.'
,
400
),
(
{
...
...
@@ -933,7 +981,9 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
'three_play_turnaround'
:
'default'
,
'preferred_languages'
:
[
'es'
,
'ur'
]
},
'Invalid languages.'
True
,
'Invalid languages.'
,
400
),
# Success
(
...
...
@@ -943,7 +993,9 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
'cielo24_turnaround'
:
'STANDARD'
,
'preferred_languages'
:
[
'en'
]
},
''
True
,
''
,
200
),
(
{
...
...
@@ -951,37 +1003,45 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
'three_play_turnaround'
:
'default'
,
'preferred_languages'
:
[
'en'
]
},
''
True
,
''
,
200
)
)
@ddt.unpack
def
test_video_transcript
(
self
,
preferences
,
error_messag
e
):
def
test_video_transcript
(
self
,
preferences
,
is_video_transcript_enabled
,
error_message
,
expected_status_cod
e
):
"""
Tests that transcript handler works correctly.
"""
video_transcript_url
=
self
.
get_url_for_course_key
(
self
.
course
.
id
)
preferences_data
=
{
'provider'
:
preferences
.
get
(
'provider'
,
''
),
'cielo24_fidelity'
:
preferences
.
get
(
'cielo24_fidelity'
,
''
),
'cielo24_turnaround'
:
preferences
.
get
(
'cielo24_turnaround'
,
''
),
'three_play_turnaround'
:
preferences
.
get
(
'three_play_turnaround'
,
''
),
'provider'
:
preferences
.
get
(
'provider'
),
'cielo24_fidelity'
:
preferences
.
get
(
'cielo24_fidelity'
),
'cielo24_turnaround'
:
preferences
.
get
(
'cielo24_turnaround'
),
'three_play_turnaround'
:
preferences
.
get
(
'three_play_turnaround'
),
'preferred_languages'
:
preferences
.
get
(
'preferred_languages'
,
[]),
}
with
patch
(
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled'
)
as
video_transcript_feature
:
video_transcript_feature
.
return_value
=
is_video_transcript_enabled
response
=
self
.
client
.
post
(
video_transcript_url
,
json
.
dumps
(
preferences_data
),
content_type
=
'application/json'
)
status_code
=
response
.
status_code
response
=
json
.
loads
(
response
.
content
)
response
=
json
.
loads
(
response
.
content
)
if
is_video_transcript_enabled
else
response
if
error_message
:
self
.
assertEqual
(
status_code
,
400
)
self
.
assertEqual
(
response
[
'error'
],
error_message
)
else
:
self
.
assertEqual
(
status_code
,
200
)
self
.
assertTrue
(
response
[
'transcript_preferences'
],
preferences_data
)
self
.
assertEqual
(
status_code
,
expected_status_code
)
self
.
assertEqual
(
response
.
get
(
'error'
,
''
),
error_message
)
# Remove modified and course_id fields from the response so as to check the expected transcript preferences.
response
.
get
(
'transcript_preferences'
,
{})
.
pop
(
'modified'
,
None
)
response
.
get
(
'transcript_preferences'
,
{})
.
pop
(
'course_id'
,
None
)
expected_preferences
=
preferences_data
if
is_video_transcript_enabled
and
not
error_message
else
{}
self
.
assertDictEqual
(
response
.
get
(
'transcript_preferences'
,
{}),
expected_preferences
)
def
test_remove_transcript_preferences
(
self
):
"""
...
...
@@ -1008,7 +1068,7 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
"""
Test that transcript handler works correctly even when no preferences are found.
"""
course_id
=
'dummy+course+id'
course_id
=
'
course-v1:
dummy+course+id'
# Verify transcript preferences do not exist
preferences
=
get_transcript_preferences
(
course_id
)
self
.
assertIsNone
(
preferences
)
...
...
@@ -1024,23 +1084,39 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
self
.
assertIsNone
(
preferences
)
@ddt.data
(
(
None
,
False
),
(
{
'provider'
:
TranscriptProvider
.
CIELO24
,
'cielo24_fidelity'
:
'PROFESSIONAL'
,
'cielo24_turnaround'
:
'STANDARD'
,
'preferred_languages'
:
[
'en'
]
}
},
False
),
(
{
'provider'
:
TranscriptProvider
.
CIELO24
,
'cielo24_fidelity'
:
'PROFESSIONAL'
,
'cielo24_turnaround'
:
'STANDARD'
,
'preferred_languages'
:
[
'en'
]
},
True
)
)
@ddt.unpack
@override_settings
(
AWS_ACCESS_KEY_ID
=
'test_key_id'
,
AWS_SECRET_ACCESS_KEY
=
'test_secret'
)
@patch
(
'boto.s3.key.Key'
)
@patch
(
'boto.s3.connection.S3Connection'
)
@patch
(
'contentstore.views.videos.get_transcript_preferences'
)
def
test_transcript_preferences_metadata
(
self
,
transcript_preferences
,
mock_transcript_preferences
,
mock_conn
,
mock_key
):
def
test_transcript_preferences_metadata
(
self
,
transcript_preferences
,
is_video_transcript_enabled
,
mock_
transcript_preferences
,
mock_
conn
,
mock_key
):
"""
Tests that transcript preference metadata is only set if it is
transcript
preferences are present in request data
.
Tests that transcript preference metadata is only set if it is
video transcript feature is enabled and
transcript preferences are already stored in the system
.
"""
file_name
=
'test-video.mp4'
request_data
=
{
'files'
:
[{
'file_name'
:
file_name
,
'content_type'
:
'video/mp4'
}]}
...
...
@@ -1058,11 +1134,16 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
mock_key
.
side_effect
=
[
mock_key_instance
]
+
[
Mock
()]
videos_handler_url
=
reverse_course_url
(
'videos_handler'
,
self
.
course
.
id
)
with
patch
(
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled'
)
as
video_transcript_feature
:
video_transcript_feature
.
return_value
=
is_video_transcript_enabled
response
=
self
.
client
.
post
(
videos_handler_url
,
json
.
dumps
(
request_data
),
content_type
=
'application/json'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Ensure `transcript_preferences` was set up in Key correctly if sent through request.
if
transcript_preferences
:
if
is_video_transcript_enabled
and
transcript_preferences
:
mock_key_instance
.
set_metadata
.
assert_any_call
(
'transcript_preferences'
,
json
.
dumps
(
transcript_preferences
))
else
:
with
self
.
assertRaises
(
AssertionError
):
...
...
cms/djangoapps/contentstore/views/videos.py
View file @
538a3d78
...
...
@@ -33,6 +33,7 @@ from edxval.api import (
remove_transcript_preferences
,
)
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.video_config.models
import
VideoTranscriptEnabledFlag
from
openedx.core.djangoapps.waffle_utils
import
WaffleSwitchNamespace
from
contentstore.models
import
VideoUploadConfig
...
...
@@ -129,7 +130,7 @@ class StatusDisplayStrings(object):
"invalid_token"
:
_INVALID_TOKEN
,
"imported"
:
_IMPORTED
,
"transcription_in_progress"
:
_TRANSCRIPTION_IN_PROGRESS
,
"transcript
ion
_ready"
:
_TRANSCRIPT_READY
,
"transcript_ready"
:
_TRANSCRIPT_READY
,
}
@staticmethod
...
...
@@ -339,6 +340,11 @@ def transcript_preferences_handler(request, course_key_string):
Returns: valid json response or 400 with error message
"""
course_key
=
CourseKey
.
from_string
(
course_key_string
)
is_video_transcript_enabled
=
VideoTranscriptEnabledFlag
.
feature_enabled
(
course_key
)
if
not
is_video_transcript_enabled
:
return
HttpResponseNotFound
()
if
request
.
method
==
'POST'
:
data
=
request
.
json
provider
=
data
.
get
(
'provider'
)
...
...
@@ -550,6 +556,7 @@ def videos_index_html(course):
"""
Returns an HTML page to display previous video uploads and allow new ones
"""
is_video_transcript_enabled
=
VideoTranscriptEnabledFlag
.
feature_enabled
(
course
.
id
)
context
=
{
'context_course'
:
course
,
'image_upload_url'
:
reverse_course_url
(
'video_images_handler'
,
unicode
(
course
.
id
)),
...
...
@@ -567,19 +574,21 @@ def videos_index_html(course):
'max_width'
:
settings
.
VIDEO_IMAGE_MAX_WIDTH
,
'max_height'
:
settings
.
VIDEO_IMAGE_MAX_HEIGHT
,
'supported_file_formats'
:
settings
.
VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
}
},
'is_video_transcript_enabled'
:
is_video_transcript_enabled
,
'video_transcript_settings'
:
None
,
'active_transcript_preferences'
:
None
}
context
.
update
({
'video_transcript_settings'
:
{
if
is_video_transcript_enabled
:
context
[
'video_transcript_settings'
]
=
{
'transcript_preferences_handler_url'
:
reverse_course_url
(
'transcript_preferences_handler'
,
unicode
(
course
.
id
)
),
'transcription_plans'
:
get_3rd_party_transcription_plans
(),
},
'active_transcript_preferences'
:
get_transcript_preferences
(
unicode
(
course
.
id
))
})
}
context
[
'active_transcript_preferences'
]
=
get_transcript_preferences
(
unicode
(
course
.
id
))
return
render_to_response
(
'videos_index.html'
,
context
)
...
...
@@ -662,6 +671,8 @@ def videos_post(course, request):
(
'course_key'
,
unicode
(
course
.
id
)),
]
is_video_transcript_enabled
=
VideoTranscriptEnabledFlag
.
feature_enabled
(
course
.
id
)
if
is_video_transcript_enabled
:
transcript_preferences
=
get_transcript_preferences
(
unicode
(
course
.
id
))
if
transcript_preferences
is
not
None
:
metadata_list
.
append
((
'transcript_preferences'
,
json
.
dumps
(
transcript_preferences
)))
...
...
cms/static/js/factories/videos_index.js
View file @
538a3d78
...
...
@@ -16,6 +16,7 @@ define([
videoUploadMaxFileSizeInGB
,
activeTranscriptPreferences
,
videoTranscriptSettings
,
isVideoTranscriptEnabled
,
videoImageSettings
)
{
var
activeView
=
new
ActiveVideoUploadListView
({
...
...
@@ -27,6 +28,7 @@ define([
videoImageSettings
:
videoImageSettings
,
activeTranscriptPreferences
:
activeTranscriptPreferences
,
videoTranscriptSettings
:
videoTranscriptSettings
,
isVideoTranscriptEnabled
:
isVideoTranscriptEnabled
,
onFileUploadDone
:
function
(
activeVideos
)
{
$
.
ajax
({
url
:
videoHandlerUrl
,
...
...
cms/static/js/spec/views/active_video_upload_list_spec.js
View file @
538a3d78
...
...
@@ -20,6 +20,10 @@ define(
fail
:
'upload_failed'
,
success
:
'upload_completed'
},
videoUploadMaxFileSizeInGB
=
5
,
videoSupportedFileFormats
=
[
'.mp4'
,
'.mov'
],
createActiveUploadListView
,
$courseVideoSettingsButton
,
makeUploadUrl
,
getSentRequests
,
verifyUploadViewInfo
,
...
...
@@ -29,6 +33,22 @@ define(
verifyA11YMessage
,
verifyUploadPostRequest
;
createActiveUploadListView
=
function
(
isVideoTranscriptEnabled
)
{
return
new
ActiveVideoUploadListView
({
concurrentUploadLimit
:
concurrentUploadLimit
,
postUrl
:
POST_URL
,
courseVideoSettingsButton
:
$courseVideoSettingsButton
,
videoSupportedFileFormats
:
videoSupportedFileFormats
,
videoUploadMaxFileSizeInGB
:
videoUploadMaxFileSizeInGB
,
activeTranscriptPreferences
:
{},
videoTranscriptSettings
:
{
transcript_preferences_handler_url
:
''
,
transcription_plans
:
{}
},
isVideoTranscriptEnabled
:
isVideoTranscriptEnabled
});
};
describe
(
'ActiveVideoUploadListView'
,
function
()
{
beforeEach
(
function
()
{
setFixtures
(
...
...
@@ -40,22 +60,8 @@ define(
);
TemplateHelpers
.
installTemplate
(
'active-video-upload'
);
TemplateHelpers
.
installTemplate
(
'active-video-upload-list'
);
this
.
postUrl
=
POST_URL
;
this
.
courseVideoSettingsButton
=
$
(
'.course-video-settings-button'
);
this
.
videoSupportedFileFormats
=
[
'.mp4'
,
'.mov'
];
this
.
videoUploadMaxFileSizeInGB
=
5
;
this
.
view
=
new
ActiveVideoUploadListView
({
concurrentUploadLimit
:
concurrentUploadLimit
,
postUrl
:
this
.
postUrl
,
courseVideoSettingsButton
:
this
.
courseVideoSettingsButton
,
videoSupportedFileFormats
:
this
.
videoSupportedFileFormats
,
videoUploadMaxFileSizeInGB
:
this
.
videoUploadMaxFileSizeInGB
,
activeTranscriptPreferences
:
{},
videoTranscriptSettings
:
{
transcript_preferences_handler_url
:
''
,
transcription_plans
:
{}
}
});
$courseVideoSettingsButton
=
$
(
'.course-video-settings-button'
);
this
.
view
=
createActiveUploadListView
(
true
);
this
.
view
.
render
();
jasmine
.
Ajax
.
install
();
});
...
...
@@ -94,9 +100,15 @@ define(
});
it
(
'shows course video settings pane when course video settings button is clicked'
,
function
()
{
expect
(
$
(
'.course-video-settings-container'
)).
not
.
toExist
();
this
.
courseVideoSettingsButton
.
click
();
expect
(
$
(
'.course-video-settings-container'
)).
toExist
();
$courseVideoSettingsButton
.
click
();
expect
(
this
.
view
.
courseVideoSettingsView
).
toBeDefined
();
expect
(
this
.
view
.
courseVideoSettingsView
.
$el
.
find
(
'.course-video-settings-container'
)).
toExist
();
});
it
(
'should not initiate course video settings view when video transcript is disabled'
,
function
()
{
this
.
view
=
createActiveUploadListView
(
false
);
$courseVideoSettingsButton
.
click
();
expect
(
this
.
view
.
courseVideoSettingsView
).
toBeUndefined
();
});
it
(
'should not show a notification message if there are no active video uploads'
,
function
()
{
...
...
@@ -328,7 +340,7 @@ define(
'Your file could not be uploaded'
,
StringUtils
.
interpolate
(
'{fileName} is not in a supported file format. Supported file formats are {supportedFormats}.'
,
// eslint-disable-line max-len
{
fileName
:
files
[
index
].
name
,
supportedFormats
:
self
.
videoSupportedFileFormats
.
join
(
' and '
)}
// eslint-disable-line max-len
{
fileName
:
files
[
index
].
name
,
supportedFormats
:
videoSupportedFileFormats
.
join
(
' and '
)}
// eslint-disable-line max-len
)
);
});
...
...
@@ -361,7 +373,7 @@ define(
verifyUploadViewInfo
(
uploadView
,
'Your file could not be uploaded'
,
'file.mp4 exceeds maximum size of '
+
this
.
videoUploadMaxFileSizeInGB
+
' GB.'
'file.mp4 exceeds maximum size of '
+
videoUploadMaxFileSizeInGB
+
' GB.'
);
verifyA11YMessage
(
StringUtils
.
interpolate
(
...
...
@@ -431,7 +443,7 @@ define(
expect
(
jasmine
.
Ajax
.
requests
.
count
()).
toEqual
(
caseInfo
.
numFiles
);
_
.
each
(
_
.
range
(
caseInfo
.
numFiles
),
function
(
index
)
{
request
=
jasmine
.
Ajax
.
requests
.
at
(
index
);
expect
(
request
.
url
).
toEqual
(
self
.
postUrl
);
expect
(
request
.
url
).
toEqual
(
POST_URL
);
expect
(
request
.
method
).
toEqual
(
'POST'
);
expect
(
request
.
requestHeaders
[
'Content-Type'
]).
toEqual
(
'application/json'
);
expect
(
request
.
requestHeaders
.
Accept
).
toContain
(
'application/json'
);
...
...
cms/static/js/views/active_video_upload_list.js
View file @
538a3d78
...
...
@@ -43,6 +43,7 @@ define([
this
.
postUrl
=
options
.
postUrl
;
this
.
activeTranscriptPreferences
=
options
.
activeTranscriptPreferences
;
this
.
videoTranscriptSettings
=
options
.
videoTranscriptSettings
;
this
.
isVideoTranscriptEnabled
=
options
.
isVideoTranscriptEnabled
;
this
.
videoSupportedFileFormats
=
options
.
videoSupportedFileFormats
;
this
.
videoUploadMaxFileSizeInGB
=
options
.
videoUploadMaxFileSizeInGB
;
this
.
onFileUploadDone
=
options
.
onFileUploadDone
;
...
...
@@ -62,6 +63,7 @@ define([
supportedVideoTypes
:
this
.
videoSupportedFileFormats
.
join
(
', '
)
}
);
if
(
this
.
isVideoTranscriptEnabled
)
{
this
.
listenTo
(
Backbone
,
'coursevideosettings:syncActiveTranscriptPreferences'
,
...
...
@@ -72,6 +74,7 @@ define([
'coursevideosettings:destroyCourseVideoSettingsView'
,
this
.
destroyCourseVideoSettingsView
);
}
},
syncActiveTranscriptPreferences
:
function
(
activeTranscriptPreferences
)
{
...
...
@@ -79,12 +82,14 @@ define([
},
showCourseVideoSettingsView
:
function
(
event
)
{
if
(
this
.
isVideoTranscriptEnabled
)
{
this
.
courseVideoSettingsView
=
new
CourseVideoSettingsView
({
activeTranscriptPreferences
:
this
.
activeTranscriptPreferences
,
videoTranscriptSettings
:
this
.
videoTranscriptSettings
});
this
.
courseVideoSettingsView
.
render
();
event
.
stopPropagation
();
}
},
destroyCourseVideoSettingsView
:
function
()
{
...
...
cms/static/js/views/course_video_settings.js
View file @
538a3d78
...
...
@@ -337,6 +337,22 @@ function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSett
},
updateSuccessResponseStatus
:
function
(
data
)
{
var
dateModified
=
data
?
moment
.
utc
(
data
.
modified
).
format
(
'll'
)
:
''
;
// Update last modified date
if
(
dateModified
)
{
HtmlUtils
.
setHtml
(
this
.
$el
.
find
(
'.last-updated-text'
),
HtmlUtils
.
interpolateHtml
(
HtmlUtils
.
HTML
(
'{lastUpdateText} {dateModified}'
),
{
lastUpdateText
:
gettext
(
'Last updated'
),
dateModified
:
dateModified
}
)
);
}
this
.
renderResponseStatus
(
gettext
(
'Settings updated'
),
'success'
);
// Sync ActiveUploadListView with latest active plan.
this
.
activeTranscriptionPlan
=
data
;
...
...
@@ -533,6 +549,8 @@ function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSett
},
closeCourseVideoSettings
:
function
()
{
// TODO: Slide out when closing settings pane. We may need to hide the view instead of destroying it.
// Trigger destroy transcript event.
Backbone
.
trigger
(
'coursevideosettings:destroyCourseVideoSettingsView'
);
...
...
cms/templates/js/course-video-settings.underscore
View file @
538a3d78
...
...
@@ -45,9 +45,11 @@
<%- gettext('Update Settings') %>
<span id='update-button-text' class='sr'><%-gettext('Press update settings to update course video settings') %></span>
</button>
<span class='last-updated-text'>
<%if (dateModified) { %>
<
span class='last-updated-text'><%- gettext('Last updated')%> <%- dateModified %></span
>
<
%- gettext('Last updated')%> <%- dateModified %
>
<% } %>
</span>
</div>
</div>
</div>
cms/templates/videos_index.html
View file @
538a3d78
...
...
@@ -40,6 +40,7 @@
${video_upload_max_file_size | n, dump_js_escaped_json},
${active_transcript_preferences | n, dump_js_escaped_json},
${video_transcript_settings | n, dump_js_escaped_json},
${is_video_transcript_enabled | n, dump_js_escaped_json},
${video_image_settings | n, dump_js_escaped_json}
);
});
...
...
@@ -55,12 +56,14 @@
<span
class=
"sr"
>
>
</span>
${_("Video Uploads")}
</h1>
% if is_video_transcript_enabled :
<nav
class=
"nav-actions"
aria-label=
"${_('Page Actions')}"
>
<h3
class=
"sr"
>
${_("Page Actions")}
</h3>
<div
class=
"nav-item"
>
<button
class=
"button course-video-settings-button"
><span
class=
"icon fa fa-cog"
aria-hidden=
"true"
></span>
${_("Course Video Settings")}
</button>
</div>
</nav>
% endif
</header>
</div>
...
...
openedx/core/djangoapps/video_config/admin.py
View file @
538a3d78
...
...
@@ -5,16 +5,24 @@ Django admin dashboard configuration for Video XModule.
from
config_models.admin
import
ConfigurationModelAdmin
,
KeyedConfigurationModelAdmin
from
django.contrib
import
admin
from
openedx.core.djangoapps.video_config.forms
import
CourseHLSPlaybackFlagAdminForm
from
openedx.core.djangoapps.video_config.models
import
CourseHLSPlaybackEnabledFlag
,
HLSPlaybackEnabledFlag
from
openedx.core.djangoapps.video_config.forms
import
(
CourseHLSPlaybackFlagAdminForm
,
CourseVideoTranscriptFlagAdminForm
)
from
openedx.core.djangoapps.video_config.models
import
(
CourseHLSPlaybackEnabledFlag
,
HLSPlaybackEnabledFlag
,
CourseVideoTranscriptEnabledFlag
,
VideoTranscriptEnabledFlag
)
class
Course
HLSPlaybackEnabledFlag
Admin
(
KeyedConfigurationModelAdmin
):
class
Course
SpecificEnabledFlagBase
Admin
(
KeyedConfigurationModelAdmin
):
"""
Admin of
HLS Playback
feature on course-by-course basis.
Admin of
course specific
feature on course-by-course basis.
Allows searching by course id.
"""
form
=
CourseHLSPlaybackFlagAdminForm
# Make abstract base class
class
Meta
(
object
):
abstract
=
True
search_fields
=
[
'course_id'
]
fieldsets
=
(
(
None
,
{
...
...
@@ -23,5 +31,23 @@ class CourseHLSPlaybackEnabledFlagAdmin(KeyedConfigurationModelAdmin):
}),
)
class
CourseHLSPlaybackEnabledFlagAdmin
(
CourseSpecificEnabledFlagBaseAdmin
):
"""
Admin of HLS Playback feature on course-by-course basis.
Allows searching by course id.
"""
form
=
CourseHLSPlaybackFlagAdminForm
class
CourseVideoTranscriptEnabledFlagAdmin
(
CourseSpecificEnabledFlagBaseAdmin
):
"""
Admin of Video Transcript feature on course-by-course basis.
Allows searching by course id.
"""
form
=
CourseHLSPlaybackFlagAdminForm
admin
.
site
.
register
(
HLSPlaybackEnabledFlag
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
CourseHLSPlaybackEnabledFlag
,
CourseHLSPlaybackEnabledFlagAdmin
)
admin
.
site
.
register
(
VideoTranscriptEnabledFlag
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
CourseVideoTranscriptEnabledFlag
,
CourseHLSPlaybackEnabledFlagAdmin
)
openedx/core/djangoapps/video_config/forms.py
View file @
538a3d78
...
...
@@ -7,20 +7,20 @@ from django import forms
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.locator
import
CourseLocator
from
openedx.core.djangoapps.video_config.models
import
CourseHLSPlaybackEnabledFlag
from
openedx.core.djangoapps.video_config.models
import
CourseHLSPlaybackEnabledFlag
,
CourseVideoTranscriptEnabledFlag
from
xmodule.modulestore.django
import
modulestore
log
=
logging
.
getLogger
(
__name__
)
class
Course
HLSPlaybackFlagAdmin
Form
(
forms
.
ModelForm
):
class
Course
SpecificFlagAdminBase
Form
(
forms
.
ModelForm
):
"""
Form for course-specific
HLS Playback
configuration.
Form for course-specific
feature
configuration.
"""
# Make abstract base class
class
Meta
(
object
):
model
=
CourseHLSPlaybackEnabledFlag
fields
=
'__all__'
abstract
=
True
def
clean_course_id
(
self
):
"""
...
...
@@ -42,3 +42,23 @@ class CourseHLSPlaybackFlagAdminForm(forms.ModelForm):
raise
forms
.
ValidationError
(
msg
)
return
course_key
class
CourseHLSPlaybackFlagAdminForm
(
CourseSpecificFlagAdminBaseForm
):
"""
Form for course-specific HLS Playback configuration.
"""
class
Meta
(
object
):
model
=
CourseHLSPlaybackEnabledFlag
fields
=
'__all__'
class
CourseVideoTranscriptFlagAdminForm
(
CourseSpecificFlagAdminBaseForm
):
"""
Form for course-specific Video Transcript configuration.
"""
class
Meta
(
object
):
model
=
CourseVideoTranscriptEnabledFlag
fields
=
'__all__'
openedx/core/djangoapps/video_config/migrations/0002_coursevideotranscriptenabledflag_videotranscriptenabledflag.py
0 → 100644
View file @
538a3d78
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
from
django.conf
import
settings
import
django.db.models.deletion
import
openedx.core.djangoapps.xmodule_django.models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
migrations
.
swappable_dependency
(
settings
.
AUTH_USER_MODEL
),
(
'video_config'
,
'0001_initial'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'CourseVideoTranscriptEnabledFlag'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'change_date'
,
models
.
DateTimeField
(
auto_now_add
=
True
,
verbose_name
=
'Change date'
)),
(
'enabled'
,
models
.
BooleanField
(
default
=
False
,
verbose_name
=
'Enabled'
)),
(
'course_id'
,
openedx
.
core
.
djangoapps
.
xmodule_django
.
models
.
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)),
(
'changed_by'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
PROTECT
,
editable
=
False
,
to
=
settings
.
AUTH_USER_MODEL
,
null
=
True
,
verbose_name
=
'Changed by'
)),
],
options
=
{
'ordering'
:
(
'-change_date'
,),
'abstract'
:
False
,
},
),
migrations
.
CreateModel
(
name
=
'VideoTranscriptEnabledFlag'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'change_date'
,
models
.
DateTimeField
(
auto_now_add
=
True
,
verbose_name
=
'Change date'
)),
(
'enabled'
,
models
.
BooleanField
(
default
=
False
,
verbose_name
=
'Enabled'
)),
(
'enabled_for_all_courses'
,
models
.
BooleanField
(
default
=
False
)),
(
'changed_by'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
PROTECT
,
editable
=
False
,
to
=
settings
.
AUTH_USER_MODEL
,
null
=
True
,
verbose_name
=
'Changed by'
)),
],
options
=
{
'ordering'
:
(
'-change_date'
,),
'abstract'
:
False
,
},
),
]
openedx/core/djangoapps/video_config/models.py
View file @
538a3d78
...
...
@@ -66,3 +66,68 @@ class CourseHLSPlaybackEnabledFlag(ConfigurationModel):
course_key
=
unicode
(
self
.
course_id
),
not_enabled
=
not_en
)
class
VideoTranscriptEnabledFlag
(
ConfigurationModel
):
"""
Enables Video Transcript across the platform.
When this feature flag is set to true, individual courses
must also have Video Transcript enabled for this feature to
take effect.
When this feature is enabled, 3rd party transcript integration functionality would be available accross all
courses or some specific courses and S3 video transcript would be served (currently as a fallback).
"""
# this field overrides course-specific settings
enabled_for_all_courses
=
BooleanField
(
default
=
False
)
@classmethod
def
feature_enabled
(
cls
,
course_id
):
"""
Looks at the currently active configuration model to determine whether
the Video Transcript feature is available.
If the feature flag is not enabled, the feature is not available.
If the flag is enabled for all the courses, feature is available.
If the flag is enabled and the provided course_id is for an course
with Video Transcript enabled, the feature is available.
Arguments:
course_id (CourseKey): course id for whom feature will be checked.
"""
if
not
VideoTranscriptEnabledFlag
.
is_enabled
():
return
False
elif
not
VideoTranscriptEnabledFlag
.
current
()
.
enabled_for_all_courses
:
feature
=
(
CourseVideoTranscriptEnabledFlag
.
objects
.
filter
(
course_id
=
course_id
)
.
order_by
(
'-change_date'
)
.
first
())
return
feature
.
enabled
if
feature
else
False
return
True
def
__unicode__
(
self
):
current_model
=
VideoTranscriptEnabledFlag
.
current
()
return
u"VideoTranscriptEnabledFlag: enabled {is_enabled}"
.
format
(
is_enabled
=
current_model
.
is_enabled
()
)
class
CourseVideoTranscriptEnabledFlag
(
ConfigurationModel
):
"""
Enables Video Transcript for a specific course. Global feature must be
enabled for this to take effect.
When this feature is enabled, 3rd party transcript integration functionality would be available for the
specific course and S3 video transcript would be served (currently as a fallback).
"""
KEY_FIELDS
=
(
'course_id'
,)
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
def
__unicode__
(
self
):
not_en
=
"Not "
if
self
.
enabled
:
not_en
=
""
return
u"Course '{course_key}': Video Transcript {not_enabled}Enabled"
.
format
(
course_key
=
unicode
(
self
.
course_id
),
not_enabled
=
not_en
)
openedx/core/djangoapps/video_config/tests/test_models.py
View file @
538a3d78
...
...
@@ -13,100 +13,198 @@ from openedx.core.djangoapps.video_config.models import CourseHLSPlaybackEnabled
@contextmanager
def
hls_playback_feature_flags
(
def
video_feature_flags
(
all_courses_model_class
,
course_specific_model_class
,
global_flag
,
enabled_for_all_courses
=
False
,
course_id
=
None
,
enabled_for_course
=
False
):
"""
Yields
HLS Playback C
onfiguration records for unit tests
Yields
video feature c
onfiguration records for unit tests
Arguments:
all_courses_model_class: Model class to enable feature for all courses
course_specific_model_class: Model class to nable feature for course specific
global_flag (bool): Specifies whether feature is enabled globally
enabled_for_all_courses (bool): Specifies whether feature is enabled for all courses
course_id (CourseLocator): Course locator for course specific configurations
enabled_for_course (bool): Specifies whether feature should be available for a course
"""
HLSPlaybackEnabledFlag
.
objects
.
create
(
enabled
=
global_flag
,
enabled_for_all_courses
=
enabled_for_all_courses
)
all_courses_model_class
.
objects
.
create
(
enabled
=
global_flag
,
enabled_for_all_courses
=
enabled_for_all_courses
)
if
course_id
:
CourseHLSPlaybackEnabledFlag
.
objects
.
create
(
course_id
=
course_id
,
enabled
=
enabled_for_course
)
course_specific_model_class
.
objects
.
create
(
course_id
=
course_id
,
enabled
=
enabled_for_course
)
yield
@ddt.ddt
class
TestHLSPlaybackFlag
(
TestCase
):
class
FeatureFlagTestMixin
(
object
):
"""
Tests the behavior of the flags for HLS Playback feature.
These are set via Django admin settings.
Adds util methods to test the behavior of the flags for video feature.
"""
def
setUp
(
self
):
super
(
TestHLSPlaybackFlag
,
self
)
.
setUp
()
self
.
course_id_1
=
CourseLocator
(
org
=
"edx"
,
course
=
"course"
,
run
=
"run"
)
self
.
course_id_2
=
CourseLocator
(
org
=
"edx"
,
course
=
"course2"
,
run
=
"run"
)
course_id_1
=
CourseLocator
(
org
=
"edx"
,
course
=
"course"
,
run
=
"run"
)
course_id_2
=
CourseLocator
(
org
=
"edx"
,
course
=
"course2"
,
run
=
"run"
)
@ddt.data
(
*
itertools
.
product
(
(
True
,
False
),
(
True
,
False
),
(
True
,
False
),
)
)
@ddt.unpack
def
test_hls_playback_feature_flags
(
self
,
global_flag
,
enabled_for_all_courses
,
enabled_for_course_1
):
def
verify_feature_flags
(
self
,
all_courses_model_class
,
course_specific_model_class
,
global_flag
,
enabled_for_all_courses
,
enabled_for_course_1
):
"""
Test
s that the feature flags works correctly on tweaking global flags in combination
Verifie
s that the feature flags works correctly on tweaking global flags in combination
with course-specific flags.
"""
with
hls_playback_feature_flags
(
with
video_feature_flags
(
all_courses_model_class
=
all_courses_model_class
,
course_specific_model_class
=
course_specific_model_class
,
global_flag
=
global_flag
,
enabled_for_all_courses
=
enabled_for_all_courses
,
course_id
=
self
.
course_id_1
,
enabled_for_course
=
enabled_for_course_1
):
self
.
assertEqual
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_1
),
all_courses_model_class
.
feature_enabled
(
self
.
course_id_1
),
global_flag
and
(
enabled_for_all_courses
or
enabled_for_course_1
)
)
self
.
assertEqual
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_2
),
all_courses_model_class
.
feature_enabled
(
self
.
course_id_2
),
global_flag
and
enabled_for_all_courses
)
def
test_enable_disable_course_flag
(
self
):
def
verify_enable_disable_course_flag
(
self
,
all_courses_model_class
,
course_specific_model_class
):
"""
Ensures that the
flag, once enabled for a course, can also be disabled.
Verifies that the course specific
flag, once enabled for a course, can also be disabled.
"""
with
hls_playback_feature_flags
(
with
video_feature_flags
(
all_courses_model_class
=
all_courses_model_class
,
course_specific_model_class
=
course_specific_model_class
,
global_flag
=
True
,
enabled_for_all_courses
=
False
,
course_id
=
self
.
course_id_1
,
enabled_for_course
=
True
):
self
.
assertTrue
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_1
))
with
hls_playback_feature_flags
(
self
.
assertTrue
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_1
))
with
video_feature_flags
(
all_courses_model_class
=
all_courses_model_class
,
course_specific_model_class
=
course_specific_model_class
,
global_flag
=
True
,
enabled_for_all_courses
=
False
,
course_id
=
self
.
course_id_1
,
enabled_for_course
=
False
):
self
.
assertFalse
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertFalse
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_1
))
def
test_enable_disable_globally
(
self
):
def
verify_enable_disable_globally
(
self
,
all_courses_model_class
,
course_specific_model_class
):
"""
Ensures that the
flag, once enabled globally, can also be disabled.
Verifies that global
flag, once enabled globally, can also be disabled.
"""
with
hls_playback_feature_flags
(
with
video_feature_flags
(
all_courses_model_class
=
all_courses_model_class
,
course_specific_model_class
=
course_specific_model_class
,
global_flag
=
True
,
enabled_for_all_courses
=
True
,
):
self
.
assertTrue
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertTrue
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_2
))
with
hls_playback_feature_flags
(
self
.
assertTrue
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertTrue
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_2
))
with
video_feature_flags
(
all_courses_model_class
=
all_courses_model_class
,
course_specific_model_class
=
course_specific_model_class
,
global_flag
=
True
,
enabled_for_all_courses
=
False
,
):
self
.
assertFalse
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertFalse
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_2
))
with
hls_playback_feature_flags
(
self
.
assertFalse
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertFalse
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_2
))
with
video_feature_flags
(
all_courses_model_class
=
all_courses_model_class
,
course_specific_model_class
=
course_specific_model_class
,
global_flag
=
False
,
):
self
.
assertFalse
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertFalse
(
HLSPlaybackEnabledFlag
.
feature_enabled
(
self
.
course_id_2
))
self
.
assertFalse
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_1
))
self
.
assertFalse
(
all_courses_model_class
.
feature_enabled
(
self
.
course_id_2
))
@ddt.ddt
class
TestHLSPlaybackFlag
(
TestCase
,
FeatureFlagTestMixin
):
"""
Tests the behavior of the flags for HLS Playback feature.
These are set via Django admin settings.
"""
@ddt.data
(
*
itertools
.
product
(
(
True
,
False
),
(
True
,
False
),
(
True
,
False
),
)
)
@ddt.unpack
def
test_hls_playback_feature_flags
(
self
,
global_flag
,
enabled_for_all_courses
,
enabled_for_course_1
):
"""
Tests that the HLS Playback feature flags works correctly on tweaking global flags in combination
with course-specific flags.
"""
self
.
verify_feature_flags
(
all_courses_model_class
=
HLSPlaybackEnabledFlag
,
course_specific_model_class
=
CourseHLSPlaybackEnabledFlag
,
global_flag
=
global_flag
,
enabled_for_all_courses
=
enabled_for_all_courses
,
enabled_for_course_1
=
enabled_for_course_1
)
def
test_enable_disable_course_flag
(
self
):
"""
Ensures that the flag, once enabled for a course, can also be disabled.
"""
self
.
verify_enable_disable_course_flag
(
all_courses_model_class
=
HLSPlaybackEnabledFlag
,
course_specific_model_class
=
CourseHLSPlaybackEnabledFlag
)
def
test_enable_disable_globally
(
self
):
"""
Ensures that the flag, once enabled globally, can also be disabled.
"""
self
.
verify_enable_disable_globally
(
all_courses_model_class
=
HLSPlaybackEnabledFlag
,
course_specific_model_class
=
CourseHLSPlaybackEnabledFlag
)
@ddt.ddt
class
TestVideoTranscriptFlag
(
TestCase
,
FeatureFlagTestMixin
):
"""
Tests the behavior of the flags for Video Transcript feature.
These are set via Django admin settings.
"""
@ddt.data
(
*
itertools
.
product
(
(
True
,
False
),
(
True
,
False
),
(
True
,
False
),
)
)
@ddt.unpack
def
test_video_transcript_feature_flags
(
self
,
global_flag
,
enabled_for_all_courses
,
enabled_for_course_1
):
"""
Tests that Video Transcript feature flags works correctly on tweaking global flags in combination
with course-specific flags.
"""
self
.
verify_feature_flags
(
all_courses_model_class
=
HLSPlaybackEnabledFlag
,
course_specific_model_class
=
CourseHLSPlaybackEnabledFlag
,
global_flag
=
global_flag
,
enabled_for_all_courses
=
enabled_for_all_courses
,
enabled_for_course_1
=
enabled_for_course_1
)
def
test_enable_disable_course_flag
(
self
):
"""
Ensures that the Video Transcript course specific flag, once enabled for a course, can also be disabled.
"""
self
.
verify_enable_disable_course_flag
(
all_courses_model_class
=
HLSPlaybackEnabledFlag
,
course_specific_model_class
=
CourseHLSPlaybackEnabledFlag
)
def
test_enable_disable_globally
(
self
):
"""
Ensures that the Video Transcript flag, once enabled globally, can also be disabled.
"""
self
.
verify_enable_disable_globally
(
all_courses_model_class
=
HLSPlaybackEnabledFlag
,
course_specific_model_class
=
CourseHLSPlaybackEnabledFlag
)
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