Commit 9009669c by Qubad786 Committed by muzaffaryousaf

Add trasncript preferences backend for studio and new transcription statuses to video.

EDU-1092
parent a953a844
...@@ -24,8 +24,10 @@ from contentstore.utils import reverse_course_url ...@@ -24,8 +24,10 @@ from contentstore.utils import reverse_course_url
from contentstore.views.videos import ( from contentstore.views.videos import (
_get_default_video_image_url, _get_default_video_image_url,
validate_video_image, validate_video_image,
validate_transcript_preferences,
VIDEO_IMAGE_UPLOAD_ENABLED, VIDEO_IMAGE_UPLOAD_ENABLED,
WAFFLE_SWITCHES, WAFFLE_SWITCHES,
TranscriptProvider
) )
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings, convert_video_status from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings, convert_video_status
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -854,6 +856,165 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase): ...@@ -854,6 +856,165 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
self.verify_image_upload_reponse(self.course.id, edx_video_id, response) 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}) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True})
@override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"}) @override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"})
class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
......
...@@ -25,7 +25,10 @@ from edxval.api import ( ...@@ -25,7 +25,10 @@ from edxval.api import (
get_videos_for_course, get_videos_for_course,
remove_video_for_course, remove_video_for_course,
update_video_status, 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 opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
...@@ -38,7 +41,7 @@ from util.json_request import JsonResponse, expect_json ...@@ -38,7 +41,7 @@ from util.json_request import JsonResponse, expect_json
from .course import get_course_and_check_access 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__) LOGGER = logging.getLogger(__name__)
...@@ -63,6 +66,14 @@ VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5 ...@@ -63,6 +66,14 @@ VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
MAX_UPLOAD_HOURS = 24 MAX_UPLOAD_HOURS = 24
class TranscriptProvider(object):
"""
3rd Party Transcription Provider Enumeration
"""
CIELO24 = 'Cielo24'
THREE_PLAY_MEDIA = '3PlayMedia'
class StatusDisplayStrings(object): class StatusDisplayStrings(object):
""" """
A class to map status strings as stored in VAL to display strings for the A class to map status strings as stored in VAL to display strings for the
...@@ -93,6 +104,10 @@ class StatusDisplayStrings(object): ...@@ -93,6 +104,10 @@ class StatusDisplayStrings(object):
_IMPORTED = ugettext_noop("Imported") _IMPORTED = ugettext_noop("Imported")
# Translators: This is the status for a video that is in an unknown state # Translators: This is the status for a video that is in an unknown state
_UNKNOWN = ugettext_noop("Unknown") _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 = { _STATUS_MAP = {
"upload": _UPLOADING, "upload": _UPLOADING,
...@@ -111,6 +126,8 @@ class StatusDisplayStrings(object): ...@@ -111,6 +126,8 @@ class StatusDisplayStrings(object):
"youtube_duplicate": _YOUTUBE_DUPLICATE, "youtube_duplicate": _YOUTUBE_DUPLICATE,
"invalid_token": _INVALID_TOKEN, "invalid_token": _INVALID_TOKEN,
"imported": _IMPORTED, "imported": _IMPORTED,
"transcription_in_progress": _TRANSCRIPTION_IN_PROGRESS,
"transcription_ready": _TRANSCRIPTION_READY,
} }
@staticmethod @staticmethod
...@@ -236,6 +253,111 @@ def video_images_handler(request, course_key_string, edx_video_id=None): ...@@ -236,6 +253,111 @@ def video_images_handler(request, course_key_string, edx_video_id=None):
return JsonResponse({'image_url': image_url}) 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 @login_required
@require_GET @require_GET
def video_encodings_download(request, course_key_string): def video_encodings_download(request, course_key_string):
...@@ -424,9 +546,7 @@ def videos_index_html(course): ...@@ -424,9 +546,7 @@ def videos_index_html(course):
""" """
Returns an HTML page to display previous video uploads and allow new ones Returns an HTML page to display previous video uploads and allow new ones
""" """
return render_to_response( context = {
'videos_index.html',
{
'context_course': course, 'context_course': course,
'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)), 'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)),
'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)), 'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)),
...@@ -445,7 +565,19 @@ def videos_index_html(course): ...@@ -445,7 +565,19 @@ def videos_index_html(course):
'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS '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): def videos_index_json(course):
...@@ -486,16 +618,17 @@ def videos_post(course, request): ...@@ -486,16 +618,17 @@ def videos_post(course, request):
The returned array corresponds exactly to the input array. The returned array corresponds exactly to the input array.
""" """
error = None 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'" error = "Request object is not JSON or does not contain 'files'"
elif any( elif any(
'file_name' not in file or 'content_type' not in file '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'" error = "Request 'files' entry does not contain 'file_name' and 'content_type'"
elif any( elif any(
file['content_type'] not in VIDEO_SUPPORTED_FILE_FORMATS.values() 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" error = "Request 'files' entry contain unsupported content_type"
...@@ -504,7 +637,7 @@ def videos_post(course, request): ...@@ -504,7 +637,7 @@ def videos_post(course, request):
bucket = storage_service_bucket() bucket = storage_service_bucket()
course_video_upload_token = course.video_upload_pipeline['course_video_upload_token'] course_video_upload_token = course.video_upload_pipeline['course_video_upload_token']
req_files = request.json['files'] req_files = data['files']
resp_files = [] resp_files = []
for req_file in req_files: for req_file in req_files:
...@@ -518,11 +651,18 @@ def videos_post(course, request): ...@@ -518,11 +651,18 @@ def videos_post(course, request):
edx_video_id = unicode(uuid4()) edx_video_id = unicode(uuid4())
key = storage_service_key(bucket, file_name=edx_video_id) key = storage_service_key(bucket, file_name=edx_video_id)
for metadata_name, value in [
metadata_list = [
('course_video_upload_token', course_video_upload_token), ('course_video_upload_token', course_video_upload_token),
('client_video_id', file_name), ('client_video_id', file_name),
('course_key', unicode(course.id)), ('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) key.set_metadata(metadata_name, value)
upload_url = key.generate_url( upload_url = key.generate_url(
KEY_EXPIRATION_IN_SECONDS, KEY_EXPIRATION_IN_SECONDS,
......
...@@ -128,6 +128,7 @@ urlpatterns += patterns( ...@@ -128,6 +128,7 @@ urlpatterns += patterns(
url(r'^textbooks/{}/(?P<textbook_id>\d[^/]*)$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_detail_handler'), 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'^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'^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'^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/{}$'.format(settings.COURSE_KEY_PATTERN), 'group_configurations_list_handler'),
url(r'^group_configurations/{}/(?P<group_configuration_id>\d+)(/)?(?P<group_id>\d+)?$'.format( url(r'^group_configurations/{}/(?P<group_configuration_id>\d+)(/)?(?P<group_id>\d+)?$'.format(
......
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