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),