Merge pull request #17718 from edx/transcripts-phase-2
Transcripts phase 2
This commit is contained in:
@@ -261,10 +261,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase):
|
||||
u'[Transcript migration] process for ge transcript started'),
|
||||
(LOGGER_NAME,
|
||||
'ERROR',
|
||||
'[Transcript migration] Exception: u"SON(['
|
||||
'(\'category\', \'asset\'), (\'name\', u\'not_found.srt\'),'
|
||||
' (\'course\', u\'{}\'), (\'tag\', \'c4x\'), (\'org\', u\'{}\'),'
|
||||
' (\'revision\', None)])"'.format(self.course_2.id.course, self.course_2.id.org)),
|
||||
"[Transcript migration] Exception: u'No transcript for `ge` language'"),
|
||||
(LOGGER_NAME,
|
||||
'INFO',
|
||||
u'[Transcript migration] process for course {} ended. Processed 1 transcripts'.format(
|
||||
@@ -272,11 +269,8 @@ class TestMigrateTranscripts(ModuleStoreTestCase):
|
||||
)),
|
||||
(LOGGER_NAME,
|
||||
'INFO',
|
||||
"[Transcript migration] Result: Failed: language ge of video test_edx_video_id_2 with exception SON(["
|
||||
"('category', 'asset'), ('name', u'not_found.srt'), ('course', u'{}'),"
|
||||
" ('tag', 'c4x'), ('org', u'{}'), ('revision', None)])".format(
|
||||
self.course_2.id.course, self.course_2.id.org)
|
||||
)
|
||||
"[Transcript migration] Result: Failed: language ge of video test_edx_video_id_2 with exception "
|
||||
"No transcript for `ge` language")
|
||||
)
|
||||
|
||||
with LogCapture(LOGGER_NAME, level=logging.INFO) as logger:
|
||||
|
||||
@@ -191,7 +191,9 @@ class TestYoutubeSubsBase(SharedModuleStoreTestCase):
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
class TestDownloadYoutubeSubs(TestYoutubeSubsBase):
|
||||
"""Tests for `download_youtube_subs` function."""
|
||||
"""
|
||||
Tests for `download_youtube_subs` function.
|
||||
"""
|
||||
|
||||
org = 'MITx'
|
||||
number = '999'
|
||||
@@ -238,13 +240,6 @@ class TestDownloadYoutubeSubs(TestYoutubeSubsBase):
|
||||
|
||||
mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_2'})
|
||||
|
||||
# Check asset status after import of transcript.
|
||||
filename = 'subs_{0}.srt.sjson'.format(good_youtube_sub)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
self.clear_sub_content(good_youtube_sub)
|
||||
|
||||
def test_subs_for_html5_vid_with_periods(self):
|
||||
"""
|
||||
This is to verify a fix whereby subtitle files uploaded against
|
||||
@@ -269,16 +264,6 @@ class TestDownloadYoutubeSubs(TestYoutubeSubsBase):
|
||||
with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
|
||||
transcripts_utils.download_youtube_subs(bad_youtube_sub, self.course, settings)
|
||||
|
||||
# Check asset status after import of transcript.
|
||||
filename = 'subs_{0}.srt.sjson'.format(bad_youtube_sub)
|
||||
content_location = StaticContent.compute_location(
|
||||
self.course.id, filename
|
||||
)
|
||||
with self.assertRaises(NotFoundError):
|
||||
contentstore().find(content_location)
|
||||
|
||||
self.clear_sub_content(bad_youtube_sub)
|
||||
|
||||
def test_success_downloading_chinese_transcripts(self):
|
||||
|
||||
# Disabled 11/14/13
|
||||
@@ -367,13 +352,6 @@ class TestDownloadYoutubeSubs(TestYoutubeSubsBase):
|
||||
params={'lang': 'en', 'v': 'good_id_2', 'name': 'Custom'}
|
||||
)
|
||||
|
||||
# Check asset status after import of transcript.
|
||||
filename = 'subs_{0}.srt.sjson'.format(good_youtube_sub)
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
self.clear_sub_content(good_youtube_sub)
|
||||
|
||||
|
||||
class TestGenerateSubsFromSource(TestDownloadYoutubeSubs):
|
||||
"""Tests for `generate_subs_from_source` function."""
|
||||
@@ -766,7 +744,7 @@ class TestGetTranscript(SharedModuleStoreTestCase):
|
||||
edx_video_id=u'1234-5678-90'
|
||||
)
|
||||
|
||||
def create_transcript(self, subs_id, language=u'en', filename='video.srt'):
|
||||
def create_transcript(self, subs_id, language=u'en', filename='video.srt', youtube_id_1_0='', html5_sources=None):
|
||||
"""
|
||||
create transcript.
|
||||
"""
|
||||
@@ -774,21 +752,26 @@ class TestGetTranscript(SharedModuleStoreTestCase):
|
||||
if language != u'en':
|
||||
transcripts = {language: filename}
|
||||
|
||||
html5_sources = html5_sources or []
|
||||
self.video = ItemFactory.create(
|
||||
category='video',
|
||||
parent_location=self.vertical.location,
|
||||
sub=subs_id,
|
||||
youtube_id_1_0=youtube_id_1_0,
|
||||
transcripts=transcripts,
|
||||
edx_video_id=u'1234-5678-90'
|
||||
edx_video_id=u'1234-5678-90',
|
||||
html5_sources=html5_sources
|
||||
)
|
||||
|
||||
if subs_id:
|
||||
transcripts_utils.save_subs_to_store(
|
||||
self.subs_sjson,
|
||||
subs_id,
|
||||
self.video,
|
||||
language=language,
|
||||
)
|
||||
possible_subs = [subs_id, youtube_id_1_0] + transcripts_utils.get_html5_ids(html5_sources)
|
||||
for possible_sub in possible_subs:
|
||||
if possible_sub:
|
||||
transcripts_utils.save_subs_to_store(
|
||||
self.subs_sjson,
|
||||
possible_sub,
|
||||
self.video,
|
||||
language=language,
|
||||
)
|
||||
|
||||
def create_srt_file(self, content):
|
||||
"""
|
||||
@@ -834,31 +817,69 @@ class TestGetTranscript(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
# video.sub transcript
|
||||
{
|
||||
'language': u'en',
|
||||
'subs_id': 'video_101',
|
||||
'filename': 'en_video_101.srt',
|
||||
'youtube_id_1_0': '',
|
||||
'html5_sources': [],
|
||||
'expected_filename': 'en_video_101.srt',
|
||||
},
|
||||
# if video.sub is present, rest will be skipped.
|
||||
{
|
||||
'language': u'en',
|
||||
'subs_id': 'video_101',
|
||||
'youtube_id_1_0': 'test_yt_id',
|
||||
'html5_sources': ['www.abc.com/foo.mp4'],
|
||||
'expected_filename': 'en_video_101.srt',
|
||||
},
|
||||
# video.youtube_id_1_0 transcript
|
||||
{
|
||||
'language': u'en',
|
||||
'subs_id': '',
|
||||
'youtube_id_1_0': 'test_yt_id',
|
||||
'html5_sources': [],
|
||||
'expected_filename': 'en_test_yt_id.srt',
|
||||
},
|
||||
# video.html5_sources transcript
|
||||
{
|
||||
'language': u'en',
|
||||
'subs_id': '',
|
||||
'youtube_id_1_0': '',
|
||||
'html5_sources': ['www.abc.com/foo.mp4'],
|
||||
'expected_filename': 'en_foo.srt',
|
||||
},
|
||||
# non-english transcript
|
||||
{
|
||||
'language': u'ur',
|
||||
'subs_id': '',
|
||||
'filename': 'ur_video_101.srt',
|
||||
'youtube_id_1_0': '',
|
||||
'html5_sources': [],
|
||||
'expected_filename': 'ur_video_101.srt',
|
||||
},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_transcript_from_content_store(self, language, subs_id, filename):
|
||||
def test_get_transcript_from_contentstore(
|
||||
self,
|
||||
language,
|
||||
subs_id,
|
||||
youtube_id_1_0,
|
||||
html5_sources,
|
||||
expected_filename
|
||||
):
|
||||
"""
|
||||
Verify that `get_transcript` function returns correct data when transcript is in content store.
|
||||
"""
|
||||
self.upload_file(self.create_srt_file(self.subs_srt), self.video.location, filename)
|
||||
self.create_transcript(subs_id, language, filename)
|
||||
content, filename, mimetype = transcripts_utils.get_transcript(
|
||||
base_filename = 'video_101.srt'
|
||||
self.upload_file(self.create_srt_file(self.subs_srt), self.video.location, base_filename)
|
||||
self.create_transcript(subs_id, language, base_filename, youtube_id_1_0, html5_sources)
|
||||
content, file_name, mimetype = transcripts_utils.get_transcript(
|
||||
self.video,
|
||||
language
|
||||
)
|
||||
|
||||
self.assertEqual(content, self.subs[language])
|
||||
self.assertEqual(filename, filename)
|
||||
self.assertEqual(file_name, expected_filename)
|
||||
self.assertEqual(mimetype, self.srt_mime_type)
|
||||
|
||||
def test_get_transcript_from_content_store_for_ur(self):
|
||||
@@ -938,3 +959,43 @@ class TestGetTranscript(SharedModuleStoreTestCase):
|
||||
|
||||
exception_message = text_type(no_en_transcript_exception.exception)
|
||||
self.assertEqual(exception_message, 'No transcript for `en` language')
|
||||
|
||||
@ddt.data(
|
||||
transcripts_utils.TranscriptsGenerationException,
|
||||
UnicodeDecodeError('aliencodec', b'\x02\x01', 1, 2, 'alien codec found!')
|
||||
)
|
||||
@patch('xmodule.video_module.transcripts_utils.Transcript')
|
||||
def test_get_transcript_val_exceptions(self, exception_to_raise, mock_Transcript):
|
||||
"""
|
||||
Verify that `get_transcript_from_val` function raises `NotFoundError` when specified exceptions raised.
|
||||
"""
|
||||
mock_Transcript.convert.side_effect = exception_to_raise
|
||||
transcripts_info = self.video.get_transcripts_info()
|
||||
lang = self.video.get_default_transcript_language(transcripts_info)
|
||||
edx_video_id = transcripts_utils.clean_video_id(self.video.edx_video_id)
|
||||
with self.assertRaises(NotFoundError):
|
||||
transcripts_utils.get_transcript_from_val(
|
||||
edx_video_id,
|
||||
lang=lang,
|
||||
output_format=transcripts_utils.Transcript.SRT
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
transcripts_utils.TranscriptsGenerationException,
|
||||
UnicodeDecodeError('aliencodec', b'\x02\x01', 1, 2, 'alien codec found!')
|
||||
)
|
||||
@patch('xmodule.video_module.transcripts_utils.Transcript')
|
||||
def test_get_transcript_content_store_exceptions(self, exception_to_raise, mock_Transcript):
|
||||
"""
|
||||
Verify that `get_transcript_from_contentstore` function raises `NotFoundError` when specified exceptions raised.
|
||||
"""
|
||||
mock_Transcript.asset.side_effect = exception_to_raise
|
||||
transcripts_info = self.video.get_transcripts_info()
|
||||
lang = self.video.get_default_transcript_language(transcripts_info)
|
||||
with self.assertRaises(NotFoundError):
|
||||
transcripts_utils.get_transcript_from_contentstore(
|
||||
self.video,
|
||||
language=lang,
|
||||
output_format=transcripts_utils.Transcript.SRT,
|
||||
transcripts_info=transcripts_info
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from io import BytesIO
|
||||
from mock import Mock, patch, ANY
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from edxval import api
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
@@ -177,26 +178,24 @@ class TranscriptCredentialsValidationTest(TestCase):
|
||||
|
||||
|
||||
@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)
|
||||
@property
|
||||
def view_url(self):
|
||||
"""
|
||||
Returns url for this view
|
||||
"""
|
||||
return reverse('transcript_download_handler')
|
||||
|
||||
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')
|
||||
response = self.client.get(self.view_url, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_405_with_not_allowed_request_method(self):
|
||||
@@ -204,26 +203,14 @@ class TranscriptDownloadTest(CourseTestCase):
|
||||
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')
|
||||
response = self.client.post(self.view_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],
|
||||
@@ -235,7 +222,7 @@ class TranscriptDownloadTest(CourseTestCase):
|
||||
|
||||
# Make request to transcript download handler
|
||||
response = self.client.get(
|
||||
transcript_download_url,
|
||||
self.view_url,
|
||||
data={
|
||||
'edx_video_id': '123',
|
||||
'language_code': 'en'
|
||||
@@ -277,34 +264,30 @@ class TranscriptDownloadTest(CourseTestCase):
|
||||
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)
|
||||
response = self.client.get(self.view_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)
|
||||
@property
|
||||
def view_url(self):
|
||||
"""
|
||||
Returns url for this view
|
||||
"""
|
||||
return reverse('transcript_upload_handler')
|
||||
|
||||
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')
|
||||
response = self.client.post(self.view_url, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_405_with_not_allowed_request_method(self):
|
||||
@@ -312,31 +295,19 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
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')
|
||||
response = self.client.get(self.view_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,
|
||||
self.view_url,
|
||||
{
|
||||
'edx_video_id': '123',
|
||||
'language_code': 'en',
|
||||
@@ -395,9 +366,8 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
"""
|
||||
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')
|
||||
response = self.client.post(self.view_url, request_payload, format='multipart')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(json.loads(response.content)['error'], expected_error_message)
|
||||
|
||||
@@ -407,14 +377,13 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
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')
|
||||
response = self.client.post(self.view_url, request_payload, format='multipart')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(
|
||||
json.loads(response.content)['error'],
|
||||
@@ -427,10 +396,9 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
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,
|
||||
self.view_url,
|
||||
{
|
||||
'edx_video_id': '123',
|
||||
'language_code': 'en',
|
||||
@@ -451,11 +419,10 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
"""
|
||||
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,
|
||||
self.view_url,
|
||||
{
|
||||
'edx_video_id': '123',
|
||||
'language_code': 'en',
|
||||
@@ -473,11 +440,7 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=True)
|
||||
)
|
||||
class TranscriptUploadTest(CourseTestCase):
|
||||
class TranscriptDeleteTest(CourseTestCase):
|
||||
"""
|
||||
Tests for transcript deletion handler.
|
||||
"""
|
||||
@@ -504,16 +467,6 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
response = self.client.post(transcript_delete_url)
|
||||
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_delete_url = self.get_url_for_course_key(self.course.id, edx_video_id='test_id', language_code='en')
|
||||
with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature:
|
||||
feature.return_value = False
|
||||
response = self.client.delete(transcript_delete_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_404_with_non_staff_user(self):
|
||||
"""
|
||||
Verify that 404 is returned if the user doesn't have studio write access.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Tests for items views."""
|
||||
|
||||
import copy
|
||||
from codecs import BOM_UTF8
|
||||
import ddt
|
||||
import json
|
||||
import os
|
||||
from mock import patch, Mock
|
||||
import tempfile
|
||||
import textwrap
|
||||
from uuid import uuid4
|
||||
@@ -11,7 +12,7 @@ from uuid import uuid4
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch, Mock
|
||||
from edxval.api import create_video
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase, mock_requests_get
|
||||
@@ -20,11 +21,32 @@ from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.video_module import transcripts_utils
|
||||
from xmodule.video_module.transcripts_utils import (
|
||||
GetTranscriptsFromYouTubeException,
|
||||
get_video_transcript_content,
|
||||
remove_subs_from_store,
|
||||
Transcript,
|
||||
)
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
|
||||
SRT_TRANSCRIPT_CONTENT = """0
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Elephant's Dream
|
||||
|
||||
1
|
||||
00:00:15,000 --> 00:00:18,000
|
||||
At the left we can see...
|
||||
|
||||
"""
|
||||
|
||||
SJSON_TRANSCRIPT_CONTENT = Transcript.convert(
|
||||
SRT_TRANSCRIPT_CONTENT,
|
||||
Transcript.SRT,
|
||||
Transcript.SJSON,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
class BaseTranscripts(CourseTestCase):
|
||||
@@ -95,498 +117,716 @@ class BaseTranscripts(CourseTestCase):
|
||||
1.5: item.youtube_id_1_5
|
||||
}
|
||||
|
||||
|
||||
class TestUploadTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/upload' url.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""Create initial data."""
|
||||
super(TestUploadTranscripts, self).setUp()
|
||||
|
||||
self.good_srt_file = tempfile.NamedTemporaryFile(suffix='.srt')
|
||||
self.good_srt_file.write(textwrap.dedent("""
|
||||
1
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Elephant's Dream
|
||||
|
||||
2
|
||||
00:00:15,000 --> 00:00:18,000
|
||||
At the left we can see...
|
||||
"""))
|
||||
self.good_srt_file.seek(0)
|
||||
|
||||
self.bad_data_srt_file = tempfile.NamedTemporaryFile(suffix='.srt')
|
||||
self.bad_data_srt_file.write('Some BAD data')
|
||||
self.bad_data_srt_file.seek(0)
|
||||
|
||||
self.bad_name_srt_file = tempfile.NamedTemporaryFile(suffix='.BAD')
|
||||
self.bad_name_srt_file.write(textwrap.dedent("""
|
||||
1
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Elephant's Dream
|
||||
|
||||
2
|
||||
00:00:15,000 --> 00:00:18,000
|
||||
At the left we can see...
|
||||
"""))
|
||||
self.bad_name_srt_file.seek(0)
|
||||
|
||||
self.ufeff_srt_file = tempfile.NamedTemporaryFile(suffix='.srt')
|
||||
|
||||
def test_success_video_module_source_subs_uploading(self):
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""")
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.video_usage_key,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Success')
|
||||
|
||||
item = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(item.sub, filename)
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
self.course.id, 'subs_{0}.srt.sjson'.format(filename))
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
def test_fail_data_without_id(self):
|
||||
link = reverse('upload_transcripts')
|
||||
resp = self.client.post(link, {'transcript-file': self.good_srt_file})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "locator" form data.')
|
||||
|
||||
def test_fail_data_without_file(self):
|
||||
link = reverse('upload_transcripts')
|
||||
resp = self.client.post(link, {'locator': self.video_usage_key})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "file" form data.')
|
||||
|
||||
def test_fail_data_with_bad_locator(self):
|
||||
# Test for raising `InvalidLocationError` exception.
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': 'BAD_LOCATOR',
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by locator.")
|
||||
|
||||
# Test for raising `ItemNotFoundError` exception.
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': '{0}_{1}'.format(self.video_usage_key, 'BAD_LOCATOR'),
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by locator.")
|
||||
|
||||
def test_fail_for_non_video_module(self):
|
||||
# non_video module: setup
|
||||
def create_non_video_module(self):
|
||||
"""
|
||||
Setup non video module for tests.
|
||||
"""
|
||||
data = {
|
||||
'parent_locator': unicode(self.course.location),
|
||||
'category': 'non_video',
|
||||
'type': 'non_video'
|
||||
}
|
||||
resp = self.client.ajax_post('/xblock/', data)
|
||||
usage_key = self._get_usage_key(resp)
|
||||
response = self.client.ajax_post('/xblock/', data)
|
||||
usage_key = self._get_usage_key(response)
|
||||
item = modulestore().get_item(usage_key)
|
||||
item.data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />'
|
||||
modulestore().update_item(item, self.user.id)
|
||||
|
||||
# non_video module: testing
|
||||
return usage_key
|
||||
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': unicode(usage_key),
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')
|
||||
def assert_response(self, response, expected_status_code, expected_message):
|
||||
response_content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
self.assertEqual(response_content['status'], expected_message)
|
||||
|
||||
def test_fail_bad_xml(self):
|
||||
self.item.data = '<<<video youtube="0.75:JMD_ifUUfsU,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" />'
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
@ddt.ddt
|
||||
class TestUploadTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/upload' endpoint.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestUploadTranscripts, self).setUp()
|
||||
self.contents = {
|
||||
'good': SRT_TRANSCRIPT_CONTENT,
|
||||
'bad': 'Some BAD data',
|
||||
}
|
||||
# Create temporary transcript files
|
||||
self.good_srt_file = self.create_transcript_file(content=self.contents['good'], suffix='.srt')
|
||||
self.bad_data_srt_file = self.create_transcript_file(content=self.contents['bad'], suffix='.srt')
|
||||
self.bad_name_srt_file = self.create_transcript_file(content=self.contents['good'], suffix='.bad')
|
||||
self.bom_srt_file = self.create_transcript_file(content=self.contents['good'], suffix='.srt', include_bom=True)
|
||||
|
||||
# Setup a VEDA produced video and persist `edx_video_id` in VAL.
|
||||
create_video({
|
||||
'edx_video_id': u'123-456-789',
|
||||
'status': 'upload',
|
||||
'client_video_id': u'Test Video',
|
||||
'duration': 0,
|
||||
'encoded_videos': [],
|
||||
'courses': [unicode(self.course.id)]
|
||||
})
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
# incorrect xml produces incorrect item category error
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')
|
||||
# Add clean up handler
|
||||
self.addCleanup(self.clean_temporary_transcripts)
|
||||
|
||||
def test_fail_bad_data_srt_file(self):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'transcript-file': self.bad_data_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Something wrong with SubRip transcripts file during parsing.')
|
||||
|
||||
def test_fail_bad_name_srt_file(self):
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'transcript-file': self.bad_name_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'We support only SubRip (*.srt) transcripts format.')
|
||||
|
||||
def test_undefined_file_extension(self):
|
||||
srt_file = tempfile.NamedTemporaryFile(suffix='')
|
||||
srt_file.write(textwrap.dedent("""
|
||||
1
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Elephant's Dream
|
||||
|
||||
2
|
||||
00:00:15,000 --> 00:00:18,000
|
||||
At the left we can see...
|
||||
"""))
|
||||
srt_file.seek(0)
|
||||
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.video_usage_key,
|
||||
'transcript-file': srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Undefined file extension.')
|
||||
|
||||
def test_subs_uploading_with_byte_order_mark(self):
|
||||
def create_transcript_file(self, content, suffix, include_bom=False):
|
||||
"""
|
||||
Test uploading subs containing BOM(Byte Order Mark), e.g. U+FEFF
|
||||
Setup a transcript file with suffix and content.
|
||||
"""
|
||||
filedata = textwrap.dedent("""
|
||||
1
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Test ufeff characters
|
||||
transcript_file = tempfile.NamedTemporaryFile(suffix=suffix)
|
||||
wrapped_content = textwrap.dedent(content)
|
||||
if include_bom:
|
||||
wrapped_content = wrapped_content.encode('utf-8-sig')
|
||||
# Verify that ufeff(BOM) character is in content.
|
||||
self.assertIn(BOM_UTF8, wrapped_content)
|
||||
|
||||
2
|
||||
00:00:15,000 --> 00:00:18,000
|
||||
At the left we can see...
|
||||
""").encode('utf-8-sig')
|
||||
transcript_file.write(wrapped_content)
|
||||
transcript_file.seek(0)
|
||||
|
||||
# Verify that ufeff character is in filedata.
|
||||
self.assertIn("ufeff", filedata)
|
||||
self.ufeff_srt_file.write(filedata)
|
||||
self.ufeff_srt_file.seek(0)
|
||||
|
||||
link = reverse('upload_transcripts')
|
||||
filename = os.path.splitext(os.path.basename(self.ufeff_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.video_usage_key,
|
||||
'transcript-file': self.ufeff_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
'mode': 'mp4',
|
||||
}])
|
||||
})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
self.course.id, 'subs_{0}.srt.sjson'.format(filename))
|
||||
self.assertTrue(contentstore().find(content_location))
|
||||
|
||||
subs_text = json.loads(contentstore().find(content_location).data).get('text')
|
||||
self.assertIn("Test ufeff characters", subs_text)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestUploadTranscripts, self).tearDown()
|
||||
return transcript_file
|
||||
|
||||
def clean_temporary_transcripts(self):
|
||||
"""
|
||||
Close transcript files gracefully.
|
||||
"""
|
||||
self.good_srt_file.close()
|
||||
self.bad_data_srt_file.close()
|
||||
self.bad_name_srt_file.close()
|
||||
self.ufeff_srt_file.close()
|
||||
self.bom_srt_file.close()
|
||||
|
||||
def upload_transcript(self, locator, transcript_file, edx_video_id=None):
|
||||
"""
|
||||
Uploads a transcript for a video
|
||||
"""
|
||||
payload = {}
|
||||
if locator:
|
||||
payload.update({'locator': locator})
|
||||
|
||||
if edx_video_id is not None:
|
||||
payload.update({'edx_video_id': edx_video_id})
|
||||
|
||||
if transcript_file:
|
||||
payload.update({'transcript-file': transcript_file})
|
||||
|
||||
upload_url = reverse('upload_transcripts')
|
||||
response = self.client.post(upload_url, payload)
|
||||
|
||||
return response
|
||||
|
||||
@ddt.data(
|
||||
(u'123-456-789', False),
|
||||
(u'', False),
|
||||
(u'123-456-789', True)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_transcript_upload_success(self, edx_video_id, include_bom):
|
||||
"""
|
||||
Tests transcript file upload to video component works as
|
||||
expected in case of following:
|
||||
|
||||
1. External video component
|
||||
2. VEDA produced video component
|
||||
3. Transcript content containing BOM character
|
||||
"""
|
||||
# In case of an external video component, the `edx_video_id` must be empty
|
||||
# and VEDA produced video component will have `edx_video_id` set to VAL video ID.
|
||||
self.item.edx_video_id = edx_video_id
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
# Upload a transcript
|
||||
transcript_file = self.bom_srt_file if include_bom else self.good_srt_file
|
||||
response = self.upload_transcript(self.video_usage_key, transcript_file, '')
|
||||
|
||||
# Verify the response
|
||||
self.assert_response(response, expected_status_code=200, expected_message='Success')
|
||||
|
||||
# Verify the `edx_video_id` on the video component
|
||||
json_response = json.loads(response.content)
|
||||
expected_edx_video_id = edx_video_id if edx_video_id else json_response['edx_video_id']
|
||||
video = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(video.edx_video_id, expected_edx_video_id)
|
||||
|
||||
# Verify transcript content
|
||||
actual_transcript = get_video_transcript_content(video.edx_video_id, language_code=u'en')
|
||||
actual_sjson_content = json.loads(actual_transcript['content'])
|
||||
expected_sjson_content = json.loads(Transcript.convert(
|
||||
self.contents['good'],
|
||||
input_format=Transcript.SRT,
|
||||
output_format=Transcript.SJSON
|
||||
))
|
||||
self.assertDictEqual(actual_sjson_content, expected_sjson_content)
|
||||
|
||||
def test_transcript_upload_without_locator(self):
|
||||
"""
|
||||
Test that transcript upload validation fails if the video locator is missing
|
||||
"""
|
||||
response = self.upload_transcript(locator=None, transcript_file=self.good_srt_file, edx_video_id='')
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Video locator is required.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_without_file(self):
|
||||
"""
|
||||
Test that transcript upload validation fails if transcript file is missing
|
||||
"""
|
||||
response = self.upload_transcript(locator=self.video_usage_key, transcript_file=None, edx_video_id='')
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'A transcript file is required.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_bad_format(self):
|
||||
"""
|
||||
Test that transcript upload validation fails if transcript format is not SRT
|
||||
"""
|
||||
response = self.upload_transcript(
|
||||
locator=self.video_usage_key,
|
||||
transcript_file=self.bad_name_srt_file,
|
||||
edx_video_id=''
|
||||
)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'This transcript file type is not supported.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_bad_content(self):
|
||||
"""
|
||||
Test that transcript upload validation fails in case of bad transcript content.
|
||||
"""
|
||||
# Request to upload transcript for the video
|
||||
response = self.upload_transcript(
|
||||
locator=self.video_usage_key,
|
||||
transcript_file=self.bad_data_srt_file,
|
||||
edx_video_id=''
|
||||
)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'There is a problem with this transcript file. Try to upload a different file.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_unknown_category(self):
|
||||
"""
|
||||
Test that transcript upload validation fails if item's category is other than video.
|
||||
"""
|
||||
# non_video module setup - i.e. an item whose category is not 'video'.
|
||||
usage_key = self.create_non_video_module()
|
||||
# Request to upload transcript for the item
|
||||
response = self.upload_transcript(locator=usage_key, transcript_file=self.good_srt_file, edx_video_id='')
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Transcripts are supported only for "video" modules.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_non_existent_item(self):
|
||||
"""
|
||||
Test that transcript upload validation fails in case of invalid item's locator.
|
||||
"""
|
||||
# Request to upload transcript for the item
|
||||
response = self.upload_transcript(
|
||||
locator='non_existent_locator',
|
||||
transcript_file=self.good_srt_file,
|
||||
edx_video_id=''
|
||||
)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Cannot find item by locator.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_without_edx_video_id(self):
|
||||
"""
|
||||
Test that transcript upload validation fails if the `edx_video_id` is missing
|
||||
"""
|
||||
response = self.upload_transcript(locator=self.video_usage_key, transcript_file=self.good_srt_file)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Video ID is required.'
|
||||
)
|
||||
|
||||
def test_transcript_upload_with_non_existant_edx_video_id(self):
|
||||
"""
|
||||
Test that transcript upload works as expected if `edx_video_id` set on
|
||||
video descriptor is different from `edx_video_id` received in POST request.
|
||||
"""
|
||||
non_existant_edx_video_id = '1111-2222-3333-4444'
|
||||
|
||||
# Upload with non-existant `edx_video_id`
|
||||
response = self.upload_transcript(
|
||||
locator=self.video_usage_key,
|
||||
transcript_file=self.good_srt_file,
|
||||
edx_video_id=non_existant_edx_video_id
|
||||
)
|
||||
# Verify the response
|
||||
self.assert_response(response, expected_status_code=400, expected_message='Invalid Video ID')
|
||||
|
||||
# Verify transcript does not exist for non-existant `edx_video_id`
|
||||
self.assertIsNone(get_video_transcript_content(non_existant_edx_video_id, language_code=u'en'))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestChooseTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/choose' endpoint.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestChooseTranscripts, self).setUp()
|
||||
|
||||
# Create test transcript in contentstore
|
||||
self.chosen_html5_id = 'test_html5_subs'
|
||||
self.sjson_subs = Transcript.convert(SRT_TRANSCRIPT_CONTENT, Transcript.SRT, Transcript.SJSON)
|
||||
self.save_subs_to_store(json.loads(self.sjson_subs), self.chosen_html5_id)
|
||||
|
||||
# Setup a VEDA produced video and persist `edx_video_id` in VAL.
|
||||
create_video({
|
||||
'edx_video_id': u'123-456-789',
|
||||
'status': 'upload',
|
||||
'client_video_id': u'Test Video',
|
||||
'duration': 0,
|
||||
'encoded_videos': [],
|
||||
'courses': [unicode(self.course.id)]
|
||||
})
|
||||
|
||||
def choose_transcript(self, locator, chosen_html5_id):
|
||||
"""
|
||||
Make an endpoint call to choose transcript
|
||||
"""
|
||||
payload = {}
|
||||
if locator:
|
||||
payload.update({'locator': unicode(locator)})
|
||||
|
||||
if chosen_html5_id:
|
||||
payload.update({'html5_id': chosen_html5_id})
|
||||
|
||||
choose_transcript_url = reverse('choose_transcripts')
|
||||
response = self.client.get(choose_transcript_url, {'data': json.dumps(payload)})
|
||||
return response
|
||||
|
||||
@ddt.data(u'123-456-789', u'')
|
||||
def test_choose_transcript_success(self, edx_video_id):
|
||||
"""
|
||||
Verify that choosing transcript file in video component basic tab works as
|
||||
expected in case of following:
|
||||
|
||||
1. External video component
|
||||
2. VEDA produced video component
|
||||
"""
|
||||
# In case of an external video component, the `edx_video_id` must be empty
|
||||
# and VEDA produced video component will have `edx_video_id` set to VAL video ID.
|
||||
self.item.edx_video_id = edx_video_id
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
# Make call to choose a transcript
|
||||
response = self.choose_transcript(self.video_usage_key, self.chosen_html5_id)
|
||||
|
||||
# Verify the response
|
||||
self.assert_response(response, expected_status_code=200, expected_message='Success')
|
||||
|
||||
# Verify the `edx_video_id` on the video component
|
||||
json_response = json.loads(response.content)
|
||||
expected_edx_video_id = edx_video_id if edx_video_id else json_response['edx_video_id']
|
||||
video = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(video.edx_video_id, expected_edx_video_id)
|
||||
|
||||
# Verify transcript content
|
||||
actual_transcript = get_video_transcript_content(video.edx_video_id, language_code=u'en')
|
||||
actual_sjson_content = json.loads(actual_transcript['content'])
|
||||
expected_sjson_content = json.loads(self.sjson_subs)
|
||||
self.assertDictEqual(actual_sjson_content, expected_sjson_content)
|
||||
|
||||
def test_choose_transcript_fails_without_data(self):
|
||||
"""
|
||||
Verify that choose transcript fails if we do not provide video data in request.
|
||||
"""
|
||||
response = self.choose_transcript(locator=None, chosen_html5_id=None)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Incoming video data is empty.'
|
||||
)
|
||||
|
||||
def test_choose_transcript_fails_without_locator(self):
|
||||
"""
|
||||
Verify that choose transcript fails if video locator is missing in request.
|
||||
"""
|
||||
response = self.choose_transcript(locator=None, chosen_html5_id=self.chosen_html5_id)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Cannot find item by locator.'
|
||||
)
|
||||
|
||||
def test_choose_transcript_with_no_html5_transcript(self):
|
||||
"""
|
||||
Verify that choose transcript fails if the chosen html5 ID don't
|
||||
have any transcript associated in contentstore.
|
||||
"""
|
||||
response = self.choose_transcript(locator=self.video_usage_key, chosen_html5_id='non-existent-html5-id')
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u"No such transcript."
|
||||
)
|
||||
|
||||
def test_choose_transcript_fails_on_unknown_category(self):
|
||||
"""
|
||||
Test that transcript choose validation fails if item's category is other than video.
|
||||
"""
|
||||
# non_video module setup - i.e. an item whose category is not 'video'.
|
||||
usage_key = self.create_non_video_module()
|
||||
# Request to choose transcript for the item
|
||||
response = self.choose_transcript(locator=usage_key, chosen_html5_id=self.chosen_html5_id)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Transcripts are supported only for "video" modules.'
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestRenameTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/rename' endpoint.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestRenameTranscripts, self).setUp()
|
||||
|
||||
# Create test transcript in contentstore and update item's sub.
|
||||
self.item.sub = 'test_video_subs'
|
||||
self.sjson_subs = Transcript.convert(SRT_TRANSCRIPT_CONTENT, Transcript.SRT, Transcript.SJSON)
|
||||
self.save_subs_to_store(json.loads(self.sjson_subs), self.item.sub)
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
# Setup a VEDA produced video and persist `edx_video_id` in VAL.
|
||||
create_video({
|
||||
'edx_video_id': u'123-456-789',
|
||||
'status': 'upload',
|
||||
'client_video_id': u'Test Video',
|
||||
'duration': 0,
|
||||
'encoded_videos': [],
|
||||
'courses': [unicode(self.course.id)]
|
||||
})
|
||||
|
||||
def rename_transcript(self, locator):
|
||||
"""
|
||||
Make an endpoint call to rename transcripts.
|
||||
"""
|
||||
payload = {}
|
||||
if locator:
|
||||
payload.update({'locator': unicode(locator)})
|
||||
|
||||
rename_transcript_url = reverse('rename_transcripts')
|
||||
response = self.client.get(rename_transcript_url, {'data': json.dumps(payload)})
|
||||
return response
|
||||
|
||||
@ddt.data(u'123-456-789', u'')
|
||||
def test_rename_transcript_success(self, edx_video_id):
|
||||
"""
|
||||
Verify that "use current transcript" in video component basic tab works as
|
||||
expected in case of following:
|
||||
|
||||
1. External video component
|
||||
2. VEDA produced video component
|
||||
"""
|
||||
# In case of an external video component, the `edx_video_id` must be empty
|
||||
# and VEDA produced video component will have `edx_video_id` set to VAL video ID.
|
||||
self.item.edx_video_id = edx_video_id
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
# Make call to use current transcript from contentstore
|
||||
response = self.rename_transcript(self.video_usage_key)
|
||||
|
||||
# Verify the response
|
||||
self.assert_response(response, expected_status_code=200, expected_message='Success')
|
||||
|
||||
# Verify the `edx_video_id` on the video component
|
||||
json_response = json.loads(response.content)
|
||||
expected_edx_video_id = edx_video_id if edx_video_id else json_response['edx_video_id']
|
||||
video = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(video.edx_video_id, expected_edx_video_id)
|
||||
|
||||
# Verify transcript content
|
||||
actual_transcript = get_video_transcript_content(video.edx_video_id, language_code=u'en')
|
||||
actual_sjson_content = json.loads(actual_transcript['content'])
|
||||
expected_sjson_content = json.loads(self.sjson_subs)
|
||||
self.assertDictEqual(actual_sjson_content, expected_sjson_content)
|
||||
|
||||
def test_rename_transcript_fails_without_data(self):
|
||||
"""
|
||||
Verify that use current transcript fails if we do not provide video data in request.
|
||||
"""
|
||||
response = self.rename_transcript(locator=None)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Incoming video data is empty.'
|
||||
)
|
||||
|
||||
def test_rename_transcript_fails_with_invalid_locator(self):
|
||||
"""
|
||||
Verify that use current transcript fails if video locator is missing in request.
|
||||
"""
|
||||
response = self.rename_transcript(locator='non-existent-locator')
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Cannot find item by locator.'
|
||||
)
|
||||
|
||||
def test_rename_transcript_with_non_existent_sub(self):
|
||||
"""
|
||||
Verify that rename transcript fails if the `item.sub` don't
|
||||
have any transcript associated in contentstore.
|
||||
"""
|
||||
# Update item's sub to an id who does not have any
|
||||
# transcript associated in contentstore.
|
||||
self.item.sub = 'non-existent-sub'
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
response = self.rename_transcript(locator=self.video_usage_key)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u"No such transcript."
|
||||
)
|
||||
|
||||
def test_rename_transcript_fails_on_unknown_category(self):
|
||||
"""
|
||||
Test that validation fails if item's category is other than video.
|
||||
"""
|
||||
# non_video module setup - i.e. an item whose category is not 'video'.
|
||||
usage_key = self.create_non_video_module()
|
||||
# Make call to use current transcript from contentstore.
|
||||
response = self.rename_transcript(usage_key)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Transcripts are supported only for "video" modules.'
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@patch('contentstore.views.transcripts_ajax.download_youtube_subs', Mock(return_value=SJSON_TRANSCRIPT_CONTENT))
|
||||
class TestReplaceTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/replace' endpoint.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestReplaceTranscripts, self).setUp()
|
||||
self.youtube_id = 'test_yt_id'
|
||||
|
||||
# Setup a VEDA produced video and persist `edx_video_id` in VAL.
|
||||
create_video({
|
||||
'edx_video_id': u'123-456-789',
|
||||
'status': 'upload',
|
||||
'client_video_id': u'Test Video',
|
||||
'duration': 0,
|
||||
'encoded_videos': [],
|
||||
'courses': [unicode(self.course.id)]
|
||||
})
|
||||
|
||||
def replace_transcript(self, locator, youtube_id):
|
||||
"""
|
||||
Make an endpoint call to replace transcripts with youtube ones.
|
||||
"""
|
||||
payload = {}
|
||||
if locator:
|
||||
payload.update({'locator': unicode(locator)})
|
||||
|
||||
if youtube_id:
|
||||
payload.update({
|
||||
'videos': [
|
||||
{
|
||||
'type': 'youtube',
|
||||
'video': youtube_id
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
replace_transcript_url = reverse('replace_transcripts')
|
||||
response = self.client.get(replace_transcript_url, {'data': json.dumps(payload)})
|
||||
return response
|
||||
|
||||
@ddt.data(u'123-456-789', u'')
|
||||
def test_replace_transcript_success(self, edx_video_id):
|
||||
"""
|
||||
Verify that "import from youtube" in video component basic tab works as
|
||||
expected in case of following:
|
||||
|
||||
1. External video component
|
||||
2. VEDA produced video component
|
||||
"""
|
||||
# In case of an external video component, the `edx_video_id` must be empty
|
||||
# and VEDA produced video component will have `edx_video_id` set to VAL video ID.
|
||||
self.item.edx_video_id = edx_video_id
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
# Make call to replace transcripts from youtube
|
||||
response = self.replace_transcript(self.video_usage_key, self.youtube_id)
|
||||
|
||||
# Verify the response
|
||||
self.assert_response(response, expected_status_code=200, expected_message='Success')
|
||||
|
||||
# Verify the `edx_video_id` on the video component
|
||||
json_response = json.loads(response.content)
|
||||
expected_edx_video_id = edx_video_id if edx_video_id else json_response['edx_video_id']
|
||||
video = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(video.edx_video_id, expected_edx_video_id)
|
||||
|
||||
# Verify transcript content
|
||||
actual_transcript = get_video_transcript_content(video.edx_video_id, language_code=u'en')
|
||||
actual_sjson_content = json.loads(actual_transcript['content'])
|
||||
expected_sjson_content = json.loads(SJSON_TRANSCRIPT_CONTENT)
|
||||
self.assertDictEqual(actual_sjson_content, expected_sjson_content)
|
||||
|
||||
def test_replace_transcript_fails_without_data(self):
|
||||
"""
|
||||
Verify that replace transcript fails if we do not provide video data in request.
|
||||
"""
|
||||
response = self.replace_transcript(locator=None, youtube_id=None)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Incoming video data is empty.'
|
||||
)
|
||||
|
||||
def test_replace_transcript_fails_with_invalid_locator(self):
|
||||
"""
|
||||
Verify that replace transcript fails if a video locator does not exist.
|
||||
"""
|
||||
response = self.replace_transcript(locator='non-existent-locator', youtube_id=self.youtube_id)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Cannot find item by locator.'
|
||||
)
|
||||
|
||||
def test_replace_transcript_fails_without_yt_id(self):
|
||||
"""
|
||||
Verify that replace transcript fails if youtube id is not provided.
|
||||
"""
|
||||
response = self.replace_transcript(locator=self.video_usage_key, youtube_id=None)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'YouTube ID is required.'
|
||||
)
|
||||
|
||||
def test_replace_transcript_no_transcript_on_yt(self):
|
||||
"""
|
||||
Verify that replace transcript fails if YouTube does not have transcript for the given youtube id.
|
||||
"""
|
||||
error_message = u'YT ID not found.'
|
||||
with patch('contentstore.views.transcripts_ajax.download_youtube_subs') as mock_download_youtube_subs:
|
||||
mock_download_youtube_subs.side_effect = GetTranscriptsFromYouTubeException(error_message)
|
||||
response = self.replace_transcript(locator=self.video_usage_key, youtube_id='non-existent-yt-id')
|
||||
self.assertContains(response, text=error_message, status_code=400)
|
||||
|
||||
def test_replace_transcript_fails_on_unknown_category(self):
|
||||
"""
|
||||
Test that validation fails if item's category is other than video.
|
||||
"""
|
||||
# non_video module setup - i.e. an item whose category is not 'video'.
|
||||
usage_key = self.create_non_video_module()
|
||||
response = self.replace_transcript(usage_key, youtube_id=self.youtube_id)
|
||||
self.assert_response(
|
||||
response,
|
||||
expected_status_code=400,
|
||||
expected_message=u'Transcripts are supported only for "video" modules.'
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/download' url.
|
||||
"""
|
||||
def test_success_download_youtube(self):
|
||||
self.item.data = '<video youtube="1:JMD_ifUUfsU" />'
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
subs = {
|
||||
'start': [100, 200, 240],
|
||||
'end': [200, 240, 380],
|
||||
'text': [
|
||||
'subs #1',
|
||||
'subs #2',
|
||||
'subs #3'
|
||||
]
|
||||
}
|
||||
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key, 'subs_id': "JMD_ifUUfsU"})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content, """0\n00:00:00,100 --> 00:00:00,200\nsubs #1\n\n1\n00:00:00,200 --> 00:00:00,240\nsubs #2\n\n2\n00:00:00,240 --> 00:00:00,380\nsubs #3\n\n""")
|
||||
|
||||
def test_success_download_nonyoutube(self):
|
||||
subs_id = str(uuid4())
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="" sub="{}">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""".format(subs_id))
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
subs = {
|
||||
'start': [100, 200, 240],
|
||||
'end': [200, 240, 380],
|
||||
'text': [
|
||||
'subs #1',
|
||||
'subs #2',
|
||||
'subs #3'
|
||||
]
|
||||
}
|
||||
self.save_subs_to_store(subs, subs_id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key, 'subs_id': subs_id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(
|
||||
resp.content,
|
||||
'0\n00:00:00,100 --> 00:00:00,200\nsubs #1\n\n1\n00:00:00,200 --> '
|
||||
'00:00:00,240\nsubs #2\n\n2\n00:00:00,240 --> 00:00:00,380\nsubs #3\n\n'
|
||||
)
|
||||
transcripts_utils.remove_subs_from_store(subs_id, self.item)
|
||||
|
||||
def test_fail_data_without_file(self):
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': ''})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
resp = self.client.get(link, {})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_fail_data_with_bad_locator(self):
|
||||
# Test for raising `InvalidLocationError` exception.
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': 'BAD_LOCATOR'})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# Test for raising `ItemNotFoundError` exception.
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': '{0}_{1}'.format(self.video_usage_key, 'BAD_LOCATOR')})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_fail_for_non_video_module(self):
|
||||
# Video module: setup
|
||||
data = {
|
||||
'parent_locator': unicode(self.course.location),
|
||||
'category': 'videoalpha',
|
||||
'type': 'videoalpha'
|
||||
}
|
||||
resp = self.client.ajax_post('/xblock/', data)
|
||||
usage_key = self._get_usage_key(resp)
|
||||
subs_id = str(uuid4())
|
||||
item = modulestore().get_item(usage_key)
|
||||
item.data = textwrap.dedent("""
|
||||
<videoalpha youtube="" sub="{}">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</videoalpha>
|
||||
""".format(subs_id))
|
||||
modulestore().update_item(item, self.user.id)
|
||||
|
||||
subs = {
|
||||
'start': [100, 200, 240],
|
||||
'end': [200, 240, 380],
|
||||
'text': [
|
||||
'subs #1',
|
||||
'subs #2',
|
||||
'subs #3'
|
||||
]
|
||||
}
|
||||
self.save_subs_to_store(subs, subs_id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': unicode(usage_key)})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_fail_nonyoutube_subs_dont_exist(self):
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="" sub="UNDEFINED">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""")
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_empty_youtube_attr_and_sub_attr(self):
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""")
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key})
|
||||
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_fail_bad_sjson_subs(self):
|
||||
subs_id = str(uuid4())
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="" sub="{}">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""".format(subs_id))
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
subs = {
|
||||
'start': [100, 200, 240],
|
||||
'end': [200, 240, 380],
|
||||
'text': [
|
||||
'subs #1'
|
||||
]
|
||||
}
|
||||
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
|
||||
|
||||
link = reverse('download_transcripts')
|
||||
resp = self.client.get(link, {'locator': self.video_usage_key})
|
||||
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data')
|
||||
def test_download_fallback_transcript(self, mock_get_video_transcript_data):
|
||||
def update_video_component(self, sub=None, youtube_id=None):
|
||||
"""
|
||||
Verify that the val transcript is returned if its not found in content-store.
|
||||
Updates video component with `sub` and `youtube_id`.
|
||||
"""
|
||||
mock_get_video_transcript_data.return_value = {
|
||||
'content': json.dumps({
|
||||
"start": [10],
|
||||
"end": [100],
|
||||
"text": ["Hi, welcome to Edx."],
|
||||
}),
|
||||
'file_name': 'edx.sjson'
|
||||
}
|
||||
sjson_transcript = json.loads(SJSON_TRANSCRIPT_CONTENT)
|
||||
self.item.sub = sub
|
||||
if sub:
|
||||
self.save_subs_to_store(sjson_transcript, sub)
|
||||
self.item.youtube_id_1_0 = youtube_id
|
||||
if youtube_id:
|
||||
self.save_subs_to_store(sjson_transcript, youtube_id)
|
||||
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="" sub="" edx_video_id="123">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""")
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
|
||||
download_transcripts_url = reverse('download_transcripts')
|
||||
response = self.client.get(download_transcripts_url, {'locator': self.video_usage_key})
|
||||
|
||||
# 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-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)
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=False),
|
||||
)
|
||||
def test_download_fallback_transcript_feature_disabled(self):
|
||||
def download_transcript(self, locator):
|
||||
"""
|
||||
Verify the transcript download when feature is disabled.
|
||||
Makes a call to download transcripts.
|
||||
"""
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="" sub="">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
|
||||
</video>
|
||||
""")
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
payload = {}
|
||||
if locator:
|
||||
payload.update({'locator': unicode(locator)})
|
||||
|
||||
download_transcripts_url = reverse('download_transcripts')
|
||||
response = self.client.get(download_transcripts_url, {'locator': self.video_usage_key})
|
||||
# Assert the actual response
|
||||
self.assertEqual(response.status_code, 404)
|
||||
download_transcript_url = reverse('download_transcripts')
|
||||
response = self.client.get(download_transcript_url, payload)
|
||||
return response
|
||||
|
||||
def assert_download_response(self, response, expected_status_code, expected_content=None):
|
||||
"""
|
||||
Verify transcript download response.
|
||||
"""
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
if expected_content:
|
||||
self.assertEqual(response.content, expected_content)
|
||||
|
||||
def test_download_youtube_transcript_success(self):
|
||||
"""
|
||||
Verify that the transcript associated to YT id is downloaded successfully.
|
||||
"""
|
||||
self.update_video_component(youtube_id='JMD_ifUUfsU')
|
||||
response = self.download_transcript(locator=self.video_usage_key)
|
||||
self.assert_download_response(response, expected_content=SRT_TRANSCRIPT_CONTENT, expected_status_code=200)
|
||||
|
||||
def test_download_non_youtube_transcript_success(self):
|
||||
"""
|
||||
Verify that the transcript associated to item's `sub` is downloaded successfully.
|
||||
"""
|
||||
self.update_video_component(sub='test_subs')
|
||||
response = self.download_transcript(locator=self.video_usage_key)
|
||||
self.assert_download_response(response, expected_content=SRT_TRANSCRIPT_CONTENT, expected_status_code=200)
|
||||
|
||||
def test_download_transcript_404_without_locator(self):
|
||||
"""
|
||||
Verify that download transcript returns 404 without locator.
|
||||
"""
|
||||
response = self.download_transcript(locator=None)
|
||||
self.assert_download_response(response, expected_status_code=404)
|
||||
|
||||
def test_download_transcript_404_with_bad_locator(self):
|
||||
"""
|
||||
Verify that download transcript returns 404 with invalid locator.
|
||||
"""
|
||||
response = self.download_transcript(locator='invalid-locator')
|
||||
self.assert_download_response(response, expected_status_code=404)
|
||||
|
||||
def test_download_transcript_404_for_non_video_module(self):
|
||||
"""
|
||||
Verify that download transcript returns 404 for a non video module.
|
||||
"""
|
||||
usage_key = self.create_non_video_module()
|
||||
response = self.download_transcript(locator=usage_key)
|
||||
self.assert_download_response(response, expected_status_code=404)
|
||||
|
||||
def test_download_transcript_404_for_no_yt_and_no_sub(self):
|
||||
"""
|
||||
Verify that download transcript returns 404 when video component
|
||||
does not have sub and youtube id.
|
||||
"""
|
||||
self.update_video_component(sub=None, youtube_id=None)
|
||||
response = self.download_transcript(locator=self.video_usage_key)
|
||||
self.assert_download_response(response, expected_status_code=404)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -631,7 +871,6 @@ class TestCheckTranscripts(BaseTranscripts):
|
||||
json.loads(resp.content),
|
||||
{
|
||||
u'status': u'Success',
|
||||
u'subs': unicode(subs_id),
|
||||
u'youtube_local': False,
|
||||
u'is_youtube_mode': False,
|
||||
u'youtube_server': False,
|
||||
@@ -643,7 +882,7 @@ class TestCheckTranscripts(BaseTranscripts):
|
||||
}
|
||||
)
|
||||
|
||||
transcripts_utils.remove_subs_from_store(subs_id, self.item)
|
||||
remove_subs_from_store(subs_id, self.item)
|
||||
|
||||
def test_check_youtube(self):
|
||||
self.item.data = '<video youtube="1:JMD_ifUUfsU" />'
|
||||
@@ -668,13 +907,14 @@ class TestCheckTranscripts(BaseTranscripts):
|
||||
'mode': 'youtube',
|
||||
}]
|
||||
}
|
||||
|
||||
resp = self.client.get(link, {'data': json.dumps(data)})
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
json.loads(resp.content),
|
||||
{
|
||||
u'status': u'Success',
|
||||
u'subs': u'JMD_ifUUfsU',
|
||||
u'youtube_local': True,
|
||||
u'is_youtube_mode': True,
|
||||
u'youtube_server': False,
|
||||
@@ -726,7 +966,6 @@ class TestCheckTranscripts(BaseTranscripts):
|
||||
json.loads(resp.content),
|
||||
{
|
||||
u'status': u'Success',
|
||||
u'subs': u'good_id_2',
|
||||
u'youtube_local': True,
|
||||
u'is_youtube_mode': True,
|
||||
u'youtube_server': True,
|
||||
@@ -824,19 +1063,21 @@ class TestCheckTranscripts(BaseTranscripts):
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')
|
||||
|
||||
@ddt.data(
|
||||
(True, 'found'),
|
||||
(False, 'not_found')
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled')
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data', Mock(return_value=True))
|
||||
def test_command_for_fallback_transcript(self, feature_enabled, expected_command, video_transcript_feature):
|
||||
@patch('xmodule.video_module.transcripts_utils.get_video_transcript_content')
|
||||
def test_command_for_fallback_transcript(self, mock_get_video_transcript_content):
|
||||
"""
|
||||
Verify the command if a transcript is not found in content-store but
|
||||
its there in edx-val.
|
||||
Verify the command if a transcript is there in edx-val.
|
||||
"""
|
||||
video_transcript_feature.return_value = feature_enabled
|
||||
mock_get_video_transcript_content.return_value = {
|
||||
'content': json.dumps({
|
||||
"start": [10],
|
||||
"end": [100],
|
||||
"text": ["Hi, welcome to Edx."],
|
||||
}),
|
||||
'file_name': 'edx.sjson'
|
||||
}
|
||||
|
||||
# video_transcript_feature.return_value = feature_enabled
|
||||
self.item.data = textwrap.dedent("""
|
||||
<video youtube="" sub="" edx_video_id="123">
|
||||
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
|
||||
@@ -864,74 +1105,13 @@ class TestCheckTranscripts(BaseTranscripts):
|
||||
json.loads(response.content),
|
||||
{
|
||||
u'status': u'Success',
|
||||
u'subs': u'',
|
||||
u'youtube_local': False,
|
||||
u'is_youtube_mode': False,
|
||||
u'youtube_server': False,
|
||||
u'command': expected_command,
|
||||
u'command': 'found',
|
||||
u'current_item_subs': None,
|
||||
u'youtube_diff': True,
|
||||
u'html5_local': [],
|
||||
u'html5_equal': False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TestSaveTranscripts(BaseTranscripts):
|
||||
"""
|
||||
Tests for '/transcripts/save' url.
|
||||
"""
|
||||
def assert_current_subs(self, expected_subs):
|
||||
"""
|
||||
Asserts the current subtitles set on the video module.
|
||||
|
||||
Arguments:
|
||||
expected_subs (String): Expected current subtitles for video.
|
||||
"""
|
||||
item = modulestore().get_item(self.video_usage_key)
|
||||
self.assertEqual(item.sub, expected_subs)
|
||||
|
||||
def test_prioritize_youtube_sub_on_save(self):
|
||||
"""
|
||||
Test that the '/transcripts/save' endpoint prioritises youtube subtitles over html5 ones
|
||||
while deciding the current subs for video module.
|
||||
"""
|
||||
# Update video module to contain 1 youtube and 2 html5 sources.
|
||||
youtube_id = str(uuid4())
|
||||
self.item.data = textwrap.dedent(
|
||||
"""
|
||||
<video youtube="1:{youtube_id}" sub="">
|
||||
<source src="http://www.testvid.org/html5/videos/testvid.mp4"/>
|
||||
<source src="http://www.testvid2.org/html5/videos/testvid2.webm"/>
|
||||
</video>
|
||||
""".format(youtube_id=youtube_id)
|
||||
)
|
||||
modulestore().update_item(self.item, self.user.id)
|
||||
self.assert_current_subs(expected_subs='')
|
||||
|
||||
# Save new subs in the content store.
|
||||
subs = {
|
||||
'start': [100, 200, 240],
|
||||
'end': [200, 240, 380],
|
||||
'text': [
|
||||
'subs #1',
|
||||
'subs #2',
|
||||
'subs #3'
|
||||
]
|
||||
}
|
||||
self.save_subs_to_store(subs, youtube_id)
|
||||
|
||||
# Now, make request to /transcripts/save endpoint with new subs.
|
||||
data = {
|
||||
'locator': unicode(self.video_usage_key),
|
||||
'metadata': {
|
||||
'sub': youtube_id
|
||||
}
|
||||
}
|
||||
resp = self.client.get(reverse('save_transcripts'), {'data': json.dumps(data)})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(json.loads(resp.content), {"status": "Success"})
|
||||
|
||||
# Now check item.sub, it should be same as youtube id because /transcripts/save prioritize
|
||||
# youtube subs over html5 ones.
|
||||
self.assert_current_subs(expected_subs=youtube_id)
|
||||
|
||||
@@ -229,7 +229,15 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
original_video = self.previous_uploads[-(i + 1)]
|
||||
self.assertEqual(
|
||||
set(response_video.keys()),
|
||||
set(['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url'])
|
||||
set([
|
||||
'edx_video_id',
|
||||
'client_video_id',
|
||||
'created',
|
||||
'duration',
|
||||
'status',
|
||||
'course_video_image_url',
|
||||
'transcripts'
|
||||
])
|
||||
)
|
||||
dateutil.parser.parse(response_video['created'])
|
||||
for field in ['edx_video_id', 'client_video_id', 'duration']:
|
||||
@@ -241,13 +249,6 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
|
||||
@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'],
|
||||
[
|
||||
@@ -262,7 +263,6 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
['en']
|
||||
),
|
||||
(
|
||||
True,
|
||||
['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url',
|
||||
'transcripts'],
|
||||
[
|
||||
@@ -285,14 +285,10 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
)
|
||||
)
|
||||
@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):
|
||||
def test_get_json_transcripts(self, expected_video_keys, uploaded_transcripts, expected_transcripts):
|
||||
"""
|
||||
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'],
|
||||
|
||||
@@ -130,23 +130,18 @@ def transcript_credentials_handler(request, course_key_string):
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def transcript_download_handler(request, course_key_string):
|
||||
def transcript_download_handler(request):
|
||||
"""
|
||||
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.
|
||||
- A 404 if there is no such transcript.
|
||||
"""
|
||||
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(
|
||||
@@ -206,27 +201,20 @@ def validate_transcript_upload_data(data, files):
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def transcript_upload_handler(request, course_key_string):
|
||||
def transcript_upload_handler(request):
|
||||
"""
|
||||
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)
|
||||
@@ -276,14 +264,13 @@ def transcript_delete_handler(request, course_key_string, edx_video_id, language
|
||||
language_code: transcript's language code.
|
||||
|
||||
Returns
|
||||
- A 404 if the corresponding feature flag is disabled or user does not have required permisions
|
||||
- A 404 if the user does not have required permisions
|
||||
- A 200 if transcript is deleted without any error(s)
|
||||
"""
|
||||
# Check whether the feature is available for this course.
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
video_transcripts_enabled = VideoTranscriptEnabledFlag.feature_enabled(course_key)
|
||||
# User needs to have studio write access for this course.
|
||||
if not video_transcripts_enabled or not has_studio_write_access(request.user, course_key):
|
||||
if not has_studio_write_access(request.user, course_key):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
delete_video_transcript(video_id=edx_video_id, language_code=language_code)
|
||||
|
||||
@@ -14,12 +14,13 @@ import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files.base import ContentFile
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from six import text_type
|
||||
|
||||
from edxval.api import create_or_update_video_transcript, create_external_video
|
||||
from student.auth import has_course_author_access
|
||||
from util.json_request import JsonResponse
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
@@ -28,21 +29,24 @@ from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.video_module.transcripts_utils import (
|
||||
clean_video_id,
|
||||
copy_or_rename_transcript,
|
||||
download_youtube_subs,
|
||||
GetTranscriptsFromYouTubeException,
|
||||
get_video_transcript_content,
|
||||
generate_subs_from_source,
|
||||
get_transcript_for_video,
|
||||
get_transcripts_from_youtube,
|
||||
manage_video_subtitles_save,
|
||||
remove_subs_from_store,
|
||||
Transcript,
|
||||
TranscriptsRequestValidationException,
|
||||
TranscriptsGenerationException,
|
||||
youtube_video_transcript_name,
|
||||
get_transcript,
|
||||
get_transcript_from_val,
|
||||
)
|
||||
from xmodule.video_module.transcripts_model_utils import (
|
||||
is_val_transcript_feature_enabled_for_course
|
||||
)
|
||||
|
||||
from cms.djangoapps.contentstore.views.videos import TranscriptProvider
|
||||
|
||||
__all__ = [
|
||||
'upload_transcripts',
|
||||
@@ -51,7 +55,6 @@ __all__ = [
|
||||
'choose_transcripts',
|
||||
'replace_transcripts',
|
||||
'rename_transcripts',
|
||||
'save_transcripts',
|
||||
]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -68,6 +71,126 @@ def error_response(response, message, status_code=400):
|
||||
return JsonResponse(response, status_code)
|
||||
|
||||
|
||||
def link_video_to_component(video_component, user):
|
||||
"""
|
||||
Links a VAL video to the video component.
|
||||
|
||||
Arguments:
|
||||
video_component: video descriptor item.
|
||||
user: A requesting user.
|
||||
|
||||
Returns:
|
||||
A cleaned Video ID.
|
||||
"""
|
||||
edx_video_id = clean_video_id(video_component.edx_video_id)
|
||||
if not edx_video_id:
|
||||
edx_video_id = create_external_video(display_name=u'external video')
|
||||
video_component.edx_video_id = edx_video_id
|
||||
video_component.save_with_metadata(user)
|
||||
|
||||
return edx_video_id
|
||||
|
||||
|
||||
def save_video_transcript(edx_video_id, input_format, transcript_content, language_code):
|
||||
"""
|
||||
Saves a video transcript to the VAL and its content to the configured django storage(DS).
|
||||
|
||||
Arguments:
|
||||
edx_video_id: A Video ID to associate the transcript.
|
||||
input_format: Input transcript format for content being passed.
|
||||
transcript_content: Content of the transcript file
|
||||
language_code: transcript language code
|
||||
|
||||
Returns:
|
||||
A boolean indicating whether the transcript was saved or not.
|
||||
"""
|
||||
try:
|
||||
# Convert the transcript into the 'sjson' and upload it to
|
||||
# configured transcript storage. For example, S3.
|
||||
sjson_subs = Transcript.convert(
|
||||
content=transcript_content,
|
||||
input_format=input_format,
|
||||
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': language_code
|
||||
},
|
||||
file_data=ContentFile(sjson_subs),
|
||||
)
|
||||
result = True
|
||||
except (TranscriptsGenerationException, UnicodeDecodeError):
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_video_module(request, locator):
|
||||
"""
|
||||
Validates video module given its locator and request. Also, checks
|
||||
if requesting user has course authoring access.
|
||||
|
||||
Arguments:
|
||||
request: WSGI request.
|
||||
locator: video locator.
|
||||
|
||||
Returns:
|
||||
A tuple containing error(or None) and video descriptor(i.e. if validation succeeds).
|
||||
|
||||
Raises:
|
||||
PermissionDenied: if requesting user does not have access to author the video component.
|
||||
"""
|
||||
error, item = None, None
|
||||
try:
|
||||
item = _get_item(request, {'locator': locator})
|
||||
if item.category != 'video':
|
||||
error = _(u'Transcripts are supported only for "video" modules.')
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
error = _(u'Cannot find item by locator.')
|
||||
|
||||
return error, item
|
||||
|
||||
|
||||
def validate_transcript_upload_data(request):
|
||||
"""
|
||||
Validates video transcript file.
|
||||
|
||||
Arguments:
|
||||
request: A WSGI request's data part.
|
||||
|
||||
Returns:
|
||||
Tuple containing an error and validated data
|
||||
If there is a validation error then, validated data will be empty.
|
||||
"""
|
||||
error, validated_data = None, {}
|
||||
data, files = request.POST, request.FILES
|
||||
video_locator = data.get('locator')
|
||||
edx_video_id = data.get('edx_video_id')
|
||||
if not video_locator:
|
||||
error = _(u'Video locator is required.')
|
||||
elif 'transcript-file' not in files:
|
||||
error = _(u'A transcript file is required.')
|
||||
elif os.path.splitext(files['transcript-file'].name)[1][1:] != Transcript.SRT:
|
||||
error = _(u'This transcript file type is not supported.')
|
||||
elif 'edx_video_id' not in data:
|
||||
error = _(u'Video ID is required.')
|
||||
|
||||
if not error:
|
||||
error, video = validate_video_module(request, video_locator)
|
||||
if not error:
|
||||
validated_data.update({
|
||||
'video': video,
|
||||
'edx_video_id': clean_video_id(edx_video_id) or clean_video_id(video.edx_video_id),
|
||||
'transcript_file': files['transcript-file']
|
||||
})
|
||||
|
||||
return error, validated_data
|
||||
|
||||
|
||||
@login_required
|
||||
def upload_transcripts(request):
|
||||
"""
|
||||
@@ -78,67 +201,51 @@ def upload_transcripts(request):
|
||||
status: 'Success' and HTTP 200 or 'Error' and HTTP 400.
|
||||
subs: Value of uploaded and saved html5 sub field in video item.
|
||||
"""
|
||||
response = {
|
||||
'status': 'Unknown server error',
|
||||
'subs': '',
|
||||
}
|
||||
|
||||
locator = request.POST.get('locator')
|
||||
if not locator:
|
||||
return error_response(response, 'POST data without "locator" form data.')
|
||||
|
||||
try:
|
||||
item = _get_item(request, request.POST)
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
return error_response(response, "Can't find item by locator.")
|
||||
|
||||
if 'transcript-file' not in request.FILES:
|
||||
return error_response(response, 'POST data without "file" form data.')
|
||||
|
||||
video_list = request.POST.get('video_list')
|
||||
if not video_list:
|
||||
return error_response(response, 'POST data without video names.')
|
||||
|
||||
try:
|
||||
video_list = json.loads(video_list)
|
||||
except ValueError:
|
||||
return error_response(response, 'Invalid video_list JSON.')
|
||||
|
||||
# Used utf-8-sig encoding type instead of utf-8 to remove BOM(Byte Order Mark), e.g. U+FEFF
|
||||
source_subs_filedata = request.FILES['transcript-file'].read().decode('utf-8-sig')
|
||||
source_subs_filename = request.FILES['transcript-file'].name
|
||||
|
||||
if '.' not in source_subs_filename:
|
||||
return error_response(response, "Undefined file extension.")
|
||||
|
||||
basename = os.path.basename(source_subs_filename)
|
||||
source_subs_name = os.path.splitext(basename)[0]
|
||||
source_subs_ext = os.path.splitext(basename)[1][1:]
|
||||
|
||||
if item.category != 'video':
|
||||
return error_response(response, 'Transcripts are supported only for "video" modules.')
|
||||
|
||||
# Allow upload only if any video link is presented
|
||||
if video_list:
|
||||
sub_attr = source_subs_name
|
||||
try:
|
||||
# Generate and save for 1.0 speed, will create subs_sub_attr.srt.sjson subtitles file in storage.
|
||||
generate_subs_from_source({1: sub_attr}, source_subs_ext, source_subs_filedata, item)
|
||||
|
||||
for video_dict in video_list:
|
||||
video_name = video_dict['video']
|
||||
# We are creating transcripts for every video source, if in future some of video sources would be deleted.
|
||||
# Updates item.sub with `video_name` on success.
|
||||
copy_or_rename_transcript(video_name, sub_attr, item, user=request.user)
|
||||
|
||||
response['subs'] = item.sub
|
||||
response['status'] = 'Success'
|
||||
except Exception as ex:
|
||||
return error_response(response, text_type(ex))
|
||||
error, validated_data = validate_transcript_upload_data(request)
|
||||
if error:
|
||||
response = JsonResponse({'status': error}, status=400)
|
||||
else:
|
||||
return error_response(response, 'Empty video sources.')
|
||||
video = validated_data['video']
|
||||
edx_video_id = validated_data['edx_video_id']
|
||||
transcript_file = validated_data['transcript_file']
|
||||
# check if we need to create an external VAL video to associate the transcript
|
||||
# and save its ID on the video component.
|
||||
if not edx_video_id:
|
||||
edx_video_id = create_external_video(display_name=u'external video')
|
||||
video.edx_video_id = edx_video_id
|
||||
video.save_with_metadata(request.user)
|
||||
|
||||
return JsonResponse(response)
|
||||
response = JsonResponse({'edx_video_id': edx_video_id, 'status': 'Success'}, status=200)
|
||||
|
||||
try:
|
||||
# Convert 'srt' transcript into the 'sjson' and upload it to
|
||||
# configured transcript storage. For example, S3.
|
||||
sjson_subs = Transcript.convert(
|
||||
content=transcript_file.read(),
|
||||
input_format=Transcript.SRT,
|
||||
output_format=Transcript.SJSON
|
||||
)
|
||||
transcript_created = create_or_update_video_transcript(
|
||||
video_id=edx_video_id,
|
||||
language_code=u'en',
|
||||
metadata={
|
||||
'provider': TranscriptProvider.CUSTOM,
|
||||
'file_format': Transcript.SJSON,
|
||||
'language_code': u'en'
|
||||
},
|
||||
file_data=ContentFile(sjson_subs),
|
||||
)
|
||||
|
||||
if transcript_created is None:
|
||||
response = JsonResponse({'status': 'Invalid Video ID'}, status=400)
|
||||
|
||||
except (TranscriptsGenerationException, UnicodeDecodeError):
|
||||
|
||||
response = JsonResponse({
|
||||
'status': _(u'There is a problem with this transcript file. Try to upload a different file.')
|
||||
}, status=400)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -148,54 +255,18 @@ def download_transcripts(request):
|
||||
|
||||
Raises Http404 if unsuccessful.
|
||||
"""
|
||||
locator = request.GET.get('locator')
|
||||
subs_id = request.GET.get('subs_id')
|
||||
if not locator:
|
||||
log.debug('GET data without "locator" property.')
|
||||
error, video = validate_video_module(request, locator=request.GET.get('locator'))
|
||||
if error:
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
item = _get_item(request, request.GET)
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
log.debug("Can't find item by locator.")
|
||||
raise Http404
|
||||
|
||||
if item.category != 'video':
|
||||
log.debug('transcripts are supported only for video" modules.')
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
if not subs_id:
|
||||
raise NotFoundError
|
||||
|
||||
filename = subs_id
|
||||
content_location = StaticContent.compute_location(
|
||||
item.location.course_key,
|
||||
'subs_{filename}.srt.sjson'.format(filename=filename),
|
||||
)
|
||||
input_format = Transcript.SJSON
|
||||
transcript_content = contentstore().find(content_location).data
|
||||
content, filename, mimetype = get_transcript(video, lang=u'en')
|
||||
except NotFoundError:
|
||||
# Try searching in VAL for the transcript as a last resort
|
||||
transcript = None
|
||||
if is_val_transcript_feature_enabled_for_course(item.location.course_key):
|
||||
transcript = get_video_transcript_content(edx_video_id=item.edx_video_id, language_code=u'en')
|
||||
|
||||
if not transcript:
|
||||
raise Http404
|
||||
|
||||
name_and_extension = os.path.splitext(transcript['file_name'])
|
||||
filename, input_format = name_and_extension[0], name_and_extension[1][1:]
|
||||
transcript_content = transcript['content']
|
||||
|
||||
# convert sjson content into srt format.
|
||||
transcript_content = Transcript.convert(transcript_content, input_format=input_format, output_format=Transcript.SRT)
|
||||
if not transcript_content:
|
||||
raise Http404
|
||||
|
||||
# Construct an HTTP response
|
||||
response = HttpResponse(transcript_content, content_type='application/x-subrip; charset=utf-8')
|
||||
response['Content-Disposition'] = 'attachment; filename="{filename}.srt"'.format(filename=filename)
|
||||
response = HttpResponse(content, content_type=mimetype)
|
||||
response['Content-Disposition'] = 'attachment; filename="{filename}"'.format(filename=filename.encode('utf-8'))
|
||||
return response
|
||||
|
||||
|
||||
@@ -237,6 +308,7 @@ def check_transcripts(request):
|
||||
'current_item_subs': None,
|
||||
'status': 'Error',
|
||||
}
|
||||
|
||||
try:
|
||||
__, videos, item = _validate_transcripts_data(request)
|
||||
except TranscriptsRequestValidationException as e:
|
||||
@@ -244,75 +316,72 @@ def check_transcripts(request):
|
||||
|
||||
transcripts_presence['status'] = 'Success'
|
||||
|
||||
filename = 'subs_{0}.srt.sjson'.format(item.sub)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
transcripts_presence['current_item_subs'] = item.sub
|
||||
edx_video_id = clean_video_id(videos.get('edx_video_id'))
|
||||
get_transcript_from_val(edx_video_id=edx_video_id, lang=u'en')
|
||||
command = 'found'
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
# Check for youtube transcripts presence
|
||||
youtube_id = videos.get('youtube', None)
|
||||
if youtube_id:
|
||||
transcripts_presence['is_youtube_mode'] = True
|
||||
|
||||
# youtube local
|
||||
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
|
||||
filename = 'subs_{0}.srt.sjson'.format(item.sub)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
transcripts_presence['youtube_local'] = True
|
||||
transcripts_presence['current_item_subs'] = item.sub
|
||||
except NotFoundError:
|
||||
log.debug("Can't find transcripts in storage for youtube id: %s", youtube_id)
|
||||
pass
|
||||
|
||||
# youtube server
|
||||
youtube_text_api = copy.deepcopy(settings.YOUTUBE['TEXT_API'])
|
||||
youtube_text_api['params']['v'] = youtube_id
|
||||
youtube_transcript_name = youtube_video_transcript_name(youtube_text_api)
|
||||
if youtube_transcript_name:
|
||||
youtube_text_api['params']['name'] = youtube_transcript_name
|
||||
youtube_response = requests.get('http://' + youtube_text_api['url'], params=youtube_text_api['params'])
|
||||
# Check for youtube transcripts presence
|
||||
youtube_id = videos.get('youtube', None)
|
||||
if youtube_id:
|
||||
transcripts_presence['is_youtube_mode'] = True
|
||||
|
||||
if youtube_response.status_code == 200 and youtube_response.text:
|
||||
transcripts_presence['youtube_server'] = True
|
||||
#check youtube local and server transcripts for equality
|
||||
if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
|
||||
# youtube local
|
||||
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
youtube_server_subs = get_transcripts_from_youtube(
|
||||
youtube_id,
|
||||
settings,
|
||||
item.runtime.service(item, "i18n")
|
||||
)
|
||||
if json.loads(local_transcripts) == youtube_server_subs: # check transcripts for equality
|
||||
transcripts_presence['youtube_diff'] = False
|
||||
except GetTranscriptsFromYouTubeException:
|
||||
pass
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
transcripts_presence['youtube_local'] = True
|
||||
except NotFoundError:
|
||||
log.debug("Can't find transcripts in storage for youtube id: %s", youtube_id)
|
||||
|
||||
# Check for html5 local transcripts presence
|
||||
html5_subs = []
|
||||
for html5_id in videos['html5']:
|
||||
filename = 'subs_{0}.srt.sjson'.format(html5_id)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
html5_subs.append(contentstore().find(content_location).data)
|
||||
transcripts_presence['html5_local'].append(html5_id)
|
||||
except NotFoundError:
|
||||
log.debug("Can't find transcripts in storage for non-youtube video_id: %s", html5_id)
|
||||
if len(html5_subs) == 2: # check html5 transcripts for equality
|
||||
transcripts_presence['html5_equal'] = json.loads(html5_subs[0]) == json.loads(html5_subs[1])
|
||||
# youtube server
|
||||
youtube_text_api = copy.deepcopy(settings.YOUTUBE['TEXT_API'])
|
||||
youtube_text_api['params']['v'] = youtube_id
|
||||
youtube_transcript_name = youtube_video_transcript_name(youtube_text_api)
|
||||
if youtube_transcript_name:
|
||||
youtube_text_api['params']['name'] = youtube_transcript_name
|
||||
youtube_response = requests.get('http://' + youtube_text_api['url'], params=youtube_text_api['params'])
|
||||
|
||||
command, subs_to_use = _transcripts_logic(transcripts_presence, videos)
|
||||
if command == 'not_found':
|
||||
# Try searching in VAL for the transcript as a last resort
|
||||
if is_val_transcript_feature_enabled_for_course(item.location.course_key):
|
||||
video_transcript = get_video_transcript_content(edx_video_id=item.edx_video_id, language_code=u'en')
|
||||
command = 'found' if video_transcript else command
|
||||
if youtube_response.status_code == 200 and youtube_response.text:
|
||||
transcripts_presence['youtube_server'] = True
|
||||
#check youtube local and server transcripts for equality
|
||||
if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
|
||||
try:
|
||||
youtube_server_subs = get_transcripts_from_youtube(
|
||||
youtube_id,
|
||||
settings,
|
||||
item.runtime.service(item, "i18n")
|
||||
)
|
||||
if json.loads(local_transcripts) == youtube_server_subs: # check transcripts for equality
|
||||
transcripts_presence['youtube_diff'] = False
|
||||
except GetTranscriptsFromYouTubeException:
|
||||
pass
|
||||
|
||||
transcripts_presence.update({
|
||||
'command': command,
|
||||
'subs': subs_to_use,
|
||||
})
|
||||
# Check for html5 local transcripts presence
|
||||
html5_subs = []
|
||||
for html5_id in videos['html5']:
|
||||
filename = 'subs_{0}.srt.sjson'.format(html5_id)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
html5_subs.append(contentstore().find(content_location).data)
|
||||
transcripts_presence['html5_local'].append(html5_id)
|
||||
except NotFoundError:
|
||||
log.debug("Can't find transcripts in storage for non-youtube video_id: %s", html5_id)
|
||||
if len(html5_subs) == 2: # check html5 transcripts for equality
|
||||
transcripts_presence['html5_equal'] = json.loads(html5_subs[0]) == json.loads(html5_subs[1])
|
||||
|
||||
command, __ = _transcripts_logic(transcripts_presence, videos)
|
||||
|
||||
transcripts_presence.update({'command': command})
|
||||
return JsonResponse(transcripts_presence)
|
||||
|
||||
|
||||
@@ -374,78 +443,6 @@ def _transcripts_logic(transcripts_presence, videos):
|
||||
return command, subs
|
||||
|
||||
|
||||
@login_required
|
||||
def choose_transcripts(request):
|
||||
"""
|
||||
Replaces html5 subtitles, presented for both html5 sources, with chosen one.
|
||||
|
||||
Code removes rejected html5 subtitles and updates sub attribute with chosen html5_id.
|
||||
|
||||
It does nothing with youtube id's.
|
||||
|
||||
Returns: status `Success` and resulted item.sub value or status `Error` and HTTP 400.
|
||||
"""
|
||||
response = {
|
||||
'status': 'Error',
|
||||
'subs': '',
|
||||
}
|
||||
|
||||
try:
|
||||
data, videos, item = _validate_transcripts_data(request)
|
||||
except TranscriptsRequestValidationException as e:
|
||||
return error_response(response, text_type(e))
|
||||
|
||||
html5_id = data.get('html5_id') # html5_id chosen by user
|
||||
|
||||
# find rejected html5_id and remove appropriate subs from store
|
||||
html5_id_to_remove = [x for x in videos['html5'] if x != html5_id]
|
||||
if html5_id_to_remove:
|
||||
remove_subs_from_store(html5_id_to_remove, item)
|
||||
|
||||
if item.sub != html5_id: # update sub value
|
||||
item.sub = html5_id
|
||||
item.save_with_metadata(request.user)
|
||||
response = {
|
||||
'status': 'Success',
|
||||
'subs': item.sub,
|
||||
}
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
@login_required
|
||||
def replace_transcripts(request):
|
||||
"""
|
||||
Replaces all transcripts with youtube ones.
|
||||
|
||||
Downloads subtitles from youtube and replaces all transcripts with downloaded ones.
|
||||
|
||||
Returns: status `Success` and resulted item.sub value or status `Error` and HTTP 400.
|
||||
"""
|
||||
response = {'status': 'Error', 'subs': ''}
|
||||
|
||||
try:
|
||||
__, videos, item = _validate_transcripts_data(request)
|
||||
except TranscriptsRequestValidationException as e:
|
||||
return error_response(response, text_type(e))
|
||||
|
||||
youtube_id = videos['youtube']
|
||||
if not youtube_id:
|
||||
return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id))
|
||||
|
||||
try:
|
||||
download_youtube_subs(youtube_id, item, settings)
|
||||
except GetTranscriptsFromYouTubeException as e:
|
||||
return error_response(response, text_type(e))
|
||||
|
||||
item.sub = youtube_id
|
||||
item.save_with_metadata(request.user)
|
||||
response = {
|
||||
'status': 'Success',
|
||||
'subs': item.sub,
|
||||
}
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
def _validate_transcripts_data(request):
|
||||
"""
|
||||
Validates, that request contains all proper data for transcripts processing.
|
||||
@@ -476,6 +473,9 @@ def _validate_transcripts_data(request):
|
||||
for video_data in data.get('videos'):
|
||||
if video_data['type'] == 'youtube':
|
||||
videos['youtube'] = video_data['video']
|
||||
elif video_data['type'] == 'edx_video_id':
|
||||
if clean_video_id(video_data['video']):
|
||||
videos['edx_video_id'] = video_data['video']
|
||||
else: # do not add same html5 videos
|
||||
if videos['html5'].get('video') != video_data['video']:
|
||||
videos['html5'][video_data['video']] = video_data['mode']
|
||||
@@ -483,78 +483,162 @@ def _validate_transcripts_data(request):
|
||||
return data, videos, item
|
||||
|
||||
|
||||
def validate_transcripts_request(request, include_yt=False, include_html5=False):
|
||||
"""
|
||||
Validates transcript handler's request.
|
||||
|
||||
NOTE: This is one central validation flow for `choose_transcripts`,
|
||||
`check_transcripts` and `replace_transcripts` handlers.
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
1. An error message in case of validation failure.
|
||||
2. validated video data
|
||||
"""
|
||||
error = None
|
||||
validated_data = {'video': None, 'youtube': '', 'html5': {}}
|
||||
# Loads the request data
|
||||
data = json.loads(request.GET.get('data', '{}'))
|
||||
if not data:
|
||||
error = _(u'Incoming video data is empty.')
|
||||
else:
|
||||
error, video = validate_video_module(request, locator=data.get('locator'))
|
||||
if not error:
|
||||
validated_data.update({'video': video})
|
||||
|
||||
videos = data.get('videos', [])
|
||||
if include_yt:
|
||||
validated_data.update({
|
||||
video['type']: video['video']
|
||||
for video in videos
|
||||
if video['type'] == 'youtube'
|
||||
})
|
||||
|
||||
if include_html5:
|
||||
validated_data['chosen_html5_id'] = data.get('html5_id')
|
||||
validated_data['html5'] = {
|
||||
video['video']: video['mode']
|
||||
for video in videos
|
||||
if video['type'] != 'youtube'
|
||||
}
|
||||
|
||||
return error, validated_data
|
||||
|
||||
|
||||
@login_required
|
||||
def choose_transcripts(request):
|
||||
"""
|
||||
Create/Update edx transcript in DS with chosen html5 subtitles from contentstore.
|
||||
|
||||
Returns:
|
||||
status `Success` and resulted `edx_video_id` value
|
||||
Or error in case of validation failures.
|
||||
"""
|
||||
error, validated_data = validate_transcripts_request(request, include_html5=True)
|
||||
if error:
|
||||
response = error_response({}, error)
|
||||
else:
|
||||
# 1. Retrieve transcript file for `chosen_html5_id` from contentstore.
|
||||
try:
|
||||
video = validated_data['video']
|
||||
chosen_html5_id = validated_data['chosen_html5_id']
|
||||
input_format, __, transcript_content = get_transcript_for_video(
|
||||
video.location,
|
||||
subs_id=chosen_html5_id,
|
||||
file_name=chosen_html5_id,
|
||||
language=u'en'
|
||||
)
|
||||
except NotFoundError:
|
||||
return error_response({}, _('No such transcript.'))
|
||||
|
||||
# 2. Link a video to video component if its not already linked to one.
|
||||
edx_video_id = link_video_to_component(video, request.user)
|
||||
|
||||
# 3. Upload the retrieved transcript to DS for the linked video ID.
|
||||
success = save_video_transcript(edx_video_id, input_format, transcript_content, language_code=u'en')
|
||||
if success:
|
||||
response = JsonResponse({'edx_video_id': edx_video_id, 'status': 'Success'}, status=200)
|
||||
else:
|
||||
response = error_response({}, _('There is a problem with the chosen transcript file.'))
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def rename_transcripts(request):
|
||||
"""
|
||||
Create copies of existing subtitles with new names of HTML5 sources.
|
||||
Copies existing transcript on video component's `sub`(from contentstore) into the
|
||||
DS for a video.
|
||||
|
||||
Old subtitles are not deleted now, because we do not have rollback functionality.
|
||||
|
||||
If succeed, Item.sub will be chosen randomly from html5 video sources provided by front-end.
|
||||
Returns:
|
||||
status `Success` and resulted `edx_video_id` value
|
||||
Or error in case of validation failures.
|
||||
"""
|
||||
response = {'status': 'Error', 'subs': ''}
|
||||
|
||||
try:
|
||||
__, videos, item = _validate_transcripts_data(request)
|
||||
except TranscriptsRequestValidationException as e:
|
||||
return error_response(response, text_type(e))
|
||||
|
||||
old_name = item.sub
|
||||
|
||||
for new_name in videos['html5'].keys(): # copy subtitles for every HTML5 source
|
||||
error, validated_data = validate_transcripts_request(request)
|
||||
if error:
|
||||
response = error_response({}, error)
|
||||
else:
|
||||
# 1. Retrieve transcript file for `video.sub` from contentstore.
|
||||
try:
|
||||
# updates item.sub with new_name if it is successful.
|
||||
copy_or_rename_transcript(new_name, old_name, item, user=request.user)
|
||||
video = validated_data['video']
|
||||
input_format, __, transcript_content = get_transcript_for_video(
|
||||
video.location,
|
||||
subs_id=video.sub,
|
||||
file_name=video.sub,
|
||||
language=u'en'
|
||||
)
|
||||
except NotFoundError:
|
||||
# subtitles file `item.sub` is not presented in the system. Nothing to copy or rename.
|
||||
error_response(response, "Can't find transcripts in storage for {}".format(old_name))
|
||||
return error_response({}, _('No such transcript.'))
|
||||
|
||||
response['status'] = 'Success'
|
||||
response['subs'] = item.sub # item.sub has been changed, it is not equal to old_name.
|
||||
log.debug("Updated item.sub to %s", item.sub)
|
||||
return JsonResponse(response)
|
||||
# 2. Link a video to video component if its not already linked to one.
|
||||
edx_video_id = link_video_to_component(video, request.user)
|
||||
|
||||
# 3. Upload the retrieved transcript to DS for the linked video ID.
|
||||
success = save_video_transcript(edx_video_id, input_format, transcript_content, language_code=u'en')
|
||||
if success:
|
||||
response = JsonResponse({'edx_video_id': edx_video_id, 'status': 'Success'}, status=200)
|
||||
else:
|
||||
response = error_response(
|
||||
{}, _('There is a problem with the existing transcript file. Please upload a different file.')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def save_transcripts(request):
|
||||
def replace_transcripts(request):
|
||||
"""
|
||||
Saves video module with updated values of fields.
|
||||
Downloads subtitles from youtube and replaces edx transcripts in DS with youtube ones.
|
||||
|
||||
Returns: status `Success` or status `Error` and HTTP 400.
|
||||
Returns:
|
||||
status `Success` and resulted `edx_video_id` value
|
||||
Or error on validation failures.
|
||||
"""
|
||||
response = {'status': 'Error'}
|
||||
error, validated_data = validate_transcripts_request(request, include_yt=True)
|
||||
youtube_id = validated_data['youtube']
|
||||
if error:
|
||||
response = error_response({}, error)
|
||||
elif not youtube_id:
|
||||
response = error_response({}, _(u'YouTube ID is required.'))
|
||||
else:
|
||||
# 1. Download transcript from YouTube.
|
||||
try:
|
||||
video = validated_data['video']
|
||||
transcript_content = download_youtube_subs(youtube_id, video, settings)
|
||||
except GetTranscriptsFromYouTubeException as e:
|
||||
return error_response({}, text_type(e))
|
||||
|
||||
data = json.loads(request.GET.get('data', '{}'))
|
||||
if not data:
|
||||
return error_response(response, 'Incoming video data is empty.')
|
||||
# 2. Link a video to video component if its not already linked to one.
|
||||
edx_video_id = link_video_to_component(video, request.user)
|
||||
|
||||
try:
|
||||
item = _get_item(request, data)
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
return error_response(response, "Can't find item by locator.")
|
||||
|
||||
metadata = data.get('metadata')
|
||||
if metadata is not None:
|
||||
new_sub = metadata.get('sub')
|
||||
|
||||
for metadata_key, value in metadata.items():
|
||||
setattr(item, metadata_key, value)
|
||||
|
||||
item.save_with_metadata(request.user) # item becomes updated with new values
|
||||
|
||||
if new_sub:
|
||||
manage_video_subtitles_save(item, request.user)
|
||||
# 3. Upload YT transcript to DS for the linked video ID.
|
||||
success = save_video_transcript(edx_video_id, Transcript.SJSON, transcript_content, language_code=u'en')
|
||||
if success:
|
||||
response = JsonResponse({'edx_video_id': edx_video_id, 'status': 'Success'}, status=200)
|
||||
else:
|
||||
# If `new_sub` is empty, it means that user explicitly does not want to use
|
||||
# transcripts for current video ids and we remove all transcripts from storage.
|
||||
current_subs = data.get('current_subs')
|
||||
if current_subs is not None:
|
||||
for sub in current_subs:
|
||||
remove_subs_from_store(sub, item)
|
||||
response = error_response({}, _('There is a problem with the YouTube transcript file.'))
|
||||
|
||||
response['status'] = 'Success'
|
||||
|
||||
return JsonResponse(response)
|
||||
return response
|
||||
|
||||
|
||||
def _get_item(request, data):
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.files.images import get_image_dimensions
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
@@ -533,15 +534,12 @@ 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:
|
||||
video['transcripts'] = get_available_transcript_languages(video_id=video['edx_video_id'])
|
||||
video['transcripts'] = get_available_transcript_languages(video_id=video['edx_video_id'])
|
||||
|
||||
return videos
|
||||
|
||||
@@ -558,10 +556,7 @@ def _get_index_videos(course):
|
||||
Returns the information about each video upload required for the video list
|
||||
"""
|
||||
course_id = unicode(course.id)
|
||||
attrs = ['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'courses']
|
||||
|
||||
if VideoTranscriptEnabledFlag.feature_enabled(course.id):
|
||||
attrs += ['transcripts']
|
||||
attrs = ['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'courses', 'transcripts']
|
||||
|
||||
def _get_values(video):
|
||||
"""
|
||||
@@ -631,14 +626,19 @@ def videos_index_html(course):
|
||||
'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
|
||||
},
|
||||
'is_video_transcript_enabled': is_video_transcript_enabled,
|
||||
'video_transcript_settings': None,
|
||||
'active_transcript_preferences': None,
|
||||
'transcript_credentials': None,
|
||||
'transcript_available_languages': None
|
||||
'transcript_available_languages': get_all_transcript_languages(),
|
||||
'video_transcript_settings': {
|
||||
'transcript_download_handler_url': reverse('transcript_download_handler'),
|
||||
'transcript_upload_handler_url': reverse('transcript_upload_handler'),
|
||||
'transcript_delete_handler_url': reverse_course_url('transcript_delete_handler', unicode(course.id)),
|
||||
'trancript_download_file_format': Transcript.SRT
|
||||
}
|
||||
}
|
||||
|
||||
if is_video_transcript_enabled:
|
||||
context['video_transcript_settings'] = {
|
||||
context['video_transcript_settings'].update({
|
||||
'transcript_preferences_handler_url': reverse_course_url(
|
||||
'transcript_preferences_handler',
|
||||
unicode(course.id)
|
||||
@@ -647,25 +647,11 @@ 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)
|
||||
),
|
||||
'transcript_delete_handler_url': reverse_course_url(
|
||||
'transcript_delete_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)
|
||||
|
||||
|
||||
@@ -145,6 +145,16 @@ if RELEASE_LINE == "master":
|
||||
'course_author': 'http://edx.readthedocs.io/projects/edx-partner-course-staff',
|
||||
}
|
||||
|
||||
########################## VIDEO TRANSCRIPTS STORAGE ############################
|
||||
VIDEO_TRANSCRIPTS_SETTINGS = dict(
|
||||
VIDEO_TRANSCRIPTS_MAX_BYTES=3 * 1024 * 1024, # 3 MB
|
||||
STORAGE_KWARGS=dict(
|
||||
location=MEDIA_ROOT,
|
||||
base_url=MEDIA_URL,
|
||||
),
|
||||
DIRECTORY_PREFIX='video-transcripts/',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
|
||||
@@ -242,10 +242,10 @@
|
||||
'js/spec/views/metadata_edit_spec',
|
||||
'js/spec/views/textbook_spec',
|
||||
'js/spec/views/upload_spec',
|
||||
'js/spec/video/transcripts/message_manager_spec',
|
||||
'js/spec/video/transcripts/utils_spec',
|
||||
'js/spec/video/transcripts/editor_spec',
|
||||
'js/spec/video/transcripts/videolist_spec',
|
||||
'js/spec/video/transcripts/message_manager_spec',
|
||||
'js/spec/video/transcripts/file_uploader_spec',
|
||||
'js/spec/models/component_template_spec',
|
||||
'js/spec/models/explicit_url_spec',
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
'jquery.iframe-transport': 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport', // eslint-disable-line max-len
|
||||
'jquery.inputnumber': 'xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill',
|
||||
'jquery.immediateDescendents': 'xmodule_js/common_static/js/src/jquery.immediateDescendents',
|
||||
'jquery.ajaxQueue': 'xmodule_js/common_static/js/vendor/jquery.ajaxQueue',
|
||||
'datepair': 'xmodule_js/common_static/js/vendor/timepicker/datepair',
|
||||
'date': 'xmodule_js/common_static/js/vendor/date',
|
||||
'text': 'xmodule_js/common_static/js/vendor/requirejs/text',
|
||||
@@ -108,6 +109,10 @@
|
||||
deps: ['jquery', 'tinymce'],
|
||||
exports: 'jQuery.fn.tinymce'
|
||||
},
|
||||
'jquery.ajaxQueue': {
|
||||
deps: ['jquery'],
|
||||
exports: 'jQuery.fn.ajaxQueue'
|
||||
},
|
||||
'datepair': {
|
||||
deps: ['jquery.ui', 'jquery.timepicker']
|
||||
},
|
||||
|
||||
@@ -55,8 +55,7 @@ define([
|
||||
videoImageSettings: videoImageSettings,
|
||||
videoTranscriptSettings: videoTranscriptSettings,
|
||||
transcriptAvailableLanguages: transcriptAvailableLanguages,
|
||||
videoSupportedFileFormats: videoSupportedFileFormats,
|
||||
isVideoTranscriptEnabled: isVideoTranscriptEnabled
|
||||
videoSupportedFileFormats: videoSupportedFileFormats
|
||||
});
|
||||
$contentWrapper.find('.wrapper-assets').replaceWith(updatedView.render().$el);
|
||||
});
|
||||
@@ -71,8 +70,7 @@ define([
|
||||
videoImageSettings: videoImageSettings,
|
||||
videoTranscriptSettings: videoTranscriptSettings,
|
||||
transcriptAvailableLanguages: transcriptAvailableLanguages,
|
||||
videoSupportedFileFormats: videoSupportedFileFormats,
|
||||
isVideoTranscriptEnabled: isVideoTranscriptEnabled
|
||||
videoSupportedFileFormats: videoSupportedFileFormats
|
||||
});
|
||||
$contentWrapper.append(activeView.render().$el);
|
||||
$contentWrapper.append(previousView.render().$el);
|
||||
|
||||
@@ -11,7 +11,8 @@ define(['backbone'], function(Backbone) {
|
||||
explicitly_set: null,
|
||||
default_value: null,
|
||||
options: null,
|
||||
type: null
|
||||
type: null,
|
||||
custom: false // Used only for non-metadata fields
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
@@ -24,6 +25,11 @@ define(['backbone'], function(Backbone) {
|
||||
* property has changed.
|
||||
*/
|
||||
isModified: function() {
|
||||
// A non-metadata field will handle itself
|
||||
if (this.get('custom') === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ function($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCol
|
||||
field_name: 'edx_video_id',
|
||||
help: 'Specifies the video ID.',
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
type: 'VideoID',
|
||||
value: 'basic tab video id'
|
||||
},
|
||||
models = [DisplayNameEntry, VideoListEntry, VideoIDEntry],
|
||||
@@ -51,7 +51,8 @@ function($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCol
|
||||
object: testData,
|
||||
string: JSON.stringify(testData)
|
||||
},
|
||||
transcripts, $container;
|
||||
component_locator = 'component_locator',
|
||||
transcripts, $container, waitForEvent, editor;
|
||||
|
||||
var waitsForDisplayName = function(collection) {
|
||||
return jasmine.waitUntil(function() {
|
||||
@@ -76,15 +77,109 @@ function($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCol
|
||||
Utils.Storage.remove('sub');
|
||||
});
|
||||
|
||||
describe('Events', function() {
|
||||
beforeEach(function() {
|
||||
Utils.command.and.callThrough();
|
||||
spyOn(Backbone, 'trigger').and.callThrough();
|
||||
spyOn(Editor.prototype, 'destroy').and.callThrough();
|
||||
spyOn(Editor.prototype, 'handleFieldChanged').and.callThrough();
|
||||
spyOn(Editor.prototype, 'getLocator').and.returnValue(component_locator);
|
||||
|
||||
appendSetFixtures(
|
||||
sandbox({ // eslint-disable-line no-undef
|
||||
class: 'wrapper-comp-settings basic_metadata_edit',
|
||||
'data-metadata': JSON.stringify({video_url: VideoListEntry, edx_video_id: VideoIDEntry})
|
||||
})
|
||||
);
|
||||
|
||||
appendSetFixtures(
|
||||
$('<script>',
|
||||
{
|
||||
id: 'metadata-videolist-entry',
|
||||
type: 'text/template'
|
||||
}
|
||||
).text(readFixtures('video/transcripts/metadata-videolist-entry.underscore'))
|
||||
);
|
||||
|
||||
appendSetFixtures(
|
||||
$('<script>',
|
||||
{
|
||||
id: 'metadata-string-entry',
|
||||
type: 'text/template'
|
||||
}
|
||||
).text(readFixtures('metadata-string-entry.underscore'))
|
||||
);
|
||||
|
||||
editor = new Editor({
|
||||
el: $('.basic_metadata_edit')
|
||||
});
|
||||
|
||||
// reset the already triggered events
|
||||
Backbone.trigger.calls.reset();
|
||||
// reset the manual call to `handleFieldChanged` we made in the `editor.js::initialize`
|
||||
Editor.prototype.handleFieldChanged.calls.reset();
|
||||
});
|
||||
|
||||
waitForEvent = function(eventName) {
|
||||
var triggerCallArgs;
|
||||
return jasmine.waitUntil(function() {
|
||||
triggerCallArgs = Backbone.trigger.calls.mostRecent().args;
|
||||
return Backbone.trigger.calls.count() === 1 && triggerCallArgs[0] === eventName;
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(function() {
|
||||
Backbone.trigger.calls.reset();
|
||||
Editor.prototype.destroy.calls.reset();
|
||||
Editor.prototype.handleFieldChanged.calls.reset();
|
||||
});
|
||||
|
||||
it('handles transcripts:basicTabFieldChanged', function(done) {
|
||||
var event = 'transcripts:basicTabFieldChanged';
|
||||
|
||||
Backbone.trigger(event);
|
||||
waitForEvent(event)
|
||||
.then(function() {
|
||||
expect(Editor.prototype.handleFieldChanged).toHaveBeenCalled();
|
||||
expect(Utils.command).toHaveBeenCalledWith(
|
||||
'check',
|
||||
component_locator,
|
||||
[
|
||||
{ mode: 'youtube', video: '12345678901', type: 'youtube' },
|
||||
{ mode: 'html5', video: 'video', type: 'mp4' },
|
||||
{ mode: 'html5', video: 'video', type: 'webm' },
|
||||
{ mode: 'edx_video_id', type: 'edx_video_id', video: 'basic tab video id' }
|
||||
]
|
||||
);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('handles xblock:editorModalHidden', function(done) {
|
||||
var event = 'xblock:editorModalHidden';
|
||||
|
||||
Backbone.trigger(event);
|
||||
waitForEvent(event)
|
||||
.then(function() {
|
||||
expect(Editor.prototype.destroy).toHaveBeenCalled();
|
||||
}).always(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test initialization', function() {
|
||||
beforeEach(function() {
|
||||
spyOn(MetadataView, 'Editor');
|
||||
spyOn(Editor.prototype, 'handleFieldChanged');
|
||||
|
||||
transcripts = new Editor({
|
||||
el: $container
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
MetadataView.Editor.calls.reset();
|
||||
Editor.prototype.handleFieldChanged.calls.reset();
|
||||
});
|
||||
|
||||
$.each(metadataDict, function(index, val) {
|
||||
it('toModels with argument as ' + index, function() {
|
||||
expect(transcripts.toModels(val)).toEqual(models);
|
||||
@@ -159,6 +254,7 @@ function($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCol
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(MetadataView, 'Editor');
|
||||
spyOn(Editor.prototype, 'handleFieldChanged');
|
||||
|
||||
transcripts = new Editor({
|
||||
el: $container
|
||||
@@ -182,6 +278,11 @@ function($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCol
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
MetadataView.Editor.calls.reset();
|
||||
Editor.prototype.handleFieldChanged.calls.reset();
|
||||
});
|
||||
|
||||
describe('Test Advanced to Basic synchronization', function() {
|
||||
it('Correct data', function(done) {
|
||||
transcripts.syncBasicTab(metadataCollection, metadataView);
|
||||
@@ -362,31 +463,6 @@ function($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCol
|
||||
}).getValue();
|
||||
expect(youtubeValue).toEqual('');
|
||||
});
|
||||
|
||||
it('Timed Transcript field is updated', function() {
|
||||
Utils.Storage.set('sub', 'test_value');
|
||||
|
||||
transcripts.syncAdvancedTab(metadataCollection);
|
||||
|
||||
var collection = metadataCollection.models,
|
||||
subValue = collection[1].getValue();
|
||||
|
||||
expect(subValue).toEqual('test_value');
|
||||
});
|
||||
|
||||
it('Timed Transcript field is updated just once', function() {
|
||||
Utils.Storage.set('sub', 'test_value');
|
||||
|
||||
var collection = metadataCollection.models,
|
||||
subModel = collection[1];
|
||||
|
||||
spyOn(subModel, 'setValue');
|
||||
|
||||
transcripts.syncAdvancedTab(metadataCollection);
|
||||
transcripts.syncAdvancedTab(metadataCollection);
|
||||
transcripts.syncAdvancedTab(metadataCollection);
|
||||
expect(subModel.setValue.calls.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore',
|
||||
'jquery', 'underscore', 'backbone',
|
||||
'js/views/video/transcripts/utils', 'js/views/video/transcripts/file_uploader',
|
||||
'xmodule', 'jquery.form'
|
||||
],
|
||||
function($, _, Utils, FileUploader) {
|
||||
function($, _, Backbone, TranscriptUtils, FileUploader) {
|
||||
'use strict';
|
||||
|
||||
describe('Transcripts.FileUploader', function() {
|
||||
@@ -34,10 +34,6 @@ function($, _, Utils, FileUploader) {
|
||||
'MessageManager',
|
||||
['render', 'showError', 'hideError']
|
||||
),
|
||||
videoListObject = jasmine.createSpyObj(
|
||||
'MetadataView.VideoList',
|
||||
['render', 'getVideoObjectsList']
|
||||
),
|
||||
$container = $('.transcripts-status');
|
||||
|
||||
$container
|
||||
@@ -49,7 +45,6 @@ function($, _, Utils, FileUploader) {
|
||||
view = new FileUploader({
|
||||
el: $container,
|
||||
messenger: messenger,
|
||||
videoListObject: videoListObject,
|
||||
component_locator: 'component_locator'
|
||||
});
|
||||
});
|
||||
@@ -100,6 +95,12 @@ function($, _, Utils, FileUploader) {
|
||||
});
|
||||
|
||||
describe('Upload', function() {
|
||||
var videoId = '123-456-789-0';
|
||||
|
||||
beforeEach(function() {
|
||||
TranscriptUtils.Storage.set('edx_video_id', videoId);
|
||||
});
|
||||
|
||||
it('File is not chosen', function() {
|
||||
spyOn($.fn, 'ajaxSubmit');
|
||||
view.upload();
|
||||
@@ -114,6 +115,9 @@ function($, _, Utils, FileUploader) {
|
||||
view.upload();
|
||||
|
||||
expect(view.$form.ajaxSubmit).toHaveBeenCalled();
|
||||
expect(view.$form.ajaxSubmit).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
data: {'edx_video_id': videoId}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,31 +200,25 @@ function($, _, Utils, FileUploader) {
|
||||
status: 200,
|
||||
responseText: JSON.stringify({
|
||||
status: 'Success',
|
||||
subs: 'test'
|
||||
edx_video_id: 'test_video_id'
|
||||
})
|
||||
};
|
||||
spyOn(Utils.Storage, 'set');
|
||||
spyOn(Backbone, 'trigger');
|
||||
view.xhrCompleteHandler(xhr);
|
||||
|
||||
expect(view.$progress).toHaveClass('is-invisible');
|
||||
expect(view.options.messenger.render.calls.mostRecent().args[0])
|
||||
.toEqual('uploaded');
|
||||
expect(Utils.Storage.set)
|
||||
.toHaveBeenCalledWith('sub', 'test');
|
||||
expect(Backbone.trigger)
|
||||
.toHaveBeenCalledWith('transcripts:basicTabUpdateEdxVideoId', 'test_video_id');
|
||||
});
|
||||
|
||||
var assertAjaxError = function(xhr) {
|
||||
spyOn(Utils.Storage, 'set');
|
||||
view.xhrCompleteHandler(xhr);
|
||||
|
||||
expect(view.options.messenger.showError).toHaveBeenCalled();
|
||||
expect(view.$progress).toHaveClass('is-invisible');
|
||||
expect(view.options.messenger.render)
|
||||
.not
|
||||
.toHaveBeenCalled();
|
||||
expect(Utils.Storage.set)
|
||||
.not
|
||||
.toHaveBeenCalledWith('sub', 'test');
|
||||
expect(view.options.messenger.render).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
it('Ajax transport Error', function() {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore',
|
||||
'jquery', 'underscore', 'backbone',
|
||||
'js/views/video/transcripts/utils', 'js/views/video/transcripts/message_manager',
|
||||
'js/views/video/transcripts/file_uploader', 'sinon',
|
||||
'xmodule'
|
||||
],
|
||||
function($, _, Utils, MessageManager, FileUploader, sinon) {
|
||||
function($, _, Backbone, Utils, MessageManager, FileUploader, sinon) {
|
||||
'use strict';
|
||||
|
||||
describe('Transcripts.MessageManager', function() {
|
||||
@@ -61,8 +61,7 @@ function($, _, Utils, MessageManager, FileUploader, sinon) {
|
||||
expect(fileUploader.initialize).toHaveBeenCalledWith({
|
||||
el: view.$el,
|
||||
messenger: view,
|
||||
component_locator: view.component_locator,
|
||||
videoListObject: view.options.parent
|
||||
component_locator: view.component_locator
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,12 +184,15 @@ function($, _, Utils, MessageManager, FileUploader, sinon) {
|
||||
};
|
||||
|
||||
it('Invoke without extraParamas', function(done) {
|
||||
|
||||
spyOn(Backbone, 'trigger');
|
||||
|
||||
sinonXhr.respondWith([
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
status: 'Success',
|
||||
subs: 'video_id'
|
||||
edx_video_id: 'video_id'
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -203,20 +205,23 @@ function($, _, Utils, MessageManager, FileUploader, sinon) {
|
||||
void(0)
|
||||
);
|
||||
expect(view.showError).not.toHaveBeenCalled();
|
||||
expect(view.render.calls.mostRecent().args[0])
|
||||
.toEqual('found');
|
||||
expect(Utils.Storage.set).toHaveBeenCalled();
|
||||
expect(view.render.calls.mostRecent().args[0]).toEqual('found');
|
||||
expect(Backbone.trigger)
|
||||
.toHaveBeenCalledWith('transcripts:basicTabUpdateEdxVideoId', 'video_id');
|
||||
})
|
||||
.always(done);
|
||||
});
|
||||
|
||||
it('Invoke with extraParamas', function(done) {
|
||||
|
||||
spyOn(Backbone, 'trigger');
|
||||
|
||||
sinonXhr.respondWith([
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
status: 'Success',
|
||||
subs: 'video_id'
|
||||
edx_video_id: 'video_id'
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -234,7 +239,8 @@ function($, _, Utils, MessageManager, FileUploader, sinon) {
|
||||
);
|
||||
expect(view.showError).not.toHaveBeenCalled();
|
||||
expect(view.render.calls.mostRecent().args[0]).toEqual('found');
|
||||
expect(Utils.Storage.set).toHaveBeenCalled();
|
||||
expect(Backbone.trigger)
|
||||
.toHaveBeenCalledWith('transcripts:basicTabUpdateEdxVideoId', 'video_id');
|
||||
})
|
||||
.always(done);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore',
|
||||
'jquery', 'underscore', 'backbone',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'js/views/video/transcripts/utils',
|
||||
'js/views/video/transcripts/editor',
|
||||
'js/views/video/transcripts/metadata_videolist', 'js/models/metadata',
|
||||
'js/views/abstract_editor',
|
||||
'js/views/video/transcripts/message_manager',
|
||||
'xmodule'
|
||||
],
|
||||
function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
function($, _, Backbone, AjaxHelpers, Utils, Editor, VideoList, MetadataModel, AbstractEditor, MessageManager) {
|
||||
'use strict';
|
||||
describe('CMS.Views.Metadata.VideoList', function() {
|
||||
var videoListEntryTemplate = readFixtures(
|
||||
@@ -46,12 +48,23 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
'video.webm'
|
||||
]
|
||||
},
|
||||
videoIDStub = {
|
||||
default_value: 'test default value',
|
||||
display_name: 'Video ID',
|
||||
explicitly_set: true,
|
||||
field_name: 'edx_video_id',
|
||||
help: 'Specifies the video ID.',
|
||||
options: [],
|
||||
type: 'VideoID',
|
||||
value: 'advanced tab video id'
|
||||
},
|
||||
response = JSON.stringify({
|
||||
command: 'found',
|
||||
status: 'Success',
|
||||
subs: 'video_id'
|
||||
}),
|
||||
MessageManager, messenger;
|
||||
waitForEvent,
|
||||
createVideoListView;
|
||||
|
||||
|
||||
var createMockAjaxServer = function() {
|
||||
@@ -67,7 +80,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
var tpl = sandbox({
|
||||
var tpl = sandbox({ // eslint-disable-line no-undef
|
||||
class: 'component',
|
||||
'data-locator': component_locator
|
||||
});
|
||||
@@ -86,24 +99,17 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
// create mock server
|
||||
this.mockServer = createMockAjaxServer();
|
||||
|
||||
spyOn($.fn, 'on').and.callThrough();
|
||||
spyOn(Backbone, 'trigger').and.callThrough();
|
||||
spyOn(Utils, 'command').and.callThrough();
|
||||
spyOn(abstractEditor, 'initialize').and.callThrough();
|
||||
spyOn(abstractEditor, 'render').and.callThrough();
|
||||
spyOn(console, 'error');
|
||||
|
||||
messenger = jasmine.createSpyObj('MessageManager', [
|
||||
'initialize', 'render', 'showError', 'hideError'
|
||||
]);
|
||||
|
||||
$.each(messenger, function(index, method) {
|
||||
method.and.returnValue(messenger);
|
||||
});
|
||||
|
||||
MessageManager = function() {
|
||||
messenger.initialize();
|
||||
|
||||
return messenger;
|
||||
};
|
||||
spyOn(MessageManager.prototype, 'initialize').and.callThrough();
|
||||
spyOn(MessageManager.prototype, 'render').and.callThrough();
|
||||
spyOn(MessageManager.prototype, 'showError').and.callThrough();
|
||||
spyOn(MessageManager.prototype, 'hideError').and.callThrough();
|
||||
|
||||
jasmine.addMatchers({
|
||||
assertValueInView: function() {
|
||||
@@ -154,13 +160,49 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
this.mockServer.restore();
|
||||
});
|
||||
|
||||
var createVideoListView = function() {
|
||||
var model = new MetadataModel(modelStub);
|
||||
return new VideoList({
|
||||
waitForEvent = function() {
|
||||
var triggerCallArgs;
|
||||
return jasmine.waitUntil(function() {
|
||||
triggerCallArgs = Backbone.trigger.calls.mostRecent().args;
|
||||
return Backbone.trigger.calls.count() === 1 &&
|
||||
triggerCallArgs[0] === 'transcripts:basicTabFieldChanged';
|
||||
});
|
||||
};
|
||||
|
||||
createVideoListView = function(mockServer) {
|
||||
var $container, editor, model, videoListView;
|
||||
|
||||
appendSetFixtures(
|
||||
sandbox({ // eslint-disable-line no-undef
|
||||
class: 'wrapper-comp-settings basic_metadata_edit',
|
||||
'data-metadata': JSON.stringify({video_url: modelStub, edx_video_id: videoIDStub})
|
||||
})
|
||||
);
|
||||
|
||||
$container = $('.basic_metadata_edit');
|
||||
editor = new Editor({
|
||||
el: $container
|
||||
});
|
||||
|
||||
spyOn(editor, 'getLocator').and.returnValue(component_locator);
|
||||
|
||||
// reset
|
||||
Backbone.trigger.calls.reset();
|
||||
mockServer.requests.length = 0;
|
||||
|
||||
model = new MetadataModel(modelStub);
|
||||
videoListView = new VideoList({
|
||||
el: $('.component'),
|
||||
model: model,
|
||||
MessageManager: MessageManager
|
||||
});
|
||||
|
||||
waitForEvent()
|
||||
.then(function() {
|
||||
return true;
|
||||
});
|
||||
|
||||
return videoListView;
|
||||
};
|
||||
|
||||
var waitsForResponse = function(mockServer) {
|
||||
@@ -174,36 +216,46 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
|
||||
|
||||
it('Initialize', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer), callArgs;
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
expect(abstractEditor.initialize).toHaveBeenCalled();
|
||||
expect(messenger.initialize).toHaveBeenCalled();
|
||||
expect(view.component_locator).toBe(component_locator);
|
||||
expect(view.$el).toHandle('input');
|
||||
}).always(done);
|
||||
.then(function() {
|
||||
expect(abstractEditor.initialize).toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.initialize).toHaveBeenCalled();
|
||||
expect(view.component_locator).toBe(component_locator);
|
||||
expect(view.$el).toHandle('input');
|
||||
callArgs = view.$el.on.calls.mostRecent().args;
|
||||
expect(callArgs[0]).toEqual('input');
|
||||
expect(callArgs[1]).toEqual('.videolist-settings-item input');
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
describe('Render', function() {
|
||||
var assertToHaveBeenRendered = function(videoList) {
|
||||
expect(abstractEditor.render).toHaveBeenCalled();
|
||||
expect(Utils.command).toHaveBeenCalledWith(
|
||||
'check',
|
||||
component_locator,
|
||||
videoList
|
||||
);
|
||||
var assertToHaveBeenRendered = function(expectedVideoList) {
|
||||
var commandCallArgs = Utils.command.calls.mostRecent().args,
|
||||
actualVideoList = commandCallArgs[2].slice(0, expectedVideoList.length);
|
||||
|
||||
expect(messenger.render).toHaveBeenCalled();
|
||||
expect(commandCallArgs[0]).toEqual('check');
|
||||
expect(commandCallArgs[1]).toEqual(component_locator);
|
||||
_.each([0, 1, 2], function(index) {
|
||||
expect(_.isEqual(expectedVideoList[index], actualVideoList[index])).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(abstractEditor.render).toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.render).toHaveBeenCalled();
|
||||
},
|
||||
resetSpies = function(mockServer) {
|
||||
abstractEditor.render.calls.reset();
|
||||
Utils.command.calls.reset();
|
||||
messenger.render.calls.reset();
|
||||
mockServer.requests.length = 0;
|
||||
MessageManager.prototype.render.calls.reset();
|
||||
mockServer.requests.length = 0; // eslint-disable-line no-param-reassign
|
||||
};
|
||||
|
||||
afterEach(function() {
|
||||
Backbone.trigger('xblock:editorModalHidden');
|
||||
});
|
||||
|
||||
it('is rendered in correct way', function(done) {
|
||||
createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
assertToHaveBeenRendered(videoList);
|
||||
@@ -212,7 +264,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('is rendered with opened extra videos bar', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
var videoListLength = [
|
||||
{
|
||||
mode: 'youtube',
|
||||
@@ -233,8 +285,8 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
}
|
||||
];
|
||||
|
||||
spyOn(view, 'getVideoObjectsList').and.returnValue(videoListLength);
|
||||
spyOn(view, 'openExtraVideosBar');
|
||||
spyOn(VideoList.prototype, 'getVideoObjectsList').and.returnValue(videoListLength);
|
||||
spyOn(VideoList.prototype, 'openExtraVideosBar');
|
||||
|
||||
resetSpies(this.mockServer);
|
||||
view.render();
|
||||
@@ -260,7 +312,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('is rendered without opened extra videos bar', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
videoList = [
|
||||
{
|
||||
mode: 'youtube',
|
||||
@@ -269,8 +321,8 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
}
|
||||
];
|
||||
|
||||
spyOn(view, 'getVideoObjectsList').and.returnValue(videoList);
|
||||
spyOn(view, 'closeExtraVideosBar');
|
||||
spyOn(VideoList.prototype, 'getVideoObjectsList').and.returnValue(videoList);
|
||||
spyOn(VideoList.prototype, 'closeExtraVideosBar');
|
||||
|
||||
resetSpies(this.mockServer);
|
||||
view.render();
|
||||
@@ -286,7 +338,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
|
||||
describe('isUniqOtherVideos', function() {
|
||||
it('Unique data - return true', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
data = videoList.concat([{
|
||||
mode: 'html5',
|
||||
type: 'other',
|
||||
@@ -302,7 +354,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('Not Unique data - return false', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
data = [
|
||||
{
|
||||
mode: 'html5',
|
||||
@@ -342,7 +394,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
|
||||
describe('isUniqVideoTypes', function() {
|
||||
it('Unique data - return true', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
data = videoList;
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
@@ -354,7 +406,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('Not Unique data - return false', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
data = [
|
||||
{
|
||||
mode: 'html5',
|
||||
@@ -389,7 +441,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
|
||||
describe('checkIsUniqVideoTypes', function() {
|
||||
it('Error is shown', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
data = [
|
||||
{
|
||||
mode: 'html5',
|
||||
@@ -417,14 +469,14 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
.then(function() {
|
||||
var result = view.checkIsUniqVideoTypes(data);
|
||||
|
||||
expect(messenger.showError).toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.showError).toHaveBeenCalled();
|
||||
expect(result).toBe(false);
|
||||
})
|
||||
.always(done);
|
||||
});
|
||||
|
||||
it('All works okay if arguments are not passed', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
spyOn(view, 'getVideoObjectsList').and.returnValue(videoList);
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
@@ -432,7 +484,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
var result = view.checkIsUniqVideoTypes();
|
||||
|
||||
expect(view.getVideoObjectsList).toHaveBeenCalled();
|
||||
expect(messenger.showError).not.toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.showError).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
})
|
||||
.always(done);
|
||||
@@ -441,7 +493,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
|
||||
describe('checkValidity', function() {
|
||||
it('Error message is shown', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
spyOn(view, 'checkIsUniqVideoTypes').and.returnValue(true);
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
@@ -449,7 +501,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
var data = {mode: 'incorrect'},
|
||||
result = view.checkValidity(data, true);
|
||||
|
||||
expect(messenger.showError).toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.showError).toHaveBeenCalled();
|
||||
expect(view.checkIsUniqVideoTypes).toHaveBeenCalled();
|
||||
expect(result).toBe(false);
|
||||
})
|
||||
@@ -457,7 +509,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('Error message is shown when flag is not passed', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
spyOn(view, 'checkIsUniqVideoTypes').and.returnValue(true);
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
@@ -465,14 +517,14 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
var data = {mode: 'incorrect'},
|
||||
result = view.checkValidity(data);
|
||||
|
||||
expect(messenger.showError).not.toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.showError).not.toHaveBeenCalled();
|
||||
expect(view.checkIsUniqVideoTypes).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('All works okay if correct data is passed', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
spyOn(view, 'checkIsUniqVideoTypes').and.returnValue(true);
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
@@ -480,7 +532,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
var data = videoList,
|
||||
result = view.checkValidity(data);
|
||||
|
||||
expect(messenger.showError).not.toHaveBeenCalled();
|
||||
expect(MessageManager.prototype.showError).not.toHaveBeenCalled();
|
||||
expect(view.checkIsUniqVideoTypes).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
})
|
||||
@@ -489,7 +541,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('openExtraVideosBar', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.$extraVideosBar.removeClass('is-visible');
|
||||
@@ -500,7 +552,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('closeExtraVideosBar', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.$extraVideosBar.addClass('is-visible');
|
||||
@@ -512,7 +564,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('toggleExtraVideosBar', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.$extraVideosBar.addClass('is-visible');
|
||||
@@ -525,7 +577,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('getValueFromEditor', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
expect(view).assertValueInView(modelStub.value);
|
||||
@@ -534,7 +586,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('setValueInEditor', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
expect(view).assertCanUpdateView(['abc.mp4']);
|
||||
@@ -543,7 +595,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
});
|
||||
|
||||
it('getVideoObjectsList', function(done) {
|
||||
var view = createVideoListView();
|
||||
var view = createVideoListView(this.mockServer);
|
||||
var value = [
|
||||
{
|
||||
mode: 'youtube',
|
||||
@@ -577,7 +629,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
|
||||
describe('getPlaceholders', function() {
|
||||
it('All works okay if empty values are passed', function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
defaultPlaceholders = view.placeholders;
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
@@ -593,7 +645,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
it('On filling less than 3 fields, remaining fields should have ' +
|
||||
'placeholders for video types that were not filled yet',
|
||||
function(done) {
|
||||
var view = createVideoListView(),
|
||||
var view = createVideoListView(this.mockServer),
|
||||
defaultPlaceholders = view.placeholders;
|
||||
var dataDict = {
|
||||
youtube: {
|
||||
@@ -640,7 +692,7 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
var eventObject;
|
||||
|
||||
var resetSpies = function(view) {
|
||||
messenger.hideError.calls.reset();
|
||||
MessageManager.prototype.hideError.calls.reset();
|
||||
view.updateModel.calls.reset();
|
||||
view.closeExtraVideosBar.calls.reset();
|
||||
};
|
||||
@@ -660,100 +712,77 @@ function($, _, AjaxHelpers, Utils, VideoList, MetadataModel, AbstractEditor) {
|
||||
resetSpies(view);
|
||||
};
|
||||
|
||||
it('Field has invalid value - nothing should happen',
|
||||
function(done) {
|
||||
var view = createVideoListView();
|
||||
setUp(view);
|
||||
$.fn.hasClass.and.returnValue(false);
|
||||
view.checkValidity.and.returnValue(false);
|
||||
var videoListView = function() {
|
||||
return new VideoList({
|
||||
el: $('.component'),
|
||||
model: new MetadataModel(modelStub),
|
||||
MessageManager: MessageManager
|
||||
});
|
||||
};
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.inputHandler(eventObject);
|
||||
expect(messenger.hideError).not.toHaveBeenCalled();
|
||||
expect(view.updateModel).not.toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith(
|
||||
'disabled', true
|
||||
);
|
||||
expect($.fn.addClass).toHaveBeenCalledWith(
|
||||
'is-disabled'
|
||||
);
|
||||
})
|
||||
.always(done);
|
||||
}
|
||||
);
|
||||
beforeEach(function() {
|
||||
MessageManager.prototype.render.and.callFake(function() { return true; });
|
||||
});
|
||||
|
||||
it('Main field has invalid value - extra Videos Bar is closed',
|
||||
function(done) {
|
||||
var view = createVideoListView();
|
||||
setUp(view);
|
||||
$.fn.hasClass.and.returnValue(true);
|
||||
view.checkValidity.and.returnValue(false);
|
||||
afterEach(function() {
|
||||
MessageManager.prototype.render.and.callThrough();
|
||||
});
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.inputHandler(eventObject);
|
||||
expect(messenger.hideError).not.toHaveBeenCalled();
|
||||
expect(view.updateModel).not.toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith(
|
||||
'disabled', true
|
||||
);
|
||||
expect($.fn.addClass).toHaveBeenCalledWith(
|
||||
'is-disabled'
|
||||
);
|
||||
})
|
||||
.always(done);
|
||||
}
|
||||
);
|
||||
it('Field has invalid value - nothing should happen', function() {
|
||||
var view = videoListView();
|
||||
setUp(view);
|
||||
$.fn.hasClass.and.returnValue(false);
|
||||
view.checkValidity.and.returnValue(false);
|
||||
|
||||
it('Model is updated if value is valid',
|
||||
function(done) {
|
||||
var view = createVideoListView();
|
||||
setUp(view);
|
||||
view.checkValidity.and.returnValue(true);
|
||||
_.isEqual.and.returnValue(false);
|
||||
view.inputHandler(eventObject);
|
||||
expect(MessageManager.prototype.hideError).not.toHaveBeenCalled();
|
||||
expect(view.updateModel).not.toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith('disabled', true);
|
||||
expect($.fn.addClass).toHaveBeenCalledWith('is-disabled');
|
||||
});
|
||||
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.inputHandler(eventObject);
|
||||
expect(messenger.hideError).not.toHaveBeenCalled();
|
||||
expect(view.updateModel).toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith(
|
||||
'disabled', false
|
||||
);
|
||||
expect($.fn.removeClass).toHaveBeenCalledWith(
|
||||
'is-disabled'
|
||||
);
|
||||
})
|
||||
.always(done);
|
||||
}
|
||||
);
|
||||
it('Main field has invalid value - extra Videos Bar is closed', function() {
|
||||
var view = videoListView();
|
||||
setUp(view);
|
||||
$.fn.hasClass.and.returnValue(true);
|
||||
view.checkValidity.and.returnValue(false);
|
||||
|
||||
it('Corner case: Error is hided',
|
||||
function(done) {
|
||||
var view = createVideoListView();
|
||||
setUp(view);
|
||||
view.checkValidity.and.returnValue(true);
|
||||
_.isEqual.and.returnValue(true);
|
||||
waitsForResponse(this.mockServer)
|
||||
.then(function() {
|
||||
view.inputHandler(eventObject);
|
||||
expect(messenger.hideError).toHaveBeenCalled();
|
||||
expect(view.updateModel).not.toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith(
|
||||
'disabled', false
|
||||
);
|
||||
expect($.fn.removeClass).toHaveBeenCalledWith(
|
||||
'is-disabled'
|
||||
);
|
||||
})
|
||||
.always(done);
|
||||
}
|
||||
);
|
||||
view.inputHandler(eventObject);
|
||||
expect(MessageManager.prototype.hideError).not.toHaveBeenCalled();
|
||||
expect(view.updateModel).not.toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith('disabled', true);
|
||||
expect($.fn.addClass).toHaveBeenCalledWith('is-disabled');
|
||||
});
|
||||
|
||||
it('Model is updated if value is valid', function() {
|
||||
var view = videoListView();
|
||||
setUp(view);
|
||||
view.checkValidity.and.returnValue(true);
|
||||
_.isEqual.and.returnValue(false);
|
||||
|
||||
view.inputHandler(eventObject);
|
||||
expect(MessageManager.prototype.hideError).not.toHaveBeenCalled();
|
||||
expect(view.updateModel).toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith('disabled', false);
|
||||
expect($.fn.removeClass).toHaveBeenCalledWith('is-disabled');
|
||||
});
|
||||
|
||||
it('Corner case: Error is hided', function() {
|
||||
var view = videoListView();
|
||||
setUp(view);
|
||||
view.checkValidity.and.returnValue(true);
|
||||
_.isEqual.and.returnValue(true);
|
||||
|
||||
view.inputHandler(eventObject);
|
||||
expect(MessageManager.prototype.hideError).toHaveBeenCalled();
|
||||
expect(view.updateModel).not.toHaveBeenCalled();
|
||||
expect(view.closeExtraVideosBar).not.toHaveBeenCalled();
|
||||
expect($.fn.prop).toHaveBeenCalledWith('disabled', false);
|
||||
expect($.fn.removeClass).toHaveBeenCalledWith('is-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
define(["js/models/metadata", "js/collections/metadata", "js/views/metadata", "cms/js/main"],
|
||||
function(MetadataModel, MetadataCollection, MetadataView, main) {
|
||||
define(["underscore", "js/models/metadata", "js/collections/metadata", "js/views/metadata", "cms/js/main",
|
||||
"js/views/video/transcripts/utils", 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'],
|
||||
function(_, MetadataModel, MetadataCollection, MetadataView, main, TranscriptUtils, AjaxHelpers) {
|
||||
const verifyInputType = function(input, expectedType) {
|
||||
// Some browsers (e.g. FireFox) do not support the "number"
|
||||
// input type. We can accept a "text" input instead
|
||||
@@ -43,6 +44,8 @@ function(MetadataModel, MetadataCollection, MetadataView, main) {
|
||||
value: "Word cloud"
|
||||
};
|
||||
|
||||
const videoIDEntry = _.extend({}, genericEntry, {field_name: "edx_video_id", type: "VideoID"});
|
||||
|
||||
const selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
@@ -271,6 +274,51 @@ function(MetadataModel, MetadataCollection, MetadataView, main) {
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.VideoID", function() {
|
||||
var waitForMock;
|
||||
|
||||
waitForMock = function(mock) {
|
||||
return jasmine.waitUntil(function() {
|
||||
return mock.calls.count() === 1;
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
const model = new MetadataModel(videoIDEntry);
|
||||
spyOn(TranscriptUtils.Storage, 'set');
|
||||
this.view = new MetadataView.VideoID({model});
|
||||
spyOn(Backbone, 'trigger');
|
||||
expect(TranscriptUtils.Storage.set).toHaveBeenCalledWith('edx_video_id', this.view.getValueFromEditor());
|
||||
});
|
||||
|
||||
it("triggers correct event on input change", function(done) {
|
||||
// change value and trigger input event
|
||||
this.view.$el.find('input').val("1234-5678-90").trigger('input');
|
||||
waitForMock(Backbone.trigger)
|
||||
.then(function() {
|
||||
expect(Backbone.trigger).toHaveBeenCalledWith('transcripts:basicTabFieldChanged');
|
||||
})
|
||||
.always(done);
|
||||
});
|
||||
|
||||
it("triggers correct event on clear", function(done) {
|
||||
this.view.clear();
|
||||
waitForMock(Backbone.trigger)
|
||||
.then(function() {
|
||||
expect(Backbone.trigger).toHaveBeenCalledWith('transcripts:basicTabFieldChanged');
|
||||
})
|
||||
.always(done);
|
||||
});
|
||||
|
||||
it("constructs correct data", function() {
|
||||
expect(
|
||||
this.view.getData()
|
||||
).toEqual(
|
||||
[{mode: 'edx_video_id', type: 'edx_video_id', video: this.view.getValueFromEditor()}]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.Option is an option input type with clear functionality", function() {
|
||||
beforeEach(function() {
|
||||
const model = new MetadataModel(selectEntry);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers',
|
||||
'js/views/modals/edit_xblock', 'js/models/xblock_info'],
|
||||
function($, _, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) {
|
||||
define(['jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'js/spec_helpers/edit_helpers', 'js/views/modals/edit_xblock', 'js/models/xblock_info'],
|
||||
function($, _, Backbone, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) {
|
||||
'use strict';
|
||||
describe('EditXBlockModal', function() {
|
||||
var model, modal, showModal;
|
||||
|
||||
@@ -30,6 +31,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
|
||||
beforeEach(function() {
|
||||
EditHelpers.installMockXBlock();
|
||||
spyOn(Backbone, 'trigger').and.callThrough();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -74,6 +76,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
modal.editorView.notifyRuntime('save', {state: 'end'});
|
||||
expect(EditHelpers.isShowingModal(modal)).toBeFalsy();
|
||||
expect(refreshed).toBeTruthy();
|
||||
expect(Backbone.trigger).toHaveBeenCalledWith('xblock:editorModalHidden');
|
||||
});
|
||||
|
||||
it('hides itself and does not refresh after cancel notification', function() {
|
||||
@@ -86,6 +89,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
modal.editorView.notifyRuntime('cancel');
|
||||
expect(EditHelpers.isShowingModal(modal)).toBeFalsy();
|
||||
expect(refreshed).toBeFalsy();
|
||||
expect(Backbone.trigger).toHaveBeenCalledWith('xblock:editorModalHidden');
|
||||
});
|
||||
|
||||
describe('Custom Buttons', function() {
|
||||
|
||||
@@ -11,7 +11,8 @@ define(
|
||||
duration: 42,
|
||||
created: '2014-11-25T23:13:05',
|
||||
edx_video_id: 'dummy_id',
|
||||
status: 'uploading'
|
||||
status: 'uploading',
|
||||
transcripts: []
|
||||
};
|
||||
var collection = new Backbone.Collection(
|
||||
_.map(
|
||||
@@ -26,6 +27,9 @@ define(
|
||||
var view = new PreviousVideoUploadListView({
|
||||
collection: collection,
|
||||
videoHandlerUrl: videoHandlerUrl,
|
||||
transcriptAvailableLanguages: [],
|
||||
videoSupportedFileFormats: [],
|
||||
videoTranscriptSettings: {},
|
||||
videoImageSettings: {}
|
||||
});
|
||||
return view.render().$el;
|
||||
|
||||
@@ -10,11 +10,15 @@ define(
|
||||
duration: 42,
|
||||
created: '2014-11-25T23:13:05',
|
||||
edx_video_id: 'dummy_id',
|
||||
status: 'uploading'
|
||||
status: 'uploading',
|
||||
transcripts: []
|
||||
},
|
||||
view = new PreviousVideoUploadView({
|
||||
model: new Backbone.Model($.extend({}, defaultData, modelData)),
|
||||
videoHandlerUrl: '/videos/course-v1:org.0+course_0+Run_0',
|
||||
transcriptAvailableLanguages: [],
|
||||
videoSupportedFileFormats: [],
|
||||
videoTranscriptSettings: {},
|
||||
videoImageSettings: {}
|
||||
});
|
||||
return view.render().$el;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
define(["sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
define(["underscore", "sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/modal_helpers"],
|
||||
(sinon, FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) =>
|
||||
(_, sinon, FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) =>
|
||||
|
||||
describe("UploadDialog", function() {
|
||||
const tpl = readFixtures("upload-dialog.underscore");
|
||||
const tpl = readFixtures("upload-dialog.underscore"),
|
||||
uploadData = {
|
||||
edx_video_id: '123-456-789-0',
|
||||
language_code: 'en',
|
||||
new_language_code: 'ur'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
let dialogResponse;
|
||||
@@ -27,7 +32,8 @@ define(["sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
onSuccess: response => {
|
||||
return test.dialogResponse.push(response.response);
|
||||
}
|
||||
},
|
||||
uploadData: uploadData
|
||||
});
|
||||
spyOn(view, 'remove').and.callThrough();
|
||||
|
||||
@@ -37,6 +43,7 @@ define(["sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
const jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith']);
|
||||
jqMockFileInput.get.and.returnValue(mockFileInput);
|
||||
const originalView$ = view.$;
|
||||
spyOn($.fn, 'ajaxSubmit').and.callThrough();
|
||||
spyOn(view, "$").and.callFake(function(selector) {
|
||||
if (selector === "input[type=file]") {
|
||||
return jqMockFileInput;
|
||||
@@ -126,6 +133,9 @@ define(["sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
view.upload();
|
||||
expect(this.model.get("uploading")).toBeTruthy();
|
||||
AjaxHelpers.expectRequest(requests, "POST", "/upload");
|
||||
expect($.fn.ajaxSubmit.calls.mostRecent().args[0].data).toEqual(
|
||||
_.extend({}, uploadData, {notifyOnError: false})
|
||||
);
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"});
|
||||
expect(this.model.get("uploading")).toBeFalsy();
|
||||
expect(this.model.get("finished")).toBeTruthy();
|
||||
|
||||
@@ -43,7 +43,8 @@ define(
|
||||
duration: 42,
|
||||
created: '2014-11-25T23:13:05',
|
||||
edx_video_id: 'dummy_id',
|
||||
status: 'uploading'
|
||||
status: 'uploading',
|
||||
transcripts: []
|
||||
},
|
||||
collection = new Backbone.Collection(_.map(_.range(numVideos), function(num, index) {
|
||||
return new Backbone.Model(
|
||||
@@ -61,7 +62,10 @@ define(
|
||||
max_height: VIDEO_IMAGE_MAX_HEIGHT,
|
||||
supported_file_formats: VIDEO_IMAGE_SUPPORTED_FILE_FORMATS,
|
||||
video_image_upload_enabled: videoImageUploadEnabled
|
||||
}
|
||||
},
|
||||
transcriptAvailableLanguages: [],
|
||||
videoSupportedFileFormats: [],
|
||||
videoTranscriptSettings: {}
|
||||
});
|
||||
$videoListEl = videoListView.render().$el;
|
||||
|
||||
|
||||
@@ -93,9 +93,8 @@ define(
|
||||
return new File([new Blob([Array(size).join('i')], {type: type})], transcriptFileName);
|
||||
};
|
||||
|
||||
renderView = function(availableTranscripts, isVideoTranscriptEnabled) {
|
||||
renderView = function(availableTranscripts) {
|
||||
var videoViewIndex = 0,
|
||||
isVideoTranscriptEnabled = isVideoTranscriptEnabled || _.isUndefined(isVideoTranscriptEnabled), // eslint-disable-line max-len, no-redeclare
|
||||
videoData = {
|
||||
client_video_id: clientVideoID,
|
||||
edx_video_id: edxVideoID,
|
||||
@@ -109,8 +108,7 @@ define(
|
||||
videoImageSettings: {},
|
||||
videoTranscriptSettings: videoTranscriptSettings,
|
||||
transcriptAvailableLanguages: transcriptAvailableLanguages,
|
||||
videoSupportedFileFormats: videoSupportedFileFormats,
|
||||
isVideoTranscriptEnabled: isVideoTranscriptEnabled
|
||||
videoSupportedFileFormats: videoSupportedFileFormats
|
||||
});
|
||||
videoListView.setElement($('.wrapper-assets'));
|
||||
videoListView.render();
|
||||
@@ -139,18 +137,6 @@ define(
|
||||
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('.video-transcripts-header')).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('.video-transcripts-wrapper').hasClass('hidden')
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
define(
|
||||
[
|
||||
'backbone',
|
||||
'js/views/baseview', 'underscore', 'js/models/metadata', 'js/views/abstract_editor',
|
||||
'js/models/uploads', 'js/views/uploads',
|
||||
'js/models/license', 'js/views/license',
|
||||
'js/views/video/transcripts/utils',
|
||||
'js/views/video/transcripts/metadata_videolist',
|
||||
'js/views/video/translations_editor'
|
||||
],
|
||||
function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
LicenseModel, LicenseView, VideoList, VideoTranslations) {
|
||||
function(Backbone, BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
LicenseModel, LicenseView, TranscriptUtils, VideoList, VideoTranslations) {
|
||||
'use strict';
|
||||
var Metadata = {};
|
||||
|
||||
Metadata.Editor = BaseView.extend({
|
||||
// Store rendered view references
|
||||
views: {},
|
||||
|
||||
// Model is CMS.Models.MetadataCollection,
|
||||
initialize: function() {
|
||||
@@ -42,10 +47,10 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
}
|
||||
|
||||
if (_.isFunction(Metadata[type])) {
|
||||
new Metadata[type](data);
|
||||
self.views[data.model.getFieldName()] = new Metadata[type](data);
|
||||
} else {
|
||||
// Everything else is treated as GENERIC_TYPE, which uses String editor.
|
||||
new Metadata.String(data);
|
||||
self.views[data.model.getFieldName()] = new Metadata.String(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -120,6 +125,40 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.VideoID = Metadata.String.extend({
|
||||
// Delay between check_transcript requests
|
||||
requestDelay: 300,
|
||||
|
||||
initialize: function() {
|
||||
Metadata.String.prototype.initialize.apply(this, arguments);
|
||||
|
||||
this.$el.on(
|
||||
'input',
|
||||
'input',
|
||||
_.debounce(_.bind(this.inputChange, this), this.requestDelay)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
Metadata.String.prototype.render.apply(this, arguments);
|
||||
TranscriptUtils.Storage.set('edx_video_id', this.getValueFromEditor());
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.model.setValue('');
|
||||
this.inputChange();
|
||||
},
|
||||
|
||||
getData: function() {
|
||||
return [{mode: 'edx_video_id', type: 'edx_video_id', video: this.getValueFromEditor()}];
|
||||
},
|
||||
|
||||
inputChange: function() {
|
||||
TranscriptUtils.Storage.set('edx_video_id', this.getValueFromEditor());
|
||||
Backbone.trigger('transcripts:basicTabFieldChanged');
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.Number = AbstractEditor.extend({
|
||||
|
||||
events: {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* It is invoked using the edit method which is passed an existing rendered xblock,
|
||||
* and upon save an optional refresh function can be invoked to update the display.
|
||||
*/
|
||||
define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal', 'common/js/components/utils/view_utils',
|
||||
'js/views/utils/xblock_utils', 'js/views/xblock_editor'],
|
||||
function($, _, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockEditorView) {
|
||||
define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_modal',
|
||||
'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils', 'js/views/xblock_editor'],
|
||||
function($, _, Backbone, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockEditorView) {
|
||||
'use strict';
|
||||
|
||||
var EditXBlockModal = BaseModal.extend({
|
||||
@@ -181,6 +181,9 @@ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal', 'common
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
// Notify child views to stop listening events
|
||||
Backbone.trigger('xblock:editorModalHidden');
|
||||
|
||||
BaseModal.prototype.hide.call(this);
|
||||
|
||||
// Notify the runtime that the modal has been hidden
|
||||
|
||||
@@ -20,7 +20,6 @@ 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({
|
||||
@@ -30,22 +29,19 @@ 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
|
||||
});
|
||||
}
|
||||
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')
|
||||
};
|
||||
@@ -59,9 +55,7 @@ define(
|
||||
if (this.videoImageUploadEnabled) {
|
||||
this.videoThumbnailView.setElement(this.$('.thumbnail-col')).render();
|
||||
}
|
||||
if (this.isVideoTranscriptEnabled) {
|
||||
this.videoTranscriptsView.setElement(this.$('.transcripts-col')).render();
|
||||
}
|
||||
this.videoTranscriptsView.setElement(this.$('.transcripts-col')).render();
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ define(
|
||||
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,
|
||||
@@ -21,8 +20,7 @@ define(
|
||||
videoTranscriptSettings: options.videoTranscriptSettings,
|
||||
model: model,
|
||||
transcriptAvailableLanguages: options.transcriptAvailableLanguages,
|
||||
videoSupportedFileFormats: options.videoSupportedFileFormats,
|
||||
isVideoTranscriptEnabled: options.isVideoTranscriptEnabled
|
||||
videoSupportedFileFormats: options.videoSupportedFileFormats
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -35,8 +33,7 @@ define(
|
||||
this.$el,
|
||||
this.template({
|
||||
encodingsDownloadUrl: this.encodingsDownloadUrl,
|
||||
videoImageUploadEnabled: this.videoImageUploadEnabled,
|
||||
isVideoTranscriptEnabled: this.isVideoTranscriptEnabled
|
||||
videoImageUploadEnabled: this.videoImageUploadEnabled
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -13,11 +13,14 @@ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal', 'jquery
|
||||
viewSpecificClasses: 'confirm'
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
initialize: function(options) {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.template = this.loadTemplate('upload-dialog');
|
||||
this.listenTo(this.model, 'change', this.renderContents);
|
||||
this.options.title = this.model.get('title');
|
||||
// `uploadData` can contain extra data that
|
||||
// can be POSTed along with the file.
|
||||
this.uploadData = _.extend({}, options.uploadData);
|
||||
},
|
||||
|
||||
addActionButtons: function() {
|
||||
@@ -73,17 +76,19 @@ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal', 'jquery
|
||||
},
|
||||
|
||||
upload: function(e) {
|
||||
|
||||
var uploadAjaxData = _.extend({}, this.uploadData);
|
||||
// don't show the generic error notification; we're in a modal,
|
||||
// and we're better off modifying it instead.
|
||||
uploadAjaxData.notifyOnError = false;
|
||||
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('uploading', true);
|
||||
this.$('form').ajaxSubmit({
|
||||
success: _.bind(this.success, this),
|
||||
error: _.bind(this.error, this),
|
||||
uploadProgress: _.bind(this.progress, this),
|
||||
data: {
|
||||
// don't show the generic error notification; we're in a modal,
|
||||
// and we're better off modifying it instead.
|
||||
notifyOnError: false
|
||||
}
|
||||
data: uploadAjaxData
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
|
||||
initialize: function() {
|
||||
// prepare data for MetadataView.Editor
|
||||
|
||||
var metadata = this.$el.data('metadata'),
|
||||
models = this.toModels(metadata);
|
||||
|
||||
@@ -23,6 +22,23 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
el: this.$el,
|
||||
collection: this.collection
|
||||
});
|
||||
|
||||
// Listen to edx_video_id update
|
||||
this.listenTo(Backbone, 'transcripts:basicTabUpdateEdxVideoId', this.handleUpdateEdxVideoId);
|
||||
|
||||
// Listen to `video_url` and `edx_video_id` updates
|
||||
this.listenTo(Backbone, 'transcripts:basicTabFieldChanged', this.handleFieldChanged);
|
||||
|
||||
// Listen to modal hidden event
|
||||
this.listenTo(Backbone, 'xblock:editorModalHidden', this.destroy);
|
||||
|
||||
// Now `video_url` and `edx_video_id` viwes are rendered so
|
||||
// send a `check_transcript` request to get transctip status
|
||||
// This is needed because we need to update the transcrript status
|
||||
// when basic tabs renders. We trigger `basicTabFieldChanged` event
|
||||
// in `video_url` field but that event triggers before event is
|
||||
// actually binded
|
||||
this.handleFieldChanged();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -72,7 +88,6 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
var result = [],
|
||||
getField = Utils.getField,
|
||||
component_locator = this.$el.closest('[data-locator]').data('locator'),
|
||||
subs = getField(metadataCollection, 'sub'),
|
||||
values = {},
|
||||
videoUrl, metadata, modifiedValues;
|
||||
|
||||
@@ -86,37 +101,6 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
|
||||
modifiedValues = metadataView.getModifiedMetadataValues();
|
||||
|
||||
var isSubsModified = (function(values) {
|
||||
var isSubsChanged = subs.hasChanged('value');
|
||||
|
||||
return Boolean(
|
||||
isSubsChanged &&
|
||||
(
|
||||
// If the user changes the field, `values.sub` contains
|
||||
// string value;
|
||||
// If the user clicks `clear` button, the field contains
|
||||
// null value.
|
||||
// Otherwise, undefined.
|
||||
_.isString(values.sub) || _.isNull(subs.getValue())
|
||||
)
|
||||
);
|
||||
}(modifiedValues));
|
||||
|
||||
// When we change value of `sub` field in the `Advanced`,
|
||||
// we update data on backend. That provides possibility to remove
|
||||
// transcripts.
|
||||
if (isSubsModified) {
|
||||
metadata = $.extend(true, {}, modifiedValues);
|
||||
// Save module state
|
||||
Utils.command('save', component_locator, null, {
|
||||
metadata: metadata,
|
||||
current_subs: _.pluck(
|
||||
Utils.getVideoList(videoUrl.getDisplayValue()),
|
||||
'video'
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Get values from `Advanced` tab fields (`html5_sources`,
|
||||
// `youtube_id_1_0`) that should be synchronized.
|
||||
var html5Sources = getField(metadataCollection, 'html5_sources').getDisplayValue();
|
||||
@@ -148,17 +132,6 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
|
||||
// Synchronize other fields that has the same `field_name` property.
|
||||
Utils.syncCollections(metadataCollection, this.collection);
|
||||
|
||||
if (isSubsModified) {
|
||||
// When `sub` field is changed, clean Storage to avoid overwriting.
|
||||
Utils.Storage.remove('sub');
|
||||
|
||||
// Trigger `change` event manually if `video_url` model
|
||||
// isn't changed.
|
||||
if (!videoUrl.hasChanged()) {
|
||||
videoUrl.trigger('change');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -174,8 +147,6 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
*/
|
||||
syncAdvancedTab: function(metadataCollection, metadataView) {
|
||||
var getField = Utils.getField,
|
||||
subsValue = Utils.Storage.get('sub'),
|
||||
subs = getField(metadataCollection, 'sub'),
|
||||
html5Sources, youtube, videoUrlValue, result;
|
||||
|
||||
// if metadataCollection is not passed, just exit.
|
||||
@@ -231,18 +202,45 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
youtube.setValue(result);
|
||||
}
|
||||
|
||||
// If Utils.Storage contain some subtitles, update them.
|
||||
if (_.isString(subsValue)) {
|
||||
subs.setValue(subsValue);
|
||||
// After updating should be removed, because it might overwrite
|
||||
// subtitles added by user manually.
|
||||
Utils.Storage.remove('sub');
|
||||
}
|
||||
|
||||
// Synchronize other fields that has the same `field_name` property.
|
||||
Utils.syncCollections(this.collection, metadataCollection);
|
||||
}
|
||||
},
|
||||
|
||||
handleUpdateEdxVideoId: function(edxVideoId) {
|
||||
var edxVideoIdField = Utils.getField(this.collection, 'edx_video_id');
|
||||
edxVideoIdField.setValue(edxVideoId);
|
||||
},
|
||||
|
||||
getLocator: function() {
|
||||
return this.$el.closest('[data-locator]').data('locator');
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler for `transcripts:basicTabFieldChanged` event.
|
||||
*/
|
||||
handleFieldChanged: function() {
|
||||
var views = this.settingsView.views,
|
||||
videoURLSView = views.video_url,
|
||||
edxVideoIdView = views.edx_video_id,
|
||||
edxVideoIdData = edxVideoIdView.getData(),
|
||||
videoURLsData = videoURLSView.getVideoObjectsList(),
|
||||
data = videoURLsData.concat(edxVideoIdData),
|
||||
locator = this.getLocator();
|
||||
|
||||
Utils.command('check', locator, data)
|
||||
.done(function(response) {
|
||||
videoURLSView.updateOnCheckTranscriptSuccess(videoURLsData, response);
|
||||
})
|
||||
.fail(function(response) {
|
||||
videoURLSView.showServerError(response);
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.stopListening();
|
||||
this.undelegateEvents();
|
||||
this.$el.empty();
|
||||
}
|
||||
});
|
||||
|
||||
return Editor;
|
||||
|
||||
@@ -3,7 +3,7 @@ define(
|
||||
'jquery', 'backbone', 'underscore',
|
||||
'js/views/video/transcripts/utils'
|
||||
],
|
||||
function($, Backbone, _, Utils) {
|
||||
function($, Backbone, _, TranscriptUtils) {
|
||||
var FileUploader = Backbone.View.extend({
|
||||
invisibleClass: 'is-invisible',
|
||||
|
||||
@@ -29,8 +29,7 @@ function($, Backbone, _, Utils) {
|
||||
|
||||
render: function() {
|
||||
var tpl = $(this.uploadTpl).text(),
|
||||
tplContainer = this.$el.find('.transcripts-file-uploader'),
|
||||
videoList = this.options.videoListObject.getVideoObjectsList();
|
||||
tplContainer = this.$el.find('.transcripts-file-uploader');
|
||||
|
||||
if (tplContainer.length) {
|
||||
if (!tpl) {
|
||||
@@ -42,8 +41,7 @@ function($, Backbone, _, Utils) {
|
||||
|
||||
tplContainer.html(this.template({
|
||||
ext: this.validFileExtensions,
|
||||
component_locator: this.options.component_locator,
|
||||
video_list: videoList
|
||||
component_locator: this.options.component_locator
|
||||
}));
|
||||
|
||||
this.$form = this.$el.find('.file-chooser');
|
||||
@@ -59,6 +57,10 @@ function($, Backbone, _, Utils) {
|
||||
*
|
||||
*/
|
||||
upload: function() {
|
||||
var data = {
|
||||
'edx_video_id': TranscriptUtils.Storage.get('edx_video_id') || ''
|
||||
};
|
||||
|
||||
if (!this.file) {
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +68,8 @@ function($, Backbone, _, Utils) {
|
||||
this.$form.ajaxSubmit({
|
||||
beforeSend: this.xhrResetProgressBar,
|
||||
uploadProgress: this.xhrProgressHandler,
|
||||
complete: this.xhrCompleteHandler
|
||||
complete: this.xhrCompleteHandler,
|
||||
data: data
|
||||
});
|
||||
},
|
||||
|
||||
@@ -186,14 +189,14 @@ function($, Backbone, _, Utils) {
|
||||
xhrCompleteHandler: function(xhr) {
|
||||
var resp = JSON.parse(xhr.responseText),
|
||||
err = resp.status || gettext('Error: Uploading failed.'),
|
||||
sub = resp.subs;
|
||||
edxVideoId = resp.edx_video_id;
|
||||
|
||||
this.$progress
|
||||
.addClass(this.invisibleClass);
|
||||
|
||||
if (xhr.status === 200) {
|
||||
this.options.messenger.render('uploaded', resp);
|
||||
Utils.Storage.set('sub', sub);
|
||||
Backbone.trigger('transcripts:basicTabUpdateEdxVideoId', edxVideoId);
|
||||
} else {
|
||||
this.options.messenger.showError(err);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
|
||||
this.fileUploader = new FileUploader({
|
||||
el: this.$el,
|
||||
messenger: this,
|
||||
component_locator: this.component_locator,
|
||||
videoListObject: this.options.parent
|
||||
component_locator: this.component_locator
|
||||
});
|
||||
},
|
||||
|
||||
@@ -218,10 +217,10 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
|
||||
|
||||
xhr = Utils.command(action, component_locator, videoList, extraParam)
|
||||
.done(function(resp) {
|
||||
var sub = resp.subs;
|
||||
var edxVideoID = resp.edx_video_id;
|
||||
|
||||
self.render('found', resp);
|
||||
Utils.Storage.set('sub', sub);
|
||||
Backbone.trigger('transcripts:basicTabUpdateEdxVideoId', edxVideoID);
|
||||
})
|
||||
.fail(function(resp) {
|
||||
var message = resp.status || errorMessage;
|
||||
|
||||
@@ -43,7 +43,7 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager) {
|
||||
.apply(this, arguments);
|
||||
|
||||
this.$el.on(
|
||||
'input', 'input',
|
||||
'input', '.videolist-settings-item input',
|
||||
_.debounce(_.bind(this.inputHandler, this), this.inputDelay)
|
||||
);
|
||||
|
||||
@@ -56,57 +56,45 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager) {
|
||||
AbstractEditor.prototype.render
|
||||
.apply(this, arguments);
|
||||
|
||||
var self = this,
|
||||
component_locator = this.$el.closest('[data-locator]')
|
||||
.data('locator'),
|
||||
videoList = this.getVideoObjectsList(),
|
||||
|
||||
showServerError = function(response) {
|
||||
var errorMessage = response.status ||
|
||||
gettext('Error: Connection with server failed.');
|
||||
|
||||
self.messenger
|
||||
.render('not_found')
|
||||
.showError(
|
||||
errorMessage,
|
||||
true // hide buttons
|
||||
);
|
||||
};
|
||||
|
||||
this.$extraVideosBar = this.$el.find('.videolist-extra-videos');
|
||||
|
||||
if (videoList.length === 0) {
|
||||
this.messenger
|
||||
.render('not_found')
|
||||
.showError(
|
||||
gettext('No sources'),
|
||||
true // hide buttons
|
||||
);
|
||||
// Check current state of Timed Transcripts.
|
||||
Backbone.trigger('transcripts:basicTabFieldChanged');
|
||||
},
|
||||
|
||||
return void(0);
|
||||
updateOnCheckTranscriptSuccess: function(videoList, response) {
|
||||
var params = response,
|
||||
len = videoList.length,
|
||||
mode = (len === 1) ? videoList[0].mode : false;
|
||||
|
||||
// If there are more than 1 video or just html5 source is
|
||||
// passed, video sources box should expand
|
||||
if (len > 1 || mode === 'html5') {
|
||||
this.openExtraVideosBar();
|
||||
} else {
|
||||
this.closeExtraVideosBar();
|
||||
}
|
||||
|
||||
// Check current state of Timed Transcripts.
|
||||
Utils.command('check', component_locator, videoList)
|
||||
.done(function(resp) {
|
||||
var params = resp,
|
||||
len = videoList.length,
|
||||
mode = (len === 1) ? videoList[0].mode : false;
|
||||
this.messenger.render(response.command, params);
|
||||
this.checkIsUniqVideoTypes();
|
||||
},
|
||||
|
||||
// If there are more than 1 video or just html5 source is
|
||||
// passed, video sources box should expand
|
||||
if (len > 1 || mode === 'html5') {
|
||||
self.openExtraVideosBar();
|
||||
} else {
|
||||
self.closeExtraVideosBar();
|
||||
}
|
||||
/**
|
||||
* Updates the message with error.
|
||||
*/
|
||||
showServerError: function(response) {
|
||||
var errorMessage = gettext('Error: Connection with server failed.');
|
||||
|
||||
self.messenger.render(resp.command, params);
|
||||
self.checkIsUniqVideoTypes();
|
||||
// Synchronize transcripts field in the `Advanced` tab.
|
||||
Utils.Storage.set('sub', resp.subs);
|
||||
})
|
||||
.fail(showServerError);
|
||||
if (response.responseJSON !== undefined) {
|
||||
errorMessage = response.responseJSON.status;
|
||||
}
|
||||
|
||||
this.messenger
|
||||
.render('not_found')
|
||||
.showError(
|
||||
errorMessage,
|
||||
true // hide buttons
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore',
|
||||
'js/views/abstract_editor', 'js/models/uploads', 'js/views/uploads'
|
||||
|
||||
'jquery', 'underscore', 'edx-ui-toolkit/js/utils/html-utils', 'js/views/video/transcripts/utils',
|
||||
'js/views/abstract_editor', 'common/js/components/utils/view_utils', 'js/models/uploads', 'js/views/uploads'
|
||||
],
|
||||
function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, ViewUtils, FileUpload, UploadDialog) {
|
||||
'use strict';
|
||||
|
||||
var VideoUploadDialog = UploadDialog.extend({
|
||||
@@ -19,7 +18,6 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
|
||||
var Translations = AbstractEditor.extend({
|
||||
events: {
|
||||
'click .setting-clear': 'clear',
|
||||
'click .create-setting': 'addEntry',
|
||||
'click .remove-setting': 'removeEntry',
|
||||
'click .upload-setting': 'upload',
|
||||
@@ -29,15 +27,29 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
templateName: 'metadata-translations-entry',
|
||||
templateItemName: 'metadata-translations-item',
|
||||
|
||||
validFileFormats: ['srt'],
|
||||
|
||||
initialize: function() {
|
||||
var templateName = _.result(this, 'templateItemName'),
|
||||
tpl = document.getElementById(templateName).text;
|
||||
tpl = document.getElementById(templateName).text,
|
||||
languageMap = {};
|
||||
|
||||
if (!tpl) {
|
||||
console.error("Couldn't load template for item: " + templateName);
|
||||
}
|
||||
|
||||
this.templateItem = _.template(tpl);
|
||||
|
||||
// Initialize language map. This maps original language to the newly selected language.
|
||||
// Keys in this map represent language codes present on server, they don't change when
|
||||
// user selects a language while values represent currently selected language.
|
||||
// Initially, the map will look like {'ar': 'ar', 'zh': 'zh'} i.e {'original_lang': 'original_lang'}
|
||||
// and corresponding dropdowns will show language names Arabic and Chinese. If user changes
|
||||
// Chinese to Russian then map will become {'ar': 'ar', 'zh': 'ru'} i.e {'original_lang': 'new_lang'}
|
||||
_.each(this.model.getDisplayValue(), function(value, lang) {
|
||||
languageMap[lang] = lang;
|
||||
});
|
||||
TranscriptUtils.Storage.set('languageMap', languageMap);
|
||||
AbstractEditor.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -111,14 +123,16 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
setValueInEditor: function(values) {
|
||||
var self = this,
|
||||
frag = document.createDocumentFragment(),
|
||||
dropdown = self.getDropdown(values);
|
||||
dropdown = self.getDropdown(values),
|
||||
languageMap = TranscriptUtils.Storage.get('languageMap');
|
||||
|
||||
_.each(values, function(value, key) {
|
||||
_.each(values, function(value, newLang) {
|
||||
var html = $(self.templateItem({
|
||||
lang: key,
|
||||
newLang: newLang,
|
||||
originalLang: _.findKey(languageMap, function(lang) { return lang === newLang; }) || '',
|
||||
value: value,
|
||||
url: self.model.get('urlRoot') + '/' + key
|
||||
})).prepend(dropdown.clone().val(key))[0];
|
||||
url: self.model.get('urlRoot')
|
||||
})).prepend(dropdown.clone().val(newLang))[0];
|
||||
|
||||
frag.appendChild(html);
|
||||
});
|
||||
@@ -130,63 +144,166 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
event.preventDefault();
|
||||
// We don't call updateModel here since it's bound to the
|
||||
// change event
|
||||
var dict = $.extend(true, {}, this.model.get('value'));
|
||||
dict[''] = '';
|
||||
this.setValueInEditor(dict);
|
||||
this.setValueInEditor(this.getAllLanguageDropdownElementsData(true));
|
||||
this.$el.find('.create-setting').addClass('is-disabled').attr('aria-disabled', true);
|
||||
},
|
||||
|
||||
removeEntry: function(event) {
|
||||
var self = this,
|
||||
$currentListItemEl = $(event.currentTarget).parent(),
|
||||
originalLang = $currentListItemEl.data('original-lang'),
|
||||
selectedLang = $currentListItemEl.find('select option:selected').val(),
|
||||
languageMap = TranscriptUtils.Storage.get('languageMap'),
|
||||
edxVideoIdField = TranscriptUtils.getField(self.model.collection, 'edx_video_id');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var entry = $(event.currentTarget).data('lang');
|
||||
this.setValueInEditor(_.omit(this.model.get('value'), entry));
|
||||
this.updateModel();
|
||||
/*
|
||||
There is a scenario when a user adds an empty video translation item and
|
||||
removes it. In such cases, omitting will have no harm on the model
|
||||
values or languages map.
|
||||
*/
|
||||
if (originalLang) {
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
gettext('Are you sure you want to remove this transcript?'),
|
||||
gettext('If you remove this transcript, the transcript will not be available for this component.'),
|
||||
gettext('Remove Transcript'),
|
||||
function() {
|
||||
ViewUtils.runOperationShowingMessage(
|
||||
gettext('Removing'),
|
||||
function() {
|
||||
return $.ajax({
|
||||
url: self.model.get('urlRoot'),
|
||||
type: 'DELETE',
|
||||
data: JSON.stringify({lang: originalLang, edx_video_id: edxVideoIdField.getValue()})
|
||||
}).done(function() {
|
||||
self.setValueInEditor(self.getAllLanguageDropdownElementsData(false, selectedLang));
|
||||
TranscriptUtils.Storage.set('languageMap', _.omit(languageMap, originalLang));
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setValueInEditor(this.getAllLanguageDropdownElementsData(false, selectedLang));
|
||||
}
|
||||
|
||||
this.$el.find('.create-setting').removeClass('is-disabled').attr('aria-disabled', false);
|
||||
},
|
||||
|
||||
upload: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
$target = $(event.currentTarget),
|
||||
lang = $target.data('lang'),
|
||||
model = new FileUpload({
|
||||
title: gettext('Upload translation'),
|
||||
fileFormats: ['srt']
|
||||
}),
|
||||
view = new VideoUploadDialog({
|
||||
model: model,
|
||||
url: self.model.get('urlRoot') + '/' + lang,
|
||||
parentElement: $target.closest('.xblock-editor'),
|
||||
onSuccess: function(response) {
|
||||
if (!response.filename) { return; }
|
||||
$listItem = $target.parents('li.list-settings-item'),
|
||||
originalLang = $listItem.data('original-lang'),
|
||||
newLang = $listItem.find(':selected').val(),
|
||||
edxVideoIdField = TranscriptUtils.getField(self.model.collection, 'edx_video_id'),
|
||||
fileUploadModel,
|
||||
uploadData,
|
||||
videoUploadDialog;
|
||||
|
||||
var dict = $.extend(true, {}, self.model.get('value'));
|
||||
event.preventDefault();
|
||||
|
||||
dict[lang] = response.filename;
|
||||
self.model.setValue(dict);
|
||||
}
|
||||
});
|
||||
// That's the case when an author is
|
||||
// uploading a new transcript.
|
||||
if (!originalLang) {
|
||||
originalLang = newLang;
|
||||
}
|
||||
|
||||
view.show();
|
||||
// Transcript data payload
|
||||
uploadData = {
|
||||
edx_video_id: edxVideoIdField.getValue(),
|
||||
language_code: originalLang,
|
||||
new_language_code: newLang
|
||||
};
|
||||
|
||||
fileUploadModel = new FileUpload({
|
||||
title: gettext('Upload translation'),
|
||||
fileFormats: this.validFileFormats
|
||||
});
|
||||
|
||||
videoUploadDialog = new VideoUploadDialog({
|
||||
model: fileUploadModel,
|
||||
url: this.model.get('urlRoot'),
|
||||
parentElement: $target.closest('.xblock-editor'),
|
||||
uploadData: uploadData,
|
||||
onSuccess: function(response) {
|
||||
var languageMap = TranscriptUtils.Storage.get('languageMap'),
|
||||
newLangObject = {};
|
||||
|
||||
// new language entry to be added to languageMap
|
||||
newLangObject[newLang] = newLang;
|
||||
|
||||
// Update edx-video-id
|
||||
edxVideoIdField.setValue(response.edx_video_id);
|
||||
|
||||
// Update language map by omitting original lang and adding new lang
|
||||
// if languageMap is empty then newLang will be added
|
||||
// if an original lang is replaced with new lang then omit the original lang and the add new lang
|
||||
languageMap = _.extend(_.omit(languageMap, originalLang), newLangObject);
|
||||
TranscriptUtils.Storage.set('languageMap', languageMap);
|
||||
|
||||
// re-render the whole view
|
||||
self.setValueInEditor(self.getAllLanguageDropdownElementsData());
|
||||
}
|
||||
});
|
||||
videoUploadDialog.show();
|
||||
},
|
||||
|
||||
enableAdd: function() {
|
||||
this.$el.find('.create-setting').removeClass('is-disabled').attr('aria-disabled', false);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
AbstractEditor.prototype.clear.apply(this, arguments);
|
||||
if (_.isNull(this.model.getValue())) {
|
||||
this.$el.find('.create-setting').removeClass('is-disabled').attr('aria-disabled', false);
|
||||
onChangeHandler: function(event) {
|
||||
var $target = $(event.currentTarget),
|
||||
$listItem = $target.parents('li.list-settings-item'),
|
||||
originalLang = $listItem.data('original-lang'),
|
||||
newLang = $listItem.find('select option:selected').val(),
|
||||
languageMap = TranscriptUtils.Storage.get('languageMap');
|
||||
|
||||
// To protect against any new/unsaved language code in the map.
|
||||
if (originalLang in languageMap) {
|
||||
languageMap[originalLang] = newLang;
|
||||
TranscriptUtils.Storage.set('languageMap', languageMap);
|
||||
|
||||
// an existing saved lang is changed, no need to re-render the whole view
|
||||
return;
|
||||
}
|
||||
|
||||
this.enableAdd();
|
||||
this.setValueInEditor(this.getAllLanguageDropdownElementsData());
|
||||
},
|
||||
|
||||
onChangeHandler: function(event) {
|
||||
this.showClearButton();
|
||||
this.enableAdd();
|
||||
this.updateModel();
|
||||
/**
|
||||
* Constructs data extracted from each dropdown. This will be used to re-render the whole view.
|
||||
*/
|
||||
getAllLanguageDropdownElementsData: function(isNew, omittedLanguage) {
|
||||
var data = {},
|
||||
languageDropdownElements = this.$el.find('select'),
|
||||
languageMap = TranscriptUtils.Storage.get('languageMap');
|
||||
|
||||
// data object will mirror the languageMap. `data` will contain lang to lang map as explained below
|
||||
// {originalLang: originalLang}; original lang not changed
|
||||
// {newLang: originalLang}; original lang changed to a new lang
|
||||
// {selectedLang: ''}; new lang to be added, no entry in languageMap
|
||||
_.each(languageDropdownElements, function(languageDropdown) {
|
||||
var language = $(languageDropdown).find(':selected').val();
|
||||
data[language] = _.findKey(languageMap, function(lang) { return lang === language; }) || '';
|
||||
});
|
||||
|
||||
// This is needed to render an empty item that
|
||||
// will be further used to upload a transcript.
|
||||
if (isNew) {
|
||||
data[''] = '';
|
||||
}
|
||||
|
||||
// This Omits a language from the dropdown's data. It is
|
||||
// needed when an item is going to be removed.
|
||||
if (typeof(omittedLanguage) !== 'undefined') {
|
||||
data = _.omit(data, omittedLanguage);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
<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,9 +5,7 @@
|
||||
<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">
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<div class="wrapper-comp-setting metadata-video-translations">
|
||||
<label class="label setting-label"><%= model.get('display_name')%></label>
|
||||
<input class="upload-transcript-input is-hidden" type="file" name="file" accept=".srt"/>
|
||||
<div class="wrapper-translations-settings">
|
||||
<ol class="list-settings"></ol>
|
||||
<a href="#" class="create-action create-setting">
|
||||
<span class="icon fa fa-plus" aria-hidden="true"></span><%= gettext("Add") %> <span class="sr"><%= model.get('display_name')%></span>
|
||||
</a>
|
||||
</div>
|
||||
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<span class="icon fa fa-undo" aria-hidden="true"></span>
|
||||
<span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<li class="list-settings-item">
|
||||
<a href="#" class="remove-action remove-setting" data-lang="<%= lang %>" data-value="<%= value %>"><span class="icon fa fa-times-circle" aria-hidden="true"></span><span class="sr"><%= gettext("Remove") %></span></a>
|
||||
<li class="list-settings-item" data-original-lang="<%= originalLang %>">
|
||||
<a href="#" class="remove-action remove-setting" data-value="<%= value %>"><span class="icon fa fa-times-circle" aria-hidden="true"></span><span class="sr"><%= gettext("Remove") %></span></a>
|
||||
<input type="hidden" class="input" value="<%= value %>">
|
||||
<div class="list-settings-buttons"><% if (lang) {
|
||||
%><a href="#" class="upload-action upload-setting" data-lang="<%= lang %>" data-value="<%= value %>"><%= value ? gettext("Replace") : gettext("Upload") %>
|
||||
<div class="list-settings-buttons"><% if (newLang) {
|
||||
%><a href="#" class="upload-action upload-setting" data-value="<%= value %>"><%= value ? gettext("Replace") : gettext("Upload") %>
|
||||
</a><%
|
||||
} %><% if (value) {
|
||||
%><a href="<%= url %>?filename=<%= value %>" class="download-action download-setting"><%= gettext("Download") %>
|
||||
%><a href="<%= url %>?language_code=<%= originalLang %>" class="download-action download-setting"><%= gettext("Download") %>
|
||||
</a><%
|
||||
}
|
||||
%><div>
|
||||
%>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -6,5 +6,4 @@
|
||||
<input type="file" class="file-input" name="transcript-file"
|
||||
accept="<%= _.map(ext, function(val){ return '.' + val; }).join(', ') %>">
|
||||
<input type="hidden" name="locator" value="<%= component_locator %>">
|
||||
<input type="hidden" name="video_list" value='<%= JSON.stringify(video_list) %>'>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Transcript") %>" data-tooltip="<%= gettext("Upload New .srt Transcript") %>">
|
||||
<span><%= gettext("Upload New Transcript") %></span>
|
||||
</button>
|
||||
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>&subs_id=<%= subs_id %>" data-tooltip="<%= gettext("Download Transcript for Editing") %>">
|
||||
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>" data-tooltip="<%= gettext("Download Transcript for Editing") %>">
|
||||
<span><%= gettext("Download Transcript for Editing") %></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Transcript") %>" data-tooltip="<%= gettext("Upload New Transcript") %>">
|
||||
<span><%= gettext("Upload New Transcript") %></span>
|
||||
</button>
|
||||
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>&subs_id=<%= subs_id %>" data-tooltip="<%= gettext("Download Transcript for Editing") %>">
|
||||
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>" data-tooltip="<%= gettext("Download Transcript for Editing") %>">
|
||||
<span><%= gettext("Download Transcript for Editing") %></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,6 @@ urlpatterns = [
|
||||
url(r'^transcripts/choose$', contentstore.views.choose_transcripts, name='choose_transcripts'),
|
||||
url(r'^transcripts/replace$', contentstore.views.replace_transcripts, name='replace_transcripts'),
|
||||
url(r'^transcripts/rename$', contentstore.views.rename_transcripts, name='rename_transcripts'),
|
||||
url(r'^transcripts/save$', contentstore.views.save_transcripts, name='save_transcripts'),
|
||||
url(r'^preview/xblock/(?P<usage_key_string>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>.*))?$',
|
||||
contentstore.views.preview_handler, name='preview_handler'),
|
||||
url(r'^xblock/(?P<usage_key_string>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>.*))?$',
|
||||
@@ -146,10 +145,8 @@ 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'^transcript_download/$', contentstore.views.transcript_download_handler, name='transcript_download_handler'),
|
||||
url(r'^transcript_upload/$', contentstore.views.transcript_upload_handler, name='transcript_upload_handler'),
|
||||
url(r'^transcript_delete/{}(?:/(?P<edx_video_id>[-\w]+))?(?:/(?P<language_code>[^/]*))?$'.format(
|
||||
settings.COURSE_KEY_PATTERN
|
||||
), contentstore.views.transcript_delete_handler, name='transcript_delete_handler'),
|
||||
|
||||
@@ -12,6 +12,7 @@ You can then use the CourseFactory and XModuleItemFactory as defined
|
||||
in common/lib/xmodule/xmodule/modulestore/tests/factories.py to create
|
||||
the course, section, subsection, unit, etc.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
import datetime
|
||||
@@ -36,7 +37,7 @@ from xblock.fields import ScopeIds
|
||||
from xmodule.tests import get_test_descriptor_system
|
||||
from xmodule.validation import StudioValidationMessage
|
||||
from xmodule.video_module import VideoDescriptor, create_youtube_string, EXPORT_IMPORT_STATIC_DIR
|
||||
from xmodule.video_module.transcripts_utils import download_youtube_subs, save_to_store
|
||||
from xmodule.video_module.transcripts_utils import download_youtube_subs, save_to_store, save_subs_to_store
|
||||
from . import LogicTest
|
||||
from .test_import import DummySystem
|
||||
|
||||
@@ -912,22 +913,21 @@ class VideoDescriptorStudentViewDataTestCase(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch('xmodule.video_module.video_module.is_val_transcript_feature_enabled_for_course')
|
||||
def test_student_view_data(self, field_data, expected_student_view_data, mock_transcript_feature):
|
||||
def test_student_view_data(self, field_data, expected_student_view_data):
|
||||
"""
|
||||
Ensure that student_view_data returns the expected results for video modules.
|
||||
"""
|
||||
mock_transcript_feature.return_value = False
|
||||
descriptor = instantiate_descriptor(**field_data)
|
||||
descriptor.runtime.course_id = MagicMock()
|
||||
student_view_data = descriptor.student_view_data()
|
||||
self.assertEquals(student_view_data, expected_student_view_data)
|
||||
|
||||
@patch('xmodule.video_module.video_module.HLSPlaybackEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.video_module.is_val_transcript_feature_enabled_for_course', Mock(return_value=False))
|
||||
@patch('xmodule.video_module.transcripts_utils.get_available_transcript_languages', Mock(return_value=['es']))
|
||||
@patch('edxval.api.get_video_info_for_course_and_profiles', Mock(return_value={}))
|
||||
@patch('xmodule.video_module.transcripts_utils.get_video_transcript_content')
|
||||
@patch('edxval.api.get_video_info')
|
||||
def test_student_view_data_with_hls_flag(self, mock_get_video_info):
|
||||
def test_student_view_data_with_hls_flag(self, mock_get_video_info, mock_get_video_transcript_content):
|
||||
mock_get_video_info.return_value = {
|
||||
'url': '/edxval/video/example',
|
||||
'edx_video_id': u'example_id',
|
||||
@@ -943,8 +943,18 @@ class VideoDescriptorStudentViewDataTestCase(unittest.TestCase):
|
||||
]
|
||||
}
|
||||
|
||||
mock_get_video_transcript_content.return_value = {
|
||||
'content': json.dumps({
|
||||
"start": [10],
|
||||
"end": [100],
|
||||
"text": ["Hi, welcome to Edx."],
|
||||
}),
|
||||
'file_name': 'edx.sjson'
|
||||
}
|
||||
|
||||
descriptor = instantiate_descriptor(edx_video_id='example_id', only_on_web=False)
|
||||
descriptor.runtime.course_id = MagicMock()
|
||||
descriptor.runtime.handler_url = MagicMock()
|
||||
student_view_data = descriptor.student_view_data()
|
||||
expected_video_data = {u'hls': {'url': u'http://www.meowmix.com', 'file_size': 25556}}
|
||||
self.assertDictEqual(student_view_data.get('encoded_videos'), expected_video_data)
|
||||
@@ -1032,9 +1042,10 @@ class VideoDescriptorIndexingTestCase(unittest.TestCase):
|
||||
<handout src="http://www.example.com/handout"/>
|
||||
</video>
|
||||
'''
|
||||
|
||||
yt_subs_id = 'OEoXaMPEzfM'
|
||||
descriptor = instantiate_descriptor(data=xml_data_sub)
|
||||
download_youtube_subs('OEoXaMPEzfM', descriptor, settings)
|
||||
subs = download_youtube_subs(yt_subs_id, descriptor, settings)
|
||||
save_subs_to_store(json.loads(subs), yt_subs_id, descriptor)
|
||||
self.assertEqual(descriptor.index_dictionary(), {
|
||||
"content": {
|
||||
"display_name": "Test Video",
|
||||
@@ -1063,9 +1074,10 @@ class VideoDescriptorIndexingTestCase(unittest.TestCase):
|
||||
<transcript language="ge" src="subs_grmtran1.srt" />
|
||||
</video>
|
||||
'''
|
||||
|
||||
yt_subs_id = 'OEoXaMPEzfM'
|
||||
descriptor = instantiate_descriptor(data=xml_data_sub_transcript)
|
||||
download_youtube_subs('OEoXaMPEzfM', descriptor, settings)
|
||||
subs = download_youtube_subs(yt_subs_id, descriptor, settings)
|
||||
save_subs_to_store(json.loads(subs), yt_subs_id, descriptor)
|
||||
save_to_store(SRT_FILEDATA, "subs_grmtran1.srt", 'text/srt', descriptor.location)
|
||||
self.assertEqual(descriptor.index_dictionary(), {
|
||||
"content": {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
Utility functions for transcripts dealing with Django models.
|
||||
"""
|
||||
from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag
|
||||
|
||||
|
||||
def is_val_transcript_feature_enabled_for_course(course_id):
|
||||
"""
|
||||
Get edx-val transcript feature flag
|
||||
|
||||
Arguments:
|
||||
course_id(CourseKey): Course key identifying a course whose feature flag is being inspected.
|
||||
"""
|
||||
return VideoTranscriptEnabledFlag.feature_enabled(course_id=course_id)
|
||||
@@ -2,6 +2,7 @@
|
||||
Utility functions for transcripts.
|
||||
++++++++++++++++++++++++++++++++++
|
||||
"""
|
||||
from functools import wraps
|
||||
from django.conf import settings
|
||||
import os
|
||||
import copy
|
||||
@@ -49,6 +50,26 @@ class TranscriptsRequestValidationException(Exception): # pylint: disable=missi
|
||||
pass
|
||||
|
||||
|
||||
def exception_decorator(func):
|
||||
"""
|
||||
Generate NotFoundError for TranscriptsGenerationException, UnicodeDecodeError.
|
||||
|
||||
Args:
|
||||
`func`: Input function
|
||||
|
||||
Returns:
|
||||
'wrapper': Decorated function
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwds):
|
||||
try:
|
||||
return func(*args, **kwds)
|
||||
except (TranscriptsGenerationException, UnicodeDecodeError) as ex:
|
||||
log.exception(text_type(ex))
|
||||
raise NotFoundError
|
||||
return wrapper
|
||||
|
||||
|
||||
def generate_subs(speed, source_speed, source_subs):
|
||||
"""
|
||||
Generate transcripts from one speed to another speed.
|
||||
@@ -179,7 +200,7 @@ def get_transcripts_from_youtube(youtube_id, settings, i18n, youtube_transcript_
|
||||
|
||||
def download_youtube_subs(youtube_id, video_descriptor, settings):
|
||||
"""
|
||||
Download transcripts from Youtube and save them to assets.
|
||||
Download transcripts from Youtube.
|
||||
|
||||
Args:
|
||||
youtube_id: str, actual youtube_id of the video.
|
||||
@@ -188,7 +209,7 @@ def download_youtube_subs(youtube_id, video_descriptor, settings):
|
||||
We save transcripts for 1.0 speed, as for other speed conversion is done on front-end.
|
||||
|
||||
Returns:
|
||||
None, if transcripts were successfully downloaded and saved.
|
||||
Serialized sjson transcript content, if transcripts were successfully downloaded and saved.
|
||||
|
||||
Raises:
|
||||
GetTranscriptsFromYouTubeException, if fails.
|
||||
@@ -197,9 +218,7 @@ def download_youtube_subs(youtube_id, video_descriptor, settings):
|
||||
_ = i18n.ugettext
|
||||
|
||||
subs = get_transcripts_from_youtube(youtube_id, settings, i18n)
|
||||
save_subs_to_store(subs, youtube_id, video_descriptor)
|
||||
|
||||
log.info("Transcripts for youtube_id %s for 1.0 speed are downloaded and saved.", youtube_id)
|
||||
return json.dumps(subs, indent=2)
|
||||
|
||||
|
||||
def remove_subs_from_store(subs_id, item, lang='en'):
|
||||
@@ -361,7 +380,7 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
|
||||
This whole action ensures that after user changes video fields, proper `sub` files, corresponding
|
||||
to new values of video fields, will be presented in system.
|
||||
|
||||
# 2 convert /static/filename.srt to filename.srt in self.transcripts.
|
||||
# 2. convert /static/filename.srt to filename.srt in self.transcripts.
|
||||
(it is done to allow user to enter both /static/filename.srt and filename.srt)
|
||||
|
||||
# 3. Generate transcripts translation only when user clicks `save` button, not while switching tabs.
|
||||
@@ -371,33 +390,32 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
|
||||
(To avoid confusing situation if you attempt to correct a translation by uploading
|
||||
a new version of the SRT file with same name).
|
||||
"""
|
||||
|
||||
_ = item.runtime.service(item, "i18n").ugettext
|
||||
|
||||
# 1.
|
||||
html5_ids = get_html5_ids(item.html5_sources)
|
||||
# # 1.
|
||||
# html5_ids = get_html5_ids(item.html5_sources)
|
||||
|
||||
# Youtube transcript source should always have a higher priority than html5 sources. Appending
|
||||
# `youtube_id_1_0` at the end helps achieve this when we read transcripts list.
|
||||
possible_video_id_list = html5_ids + [item.youtube_id_1_0]
|
||||
sub_name = item.sub
|
||||
for video_id in possible_video_id_list:
|
||||
if not video_id:
|
||||
continue
|
||||
if not sub_name:
|
||||
remove_subs_from_store(video_id, item)
|
||||
continue
|
||||
# copy_or_rename_transcript changes item.sub of module
|
||||
try:
|
||||
# updates item.sub with `video_id`, if it is successful.
|
||||
copy_or_rename_transcript(video_id, sub_name, item, user=user)
|
||||
except NotFoundError:
|
||||
# subtitles file `sub_name` is not presented in the system. Nothing to copy or rename.
|
||||
log.debug(
|
||||
"Copying %s file content to %s name is failed, "
|
||||
"original file does not exist.",
|
||||
sub_name, video_id
|
||||
)
|
||||
# # Youtube transcript source should always have a higher priority than html5 sources. Appending
|
||||
# # `youtube_id_1_0` at the end helps achieve this when we read transcripts list.
|
||||
# possible_video_id_list = html5_ids + [item.youtube_id_1_0]
|
||||
# sub_name = item.sub
|
||||
# for video_id in possible_video_id_list:
|
||||
# if not video_id:
|
||||
# continue
|
||||
# if not sub_name:
|
||||
# remove_subs_from_store(video_id, item)
|
||||
# continue
|
||||
# # copy_or_rename_transcript changes item.sub of module
|
||||
# try:
|
||||
# # updates item.sub with `video_id`, if it is successful.
|
||||
# copy_or_rename_transcript(video_id, sub_name, item, user=user)
|
||||
# except NotFoundError:
|
||||
# # subtitles file `sub_name` is not presented in the system. Nothing to copy or rename.
|
||||
# log.debug(
|
||||
# "Copying %s file content to %s name is failed, "
|
||||
# "original file does not exist.",
|
||||
# sub_name, video_id
|
||||
# )
|
||||
|
||||
# 2.
|
||||
if generate_translation:
|
||||
@@ -409,6 +427,9 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
|
||||
old_langs = set(old_metadata.get('transcripts', {})) if old_metadata else set()
|
||||
new_langs = set(item.transcripts)
|
||||
|
||||
html5_ids = get_html5_ids(item.html5_sources)
|
||||
possible_video_id_list = html5_ids + [item.youtube_id_1_0]
|
||||
|
||||
for lang in old_langs.difference(new_langs): # 3a
|
||||
for video_id in possible_video_id_list:
|
||||
if video_id:
|
||||
@@ -637,7 +658,8 @@ class Transcript(object):
|
||||
# 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'),
|
||||
# Skip byte order mark(BOM) character
|
||||
content.decode('utf-8-sig'),
|
||||
error_handling=SubRipFile.ERROR_RAISE
|
||||
)
|
||||
except Error as ex: # Base exception from pysrt
|
||||
@@ -707,7 +729,7 @@ class VideoTranscriptsMixin(object):
|
||||
This is necessary for both VideoModule and VideoDescriptor.
|
||||
"""
|
||||
|
||||
def available_translations(self, transcripts, verify_assets=None, include_val_transcripts=None):
|
||||
def available_translations(self, transcripts, verify_assets=None, is_bumper=False):
|
||||
"""
|
||||
Return a list of language codes for which we have transcripts.
|
||||
|
||||
@@ -729,39 +751,27 @@ class VideoTranscriptsMixin(object):
|
||||
|
||||
sub, other_langs = transcripts["sub"], transcripts["transcripts"]
|
||||
|
||||
# If we're not verifying the assets, we just trust our field values
|
||||
if not verify_assets:
|
||||
if other_langs:
|
||||
translations = list(other_langs)
|
||||
if verify_assets:
|
||||
all_langs = dict(**other_langs)
|
||||
if sub:
|
||||
all_langs.update({'en': sub})
|
||||
|
||||
for language, filename in all_langs.iteritems():
|
||||
try:
|
||||
# for bumper videos, transcripts are stored in content store only
|
||||
if is_bumper:
|
||||
get_transcript_for_video(self.location, filename, filename, language)
|
||||
else:
|
||||
get_transcript(self, language)
|
||||
except NotFoundError:
|
||||
continue
|
||||
|
||||
translations.append(language)
|
||||
else:
|
||||
# If we're not verifying the assets, we just trust our field values
|
||||
translations = list(other_langs)
|
||||
if not translations or sub:
|
||||
translations += ['en']
|
||||
return translations
|
||||
|
||||
# If we've gotten this far, we're going to verify that the transcripts
|
||||
# being referenced are actually either in the contentstore or in edx-val.
|
||||
if include_val_transcripts:
|
||||
translations = get_available_transcript_languages(edx_video_id=self.edx_video_id)
|
||||
|
||||
if sub: # check if sjson exists for 'en'.
|
||||
try:
|
||||
Transcript.asset(self.location, sub, 'en')
|
||||
except NotFoundError:
|
||||
try:
|
||||
Transcript.asset(self.location, None, None, sub)
|
||||
except NotFoundError:
|
||||
pass
|
||||
else:
|
||||
translations.append('en')
|
||||
else:
|
||||
translations.append('en')
|
||||
|
||||
for lang in other_langs:
|
||||
try:
|
||||
Transcript.asset(self.location, None, None, other_langs[lang])
|
||||
except NotFoundError:
|
||||
continue
|
||||
|
||||
translations.append(lang)
|
||||
|
||||
# to clean redundant language codes.
|
||||
return list(set(translations))
|
||||
@@ -827,7 +837,7 @@ class VideoTranscriptsMixin(object):
|
||||
transcript_language = u'en'
|
||||
return transcript_language
|
||||
|
||||
def get_transcripts_info(self, is_bumper=False, include_val_transcripts=False):
|
||||
def get_transcripts_info(self, is_bumper=False):
|
||||
"""
|
||||
Returns a transcript dictionary for the video.
|
||||
|
||||
@@ -848,9 +858,8 @@ class VideoTranscriptsMixin(object):
|
||||
for language_code, transcript_file in transcripts.items() if transcript_file != ''
|
||||
}
|
||||
|
||||
# For phase 2, removing `include_val_transcripts` will make edx-val
|
||||
# taking over the control for transcripts.
|
||||
if include_val_transcripts:
|
||||
# bumper transcripts are stored in content store so we don't need to include val transcripts
|
||||
if not is_bumper:
|
||||
transcript_languages = get_available_transcript_languages(edx_video_id=self.edx_video_id)
|
||||
# HACK Warning! this is temporary and will be removed once edx-val take over the
|
||||
# transcript module and contentstore will only function as fallback until all the
|
||||
@@ -867,11 +876,12 @@ class VideoTranscriptsMixin(object):
|
||||
}
|
||||
|
||||
|
||||
@exception_decorator
|
||||
def get_transcript_from_val(edx_video_id, lang=None, output_format=Transcript.SRT):
|
||||
"""
|
||||
Get video transcript from edx-val.
|
||||
Arguments:
|
||||
edx_video_id (unicode): course identifier
|
||||
edx_video_id (unicode): video identifier
|
||||
lang (unicode): transcript language
|
||||
output_format (unicode): transcript output format
|
||||
Returns:
|
||||
@@ -894,6 +904,10 @@ def get_transcript_for_video(video_location, subs_id, file_name, language):
|
||||
"""
|
||||
Get video transcript from content store.
|
||||
|
||||
NOTE: Transcripts can be searched from content store by two ways:
|
||||
1. by an id(a.k.a subs_id) which will be used to construct transcript filename
|
||||
2. by providing transcript filename
|
||||
|
||||
Arguments:
|
||||
video_location (Locator): Video location
|
||||
subs_id (unicode): id for a transcript in content store
|
||||
@@ -917,6 +931,7 @@ def get_transcript_for_video(video_location, subs_id, file_name, language):
|
||||
return input_format, base_name, content
|
||||
|
||||
|
||||
@exception_decorator
|
||||
def get_transcript_from_contentstore(video, language, output_format, transcripts_info, youtube_id=None):
|
||||
"""
|
||||
Get video transcript from content store.
|
||||
@@ -931,6 +946,7 @@ def get_transcript_from_contentstore(video, language, output_format, transcripts
|
||||
Returns:
|
||||
tuple containing content, filename, mimetype
|
||||
"""
|
||||
input_format, base_name, transcript_content = None, None, None
|
||||
if output_format not in (Transcript.SRT, Transcript.SJSON, Transcript.TXT):
|
||||
raise NotFoundError('Invalid transcript format `{output_format}`'.format(output_format=output_format))
|
||||
|
||||
@@ -938,30 +954,29 @@ def get_transcript_from_contentstore(video, language, output_format, transcripts
|
||||
transcripts = dict(other_languages)
|
||||
|
||||
# this is sent in case of a translation dispatch and we need to use it as our subs_id.
|
||||
if youtube_id:
|
||||
transcripts['en'] = youtube_id
|
||||
elif sub:
|
||||
transcripts['en'] = sub
|
||||
elif video.youtube_id_1_0:
|
||||
transcripts['en'] = video.youtube_id_1_0
|
||||
elif language == u'en':
|
||||
raise NotFoundError('No transcript for `en` language')
|
||||
possible_sub_ids = [youtube_id, sub, video.youtube_id_1_0] + get_html5_ids(video.html5_sources)
|
||||
for sub_id in possible_sub_ids:
|
||||
try:
|
||||
transcripts[u'en'] = sub_id
|
||||
input_format, base_name, transcript_content = get_transcript_for_video(
|
||||
video.location,
|
||||
subs_id=sub_id,
|
||||
file_name=transcripts[language],
|
||||
language=language
|
||||
)
|
||||
break
|
||||
except (KeyError, NotFoundError):
|
||||
continue
|
||||
|
||||
try:
|
||||
input_format, base_name, transcript_content = get_transcript_for_video(
|
||||
video.location,
|
||||
subs_id=transcripts.get('en'),
|
||||
file_name=transcripts[language],
|
||||
language=language
|
||||
)
|
||||
except KeyError:
|
||||
raise NotFoundError
|
||||
if transcript_content is None:
|
||||
raise NotFoundError('No transcript for `{lang}` language'.format(
|
||||
lang=language
|
||||
))
|
||||
|
||||
# add language prefix to transcript file only if language is not None
|
||||
language_prefix = '{}_'.format(language) if language else ''
|
||||
transcript_name = u'{}{}.{}'.format(language_prefix, base_name, output_format)
|
||||
transcript_content = Transcript.convert(transcript_content, input_format=input_format, output_format=output_format)
|
||||
|
||||
if not transcript_content.strip():
|
||||
raise NotFoundError('No transcript content')
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ StudioViewHandlers are handlers for video descriptor instance.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import six
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from webob import Response
|
||||
|
||||
@@ -20,20 +20,21 @@ from xmodule.exceptions import NotFoundError
|
||||
from xmodule.fields import RelativeTime
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
from edxval.api import create_or_update_video_transcript, create_external_video, delete_video_transcript
|
||||
from .transcripts_utils import (
|
||||
convert_video_transcript,
|
||||
clean_video_id,
|
||||
get_or_create_sjson,
|
||||
generate_sjson_for_all_speeds,
|
||||
get_video_transcript_content,
|
||||
save_to_store,
|
||||
subs_filename,
|
||||
Transcript,
|
||||
TranscriptException,
|
||||
TranscriptsGenerationException,
|
||||
youtube_speed_dict,
|
||||
)
|
||||
from .transcripts_model_utils import (
|
||||
is_val_transcript_feature_enabled_for_course
|
||||
get_transcript,
|
||||
get_transcript_from_contentstore,
|
||||
remove_subs_from_store,
|
||||
get_html5_ids
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -246,6 +247,36 @@ class VideoStudentViewHandlers(object):
|
||||
self.runtime.publish(self, "completion", data)
|
||||
return {"result": "ok"}
|
||||
|
||||
@staticmethod
|
||||
def make_transcript_http_response(content, filename, language, content_type, add_attachment_header=True):
|
||||
"""
|
||||
Construct `Response` object.
|
||||
|
||||
Arguments:
|
||||
content (unicode): transcript content
|
||||
filename (unicode): transcript filename
|
||||
language (unicode): transcript language
|
||||
mimetype (unicode): transcript content type
|
||||
add_attachment_header (bool): whether to add attachment header or not
|
||||
"""
|
||||
headerlist = [
|
||||
('Content-Language', language),
|
||||
]
|
||||
|
||||
if add_attachment_header:
|
||||
headerlist.append(
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(filename.encode('utf-8')))
|
||||
)
|
||||
|
||||
response = Response(
|
||||
content,
|
||||
headerlist=headerlist,
|
||||
charset='utf8'
|
||||
)
|
||||
response.content_type = content_type
|
||||
|
||||
return response
|
||||
|
||||
@XBlock.handler
|
||||
def transcript(self, request, dispatch):
|
||||
"""
|
||||
@@ -270,9 +301,8 @@ class VideoStudentViewHandlers(object):
|
||||
For 'en' check if SJSON exists. For non-`en` check if SRT file exists.
|
||||
"""
|
||||
is_bumper = request.GET.get('is_bumper', False)
|
||||
# Currently, we don't handle video pre-load/bumper transcripts in edx-val.
|
||||
feature_enabled = is_val_transcript_feature_enabled_for_course(self.course_id) and not is_bumper
|
||||
transcripts = self.get_transcripts_info(is_bumper, include_val_transcripts=feature_enabled)
|
||||
transcripts = self.get_transcripts_info(is_bumper)
|
||||
|
||||
if dispatch.startswith('translation'):
|
||||
language = dispatch.replace('translation', '').strip('/')
|
||||
|
||||
@@ -288,85 +318,51 @@ class VideoStudentViewHandlers(object):
|
||||
self.transcript_language = language
|
||||
|
||||
try:
|
||||
transcript = self.translation(request.GET.get('videoId', None), transcripts)
|
||||
except (TypeError, TranscriptException, NotFoundError) as ex:
|
||||
# Catching `TranscriptException` because its also getting raised at places
|
||||
# when transcript is not found in contentstore.
|
||||
log.debug(six.text_type(ex))
|
||||
# Try to return static URL redirection as last resort
|
||||
# if no translation is required
|
||||
response = self.get_static_transcript(request, transcripts)
|
||||
if response.status_code == 404 and feature_enabled:
|
||||
# Try to get transcript from edx-val as a last resort.
|
||||
transcript = get_video_transcript_content(self.edx_video_id, self.transcript_language)
|
||||
if transcript:
|
||||
transcript_conversion_props = dict(transcript, output_format=Transcript.SJSON)
|
||||
transcript = convert_video_transcript(**transcript_conversion_props)
|
||||
response = Response(
|
||||
transcript['content'],
|
||||
headerlist=[('Content-Language', self.transcript_language)],
|
||||
charset='utf8',
|
||||
)
|
||||
response.content_type = Transcript.mime_types[Transcript.SJSON]
|
||||
if is_bumper:
|
||||
content, filename, mimetype = get_transcript_from_contentstore(
|
||||
self,
|
||||
self.transcript_language,
|
||||
Transcript.SJSON,
|
||||
transcripts
|
||||
)
|
||||
else:
|
||||
content, filename, mimetype = get_transcript(
|
||||
self,
|
||||
lang=self.transcript_language,
|
||||
output_format=Transcript.SJSON,
|
||||
youtube_id=request.GET.get('videoId'),
|
||||
)
|
||||
|
||||
return response
|
||||
except (UnicodeDecodeError, TranscriptsGenerationException) as ex:
|
||||
log.info(six.text_type(ex))
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(transcript, headerlist=[('Content-Language', language)])
|
||||
response.content_type = Transcript.mime_types['sjson']
|
||||
response = self.make_transcript_http_response(
|
||||
content,
|
||||
filename,
|
||||
self.transcript_language,
|
||||
mimetype,
|
||||
add_attachment_header=False
|
||||
)
|
||||
except NotFoundError:
|
||||
log.exception('[Translation Dispatch] %s', self.location)
|
||||
response = self.get_static_transcript(request, transcripts)
|
||||
|
||||
elif dispatch == 'download':
|
||||
lang = request.GET.get('lang', None)
|
||||
|
||||
try:
|
||||
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(
|
||||
transcripts, transcript_format=self.transcript_download_format, lang=lang
|
||||
)
|
||||
except (KeyError, UnicodeDecodeError):
|
||||
content, filename, mimetype = get_transcript(self, lang)
|
||||
except NotFoundError:
|
||||
return Response(status=404)
|
||||
except (ValueError, NotFoundError):
|
||||
response = Response(status=404)
|
||||
# Check for transcripts in edx-val as a last resort if corresponding feature is enabled.
|
||||
if feature_enabled:
|
||||
# Make sure the language is set.
|
||||
if not lang:
|
||||
lang = self.get_default_transcript_language(transcripts)
|
||||
|
||||
transcript = get_video_transcript_content(edx_video_id=self.edx_video_id, language_code=lang)
|
||||
if transcript:
|
||||
transcript_conversion_props = dict(transcript, output_format=self.transcript_download_format)
|
||||
transcript = convert_video_transcript(**transcript_conversion_props)
|
||||
response = Response(
|
||||
transcript['content'],
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{filename}"'.format(
|
||||
filename=transcript['filename']
|
||||
)),
|
||||
('Content-Language', lang),
|
||||
],
|
||||
charset='utf8',
|
||||
)
|
||||
response.content_type = Transcript.mime_types[self.transcript_download_format]
|
||||
|
||||
return response
|
||||
else:
|
||||
response = Response(
|
||||
transcript_content,
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(transcript_filename.encode('utf8'))),
|
||||
('Content-Language', self.transcript_language),
|
||||
],
|
||||
charset='utf8'
|
||||
)
|
||||
response.content_type = transcript_mime_type
|
||||
|
||||
response = self.make_transcript_http_response(
|
||||
content,
|
||||
filename,
|
||||
self.transcript_language,
|
||||
mimetype
|
||||
)
|
||||
elif dispatch.startswith('available_translations'):
|
||||
|
||||
available_translations = self.available_translations(
|
||||
transcripts,
|
||||
verify_assets=True,
|
||||
include_val_transcripts=feature_enabled,
|
||||
is_bumper=is_bumper
|
||||
)
|
||||
if available_translations:
|
||||
response = Response(json.dumps(available_translations))
|
||||
@@ -384,6 +380,38 @@ class VideoStudioViewHandlers(object):
|
||||
"""
|
||||
Handlers for Studio view.
|
||||
"""
|
||||
def validate_transcript_upload_data(self, data):
|
||||
"""
|
||||
Validates video transcript file.
|
||||
Arguments:
|
||||
data: Transcript data to be validated.
|
||||
Returns:
|
||||
None or String
|
||||
If there is error returns error message otherwise None.
|
||||
"""
|
||||
error = None
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
# 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]
|
||||
|
||||
# Get available transcript languages.
|
||||
transcripts = self.get_transcripts_info()
|
||||
available_translations = self.available_translations(transcripts, verify_assets=True)
|
||||
|
||||
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 available_translations
|
||||
):
|
||||
error = _(u'A transcript with the "{language_code}" language code already exists.'.format(
|
||||
language_code=data['new_language_code']
|
||||
))
|
||||
elif 'file' not in data:
|
||||
error = _(u'A transcript file is required.')
|
||||
|
||||
return error
|
||||
|
||||
@XBlock.handler
|
||||
def studio_transcript(self, request, dispatch):
|
||||
"""
|
||||
@@ -415,39 +443,104 @@ class VideoStudioViewHandlers(object):
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
if dispatch.startswith('translation'):
|
||||
language = dispatch.replace('translation', '').strip('/')
|
||||
|
||||
if not language:
|
||||
log.info("Invalid /translation request: no language.")
|
||||
return Response(status=400)
|
||||
|
||||
if request.method == 'POST':
|
||||
subtitles = request.POST['file']
|
||||
try:
|
||||
file_data = subtitles.file.read()
|
||||
unicode(file_data, "utf-8", "strict")
|
||||
except UnicodeDecodeError:
|
||||
log.info("Invalid encoding type for transcript file: {}".format(subtitles.filename))
|
||||
msg = _("Invalid encoding type, transcripts should be UTF-8 encoded.")
|
||||
return Response(msg, status=400)
|
||||
save_to_store(file_data, unicode(subtitles.filename), 'application/x-subrip', self.location)
|
||||
generate_sjson_for_all_speeds(self, unicode(subtitles.filename), {}, language)
|
||||
response = {'filename': unicode(subtitles.filename), 'status': 'Success'}
|
||||
return Response(json.dumps(response), status=201)
|
||||
error = self.validate_transcript_upload_data(data=request.POST)
|
||||
if error:
|
||||
response = Response(json={'error': error}, status=400)
|
||||
else:
|
||||
edx_video_id = clean_video_id(request.POST['edx_video_id'])
|
||||
language_code = request.POST['language_code']
|
||||
new_language_code = request.POST['new_language_code']
|
||||
transcript_file = request.POST['file'].file
|
||||
|
||||
elif request.method == 'GET':
|
||||
if not edx_video_id:
|
||||
# Back-populate the video ID for an external video.
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.edx_video_id = edx_video_id = create_external_video(display_name=u'external video')
|
||||
|
||||
filename = request.GET.get('filename')
|
||||
if not filename:
|
||||
log.info("Invalid /translation request: no filename in request.GET")
|
||||
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={
|
||||
'file_format': Transcript.SJSON,
|
||||
'language_code': new_language_code
|
||||
},
|
||||
file_data=ContentFile(sjson_subs),
|
||||
)
|
||||
payload = {
|
||||
'edx_video_id': edx_video_id,
|
||||
'language_code': new_language_code
|
||||
}
|
||||
response = Response(json.dumps(payload), status=201)
|
||||
except (TranscriptsGenerationException, UnicodeDecodeError):
|
||||
response = Response(
|
||||
json={
|
||||
'error': _(
|
||||
u'There is a problem with this transcript file. Try to upload a different file.'
|
||||
)
|
||||
},
|
||||
status=400
|
||||
)
|
||||
elif request.method == 'DELETE':
|
||||
request_data = request.json
|
||||
|
||||
if 'lang' not in request_data or 'edx_video_id' not in request_data:
|
||||
return Response(status=400)
|
||||
|
||||
content = Transcript.get_asset(self.location, filename).data
|
||||
response = Response(content, headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(filename.encode('utf8'))),
|
||||
('Content-Language', language),
|
||||
])
|
||||
response.content_type = Transcript.mime_types['srt']
|
||||
language = request_data['lang']
|
||||
edx_video_id = clean_video_id(request_data['edx_video_id'])
|
||||
|
||||
if edx_video_id:
|
||||
delete_video_transcript(video_id=edx_video_id, language_code=language)
|
||||
|
||||
if language == u'en':
|
||||
# remove any transcript file from content store for the video ids
|
||||
possible_sub_ids = [
|
||||
self.sub, # pylint: disable=access-member-before-definition
|
||||
self.youtube_id_1_0
|
||||
] + get_html5_ids(self.html5_sources)
|
||||
for sub_id in possible_sub_ids:
|
||||
remove_subs_from_store(sub_id, self, language)
|
||||
|
||||
# update metadata as `en` can also be present in `transcripts` field
|
||||
remove_subs_from_store(self.transcripts.pop(language, None), self, language)
|
||||
|
||||
# also empty `sub` field
|
||||
self.sub = '' # pylint: disable=attribute-defined-outside-init
|
||||
else:
|
||||
remove_subs_from_store(self.transcripts.pop(language, None), self, language)
|
||||
|
||||
return Response(status=200)
|
||||
|
||||
elif request.method == 'GET':
|
||||
language = request.GET.get('language_code')
|
||||
if not language:
|
||||
return Response(json={'error': _(u'Language is required.')}, status=400)
|
||||
|
||||
try:
|
||||
transcript_content, transcript_name, mime_type = get_transcript(
|
||||
video=self, lang=language, output_format=Transcript.SRT
|
||||
)
|
||||
response = Response(transcript_content, headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(transcript_name.encode('utf8'))),
|
||||
('Content-Language', language),
|
||||
('Content-Type', mime_type)
|
||||
])
|
||||
except (UnicodeDecodeError, TranscriptsGenerationException, NotFoundError):
|
||||
response = Response(status=404)
|
||||
|
||||
else:
|
||||
# Any other HTTP method is not allowed.
|
||||
response = Response(status=404)
|
||||
|
||||
else: # unknown dispatch
|
||||
log.debug("Dispatch is not allowed")
|
||||
|
||||
@@ -49,10 +49,9 @@ from .transcripts_utils import (
|
||||
VideoTranscriptsMixin,
|
||||
clean_video_id,
|
||||
subs_filename,
|
||||
get_transcript_for_video
|
||||
)
|
||||
from .transcripts_model_utils import (
|
||||
is_val_transcript_feature_enabled_for_course
|
||||
)
|
||||
|
||||
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
|
||||
from .video_utils import create_youtube_string, format_xml_exception_message, get_poster, rewrite_video_url
|
||||
from .video_xfields import VideoFields
|
||||
@@ -304,8 +303,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
if download_video_link and download_video_link.endswith('.m3u8'):
|
||||
download_video_link = None
|
||||
|
||||
feature_enabled = is_val_transcript_feature_enabled_for_course(self.course_id)
|
||||
transcripts = self.get_transcripts_info(include_val_transcripts=feature_enabled)
|
||||
transcripts = self.get_transcripts_info()
|
||||
track_url, transcript_language, sorted_languages = self.get_transcripts_for_student(transcripts=transcripts)
|
||||
|
||||
# CDN_VIDEO_URLS is only to be used here and will be deleted
|
||||
@@ -363,7 +361,6 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
|
||||
'saveStateUrl': self.system.ajax_url + '/save_user_state',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'streams': self.youtube_streams,
|
||||
'sub': self.sub,
|
||||
'sources': sources,
|
||||
'poster': poster,
|
||||
'duration': video_duration,
|
||||
@@ -607,10 +604,39 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
else:
|
||||
editable_fields.pop('source')
|
||||
|
||||
# Default Timed Transcript a.k.a `sub` has been deprecated and end users shall
|
||||
# not be able to modify it.
|
||||
editable_fields.pop('sub')
|
||||
|
||||
languages = [{'label': label, 'code': lang} for lang, label in settings.ALL_LANGUAGES]
|
||||
languages.sort(key=lambda l: l['label'])
|
||||
editable_fields['transcripts']['custom'] = True
|
||||
editable_fields['transcripts']['languages'] = languages
|
||||
editable_fields['transcripts']['type'] = 'VideoTranslations'
|
||||
|
||||
# We need to send ajax requests to show transcript status
|
||||
# whenever edx_video_id changes on frontend. Thats why we
|
||||
# are changing type to `VideoID` so that a specific
|
||||
# Backbonjs view can handle it.
|
||||
editable_fields['edx_video_id']['type'] = 'VideoID'
|
||||
|
||||
# construct transcripts info and also find if `en` subs exist
|
||||
transcripts_info = self.get_transcripts_info()
|
||||
possible_sub_ids = [self.sub, self.youtube_id_1_0] + get_html5_ids(self.html5_sources)
|
||||
for sub_id in possible_sub_ids:
|
||||
try:
|
||||
get_transcript_for_video(
|
||||
self.location,
|
||||
subs_id=sub_id,
|
||||
file_name=sub_id,
|
||||
language=u'en'
|
||||
)
|
||||
transcripts_info['transcripts'] = dict(transcripts_info['transcripts'], en=sub_id)
|
||||
break
|
||||
except NotFoundError:
|
||||
continue
|
||||
|
||||
editable_fields['transcripts']['value'] = transcripts_info['transcripts']
|
||||
editable_fields['transcripts']['urlRoot'] = self.runtime.handler_url(
|
||||
self,
|
||||
'studio_transcript',
|
||||
@@ -1098,9 +1124,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
"file_size": 0, # File size is not relevant for external link
|
||||
}
|
||||
|
||||
feature_enabled = is_val_transcript_feature_enabled_for_course(self.runtime.course_id.for_branch(None))
|
||||
transcripts_info = self.get_transcripts_info(include_val_transcripts=feature_enabled)
|
||||
available_translations = self.available_translations(transcripts_info, include_val_transcripts=feature_enabled)
|
||||
available_translations = self.available_translations(self.get_transcripts_info())
|
||||
transcripts = {
|
||||
lang: self.runtime.handler_url(self, 'transcript', 'download', query="lang=" + lang, thirdparty=True)
|
||||
for lang in available_translations
|
||||
|
||||
@@ -104,6 +104,8 @@ class VideoFields(object):
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
# `sub` is deprecated field and should not be used in future. Now, transcripts are primarily handled in VAL and
|
||||
# backward compatibility for the video modules already using this field has been ensured.
|
||||
sub = String(
|
||||
help=_("The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting."), # pylint: disable=line-too-long
|
||||
display_name=_("Default Timed Transcript"),
|
||||
|
||||
@@ -64,7 +64,6 @@ DEFAULT_SETTINGS = [
|
||||
|
||||
# advanced
|
||||
[DISPLAY_NAME, 'Video', False],
|
||||
['Default Timed Transcript', '', False],
|
||||
['Download Transcript Allowed', 'False', False],
|
||||
['Downloadable Transcript URL', '', False],
|
||||
['Show Transcript', 'True', False],
|
||||
@@ -82,6 +81,11 @@ DEFAULT_SETTINGS = [
|
||||
['YouTube ID for 1.5x speed', '', False]
|
||||
]
|
||||
|
||||
# field names without clear button
|
||||
FIELDS_WO_CLEAR = [
|
||||
'Transcript Languages'
|
||||
]
|
||||
|
||||
|
||||
# We should wait 300 ms for event handler invocation + 200ms for safety.
|
||||
DELAY = 0.5
|
||||
@@ -347,15 +351,22 @@ class VideoComponentPage(VideoPage):
|
||||
"""
|
||||
Verify that video component has correct default settings.
|
||||
"""
|
||||
query = '.wrapper-comp-setting'
|
||||
settings = self.q(css=query).results
|
||||
if len(DEFAULT_SETTINGS) != len(settings):
|
||||
return False
|
||||
def _check_settings_length():
|
||||
"""Check video settings"""
|
||||
query = '.wrapper-comp-setting'
|
||||
settings = self.q(css=query).results
|
||||
if len(DEFAULT_SETTINGS) == len(settings):
|
||||
return True, settings
|
||||
return (False, None)
|
||||
|
||||
settings = Promise(_check_settings_length, 'All video fields are present').fulfill()
|
||||
|
||||
for counter, setting in enumerate(settings):
|
||||
is_verified = self._verify_setting_entry(setting,
|
||||
DEFAULT_SETTINGS[counter][0],
|
||||
DEFAULT_SETTINGS[counter][1])
|
||||
is_verified = self._verify_setting_entry(
|
||||
setting,
|
||||
DEFAULT_SETTINGS[counter][0],
|
||||
DEFAULT_SETTINGS[counter][1]
|
||||
)
|
||||
|
||||
if not is_verified:
|
||||
return is_verified
|
||||
@@ -396,9 +407,8 @@ class VideoComponentPage(VideoPage):
|
||||
if field_value != current_value:
|
||||
return False
|
||||
|
||||
# Clear button should be visible(active class is present) for
|
||||
# every setting that don't have 'metadata-videolist-enum' class
|
||||
if 'metadata-videolist-enum' not in setting.get_attribute('class'):
|
||||
# Verify if clear button is active for expected video fields
|
||||
if field_name not in FIELDS_WO_CLEAR and 'metadata-videolist-enum' not in setting.get_attribute('class'):
|
||||
setting_clear_button = setting.find_elements_by_class_name('setting-clear')[0]
|
||||
if 'active' not in setting_clear_button.get_attribute('class'):
|
||||
return False
|
||||
@@ -514,8 +524,8 @@ class VideoComponentPage(VideoPage):
|
||||
list: list of translation language codes
|
||||
|
||||
"""
|
||||
translations_selector = '.metadata-video-translations .remove-setting'
|
||||
return self.q(css=translations_selector).attrs('data-lang')
|
||||
translations_selector = '.metadata-video-translations .list-settings-item'
|
||||
return self.q(css=translations_selector).attrs('data-original-lang')
|
||||
|
||||
def download_translation(self, language_code, text_to_search):
|
||||
"""
|
||||
@@ -530,7 +540,7 @@ class VideoComponentPage(VideoPage):
|
||||
|
||||
"""
|
||||
mime_type = 'application/x-subrip'
|
||||
lang_code = '/{}?'.format(language_code)
|
||||
lang_code = '?language_code={}'.format(language_code)
|
||||
link = [link for link in self.q(css='.download-action').attrs('href') if lang_code in link]
|
||||
result, headers, content = self._get_transcript(link[0])
|
||||
|
||||
@@ -544,7 +554,9 @@ class VideoComponentPage(VideoPage):
|
||||
language_code (str): language code
|
||||
|
||||
"""
|
||||
self.q(css='.remove-action').filter(lambda el: language_code == el.get_attribute('data-lang')).click()
|
||||
selector = '.metadata-video-translations .list-settings-item'
|
||||
translation = self.q(css=selector).filter(lambda el: language_code == el.get_attribute('data-original-lang'))
|
||||
translation[0].find_element_by_class_name('remove-action').click()
|
||||
|
||||
@property
|
||||
def upload_status_message(self):
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
"""
|
||||
Acceptance tests for CMS Video Editor.
|
||||
"""
|
||||
import ddt
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from common.test.acceptance.pages.common.utils import confirm_prompt
|
||||
from common.test.acceptance.tests.video.test_studio_video_module import CMSVideoBaseTest
|
||||
|
||||
|
||||
@attr(shard=6)
|
||||
@ddt.ddt
|
||||
class VideoEditorTest(CMSVideoBaseTest):
|
||||
"""
|
||||
CMS Video Editor Test Class
|
||||
@@ -263,6 +265,7 @@ class VideoEditorTest(CMSVideoBaseTest):
|
||||
self.open_advanced_tab()
|
||||
self.assertEqual(self.video.translations(), ['zh', 'uk'])
|
||||
self.video.remove_translation('uk')
|
||||
confirm_prompt(self.video)
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
unicode_text = "好 各位同学".decode('utf-8')
|
||||
@@ -271,6 +274,7 @@ class VideoEditorTest(CMSVideoBaseTest):
|
||||
self.open_advanced_tab()
|
||||
self.assertEqual(self.video.translations(), ['zh'])
|
||||
self.video.remove_translation('zh')
|
||||
confirm_prompt(self.video)
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
|
||||
@@ -292,69 +296,27 @@ class VideoEditorTest(CMSVideoBaseTest):
|
||||
self.video.upload_translation('uk_transcripts.srt', 'uk')
|
||||
self.assertEqual(self.video.translations(), ['uk'])
|
||||
self.video.remove_translation('uk')
|
||||
confirm_prompt(self.video)
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
|
||||
def test_translations_clearing_works_w_saving(self):
|
||||
def test_translations_entry_remove_works(self):
|
||||
"""
|
||||
Scenario: Translations clearing works correctly w/ preliminary saving
|
||||
Scenario: Translations entry removal works correctly when transcript is not uploaded
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
And I click button "Clear"
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
And I click on "+ Add" button for "Transcript Languages" field
|
||||
Then I click on "Remove" button
|
||||
And I see newly created entry is removed
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
self.open_advanced_tab()
|
||||
self.video.upload_translation('uk_transcripts.srt', 'uk')
|
||||
self.video.upload_translation('chinese_transcripts.srt', 'zh')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
unicode_text = "Привіт, edX вітає вас.".decode('utf-8')
|
||||
self.assertIn(unicode_text, self.video.captions_text)
|
||||
self.assertEqual(self.video.caption_languages.keys(), ['zh', 'uk'])
|
||||
self.edit_component()
|
||||
self.open_advanced_tab()
|
||||
self.assertEqual(self.video.translations(), ['zh', 'uk'])
|
||||
self.video.click_button('translations_clear')
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
|
||||
def test_translations_clearing_works_wo_saving(self):
|
||||
"""
|
||||
Scenario: Translations clearing works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I click button "Clear"
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
self.open_advanced_tab()
|
||||
self.video.upload_translation('uk_transcripts.srt', 'uk')
|
||||
self.video.upload_translation('chinese_transcripts.srt', 'zh')
|
||||
self.video.click_button('translations_clear')
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
self.video.click_button("translation_add")
|
||||
self.assertEqual(self.video.translations_count(), 1)
|
||||
self.video.remove_translation("")
|
||||
self.assertEqual(self.video.translations_count(), 0)
|
||||
|
||||
def test_cannot_upload_sjson_translation(self):
|
||||
"""
|
||||
@@ -455,6 +417,7 @@ class VideoEditorTest(CMSVideoBaseTest):
|
||||
self.video.upload_translation('chinese_transcripts.srt', 'zh')
|
||||
self.assertEqual(self.video.translations(), ['zh'])
|
||||
self.video.remove_translation('zh')
|
||||
confirm_prompt(self.video)
|
||||
self.video.upload_translation('uk_transcripts.srt', 'zh')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
@@ -46,6 +46,7 @@ class CMSVideoBaseTest(UniqueCourseTest):
|
||||
)
|
||||
|
||||
self.assets = []
|
||||
self.metadata = None
|
||||
self.addCleanup(YouTubeStubConfig.reset)
|
||||
|
||||
def _create_course_unit(self, youtube_stub_config=None, subtitles=False):
|
||||
@@ -87,6 +88,7 @@ class CMSVideoBaseTest(UniqueCourseTest):
|
||||
Create a user and make that user a course author
|
||||
Log the user into studio
|
||||
"""
|
||||
|
||||
if self.assets:
|
||||
self.course_fixture.add_asset(self.assets)
|
||||
|
||||
@@ -95,7 +97,7 @@ class CMSVideoBaseTest(UniqueCourseTest):
|
||||
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
||||
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
||||
XBlockFixtureDesc('video', 'Video')
|
||||
XBlockFixtureDesc('video', 'Video', metadata=self.metadata)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -123,40 +123,6 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertFalse(self.video.is_transcript_button_visible('import'))
|
||||
self.assertTrue(self.video.is_transcript_button_visible('disabled_download_to_edit'))
|
||||
|
||||
def test_youtube_id_w_found_state(self):
|
||||
"""
|
||||
Scenario: Youtube id only: check "Found" state
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "Timed Transcript Found"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('http://youtu.be/t_not_exist', 1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't_not_exist'))
|
||||
|
||||
def test_youtube_id_w_same_local_server_subs(self):
|
||||
"""
|
||||
Scenario: Youtube id only: check "Found" state when user sets youtube_id with same local and server subs
|
||||
Given I have created a Video component with subtitles "t__eq_exist"
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
And I see status message "Timed Transcript Found"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t__eq_exist')
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('http://youtu.be/t__eq_exist', 1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't__eq_exist'))
|
||||
|
||||
def test_youtube_id_w_different_local_server_sub(self):
|
||||
"""
|
||||
Scenario: Youtube id only: check "Found" state when user sets youtube_id with different local and server subs
|
||||
@@ -167,7 +133,7 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
And I see button "replace"
|
||||
And I click transcript button "replace"
|
||||
And I see status message "Timed Transcript Found"
|
||||
And I see value "t_neq_exist" in the field "Default Timed Transcript"
|
||||
Then I save video component And captions are visible.
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_neq_exist')
|
||||
self.edit_component()
|
||||
@@ -177,8 +143,8 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertTrue(self.video.is_transcript_button_visible('replace'))
|
||||
self.video.click_button_subtitles()
|
||||
self.video.wait_for_message('status', 'Timed Transcript Found')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't_neq_exist'))
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
def test_html5_source_w_not_found_state(self):
|
||||
"""
|
||||
@@ -187,54 +153,12 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "No Timed Transcript"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('t_not_exist.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', ''))
|
||||
|
||||
def test_html5_source_w_found_state(self):
|
||||
"""
|
||||
Scenario: html5 source only: check "Found" state
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "Timed Transcript Found"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('t_not_exist.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't_not_exist'))
|
||||
|
||||
def test_set_youtube_id_wo_server(self):
|
||||
"""
|
||||
Scenario: User sets youtube_id w/o server but with local subs and one html5 link w/o subs
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
|
||||
urls = ['http://youtu.be/t_not_exist', 'test_video_name.mp4']
|
||||
for each url in urls do the following
|
||||
Enter `url` to field number n
|
||||
Status message "Timed Transcript Found" is shown
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
urls = ['http://youtu.be/t_not_exist', 'test_video_name.mp4']
|
||||
for index, url in enumerate(urls, 1):
|
||||
self.video.set_url_field(url, index)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't_not_exist'))
|
||||
|
||||
def test_set_youtube_id_wo_local(self):
|
||||
"""
|
||||
@@ -467,7 +391,6 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "Timed Transcript Uploaded Successfully"
|
||||
`download_to_edit` and `upload_new_timed_transcripts` buttons are shown
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 2
|
||||
Then I see status message "Timed Transcript Found"
|
||||
@@ -487,9 +410,6 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('download_to_edit'))
|
||||
self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts'))
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't__eq_exist'))
|
||||
self.open_basic_tab()
|
||||
|
||||
self.video.set_url_field('http://youtu.be/t_not_exist', 2)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
@@ -502,40 +422,50 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
def test_two_html5_sources_w_transcripts(self):
|
||||
"""
|
||||
Scenario: Enter 2 HTML5 sources with transcripts, they are not the same, choose
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
Given I have created a Video component and subtitles "t_not_exist" and "t__eq_exist"
|
||||
are present in contentstore.
|
||||
|
||||
And I enter a "uk_transcripts.mp4" source to field number 1
|
||||
Then I see status message "No Timed Transcript"
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "Timed Transcript Found"
|
||||
`download_to_edit` and `upload_new_timed_transcripts` buttons are shown
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "Timed Transcript Uploaded Successfully"
|
||||
And I see value "uk_transcripts" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 2
|
||||
And I enter a "t__eq_exist.webm" source to field number 2
|
||||
Then I see status message "Timed Transcript Conflict"
|
||||
`Timed Transcript from uk_transcripts.mp4` and `Timed Transcript from t_not_exist.webm` buttons are shown
|
||||
And I click transcript button "Timed Transcript from t_not_exist.webm"
|
||||
And I see value "uk_transcripts|t_not_exist" in the field "Default Timed Transcript"
|
||||
`Timed Transcript from t_not_exist.mp4` and `Timed Transcript from t__eq_exist.webm` buttons are shown
|
||||
And I click transcript button "Timed Transcript from t_not_exist.mp4"
|
||||
Then I see status message "Timed Transcript Found" And I save video component
|
||||
Then I see that the captions are visible.
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
# Setup a course, navigate to the unit containing
|
||||
# video component and edit the video component.
|
||||
self.assets.append('subs_t_not_exist.srt.sjson')
|
||||
self.assets.append('subs_t__eq_exist.srt.sjson')
|
||||
self.navigate_to_course_unit()
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('uk_transcripts.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.set_url_field('t_not_exist.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('download_to_edit'))
|
||||
self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts'))
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'uk_transcripts'))
|
||||
self.open_basic_tab()
|
||||
|
||||
self.video.set_url_field('t_not_exist.webm', 2)
|
||||
self.video.set_url_field('t__eq_exist.webm', 2)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Conflict')
|
||||
self.assertTrue(
|
||||
self.video.is_transcript_button_visible('choose', button_text='Timed Transcript from uk_transcripts.mp4'))
|
||||
self.assertTrue(self.video.is_transcript_button_visible('choose', index=1,
|
||||
button_text='Timed Transcript from t_not_exist.webm'))
|
||||
|
||||
self.assertTrue(self.video.is_transcript_button_visible(
|
||||
'choose',
|
||||
button_text='Timed Transcript from t__eq_exist.webm'
|
||||
))
|
||||
self.assertTrue(self.video.is_transcript_button_visible(
|
||||
'choose',
|
||||
index=1,
|
||||
button_text='Timed Transcript from t_not_exist.mp4'
|
||||
))
|
||||
|
||||
self.video.click_button('choose', index=1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.save_unit_settings()
|
||||
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
def test_one_field_only(self):
|
||||
"""
|
||||
@@ -547,20 +477,21 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
|
||||
If i enter "t_not_exist.mp4" source to field number 1 Then I see status message "Timed Transcript Found"
|
||||
`download_to_edit` and `upload_new_timed_transcripts` buttons are shown
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
And I save changes And then edit the component
|
||||
|
||||
If i enter "video_name_2.mp4" source to field number 1 Then I see status message "Confirm Timed Transcript"
|
||||
I see button "use_existing" And I click on it
|
||||
And I see value "video_name_2" in the field "Default Timed Transcript"
|
||||
I see button "use_existing"
|
||||
|
||||
If i enter "video_name_3.mp4" source to field number 1 Then I see status message "Confirm Timed Transcript"
|
||||
And I see button "use_existing"
|
||||
|
||||
If i enter a "video_name_4.mp4" source to field number 1 Then I see status message "Confirm Timed Transcript"
|
||||
I see button "use_existing" And I click on it
|
||||
And I see value "video_name_4" in the field "Default Timed Transcript"
|
||||
I see button "use_existing" And I click on it And I see status message "Timed Transcript Found"
|
||||
|
||||
I save video component And see that the captions are visible
|
||||
Then I edit video component And I see status message "Timed Transcript Found"
|
||||
"""
|
||||
self.metadata = {'sub': 't_not_exist'}
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
@@ -568,20 +499,13 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('download_to_edit'))
|
||||
self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts'))
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 't_not_exist'))
|
||||
self.open_basic_tab()
|
||||
self.save_unit_settings()
|
||||
self.edit_component()
|
||||
|
||||
self.edit_component()
|
||||
self.video.set_url_field('video_name_2.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'Confirm Timed Transcript')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('use_existing'))
|
||||
self.video.click_button('use_existing')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts'))
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_2'))
|
||||
self.open_basic_tab()
|
||||
|
||||
self.video.set_url_field('video_name_3.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'Confirm Timed Transcript')
|
||||
@@ -591,8 +515,12 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'Confirm Timed Transcript')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('use_existing'))
|
||||
self.video.click_button('use_existing')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_4'))
|
||||
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
def test_two_fields_only(self):
|
||||
"""
|
||||
@@ -615,8 +543,12 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
Then I see status message "Confirm Timed Transcript"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_3" in the field "Default Timed Transcript"
|
||||
And I see status message "Timed Transcript Found"
|
||||
|
||||
I save video component And see that the captions are visible
|
||||
Then I edit video component And I see status message "Timed Transcript Found"
|
||||
"""
|
||||
self.metadata = {'sub': 't_not_exist'}
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
@@ -635,36 +567,45 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'Confirm Timed Transcript')
|
||||
self.assertTrue(self.video.is_transcript_button_visible('use_existing'))
|
||||
self.video.click_button('use_existing')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_3'))
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
self.save_unit_settings()
|
||||
self.video.is_captions_visible()
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
def test_upload_subtitles(self):
|
||||
"""
|
||||
Scenario: File name and name of subs are different (Uploading subtitles with different file name than file)
|
||||
Scenario: Transcript upload for a video who has Video ID set on it.
|
||||
Given I have created a Video component
|
||||
|
||||
And I enter a "video_name_1.mp4" source to field number 1
|
||||
I enter a "video_name_1.mp4" source to field number 1
|
||||
And set "Video ID" to "video_001"
|
||||
And I see status message "No Timed Transcript"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "Timed Transcript Uploaded Successfully"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I edit the component
|
||||
Then I see status message "Timed Transcript Found"
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
|
||||
self.edit_component()
|
||||
self.video.set_field_value('Video ID', 'video_001')
|
||||
self.save_unit_settings()
|
||||
|
||||
self.edit_component()
|
||||
self.video.set_url_field('video_name_1.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_1'))
|
||||
self.save_unit_settings()
|
||||
self.video.is_captions_visible()
|
||||
|
||||
self.edit_component()
|
||||
self.video.verify_field_value('Video ID', 'video_001')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
def test_video_wo_subtitles(self):
|
||||
@@ -684,37 +625,6 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.video.set_url_field('video_name_1.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
|
||||
def test_subtitles_copy(self):
|
||||
"""
|
||||
Scenario: Subtitles are copied for every html5 video source
|
||||
Given I have created a Video component
|
||||
|
||||
After I enter a "video_name_1.mp4" source to field number 1 Then I see status message "No Timed Transcript"
|
||||
|
||||
After I enter a "video_name_2.webm" source to field number 2 Then I see status message "No Timed Transcript"
|
||||
After uploading transcript "uk_transcripts.srt" I should see message "Timed Transcript Uploaded Successfully"
|
||||
And I see value "video_name_2" in the field "Default Timed Transcript"
|
||||
When I clear field number 1 Then I see status message "Timed Transcript Found"
|
||||
And I see value "video_name_2" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('video_name_1.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
|
||||
self.video.set_url_field('video_name_2.webm', 2)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_2'))
|
||||
self.open_basic_tab()
|
||||
self.video.clear_field(1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_2'))
|
||||
|
||||
def test_upload_button_w_youtube(self):
|
||||
"""
|
||||
Scenario: Upload button for single youtube id
|
||||
@@ -736,6 +646,7 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
@@ -751,8 +662,7 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
After I upload the transcripts file "uk_transcripts.srt"I see message "Timed Transcript Uploaded Successfully"
|
||||
When I clear field number 1 Then I see status message "Timed Transcript Found"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
After saving the changes video captions should be visible
|
||||
After saving the changes video captions are visible
|
||||
When I edit the component Then I see status message "Timed Transcript Found"
|
||||
"""
|
||||
self._create_video_component()
|
||||
@@ -767,39 +677,15 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts'))
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
# Removing a source from "Video URL" field will make an ajax call to `check_transcripts`.
|
||||
self.video.clear_field(1)
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_1'))
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
def test_advanced_tab_transcript_fields(self):
|
||||
"""
|
||||
Scenario: Change transcripts field in Advanced tab
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
|
||||
After I enter a "video_name_1.mp4" source to field number 1 Then I see status message "No Timed Transcript"
|
||||
Open tab "Advanced" and set value "t_not_exist" to the field "Default Timed Transcript"
|
||||
After saving the changes video captions should be visible
|
||||
When I edit the component Then I see status message "Timed Transcript Found"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('video_name_1.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.open_advanced_tab()
|
||||
self.video.set_field_value('Default Timed Transcript', 't_not_exist')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_1'))
|
||||
|
||||
def test_non_ascii_transcripts(self):
|
||||
"""
|
||||
Scenario: Check non-ascii (chinese) transcripts
|
||||
@@ -819,175 +705,13 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
def test_module_metadata_save(self):
|
||||
"""
|
||||
Scenario: Check saving module metadata on switching between tabs
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
|
||||
After I enter a "video_name_1.mp4" source to field number 1 I should see status message "No Timed Transcript"
|
||||
Open tab "Advanced" and set value "t_not_exist" to the field "Default Timed Transcript"
|
||||
When I open tab "Basic" Then I see status message "Timed Transcript Found"
|
||||
After saving the changes video captions should be visible
|
||||
When I edit the component I should see status message "Timed Transcript Found"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('video_name_1.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.open_advanced_tab()
|
||||
self.video.set_field_value('Default Timed Transcript', 't_not_exist')
|
||||
self.open_basic_tab()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_1'))
|
||||
|
||||
def test_clearing_transcripts_wo_save(self):
|
||||
"""
|
||||
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible w/o saving
|
||||
Given I have created a Video component
|
||||
|
||||
After I enter a "t_not_exist.mp4" source to field number 1 I should see status message "No Timed Transcript"
|
||||
After uploading transcripts "chinese_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
Open tab "Advanced" and set value "" to the field "Default Timed Transcript"
|
||||
When I open tab "Basic" I see status message "No Timed Transcript"
|
||||
After saving the changes video captions should not be visible
|
||||
When I edit the component I should see status message "No Timed Transcript"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('t_not_exist.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('chinese_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.video.set_field_value('Default Timed Transcript', '')
|
||||
self.open_basic_tab()
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', ''))
|
||||
|
||||
def test_clearing_transcripts_w_save(self):
|
||||
"""
|
||||
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible with saving
|
||||
Given I have created a Video component
|
||||
|
||||
After I enter a "t_not_exist.mp4" source to field number 1 I see status message "No Timed Transcript"
|
||||
After uploading the transcripts "chinese_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
After saving changes I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
Open tab "Advanced" I set value "" to the field "Default Timed Transcript"
|
||||
When I open tab "Basic" I see status message "No Timed Transcript"
|
||||
After saving the changes video captions should not be visible
|
||||
After I edit the component I should see status message "No Timed Transcript"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('t_not_exist.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('chinese_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.save_unit_settings()
|
||||
unicode_text = "好 各位同学".decode('utf-8')
|
||||
self.assertIn(unicode_text, self.video.captions_text)
|
||||
self.edit_component()
|
||||
self.open_advanced_tab()
|
||||
self.video.set_field_value('Default Timed Transcript', '')
|
||||
self.open_basic_tab()
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', ''))
|
||||
|
||||
def test_video_w_existing_subtitles(self):
|
||||
"""
|
||||
Scenario: Video with existing subs - Advanced tab - change to another one subs -
|
||||
Basic tab - Found message - Save - see correct subs
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
|
||||
After I enter a "video_name_1.mp4" source to field number 1 I see status message "No Timed Transcript"
|
||||
After uploading the transcripts "chinese_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
After saving the changes video captions should be visible
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
Open tab "Advanced" And set value "t_not_exist" to the field "Default Timed Transcript"
|
||||
When I open tab "Basic" I should see status message "Timed Transcript Found"
|
||||
After saving the changes video captions should be visible
|
||||
And I see "LILA FISHER: Hi, welcome to Edx." text in the captions
|
||||
"""
|
||||
self._create_video_component(subtitles=True, subtitle_id='t_not_exist')
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('video_name_1.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('chinese_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
unicode_text = "好 各位同学".decode('utf-8')
|
||||
self.assertIn(unicode_text, self.video.captions_text)
|
||||
self.edit_component()
|
||||
self.open_advanced_tab()
|
||||
self.video.set_field_value('Default Timed Transcript', 't_not_exist')
|
||||
self.open_basic_tab()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
self.assertIn('LILA FISHER: Hi, welcome to Edx.', self.video.captions_text)
|
||||
|
||||
def test_reverting_transcripts(self):
|
||||
"""
|
||||
Scenario: After reverting Transcripts field in the Advanced tab "not found" message should be visible
|
||||
Given I have created a Video component
|
||||
|
||||
After I enter a "t_not_exist.mp4" source to field number 1 Then I see status message "No Timed Transcript"
|
||||
After uploading transcripts "chinese_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
After saving the changes I should see "好 各位同学" text in the captions
|
||||
After I edit the component I open tab "Advanced"
|
||||
And I revert the transcript field "Default Timed Transcript"
|
||||
After saving the changes video captions should not be visible
|
||||
After I edit the component I should see status message "No Timed Transcript"
|
||||
"""
|
||||
self._create_video_component()
|
||||
self.edit_component()
|
||||
|
||||
self.video.set_url_field('t_not_exist.mp4', 1)
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('chinese_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.save_unit_settings()
|
||||
unicode_text = "好 各位同学".decode('utf-8')
|
||||
self.assertIn(unicode_text, self.video.captions_text)
|
||||
self.edit_component()
|
||||
self.open_advanced_tab()
|
||||
self.video.revert_field('Default Timed Transcript')
|
||||
self.save_unit_settings()
|
||||
self.assertFalse(self.video.is_captions_visible())
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
|
||||
def test_upload_subtitles_w_different_names2(self):
|
||||
"""
|
||||
Scenario: File name and name of subs are different -- Uploading subtitles for file with periods
|
||||
in it should properly set the transcript name and keep the periods
|
||||
Scenario: Uploading subtitles for file with periods in it does not effect the uploaded transcript in anyway
|
||||
Given I have created a Video component
|
||||
|
||||
After I enter a "video_name_1.1.2.mp4" source to field number 1, I see status message "No Timed Transcript"
|
||||
After I upload the transcripts file "uk_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
And I see value "video_name_1.1.2" in the field "Default Timed Transcript"
|
||||
After saving the changes video captions should be visible
|
||||
After I edit the component I should see status message "Timed Transcript Found"
|
||||
"""
|
||||
@@ -998,21 +722,19 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'video_name_1.1.2'))
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
def test_upload_subtitles_w_different_names3(self):
|
||||
"""
|
||||
Scenario: Shortened link: File name and name of subs are different
|
||||
Given I have created a Video component
|
||||
Scenario: Shortened link: Shortened link to the source does not effect the uploaded
|
||||
transcript, given I have created a Video component
|
||||
|
||||
After I enter a "http://goo.gl/pxxZrg" source to field number 1 Then I see status message "No Timed Transcript"
|
||||
After I upload the transcripts file "uk_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
And I see value "pxxZrg" in the field "Default Timed Transcript"
|
||||
After saving the changes video captions should be visible
|
||||
After I edit the component I should see status message "Timed Transcript Found"
|
||||
"""
|
||||
@@ -1023,21 +745,19 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'pxxZrg'))
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
def test_upload_subtitles_w_different_names4(self):
|
||||
"""
|
||||
Scenario: Relative link: File name and name of subs are different
|
||||
Given I have created a Video component
|
||||
Scenario: Relative link: Relative link to the source does not effect the uploaded
|
||||
transcript, given I have created a Video component
|
||||
|
||||
After i enter a "/gizmo.webm" source to field number 1 Then I see status message "No Timed Transcript"
|
||||
After I upload the transcripts file "uk_transcripts.srt" I see message "Timed Transcript Uploaded Successfully"
|
||||
And I see value "gizmo" in the field "Default Timed Transcript"
|
||||
After saving the changes video captions should be visible
|
||||
After I edit the component I should see status message "Timed Transcript Found"
|
||||
"""
|
||||
@@ -1048,9 +768,8 @@ class VideoTranscriptTest(CMSVideoBaseTest):
|
||||
self.assertEqual(self.video.message('status'), 'No Timed Transcript')
|
||||
self.video.upload_transcript('uk_transcripts.srt')
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully')
|
||||
self.open_advanced_tab()
|
||||
self.assertTrue(self.video.verify_field_value('Default Timed Transcript', 'gizmo'))
|
||||
self.save_unit_settings()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
self.edit_component()
|
||||
self.assertEqual(self.video.message('status'), 'Timed Transcript Found')
|
||||
|
||||
@@ -9,6 +9,7 @@ from datetime import timedelta
|
||||
|
||||
import ddt
|
||||
import freezegun
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from mock import MagicMock, Mock, patch
|
||||
from nose.plugins.attrib import attr
|
||||
@@ -21,12 +22,19 @@ from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.video_module.transcripts_utils import TranscriptException, TranscriptsGenerationException
|
||||
from xmodule.video_module.transcripts_utils import (
|
||||
Transcript,
|
||||
edxval_api,
|
||||
subs_filename,
|
||||
)
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
from edxval import api
|
||||
|
||||
from .helpers import BaseTestXmodule
|
||||
from .test_video_xml import SOURCE_XML
|
||||
|
||||
|
||||
TRANSCRIPT = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
BUMPER_TRANSCRIPT = {"start": [1], "end": [10], "text": ["A bumper"]}
|
||||
SRT_content = textwrap.dedent("""
|
||||
@@ -242,7 +250,17 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['uk'])
|
||||
|
||||
def test_multiple_available_translations(self):
|
||||
@patch('xmodule.video_module.transcripts_utils.get_video_transcript_content')
|
||||
def test_multiple_available_translations(self, mock_get_video_transcript_content):
|
||||
mock_get_video_transcript_content.return_value = {
|
||||
'content': json.dumps({
|
||||
"start": [10],
|
||||
"end": [100],
|
||||
"text": ["Hi, welcome to Edx."],
|
||||
}),
|
||||
'file_name': 'edx.sjson'
|
||||
}
|
||||
|
||||
good_sjson = _create_file(json.dumps(self.subs))
|
||||
|
||||
# Upload english transcript.
|
||||
@@ -252,12 +270,13 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
|
||||
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
self.item.edx_video_id = 'an-edx-video-id'
|
||||
|
||||
request = Request.blank('/available_translations')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['en', 'uk'])
|
||||
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.transcripts_utils.get_video_transcript_content')
|
||||
@patch('xmodule.video_module.transcripts_utils.get_available_transcript_languages')
|
||||
@ddt.data(
|
||||
(
|
||||
@@ -288,11 +307,19 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
'uk': True,
|
||||
'ro': False,
|
||||
},
|
||||
['en', 'uk', 'de']
|
||||
['en', 'uk', 'de', 'ro']
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_val_available_translations(self, val_transcripts, sub, transcripts, result, mock_get_transcript_languages):
|
||||
def test_val_available_translations(
|
||||
self,
|
||||
val_transcripts,
|
||||
sub,
|
||||
transcripts,
|
||||
result,
|
||||
mock_get_transcript_languages,
|
||||
mock_get_video_transcript_content
|
||||
):
|
||||
"""
|
||||
Tests available translations with video component's and val's transcript languages
|
||||
while the feature is enabled.
|
||||
@@ -309,18 +336,23 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
_upload_sjson_file(sjson_transcript, self.item_descriptor.location)
|
||||
sub = _get_subs_id(sjson_transcript.name)
|
||||
|
||||
mock_get_video_transcript_content.return_value = {
|
||||
'content': json.dumps({
|
||||
"start": [10],
|
||||
"end": [100],
|
||||
"text": ["Hi, welcome to Edx."],
|
||||
}),
|
||||
'file_name': 'edx.sjson'
|
||||
}
|
||||
mock_get_transcript_languages.return_value = val_transcripts
|
||||
self.item.transcripts = transcripts
|
||||
self.item.sub = sub
|
||||
self.item.edx_video_id = 'an-edx-video-id'
|
||||
# Make request to available translations dispatch.
|
||||
request = Request.blank('/available_translations')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertItemsEqual(json.loads(response.body), result)
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=False),
|
||||
)
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages')
|
||||
def test_val_available_translations_feature_disabled(self, mock_get_available_transcript_languages):
|
||||
"""
|
||||
@@ -372,16 +404,12 @@ class TestTranscriptAvailableTranslationsBumperDispatch(TestVideo):
|
||||
response = self.item.transcript(request=request, dispatch=self.dispatch)
|
||||
self.assertEqual(json.loads(response.body), [lang])
|
||||
|
||||
@ddt.data(True, False)
|
||||
@patch('xmodule.video_module.transcripts_utils.get_available_transcript_languages')
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled')
|
||||
def test_multiple_available_translations(self, feature_enabled,
|
||||
mock_val_video_transcript_feature, mock_get_transcript_languages):
|
||||
def test_multiple_available_translations(self, mock_get_transcript_languages):
|
||||
"""
|
||||
Verify that the available translations dispatch works as expected for multiple translations with
|
||||
or without enabling the edx-val video transcripts feature.
|
||||
Verify that available translations dispatch works as expected for multiple
|
||||
translations and returns both content store and edxval translations.
|
||||
"""
|
||||
mock_val_video_transcript_feature.return_value = feature_enabled
|
||||
# Assuming that edx-val has German translation available for this video component.
|
||||
mock_get_transcript_languages.return_value = ['de']
|
||||
en_translation = _create_srt_file()
|
||||
@@ -435,7 +463,10 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'test_filename.srt', 'application/x-subrip; charset=utf-8'))
|
||||
@patch(
|
||||
'xmodule.video_module.video_handlers.get_transcript',
|
||||
return_value=('Subs!', 'test_filename.srt', 'application/x-subrip; charset=utf-8')
|
||||
)
|
||||
def test_download_srt_exist(self, __):
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
@@ -443,7 +474,10 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Language'], 'en')
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'txt', 'text/plain; charset=utf-8'))
|
||||
@patch(
|
||||
'xmodule.video_module.video_handlers.get_transcript',
|
||||
return_value=('Subs!', 'txt', 'text/plain; charset=utf-8')
|
||||
)
|
||||
def test_download_txt_exist(self, __):
|
||||
self.item.transcript_format = 'txt'
|
||||
request = Request.blank('/download')
|
||||
@@ -460,16 +494,18 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript(transcripts)
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', u"塞.srt", 'application/x-subrip; charset=utf-8'))
|
||||
@patch(
|
||||
'xmodule.video_module.transcripts_utils.get_transcript_for_video',
|
||||
return_value=(Transcript.SRT, u"塞", 'Subs!')
|
||||
)
|
||||
def test_download_non_en_non_ascii_filename(self, __):
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.body, 'Subs!')
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="塞.srt"')
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="en_塞.srt"')
|
||||
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data')
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', Mock(side_effect=NotFoundError))
|
||||
def test_download_fallback_transcript(self, mock_get_video_transcript_data):
|
||||
"""
|
||||
@@ -502,21 +538,6 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
for attribute, value in expected_headers.iteritems():
|
||||
self.assertEqual(response.headers[attribute], value)
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=False),
|
||||
)
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', Mock(side_effect=NotFoundError))
|
||||
def test_download_fallback_transcript_feature_disabled(self):
|
||||
"""
|
||||
Verify val transcript if its feature is disabled.
|
||||
"""
|
||||
# Make request to XModule transcript handler
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
# Assert the actual response
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
@ddt.ddt
|
||||
@@ -600,6 +621,7 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
# youtube 1_0 request, will generate for all speeds for existing ids
|
||||
self.item.youtube_id_1_0 = subs_id
|
||||
self.item.youtube_id_0_75 = '0_75'
|
||||
self.store.update_item(self.item, self.user.id)
|
||||
request = Request.blank('/translation/uk?videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
@@ -614,9 +636,11 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
|
||||
]
|
||||
}
|
||||
|
||||
self.assertDictEqual(json.loads(response.body), calculated_0_75)
|
||||
# 1_5 will be generated from 1_0
|
||||
self.item.youtube_id_1_5 = '1_5'
|
||||
self.store.update_item(self.item, self.user.id)
|
||||
request = Request.blank('/translation/uk?videoId={}'.format('1_5'))
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
calculated_1_5 = {
|
||||
@@ -638,6 +662,7 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
attach(self.item, subs_id)
|
||||
self.store.update_item(self.item, self.user.id)
|
||||
request = Request.blank(url)
|
||||
response = self.item.transcript(request=request, dispatch=dispatch)
|
||||
self.assertDictEqual(json.loads(response.body), TRANSCRIPT)
|
||||
@@ -752,7 +777,6 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
store.update_item(self.course, self.user.id)
|
||||
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data')
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.VideoModule.translation', Mock(side_effect=NotFoundError))
|
||||
@patch('xmodule.video_module.VideoModule.get_static_transcript', Mock(return_value=Response(status=404)))
|
||||
def test_translation_fallback_transcript(self, mock_get_video_transcript_data):
|
||||
@@ -785,10 +809,6 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
for attribute, value in expected_headers.iteritems():
|
||||
self.assertEqual(response.headers[attribute], value)
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=False),
|
||||
)
|
||||
@patch('xmodule.video_module.VideoModule.translation', Mock(side_effect=NotFoundError))
|
||||
@patch('xmodule.video_module.VideoModule.get_static_transcript', Mock(return_value=Response(status=404)))
|
||||
def test_translation_fallback_transcript_feature_disabled(self):
|
||||
@@ -800,33 +820,6 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
# Assert the actual response
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data')
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled')
|
||||
def test_translations_bumper_transcript(self, feature_enabled,
|
||||
mock_val_video_transcript_feature, mock_get_video_transcript_data):
|
||||
"""
|
||||
Tests that the translations dispatch response remains the same with or without enabling
|
||||
video transcript feature.
|
||||
"""
|
||||
# Mock val api util and return the valid transcript file.
|
||||
transcript = {
|
||||
'content': json.dumps({
|
||||
"start": [10],
|
||||
"end": [100],
|
||||
"text": ["Hi, welcome to Edx."],
|
||||
}),
|
||||
'file_name': 'edx.sjson'
|
||||
}
|
||||
mock_get_video_transcript_data.return_value = transcript
|
||||
|
||||
mock_val_video_transcript_feature.return_value = feature_enabled
|
||||
self.item.video_bumper = {"transcripts": {"en": "unknown.srt.sjson"}}
|
||||
request = Request.blank('/translations/en?is_bumper=1')
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
# Assert that despite the existence of val video transcripts, response remains 404.
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
@@ -851,42 +844,44 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
|
||||
def test_translation_fails(self):
|
||||
# No language
|
||||
request = Request.blank('')
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
request = Request.blank("")
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch="translation")
|
||||
self.assertEqual(response.status, "400 Bad Request")
|
||||
|
||||
# No filename in request.GET
|
||||
request = Request.blank('')
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
# No language_code param in request.GET
|
||||
request = Request.blank("")
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch="translation")
|
||||
self.assertEqual(response.status, "400 Bad Request")
|
||||
self.assertEqual(response.json["error"], "Language is required.")
|
||||
|
||||
# Correct case:
|
||||
filename = os.path.split(self.srt_file.name)[1]
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, filename)
|
||||
request = Request.blank(u"translation?language_code=uk")
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch="translation?language_code=uk")
|
||||
self.srt_file.seek(0)
|
||||
request = Request.blank(u'translation/uk?filename={}'.format(filename))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.body, self.srt_file.read())
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers["Content-Type"], "application/x-subrip; charset=utf-8")
|
||||
self.assertEqual(
|
||||
response.headers['Content-Disposition'],
|
||||
'attachment; filename="{}"'.format(filename)
|
||||
response.headers["Content-Disposition"],
|
||||
'attachment; filename="uk_{}"'.format(filename)
|
||||
)
|
||||
self.assertEqual(response.headers['Content-Language'], 'uk')
|
||||
self.assertEqual(response.headers["Content-Language"], "uk")
|
||||
|
||||
# Non ascii file name download:
|
||||
self.srt_file.seek(0)
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, u'塞.srt')
|
||||
_upload_file(self.srt_file, self.item_descriptor.location, u"塞.srt")
|
||||
request = Request.blank("translation?language_code=zh")
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch="translation?language_code=zh")
|
||||
self.srt_file.seek(0)
|
||||
request = Request.blank('translation/zh?filename={}'.format(u'塞.srt'.encode('utf8')))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/zh')
|
||||
self.assertEqual(response.body, self.srt_file.read())
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="塞.srt"')
|
||||
self.assertEqual(response.headers['Content-Language'], 'zh')
|
||||
self.assertEqual(response.headers["Content-Type"], "application/x-subrip; charset=utf-8")
|
||||
self.assertEqual(response.headers["Content-Disposition"], 'attachment; filename="zh_塞.srt"')
|
||||
self.assertEqual(response.headers["Content-Language"], "zh")
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
@ddt.ddt
|
||||
class TestStudioTranscriptTranslationPostDispatch(TestVideo):
|
||||
"""
|
||||
Test Studio video handler that provide translation transcripts.
|
||||
@@ -909,42 +904,219 @@ class TestStudioTranscriptTranslationPostDispatch(TestVideo):
|
||||
|
||||
METADATA = {}
|
||||
|
||||
def test_studio_transcript_post(self):
|
||||
# Check for exceptons:
|
||||
|
||||
# Language is passed, bad content or filename:
|
||||
|
||||
# should be first, as other tests save transcrips to store.
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', SRT_content)})
|
||||
with patch('xmodule.video_module.video_handlers.save_to_store'):
|
||||
with self.assertRaises(TranscriptException): # transcripts were not saved to store for some reason.
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename', 'content')})
|
||||
with self.assertRaises(TranscriptsGenerationException): # Not an srt filename
|
||||
self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', 'content')})
|
||||
with self.assertRaises(TranscriptsGenerationException): # Content format is not srt.
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', SRT_content.decode('utf8').encode('cp1251'))})
|
||||
# Non-UTF8 file content encoding.
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.body, "Invalid encoding type, transcripts should be UTF-8 encoded.")
|
||||
|
||||
# No language is passed.
|
||||
request = Request.blank('/translation', POST={'file': ('filename', SRT_content)})
|
||||
@ddt.data(
|
||||
{
|
||||
"post_data": {},
|
||||
"error_message": "The following parameters are required: edx_video_id, language_code, new_language_code."
|
||||
},
|
||||
{
|
||||
"post_data": {"edx_video_id": "111", "language_code": "ar", "new_language_code": "ur"},
|
||||
"error_message": 'A transcript with the "ur" language code already exists.'
|
||||
},
|
||||
{
|
||||
"post_data": {"edx_video_id": "111", "language_code": "ur", "new_language_code": "ur"},
|
||||
"error_message": "A transcript file is required."
|
||||
},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_studio_transcript_post_validations(self, post_data, error_message):
|
||||
"""
|
||||
Verify that POST request validations works as expected.
|
||||
"""
|
||||
# mock available_translations method
|
||||
self.item_descriptor.available_translations = lambda transcripts, verify_assets: ['ur']
|
||||
request = Request.blank('/translation', POST=post_data)
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
self.assertEqual(response.json["error"], error_message)
|
||||
|
||||
# Language, good filename and good content.
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', SRT_content)})
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
@ddt.data(
|
||||
{
|
||||
"edx_video_id": "",
|
||||
},
|
||||
{
|
||||
"edx_video_id": "1234-5678-90",
|
||||
},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_studio_transcript_post_w_no_edx_video_id(self, edx_video_id):
|
||||
"""
|
||||
Verify that POST request works as expected
|
||||
"""
|
||||
post_data = {
|
||||
"edx_video_id": edx_video_id,
|
||||
"language_code": "ar",
|
||||
"new_language_code": "uk",
|
||||
"file": ("filename.srt", SRT_content)
|
||||
}
|
||||
|
||||
if edx_video_id:
|
||||
edxval_api.create_video({
|
||||
"edx_video_id": edx_video_id,
|
||||
"status": "uploaded",
|
||||
"client_video_id": "a video",
|
||||
"duration": 0,
|
||||
"encoded_videos": [],
|
||||
"courses": []
|
||||
})
|
||||
|
||||
request = Request.blank('/translation', POST=post_data)
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '201 Created')
|
||||
self.assertDictEqual(json.loads(response.body), {'filename': u'filename.srt', 'status': 'Success'})
|
||||
response = json.loads(response.body)
|
||||
self.assertTrue(response["language_code"], "uk")
|
||||
self.assertDictEqual(self.item_descriptor.transcripts, {})
|
||||
self.assertTrue(_check_asset(self.item_descriptor.location, u'filename.srt'))
|
||||
self.assertTrue(edxval_api.get_video_transcript_data(video_id=response["edx_video_id"], language_code="uk"))
|
||||
|
||||
def test_studio_transcript_post_bad_content(self):
|
||||
"""
|
||||
Verify that transcript content encode/decode errors handled as expected
|
||||
"""
|
||||
post_data = {
|
||||
"edx_video_id": "",
|
||||
"language_code": "ar",
|
||||
"new_language_code": "uk",
|
||||
"file": ("filename.srt", SRT_content.decode("utf8").encode("cp1251"))
|
||||
}
|
||||
|
||||
request = Request.blank("/translation", POST=post_data)
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch="translation")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(
|
||||
response.json["error"],
|
||||
"There is a problem with this transcript file. Try to upload a different file."
|
||||
)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
@ddt.ddt
|
||||
class TestStudioTranscriptTranslationDeleteDispatch(TestVideo):
|
||||
"""
|
||||
Test studio video handler that provide translation transcripts.
|
||||
|
||||
Tests for `translation` dispatch DELETE HTTP method.
|
||||
"""
|
||||
EDX_VIDEO_ID, LANGUAGE_CODE_UK, LANGUAGE_CODE_EN = u'an_edx_video_id', u'uk', u'en'
|
||||
REQUEST_META = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'DELETE'}
|
||||
SRT_FILE = _create_srt_file()
|
||||
|
||||
@ddt.data(
|
||||
{
|
||||
'params': {'lang': 'uk'}
|
||||
},
|
||||
{
|
||||
'params': {'edx_video_id': '12345'}
|
||||
},
|
||||
{
|
||||
'params': {}
|
||||
},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_translation_missing_required_params(self, params):
|
||||
"""
|
||||
Verify that DELETE dispatch works as expected when required args are missing from request
|
||||
"""
|
||||
request = Request(self.REQUEST_META, body=json.dumps(params))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_translation_delete_w_edx_video_id(self):
|
||||
"""
|
||||
Verify that DELETE dispatch works as expected when video has edx_video_id
|
||||
"""
|
||||
request_body = json.dumps({'lang': self.LANGUAGE_CODE_UK, 'edx_video_id': self.EDX_VIDEO_ID})
|
||||
api.create_video({
|
||||
'edx_video_id': self.EDX_VIDEO_ID,
|
||||
'status': 'upload',
|
||||
'client_video_id': 'awesome.mp4',
|
||||
'duration': 0,
|
||||
'encoded_videos': [],
|
||||
'courses': [unicode(self.course.id)]
|
||||
})
|
||||
api.create_video_transcript(
|
||||
video_id=self.EDX_VIDEO_ID,
|
||||
language_code=self.LANGUAGE_CODE_UK,
|
||||
file_format='srt',
|
||||
content=ContentFile(SRT_content)
|
||||
)
|
||||
|
||||
# verify that a video transcript exists for expected data
|
||||
self.assertTrue(api.get_video_transcript_data(video_id=self.EDX_VIDEO_ID, language_code=self.LANGUAGE_CODE_UK))
|
||||
|
||||
request = Request(self.REQUEST_META, body=request_body)
|
||||
self.item_descriptor.edx_video_id = self.EDX_VIDEO_ID
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# verify that a video transcript dose not exist for expected data
|
||||
self.assertFalse(api.get_video_transcript_data(video_id=self.EDX_VIDEO_ID, language_code=self.LANGUAGE_CODE_UK))
|
||||
|
||||
def test_translation_delete_wo_edx_video_id(self):
|
||||
"""
|
||||
Verify that DELETE dispatch works as expected when video has no edx_video_id
|
||||
"""
|
||||
request_body = json.dumps({'lang': self.LANGUAGE_CODE_UK, 'edx_video_id': ''})
|
||||
srt_file_name_uk = subs_filename('ukrainian_translation.srt', lang=self.LANGUAGE_CODE_UK)
|
||||
request = Request(self.REQUEST_META, body=request_body)
|
||||
|
||||
# upload and verify that srt file exists in assets
|
||||
_upload_file(self.SRT_FILE, self.item_descriptor.location, srt_file_name_uk)
|
||||
self.assertTrue(_check_asset(self.item_descriptor.location, srt_file_name_uk))
|
||||
|
||||
# verify transcripts field
|
||||
self.assertNotEqual(self.item_descriptor.transcripts, {})
|
||||
self.assertTrue(self.LANGUAGE_CODE_UK in self.item_descriptor.transcripts)
|
||||
|
||||
# make request and verify response
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# verify that srt file is deleted
|
||||
self.assertEqual(self.item_descriptor.transcripts, {})
|
||||
self.assertFalse(_check_asset(self.item_descriptor.location, srt_file_name_uk))
|
||||
|
||||
def test_translation_delete_w_english_lang(self):
|
||||
"""
|
||||
Verify that DELETE dispatch works as expected for english language translation
|
||||
"""
|
||||
request_body = json.dumps({'lang': self.LANGUAGE_CODE_EN, 'edx_video_id': ''})
|
||||
srt_file_name_en = subs_filename('english_translation.srt', lang=self.LANGUAGE_CODE_EN)
|
||||
self.item_descriptor.transcripts['en'] = 'english_translation.srt'
|
||||
request = Request(self.REQUEST_META, body=request_body)
|
||||
|
||||
# upload and verify that srt file exists in assets
|
||||
_upload_file(self.SRT_FILE, self.item_descriptor.location, srt_file_name_en)
|
||||
self.assertTrue(_check_asset(self.item_descriptor.location, srt_file_name_en))
|
||||
|
||||
# make request and verify response
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# verify that srt file is deleted
|
||||
self.assertTrue(self.LANGUAGE_CODE_EN not in self.item_descriptor.transcripts)
|
||||
self.assertFalse(_check_asset(self.item_descriptor.location, srt_file_name_en))
|
||||
|
||||
def test_translation_delete_w_sub(self):
|
||||
"""
|
||||
Verify that DELETE dispatch works as expected when translation is present against `sub` field
|
||||
"""
|
||||
request_body = json.dumps({'lang': self.LANGUAGE_CODE_EN, 'edx_video_id': ''})
|
||||
sub_file_name = subs_filename(self.item_descriptor.sub, lang=self.LANGUAGE_CODE_EN)
|
||||
request = Request(self.REQUEST_META, body=request_body)
|
||||
|
||||
# sub should not be empy
|
||||
self.assertFalse(self.item_descriptor.sub == u'')
|
||||
|
||||
# upload and verify that srt file exists in assets
|
||||
_upload_file(self.SRT_FILE, self.item_descriptor.location, sub_file_name)
|
||||
self.assertTrue(_check_asset(self.item_descriptor.location, sub_file_name))
|
||||
|
||||
# make request and verify response
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# verify that sub is empty and transcript is deleted also
|
||||
self.assertTrue(self.item_descriptor.sub == u'')
|
||||
self.assertFalse(_check_asset(self.item_descriptor.location, sub_file_name))
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
|
||||
@@ -92,7 +92,6 @@ class TestVideoYouTube(TestVideo):
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': False,
|
||||
'streams': '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
'sources': sources,
|
||||
'duration': None,
|
||||
'poster': None,
|
||||
@@ -174,7 +173,6 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': False,
|
||||
'streams': '1.00:3_yD_cEKoCk',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
'sources': sources,
|
||||
'duration': None,
|
||||
'poster': None,
|
||||
@@ -232,7 +230,6 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'saveStateUrl': '',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'streams': '1.00:3_yD_cEKoCk',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
'sources': '[]',
|
||||
'duration': 111.0,
|
||||
'poster': None,
|
||||
@@ -363,7 +360,6 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
|
||||
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sub': data['sub'],
|
||||
})
|
||||
expected_context.update({
|
||||
'transcript_download_format': (
|
||||
@@ -1249,7 +1245,6 @@ class TestEditorSavedMethod(BaseTestXmodule):
|
||||
# calling editor_saved will generate new file subs_video.srt.sjson for html5_sources
|
||||
item.editor_saved(self.user, old_metadata, None)
|
||||
self.assertIsInstance(Transcript.get_asset(item.location, 'subs_3_yD_cEKoCk.srt.sjson'), StaticContent)
|
||||
self.assertIsInstance(Transcript.get_asset(item.location, 'subs_video.srt.sjson'), StaticContent)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_editor_saved_when_youtube_and_html5_subs_exist(self, default_store):
|
||||
@@ -1494,7 +1489,6 @@ class TestVideoDescriptorStudentViewJson(TestCase):
|
||||
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de']),
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages')
|
||||
def test_student_view_with_val_transcripts_enabled(self, transcripts, english_sub, val_transcripts,
|
||||
expected_transcripts, mock_get_transcript_languages):
|
||||
@@ -1507,21 +1501,6 @@ class TestVideoDescriptorStudentViewJson(TestCase):
|
||||
student_view_response = self.get_result()
|
||||
self.assertItemsEqual(student_view_response['transcripts'].keys(), expected_transcripts)
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=False),
|
||||
)
|
||||
@patch(
|
||||
'xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages',
|
||||
Mock(return_value=['ro', 'es']),
|
||||
)
|
||||
def test_student_view_with_val_transcripts_disabled(self):
|
||||
"""
|
||||
Test `student_view_data` with edx-val transcripts disabled.
|
||||
"""
|
||||
student_view_response = self.get_result()
|
||||
self.assertDictEqual(student_view_response['transcripts'], {self.TEST_LANGUAGE: self.transcript_url})
|
||||
|
||||
|
||||
@attr(shard=7)
|
||||
@ddt.ddt
|
||||
@@ -2076,7 +2055,6 @@ class TestVideoWithBumper(TestVideo):
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': False,
|
||||
'streams': '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
'sources': sources,
|
||||
'poster': None,
|
||||
'duration': None,
|
||||
@@ -2148,7 +2126,6 @@ class TestAutoAdvanceVideo(TestVideo):
|
||||
'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'autoplay': False,
|
||||
'streams': '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
'sources': [u'example.mp4', u'example.webm'],
|
||||
'duration': None,
|
||||
'poster': None,
|
||||
|
||||
@@ -11,7 +11,6 @@ from courseware.module_render import get_module_for_descriptor
|
||||
from util.module_utils import get_dynamic_descriptor_children
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.mongo.base import BLOCK_TYPES_WITH_CHILDREN
|
||||
from xmodule.video_module.transcripts_model_utils import is_val_transcript_feature_enabled_for_course
|
||||
|
||||
|
||||
class BlockOutline(object):
|
||||
@@ -216,12 +215,8 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca
|
||||
size = default_encoded_video.get('file_size', 0)
|
||||
|
||||
# Transcripts...
|
||||
feature_enabled = is_val_transcript_feature_enabled_for_course(course_id)
|
||||
transcripts_info = video_descriptor.get_transcripts_info(include_val_transcripts=feature_enabled)
|
||||
transcript_langs = video_descriptor.available_translations(
|
||||
transcripts=transcripts_info,
|
||||
include_val_transcripts=feature_enabled
|
||||
)
|
||||
transcripts_info = video_descriptor.get_transcripts_info()
|
||||
transcript_langs = video_descriptor.available_translations(transcripts=transcripts_info)
|
||||
|
||||
transcripts = {
|
||||
lang: reverse(
|
||||
|
||||
@@ -922,7 +922,6 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
|
||||
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de']),
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True))
|
||||
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages')
|
||||
def test_val_transcripts_with_feature_enabled(self, transcripts, english_sub, val_transcripts,
|
||||
expected_transcripts, mock_get_transcript_languages):
|
||||
@@ -973,10 +972,6 @@ class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCou
|
||||
self.login_and_enroll()
|
||||
self.api_response(expected_response_code=200, lang='en')
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=True),
|
||||
)
|
||||
@patch(
|
||||
'xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages',
|
||||
Mock(return_value=['uk']),
|
||||
@@ -1009,20 +1004,3 @@ class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCou
|
||||
self.assertEqual(response.content, expected_content)
|
||||
for attribute, value in expected_headers.iteritems():
|
||||
self.assertEqual(response.get(attribute), value)
|
||||
|
||||
@patch(
|
||||
'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
|
||||
Mock(return_value=False),
|
||||
)
|
||||
@patch(
|
||||
'xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages',
|
||||
Mock(return_value=['uk']),
|
||||
)
|
||||
def test_val_transcript_feature_disabled(self):
|
||||
"""
|
||||
Tests transcript retrieval view with val transcripts when
|
||||
the corresponding feature is disabled.
|
||||
"""
|
||||
self.login_and_enroll()
|
||||
# request to retrieval endpoint will result in 404 as val transcripts are disabled.
|
||||
self.api_response(expected_response_code=404, lang='uk')
|
||||
|
||||
@@ -21,9 +21,7 @@ from xmodule.video_module.transcripts_utils import (
|
||||
convert_video_transcript,
|
||||
get_video_transcript_content,
|
||||
Transcript,
|
||||
)
|
||||
from xmodule.video_module.transcripts_model_utils import (
|
||||
is_val_transcript_feature_enabled_for_course
|
||||
get_transcript,
|
||||
)
|
||||
|
||||
from ..decorators import mobile_course_access, mobile_view
|
||||
@@ -122,26 +120,11 @@ class VideoTranscripts(generics.RetrieveAPIView):
|
||||
|
||||
usage_key = BlockUsageLocator(course.id, block_type='video', block_id=block_id)
|
||||
video_descriptor = modulestore().get_item(usage_key)
|
||||
feature_enabled = is_val_transcript_feature_enabled_for_course(usage_key.course_key)
|
||||
|
||||
try:
|
||||
transcripts = video_descriptor.get_transcripts_info(include_val_transcripts=feature_enabled)
|
||||
content, filename, mimetype = video_descriptor.get_transcript(transcripts, lang=lang)
|
||||
except (ValueError, NotFoundError):
|
||||
# Fallback mechanism for edx-val transcripts
|
||||
transcript = None
|
||||
if feature_enabled:
|
||||
transcript = get_video_transcript_content(video_descriptor.edx_video_id, lang)
|
||||
|
||||
if not transcript:
|
||||
raise Http404(u'Transcript not found for {}, lang: {}'.format(block_id, lang))
|
||||
|
||||
transcript_conversion_props = dict(transcript, output_format=Transcript.SRT)
|
||||
transcript = convert_video_transcript(**transcript_conversion_props)
|
||||
filename = transcript['filename']
|
||||
content = transcript['content']
|
||||
mimetype = Transcript.mime_types[Transcript.SRT]
|
||||
except KeyError:
|
||||
raise Http404(u"Transcript not found for {}, lang: {}".format(block_id, lang))
|
||||
content, filename, mimetype = get_transcript(video_descriptor, lang=lang)
|
||||
except NotFoundError:
|
||||
raise Http404(u'Transcript not found for {}, lang: {}'.format(block_id, lang))
|
||||
|
||||
response = HttpResponse(content, content_type=mimetype)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename.encode('utf-8'))
|
||||
|
||||
@@ -126,7 +126,7 @@ edx-rest-api-client==1.7.1
|
||||
edx-search==1.1.0
|
||||
edx-submissions==2.0.12
|
||||
edx-user-state-client==1.0.4
|
||||
edxval==0.1.13
|
||||
edxval==0.1.14
|
||||
elasticsearch==1.9.0 # via edx-search
|
||||
enum34==1.1.6
|
||||
event-tracking==0.2.4
|
||||
|
||||
@@ -147,7 +147,7 @@ edx-search==1.1.0
|
||||
edx-sphinx-theme==1.3.0
|
||||
edx-submissions==2.0.12
|
||||
edx-user-state-client==1.0.4
|
||||
edxval==0.1.13
|
||||
edxval==0.1.14
|
||||
elasticsearch==1.9.0
|
||||
enum34==1.1.6
|
||||
event-tracking==0.2.4
|
||||
|
||||
@@ -141,7 +141,7 @@ edx-rest-api-client==1.7.1
|
||||
edx-search==1.1.0
|
||||
edx-submissions==2.0.12
|
||||
edx-user-state-client==1.0.4
|
||||
edxval==0.1.13
|
||||
edxval==0.1.14
|
||||
elasticsearch==1.9.0
|
||||
enum34==1.1.6
|
||||
event-tracking==0.2.4
|
||||
|
||||
Reference in New Issue
Block a user