Merge pull request #17052 from edx/show-transcripts-feature
Show transcripts feature
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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.'
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
351
cms/static/js/spec/views/video_transcripts_spec.js
Normal file
351
cms/static/js/spec/views/video_transcripts_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
292
cms/static/js/views/video_transcripts.js
Normal file
292
cms/static/js/views/video_transcripts.js
Normal 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;
|
||||
}
|
||||
);
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
42
cms/templates/js/video-transcripts.underscore
Normal file
42
cms/templates/js/video-transcripts.underscore
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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':
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user