Commit 538a3d78 by Mushtaq Ali Committed by muzaffaryousaf

Add video transcript config model flags - EDUCATOR-1224

parent 2203de4a
......@@ -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_message):
def test_video_transcript(self, preferences, is_video_transcript_enabled, error_message, expected_status_code):
"""
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):
......
......@@ -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,
"transcription_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)))
......
......@@ -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,
......
......@@ -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');
......
......@@ -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() {
......
......@@ -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');
......
......@@ -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>
......@@ -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">&gt; </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>
......
......@@ -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 CourseHLSPlaybackEnabledFlagAdmin(KeyedConfigurationModelAdmin):
class CourseSpecificEnabledFlagBaseAdmin(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)
......@@ -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 CourseHLSPlaybackFlagAdminForm(forms.ModelForm):
class CourseSpecificFlagAdminBaseForm(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__'
# -*- 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,
},
),
]
......@@ -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
)
......@@ -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 Configuration records for unit tests
Yields video feature configuration 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):
"""
Tests that the feature flags works correctly on tweaking global flags in combination
Verifies 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
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment