From 7157ec2e2fdbfc901cd94105479444648a861ec1 Mon Sep 17 00:00:00 2001 From: Muzaffar yousaf Date: Wed, 8 Nov 2017 14:43:26 +0500 Subject: [PATCH] Revert "Revert "Transcript secure credentials"" --- cms/djangoapps/contentstore/views/__init__.py | 1 + .../views/tests/test_transcript_settings.py | 171 ++++++ .../contentstore/views/transcript_settings.py | 110 ++++ cms/djangoapps/contentstore/views/videos.py | 41 +- cms/envs/common.py | 3 + cms/static/js/factories/videos_index.js | 2 + .../views/active_video_upload_list_spec.js | 2 +- .../spec/views/course_video_settings_spec.js | 530 +++++++++++++++--- .../js/views/active_video_upload_list.js | 2 + cms/static/js/views/course_video_settings.js | 328 +++++++++-- cms/static/sass/views/_video-upload.scss | 43 +- ...s-update-org-credentials-footer.underscore | 8 + ...settings-update-settings-footer.underscore | 13 + .../js/course-video-settings.underscore | 47 +- ...se-video-transcript-preferences.underscore | 31 + ...video-transcript-provider-empty.underscore | 7 + ...eo-transcript-provider-selected.underscore | 8 + ...script-organization-credentials.underscore | 43 ++ cms/templates/videos_index.html | 1 + cms/urls.py | 2 + lms/envs/common.py | 3 + .../djangoapps/video_pipeline/__init__.py | 0 .../core/djangoapps/video_pipeline/admin.py | 9 + openedx/core/djangoapps/video_pipeline/api.py | 51 ++ .../video_pipeline/migrations/0001_initial.py | 31 + .../video_pipeline/migrations/__init__.py | 0 .../core/djangoapps/video_pipeline/models.py | 31 + .../video_pipeline/tests/__init__.py | 0 .../djangoapps/video_pipeline/tests/mixins.py | 23 + .../video_pipeline/tests/test_api.py | 99 ++++ .../core/djangoapps/video_pipeline/utils.py | 19 + requirements/edx/base.txt | 2 +- 32 files changed, 1461 insertions(+), 200 deletions(-) create mode 100644 cms/djangoapps/contentstore/views/tests/test_transcript_settings.py create mode 100644 cms/djangoapps/contentstore/views/transcript_settings.py create mode 100644 cms/templates/js/course-video-settings-update-org-credentials-footer.underscore create mode 100644 cms/templates/js/course-video-settings-update-settings-footer.underscore create mode 100644 cms/templates/js/course-video-transcript-preferences.underscore create mode 100644 cms/templates/js/course-video-transcript-provider-empty.underscore create mode 100644 cms/templates/js/course-video-transcript-provider-selected.underscore create mode 100644 cms/templates/js/transcript-organization-credentials.underscore create mode 100644 openedx/core/djangoapps/video_pipeline/__init__.py create mode 100644 openedx/core/djangoapps/video_pipeline/admin.py create mode 100644 openedx/core/djangoapps/video_pipeline/api.py create mode 100644 openedx/core/djangoapps/video_pipeline/migrations/0001_initial.py create mode 100644 openedx/core/djangoapps/video_pipeline/migrations/__init__.py create mode 100644 openedx/core/djangoapps/video_pipeline/models.py create mode 100644 openedx/core/djangoapps/video_pipeline/tests/__init__.py create mode 100644 openedx/core/djangoapps/video_pipeline/tests/mixins.py create mode 100644 openedx/core/djangoapps/video_pipeline/tests/test_api.py create mode 100644 openedx/core/djangoapps/video_pipeline/utils.py diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index ae2b317657..0242ac5723 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -19,6 +19,7 @@ from .export_git import * from .user import * from .tabs import * from .videos import * +from .transcript_settings import * from .transcripts_ajax import * try: from .dev import * diff --git a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py new file mode 100644 index 0000000000..32eb1a0edb --- /dev/null +++ b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py @@ -0,0 +1,171 @@ +import ddt +import json +from mock import Mock, patch + +from django.test.testcases import TestCase + +from contentstore.tests.utils import CourseTestCase +from contentstore.utils import reverse_course_url +from contentstore.views.transcript_settings import TranscriptionProviderErrorType, validate_transcript_credentials + + +@ddt.ddt +@patch( + 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', + Mock(return_value=True) +) +class TranscriptCredentialsTest(CourseTestCase): + """ + Tests for transcript credentials handler. + """ + VIEW_NAME = 'transcript_credentials_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_credentials_url = self.get_url_for_course_key(self.course.id) + response = self.client.post(transcript_credentials_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 POST. + """ + transcript_credentials_url = self.get_url_for_course_key(self.course.id) + response = self.client.get(transcript_credentials_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_credentials_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.post(transcript_credentials_url, content_type='application/json') + self.assertEqual(response.status_code, 404) + + @ddt.data( + ( + { + 'provider': 'abc_provider', + 'api_key': '1234' + }, + ({}, None), + 400, + '{\n "error": "Invalid Provider abc_provider."\n}' + ), + ( + { + 'provider': '3PlayMedia', + 'api_key': '11111', + 'api_secret_key': '44444' + }, + ({'error_type': TranscriptionProviderErrorType.INVALID_CREDENTIALS}, False), + 400, + '{\n "error": "The information you entered is incorrect."\n}' + ), + ( + { + 'provider': 'Cielo24', + 'api_key': '12345', + 'username': 'test_user' + }, + ({}, True), + 200, + '' + ) + ) + @ddt.unpack + @patch('contentstore.views.transcript_settings.update_3rd_party_transcription_service_credentials') + def test_transcript_credentials_handler(self, request_payload, update_credentials_response, expected_status_code, + expected_response, mock_update_credentials): + """ + Tests that transcript credentials handler works as expected. + """ + mock_update_credentials.return_value = update_credentials_response + transcript_credentials_url = self.get_url_for_course_key(self.course.id) + response = self.client.post( + transcript_credentials_url, + data=json.dumps(request_payload), + content_type='application/json' + ) + self.assertEqual(response.status_code, expected_status_code) + self.assertEqual(response.content, expected_response) + + +@ddt.ddt +class TranscriptCredentialsValidationTest(TestCase): + """ + Tests for credentials validations. + """ + + @ddt.data( + ( + 'ABC', + { + 'username': 'test_user', + 'password': 'test_pass' + }, + 'Invalid Provider ABC.', + {} + ), + ( + 'Cielo24', + { + 'username': 'test_user' + }, + 'api_key must be specified.', + {} + ), + ( + 'Cielo24', + { + 'username': 'test_user', + 'api_key': 'test_api_key', + 'extra_param': 'extra_value' + }, + '', + { + 'username': 'test_user', + 'api_key': 'test_api_key' + } + ), + ( + '3PlayMedia', + { + 'username': 'test_user' + }, + 'api_key and api_secret_key must be specified.', + {} + ), + ( + '3PlayMedia', + { + 'api_key': 'test_key', + 'api_secret_key': 'test_secret', + 'extra_param': 'extra_value' + }, + '', + { + 'api_key': 'test_key', + 'api_secret_key': 'test_secret' + } + ), + + ) + @ddt.unpack + def test_invalid_credentials(self, provider, credentials, expected_error_message, expected_validated_credentials): + """ + Test validation with invalid transcript credentials. + """ + error_message, validated_credentials = validate_transcript_credentials(provider, **credentials) + # Assert the results. + self.assertEqual(error_message, expected_error_message) + self.assertDictEqual(validated_credentials, expected_validated_credentials) diff --git a/cms/djangoapps/contentstore/views/transcript_settings.py b/cms/djangoapps/contentstore/views/transcript_settings.py new file mode 100644 index 0000000000..86374d469d --- /dev/null +++ b/cms/djangoapps/contentstore/views/transcript_settings.py @@ -0,0 +1,110 @@ +""" +Views related to the transcript preferences feature +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseNotFound +from django.utils.translation import ugettext as _ +from django.views.decorators.http import require_POST +from edxval.api import ( + get_3rd_party_transcription_plans, + update_transcript_credentials_state_for_org, +) +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 util.json_request import JsonResponse, expect_json + +from contentstore.views.videos import TranscriptProvider + +__all__ = ['transcript_credentials_handler'] + + +class TranscriptionProviderErrorType: + """ + Transcription provider's error types enumeration. + """ + INVALID_CREDENTIALS = 1 + + +def validate_transcript_credentials(provider, **credentials): + """ + Validates transcript credentials. + + Validations: + Providers must be either 3PlayMedia or Cielo24. + In case of: + 3PlayMedia - 'api_key' and 'api_secret_key' are required. + Cielo24 - 'api_key' and 'username' are required. + + It ignores any extra/unrelated parameters passed in credentials and + only returns the validated ones. + """ + error_message, validated_credentials = '', {} + valid_providers = get_3rd_party_transcription_plans().keys() + if provider in valid_providers: + must_have_props = [] + if provider == TranscriptProvider.THREE_PLAY_MEDIA: + must_have_props = ['api_key', 'api_secret_key'] + elif provider == TranscriptProvider.CIELO24: + must_have_props = ['api_key', 'username'] + + missing = [must_have_prop for must_have_prop in must_have_props if must_have_prop not in credentials.keys()] + if missing: + error_message = u'{missing} must be specified.'.format(missing=' and '.join(missing)) + return error_message, validated_credentials + + validated_credentials.update({ + prop: credentials[prop] for prop in must_have_props + }) + else: + error_message = u'Invalid Provider {provider}.'.format(provider=provider) + + return error_message, validated_credentials + + +@expect_json +@login_required +@require_POST +def transcript_credentials_handler(request, course_key_string): + """ + JSON view handler to update the transcript organization credentials. + + Arguments: + request: WSGI request object + course_key_string: A course identifier to extract the org. + + Returns: + - A 200 response if credentials are valid and successfully updated in edx-video-pipeline. + - A 404 response if transcript feature is not enabled for this course. + - A 400 if credentials do not pass validations, hence not updated in edx-video-pipeline. + """ + course_key = CourseKey.from_string(course_key_string) + if not VideoTranscriptEnabledFlag.feature_enabled(course_key): + return HttpResponseNotFound() + + provider = request.json.pop('provider') + error_message, validated_credentials = validate_transcript_credentials(provider=provider, **request.json) + if error_message: + response = JsonResponse({'error': error_message}, status=400) + else: + # Send the validated credentials to edx-video-pipeline. + credentials_payload = dict(validated_credentials, org=course_key.org, provider=provider) + error_response, is_updated = update_3rd_party_transcription_service_credentials(**credentials_payload) + # Send appropriate response based on whether credentials were updated or not. + if is_updated: + # Cache credentials state in edx-val. + update_transcript_credentials_state_for_org(org=course_key.org, provider=provider, exists=is_updated) + response = JsonResponse(status=200) + else: + # Error response would contain error types and the following + # error type is received from edx-video-pipeline whenever we've + # got invalid credentials for a provider. Its kept this way because + # edx-video-pipeline doesn't support i18n translations yet. + error_type = error_response.get('error_type') + if error_type == TranscriptionProviderErrorType.INVALID_CREDENTIALS: + error_message = _('The information you entered is incorrect.') + + response = JsonResponse({'error': error_message}, status=400) + + return response diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index c7ef4be093..7df8135f60 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -1,11 +1,10 @@ """ Views related to the video upload feature """ -from contextlib import closing - import csv import json import logging +from contextlib import closing from datetime import datetime, timedelta from uuid import uuid4 @@ -18,33 +17,38 @@ from django.core.files.images import get_image_dimensions from django.http import HttpResponse, HttpResponseNotFound from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop -from django.views.decorators.http import require_GET, require_POST, require_http_methods +from django.views.decorators.http import require_GET, require_http_methods, require_POST from edxval.api import ( SortDirection, VideoSortField, - create_video, - get_videos_for_course, - remove_video_for_course, - update_video_status, - update_video_image, - get_3rd_party_transcription_plans, - get_transcript_preferences, create_or_update_transcript_preferences, + create_video, + get_3rd_party_transcription_plans, + get_transcript_credentials_state_for_org, + get_transcript_preferences, + get_videos_for_course, remove_transcript_preferences, + remove_video_for_course, + update_video_image, + update_video_status ) from opaque_keys.edx.keys import CourseKey -from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag -from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from contentstore.models import VideoUploadConfig from contentstore.utils import reverse_course_url from edxmako.shortcuts import render_to_response +from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag +from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from util.json_request import JsonResponse, expect_json from .course import get_course_and_check_access - -__all__ = ['videos_handler', 'video_encodings_download', 'video_images_handler', 'transcript_preferences_handler'] +__all__ = [ + 'videos_handler', + 'video_encodings_download', + 'video_images_handler', + 'transcript_preferences_handler', +] LOGGER = logging.getLogger(__name__) @@ -589,7 +593,8 @@ def videos_index_html(course): }, 'is_video_transcript_enabled': is_video_transcript_enabled, 'video_transcript_settings': None, - 'active_transcript_preferences': None + 'active_transcript_preferences': None, + 'transcript_credentials': None } if is_video_transcript_enabled: @@ -598,9 +603,15 @@ def videos_index_html(course): 'transcript_preferences_handler', unicode(course.id) ), + 'transcript_credentials_handler_url': reverse_course_url( + 'transcript_credentials_handler', + unicode(course.id) + ), 'transcription_plans': get_3rd_party_transcription_plans(), } context['active_transcript_preferences'] = get_transcript_preferences(unicode(course.id)) + # Cached state for transcript providers' credentials (org-specific) + context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org) return render_to_response('videos_index.html', context) diff --git a/cms/envs/common.py b/cms/envs/common.py index 4797aa1af6..f1572e34d0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -947,6 +947,9 @@ INSTALLED_APPS = [ # Video module configs (This will be moved to Video once it becomes an XBlock) 'openedx.core.djangoapps.video_config', + # edX Video Pipeline integration + 'openedx.core.djangoapps.video_pipeline', + # For CMS 'contentstore.apps.ContentstoreConfig', diff --git a/cms/static/js/factories/videos_index.js b/cms/static/js/factories/videos_index.js index abbc23b952..1ad46cada1 100644 --- a/cms/static/js/factories/videos_index.js +++ b/cms/static/js/factories/videos_index.js @@ -15,6 +15,7 @@ define([ videoSupportedFileFormats, videoUploadMaxFileSizeInGB, activeTranscriptPreferences, + transcriptOrganizationCredentials, videoTranscriptSettings, isVideoTranscriptEnabled, videoImageSettings @@ -27,6 +28,7 @@ define([ videoUploadMaxFileSizeInGB: videoUploadMaxFileSizeInGB, videoImageSettings: videoImageSettings, activeTranscriptPreferences: activeTranscriptPreferences, + transcriptOrganizationCredentials: transcriptOrganizationCredentials, videoTranscriptSettings: videoTranscriptSettings, isVideoTranscriptEnabled: isVideoTranscriptEnabled, onFileUploadDone: function(activeVideos) { diff --git a/cms/static/js/spec/views/active_video_upload_list_spec.js b/cms/static/js/spec/views/active_video_upload_list_spec.js index 80fe73b87a..920100766f 100644 --- a/cms/static/js/spec/views/active_video_upload_list_spec.js +++ b/cms/static/js/spec/views/active_video_upload_list_spec.js @@ -43,7 +43,7 @@ define( activeTranscriptPreferences: {}, videoTranscriptSettings: { transcript_preferences_handler_url: '', - transcription_plans: {} + transcription_plans: null }, isVideoTranscriptEnabled: isVideoTranscriptEnabled }); diff --git a/cms/static/js/spec/views/course_video_settings_spec.js b/cms/static/js/spec/views/course_video_settings_spec.js index 6035e0b966..be7575ee33 100644 --- a/cms/static/js/spec/views/course_video_settings_spec.js +++ b/cms/static/js/spec/views/course_video_settings_spec.js @@ -8,10 +8,23 @@ define( courseVideoSettingsView, renderCourseVideoSettingsView, destroyCourseVideoSettingsView, + verifyTranscriptPreferences, + verifyTranscriptPreferencesView, + verifyOrganizationCredentialsView, + verifyCredentialFieldsPresent, + verifyOrganizationCredentialField, + verifyMessage, verifyPreferanceErrorState, selectPreference, - chooseProvider, + verifyProviderList, + verifyProviderSelectedView, + verifyCredentialsSaved, + resetProvider, + changeProvider, + submitOrganizationCredentials, transcriptPreferencesUrl = '/transcript_preferences/course-v1:edX+DemoX+Demo_Course', + transcriptCredentialsHandlerUrl = '/transcript_credentials/course-v1:edX+DemoX+Demo_Course', + INTERNAL_SERVER_ERROR = 'An error has occurred. Wait a few minutes, and then try again.', activeTranscriptPreferences = { provider: 'Cielo24', cielo24_fidelity: 'PROFESSIONAL', @@ -21,6 +34,10 @@ define( preferred_languages: ['fr', 'en'], modified: '2017-08-27T12:28:17.421260Z' }, + transcriptOrganizationCredentials = { + Cielo24: true, + '3PlayMedia': true + }, transcriptionPlans = { '3PlayMedia': { languages: { @@ -72,15 +89,37 @@ define( }, display_name: 'Cielo24' } + }, + providers = { + none: { + key: 'none', + value: '', + displayName: 'None' + }, + Cielo24: { + key: 'Cielo24', + value: 'Cielo24', + displayName: 'Cielo24' + }, + '3PlayMedia': { + key: '3PlayMedia', + value: '3PlayMedia', + displayName: '3Play Media' + } }; - renderCourseVideoSettingsView = function(activeTranscriptPreferencesData, transcriptionPlansData) { + renderCourseVideoSettingsView = function(activeTranscriptPreferencesData, transcriptionPlansData, transcriptOrganizationCredentialsData) { // eslint-disable-line max-len + // First destroy old referance to the view if present. + destroyCourseVideoSettingsView(); + courseVideoSettingsView = new CourseVideoSettingsView({ activeTranscriptPreferences: activeTranscriptPreferencesData || null, videoTranscriptSettings: { transcript_preferences_handler_url: transcriptPreferencesUrl, + transcript_credentials_handler_url: transcriptCredentialsHandlerUrl, transcription_plans: transcriptionPlansData || null - } + }, + transcriptOrganizationCredentials: transcriptOrganizationCredentialsData || null }); $courseVideoSettingsEl = courseVideoSettingsView.render().$el; }; @@ -108,10 +147,167 @@ define( $preference.change(); }; - chooseProvider = function(selectedProvider) { + verifyMessage = function(state, message) { + var icon = state === 'error' ? 'fa-info-circle' : 'fa-check-circle'; + expect($courseVideoSettingsEl.find('.course-video-settings-message-wrapper.' + state).html()).toEqual( + '
' + + '' + + '' + message + '' + + '
' + ); + }; + + verifyProviderList = function(selectedProvider) { + var $transcriptProvidersListEl = $courseVideoSettingsEl.find('.transcript-provider-wrapper .transcript-provider-group'); // eslint-disable-line max-len + // Check None provider is selected. + expect($transcriptProvidersListEl.find('input[type=radio]:checked').val()).toEqual(selectedProvider.value); // eslint-disable-line max-len + _.each(providers, function(provider, key) { + $transcriptProvidersListEl.find('label[for=transcript-provider-' + key + ']').val(provider.displayName); // eslint-disable-line max-len + }); + }; + + verifyTranscriptPreferences = function() { + expect($courseVideoSettingsEl.find('#transcript-turnaround').val()).toEqual( + activeTranscriptPreferences.cielo24_turnaround + ); + expect($courseVideoSettingsEl.find('#transcript-fidelity').val()).toEqual( + activeTranscriptPreferences.cielo24_fidelity + ); + expect($courseVideoSettingsEl.find('.transcript-language-container').length).toEqual( + activeTranscriptPreferences.preferred_languages.length + ); + // Now check values are assigned correctly. + expect(courseVideoSettingsView.selectedTurnaroundPlan, activeTranscriptPreferences.cielo24_turnaround); + expect(courseVideoSettingsView.selectedFidelityPlan, activeTranscriptPreferences.cielo24_fidelity); + expect(courseVideoSettingsView.selectedLanguages, activeTranscriptPreferences.preferred_languages); + }; + + verifyProviderSelectedView = function() { + // Verify provider + expect( + $courseVideoSettingsEl.find('.selected-transcript-provider .title').html() + ).toEqual(courseVideoSettingsView.selectedProvider); + + expect($courseVideoSettingsEl.find('.selected-transcript-provider .action-change-provider')).toExist(); + expect( + $courseVideoSettingsEl.find('.selected-transcript-provider .action-change-provider .sr').html() + ).toEqual('Press change to change selected transcript provider.'); + }; + + verifyTranscriptPreferencesView = function() { + expect($courseVideoSettingsEl.find('.course-video-transcript-preferances-wrapper')).toExist(); + }; + + verifyOrganizationCredentialsView = function() { + expect($courseVideoSettingsEl.find('.organization-credentials-content')).toExist(); + }; + + verifyCredentialFieldsPresent = function(fields) { + // Verify correct number of input fields are shown. + expect( + $courseVideoSettingsEl.find( + '.organization-credentials-wrapper .transcript-preferance-wrapper input' + ).length + ).toEqual(_.keys(fields).length + ); + + // Verify individual field has correct label and key. + _.each(fields, function(label, fieldName) { + verifyOrganizationCredentialField(fieldName, label); + }); + }; + + verifyOrganizationCredentialField = function(fieldName, label) { + var elementSelector = courseVideoSettingsView.selectedProvider + '-' + fieldName; + // Verify that correct label is shown. + expect( + $courseVideoSettingsEl.find('.' + elementSelector + '-wrapper label .title').html() + ).toEqual(label); + + // Verify that credential field is shown. + expect( + $courseVideoSettingsEl.find('.' + elementSelector + '-wrapper .' + elementSelector) + ).toExist(); + }; + + verifyCredentialsSaved = function() { + // Verify that success message is shown. + verifyMessage( + 'success', + transcriptionPlans[courseVideoSettingsView.selectedProvider].display_name + ' credentials saved' + ); + + // Also verify that transcript credential state is updated. + expect( + courseVideoSettingsView.transcriptOrganizationCredentials[courseVideoSettingsView.selectedProvider] + ).toBeTruthy(); + + // Verify that selected provider view after credentials are saved. + verifyProviderSelectedView(); + }; + + changeProvider = function(selectedProvider) { + // If Provider Selected view is show, first click on "Change Provider" button to + // show all list of providers. + if ($courseVideoSettingsEl.find('.selected-transcript-provider').length) { + $courseVideoSettingsEl.find('.selected-transcript-provider .action-change-provider').click(); + } $courseVideoSettingsEl.find('#transcript-provider-' + selectedProvider).click(); }; + resetProvider = function() { + var requests = AjaxHelpers.requests(this); + // Set no provider selected + changeProvider('none'); + $courseVideoSettingsEl.find('.action-update-course-video-settings').click(); + + AjaxHelpers.expectRequest( + requests, + 'DELETE', + transcriptPreferencesUrl + ); + + // Send successful empty content response. + AjaxHelpers.respondWithJson(requests, {}); + }; + + submitOrganizationCredentials = function(fieldValues, statusCode, errorMessage) { + var requests = AjaxHelpers.requests(this); + // Click change button to render organization credentials view. + $courseVideoSettingsEl.find('.action-change-provider').click(); + + // Provide organization credentials. + _.each(fieldValues, function(key) { + $courseVideoSettingsEl.find('.' + courseVideoSettingsView.selectedProvider + '-' + key).val(key); + }); + // Click save organization credentials button to save credentials. + $courseVideoSettingsEl.find('.action-update-org-credentials').click(); + + AjaxHelpers.expectRequest( + requests, + 'POST', + transcriptCredentialsHandlerUrl, + JSON.stringify( + _.extend( + {provider: courseVideoSettingsView.selectedProvider}, + fieldValues, + {global: false} + ) + ) + ); + + if (statusCode === 400) { + // Send bad request error response. + AjaxHelpers.respondWithError(requests, statusCode, {error: errorMessage}); + } else if (statusCode === 500) { + // Send internal server error response. + AjaxHelpers.respondWithError(requests, statusCode); + } else { + // Send empty response. + AjaxHelpers.respondWithJson(requests, {}); + } + }; + beforeEach(function() { setFixtures( '
' + @@ -148,17 +344,10 @@ define( }); it('does not populate transcription plans if transcription plans are not provided', function() { - // First detroy old referance to the view. - destroyCourseVideoSettingsView(); - // Create view with empty data. - renderCourseVideoSettingsView(null, null); - - expect($courseVideoSettingsEl.find('.transcript-provider-group').html()).toEqual(''); - expect($courseVideoSettingsEl.find('.transcript-turnaround').html()).toEqual(''); - expect($courseVideoSettingsEl.find('.transcript-fidelity').html()).toEqual(''); - expect($courseVideoSettingsEl.find('.video-source-language').html()).toEqual(''); - expect($courseVideoSettingsEl.find('.transcript-language-menu').html()).toEqual(''); + renderCourseVideoSettingsView(); + // Checking turnaround is sufficient to check preferences are are shown or not. + expect($courseVideoSettingsEl.find('.transcript-turnaround-wrapper')).not.toExist(); }); it('populates transcription plans correctly', function() { @@ -169,39 +358,51 @@ define( it('populates active preferances correctly', function() { // First check preferance are selected correctly in HTML. - expect($courseVideoSettingsEl.find('.transcript-provider-group input:checked').val()).toEqual( - activeTranscriptPreferences.provider - ); - expect($courseVideoSettingsEl.find('.transcript-turnaround').val()).toEqual( - activeTranscriptPreferences.cielo24_turnaround - ); - expect($courseVideoSettingsEl.find('.transcript-fidelity').val()).toEqual( - activeTranscriptPreferences.cielo24_fidelity - ); - expect($courseVideoSettingsEl.find('.video-source-language').val()).toEqual( - activeTranscriptPreferences.video_source_language - ); - expect($courseVideoSettingsEl.find('.transcript-language-container').length).toEqual( - activeTranscriptPreferences.preferred_languages.length + verifyTranscriptPreferences(); + }); + + it('resets to active preferences when clicked on cancel', function() { + var selectedProvider = '3PlayMedia'; + + renderCourseVideoSettingsView( + activeTranscriptPreferences, + transcriptionPlans, + transcriptOrganizationCredentials ); - // Now check values are assigned correctly. - expect(courseVideoSettingsView.selectedProvider, activeTranscriptPreferences.provider); - expect(courseVideoSettingsView.selectedTurnaroundPlan, activeTranscriptPreferences.cielo24_turnaround); - expect(courseVideoSettingsView.selectedFidelityPlan, activeTranscriptPreferences.cielo24_fidelity); - expect( - courseVideoSettingsView.selectedSourceLanguage, - activeTranscriptPreferences.video_source_language + // First check preferance are selected correctly in HTML. + verifyTranscriptPreferences(); + expect(courseVideoSettingsView.selectedProvider, providers.Cielo24); + + // Now change preferences. + // Select provider. + changeProvider(selectedProvider); + expect(courseVideoSettingsView.selectedProvider, selectedProvider); + + // Select turnaround. + selectPreference( + '.transcript-turnaround', + transcriptionPlans[selectedProvider].turnaround.default ); - expect(courseVideoSettingsView.selectedLanguages, activeTranscriptPreferences.preferred_languages); + expect( + courseVideoSettingsView.selectedTurnaroundPlan, + transcriptionPlans[selectedProvider].turnaround.default + ); + + // Now click cancel button and verify active preferences are shown. + $courseVideoSettingsEl.find('.action-cancel-course-video-settings').click(); + verifyTranscriptPreferences(); + expect(courseVideoSettingsView.selectedProvider, providers.Cielo24); }); it('shows video source language directly in case of 3Play provider', function() { var sourceLanguages, selectedProvider = '3PlayMedia'; - // Select CIELIO24 provider - chooseProvider(selectedProvider); + renderCourseVideoSettingsView(null, transcriptionPlans, transcriptOrganizationCredentials); + + // Select provider + changeProvider(selectedProvider); expect(courseVideoSettingsView.selectedProvider).toEqual(selectedProvider); // Verify source langauges menu is shown. @@ -219,10 +420,10 @@ define( selectedProvider = 'Cielo24', selectedFidelity = 'PROFESSIONAL'; - renderCourseVideoSettingsView(null, transcriptionPlans); + renderCourseVideoSettingsView(null, transcriptionPlans, transcriptOrganizationCredentials); - // Select CIELIO24 provider - chooseProvider(selectedProvider); + // Select provider + changeProvider(selectedProvider); expect(courseVideoSettingsView.selectedProvider).toEqual(selectedProvider); // Verify source language is not shown. @@ -254,8 +455,10 @@ define( selectedProvider = 'Cielo24', selectedFidelity = 'PROFESSIONAL'; - // Select CIELIO24 provider - chooseProvider(selectedProvider); + renderCourseVideoSettingsView(null, transcriptionPlans, transcriptOrganizationCredentials); + + // Select provider + changeProvider(selectedProvider); expect(courseVideoSettingsView.selectedProvider).toEqual(selectedProvider); // Select fidelity @@ -283,8 +486,10 @@ define( selectedProvider = 'Cielo24', selectedFidelity = 'MECHANICAL'; - // Select CIELIO24 provider - chooseProvider(selectedProvider); + renderCourseVideoSettingsView(null, transcriptionPlans, transcriptOrganizationCredentials); + + // Select provider + changeProvider(selectedProvider); expect(courseVideoSettingsView.selectedProvider).toEqual(selectedProvider); // Select fidelity @@ -332,37 +537,16 @@ define( }); // Verify that success message is shown. - expect($courseVideoSettingsEl.find('.course-video-settings-message-wrapper.success').html()).toEqual( - '
' + - '' + - 'Settings updated' + - '
' - ); + verifyMessage('success', 'Settings updated'); }); it('removes transcript settings on update settings button click when no provider is selected', function() { - var requests = AjaxHelpers.requests(this); - - // Set no provider selected - courseVideoSettingsView.selectedProvider = null; - $courseVideoSettingsEl.find('.action-update-course-video-settings').click(); - - AjaxHelpers.expectRequest( - requests, - 'DELETE', - transcriptPreferencesUrl - ); - - // Send successful empty content response. - AjaxHelpers.respondWithJson(requests, {}); + // Reset to None provider + resetProvider(); + verifyProviderList(providers.none); // Verify that success message is shown. - expect($courseVideoSettingsEl.find('.course-video-settings-message-wrapper.success').html()).toEqual( - '
' + - '' + - 'Settings updated' + - '
' - ); + verifyMessage('success', 'Automatic transcripts are disabled.'); }); it('shows error message if server sends error', function() { @@ -390,12 +574,7 @@ define( }); // Verify that error message is shown. - expect($courseVideoSettingsEl.find('.course-video-settings-message-wrapper.error').html()).toEqual( - '
' + - '' + - 'Error message' + - '
' - ); + verifyMessage('error', 'Error message'); }); it('implies preferences are required if not selected when saving preferances', function() { @@ -423,8 +602,203 @@ define( verifyPreferanceErrorState($courseVideoSettingsEl.find('.transcript-languages-wrapper'), false); }); + it('shows provider selected view if active provider is present', function() { + var $selectedProviderContainerEl = $courseVideoSettingsEl.find('.transcript-provider-wrapper .selected-transcript-provider'); // eslint-disable-line max-len + expect($selectedProviderContainerEl.find('span').html()).toEqual(courseVideoSettingsView.selectedProvider); // eslint-disable-line max-len + expect($selectedProviderContainerEl.find('button.action-change-provider')).toExist(); + // Verify provider list view is not shown. + expect($courseVideoSettingsEl.find('.transcript-provider-wrapper .transcript-provider-group')).not.toExist(); // eslint-disable-line max-len + }); + + it('does not show transcript preferences or organization credentials if None provider is saved', function() { // eslint-disable-line max-len + renderCourseVideoSettingsView(null, transcriptionPlans); + + // Check None provider + resetProvider(); + verifyProviderList(providers.none); + + // Verify selected provider view is not shown. + expect($courseVideoSettingsEl.find('.transcript-provider-wrapper .selected-transcript-provider')).not.toExist(); // eslint-disable-line max-len + }); + + it('does not show transcript preferences or organization credentials if None provider is checked', function() { // eslint-disable-line max-len + renderCourseVideoSettingsView(null, transcriptionPlans); + + // Check None provider + resetProvider(); + verifyProviderList(providers.none); + + // Verify selected provider view is not shown. + expect($courseVideoSettingsEl.find('.transcript-provider-wrapper .selected-transcript-provider')).not.toExist(); // eslint-disable-line max-len + // Verify transcript preferences are not shown. + expect($courseVideoSettingsEl.find('.course-video-transcript-preferances-wrapper')).not.toExist(); + // Verify org credentials are not shown. + expect($courseVideoSettingsEl.find('.organization-credentials-content')).not.toExist(); + }); + + it('shows organization credentials when organization credentials for selected provider are not present', function() { // eslint-disable-line max-len + renderCourseVideoSettingsView(null, transcriptionPlans); + + // Check Cielo24 provider + changeProvider(providers.Cielo24.key); + verifyProviderList(providers.Cielo24); + + // Verify organization credentials are shown. + verifyOrganizationCredentialsView(); + + // Verify transcript preferences are not shown. + expect($courseVideoSettingsEl.find('.course-video-transcript-preferances-wrapper')).not.toExist(); + }); + + it('shows transcript preferences when organization credentials for selected provider are present', function() { // eslint-disable-line max-len + renderCourseVideoSettingsView(null, transcriptionPlans, transcriptOrganizationCredentials); + + // Check Cielo24 provider + changeProvider('Cielo24'); + verifyProviderList(providers.Cielo24); + + // Verify organization credentials are not shown. + expect($courseVideoSettingsEl.find('.organization-credentials-content')).not.toExist(); + + // Verify transcript preferences are shown. + verifyTranscriptPreferencesView(); + }); + + it('shows organization credentials view if clicked on change provider button', function() { + // Verify organization credentials view is not shown initially. + expect($courseVideoSettingsEl.find('.organization-credentials-content')).not.toExist(); + + verifyProviderSelectedView(); + // Click change button to render organization credentials view. + $courseVideoSettingsEl.find('.action-change-provider').click(); + + // Verify organization credentials is now shown. + verifyOrganizationCredentialsView(); + }); + + it('shows cielo specific organization credentials fields only', function() { + verifyProviderSelectedView(); + // Click change button to render organization credentials view. + $courseVideoSettingsEl.find('.action-change-provider').click(); + + // Verify api key is present. + verifyCredentialFieldsPresent({ + 'api-key': 'API Key', + username: 'Username' + }); + }); + + it('shows 3play specific organization credentials fields only', function() { + // Set selected provider to 3Play Media + changeProvider('3PlayMedia'); + + // Verify api key and api secret input fields are present. + verifyCredentialFieldsPresent({ + 'api-key': 'API Key', + 'api-secret': 'API Secret' + }); + }); + + it('shows warning message when changing organization credentials if present already', function() { + // Set selectedProvider organization credentials. + courseVideoSettingsView.transcriptOrganizationCredentials[courseVideoSettingsView.selectedProvider] = true; // eslint-disable-line max-len + + verifyProviderSelectedView(); + // Click change button to render organization credentials view. + $courseVideoSettingsEl.find('.action-change-provider').click(); + + // Verify credentials are shown + verifyOrganizationCredentialsView(); + // Verify warning message is shown. + expect($courseVideoSettingsEl.find('.transcription-account-details.warning')).toExist(); + // Verify message + expect($courseVideoSettingsEl.find('.transcription-account-details').html()).toEqual( + 'This action updates the ' + courseVideoSettingsView.selectedProvider + + ' information for your entire organization.' + ); + }); + + it('does not show warning message when changing organization credentials if not present already', function() { // eslint-disable-line max-len + verifyProviderSelectedView(); + // Click change button to render organization credentials view. + $courseVideoSettingsEl.find('.action-change-provider').click(); + + // Verify warning message is not shown. + expect($courseVideoSettingsEl.find('.transcription-account-details.warning')).not.toExist(); + // Initial detail message is shown instead. + expect($courseVideoSettingsEl.find('.transcription-account-details').html()).toEqual( + 'Enter the account information for your organization.' + ); + }); + + it('shows validation errors if no organization credentials are provided when saving credentials', function() { // eslint-disable-line max-len + // Set selected provider to 3Play Media + changeProvider('3PlayMedia'); + + // Click save organization credentials button to save credentials. + $courseVideoSettingsEl.find('.action-update-org-credentials').click(); + + verifyPreferanceErrorState( + $courseVideoSettingsEl.find('.' + courseVideoSettingsView.selectedProvider + '-api-key-wrapper'), + true + ); + + verifyPreferanceErrorState( + $courseVideoSettingsEl.find('.' + courseVideoSettingsView.selectedProvider + '-api-secret-wrapper'), + true + ); + }); + + it('saves cielo organization credentials on clicking save credentials button', function() { + verifyProviderSelectedView(); + submitOrganizationCredentials({ + api_key: 'api-key', + username: 'username' + }); + + verifyCredentialsSaved(); + }); + + it('saves 3Play organization credentials on clicking save credentials button', function() { + verifyProviderSelectedView(); + + // Set selected provider to 3Play Media + changeProvider('3PlayMedia'); + + submitOrganizationCredentials({ + api_key: 'api-key', + api_secret_key: 'api-secret' + }); + + verifyCredentialsSaved(); + }); + + it('shows error message on saving organization credentials if server sends bad request error', function() { + verifyProviderSelectedView(); + + submitOrganizationCredentials({ + api_key: 'api-key', + username: 'username' + }, 400, 'Error saving credentials.'); + + // Verify that bad request error message is shown. + verifyMessage('error', 'Error saving credentials.'); + }); + + it('shows error message on saving organization credentials if server sends error', function() { + verifyProviderSelectedView(); + + submitOrganizationCredentials({ + api_key: 'api-key', + username: 'username' + }, 500); + + // Verify that server error message is shown. + verifyMessage('error', INTERNAL_SERVER_ERROR); + }); + // TODO: Add more tests like clicking on add language, remove and their scenarios and some other tests - // like N/A selected, specific provider selected tests, specific preferance selected tests etc. + // for specific preferance selected tests etc. - See EDUCATOR-1478 }); } ); diff --git a/cms/static/js/views/active_video_upload_list.js b/cms/static/js/views/active_video_upload_list.js index 88afe648dc..6b0d067927 100644 --- a/cms/static/js/views/active_video_upload_list.js +++ b/cms/static/js/views/active_video_upload_list.js @@ -42,6 +42,7 @@ define([ this.concurrentUploadLimit = options.concurrentUploadLimit || 0; this.postUrl = options.postUrl; this.activeTranscriptPreferences = options.activeTranscriptPreferences; + this.transcriptOrganizationCredentials = options.transcriptOrganizationCredentials; this.videoTranscriptSettings = options.videoTranscriptSettings; this.isVideoTranscriptEnabled = options.isVideoTranscriptEnabled; this.videoSupportedFileFormats = options.videoSupportedFileFormats; @@ -85,6 +86,7 @@ define([ if (this.isVideoTranscriptEnabled) { this.courseVideoSettingsView = new CourseVideoSettingsView({ activeTranscriptPreferences: this.activeTranscriptPreferences, + transcriptOrganizationCredentials: this.transcriptOrganizationCredentials, videoTranscriptSettings: this.videoTranscriptSettings }); this.courseVideoSettingsView.render(); diff --git a/cms/static/js/views/course_video_settings.js b/cms/static/js/views/course_video_settings.js index bbaa675af5..2126c92717 100644 --- a/cms/static/js/views/course_video_settings.js +++ b/cms/static/js/views/course_video_settings.js @@ -3,16 +3,26 @@ */ define([ 'jquery', 'backbone', 'underscore', 'gettext', 'moment', + 'common/js/components/utils/view_utils', 'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/string-utils', - 'text!templates/course-video-settings.underscore' + 'text!templates/course-video-settings.underscore', + 'text!templates/course-video-transcript-preferences.underscore', + 'text!templates/course-video-transcript-provider-empty.underscore', + 'text!templates/course-video-transcript-provider-selected.underscore', + 'text!templates/transcript-organization-credentials.underscore', + 'text!templates/course-video-settings-update-settings-footer.underscore', + 'text!templates/course-video-settings-update-org-credentials-footer.underscore' ], -function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSettingsTemplate) { +function($, Backbone, _, gettext, moment, ViewUtils, HtmlUtils, StringUtils, TranscriptSettingsTemplate, + TranscriptPreferencesTemplate, TranscriptProviderEmptyStateTemplate, TranscriptProviderSelectedStateTemplate, + OrganizationCredentialsTemplate, UpdateSettingsFooterTemplate, OrganizationCredentialsFooterTemplate) { 'use strict'; var CourseVideoSettingsView, CIELO24 = 'Cielo24', - THREE_PLAY_MEDIA = '3PlayMedia'; + THREE_PLAY_MEDIA = '3PlayMedia', + INTERNAL_SERVER_ERROR_MESSAGE = gettext('An error has occurred. Wait a few minutes, and then try again.'); CourseVideoSettingsView = Backbone.View.extend({ el: 'div.video-transcript-settings-wrapper', @@ -24,16 +34,27 @@ function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSett 'change #video-source-language': 'videoSourceLanguageSelected', 'click .action-add-language': 'languageSelected', 'click .action-remove-language': 'languageRemoved', + 'click .action-change-provider': 'renderOrganizationCredentials', + 'click .action-update-org-credentials': 'updateOrganizationCredentials', 'click .action-update-course-video-settings': 'updateCourseVideoSettings', + 'click .action-cancel-course-video-settings': 'discardChanges', 'click .action-close-course-video-settings': 'closeCourseVideoSettings' }, initialize: function(options) { var videoTranscriptSettings = options.videoTranscriptSettings; this.activeTranscriptionPlan = options.activeTranscriptPreferences; + this.transcriptOrganizationCredentials = _.extend({}, options.transcriptOrganizationCredentials); this.availableTranscriptionPlans = videoTranscriptSettings.transcription_plans; this.transcriptHandlerUrl = videoTranscriptSettings.transcript_preferences_handler_url; + this.transcriptCredentialsHandlerUrl = videoTranscriptSettings.transcript_credentials_handler_url; this.template = HtmlUtils.template(TranscriptSettingsTemplate); + this.transcriptPreferencesTemplate = HtmlUtils.template(TranscriptPreferencesTemplate); + this.organizationCredentialsTemplate = HtmlUtils.template(OrganizationCredentialsTemplate); + this.organizationCredentialsFooterTemplate = HtmlUtils.template(OrganizationCredentialsFooterTemplate); + this.updateSettingsFooterTemplate = HtmlUtils.template(UpdateSettingsFooterTemplate); + this.transcriptProviderEmptyStateTemplate = HtmlUtils.template(TranscriptProviderEmptyStateTemplate); + this.transcriptProviderSelectedStateTemplate = HtmlUtils.template(TranscriptProviderSelectedStateTemplate); this.setActiveTranscriptPlanData(); this.selectedLanguages = []; }, @@ -49,7 +70,7 @@ function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSett // Click anywhere outside the course video settings pane would close the pane. $(document).click(function(event) { - // if the target of the click isn't the container nor a descendant of the contain + // If the target of the click isn't the container nor a descendant of the contain if (!self.$el.is(event.target) && self.$el.has(event.target).length === 0) { self.closeCourseVideoSettings(); } @@ -160,7 +181,35 @@ function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSett providerSelected: function(event) { this.resetPlanData(); this.selectedProvider = event.target.value; - this.renderPreferences(); + // Re-render view + this.reRenderView(); + }, + + reRenderView: function() { + var $courseVideoSettingsContentEl = this.$el.find('.course-video-settings-content'), + dateModified = this.activeTranscriptionPlan ? + moment.utc(this.activeTranscriptionPlan.modified).format('ll') : ''; + + if (!this.selectedProvider) { + // Hide organization credentials and transcript preferences views + $courseVideoSettingsContentEl.hide(); + + // Render footer + HtmlUtils.setHtml( + this.$el.find('.course-video-settings-footer'), + this.updateSettingsFooterTemplate({ + dateModified: dateModified + }) + ); + return; + } + $courseVideoSettingsContentEl.show(); + // If org provider specific credentials are present + if (this.transcriptOrganizationCredentials[this.selectedProvider]) { + this.renderTranscriptPreferences(); + } else { + this.renderOrganizationCredentials(); + } }, languageSelected: function(event) { @@ -187,43 +236,56 @@ function($, Backbone, _, gettext, moment, HtmlUtils, StringUtils, TranscriptSett $(event.target.parentElement).parent().remove(); // Remove language from selected languages. - this.selectedLanguages = _.without(this.selectedLanguages, selectedLanguage); + this.selectedLanguages = this.activeLanguages = _.without(this.selectedLanguages, selectedLanguage); // Populate menu again to reflect latest changes. this.populateLanguageMenu(); }, - renderProviders: function() { - var self = this, - providerPlan = self.availableTranscriptionPlans, - $providerEl = self.$el.find('.transcript-provider-group'); + renderProviders: function(state) { + var $transcriptProviderWrapperEl = this.$el.find('.transcript-provider-wrapper'); + if (!state) { + state = this.selectedProvider ? 'selected' : 'empty'; // eslint-disable-line no-param-reassign + } - if (providerPlan) { + // If no transcription plans are sentm return. + if (!this.availableTranscriptionPlans) { + return; + } + if (state === 'empty') { HtmlUtils.setHtml( - $providerEl, - HtmlUtils.interpolateHtml( - HtmlUtils.HTML(''), // eslint-disable-line max-len - { - text: gettext('N/A'), - checked: self.selectedProvider === '' ? 'checked' : '' - } - ) - ); - - _.each(providerPlan, function(providerObject, key) { - var checked = self.selectedProvider === key ? 'checked' : ''; - HtmlUtils.append( - $providerEl, - HtmlUtils.interpolateHtml( - HtmlUtils.HTML('