Merge pull request #17052 from edx/show-transcripts-feature

Show transcripts feature
This commit is contained in:
Mushtaq Ali
2018-01-09 19:29:47 +05:00
committed by GitHub
21 changed files with 1473 additions and 46 deletions

View File

@@ -2,6 +2,7 @@
""" Tests for transcripts_utils. """
import copy
import ddt
import json
import textwrap
import unittest
from uuid import uuid4
@@ -610,28 +611,53 @@ class TestTranscript(unittest.TestCase):
self.txt_transcript = u"Elephant's Dream\nAt the left we can see..."
def test_convert_srt_to_txt(self):
"""
Tests that the srt transcript is successfully converted into txt format.
"""
expected = self.txt_transcript
actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'txt')
self.assertEqual(actual, expected)
def test_convert_srt_to_srt(self):
"""
Tests that srt to srt conversion works as expected.
"""
expected = self.srt_transcript
actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'srt')
self.assertEqual(actual, expected)
def test_convert_sjson_to_txt(self):
"""
Tests that the sjson transcript is successfully converted into txt format.
"""
expected = self.txt_transcript
actual = transcripts_utils.Transcript.convert(self.sjson_transcript, 'sjson', 'txt')
self.assertEqual(actual, expected)
def test_convert_sjson_to_srt(self):
"""
Tests that the sjson transcript is successfully converted into srt format.
"""
expected = self.srt_transcript
actual = transcripts_utils.Transcript.convert(self.sjson_transcript, 'sjson', 'srt')
self.assertEqual(actual, expected)
def test_convert_srt_to_sjson(self):
with self.assertRaises(NotImplementedError):
transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson')
"""
Tests that the srt transcript is successfully converted into sjson format.
"""
expected = json.loads(self.sjson_transcript)
actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson')
self.assertDictEqual(actual, expected)
def test_convert_invalid_srt_to_sjson(self):
"""
Tests that TranscriptsGenerationException was raises on trying
to convert invalid srt transcript to sjson.
"""
invalid_srt_transcript = 'invalid SubRip file content'
with self.assertRaises(transcripts_utils.TranscriptsGenerationException):
transcripts_utils.Transcript.convert(invalid_srt_transcript, 'srt', 'sjson')
def test_dummy_non_existent_transcript(self):
"""

View File

@@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
import ddt
import json
from mock import Mock, patch
from io import BytesIO
from mock import Mock, patch, ANY
from django.test.testcases import TestCase
from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url
from contentstore.views.transcript_settings import TranscriptionProviderErrorType, validate_transcript_credentials
from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file
@ddt.ddt
@@ -169,3 +172,299 @@ class TranscriptCredentialsValidationTest(TestCase):
# Assert the results.
self.assertEqual(error_message, expected_error_message)
self.assertDictEqual(validated_credentials, expected_validated_credentials)
@ddt.ddt
@patch(
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
Mock(return_value=True)
)
class TranscriptDownloadTest(CourseTestCase):
"""
Tests for transcript download handler.
"""
VIEW_NAME = 'transcript_download_handler'
def get_url_for_course_key(self, course_id):
return reverse_course_url(self.VIEW_NAME, course_id)
def test_302_with_anonymous_user(self):
"""
Verify that redirection happens in case of unauthorized request.
"""
self.client.logout()
transcript_download_url = self.get_url_for_course_key(self.course.id)
response = self.client.get(transcript_download_url, content_type='application/json')
self.assertEqual(response.status_code, 302)
def test_405_with_not_allowed_request_method(self):
"""
Verify that 405 is returned in case of not-allowed request methods.
Allowed request methods include GET.
"""
transcript_download_url = self.get_url_for_course_key(self.course.id)
response = self.client.post(transcript_download_url, content_type='application/json')
self.assertEqual(response.status_code, 405)
def test_404_with_feature_disabled(self):
"""
Verify that 404 is returned if the corresponding feature is disabled.
"""
transcript_download_url = self.get_url_for_course_key(self.course.id)
with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature:
feature.return_value = False
response = self.client.get(transcript_download_url, content_type='application/json')
self.assertEqual(response.status_code, 404)
@patch('contentstore.views.transcript_settings.get_video_transcript_data')
def test_transcript_download_handler(self, mock_get_video_transcript_data):
"""
Tests that transcript download handler works as expected.
"""
transcript_download_url = self.get_url_for_course_key(self.course.id)
mock_get_video_transcript_data.return_value = {
'content': json.dumps({
"start": [10],
"end": [100],
"text": ["Hi, welcome to Edx."],
}),
'file_name': 'edx.sjson'
}
# Make request to transcript download handler
response = self.client.get(
transcript_download_url,
data={
'edx_video_id': '123',
'language_code': 'en'
},
content_type='application/json'
)
# Expected response
expected_content = u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n'
expected_headers = {
'Content-Disposition': 'attachment; filename="edx.srt"',
'Content-Language': u'en',
'Content-Type': 'application/x-subrip; charset=utf-8'
}
# Assert the actual response
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, expected_content)
for attribute, value in expected_headers.iteritems():
self.assertEqual(response.get(attribute), value)
@ddt.data(
(
{},
u'The following parameters are required: edx_video_id, language_code.'
),
(
{'edx_video_id': '123'},
u'The following parameters are required: language_code.'
),
(
{'language_code': 'en'},
u'The following parameters are required: edx_video_id.'
),
)
@ddt.unpack
def test_transcript_download_handler_missing_attrs(self, request_payload, expected_error_message):
"""
Tests that transcript download handler with missing attributes.
"""
# Make request to transcript download handler
transcript_download_url = self.get_url_for_course_key(self.course.id)
response = self.client.get(transcript_download_url, data=request_payload)
# Assert the response
self.assertEqual(response.status_code, 400)
self.assertEqual(json.loads(response.content)['error'], expected_error_message)
@ddt.ddt
@patch(
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
Mock(return_value=True)
)
class TranscriptUploadTest(CourseTestCase):
"""
Tests for transcript upload handler.
"""
VIEW_NAME = 'transcript_upload_handler'
def get_url_for_course_key(self, course_id):
return reverse_course_url(self.VIEW_NAME, course_id)
def test_302_with_anonymous_user(self):
"""
Verify that redirection happens in case of unauthorized request.
"""
self.client.logout()
transcript_upload_url = self.get_url_for_course_key(self.course.id)
response = self.client.post(transcript_upload_url, content_type='application/json')
self.assertEqual(response.status_code, 302)
def test_405_with_not_allowed_request_method(self):
"""
Verify that 405 is returned in case of not-allowed request methods.
Allowed request methods include POST.
"""
transcript_upload_url = self.get_url_for_course_key(self.course.id)
response = self.client.get(transcript_upload_url, content_type='application/json')
self.assertEqual(response.status_code, 405)
def test_404_with_feature_disabled(self):
"""
Verify that 404 is returned if the corresponding feature is disabled.
"""
transcript_upload_url = self.get_url_for_course_key(self.course.id)
with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature:
feature.return_value = False
response = self.client.post(transcript_upload_url, content_type='application/json')
self.assertEqual(response.status_code, 404)
@patch('contentstore.views.transcript_settings.create_or_update_video_transcript')
@patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en']))
def test_transcript_upload_handler(self, mock_create_or_update_video_transcript):
"""
Tests that transcript upload handler works as expected.
"""
transcript_upload_url = self.get_url_for_course_key(self.course.id)
transcript_file_stream = BytesIO('0\n00:00:00,010 --> 00:00:00,100\nПривіт, edX вітає вас.\n\n')
# Make request to transcript upload handler
response = self.client.post(
transcript_upload_url,
{
'edx_video_id': '123',
'language_code': 'en',
'new_language_code': 'es',
'file': transcript_file_stream,
},
format='multipart'
)
self.assertEqual(response.status_code, 201)
mock_create_or_update_video_transcript.assert_called_with(
video_id='123',
language_code='en',
metadata={
'language_code': u'es',
'file_format': 'sjson',
'provider': 'Custom'
},
file_data=ANY,
)
@ddt.data(
(
{
'edx_video_id': '123',
'language_code': 'en',
'new_language_code': 'en',
},
u'A transcript file is required.'
),
(
{
'language_code': u'en',
'file': u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n'
},
u'The following parameters are required: edx_video_id, new_language_code.'
),
(
{
'language_code': u'en',
'new_language_code': u'en',
'file': u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n'
},
u'The following parameters are required: edx_video_id.'
),
(
{
'file': u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n'
},
u'The following parameters are required: edx_video_id, language_code, new_language_code.'
)
)
@ddt.unpack
@patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en']))
def test_transcript_upload_handler_missing_attrs(self, request_payload, expected_error_message):
"""
Tests the transcript upload handler when the required attributes are missing.
"""
transcript_upload_url = self.get_url_for_course_key(self.course.id)
# Make request to transcript upload handler
response = self.client.post(transcript_upload_url, request_payload, format='multipart')
self.assertEqual(response.status_code, 400)
self.assertEqual(json.loads(response.content)['error'], expected_error_message)
@patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en', 'es']))
def test_transcript_upload_handler_existing_transcript(self):
"""
Tests that upload handler do not update transcript's language if a transcript
with the same language already present for an edx_video_id.
"""
transcript_upload_url = self.get_url_for_course_key(self.course.id)
# Make request to transcript upload handler
request_payload = {
'edx_video_id': '1234',
'language_code': 'en',
'new_language_code': 'es'
}
response = self.client.post(transcript_upload_url, request_payload, format='multipart')
self.assertEqual(response.status_code, 400)
self.assertEqual(
json.loads(response.content)['error'],
u'A transcript with the "es" language code already exists.'
)
@patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en']))
def test_transcript_upload_handler_with_image(self):
"""
Tests the transcript upload handler with an image file.
"""
with make_image_file() as image_file:
transcript_upload_url = self.get_url_for_course_key(self.course.id)
# Make request to transcript upload handler
response = self.client.post(
transcript_upload_url,
{
'edx_video_id': '123',
'language_code': 'en',
'new_language_code': 'es',
'file': image_file,
},
format='multipart'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
json.loads(response.content)['error'],
u'There is a problem with this transcript file. Try to upload a different file.'
)
@patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en']))
def test_transcript_upload_handler_with_invalid_transcript(self):
"""
Tests the transcript upload handler with an invalid transcript file.
"""
transcript_upload_url = self.get_url_for_course_key(self.course.id)
transcript_file_stream = BytesIO('An invalid transcript SubRip file content')
# Make request to transcript upload handler
response = self.client.post(
transcript_upload_url,
{
'edx_video_id': '123',
'language_code': 'en',
'new_language_code': 'es',
'file': transcript_file_stream,
},
format='multipart'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
json.loads(response.content)['error'],
u'There is a problem with this transcript file. Try to upload a different file.'
)

View File

@@ -15,7 +15,13 @@ import pytz
from django.conf import settings
from django.core.files.uploadedfile import UploadedFile
from django.test.utils import override_settings
from edxval.api import create_profile, create_video, get_video_info, get_course_video_image_url
from edxval.api import (
create_profile,
create_video,
get_video_info,
get_course_video_image_url,
create_or_update_video_transcript
)
from mock import Mock, patch
from contentstore.models import VideoUploadConfig
@@ -228,6 +234,86 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
convert_video_status(original_video)
)
@ddt.data(
(
False,
['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url'],
[],
{}
),
(
True,
['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url',
'transcripts'],
[
{
'video_id': 'test1',
'language_code': 'en',
'file_name': 'edx101.srt',
'file_format': 'srt',
'provider': 'Cielo24'
}
],
{
'en': 'English'
}
),
(
True,
['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url',
'transcripts'],
[
{
'video_id': 'test1',
'language_code': 'en',
'file_name': 'edx101_en.srt',
'file_format': 'srt',
'provider': 'Cielo24'
},
{
'video_id': 'test1',
'language_code': 'es',
'file_name': 'edx101_es.srt',
'file_format': 'srt',
'provider': 'Cielo24'
}
],
{
'en': 'English',
'es': 'Spanish'
}
)
)
@ddt.unpack
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled')
def test_get_json_transcripts(self, is_video_transcript_enabled, expected_video_keys, uploaded_transcripts,
expected_transcripts, video_transcript_feature):
"""
Test that transcripts are attached based on whether the video transcript feature is enabled.
"""
video_transcript_feature.return_value = is_video_transcript_enabled
for transcript in uploaded_transcripts:
create_or_update_video_transcript(
transcript['video_id'],
transcript['language_code'],
metadata={
'file_name': transcript['file_name'],
'file_format': transcript['file_format'],
'provider': transcript['provider']
}
)
response = self.client.get_json(self.url)
self.assertEqual(response.status_code, 200)
response_videos = json.loads(response.content)['videos']
self.assertEqual(len(response_videos), len(self.previous_uploads))
for response_video in response_videos:
self.assertEqual(set(response_video.keys()), set(expected_video_keys))
if response_video['edx_video_id'] == self.previous_uploads[0]['edx_video_id']:
self.assertDictEqual(response_video.get('transcripts', {}), expected_transcripts)
def test_get_html(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

View File

@@ -1,12 +1,20 @@
"""
Views related to the transcript preferences feature
"""
import os
import json
import logging
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotFound
from django.core.files.base import ContentFile
from django.http import HttpResponseNotFound, HttpResponse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django.views.decorators.http import require_POST, require_GET
from edxval.api import (
create_or_update_video_transcript,
get_available_transcript_languages,
get_3rd_party_transcription_plans,
get_video_transcript_data,
update_transcript_credentials_state_for_org,
)
from opaque_keys.edx.keys import CourseKey
@@ -16,8 +24,11 @@ from openedx.core.djangoapps.video_pipeline.api import update_3rd_party_transcri
from util.json_request import JsonResponse, expect_json
from contentstore.views.videos import TranscriptProvider
from xmodule.video_module.transcripts_utils import Transcript, TranscriptsGenerationException
__all__ = ['transcript_credentials_handler']
__all__ = ['transcript_credentials_handler', 'transcript_download_handler', 'transcript_upload_handler']
LOGGER = logging.getLogger(__name__)
class TranscriptionProviderErrorType:
@@ -108,3 +119,138 @@ def transcript_credentials_handler(request, course_key_string):
response = JsonResponse({'error': error_message}, status=400)
return response
@login_required
@require_GET
def transcript_download_handler(request, course_key_string):
"""
JSON view handler to download a transcript.
Arguments:
request: WSGI request object
course_key_string: course key
Returns:
- A 200 response with SRT transcript file attached.
- A 400 if there is a validation error.
- A 404 if there is no such transcript or feature flag is disabled.
"""
course_key = CourseKey.from_string(course_key_string)
if not VideoTranscriptEnabledFlag.feature_enabled(course_key):
return HttpResponseNotFound()
missing = [attr for attr in ['edx_video_id', 'language_code'] if attr not in request.GET]
if missing:
return JsonResponse(
{'error': _(u'The following parameters are required: {missing}.').format(missing=', '.join(missing))},
status=400
)
edx_video_id = request.GET['edx_video_id']
language_code = request.GET['language_code']
transcript = get_video_transcript_data(video_ids=[edx_video_id], language_code=language_code)
if transcript:
name_and_extension = os.path.splitext(transcript['file_name'])
basename, file_format = name_and_extension[0], name_and_extension[1][1:]
transcript_filename = '{base_name}.{ext}'.format(base_name=basename.encode('utf8'), ext=Transcript.SRT)
transcript_content = Transcript.convert(
content=transcript['content'],
input_format=file_format,
output_format=Transcript.SRT
)
# Construct an HTTP response
response = HttpResponse(transcript_content, content_type=Transcript.mime_types[Transcript.SRT])
response['Content-Disposition'] = 'attachment; filename="{filename}"'.format(filename=transcript_filename)
else:
response = HttpResponseNotFound()
return response
def validate_transcript_upload_data(data, files):
"""
Validates video transcript file.
Arguments:
data: A request's data part.
files: A request's files part.
Returns:
None or String
If there is error returns error message otherwise None.
"""
error = None
# Validate the must have attributes - this error is unlikely to be faced by common users.
must_have_attrs = ['edx_video_id', 'language_code', 'new_language_code']
missing = [attr for attr in must_have_attrs if attr not in data]
if missing:
error = _(u'The following parameters are required: {missing}.').format(missing=', '.join(missing))
elif (
data['language_code'] != data['new_language_code'] and
data['new_language_code'] in get_available_transcript_languages([data['edx_video_id']])
):
error = _(u'A transcript with the "{language_code}" language code already exists.'.format(
language_code=data['new_language_code']
))
elif 'file' not in files:
error = _(u'A transcript file is required.')
return error
@login_required
@require_POST
def transcript_upload_handler(request, course_key_string):
"""
View to upload a transcript file.
Arguments:
request: A WSGI request object
course_key_string: Course key identifying a course
Transcript file, edx video id and transcript language are required.
Transcript file should be in SRT(SubRip) format.
Returns
- A 400 if any of the validation fails
- A 404 if the corresponding feature flag is disabled
- A 200 if transcript has been uploaded successfully
"""
# Check whether the feature is available for this course.
course_key = CourseKey.from_string(course_key_string)
if not VideoTranscriptEnabledFlag.feature_enabled(course_key):
return HttpResponseNotFound()
error = validate_transcript_upload_data(data=request.POST, files=request.FILES)
if error:
response = JsonResponse({'error': error}, status=400)
else:
edx_video_id = request.POST['edx_video_id']
language_code = request.POST['language_code']
new_language_code = request.POST['new_language_code']
transcript_file = request.FILES['file']
try:
# Convert SRT transcript into an SJSON format
# and upload it to S3.
sjson_subs = Transcript.convert(
content=transcript_file.read(),
input_format=Transcript.SRT,
output_format=Transcript.SJSON
)
create_or_update_video_transcript(
video_id=edx_video_id,
language_code=language_code,
metadata={
'provider': TranscriptProvider.CUSTOM,
'file_format': Transcript.SJSON,
'language_code': new_language_code
},
file_data=ContentFile(json.dumps(sjson_subs)),
)
response = JsonResponse(status=201)
except (TranscriptsGenerationException, UnicodeDecodeError):
response = JsonResponse(
{'error': _(u'There is a problem with this transcript file. Try to upload a different file.')},
status=400
)
return response

View File

@@ -30,9 +30,11 @@ from edxval.api import (
remove_transcript_preferences,
remove_video_for_course,
update_video_image,
update_video_status
update_video_status,
get_available_transcript_languages
)
from opaque_keys.edx.keys import CourseKey
from xmodule.video_module.transcripts_utils import Transcript
from contentstore.models import VideoUploadConfig
from contentstore.utils import reverse_course_url
@@ -75,10 +77,11 @@ MAX_UPLOAD_HOURS = 24
class TranscriptProvider(object):
"""
3rd Party Transcription Provider Enumeration
Transcription Provider Enumeration
"""
CIELO24 = 'Cielo24'
THREE_PLAY_MEDIA = '3PlayMedia'
CUSTOM = 'Custom'
class StatusDisplayStrings(object):
@@ -528,12 +531,20 @@ def _get_videos(course):
"""
Retrieves the list of videos from VAL corresponding to this course.
"""
is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id)
videos = list(get_videos_for_course(unicode(course.id), VideoSortField.created, SortDirection.desc))
# convert VAL's status to studio's Video Upload feature status.
for video in videos:
video["status"] = convert_video_status(video)
if is_video_transcript_enabled:
all_languages = get_all_transcript_languages()
video['transcripts'] = {
lang_code: all_languages[lang_code]
for lang_code in get_available_transcript_languages([video['edx_video_id']])
}
return videos
@@ -551,6 +562,9 @@ def _get_index_videos(course):
course_id = unicode(course.id)
attrs = ['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'courses']
if VideoTranscriptEnabledFlag.feature_enabled(course.id):
attrs += ['transcripts']
def _get_values(video):
"""
Get data for predefined video attributes.
@@ -570,6 +584,24 @@ def _get_index_videos(course):
]
def get_all_transcript_languages():
"""
Returns all possible languages for transcript.
"""
third_party_transcription_languages = {}
transcription_plans = get_3rd_party_transcription_plans()
cielo_fidelity = transcription_plans[TranscriptProvider.CIELO24]['fidelity']
# Get third party transcription languages.
third_party_transcription_languages.update(transcription_plans[TranscriptProvider.THREE_PLAY_MEDIA]['languages'])
third_party_transcription_languages.update(cielo_fidelity['MECHANICAL']['languages'])
third_party_transcription_languages.update(cielo_fidelity['PREMIUM']['languages'])
third_party_transcription_languages.update(cielo_fidelity['PROFESSIONAL']['languages'])
# Return combined system settings and 3rd party transcript languages.
return dict(settings.ALL_LANGUAGES, **third_party_transcription_languages)
def videos_index_html(course):
"""
Returns an HTML page to display previous video uploads and allow new ones
@@ -596,7 +628,8 @@ def videos_index_html(course):
'is_video_transcript_enabled': is_video_transcript_enabled,
'video_transcript_settings': None,
'active_transcript_preferences': None,
'transcript_credentials': None
'transcript_credentials': None,
'transcript_available_languages': None
}
if is_video_transcript_enabled:
@@ -609,11 +642,21 @@ def videos_index_html(course):
'transcript_credentials_handler',
unicode(course.id)
),
'transcript_download_handler_url': reverse_course_url(
'transcript_download_handler',
unicode(course.id)
),
'transcript_upload_handler_url': reverse_course_url(
'transcript_upload_handler',
unicode(course.id)
),
'transcription_plans': get_3rd_party_transcription_plans(),
'trancript_download_file_format': Transcript.SRT
}
context['active_transcript_preferences'] = get_transcript_preferences(unicode(course.id))
# Cached state for transcript providers' credentials (org-specific)
context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org)
context['transcript_available_languages'] = get_all_transcript_languages()
return render_to_response('videos_index.html', context)

View File

@@ -259,6 +259,7 @@
'js/spec/views/previous_video_upload_spec',
'js/spec/views/video_thumbnail_spec',
'js/spec/views/course_video_settings_spec',
'js/spec/views/video_transcripts_spec',
'js/spec/views/previous_video_upload_list_spec',
'js/spec/views/assets_spec',
'js/spec/views/baseview_spec',

View File

@@ -18,7 +18,8 @@ define([
transcriptOrganizationCredentials,
videoTranscriptSettings,
isVideoTranscriptEnabled,
videoImageSettings
videoImageSettings,
transcriptAvailableLanguages
) {
var activeView = new ActiveVideoUploadListView({
postUrl: videoHandlerUrl,
@@ -51,7 +52,11 @@ define([
videoHandlerUrl: videoHandlerUrl,
collection: updatedCollection,
encodingsDownloadUrl: encodingsDownloadUrl,
videoImageSettings: videoImageSettings
videoImageSettings: videoImageSettings,
videoTranscriptSettings: videoTranscriptSettings,
transcriptAvailableLanguages: transcriptAvailableLanguages,
videoSupportedFileFormats: videoSupportedFileFormats,
isVideoTranscriptEnabled: isVideoTranscriptEnabled
});
$contentWrapper.find('.wrapper-assets').replaceWith(updatedView.render().$el);
});
@@ -63,7 +68,11 @@ define([
videoHandlerUrl: videoHandlerUrl,
collection: new Backbone.Collection(previousUploads),
encodingsDownloadUrl: encodingsDownloadUrl,
videoImageSettings: videoImageSettings
videoImageSettings: videoImageSettings,
videoTranscriptSettings: videoTranscriptSettings,
transcriptAvailableLanguages: transcriptAvailableLanguages,
videoSupportedFileFormats: videoSupportedFileFormats,
isVideoTranscriptEnabled: isVideoTranscriptEnabled
});
$contentWrapper.append(activeView.render().$el);
$contentWrapper.append(previousView.render().$el);

View File

@@ -0,0 +1,351 @@
define(
['jquery', 'underscore', 'backbone', 'js/views/video_transcripts', 'js/views/previous_video_upload_list',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers'],
function($, _, Backbone, VideoTranscriptsView, PreviousVideoUploadListView, AjaxHelpers, TemplateHelpers) {
'use strict';
describe('VideoTranscriptsView', function() {
var videoTranscriptsView,
renderView,
verifyTranscriptStateInfo,
verifyMessage,
verifyDetailedErrorMessage,
createFakeTranscriptFile,
transcripts = {
en: 'English',
es: 'Spanish',
ur: 'Urdu'
},
edxVideoID = 'test-edx-video-id',
clientVideoID = 'Video client title name.mp4',
transcriptAvailableLanguages = {
en: 'English',
es: 'Spanish',
cn: 'Chinese',
ar: 'Arabic',
ur: 'Urdu'
},
TRANSCRIPT_DOWNLOAD_FILE_FORMAT = 'srt',
TRANSCRIPT_DOWNLOAD_URL = 'abc.com/transcript_download/course_id',
TRANSCRIPT_UPLOAD_URL = 'abc.com/transcript_upload/course_id',
videoSupportedFileFormats = ['.mov', '.mp4'],
videoTranscriptSettings = {
trancript_download_file_format: TRANSCRIPT_DOWNLOAD_FILE_FORMAT,
transcript_download_handler_url: TRANSCRIPT_DOWNLOAD_URL,
transcript_upload_handler_url: TRANSCRIPT_UPLOAD_URL
},
videoListView;
verifyTranscriptStateInfo = function($transcriptEl, transcriptLanguage) {
var $transcriptActionsEl = $transcriptEl.find('.transcript-actions'),
downloadTranscriptActionEl = $transcriptActionsEl.find('.download-transcript-button'),
uploadTranscriptActionEl = $transcriptActionsEl.find('.upload-transcript-button');
// Verify transcript data attributes
expect($transcriptEl.data('edx-video-id')).toEqual(edxVideoID);
expect($transcriptEl.data('language-code')).toEqual(transcriptLanguage);
// Verify transcript language dropdown has correct value set.
expect($transcriptEl.find('.transcript-language-menu').val(), transcriptLanguage);
// Verify transcript actions
expect(downloadTranscriptActionEl.html().trim(), 'Download');
expect(
downloadTranscriptActionEl.attr('href'),
TRANSCRIPT_DOWNLOAD_URL + '?edx_video_id=' + edxVideoID + '&language_code=' + transcriptLanguage
);
expect(uploadTranscriptActionEl.html().trim(), 'Replace');
};
verifyMessage = function($transcriptEl, status) {
var $transcriptStatusEl = $transcriptEl.find('.transcript-upload-status-container'),
statusData = videoTranscriptsView.transcriptUploadStatuses[status];
expect($transcriptStatusEl.hasClass(statusData.statusClass)).toEqual(true);
expect($transcriptStatusEl.find('span.fa').hasClass(statusData.iconClasses)).toEqual(true);
expect(
$transcriptStatusEl.find('.more-details-action').hasClass('hidden')
).toEqual(statusData.hiddenClass === 'hidden');
expect(
$transcriptStatusEl.find('.transcript-detail-status').html().trim()
).toEqual(statusData.shortMessage);
};
verifyDetailedErrorMessage = function($transcriptEl, expectedTitle, expectedMessage) {
$transcriptEl.find('.more-details-action').click();
expect($('#prompt-warning-title').text().trim()).toEqual(expectedTitle);
expect($('#prompt-warning-description').text().trim()).toEqual(expectedMessage);
};
createFakeTranscriptFile = function(transcriptFileName) {
var transcriptFileName = transcriptFileName || 'test-transcript.srt', // eslint-disable-line no-redeclare, max-len
size = 100,
type = '';
return new File([new Blob([Array(size).join('i')], {type: type})], transcriptFileName);
};
renderView = function(availableTranscripts, isVideoTranscriptEnabled) {
var videoViewIndex = 0,
isVideoTranscriptEnabled = isVideoTranscriptEnabled || _.isUndefined(isVideoTranscriptEnabled), // eslint-disable-line max-len, no-redeclare
videoData = {
client_video_id: clientVideoID,
edx_video_id: edxVideoID,
created: '2014-11-25T23:13:05',
transcripts: availableTranscripts
},
videoCollection = new Backbone.Collection([new Backbone.Model(videoData)]);
videoListView = new PreviousVideoUploadListView({
collection: videoCollection,
videoImageSettings: {},
videoTranscriptSettings: videoTranscriptSettings,
transcriptAvailableLanguages: transcriptAvailableLanguages,
videoSupportedFileFormats: videoSupportedFileFormats,
isVideoTranscriptEnabled: isVideoTranscriptEnabled
});
videoListView.setElement($('.wrapper-assets'));
videoListView.render();
videoTranscriptsView = videoListView.itemViews[videoViewIndex].videoTranscriptsView;
};
beforeEach(function() {
setFixtures(
'<div id="page-prompt"></div>' +
'<section class="wrapper-assets"></section>'
);
TemplateHelpers.installTemplate('previous-video-upload-list');
renderView(transcripts);
});
it('renders as expected', function() {
// Verify transcript container is present.
expect(videoListView.$el.find('.show-video-transcripts-container')).toExist();
// Veirfy transcript column header is present.
expect(videoListView.$el.find('.js-table-head .video-head-col.transcripts-col')).toExist();
// Verify transcript data column is present.
expect(videoListView.$el.find('.js-table-body .transcripts-col')).toExist();
// Verify view has initiallized.
expect(_.isUndefined(videoTranscriptsView)).toEqual(false);
});
it('does not render transcripts view if feature is disabled', function() {
renderView(transcripts, false);
// Verify transcript container is not present.
expect(videoListView.$el.find('.show-video-transcripts-container')).not.toExist();
// Veirfy transcript column header is not present.
expect(videoListView.$el.find('.js-table-head .video-head-col.transcripts-col')).not.toExist();
// Verify transcript data column is not present.
expect(videoListView.$el.find('.js-table-body .transcripts-col')).not.toExist();
// Verify view has not initiallized.
expect(_.isUndefined(videoTranscriptsView)).toEqual(true);
});
it('does not show list of transcripts initially', function() {
expect(
videoTranscriptsView.$el.find('.show-video-transcripts-wrapper').hasClass('hidden')
).toEqual(true);
expect(videoTranscriptsView.$el.find('.toggle-show-transcripts-button-text').html().trim()).toEqual(
'Show transcripts (' + _.size(transcripts) + ')'
);
});
it('shows list of transcripts when clicked on show transcript button', function() {
// Verify transcript container is hidden
expect(
videoTranscriptsView.$el.find('.show-video-transcripts-wrapper').hasClass('hidden')
).toEqual(true);
// Verify initial button text
expect(videoTranscriptsView.$el.find('.toggle-show-transcripts-button-text').html().trim()).toEqual(
'Show transcripts (' + _.size(transcripts) + ')'
);
videoTranscriptsView.$el.find('.toggle-show-transcripts-button').click();
// Verify transcript container is not hidden
expect(
videoTranscriptsView.$el.find('.show-video-transcripts-wrapper').hasClass('hidden')
).toEqual(false);
// Verify button text is changed.
expect(videoTranscriptsView.$el.find('.toggle-show-transcripts-button-text').html().trim()).toEqual(
'Hide transcripts (' + _.size(transcripts) + ')'
);
});
it('hides list of transcripts when clicked on hide transcripts button', function() {
// Click to show transcripts first.
videoTranscriptsView.$el.find('.toggle-show-transcripts-button').click();
// Verify button text.
expect(videoTranscriptsView.$el.find('.toggle-show-transcripts-button-text').html().trim()).toEqual(
'Hide transcripts (' + _.size(transcripts) + ')'
);
// Verify transcript container is not hidden
expect(
videoTranscriptsView.$el.find('.show-video-transcripts-wrapper').hasClass('hidden')
).toEqual(false);
videoTranscriptsView.$el.find('.toggle-show-transcripts-button').click();
// Verify button text is changed.
expect(videoTranscriptsView.$el.find('.toggle-show-transcripts-button-text').html().trim()).toEqual(
'Show transcripts (' + _.size(transcripts) + ')'
);
// Verify transcript container is hidden
expect(
videoTranscriptsView.$el.find('.show-video-transcripts-wrapper').hasClass('hidden')
).toEqual(true);
});
it('renders appropriate text when no transcript is available', function() {
// Render view with no transcripts
renderView({});
// Verify appropriate text is shown
expect(
videoTranscriptsView.$el.find('.transcripts-empty-text').html()
).toEqual('No transcript uploaded.');
});
it('renders correct transcript attributes', function() {
var $transcriptEl;
// Show transcripts
videoTranscriptsView.$el.find('.toggle-show-transcripts-button').click();
expect(videoTranscriptsView.$el.find('.show-video-transcript-content').length).toEqual(
_.size(transcripts)
);
_.each(transcripts, function(langaugeText, languageCode) {
$transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
// Verify correct transcript title is set.
expect($transcriptEl.find('.transcript-title').html()).toEqual(
'Video client title n_' + languageCode + '.' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT
);
// Verify transcript is rendered with correct info.
verifyTranscriptStateInfo($transcriptEl, languageCode);
});
});
it('can upload transcript', function() {
var languageCode = 'en',
newLanguageCode = 'ar',
requests = AjaxHelpers.requests(this),
$transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
// Verify correct transcript title is set.
expect($transcriptEl.find('.transcript-title').html()).toEqual(
'Video client title n_' + languageCode + '.' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT
);
// Select a language
$transcriptEl.find('.transcript-language-menu').val(newLanguageCode);
$transcriptEl.find('.upload-transcript-button').click();
// Add transcript to upload queue and send POST request to upload transcript.
$transcriptEl.find('.upload-transcript-input').fileupload('add', {files: [createFakeTranscriptFile()]});
// Verify if POST request received for image upload
AjaxHelpers.expectRequest(
requests,
'POST',
TRANSCRIPT_UPLOAD_URL
);
// Send successful upload response
AjaxHelpers.respondWithJson(requests, {});
// Verify correct transcript title is set.
expect($transcriptEl.find('.transcript-title').html()).toEqual(
'Video client title n_' + newLanguageCode + '.' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT
);
verifyMessage($transcriptEl, 'uploaded');
// Verify transcript is rendered with correct info.
verifyTranscriptStateInfo($transcriptEl, newLanguageCode);
});
it('shows error state correctly', function() {
var languageCode = 'en',
requests = AjaxHelpers.requests(this),
errorMessage = 'Transcript failed error message',
$transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
$transcriptEl.find('.upload-transcript-button').click();
// Add transcript to upload queue and send POST request to upload transcript.
$transcriptEl.find('.upload-transcript-input').fileupload('add', {files: [createFakeTranscriptFile()]});
// Server response with bad request.
AjaxHelpers.respondWithError(requests, 400, {error: errorMessage});
verifyMessage($transcriptEl, 'failed');
// verify detailed error message
verifyDetailedErrorMessage(
$transcriptEl,
videoTranscriptsView.defaultFailureTitle,
errorMessage
);
// Verify transcript is rendered with correct info.
verifyTranscriptStateInfo($transcriptEl, languageCode);
});
it('should show error message in case of server error', function() {
var languageCode = 'en',
requests = AjaxHelpers.requests(this),
$transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
$transcriptEl.find('.upload-transcript-button').click();
// Add transcript to upload queue and send POST request to upload transcript.
$transcriptEl.find('.upload-transcript-input').fileupload('add', {files: [createFakeTranscriptFile()]});
AjaxHelpers.respondWithError(requests, 500);
verifyMessage($transcriptEl, 'failed');
// verify detailed error message
verifyDetailedErrorMessage(
$transcriptEl,
videoTranscriptsView.defaultFailureTitle,
videoTranscriptsView.defaultFailureMessage
);
// Verify transcript is rendered with correct info.
verifyTranscriptStateInfo($transcriptEl, languageCode);
});
it('should show error message in case of unsupported transcript file format', function() {
var languageCode = 'en',
transcriptFileName = 'unsupported-transcript-file-format.txt',
errorMessage = 'This file type is not supported. Supported file type is ' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT + '.', // eslint-disable-line max-len
$transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
$transcriptEl.find('.upload-transcript-button').click();
// Add transcript to upload queue and send POST request to upload transcript.
$transcriptEl.find('.upload-transcript-input').fileupload('add', {
files: [createFakeTranscriptFile(transcriptFileName)]
});
verifyMessage($transcriptEl, 'validationFailed');
// verify detailed error message
verifyDetailedErrorMessage(
$transcriptEl,
videoTranscriptsView.defaultFailureTitle,
errorMessage
);
// Verify transcript is rendered with correct info.
verifyTranscriptStateInfo($transcriptEl, languageCode);
});
});
}
);

View File

@@ -1,10 +1,10 @@
define(
['underscore', 'gettext', 'js/utils/date_utils', 'js/views/baseview', 'common/js/components/views/feedback_prompt',
'common/js/components/views/feedback_notification', 'js/views/video_thumbnail',
'common/js/components/views/feedback_notification', 'js/views/video_thumbnail', 'js/views/video_transcripts',
'common/js/components/utils/view_utils', 'edx-ui-toolkit/js/utils/html-utils',
'text!templates/previous-video-upload.underscore'],
function(_, gettext, DateUtils, BaseView, PromptView, NotificationView, VideoThumbnailView, ViewUtils, HtmlUtils,
previousVideoUploadTemplate) {
function(_, gettext, DateUtils, BaseView, PromptView, NotificationView, VideoThumbnailView, VideoTranscriptsView,
ViewUtils, HtmlUtils, previousVideoUploadTemplate) {
'use strict';
var PreviousVideoUploadView = BaseView.extend({
@@ -20,6 +20,7 @@ define(
this.template = HtmlUtils.template(previousVideoUploadTemplate);
this.videoHandlerUrl = options.videoHandlerUrl;
this.videoImageUploadEnabled = options.videoImageSettings.video_image_upload_enabled;
this.isVideoTranscriptEnabled = options.isVideoTranscriptEnabled;
if (this.videoImageUploadEnabled) {
this.videoThumbnailView = new VideoThumbnailView({
@@ -29,11 +30,22 @@ define(
videoImageSettings: options.videoImageSettings
});
}
if (this.isVideoTranscriptEnabled) {
this.videoTranscriptsView = new VideoTranscriptsView({
transcripts: this.model.get('transcripts'),
edxVideoID: this.model.get('edx_video_id'),
clientVideoID: this.model.get('client_video_id'),
transcriptAvailableLanguages: options.transcriptAvailableLanguages,
videoSupportedFileFormats: options.videoSupportedFileFormats,
videoTranscriptSettings: options.videoTranscriptSettings
});
}
},
render: function() {
var renderedAttributes = {
videoImageUploadEnabled: this.videoImageUploadEnabled,
isVideoTranscriptEnabled: this.isVideoTranscriptEnabled,
created: DateUtils.renderDate(this.model.get('created')),
status: this.model.get('status')
};
@@ -47,6 +59,9 @@ define(
if (this.videoImageUploadEnabled) {
this.videoThumbnailView.setElement(this.$('.thumbnail-col')).render();
}
if (this.isVideoTranscriptEnabled) {
this.videoTranscriptsView.setElement(this.$('.transcripts-col')).render();
}
return this;
},

View File

@@ -1,22 +1,28 @@
define(
['jquery', 'underscore', 'backbone', 'js/views/baseview', 'js/views/previous_video_upload'],
function($, _, Backbone, BaseView, PreviousVideoUploadView) {
['jquery', 'underscore', 'backbone', 'js/views/baseview', 'edx-ui-toolkit/js/utils/html-utils',
'js/views/previous_video_upload', 'text!templates/previous-video-upload-list.underscore'],
function($, _, Backbone, BaseView, HtmlUtils, PreviousVideoUploadView, previousVideoUploadListTemplate) {
'use strict';
var PreviousVideoUploadListView = BaseView.extend({
tagName: 'section',
className: 'wrapper-assets',
initialize: function(options) {
this.template = this.loadTemplate('previous-video-upload-list');
this.template = HtmlUtils.template(previousVideoUploadListTemplate);
this.encodingsDownloadUrl = options.encodingsDownloadUrl;
this.videoImageUploadEnabled = options.videoImageSettings.video_image_upload_enabled;
this.isVideoTranscriptEnabled = options.isVideoTranscriptEnabled;
this.itemViews = this.collection.map(function(model) {
return new PreviousVideoUploadView({
videoImageUploadURL: options.videoImageUploadURL,
defaultVideoImageURL: options.defaultVideoImageURL,
videoHandlerUrl: options.videoHandlerUrl,
videoImageSettings: options.videoImageSettings,
model: model
videoTranscriptSettings: options.videoTranscriptSettings,
model: model,
transcriptAvailableLanguages: options.transcriptAvailableLanguages,
videoSupportedFileFormats: options.videoSupportedFileFormats,
isVideoTranscriptEnabled: options.isVideoTranscriptEnabled
});
});
},
@@ -24,10 +30,16 @@ define(
render: function() {
var $el = this.$el,
$tabBody;
$el.html(this.template({
encodingsDownloadUrl: this.encodingsDownloadUrl,
videoImageUploadEnabled: this.videoImageUploadEnabled
}));
HtmlUtils.setHtml(
this.$el,
this.template({
encodingsDownloadUrl: this.encodingsDownloadUrl,
videoImageUploadEnabled: this.videoImageUploadEnabled,
isVideoTranscriptEnabled: this.isVideoTranscriptEnabled
})
);
$tabBody = $el.find('.js-table-body');
_.each(this.itemViews, function(view) {
$tabBody.append(view.render().$el);

View File

@@ -0,0 +1,292 @@
define(
['underscore', 'gettext', 'js/views/baseview', 'common/js/components/views/feedback_prompt',
'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/string-utils',
'text!templates/video-transcripts.underscore', 'text!templates/video-transcript-upload-status.underscore'],
function(_, gettext, BaseView, PromptView, HtmlUtils, StringUtils, videoTranscriptsTemplate,
videoTranscriptUploadStatusTemplate) {
'use strict';
var VideoTranscriptsView = BaseView.extend({
tagName: 'div',
events: {
'click .toggle-show-transcripts-button': 'toggleShowTranscripts',
'click .upload-transcript-button': 'chooseFile',
'click .more-details-action': 'showUploadFailureMessage'
},
initialize: function(options) {
this.transcripts = options.transcripts;
this.edxVideoID = options.edxVideoID;
this.clientVideoID = options.clientVideoID;
this.transcriptAvailableLanguages = options.transcriptAvailableLanguages;
this.videoSupportedFileFormats = options.videoSupportedFileFormats;
this.videoTranscriptSettings = options.videoTranscriptSettings;
this.template = HtmlUtils.template(videoTranscriptsTemplate);
this.transcriptUploadStatusTemplate = HtmlUtils.template(videoTranscriptUploadStatusTemplate);
this.defaultFailureTitle = gettext('The file could not be uploaded.');
this.defaultFailureMessage = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); // eslint-disable-line max-len
this.transcriptUploadStatuses = {
uploaded: {
statusClass: 'success',
iconClasses: 'fa-check',
shortMessage: 'Transcript uploaded.',
hiddenClass: 'hidden'
},
uploading: {
statusClass: '',
iconClasses: 'fa-spinner fa-pulse',
shortMessage: 'Uploading transcript.',
hiddenClass: 'hidden'
},
failed: {
statusClass: 'error',
iconClasses: 'fa-warning',
shortMessage: 'Upload failed.',
hiddenClass: ''
},
validationFailed: {
statusClass: 'error',
iconClasses: 'fa-warning',
shortMessage: 'Validation failed.',
hiddenClass: ''
}
};
// This is needed to attach transcript methods to this object while uploading.
_.bindAll(
this, 'render', 'chooseFile', 'transcriptSelected', 'transcriptUploadSucceeded',
'transcriptUploadFailed'
);
},
/*
Sorts object by value and returns a sorted array.
*/
sortByValue: function(itemObject) {
var sortedArray = [];
_.each(itemObject, function(value, key) {
// Push each JSON Object entry in array by [value, key]
sortedArray.push([value, key]);
});
return sortedArray.sort();
},
/*
Returns transcript title.
*/
getTranscriptClientTitle: function() {
var clientTitle = this.clientVideoID;
// Remove video file extension for transcript title.
_.each(this.videoSupportedFileFormats, function(videoFormat) {
clientTitle = clientTitle.replace(videoFormat, '');
});
return clientTitle.substring(0, 20);
},
/*
Returns transcript download link.
*/
getTranscriptDownloadLink: function(edxVideoID, transcriptLanguageCode, transcriptDownloadHandlerUrl) {
return StringUtils.interpolate(
'{transcriptDownloadHandlerUrl}?edx_video_id={edxVideoID}&language_code={transcriptLanguageCode}',
{
transcriptDownloadHandlerUrl: transcriptDownloadHandlerUrl,
edxVideoID: edxVideoID,
transcriptLanguageCode: transcriptLanguageCode
}
);
},
/*
Toggles Show/Hide transcript button and transcripts container.
*/
toggleShowTranscripts: function() {
var $transcriptsWrapperEl = this.$el.find('.show-video-transcripts-wrapper');
// Toggle show transcript wrapper.
$transcriptsWrapperEl.toggleClass('hidden');
// Toggle button text.
HtmlUtils.setHtml(
this.$el.find('.toggle-show-transcripts-button-text'),
StringUtils.interpolate(
gettext('{toggleShowTranscriptText} transcripts ({totalTranscripts})'),
{
toggleShowTranscriptText: $transcriptsWrapperEl.hasClass('hidden') ? gettext('Show') : gettext('Hide'), // eslint-disable-line max-len
totalTranscripts: _.size(this.transcripts)
}
)
);
// Toggle icon class.
if ($transcriptsWrapperEl.hasClass('hidden')) {
this.$el.find('.toggle-show-transcripts-icon').removeClass('fa-caret-down').addClass('fa-caret-right'); // eslint-disable-line max-len
} else {
this.$el.find('.toggle-show-transcripts-icon').removeClass('fa-caret-right').addClass('fa-caret-down'); // eslint-disable-line max-len
}
},
validateTranscriptUpload: function(file) {
var errorMessage = '',
fileName = file.name,
fileType = fileName.substr(fileName.lastIndexOf('.') + 1);
if (fileType !== this.videoTranscriptSettings.trancript_download_file_format) {
errorMessage = gettext(
'This file type is not supported. Supported file type is {supportedFileFormat}.'
)
.replace('{supportedFileFormat}', this.videoTranscriptSettings.trancript_download_file_format);
}
return errorMessage;
},
chooseFile: function(event) {
var $transcriptContainer = $(event.target).parents('.show-video-transcript-content'),
$transcriptUploadEl = $transcriptContainer.find('.upload-transcript-input');
$transcriptUploadEl.fileupload({
url: this.videoTranscriptSettings.transcript_upload_handler_url,
add: this.transcriptSelected,
done: this.transcriptUploadSucceeded,
fail: this.transcriptUploadFailed,
formData: {
edx_video_id: this.edxVideoID,
language_code: $transcriptContainer.attr('data-language-code'),
new_language_code: $transcriptContainer.find('.transcript-language-menu').val()
}
});
$transcriptUploadEl.click();
},
transcriptSelected: function(event, data) {
var errorMessage,
$transcriptContainer = $(event.target).parents('.show-video-transcript-content');
errorMessage = this.validateTranscriptUpload(data.files[0]);
if (!errorMessage) {
// Do not trigger global AJAX error handler
data.global = false; // eslint-disable-line no-param-reassign
data.submit();
this.renderMessage($transcriptContainer, 'uploading');
} else {
// Reset transcript language back to original.
$transcriptContainer.find('.transcript-language-menu').val($transcriptContainer.attr('data-language-code')); // eslint-disable-line max-len
this.renderMessage($transcriptContainer, 'validationFailed', errorMessage);
}
},
transcriptUploadSucceeded: function(event, data) {
var languageCode = data.formData.language_code,
newLanguageCode = data.formData.new_language_code,
$transcriptContainer = this.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
$transcriptContainer.attr('data-language-code', newLanguageCode);
$transcriptContainer.find('.download-transcript-button').attr(
'href',
this.getTranscriptDownloadLink(
this.edxVideoID,
newLanguageCode,
this.videoTranscriptSettings.transcript_download_handler_url
)
);
HtmlUtils.setHtml(
$transcriptContainer.find('.transcript-title'),
StringUtils.interpolate(gettext('{transcriptClientTitle}_{transcriptLanguageCode}.{fileExtension}'),
{
transcriptClientTitle: this.getTranscriptClientTitle(),
transcriptLanguageCode: newLanguageCode,
fileExtension: this.videoTranscriptSettings.trancript_download_file_format
}
)
);
this.renderMessage($transcriptContainer, 'uploaded');
},
transcriptUploadFailed: function(event, data) {
var errorMessage,
languageCode = data.formData.language_code,
$transcriptContainer = this.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len
try {
errorMessage = JSON.parse(data.jqXHR.responseText).error;
errorMessage = errorMessage || this.defaultFailureMessage;
} catch (error) {
errorMessage = this.defaultFailureMessage;
}
// Reset transcript language back to original.
$transcriptContainer.find('.transcript-language-menu').val(languageCode);
this.renderMessage($transcriptContainer, 'failed', errorMessage);
},
clearMessage: function() {
var $transcriptStatusesEl = this.$el.find('.transcript-upload-status-container');
// Clear all message containers
HtmlUtils.setHtml($transcriptStatusesEl, '');
$transcriptStatusesEl.removeClass('success error');
},
renderMessage: function($transcriptContainer, status, errorMessage) {
var statusData = this.transcriptUploadStatuses[status],
$transcriptStatusEl = $transcriptContainer.find('.transcript-upload-status-container');
// If a messge is already present above the video transcript element, remove it.
this.clearMessage();
HtmlUtils.setHtml(
$transcriptStatusEl,
this.transcriptUploadStatusTemplate({
status: statusData.statusClass,
iconClasses: statusData.iconClasses,
shortMessage: gettext(statusData.shortMessage),
errorMessage: errorMessage || '',
hiddenClass: statusData.hiddenClass
})
);
$transcriptStatusEl.addClass(statusData.statusClass);
},
showUploadFailureMessage: function(event) {
var errorMessage = $(event.target).data('error-message');
return new PromptView.Warning({
title: this.defaultFailureTitle,
message: errorMessage,
actions: {
primary: {
text: gettext('Close'),
click: function(prompt) {
return prompt.hide();
}
}
}
}).show();
},
/*
Renders transcripts view.
*/
render: function() {
HtmlUtils.setHtml(
this.$el,
this.template({
transcripts: this.transcripts,
transcriptAvailableLanguages: this.sortByValue(this.transcriptAvailableLanguages),
edxVideoID: this.edxVideoID,
transcriptClientTitle: this.getTranscriptClientTitle(),
transcriptFileFormat: this.videoTranscriptSettings.trancript_download_file_format,
getTranscriptDownloadLink: this.getTranscriptDownloadLink,
transcriptDownloadHandlerUrl: this.videoTranscriptSettings.transcript_download_handler_url
})
);
return this;
}
});
return VideoTranscriptsView;
}
);

View File

@@ -8,7 +8,7 @@
@extend %t-copy;
vertical-align: bottom;
margin-right: ($baseline/5);
@include margin-right($baseline/45);
}
}
@@ -17,6 +17,52 @@
top: 0 !important;
}
.button-link {
background:none;
border:none;
padding:0;
color: $ui-link-color;
cursor:pointer
}
.show-video-transcripts-wrapper {
display: block;
.button-link {
color: $ui-link-color !important;
}
}
.hidden {
display: none;
}
.show-video-transcript-content {
margin-top: ($baseline/2);
.transcript-upload-status-container {
.video-transcript-detail-status, .more-details-action {
@include font-size(12);
@include line-height(12);
@include margin-left($baseline/4);
}
}
.transcript-upload-status-container.error {
color: $color-error;
}
.transcript-upload-status-container.success {
color: $color-ready;
}
.transcript-language-menu {
display: block;
width: 200px;
}
}
.course-video-settings-container {
position: absolute;
overflow: scroll;
@@ -30,14 +76,6 @@
-moz-box-shadow: -3px 0px 3px 0px rgba(153,153,153,0.3);
box-shadow: -3px 0px 3px 0px rgba(153,153,153,0.3);
.button-link {
background:none;
border:none;
padding:0;
color: $ui-link-color;
cursor:pointer
}
.action-close-wrapper {
.action-close-course-video-settings {
width: 100%;
@@ -416,6 +454,10 @@
width: 25%;
}
.transcripts-col {
width: 17%;
}
.thumbnail-col, .video-id-col {
width: 15%;
}

View File

@@ -14,6 +14,9 @@
<div class="video-head-col video-col name-col"><%- gettext("Name") %></div>
<div class="video-head-col video-col date-col"><%- gettext("Date Added") %></div>
<div class="video-head-col video-col video-id-col"><%- gettext("Video ID") %></div>
<% if (isVideoTranscriptEnabled) { %>
<div class="video-head-col video-col transcripts-col"><%- gettext("Transcripts") %></div>
<% } %>
<div class="video-head-col video-col status-col"><%- gettext("Status") %></div>
<div class="video-head-col video-col actions-col"><%- gettext("Action") %></div>
</div>

View File

@@ -5,6 +5,9 @@
<div class="video-col name-col"><%- client_video_id %></div>
<div class="video-col date-col"><%- created %></div>
<div class="video-col video-id-col"><%- edx_video_id %></div>
<% if (isVideoTranscriptEnabled) { %>
<div class="video-col transcripts-col"></div>
<% } %>
<div class="video-col status-col"><%- status %></div>
<div class="video-col actions-col">
<ul class="actions-list">

View File

@@ -0,0 +1,5 @@
<span class="fa <%- iconClasses %>" aria-hidden="true"></span>
<span class='transcript-detail-status'><%- shortMessage %></span>
<button class='button-link more-details-action <%- hiddenClass %>' data-error-message='<%- errorMessage %>'>
<%- gettext('Read more') %>
</button>

View File

@@ -0,0 +1,42 @@
<div class='show-video-transcripts-container'>
<% if (_.size(transcripts)) { %>
<button class="button-link toggle-show-transcripts-button">
<strong>
<span class="icon fa fa-caret-right toggle-show-transcripts-icon" aria-hidden="true"></span>
<span class="toggle-show-transcripts-button-text">
<%- StringUtils.interpolate(gettext('Show transcripts ({totalTranscripts})'), {totalTranscripts: _.size(transcripts)})%>
</span>
</strong>
</button>
<% } else { %>
<span class='transcripts-empty-text'><%- gettext('No transcript uploaded.') %></span>
<% }%>
<div class='show-video-transcripts-wrapper hidden'>
<% _.each(transcripts, function(transcriptLanguageText, transcriptLanguageCode){ %>
<% selectedLanguageCodes = _.keys(_.omit(transcripts, transcriptLanguageCode)); %>
<div class='show-video-transcript-content' data-edx-video-id="<%- edxVideoID %>" data-language-code="<%- transcriptLanguageCode %>">
<div class='transcript-upload-status-container'></div>
<strong class='transcript-title'><%- StringUtils.interpolate(gettext('{transcriptClientTitle}_{transcriptLanguageCode}.{fileExtension}'), {transcriptClientTitle: transcriptClientTitle, transcriptLanguageCode: transcriptLanguageCode, fileExtension: transcriptFileFormat}) %></strong>
<select class='transcript-language-menu'>
<option value=''>Select Language</option>
<% _.each(transcriptAvailableLanguages, function(availableLanguage){ %>
<% if (!_.contains(selectedLanguageCodes, availableLanguage[1])) { %>
<option value='<%- availableLanguage[1] %>' <%- transcriptLanguageCode === availableLanguage[1] ? 'selected': '' %>><%- availableLanguage[0] %></option>
<% } %>
<% }) %>
</select>
<input class="upload-transcript-input hidden" type="file" name="file" accept=".<%- transcriptFileFormat %>"/>
<div class='transcript-actions'>
<a
class="button-link download-transcript-button"
href="<%- getTranscriptDownloadLink(edxVideoID, transcriptLanguageCode, transcriptDownloadHandlerUrl) %>"
>
<%- gettext('Download') %>
</a>
<span class='transcript-actions-separator'> | </span>
<button class="button-link upload-transcript-button"><%- gettext('Replace') %></button>
</div>
</div>
<% }) %>
</div>
</div>

View File

@@ -42,7 +42,8 @@
${transcript_credentials | 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}
${video_image_settings | n, dump_js_escaped_json},
${transcript_available_languages | n, dump_js_escaped_json}
);
});
</%block>

View File

@@ -142,6 +142,10 @@ urlpatterns = [
contentstore.views.transcript_preferences_handler, name='transcript_preferences_handler'),
url(r'^transcript_credentials/{}$'.format(settings.COURSE_KEY_PATTERN),
contentstore.views.transcript_credentials_handler, name='transcript_credentials_handler'),
url(r'^transcript_download/{}$'.format(settings.COURSE_KEY_PATTERN),
contentstore.views.transcript_download_handler, name='transcript_download_handler'),
url(r'^transcript_upload/{}$'.format(settings.COURSE_KEY_PATTERN),
contentstore.views.transcript_upload_handler, name='transcript_upload_handler'),
url(r'^video_encodings_download/{}$'.format(settings.COURSE_KEY_PATTERN),
contentstore.views.video_encodings_download, name='video_encodings_download'),
url(r'^group_configurations/{}$'.format(settings.COURSE_KEY_PATTERN),

View File

@@ -9,6 +9,7 @@ import json
import requests
import logging
from pysrt import SubRipTime, SubRipItem, SubRipFile
from pysrt.srtexc import Error
from lxml import etree
from HTMLParser import HTMLParser
@@ -284,6 +285,32 @@ def generate_srt_from_sjson(sjson_subs, speed):
return output
def generate_sjson_from_srt(srt_subs):
"""
Generate transcripts from sjson to SubRip (*.srt).
Arguments:
srt_subs(SubRip): "SRT" subs object
Returns:
Subs converted to "SJSON" format.
"""
sub_starts = []
sub_ends = []
sub_texts = []
for sub in srt_subs:
sub_starts.append(sub.start.ordinal)
sub_ends.append(sub.end.ordinal)
sub_texts.append(sub.text.replace('\n', ' '))
sjson_subs = {
'start': sub_starts,
'end': sub_ends,
'text': sub_texts
}
return sjson_subs
def copy_or_rename_transcript(new_name, old_name, item, delete_old=False, user=None):
"""
Renames `old_name` transcript file in storage to `new_name`.
@@ -544,10 +571,13 @@ class Transcript(object):
"""
Container for transcript methods.
"""
SRT = 'srt'
TXT = 'txt'
SJSON = 'sjson'
mime_types = {
'srt': 'application/x-subrip; charset=utf-8',
'txt': 'text/plain; charset=utf-8',
'sjson': 'application/json',
SRT: 'application/x-subrip; charset=utf-8',
TXT: 'text/plain; charset=utf-8',
SJSON: 'application/json',
}
@staticmethod
@@ -556,7 +586,10 @@ class Transcript(object):
Convert transcript `content` from `input_format` to `output_format`.
Accepted input formats: sjson, srt.
Accepted output format: srt, txt.
Accepted output format: srt, txt, sjson.
Raises:
TranscriptsGenerationException: On parsing the invalid srt content during conversion from srt to sjson.
"""
assert input_format in ('srt', 'sjson')
assert output_format in ('txt', 'srt', 'sjson')
@@ -571,7 +604,17 @@ class Transcript(object):
return HTMLParser().unescape(text)
elif output_format == 'sjson':
raise NotImplementedError
try:
# With error handling (set to 'ERROR_RAISE'), we will be getting
# the exception if something went wrong in parsing the transcript.
srt_subs = SubRipFile.from_string(
content.decode('utf8'),
error_handling=SubRipFile.ERROR_RAISE
)
except Error as ex: # Base exception from pysrt
raise TranscriptsGenerationException(ex.message)
return generate_sjson_from_srt(srt_subs)
if input_format == 'sjson':

View File

@@ -1562,9 +1562,11 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase):
create_or_update_video_transcript(
video_id=self.descriptor.edx_video_id,
language_code='ar',
file_name='ext101.srt',
file_format='srt',
provider='Cielo24',
metadata={
'provider': 'Cielo24',
'file_name': 'ext101.srt',
'file_format': 'srt'
}
)
actual = self.descriptor.definition_to_xml(resource_fs=None)
@@ -1590,9 +1592,11 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase):
create_or_update_video_transcript(
video_id=external_video_id,
language_code='ar',
file_name='ext101.srt',
file_format='srt',
provider='Cielo24',
metadata={
'provider': 'Cielo24',
'file_name': 'ext101.srt',
'file_format': 'srt'
}
)
actual = self.descriptor.definition_to_xml(resource_fs=None)

View File

@@ -60,7 +60,7 @@ edx-organizations==0.4.9
edx-rest-api-client==1.7.1
edx-search==1.1.0
edx-submissions==2.0.12
edxval==0.1.6
edxval==0.1.7
event-tracking==0.2.4
feedparser==5.1.3
firebase-token-generator==1.3.2