diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py
index 446c5703b625..c458b7791160 100644
--- a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py
+++ b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py
@@ -261,10 +261,7 @@ def test_migrate_transcripts_exception_logging(self):
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 @@ def test_migrate_transcripts_exception_logging(self):
)),
(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:
diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
index bc67a6aad2fc..2a004433f814 100644
--- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
@@ -191,7 +191,9 @@ def setUpClass(cls):
@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 @@ def test_success_downloading_subs(self):
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 @@ def test_fail_downloading_subs(self, mock_get):
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 @@ def test_downloading_subs_using_transcript_name(self, mock_get):
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 @@ def setUp(self):
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 @@ def create_transcript(self, subs_id, language=u'en', filename='video.srt'):
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 @@ def test_get_transcript_not_found(self, lang):
)
@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 @@ def test_get_transcript_no_en_transcript(self):
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
+ )
diff --git a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py
index 443131af6b62..da838b180684 100644
--- a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py
+++ b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py
@@ -5,6 +5,7 @@
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 @@ def test_invalid_credentials(self, provider, credentials, expected_error_message
@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 @@ def test_405_with_not_allowed_request_method(self):
Verify that 405 is returned in case of not-allowed request methods.
Allowed request methods include GET.
"""
- transcript_download_url = self.get_url_for_course_key(self.course.id)
- response = self.client.post(transcript_download_url, content_type='application/json')
+ 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 @@ def test_transcript_download_handler(self, mock_get_video_transcript_data):
# 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 @@ def test_transcript_download_handler_missing_attrs(self, request_payload, expect
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 @@ def test_405_with_not_allowed_request_method(self):
Verify that 405 is returned in case of not-allowed request methods.
Allowed request methods include POST.
"""
- transcript_upload_url = self.get_url_for_course_key(self.course.id)
- response = self.client.get(transcript_upload_url, content_type='application/json')
+ 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 @@ def test_transcript_upload_handler_missing_attrs(self, request_payload, expected
"""
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 @@ def test_transcript_upload_handler_existing_transcript(self):
Tests that upload handler do not update transcript's language if a transcript
with the same language already present for an edx_video_id.
"""
- transcript_upload_url = self.get_url_for_course_key(self.course.id)
# Make request to transcript upload handler
request_payload = {
'edx_video_id': '1234',
'language_code': 'en',
'new_language_code': 'es'
}
- response = self.client.post(transcript_upload_url, request_payload, format='multipart')
+ 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 @@ def test_transcript_upload_handler_with_image(self):
Tests the transcript upload handler with an image file.
"""
with make_image_file() as image_file:
- transcript_upload_url = self.get_url_for_course_key(self.course.id)
# Make request to transcript upload handler
response = self.client.post(
- transcript_upload_url,
+ self.view_url,
{
'edx_video_id': '123',
'language_code': 'en',
@@ -451,11 +419,10 @@ def test_transcript_upload_handler_with_invalid_transcript(self):
"""
Tests the transcript upload handler with an invalid transcript file.
"""
- transcript_upload_url = self.get_url_for_course_key(self.course.id)
transcript_file_stream = BytesIO('An invalid transcript SubRip file content')
# Make request to transcript upload handler
response = self.client.post(
- transcript_upload_url,
+ self.view_url,
{
'edx_video_id': '123',
'language_code': 'en',
@@ -473,11 +440,7 @@ def test_transcript_upload_handler_with_invalid_transcript(self):
@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 @@ def test_405_with_not_allowed_request_method(self):
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.
diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py
index 9d99c072d9af..0fae175fd917 100644
--- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py
+++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py
@@ -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 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.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 @@ def get_youtube_ids(self):
1.5: item.youtube_id_1_5
}
+ 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'
+ }
+ response = self.client.ajax_post('/xblock/', data)
+ usage_key = self._get_usage_key(response)
+ item = modulestore().get_item(usage_key)
+ item.data = ''
+ modulestore().update_item(item, self.user.id)
+
+ return usage_key
+
+ 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)
+
+@ddt.ddt
class TestUploadTranscripts(BaseTranscripts):
"""
- Tests for '/transcripts/upload' url.
+ Tests for '/transcripts/upload' endpoint.
"""
def setUp(self):
- """Create initial data."""
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.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("""
-
- """)
- modulestore().update_item(self.item, self.user.id)
+ # Add clean up handler
+ self.addCleanup(self.clean_temporary_transcripts)
- 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')
+ def create_transcript_file(self, content, suffix, include_bom=False):
+ """
+ Setup a transcript file with suffix and content.
+ """
+ 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)
- item = modulestore().get_item(self.video_usage_key)
- self.assertEqual(item.sub, filename)
+ transcript_file.write(wrapped_content)
+ transcript_file.seek(0)
- content_location = StaticContent.compute_location(
- self.course.id, 'subs_{0}.srt.sjson'.format(filename))
- self.assertTrue(contentstore().find(content_location))
+ return transcript_file
- 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 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.bom_srt_file.close()
- 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 upload_transcript(self, locator, transcript_file, edx_video_id=None):
+ """
+ Uploads a transcript for a video
+ """
+ payload = {}
+ if locator:
+ payload.update({'locator': locator})
- 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.")
+ if edx_video_id is not None:
+ payload.update({'edx_video_id': edx_video_id})
- # 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.")
+ if transcript_file:
+ payload.update({'transcript-file': transcript_file})
- def test_fail_for_non_video_module(self):
- # non_video module: setup
- 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)
- item = modulestore().get_item(usage_key)
- item.data = ''
- modulestore().update_item(item, self.user.id)
+ upload_url = reverse('upload_transcripts')
+ response = self.client.post(upload_url, payload)
- # non_video module: testing
+ return response
- 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.')
+ @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:
- def test_fail_bad_xml(self):
- self.item.data = '<<'
+ 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)
- 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',
- }])
- })
+ # 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.'
+ )
- 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.')
+ 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_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_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_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_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_subs_uploading_with_byte_order_mark(self):
+ def test_transcript_upload_without_edx_video_id(self):
"""
- Test uploading subs containing BOM(Byte Order Mark), e.g. U+FEFF
+ Test that transcript upload validation fails if the `edx_video_id` is missing
"""
- filedata = textwrap.dedent("""
- 1
- 00:00:10,500 --> 00:00:13,000
- Test ufeff characters
+ 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.'
+ )
- 2
- 00:00:15,000 --> 00:00:18,000
- At the left we can see...
- """).encode('utf-8-sig')
+ 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'
- # Verify that ufeff character is in filedata.
- self.assertIn("ufeff", filedata)
- self.ufeff_srt_file.write(filedata)
- self.ufeff_srt_file.seek(0)
+ # 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')
- 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',
- }])
+ # 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)]
})
- 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))
+ def choose_transcript(self, locator, chosen_html5_id):
+ """
+ Make an endpoint call to choose transcript
+ """
+ payload = {}
+ if locator:
+ payload.update({'locator': unicode(locator)})
- subs_text = json.loads(contentstore().find(content_location).data).get('text')
- self.assertIn("Test ufeff characters", subs_text)
+ if chosen_html5_id:
+ payload.update({'html5_id': chosen_html5_id})
- def tearDown(self):
- super(TestUploadTranscripts, self).tearDown()
+ choose_transcript_url = reverse('choose_transcripts')
+ response = self.client.get(choose_transcript_url, {'data': json.dumps(payload)})
+ return response
- self.good_srt_file.close()
- self.bad_data_srt_file.close()
- self.bad_name_srt_file.close()
- self.ufeff_srt_file.close()
+ @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)
-class TestDownloadTranscripts(BaseTranscripts):
+ # 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/download' url.
+ Tests for '/transcripts/rename' endpoint.
"""
- def test_success_download_youtube(self):
- self.item.data = ''
+ 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)
- subs = {
- 'start': [100, 200, 240],
- 'end': [200, 240, 380],
- 'text': [
- 'subs #1',
- 'subs #2',
- 'subs #3'
- ]
- }
- self.save_subs_to_store(subs, 'JMD_ifUUfsU')
+ # 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)]
+ })
- 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 rename_transcript(self, locator):
+ """
+ Make an endpoint call to rename transcripts.
+ """
+ payload = {}
+ if locator:
+ payload.update({'locator': unicode(locator)})
- def test_success_download_nonyoutube(self):
- subs_id = str(uuid4())
- self.item.data = textwrap.dedent("""
-
- """.format(subs_id))
+ 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)
- subs = {
- 'start': [100, 200, 240],
- 'end': [200, 240, 380],
- 'text': [
- 'subs #1',
- 'subs #2',
- 'subs #3'
- ]
- }
- self.save_subs_to_store(subs, subs_id)
+ # Make call to use current transcript from contentstore
+ response = self.rename_transcript(self.video_usage_key)
- 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'
+ # 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.'
)
- 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)
+ 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.'
+ )
- resp = self.client.get(link, {})
- self.assertEqual(resp.status_code, 404)
+ 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)
- 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)
+ response = self.rename_transcript(locator=self.video_usage_key)
+ self.assert_response(
+ response,
+ expected_status_code=400,
+ expected_message=u"No such transcript."
+ )
- # 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_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.'
+ )
- 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("""
-
-
-
-
-
- """.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)
+@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)]
+ })
- link = reverse('download_transcripts')
- resp = self.client.get(link, {'locator': unicode(usage_key)})
- self.assertEqual(resp.status_code, 404)
+ 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:
- def test_fail_nonyoutube_subs_dont_exist(self):
- self.item.data = textwrap.dedent("""
-
- """)
+ 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)
- link = reverse('download_transcripts')
- resp = self.client.get(link, {'locator': self.video_usage_key})
- self.assertEqual(resp.status_code, 404)
+ # Make call to replace transcripts from youtube
+ response = self.replace_transcript(self.video_usage_key, self.youtube_id)
- def test_empty_youtube_attr_and_sub_attr(self):
- self.item.data = textwrap.dedent("""
-
- """)
- modulestore().update_item(self.item, self.user.id)
+ # Verify the response
+ self.assert_response(response, expected_status_code=200, expected_message='Success')
- link = reverse('download_transcripts')
- resp = self.client.get(link, {'locator': self.video_usage_key})
+ # 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)
- self.assertEqual(resp.status_code, 404)
+ # 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_fail_bad_sjson_subs(self):
- subs_id = str(uuid4())
- self.item.data = textwrap.dedent("""
-
- """.format(subs_id))
- modulestore().update_item(self.item, self.user.id)
+ 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.'
+ )
- subs = {
- 'start': [100, 200, 240],
- 'end': [200, 240, 380],
- 'text': [
- 'subs #1'
- ]
- }
- self.save_subs_to_store(subs, 'JMD_ifUUfsU')
+ 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.'
+ )
- link = reverse('download_transcripts')
- resp = self.client.get(link, {'locator': self.video_usage_key})
+ 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.'
+ )
- self.assertEqual(resp.status_code, 404)
+ 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)
- @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 test_replace_transcript_fails_on_unknown_category(self):
"""
- Verify that the val transcript is returned if its not found in content-store.
+ Test that validation fails if item's category is other than video.
"""
- mock_get_video_transcript_data.return_value = {
- 'content': json.dumps({
- "start": [10],
- "end": [100],
- "text": ["Hi, welcome to Edx."],
- }),
- 'file_name': 'edx.sjson'
- }
+ # 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 update_video_component(self, sub=None, youtube_id=None):
+ """
+ Updates video component with `sub` and `youtube_id`.
+ """
+ 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("""
-
- """)
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})
+ def download_transcript(self, locator):
+ """
+ Makes a call to download transcripts.
+ """
+ payload = {}
+ if locator:
+ payload.update({'locator': unicode(locator)})
- # 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'
- }
+ download_transcript_url = reverse('download_transcripts')
+ response = self.client.get(download_transcript_url, payload)
+ return response
- # 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)
+ 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)
- @patch(
- 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled',
- Mock(return_value=False),
- )
- def test_download_fallback_transcript_feature_disabled(self):
+ def test_download_youtube_transcript_success(self):
"""
- Verify the transcript download when feature is disabled.
+ Verify that the transcript associated to YT id is downloaded successfully.
"""
- self.item.data = textwrap.dedent("""
-
- """)
- modulestore().update_item(self.item, self.user.id)
+ 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)
- 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)
+ 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 @@ def test_success_download_nonyoutube(self):
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 @@ def test_success_download_nonyoutube(self):
}
)
- 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 = ''
@@ -668,13 +907,14 @@ def test_check_youtube(self):
'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 @@ def test_check_youtube_with_transcript_name(self, mock_get):
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 @@ def test_fail_for_non_video_module(self):
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("""