From d5758bd1f9038028e4e34ea42080044ff0054608 Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Tue, 27 Mar 2018 12:53:40 +0500 Subject: [PATCH] delete transcript from advance tab EDUCATOR-1759 --- cms/static/js/views/uploads.js | 2 +- .../js/views/video/translations_editor.js | 73 +++++---- .../metadata-translations-entry.underscore | 4 - .../xmodule/video_module/video_handlers.py | 38 ++++- .../acceptance/pages/studio/video/video.py | 35 +++-- .../tests/video/test_studio_video_editor.py | 26 +++- .../courseware/tests/test_video_handlers.py | 140 +++++++++++++++++- 7 files changed, 266 insertions(+), 52 deletions(-) diff --git a/cms/static/js/views/uploads.js b/cms/static/js/views/uploads.js index 1b7142b6b0..9f70363cb7 100644 --- a/cms/static/js/views/uploads.js +++ b/cms/static/js/views/uploads.js @@ -80,7 +80,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal', 'jquery 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; + uploadAjaxData.notifyOnError = false; if (e && e.preventDefault) { e.preventDefault(); } this.model.set('uploading', true); diff --git a/cms/static/js/views/video/translations_editor.js b/cms/static/js/views/video/translations_editor.js index a68dcee05a..23c67bebe9 100644 --- a/cms/static/js/views/video/translations_editor.js +++ b/cms/static/js/views/video/translations_editor.js @@ -1,9 +1,9 @@ define( [ 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/html-utils', 'js/views/video/transcripts/utils', - 'js/views/abstract_editor', 'js/models/uploads', 'js/views/uploads' + 'js/views/abstract_editor', 'common/js/components/utils/view_utils', 'js/models/uploads', 'js/views/uploads' ], -function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDialog) { +function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, ViewUtils, FileUpload, UploadDialog) { 'use strict'; var VideoUploadDialog = UploadDialog.extend({ @@ -18,7 +18,6 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia var Translations = AbstractEditor.extend({ events: { - 'click .setting-clear': 'clear', 'click .create-setting': 'addEntry', 'click .remove-setting': 'removeEntry', 'click .upload-setting': 'upload', @@ -48,7 +47,6 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia languageMap[lang] = lang; }); TranscriptUtils.Storage.set('languageMap', languageMap); - AbstractEditor.prototype.initialize.apply(this, arguments); }, @@ -128,7 +126,7 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia _.each(values, function(value, newLang) { var html = $(self.templateItem({ newLang: newLang, - originalLang: _.findKey(languageMap, function(lang){ return lang === newLang}) || '', + originalLang: _.findKey(languageMap, function(lang) { return lang === newLang; }) || '', value: value, url: self.model.get('urlRoot') })).prepend(dropdown.clone().val(newLang))[0]; @@ -148,26 +146,49 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia }, removeEntry: function(event) { - event.preventDefault(); - var $currentListItemEl = $(event.currentTarget).parent(), + var self = this, + $currentListItemEl = $(event.currentTarget).parent(), originalLang = $currentListItemEl.data('original-lang'), selectedLang = $currentListItemEl.find('select option:selected').val(), - languageMap = TranscriptUtils.Storage.get('languageMap'); + languageMap = TranscriptUtils.Storage.get('languageMap'), + edxVideoIdField = TranscriptUtils.getField(self.model.collection, 'edx_video_id'); - // re-render dropdowns - this.setValueInEditor(this.getAllLanguageDropdownElementsData(false, selectedLang)); + event.preventDefault(); - // remvove the `originalLang` from `languageMap` + /* + 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) { - TranscriptUtils.Storage.set('languageMap', _.omit(languageMap, 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), $listItem = $target.parents('li.list-settings-item'), @@ -178,10 +199,12 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia uploadData, videoUploadDialog; + event.preventDefault(); + // That's the case when an author is // uploading a new transcript. if (!originalLang) { - originalLang = newLang + originalLang = newLang; } // Transcript data payload @@ -208,7 +231,7 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia // new language entry to be added to languageMap newLangObject[newLang] = newLang; - //Update edx-video-id + // Update edx-video-id edxVideoIdField.setValue(response.edx_video_id); // Update language map by omitting original lang and adding new lang @@ -228,13 +251,6 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia 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'), @@ -266,27 +282,26 @@ function($, _, HtmlUtils, TranscriptUtils, AbstractEditor, FileUpload, UploadDia // 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){ + // {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}) || ""; + 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[""] = ""; + 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) + 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 421513dd8f..1afef78cd9 100644 --- a/cms/templates/js/video/metadata-translations-entry.underscore +++ b/cms/templates/js/video/metadata-translations-entry.underscore @@ -7,9 +7,5 @@ <%= gettext("Add") %> <%= model.get('display_name')%> - <%= model.get('help') %> diff --git a/common/lib/xmodule/xmodule/video_module/video_handlers.py b/common/lib/xmodule/xmodule/video_module/video_handlers.py index 1e7e193353..19b17f2dd9 100644 --- a/common/lib/xmodule/xmodule/video_module/video_handlers.py +++ b/common/lib/xmodule/xmodule/video_module/video_handlers.py @@ -20,19 +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 +from edxval.api import create_or_update_video_transcript, create_external_video, delete_video_transcript from .transcripts_utils import ( 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, get_transcript, - get_transcript_from_contentstore + get_transcript_from_contentstore, + remove_subs_from_store, + get_html5_ids ) log = logging.getLogger(__name__) @@ -488,6 +490,36 @@ class VideoStudioViewHandlers(object): }, 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) + + 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') diff --git a/common/test/acceptance/pages/studio/video/video.py b/common/test/acceptance/pages/studio/video/video.py index f5bb6468ce..58abc54c85 100644 --- a/common/test/acceptance/pages/studio/video/video.py +++ b/common/test/acceptance/pages/studio/video/video.py @@ -81,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 @@ -346,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 @@ -395,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 @@ -543,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): 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 5a09b64c3f..cd1a409a02 100644 --- a/common/test/acceptance/tests/video/test_studio_video_editor.py +++ b/common/test/acceptance/tests/video/test_studio_video_editor.py @@ -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,9 +296,28 @@ 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_entry_remove_works(self): + """ + 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 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.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): """ Scenario: User cannot upload translations in sjson format @@ -394,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()) diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index c941e4fc08..adbb4d2d66 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -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 @@ -22,13 +23,14 @@ 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, - edxval_api + 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 @@ -1011,6 +1013,138 @@ class TestStudioTranscriptTranslationPostDispatch(TestVideo): ) +@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) class TestGetTranscript(TestVideo): """