Unverified Commit 8d71b98a by Feanil Patel Committed by GitHub

Merge pull request #16479 from edx/revert-16201-transcript-secure-credentials

Revert "Transcript secure credentials"
parents c2daef61 9a35a543
......@@ -19,7 +19,6 @@ 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 *
......
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)
"""
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
"""
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
......@@ -17,38 +18,33 @@ 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_http_methods, require_POST
from django.views.decorators.http import require_GET, require_POST, require_http_methods
from edxval.api import (
SortDirection,
VideoSortField,
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_status,
update_video_image,
update_video_status
get_3rd_party_transcription_plans,
get_transcript_preferences,
create_or_update_transcript_preferences,
remove_transcript_preferences,
)
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__)
......@@ -593,8 +589,7 @@ def videos_index_html(course):
},
'is_video_transcript_enabled': is_video_transcript_enabled,
'video_transcript_settings': None,
'active_transcript_preferences': None,
'transcript_credentials': None
'active_transcript_preferences': None
}
if is_video_transcript_enabled:
......@@ -603,15 +598,9 @@ 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)
......
......@@ -947,9 +947,6 @@ 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',
......
......@@ -15,7 +15,6 @@ define([
videoSupportedFileFormats,
videoUploadMaxFileSizeInGB,
activeTranscriptPreferences,
transcriptOrganizationCredentials,
videoTranscriptSettings,
isVideoTranscriptEnabled,
videoImageSettings
......@@ -28,7 +27,6 @@ define([
videoUploadMaxFileSizeInGB: videoUploadMaxFileSizeInGB,
videoImageSettings: videoImageSettings,
activeTranscriptPreferences: activeTranscriptPreferences,
transcriptOrganizationCredentials: transcriptOrganizationCredentials,
videoTranscriptSettings: videoTranscriptSettings,
isVideoTranscriptEnabled: isVideoTranscriptEnabled,
onFileUploadDone: function(activeVideos) {
......
......@@ -43,7 +43,7 @@ define(
activeTranscriptPreferences: {},
videoTranscriptSettings: {
transcript_preferences_handler_url: '',
transcription_plans: null
transcription_plans: {}
},
isVideoTranscriptEnabled: isVideoTranscriptEnabled
});
......
......@@ -42,7 +42,6 @@ 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;
......@@ -86,7 +85,6 @@ define([
if (this.isVideoTranscriptEnabled) {
this.courseVideoSettingsView = new CourseVideoSettingsView({
activeTranscriptPreferences: this.activeTranscriptPreferences,
transcriptOrganizationCredentials: this.transcriptOrganizationCredentials,
videoTranscriptSettings: this.videoTranscriptSettings
});
this.courseVideoSettingsView.render();
......
......@@ -83,15 +83,6 @@
border: solid 1px $state-danger-border;
}
.organization-credentials-content {
margin-top: ($baseline*1.6);
.org-credentials-wrapper input {
width: 65%;
margin-top: ($baseline*0.8);
display: inline-block;
}
}
.transcript-preferance-wrapper {
margin-top: ($baseline*1.6);
.icon.fa-info-circle {
......@@ -108,7 +99,6 @@
.error-info {
@include font-size(16);
@include margin-left($baseline/2);
}
.transcript-preferance-label {
......@@ -118,15 +108,10 @@
display: block;
}
.transcript-provider-group, .transcript-turnaround, .transcript-fidelity, .video-source-language, .selected-transcript-provider {
.transcript-provider-group, .transcript-turnaround, .transcript-fidelity, .video-source-language {
margin-top: ($baseline*0.8);
}
.selected-transcript-provider {
.action-change-provider {
@include margin-left($baseline/2);
}
}
.transcript-provider-group {
input[type=radio] {
......@@ -143,10 +128,6 @@
display: none;
}
.transcript-languages-wrapper .transcript-preferance-label {
display: inline-block;
}
.transcript-languages-container .languages-container {
margin-top: ($baseline*0.8);
.transcript-language-container {
......@@ -167,9 +148,6 @@
.action-add-language {
@include margin-left($baseline/4);
}
.error-info {
display: inline-block;
}
}
}
.transcript-language-menu, .video-source-language {
......@@ -177,28 +155,11 @@
}
}
.transcription-account-details {
margin-top: ($baseline*0.8);
span {
@include font-size(15);
}
}
.transcription-account-details.warning {
background-color: $state-warning-bg;
padding: ($baseline/2);
}
.action-cancel-course-video-settings {
@include margin-right($baseline/2);
}
.course-video-settings-footer {
margin-top: ($baseline*1.6);
.last-updated-text {
@include font-size(12);
display: block;
margin-top: ($baseline/2);
@include margin-left($baseline/4);
}
}
......
<button class='button-link action-cancel-course-video-settings' aria-describedby='cancel-button-text'>
<%- gettext('Discard Changes') %>
<span id='cancel-button-text' class='sr'><%-gettext('Press discard changes to discard your changes.') %></span>
</button>
<button class='button action-update-org-credentials' aria-describedby='update-org-credentials-button-text'>
<%- gettext('Update Settings') %>
<span id='update-org-credentials-button-text' class='sr'><%-gettext('Press update settings to update the information for your organization.') %></span>
</button>
<button class='button-link action-cancel-course-video-settings' aria-describedby='cancel-button-text'>
<%- gettext('Discard Changes') %>
<span id='cancel-button-text' class='sr'><%-gettext('Press discard changes to discard changes.') %></span>
</button>
<button class="button action-update-course-video-settings" aria-describedby='update-button-text'>
<%- gettext('Update Settings') %>
<span id='update-button-text' class='sr'><%-gettext('Press update settings to update course video settings') %></span>
</button>
<span class='last-updated-text'>
<%if (dateModified) { %>
<%- gettext('Last updated')%> <%- dateModified %>
<% } %>
</span>
......@@ -11,8 +11,49 @@
<div class='course-video-settings-wrapper'>
<div class='course-video-settings-message-wrapper'></div>
<span class="course-video-settings-title"><%- gettext('Course Video Settings') %></span>
<div class='transcript-preferance-wrapper transcript-provider-wrapper'></div>
<div class='course-video-settings-content'></div>
<div class='course-video-settings-footer'></div>
<div class='transcript-preferance-wrapper transcript-provider-wrapper'>
<label class='transcript-preferance-label' for='transcript-provider'><%- gettext('Transcript Provider') %><span class='error-icon' aria-hidden="true"></span></label>
<div class='transcript-provider-group' id='transcript-provider'></div>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper transcript-turnaround-wrapper'>
<label class='transcript-preferance-label' for='transcript-turnaround'><%- gettext('Transcript Turnaround') %><span class='error-icon' aria-hidden="true"></span></label>
<select id='transcript-turnaround' class='transcript-turnaround'></select>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper transcript-fidelity-wrapper'>
<label class='transcript-preferance-label' for='transcript-fidelity'><%- gettext('Transcript Fidelity') %><span class='error-icon' aria-hidden="true"></span></label>
<select id='transcript-fidelity' class='transcript-fidelity'></select>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper video-source-language-wrapper'>
<label class='transcript-preferance-label' for='video-source-language'><%- gettext('Video Source Language') %><span class='error-icon' aria-hidden="true"></span></label>
<select id='video-source-language' class='video-source-language' aria-labelledby="video-source-language-none"></select>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper transcript-languages-wrapper'>
<span class='transcript-preferance-label'><%- gettext('Transcript Languages') %><span class='error-icon' aria-hidden="true"></span></span>
<div class='transcript-languages-container'>
<div class='languages-container'></div>
<div class="transcript-language-menu-container">
<select class="transcript-language-menu" id="transcript-language" aria-labelledby="transcript-language-none"></select>
<div class="add-language-action">
<button class="button-link action-add-language"><%- gettext('Add') %><span class="sr"><%- gettext('Press Add to language') %></span></button>
<span class="error-info" aria-hidden="true"></span>
</div>
</div>
</div>
</div>
<div class='course-video-settings-footer'>
<button class="button button action-update-course-video-settings" aria-describedby='update-button-text'>
<%- gettext('Update Settings') %>
<span id='update-button-text' class='sr'><%-gettext('Press update settings to update course video settings') %></span>
</button>
<span class='last-updated-text'>
<%if (dateModified) { %>
<%- gettext('Last updated')%> <%- dateModified %>
<% } %>
</span>
</div>
</div>
</div>
<div class='course-video-transcript-preferances-wrapper'>
<div class='transcript-preferance-wrapper transcript-turnaround-wrapper'>
<label class='transcript-preferance-label' for='transcript-turnaround'><%- gettext('Transcript Turnaround') %><span class='error-icon' aria-hidden="true"></span></label>
<select id='transcript-turnaround' class='transcript-turnaround'></select>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper transcript-fidelity-wrapper'>
<label class='transcript-preferance-label' for='transcript-fidelity'><%- gettext('Transcript Fidelity') %><span class='error-icon' aria-hidden="true"></span></label>
<select id='transcript-fidelity' class='transcript-fidelity'></select>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper video-source-language-wrapper'>
<label class='transcript-preferance-label' for='video-source-language'><%- gettext('Video Source Language') %><span class='error-icon' aria-hidden="true"></span></label>
<select id='video-source-language' class='video-source-language' aria-labelledby="video-source-language-none"></select>
<span class='error-info' aria-hidden="true"></span>
</div>
<div class='transcript-preferance-wrapper transcript-languages-wrapper'>
<span class='transcript-preferance-label'><%- gettext('Transcript Languages') %></span>
<span class='error-icon' aria-hidden="true"></span>
<div class='transcript-languages-container'>
<div class='languages-container'></div>
<div class="transcript-language-menu-container">
<select class="transcript-language-menu" id="transcript-language" aria-labelledby="transcript-language-none"></select>
<div class="add-language-action">
<button class="button-link action-add-language"><%- gettext('Add') %><span class="sr"><%- gettext('Press Add to language') %></span></button>
</div>
<span class="error-info" aria-hidden="true"></span>
</div>
</div>
</div>
</div>
<label class='transcript-preferance-label' for='transcript-provider'><%- gettext('Automated Transcripts') %></label>
<div class="transcript-provider-group" id="transcript-provider">
<% for (var i = 0; i < providers.length; i++) { %>
<input type='radio' id='transcript-provider-<%- providers[i].key %>' name='transcript-provider' value='<%- providers[i].value %>' <%- providers[i].checked %>>
<label for='transcript-provider-<%- providers[i].key %>'><%- providers[i].name %></label>
<% } %>
</div>
<label class='transcript-preferance-label' for='transcript-provider'><%- gettext('Transcript Provider') %></label>
<div class='selected-transcript-provider'>
<span class='title'><%- selectedProvider %></span>
<button class='button-link action-change-provider' aria-describedby='change-provider-button-text'>
<%- gettext('Change') %>
<span id='change-provider-button-text' class='sr'><%-gettext('Press change to change selected transcript provider.') %></span>
</button>
</div>
<div class='organization-credentials-wrapper'>
<div class='organization-credentials-content'>
<label class='transcript-preferance-label selected-provider-account'><%- selectedProvider.name %> <%- gettext('Account') %></label>
<% if (organizationCredentialsExists) { %>
<div class='transcription-account-details warning'><span><%- gettext("This action updates the {provider} information for your entire organization.").replace('{provider}', selectedProvider.name) %></span></div>
<% } else { %>
<div class='transcription-account-details'><span><%- gettext("Enter the account information for your organization.") %></span></div>
<% } %>
<div class='transcript-preferance-wrapper org-credentials-wrapper <%- selectedProvider.key %>-api-key-wrapper'>
<label class='transcript-preferance-label' for='<%- selectedProvider.key %>-api-key'>
<span class='title'><%- gettext('API Key') %></span>
<span class='error-icon' aria-hidden="true"></span>
</label>
<div>
<input type='text' class='<%- selectedProvider.key %>-api-key'>
<span class='error-info' aria-hidden="true"></span>
</div>
</div>
<% if (selectedProvider.key === THREE_PLAY_MEDIA) { %>
<div class='transcript-preferance-wrapper org-credentials-wrapper <%- selectedProvider.key %>-api-secret-wrapper'>
<label class='transcript-preferance-label' for='<%- selectedProvider.key %>-api-secret'>
<span class='title'><%- gettext('API Secret') %></span>
<span class='error-icon' aria-hidden="true"></span>
</label>
<div>
<input type='text' class='<%- selectedProvider.key %>-api-secret'>
<span class='error-info' aria-hidden="true"></span>
</div>
</div>
<% } else { %>
<div class='transcript-preferance-wrapper org-credentials-wrapper <%- selectedProvider.key %>-username-wrapper'>
<label class='transcript-preferance-label' for='<%- selectedProvider.key %>-username'>
<span class='title'><%- gettext('Username') %></span>
<span class='error-icon' aria-hidden="true"></span>
</label>
<div>
<input type='text' class='<%- selectedProvider.key %>-username'>
<span class='error-info' aria-hidden="true"></span>
</div>
</div>
<% } %>
</div>
</div>
......@@ -39,7 +39,6 @@
${video_supported_file_formats | n, dump_js_escaped_json},
${video_upload_max_file_size | n, dump_js_escaped_json},
${active_transcript_preferences | n, dump_js_escaped_json},
${transcript_credentials | n, dump_js_escaped_json},
${video_transcript_settings | n, dump_js_escaped_json},
${is_video_transcript_enabled | n, dump_js_escaped_json},
${video_image_settings | n, dump_js_escaped_json}
......
......@@ -141,8 +141,6 @@ urlpatterns = [
contentstore.views.video_images_handler, name='video_images_handler'),
url(r'^transcript_preferences/{}$'.format(settings.COURSE_KEY_PATTERN),
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'^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),
......
......@@ -2078,9 +2078,6 @@ 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',
# Bookmarks
'openedx.core.djangoapps.bookmarks.apps.BookmarksConfig',
......
"""
Django admin for Video Pipeline models.
"""
from config_models.admin import ConfigurationModelAdmin
from django.contrib import admin
from openedx.core.djangoapps.video_pipeline.models import VideoPipelineIntegration
admin.site.register(VideoPipelineIntegration, ConfigurationModelAdmin)
"""
API utils in order to communicate to edx-video-pipeline.
"""
import json
import logging
from django.core.exceptions import ObjectDoesNotExist
from slumber.exceptions import HttpClientError
from openedx.core.djangoapps.video_pipeline.models import VideoPipelineIntegration
from openedx.core.djangoapps.video_pipeline.utils import create_video_pipeline_api_client
log = logging.getLogger(__name__)
def update_3rd_party_transcription_service_credentials(**credentials_payload):
"""
Updates the 3rd party transcription service's credentials.
Arguments:
credentials_payload(dict): A payload containing org, provider and its credentials.
Returns:
A Boolean specifying whether the credentials were updated or not
and an error response received from pipeline.
"""
error_response, is_updated = {}, False
pipeline_integration = VideoPipelineIntegration.current()
if pipeline_integration.enabled:
try:
video_pipeline_user = pipeline_integration.get_service_user()
except ObjectDoesNotExist:
return error_response, is_updated
client = create_video_pipeline_api_client(user=video_pipeline_user, api_url=pipeline_integration.api_url)
try:
client.transcript_credentials.post(credentials_payload)
is_updated = True
except HttpClientError as ex:
is_updated = False
log.exception(
('[video-pipeline-service] Unable to update transcript credentials '
'-- org=%s -- provider=%s -- response=%s.'),
credentials_payload.get('org'),
credentials_payload.get('provider'),
ex.content,
)
error_response = json.loads(ex.content)
return error_response, is_updated
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VideoPipelineIntegration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('api_url', models.URLField(help_text='edx-video-pipeline API URL.', verbose_name='Internal API URL')),
('service_username', models.CharField(default=b'video_pipeline_service_user', help_text='Username created for Video Pipeline Integration, e.g. video_pipeline_service_user.', max_length=100)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]
"""
Model to hold edx-video-pipeline configurations.
"""
from config_models.models import ConfigurationModel
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import ugettext_lazy as _
class VideoPipelineIntegration(ConfigurationModel):
"""
Manages configuration for connecting to the edx-video-pipeline service and using its API.
"""
api_url = models.URLField(
verbose_name=_('Internal API URL'),
help_text=_('edx-video-pipeline API URL.')
)
service_username = models.CharField(
max_length=100,
default='video_pipeline_service_user',
null=False,
blank=False,
help_text=_('Username created for Video Pipeline Integration, e.g. video_pipeline_service_user.')
)
def get_service_user(self):
# NOTE: We load the user model here to avoid issues at startup time that result from the hacks
# in lms/startup.py.
User = get_user_model() # pylint: disable=invalid-name
return User.objects.get(username=self.service_username)
"""
Mixins to test video pipeline integration.
"""
from openedx.core.djangoapps.video_pipeline.models import VideoPipelineIntegration
class VideoPipelineIntegrationMixin(object):
"""
Utility for working with the video pipeline service during testing.
"""
video_pipeline_integration_defaults = {
'enabled': True,
'api_url': 'https://video-pipeline.example.com/api/v1/',
'service_username': 'cms_video_pipeline_service_user',
}
def create_video_pipeline_integration(self, **kwargs):
"""
Creates a new `VideoPipelineIntegration` record with `video_pipeline_integration_defaults`,
and it can be updated with any provided overrides.
"""
fields = dict(self.video_pipeline_integration_defaults, **kwargs)
return VideoPipelineIntegration.objects.create(**fields)
"""
Tests for Video Pipeline api utils.
"""
import ddt
import json
from mock import Mock, patch
from django.test.testcases import TestCase
from slumber.exceptions import HttpClientError
from student.tests.factories import UserFactory
from openedx.core.djangoapps.video_pipeline.api import update_3rd_party_transcription_service_credentials
from openedx.core.djangoapps.video_pipeline.tests.mixins import VideoPipelineIntegrationMixin
@ddt.ddt
class TestAPIUtils(VideoPipelineIntegrationMixin, TestCase):
"""
Tests for API Utils.
"""
def setUp(self):
self.pipeline_integration = self.create_video_pipeline_integration()
self.user = UserFactory(username=self.pipeline_integration.service_username)
def test_update_transcription_service_credentials_with_integration_disabled(self):
"""
Test updating the credentials when service integration is disabled.
"""
self.pipeline_integration.enabled = False
self.pipeline_integration.save()
__, is_updated = update_3rd_party_transcription_service_credentials()
self.assertFalse(is_updated)
def test_update_transcription_service_credentials_with_unknown_user(self):
"""
Test updating the credentials when expected service user is not registered.
"""
self.pipeline_integration.service_username = 'non_existent_user'
self.pipeline_integration.save()
__, is_updated = update_3rd_party_transcription_service_credentials()
self.assertFalse(is_updated)
@ddt.data(
{
'username': 'Jason_cielo_24',
'api_key': '12345678',
},
{
'api_key': '12345678',
'api_secret': '11111111',
}
)
@patch('openedx.core.djangoapps.video_pipeline.api.log')
@patch('openedx.core.djangoapps.video_pipeline.utils.EdxRestApiClient')
def test_update_transcription_service_credentials(self, credentials_payload, mock_client, mock_logger):
"""
Tests that the update transcription service credentials api util works as expected.
"""
# Mock the post request
mock_credentials_endpoint = mock_client.return_value.transcript_credentials
# Try updating the transcription service credentials
error_response, is_updated = update_3rd_party_transcription_service_credentials(**credentials_payload)
mock_credentials_endpoint.post.assert_called_with(credentials_payload)
# Making sure log.exception is not called.
self.assertDictEqual(error_response, {})
self.assertFalse(mock_logger.exception.called)
self.assertTrue(is_updated)
@patch('openedx.core.djangoapps.video_pipeline.api.log')
@patch('openedx.core.djangoapps.video_pipeline.utils.EdxRestApiClient')
def test_update_transcription_service_credentials_exceptions(self, mock_client, mock_logger):
"""
Tests that the update transcription service credentials logs the exception occurring
during communication with edx-video-pipeline.
"""
error_content = '{"error_type": "1"}'
# Mock the post request
mock_credentials_endpoint = mock_client.return_value.transcript_credentials
mock_credentials_endpoint.post = Mock(side_effect=HttpClientError(content=error_content))
# try updating the transcription service credentials
credentials_payload = {
'org': 'mit',
'provider': 'ABC Provider',
'api_key': '61c56a8d0'
}
error_response, is_updated = update_3rd_party_transcription_service_credentials(**credentials_payload)
mock_credentials_endpoint.post.assert_called_with(credentials_payload)
# Assert the results.
self.assertFalse(is_updated)
self.assertDictEqual(error_response, json.loads(error_content))
mock_logger.exception.assert_called_with(
'[video-pipeline-service] Unable to update transcript credentials -- org=%s -- provider=%s -- response=%s.',
credentials_payload['org'],
credentials_payload['provider'],
error_content
)
from django.conf import settings
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.lib.token_utils import JwtBuilder
def create_video_pipeline_api_client(user, api_url):
"""
Returns an API client which can be used to make Video Pipeline API requests.
Arguments:
user(User): A requesting user.
api_url(unicode): It is video pipeline's API URL.
"""
jwt_token = JwtBuilder(user).build_token(
scopes=[],
expires_in=settings.OAUTH_ID_TOKEN_EXPIRATION
)
return EdxRestApiClient(api_url, jwt=jwt_token)
......@@ -54,7 +54,7 @@ edx-organizations==0.4.7
edx-rest-api-client==1.7.1
edx-search==1.1.0
edx-submissions==2.0.12
edxval==0.1.3
edxval==0.1.2
event-tracking==0.2.4
feedparser==5.1.3
firebase-token-generator==1.3.2
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment