From cadd4378c71e587184856a13df5360d08d7d43a7 Mon Sep 17 00:00:00 2001 From: Qubad786 Date: Sun, 1 Apr 2018 16:02:17 +0500 Subject: [PATCH] updload/download/replace transcripts EDUCATOR-2167 EDUCATOR-2168 EDUCATOR-2169 --- cms/static/js/views/uploads.js | 17 +- .../js/views/video/translations_editor.js | 167 ++++++++++++++---- .../metadata-translations-entry.underscore | 1 + .../metadata-translations-item.underscore | 13 +- .../xmodule/video_module/video_handlers.py | 130 ++++++++++---- .../acceptance/pages/studio/video/video.py | 6 +- .../tests/video/test_studio_video_editor.py | 61 ------- .../courseware/tests/test_video_handlers.py | 156 ++++++++++------ 8 files changed, 356 insertions(+), 195 deletions(-) diff --git a/cms/static/js/views/uploads.js b/cms/static/js/views/uploads.js index 8a24c7984f..1b7142b6b0 100644 --- a/cms/static/js/views/uploads.js +++ b/cms/static/js/views/uploads.js @@ -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 }); }, diff --git a/cms/static/js/views/video/translations_editor.js b/cms/static/js/views/video/translations_editor.js index 2e776656f4..a68dcee05a 100644 --- a/cms/static/js/views/video/translations_editor.js +++ b/cms/static/js/views/video/translations_editor.js @@ -1,10 +1,9 @@ define( [ - 'jquery', 'underscore', + 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/html-utils', 'js/views/video/transcripts/utils', 'js/views/abstract_editor', 'js/models/uploads', 'js/views/uploads' - ], -function($, _, AbstractEditor, FileUpload, UploadDialog) { +function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDialog) { 'use strict'; var VideoUploadDialog = UploadDialog.extend({ @@ -31,13 +30,25 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) { 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. Keys in this map represent language codes present on + // server. Values will change if user changes the language from a dropdown. + // For example: Initially map will look like {'ar': 'ar', 'zh': 'zh'} and corresponding + // dropdowns will show language names `Arabic` and `Chinese`. If user changes `Chinese` + // to `Russian` then map will become {'ar': 'ar', 'zh': 'ru'} + _.each(this.model.getDisplayValue(), function(value, lang) { + languageMap[lang] = lang; + }); + TranscriptUtils.Storage.set('languageMap', languageMap); + AbstractEditor.prototype.initialize.apply(this, arguments); }, @@ -111,14 +122,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,18 +143,25 @@ 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) { event.preventDefault(); + var $currentListItemEl = $(event.currentTarget).parent(), + originalLang = $currentListItemEl.data('original-lang'), + selectedLang = $currentListItemEl.find('select option:selected').val(), + languageMap = TranscriptUtils.Storage.get('languageMap'); + + // re-render dropdowns + this.setValueInEditor(this.getAllLanguageDropdownElementsData(false, selectedLang)); + + // remvove the `originalLang` from `languageMap` + if (originalLang) { + TranscriptUtils.Storage.set('languageMap', _.omit(languageMap, originalLang)); + } - var entry = $(event.currentTarget).data('lang'); - this.setValueInEditor(_.omit(this.model.get('value'), entry)); - this.updateModel(); this.$el.find('.create-setting').removeClass('is-disabled').attr('aria-disabled', false); }, @@ -150,26 +170,58 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) { 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')); + // That's the case when an author is + // uploading a new transcript. + if (!originalLang) { + originalLang = newLang + } - dict[lang] = response.filename; - self.model.setValue(dict); - } - }); + // Transcript data payload + uploadData = { + edx_video_id: edxVideoIdField.getValue(), + language_code: originalLang, + new_language_code: newLang + }; - view.show(); + fileUploadModel = new FileUpload({ + title: gettext('Upload translation'), + fileFormats: ['srt'] + }); + + 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() { @@ -184,10 +236,57 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) { }, onChangeHandler: function(event) { - this.showClearButton(); + 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.updateModel(); + this.setValueInEditor(this.getAllLanguageDropdownElementsData()); + }, + + /** + * 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, index){ + 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; } + }); return Translations; diff --git a/cms/templates/js/video/metadata-translations-entry.underscore b/cms/templates/js/video/metadata-translations-entry.underscore index a7a5453f73..421513dd8f 100644 --- a/cms/templates/js/video/metadata-translations-entry.underscore +++ b/cms/templates/js/video/metadata-translations-entry.underscore @@ -1,5 +1,6 @@
+
    diff --git a/cms/templates/js/video/metadata-translations-item.underscore b/cms/templates/js/video/metadata-translations-item.underscore index 55e75d23f7..cb2fdd23a4 100644 --- a/cms/templates/js/video/metadata-translations-item.underscore +++ b/cms/templates/js/video/metadata-translations-item.underscore @@ -1,12 +1,13 @@ -
  1. - <%= gettext("Remove") %> +
  2. + <%= gettext("Remove") %> -
  3. diff --git a/common/lib/xmodule/xmodule/video_module/video_handlers.py b/common/lib/xmodule/xmodule/video_module/video_handlers.py index 7c8f1560cf..1e7e193353 100644 --- a/common/lib/xmodule/xmodule/video_module/video_handlers.py +++ b/common/lib/xmodule/xmodule/video_module/video_handlers.py @@ -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,12 +20,12 @@ 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 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, @@ -34,9 +34,6 @@ from .transcripts_utils import ( get_transcript, get_transcript_from_contentstore ) -from .transcripts_model_utils import ( - is_val_transcript_feature_enabled_for_course -) log = logging.getLogger(__name__) @@ -381,6 +378,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): """ @@ -412,39 +441,74 @@ 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 + + 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') + + 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 == 'GET': + language = request.GET.get('language_code') + if not language: + return Response(json={'error': _(u'Language is required.')}, status=400) - filename = request.GET.get('filename') - if not filename: - log.info("Invalid /translation request: no filename in request.GET") - return Response(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) - 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'] + else: + # Any other HTTP method is not allowed. + response = Response(status=404) else: # unknown dispatch log.debug("Dispatch is not allowed") diff --git a/common/test/acceptance/pages/studio/video/video.py b/common/test/acceptance/pages/studio/video/video.py index 5f42a7d21f..f5bb6468ce 100644 --- a/common/test/acceptance/pages/studio/video/video.py +++ b/common/test/acceptance/pages/studio/video/video.py @@ -513,8 +513,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): """ @@ -529,7 +529,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]) diff --git a/common/test/acceptance/tests/video/test_studio_video_editor.py b/common/test/acceptance/tests/video/test_studio_video_editor.py index 73c2d126b5..5a09b64c3f 100644 --- a/common/test/acceptance/tests/video/test_studio_video_editor.py +++ b/common/test/acceptance/tests/video/test_studio_video_editor.py @@ -295,67 +295,6 @@ class VideoEditorTest(CMSVideoBaseTest): self.save_unit_settings() self.assertFalse(self.video.is_captions_visible()) - def test_translations_clearing_works_w_saving(self): - """ - Scenario: Translations clearing works correctly w/ 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 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 - """ - 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()) - def test_cannot_upload_sjson_translation(self): """ Scenario: User cannot upload translations in sjson format diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index 4829b0590d..c941e4fc08 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -21,7 +21,12 @@ 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, Transcript +from xmodule.video_module.transcripts_utils import ( + TranscriptException, + TranscriptsGenerationException, + Transcript, + edxval_api +) from xmodule.x_module import STUDENT_VIEW from .helpers import BaseTestXmodule @@ -863,42 +868,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. @@ -921,42 +928,87 @@ 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)