diff --git a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py index 32eb1a0edb..e6c4a9664d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py @@ -169,3 +169,111 @@ class TranscriptCredentialsValidationTest(TestCase): # Assert the results. self.assertEqual(error_message, expected_error_message) self.assertDictEqual(validated_credentials, expected_validated_credentials) + + +@ddt.ddt +@patch( + 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', + Mock(return_value=True) +) +class TranscriptDownloadTest(CourseTestCase): + """ + Tests for transcript download handler. + """ + VIEW_NAME = 'transcript_download_handler' + + def get_url_for_course_key(self, course_id): + return reverse_course_url(self.VIEW_NAME, course_id) + + def test_302_with_anonymous_user(self): + """ + Verify that redirection happens in case of unauthorized request. + """ + self.client.logout() + transcript_download_url = self.get_url_for_course_key(self.course.id) + response = self.client.get(transcript_download_url, content_type='application/json') + 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 GET. + """ + transcript_download_url = self.get_url_for_course_key(self.course.id) + response = self.client.post(transcript_download_url, content_type='application/json') + 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_download_url = self.get_url_for_course_key(self.course.id) + with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature: + feature.return_value = False + response = self.client.get(transcript_download_url, content_type='application/json') + self.assertEqual(response.status_code, 404) + + @patch('contentstore.views.transcript_settings.get_video_transcript_data') + def test_transcript_download_handler(self, mock_get_video_transcript_data): + """ + Tests that transcript download handler works as expected. + """ + transcript_download_url = self.get_url_for_course_key(self.course.id) + mock_get_video_transcript_data.return_value = { + 'content': json.dumps({ + "start": [10], + "end": [100], + "text": ["Hi, welcome to Edx."], + }), + 'file_name': 'edx.sjson' + } + + # Make request to transcript download handler + response = self.client.get( + transcript_download_url, + data={ + 'edx_video_id': '123', + 'language_code': 'en' + }, + content_type='application/json' + ) + + # Expected response + expected_content = u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n' + expected_headers = { + 'Content-Disposition': 'attachment; filename="edx.srt"', + 'Content-Language': u'en', + 'Content-Type': 'application/x-subrip; charset=utf-8' + } + + # Assert the actual response + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, expected_content) + for attribute, value in expected_headers.iteritems(): + self.assertEqual(response.get(attribute), value) + + @ddt.data( + ( + {}, + u'Following parameters are required: edx_video_id, language_code.' + ), + ( + {'edx_video_id': '123'}, + u'Following parameters are required: language_code.' + ), + ( + {'language_code': 'en'}, + u'Following parameters are required: edx_video_id.' + ), + ) + @ddt.unpack + def test_transcript_download_handler_missing_attrs(self, request_payload, expected_error_message): + """ + Tests that transcript download handler with missing attributes. + """ + # Make request to transcript download handler + transcript_download_url = self.get_url_for_course_key(self.course.id) + response = self.client.get(transcript_download_url, data=request_payload) + # Assert the response + self.assertEqual(response.status_code, 400) + self.assertEqual(json.loads(response.content)['error'], expected_error_message) diff --git a/cms/djangoapps/contentstore/views/transcript_settings.py b/cms/djangoapps/contentstore/views/transcript_settings.py index 86374d469d..18c79b39cd 100644 --- a/cms/djangoapps/contentstore/views/transcript_settings.py +++ b/cms/djangoapps/contentstore/views/transcript_settings.py @@ -1,12 +1,15 @@ """ Views related to the transcript preferences feature """ +import os + from django.contrib.auth.decorators import login_required -from django.http import HttpResponseNotFound +from django.http import HttpResponseNotFound, HttpResponse from django.utils.translation import ugettext as _ -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_POST, require_GET from edxval.api import ( get_3rd_party_transcription_plans, + get_video_transcript_data, update_transcript_credentials_state_for_org, ) from opaque_keys.edx.keys import CourseKey @@ -16,8 +19,9 @@ from openedx.core.djangoapps.video_pipeline.api import update_3rd_party_transcri from util.json_request import JsonResponse, expect_json from contentstore.views.videos import TranscriptProvider +from xmodule.video_module.transcripts_utils import Transcript -__all__ = ['transcript_credentials_handler'] +__all__ = ['transcript_credentials_handler', 'transcript_download_handler'] class TranscriptionProviderErrorType: @@ -108,3 +112,46 @@ def transcript_credentials_handler(request, course_key_string): response = JsonResponse({'error': error_message}, status=400) return response + + +@login_required +@require_GET +def transcript_download_handler(request, course_key_string): + """ + JSON view handler to download a transcript. + + Arguments: + request: WSGI request object + course_key_string: course key + + Returns: + - A 200 response with SRT transcript file attached. + - A 400 if there is a validation error. + - A 404 if there is no such transcript or feature flag is disabled. + """ + course_key = CourseKey.from_string(course_key_string) + if not VideoTranscriptEnabledFlag.feature_enabled(course_key): + return HttpResponseNotFound() + + missing = [attr for attr in ['edx_video_id', 'language_code'] if attr not in request.GET] + if missing: + return JsonResponse( + {'error': _(u'Following parameters are required: {missing}.').format(missing=', '.join(missing))}, + status=400 + ) + + edx_video_id = request.GET['edx_video_id'] + language_code = request.GET['language_code'] + transcript = get_video_transcript_data(video_ids=[edx_video_id], language_code=language_code) + if transcript: + name_and_extension = os.path.splitext(transcript['file_name']) + basename, file_format = name_and_extension[0], name_and_extension[1][1:] + transcript_filename = '{base_name}.srt'.format(base_name=basename.encode('utf8')) + transcript_content = Transcript.convert(transcript['content'], input_format=file_format, output_format='srt') + # Construct an HTTP response + response = HttpResponse(transcript_content, content_type=Transcript.mime_types['srt']) + response['Content-Disposition'] = 'attachment; filename="{filename}"'.format(filename=transcript_filename) + else: + response = HttpResponseNotFound() + + return response diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index 62302f9e91..47e36822d7 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -643,6 +643,10 @@ def videos_index_html(course): 'transcript_credentials_handler', unicode(course.id) ), + 'transcript_download_handler_url': reverse_course_url( + 'transcript_download_handler', + unicode(course.id) + ), 'transcription_plans': get_3rd_party_transcription_plans(), 'trancript_download_file_format': TRANSCRIPT_DOWNLOAD_FILE_FORMAT } diff --git a/cms/static/js/spec/views/video_transcripts_spec.js b/cms/static/js/spec/views/video_transcripts_spec.js index ee21b0f2bb..fe1e20118f 100644 --- a/cms/static/js/spec/views/video_transcripts_spec.js +++ b/cms/static/js/spec/views/video_transcripts_spec.js @@ -22,9 +22,11 @@ define( ur: 'Urdu' }, TRANSCRIPT_DOWNLOAD_FILE_FORMAT = 'srt', + TRANSCRIPT_DOWNLOAD_URL = 'abc.com/transcript_download/course_id', videoSupportedFileFormats = ['.mov', '.mp4'], videoTranscriptSettings = { - trancript_download_file_format: TRANSCRIPT_DOWNLOAD_FILE_FORMAT + trancript_download_file_format: TRANSCRIPT_DOWNLOAD_FILE_FORMAT, + transcript_download_handler_url: TRANSCRIPT_DOWNLOAD_URL }, videoListView; @@ -33,7 +35,10 @@ define( uploadTranscriptActionEl = $transcriptActionsEl.find('.upload-transcript-button'); expect(downloadTranscriptActionEl.html().trim(), 'Download'); - expect(downloadTranscriptActionEl.attr('href'), '#'); + expect( + downloadTranscriptActionEl.attr('href'), + TRANSCRIPT_DOWNLOAD_URL + '?edx_video_id=' + edxVideoID + '&language_code=' + transcriptLanguage + ); expect(uploadTranscriptActionEl.html().trim(), 'Upload'); expect(uploadTranscriptActionEl.data('edx-video-id'), edxVideoID); diff --git a/cms/static/js/views/previous_video_upload_list.js b/cms/static/js/views/previous_video_upload_list.js index a356950315..244df65974 100644 --- a/cms/static/js/views/previous_video_upload_list.js +++ b/cms/static/js/views/previous_video_upload_list.js @@ -18,6 +18,7 @@ define( defaultVideoImageURL: options.defaultVideoImageURL, videoHandlerUrl: options.videoHandlerUrl, videoImageSettings: options.videoImageSettings, + videoTranscriptSettings: options.videoTranscriptSettings, model: model, transcriptAvailableLanguages: options.transcriptAvailableLanguages, videoSupportedFileFormats: options.videoSupportedFileFormats, diff --git a/cms/static/js/views/video_transcripts.js b/cms/static/js/views/video_transcripts.js index a1a3c17e72..fb78f2526a 100644 --- a/cms/static/js/views/video_transcripts.js +++ b/cms/static/js/views/video_transcripts.js @@ -85,7 +85,8 @@ define( transcriptAvailableLanguages: this.sortByValue(this.transcriptAvailableLanguages), edxVideoID: this.edxVideoID, transcriptClientTitle: this.getTranscriptClientTitle(), - transcriptDownloadFileFormat: this.videoTranscriptSettings.trancript_download_file_format + transcriptDownloadFileFormat: this.videoTranscriptSettings.trancript_download_file_format, + transcriptDownloadHandlerUrl: this.videoTranscriptSettings.transcript_download_handler_url }) ); return this; diff --git a/cms/templates/js/video-transcripts.underscore b/cms/templates/js/video-transcripts.underscore index 7b96ff4b5d..355c2dcbbf 100644 --- a/cms/templates/js/video-transcripts.underscore +++ b/cms/templates/js/video-transcripts.underscore @@ -22,7 +22,17 @@ <% }) %>
- + <%- gettext('Download') %> | diff --git a/cms/urls.py b/cms/urls.py index 28becfbffe..647e0b1841 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -142,6 +142,8 @@ urlpatterns = [ contentstore.views.transcript_preferences_handler, name='transcript_preferences_handler'), url(r'^transcript_credentials/{}$'.format(settings.COURSE_KEY_PATTERN), contentstore.views.transcript_credentials_handler, name='transcript_credentials_handler'), + url(r'^transcript_download/{}$'.format(settings.COURSE_KEY_PATTERN), + contentstore.views.transcript_download_handler, name='transcript_download_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), diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index b8c4d16c2e..098ed8115d 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -105,4 +105,4 @@ xblock-review==1.1.2 git+https://github.com/mitodl/edx-sga.git@d019b8a050c056db535e3ff13c93096145a932de#egg=edx-sga==0.7.1 git+https://github.com/open-craft/xblock-poll@7ba819b968fe8faddb78bb22e1fe7637005eb414#egg=xblock-poll==1.2.7 -git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@2.1.5#egg=xblock-drag-and-drop-v2==2.1.5 +git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@2.1.5#egg=xblock-drag-and-drop-v2==2.1.5 \ No newline at end of file