diff --git a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py index 05dc62a011..40174725b4 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py @@ -5,11 +5,13 @@ from io import BytesIO from mock import Mock, patch, ANY from django.test.testcases import TestCase +from edxval import api from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url from contentstore.views.transcript_settings import TranscriptionProviderErrorType, validate_transcript_credentials from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file +from student.roles import CourseStaffRole @ddt.ddt @@ -468,3 +470,112 @@ class TranscriptUploadTest(CourseTestCase): json.loads(response.content)['error'], u'There is a problem with this transcript file. Try to upload a different file.' ) + + +@ddt.ddt +@patch( + 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', + Mock(return_value=True) +) +class TranscriptUploadTest(CourseTestCase): + """ + Tests for transcript deletion handler. + """ + VIEW_NAME = 'transcript_delete_handler' + + def get_url_for_course_key(self, course_id, **kwargs): + return reverse_course_url(self.VIEW_NAME, course_id, kwargs) + + def test_302_with_anonymous_user(self): + """ + Verify that redirection happens in case of unauthorized request. + """ + self.client.logout() + transcript_delete_url = self.get_url_for_course_key(self.course.id, edx_video_id='test_id', language_code='en') + response = self.client.delete(transcript_delete_url) + self.assertEqual(response.status_code, 302) + + def test_405_with_not_allowed_request_method(self): + """ + Verify that 405 is returned in case of not-allowed request methods. + Allowed request methods include DELETE. + """ + transcript_delete_url = self.get_url_for_course_key(self.course.id, edx_video_id='test_id', language_code='en') + 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. + """ + # Making sure that user is not a staff / course's staff. + self.user.is_staff = False + self.user.save() + + # Assert the user's role + self.assertFalse(self.user.is_staff) + self.assertFalse(CourseStaffRole(self.course.id).has_user(self.user)) + + # Now, Make request to deletion handler + transcript_delete_url = self.get_url_for_course_key(self.course.id, edx_video_id='test_id', language_code='en') + response = self.client.delete(transcript_delete_url) + self.assertEqual(response.status_code, 404) + + @ddt.data( + { + 'is_staff': True, + 'is_course_staff': True + }, + { + 'is_staff': False, + 'is_course_staff': True + }, + { + 'is_staff': True, + 'is_course_staff': False + }, + ) + @ddt.unpack + def test_transcript_delete_handler(self, is_staff, is_course_staff): + """ + Tests that transcript delete handler works as expected with combinations of staff and course's staff. + """ + # Setup user's roles + self.user.is_staff = is_staff + self.user.save() + course_staff_role = CourseStaffRole(self.course.id) + if is_course_staff: + course_staff_role.add_users(self.user) + else: + course_staff_role.remove_users(self.user) + + # Assert the user role + self.assertEqual(self.user.is_staff, is_staff) + self.assertEqual(CourseStaffRole(self.course.id).has_user(self.user), is_course_staff) + + video_id, language_code = u'1234', u'en' + # Create a real transcript in VAL. + api.create_or_update_video_transcript( + video_id=video_id, + language_code=language_code, + metadata={'file_format': 'srt'} + ) + + # Make request to transcript deletion handler + response = self.client.delete(self.get_url_for_course_key( + self.course.id, + edx_video_id=video_id, + language_code=language_code + )) + self.assertEqual(response.status_code, 200) + self.assertFalse(api.get_video_transcript_data([video_id], language_code=language_code)) diff --git a/cms/djangoapps/contentstore/views/transcript_settings.py b/cms/djangoapps/contentstore/views/transcript_settings.py index e5457aee9b..6ed21e03b2 100644 --- a/cms/djangoapps/contentstore/views/transcript_settings.py +++ b/cms/djangoapps/contentstore/views/transcript_settings.py @@ -9,9 +9,10 @@ from django.contrib.auth.decorators import login_required from django.core.files.base import ContentFile from django.http import HttpResponseNotFound, HttpResponse from django.utils.translation import ugettext as _ -from django.views.decorators.http import require_POST, require_GET +from django.views.decorators.http import require_http_methods, require_POST, require_GET from edxval.api import ( create_or_update_video_transcript, + delete_video_transcript, get_available_transcript_languages, get_3rd_party_transcription_plans, get_video_transcript_data, @@ -21,12 +22,18 @@ from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag from openedx.core.djangoapps.video_pipeline.api import update_3rd_party_transcription_service_credentials +from student.auth import has_studio_write_access from util.json_request import JsonResponse, expect_json from contentstore.views.videos import TranscriptProvider from xmodule.video_module.transcripts_utils import Transcript, TranscriptsGenerationException -__all__ = ['transcript_credentials_handler', 'transcript_download_handler', 'transcript_upload_handler'] +__all__ = [ + 'transcript_credentials_handler', + 'transcript_download_handler', + 'transcript_upload_handler', + 'transcript_delete_handler' +] LOGGER = logging.getLogger(__name__) @@ -254,3 +261,31 @@ def transcript_upload_handler(request, course_key_string): ) return response + + +@login_required +@require_http_methods(["DELETE"]) +def transcript_delete_handler(request, course_key_string, edx_video_id, language_code): + """ + View to delete a transcript file. + + Arguments: + request: A WSGI request object + course_key_string: Course key identifying a course. + edx_video_id: edX video identifier whose transcript need to be deleted. + language_code: transcript's language code. + + Returns + - A 404 if the corresponding feature flag is disabled or user does not have required permisions + - A 200 if transcript is deleted without any error(s) + """ + # Check whether the feature is available for this course. + course_key = CourseKey.from_string(course_key_string) + video_transcripts_enabled = VideoTranscriptEnabledFlag.feature_enabled(course_key) + # User needs to have studio write access for this course. + if not video_transcripts_enabled or not has_studio_write_access(request.user, course_key): + return HttpResponseNotFound() + + delete_video_transcript(video_id=edx_video_id, language_code=language_code) + + return JsonResponse(status=200) diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index e8ae989c99..03f288966c 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -653,6 +653,10 @@ def videos_index_html(course): 'transcript_upload_handler', unicode(course.id) ), + 'transcript_delete_handler_url': reverse_course_url( + 'transcript_delete_handler', + unicode(course.id) + ), 'transcription_plans': get_3rd_party_transcription_plans(), 'trancript_download_file_format': Transcript.SRT } diff --git a/cms/static/js/views/video_transcripts.js b/cms/static/js/views/video_transcripts.js index c034ae224e..1eca27f741 100644 --- a/cms/static/js/views/video_transcripts.js +++ b/cms/static/js/views/video_transcripts.js @@ -1,8 +1,9 @@ define( ['underscore', 'gettext', 'js/views/baseview', 'common/js/components/views/feedback_prompt', 'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/string-utils', - 'text!templates/video-transcripts.underscore', 'text!templates/video-transcript-upload-status.underscore'], - function(_, gettext, BaseView, PromptView, HtmlUtils, StringUtils, videoTranscriptsTemplate, + 'common/js/components/utils/view_utils', 'text!templates/video-transcripts.underscore', + 'text!templates/video-transcript-upload-status.underscore'], + function(_, gettext, BaseView, PromptView, HtmlUtils, StringUtils, ViewUtils, videoTranscriptsTemplate, videoTranscriptUploadStatusTemplate) { 'use strict'; @@ -12,10 +13,12 @@ define( events: { 'click .toggle-show-transcripts-button': 'toggleShowTranscripts', 'click .upload-transcript-button': 'chooseFile', + 'click .delete-transcript-button': 'removeTranscript', 'click .more-details-action': 'showUploadFailureMessage' }, initialize: function(options) { + this.isCollapsed = true; this.transcripts = options.transcripts; this.edxVideoID = options.edxVideoID; this.clientVideoID = options.clientVideoID; @@ -85,33 +88,71 @@ define( ); }, + /* + Returns transcript delete handler url. + */ + getTranscriptDeleteUrl: function(edxVideoID, transcriptLanguageCode, transcriptDeleteHandlerUrl) { + return StringUtils.interpolate( + '{transcriptDeleteHandlerUrl}/{edxVideoID}/{transcriptLanguageCode}', + { + transcriptDeleteHandlerUrl: transcriptDeleteHandlerUrl, + edxVideoID: edxVideoID, + transcriptLanguageCode: transcriptLanguageCode + } + ); + }, + /* Toggles Show/Hide transcript button and transcripts container. */ toggleShowTranscripts: function() { var $transcriptsWrapperEl = this.$el.find('.show-video-transcripts-wrapper'); - // Toggle show transcript wrapper. - $transcriptsWrapperEl.toggleClass('hidden'); + if ($transcriptsWrapperEl.hasClass('hidden')) { + this.showTranscripts(); + this.isCollapsed = false; + } else { + this.hideTranscripts(); + this.isCollapsed = true; + } + }, - // Toggle button text. + showTranscripts: function() { + // Show transcript wrapper + this.$el.find('.show-video-transcripts-wrapper').removeClass('hidden'); + + // Update button text. HtmlUtils.setHtml( this.$el.find('.toggle-show-transcripts-button-text'), StringUtils.interpolate( - gettext('{toggleShowTranscriptText} transcripts ({totalTranscripts})'), + gettext('Show transcripts ({transcriptCount})'), { - toggleShowTranscriptText: $transcriptsWrapperEl.hasClass('hidden') ? gettext('Show') : gettext('Hide'), // eslint-disable-line max-len - totalTranscripts: this.transcripts.length + transcriptCount: this.transcripts.length } ) ); + this.$el.find('.toggle-show-transcripts-icon') + .removeClass('fa-caret-right') + .addClass('fa-caret-down'); + }, - // Toggle icon class. - if ($transcriptsWrapperEl.hasClass('hidden')) { - this.$el.find('.toggle-show-transcripts-icon').removeClass('fa-caret-down').addClass('fa-caret-right'); // eslint-disable-line max-len - } else { - this.$el.find('.toggle-show-transcripts-icon').removeClass('fa-caret-right').addClass('fa-caret-down'); // eslint-disable-line max-len - } + hideTranscripts: function() { + // Hide transcript wrapper + this.$el.find('.show-video-transcripts-wrapper').addClass('hidden'); + + // Update button text. + HtmlUtils.setHtml( + this.$el.find('.toggle-show-transcripts-button-text'), + StringUtils.interpolate( + gettext('Hide transcripts ({transcriptCount})'), + { + transcriptCount: _.size(this.transcripts) + } + ) + ); + this.$el.find('.toggle-show-transcripts-icon') + .removeClass('fa-caret-down') + .addClass('fa-caret-right'); }, validateTranscriptUpload: function(file) { @@ -211,6 +252,39 @@ define( this.renderMessage($transcriptContainer, 'failed', errorMessage); }, + removeTranscript: function(event) { + var self = this, + $transcriptEl = $(event.target).parents('.show-video-transcript-content'), + languageCode = $transcriptEl.attr('data-language-code'), + transcriptDeleteUrl = this.getTranscriptDeleteUrl( + this.edxVideoID, + languageCode, + this.videoTranscriptSettings.transcript_delete_handler_url + ); + + ViewUtils.confirmThenRunOperation( + gettext('Are you sure you want to remove this transcript from the video?'), + gettext('Removing a transcript from this video will have impact on all the video components using this video.'), // eslint-disable-line max-len + gettext('Remove'), + function() { + ViewUtils.runOperationShowingMessage( + gettext('Removing'), + function() { + return $.ajax({ + url: transcriptDeleteUrl, + type: 'DELETE' + }).done(function() { + // Update transcripts. + self.transcripts = _.omit(self.transcripts, languageCode); + // re-render transcripts. + self.render(); + }); + } + ); + } + ); + }, + clearMessage: function() { var $transcriptStatusesEl = this.$el.find('.transcript-upload-status-container'); // Clear all message containers @@ -271,6 +345,8 @@ define( transcriptDownloadHandlerUrl: this.videoTranscriptSettings.transcript_download_handler_url }) ); + + this.isCollapsed ? this.hideTranscripts() : this.showTranscripts(); return this; } }); diff --git a/cms/templates/js/video-transcripts.underscore b/cms/templates/js/video-transcripts.underscore index 8c389c91cc..4d9a82cbbe 100644 --- a/cms/templates/js/video-transcripts.underscore +++ b/cms/templates/js/video-transcripts.underscore @@ -35,6 +35,8 @@ | + | + <% }) %> diff --git a/cms/urls.py b/cms/urls.py index 1dee28ec66..314222e1a1 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -146,6 +146,9 @@ urlpatterns = [ contentstore.views.transcript_download_handler, name='transcript_download_handler'), url(r'^transcript_upload/{}$'.format(settings.COURSE_KEY_PATTERN), contentstore.views.transcript_upload_handler, name='transcript_upload_handler'), + url(r'^transcript_delete/{}(?:/(?P[-\w]+))?(?:/(?P[^/]*))?$'.format( + settings.COURSE_KEY_PATTERN + ), contentstore.views.transcript_delete_handler, name='transcript_delete_handler'), url(r'^video_encodings_download/{}$'.format(settings.COURSE_KEY_PATTERN), contentstore.views.video_encodings_download, name='video_encodings_download'), url(r'^group_configurations/{}$'.format(settings.COURSE_KEY_PATTERN),