Add trasncript preferences backend for studio and new transcription statuses to video.
EDU-1092
This commit is contained in:
@@ -24,8 +24,10 @@ from contentstore.utils import reverse_course_url
|
||||
from contentstore.views.videos import (
|
||||
_get_default_video_image_url,
|
||||
validate_video_image,
|
||||
validate_transcript_preferences,
|
||||
VIDEO_IMAGE_UPLOAD_ENABLED,
|
||||
WAFFLE_SWITCHES,
|
||||
TranscriptProvider
|
||||
)
|
||||
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings, convert_video_status
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -854,6 +856,165 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
self.verify_image_upload_reponse(self.course.id, edx_video_id, response)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_VIDEO_UPLOAD_PIPELINE': True})
|
||||
class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
"""
|
||||
Tests for video transcripts preferences.
|
||||
"""
|
||||
|
||||
VIEW_NAME = 'transcript_preferences_handler'
|
||||
|
||||
@ddt.data(
|
||||
# Error cases
|
||||
(
|
||||
{},
|
||||
'Invalid provider.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.CIELO24
|
||||
},
|
||||
'Invalid cielo24 fidelity.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.CIELO24,
|
||||
'cielo24_fidelity': 'PROFESSIONAL',
|
||||
},
|
||||
'Invalid cielo24 turnaround.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.CIELO24,
|
||||
'cielo24_fidelity': 'PROFESSIONAL',
|
||||
'cielo24_turnaround': 'STANDARD'
|
||||
},
|
||||
'Invalid languages.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.CIELO24,
|
||||
'cielo24_fidelity': 'PROFESSIONAL',
|
||||
'cielo24_turnaround': 'STANDARD',
|
||||
'preferred_languages': ['es', 'ur']
|
||||
},
|
||||
'Invalid languages.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.THREE_PLAY_MEDIA
|
||||
},
|
||||
'Invalid 3play turnaround.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.THREE_PLAY_MEDIA,
|
||||
'three_play_turnaround': 'default'
|
||||
},
|
||||
'Invalid languages.'
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.THREE_PLAY_MEDIA,
|
||||
'three_play_turnaround': 'default',
|
||||
'preferred_languages': ['es', 'ur']
|
||||
},
|
||||
'Invalid languages.'
|
||||
),
|
||||
# Success
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.CIELO24,
|
||||
'cielo24_fidelity': 'PROFESSIONAL',
|
||||
'cielo24_turnaround': 'STANDARD',
|
||||
'preferred_languages': ['en']
|
||||
},
|
||||
''
|
||||
),
|
||||
(
|
||||
{
|
||||
'provider': TranscriptProvider.THREE_PLAY_MEDIA,
|
||||
'three_play_turnaround': 'default',
|
||||
'preferred_languages': ['en']
|
||||
},
|
||||
''
|
||||
)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_video_transcript(self, preferences, error_message):
|
||||
"""
|
||||
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', ''),
|
||||
'preferred_languages': preferences.get('preferred_languages', []),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@ddt.data(
|
||||
None,
|
||||
{
|
||||
'provider': TranscriptProvider.CIELO24,
|
||||
'cielo24_fidelity': 'PROFESSIONAL',
|
||||
'cielo24_turnaround': 'STANDARD',
|
||||
'preferred_languages': ['en']
|
||||
}
|
||||
)
|
||||
@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')
|
||||
def test_transcript_preferences_metadata(self, transcript_preferences, mock_conn, mock_key):
|
||||
"""
|
||||
Tests that transcript preference metadata is only set if it is transcript
|
||||
preferences are present in request data.
|
||||
"""
|
||||
file_name = 'test-video.mp4'
|
||||
request_data = {'files': [{'file_name': file_name, 'content_type': 'video/mp4'}]}
|
||||
|
||||
if transcript_preferences:
|
||||
request_data.update({'transcript_preferences': transcript_preferences})
|
||||
|
||||
bucket = Mock()
|
||||
mock_conn.return_value = Mock(get_bucket=Mock(return_value=bucket))
|
||||
mock_key_instance = Mock(
|
||||
generate_url=Mock(
|
||||
return_value='http://example.com/url_{file_name}'.format(file_name=file_name)
|
||||
)
|
||||
)
|
||||
# If extra calls are made, return a dummy
|
||||
mock_key.side_effect = [mock_key_instance] + [Mock()]
|
||||
|
||||
videos_handler_url = reverse_course_url('videos_handler', self.course.id)
|
||||
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:
|
||||
mock_key_instance.set_metadata.assert_any_call('transcript_preferences', transcript_preferences)
|
||||
else:
|
||||
with self.assertRaises(AssertionError):
|
||||
mock_key_instance.set_metadata.assert_any_call('transcript_preferences', transcript_preferences)
|
||||
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True})
|
||||
@override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"})
|
||||
class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
|
||||
@@ -25,7 +25,10 @@ from edxval.api import (
|
||||
get_videos_for_course,
|
||||
remove_video_for_course,
|
||||
update_video_status,
|
||||
update_video_image
|
||||
update_video_image,
|
||||
get_3rd_party_transcription_plans,
|
||||
get_transcript_preferences,
|
||||
create_or_update_transcript_preferences,
|
||||
)
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
|
||||
@@ -38,7 +41,7 @@ from util.json_request import JsonResponse, expect_json
|
||||
from .course import get_course_and_check_access
|
||||
|
||||
|
||||
__all__ = ['videos_handler', 'video_encodings_download', 'video_images_handler']
|
||||
__all__ = ['videos_handler', 'video_encodings_download', 'video_images_handler', 'transcript_preferences_handler']
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,6 +66,14 @@ VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
|
||||
MAX_UPLOAD_HOURS = 24
|
||||
|
||||
|
||||
class TranscriptProvider(object):
|
||||
"""
|
||||
3rd Party Transcription Provider Enumeration
|
||||
"""
|
||||
CIELO24 = 'Cielo24'
|
||||
THREE_PLAY_MEDIA = '3PlayMedia'
|
||||
|
||||
|
||||
class StatusDisplayStrings(object):
|
||||
"""
|
||||
A class to map status strings as stored in VAL to display strings for the
|
||||
@@ -93,6 +104,10 @@ class StatusDisplayStrings(object):
|
||||
_IMPORTED = ugettext_noop("Imported")
|
||||
# Translators: This is the status for a video that is in an unknown state
|
||||
_UNKNOWN = ugettext_noop("Unknown")
|
||||
# Translators: This is the status for a video that is having its transcription in progress on servers
|
||||
_TRANSCRIPTION_IN_PROGRESS = ugettext_noop("Transcription in Progress")
|
||||
# Translators: This is the status for a video whose transcription is complete
|
||||
_TRANSCRIPTION_READY = ugettext_noop("Transcription Ready")
|
||||
|
||||
_STATUS_MAP = {
|
||||
"upload": _UPLOADING,
|
||||
@@ -111,6 +126,8 @@ class StatusDisplayStrings(object):
|
||||
"youtube_duplicate": _YOUTUBE_DUPLICATE,
|
||||
"invalid_token": _INVALID_TOKEN,
|
||||
"imported": _IMPORTED,
|
||||
"transcription_in_progress": _TRANSCRIPTION_IN_PROGRESS,
|
||||
"transcription_ready": _TRANSCRIPTION_READY,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -236,6 +253,111 @@ def video_images_handler(request, course_key_string, edx_video_id=None):
|
||||
return JsonResponse({'image_url': image_url})
|
||||
|
||||
|
||||
def validate_transcript_preferences(
|
||||
provider, cielo24_fidelity, cielo24_turnaround, three_play_turnaround, preferred_languages
|
||||
):
|
||||
"""
|
||||
Validate 3rd Party Transcription Preferences.
|
||||
|
||||
Arguments:
|
||||
provider: Transcription provider
|
||||
cielo24_fidelity: Cielo24 transcription fidelity.
|
||||
cielo24_turnaround: Cielo24 transcription turnaround.
|
||||
three_play_turnaround: 3PlayMedia transcription turnaround.
|
||||
preferred_languages: list of language codes.
|
||||
|
||||
Returns:
|
||||
validated preferences or a validation error.
|
||||
"""
|
||||
error, preferences = None, {}
|
||||
|
||||
# validate transcription providers
|
||||
transcription_plans = get_3rd_party_transcription_plans()
|
||||
if provider in transcription_plans.keys():
|
||||
|
||||
# Further validations for providers
|
||||
if provider == TranscriptProvider.CIELO24:
|
||||
|
||||
# Validate transcription fidelity
|
||||
if cielo24_fidelity in transcription_plans[provider]['fidelity']:
|
||||
|
||||
# Validate transcription turnaround
|
||||
if cielo24_turnaround not in transcription_plans[provider]['turnaround']:
|
||||
error = _('Invalid cielo24 turnaround.')
|
||||
return error, preferences
|
||||
|
||||
# Validate transcription languages
|
||||
supported_languages = transcription_plans[provider]['fidelity'][cielo24_fidelity]['languages']
|
||||
if not len(preferred_languages) or not (set(preferred_languages) <= set(supported_languages.keys())):
|
||||
error = _('Invalid languages.')
|
||||
return error, preferences
|
||||
|
||||
# Validated Cielo24 preferences
|
||||
preferences = {
|
||||
'cielo24_fidelity': cielo24_fidelity,
|
||||
'cielo24_turnaround': cielo24_turnaround,
|
||||
'preferred_languages': list(preferred_languages),
|
||||
}
|
||||
else:
|
||||
error = _('Invalid cielo24 fidelity.')
|
||||
elif provider == TranscriptProvider.THREE_PLAY_MEDIA:
|
||||
|
||||
# Validate transcription turnaround
|
||||
if three_play_turnaround not in transcription_plans[provider]['turnaround']:
|
||||
error = _('Invalid 3play turnaround.')
|
||||
return error, preferences
|
||||
|
||||
# Validate transcription languages
|
||||
supported_languages = transcription_plans[provider]['languages']
|
||||
if not len(preferred_languages) or not (set(preferred_languages) <= set(supported_languages.keys())):
|
||||
error = _('Invalid languages.')
|
||||
return error, preferences
|
||||
|
||||
# Validated 3PlayMedia preferences
|
||||
preferences = {
|
||||
'three_play_turnaround': three_play_turnaround,
|
||||
'preferred_languages': list(preferred_languages),
|
||||
}
|
||||
else:
|
||||
error = _('Invalid provider.')
|
||||
|
||||
return error, preferences
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@require_POST
|
||||
def transcript_preferences_handler(request, course_key_string):
|
||||
"""
|
||||
JSON view handler to post the transcript preferences.
|
||||
|
||||
Arguments:
|
||||
request: WSGI request object
|
||||
course_key_string: string for course key
|
||||
|
||||
Returns: valid json response or 400 with error message
|
||||
"""
|
||||
data = request.json
|
||||
provider = data.get('provider', '')
|
||||
|
||||
error, preferences = validate_transcript_preferences(
|
||||
provider=provider,
|
||||
cielo24_fidelity=data.get('cielo24_fidelity', ''),
|
||||
cielo24_turnaround=data.get('cielo24_turnaround', ''),
|
||||
three_play_turnaround=data.get('three_play_turnaround', ''),
|
||||
preferred_languages=data.get('preferred_languages', [])
|
||||
)
|
||||
|
||||
if error:
|
||||
response = JsonResponse({'error': error}, status=400)
|
||||
else:
|
||||
preferences.update({'provider': provider})
|
||||
transcript_preferences = create_or_update_transcript_preferences(course_key_string, **preferences)
|
||||
response = JsonResponse({'transcript_preferences': transcript_preferences}, status=200)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def video_encodings_download(request, course_key_string):
|
||||
@@ -424,28 +546,38 @@ def videos_index_html(course):
|
||||
"""
|
||||
Returns an HTML page to display previous video uploads and allow new ones
|
||||
"""
|
||||
return render_to_response(
|
||||
'videos_index.html',
|
||||
{
|
||||
'context_course': course,
|
||||
'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)),
|
||||
'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)),
|
||||
'encodings_download_url': reverse_course_url('video_encodings_download', unicode(course.id)),
|
||||
'default_video_image_url': _get_default_video_image_url(),
|
||||
'previous_uploads': _get_index_videos(course),
|
||||
'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0),
|
||||
'video_supported_file_formats': VIDEO_SUPPORTED_FILE_FORMATS.keys(),
|
||||
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB,
|
||||
'video_image_settings': {
|
||||
'video_image_upload_enabled': WAFFLE_SWITCHES.is_enabled(VIDEO_IMAGE_UPLOAD_ENABLED),
|
||||
'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'],
|
||||
'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'],
|
||||
'max_width': settings.VIDEO_IMAGE_MAX_WIDTH,
|
||||
'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT,
|
||||
'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
|
||||
}
|
||||
context = {
|
||||
'context_course': course,
|
||||
'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)),
|
||||
'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)),
|
||||
'encodings_download_url': reverse_course_url('video_encodings_download', unicode(course.id)),
|
||||
'default_video_image_url': _get_default_video_image_url(),
|
||||
'previous_uploads': _get_index_videos(course),
|
||||
'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0),
|
||||
'video_supported_file_formats': VIDEO_SUPPORTED_FILE_FORMATS.keys(),
|
||||
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB,
|
||||
'video_image_settings': {
|
||||
'video_image_upload_enabled': WAFFLE_SWITCHES.is_enabled(VIDEO_IMAGE_UPLOAD_ENABLED),
|
||||
'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'],
|
||||
'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'],
|
||||
'max_width': settings.VIDEO_IMAGE_MAX_WIDTH,
|
||||
'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT,
|
||||
'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
context.update({
|
||||
'third_party_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))
|
||||
})
|
||||
|
||||
return render_to_response('videos_index.html', context)
|
||||
|
||||
|
||||
def videos_index_json(course):
|
||||
@@ -486,16 +618,17 @@ def videos_post(course, request):
|
||||
The returned array corresponds exactly to the input array.
|
||||
"""
|
||||
error = None
|
||||
if 'files' not in request.json:
|
||||
data = request.json
|
||||
if 'files' not in data:
|
||||
error = "Request object is not JSON or does not contain 'files'"
|
||||
elif any(
|
||||
'file_name' not in file or 'content_type' not in file
|
||||
for file in request.json['files']
|
||||
for file in data['files']
|
||||
):
|
||||
error = "Request 'files' entry does not contain 'file_name' and 'content_type'"
|
||||
elif any(
|
||||
file['content_type'] not in VIDEO_SUPPORTED_FILE_FORMATS.values()
|
||||
for file in request.json['files']
|
||||
for file in data['files']
|
||||
):
|
||||
error = "Request 'files' entry contain unsupported content_type"
|
||||
|
||||
@@ -504,7 +637,7 @@ def videos_post(course, request):
|
||||
|
||||
bucket = storage_service_bucket()
|
||||
course_video_upload_token = course.video_upload_pipeline['course_video_upload_token']
|
||||
req_files = request.json['files']
|
||||
req_files = data['files']
|
||||
resp_files = []
|
||||
|
||||
for req_file in req_files:
|
||||
@@ -518,11 +651,18 @@ def videos_post(course, request):
|
||||
|
||||
edx_video_id = unicode(uuid4())
|
||||
key = storage_service_key(bucket, file_name=edx_video_id)
|
||||
for metadata_name, value in [
|
||||
('course_video_upload_token', course_video_upload_token),
|
||||
('client_video_id', file_name),
|
||||
('course_key', unicode(course.id)),
|
||||
]:
|
||||
|
||||
metadata_list = [
|
||||
('course_video_upload_token', course_video_upload_token),
|
||||
('client_video_id', file_name),
|
||||
('course_key', unicode(course.id)),
|
||||
]
|
||||
|
||||
transcript_preferences = data.get('transcript_preferences', None)
|
||||
if transcript_preferences is not None:
|
||||
metadata_list.append(('transcript_preferences', transcript_preferences))
|
||||
|
||||
for metadata_name, value in metadata_list:
|
||||
key.set_metadata(metadata_name, value)
|
||||
upload_url = key.generate_url(
|
||||
KEY_EXPIRATION_IN_SECONDS,
|
||||
|
||||
@@ -128,6 +128,7 @@ urlpatterns += patterns(
|
||||
url(r'^textbooks/{}/(?P<textbook_id>\d[^/]*)$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_detail_handler'),
|
||||
url(r'^videos/{}(?:/(?P<edx_video_id>[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'videos_handler'),
|
||||
url(r'^video_images/{}(?:/(?P<edx_video_id>[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'video_images_handler'),
|
||||
url(r'^transcript_preferences/{}$'.format(settings.COURSE_KEY_PATTERN), 'transcript_preferences_handler'),
|
||||
url(r'^video_encodings_download/{}$'.format(settings.COURSE_KEY_PATTERN), 'video_encodings_download'),
|
||||
url(r'^group_configurations/{}$'.format(settings.COURSE_KEY_PATTERN), 'group_configurations_list_handler'),
|
||||
url(r'^group_configurations/{}/(?P<group_configuration_id>\d+)(/)?(?P<group_id>\d+)?$'.format(
|
||||
|
||||
Reference in New Issue
Block a user