Transcript delete on Video Upload Page - EDUCATOR-1961
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
</a>
|
||||
<span class='transcript-actions-separator'> | </span>
|
||||
<button class="button-link upload-transcript-button"><%- gettext('Replace') %></button>
|
||||
<span class='transcript-actions-separator'> | </span>
|
||||
<button class="button-link delete-transcript-button"><%- gettext('Delete') %></button>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
|
||||
@@ -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<edx_video_id>[-\w]+))?(?:/(?P<language_code>[^/]*))?$'.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),
|
||||
|
||||
Reference in New Issue
Block a user