Save credentials in edx-video-pipeline

EDUCATOR-1335 - This Adds a dedicated app which is responsible for communication with edx-video-pipeline service. It also adds the backend for saving 3rd party transcription service credentials on edx-video-pipline and cache it in edx-val.
This commit is contained in:
Qubad786
2017-10-04 20:12:53 +05:00
committed by Mushtaq Ali
parent f3b9ea4311
commit 2310614bb6
17 changed files with 564 additions and 43 deletions

View File

@@ -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 *

View File

@@ -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": "Transcript credentials are not valid."\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)

View File

@@ -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 = _('Transcript credentials are not valid.')
response = JsonResponse({'error': error_message}, status=400)
return response

View File

@@ -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,40 +17,37 @@ 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,
remove_transcript_preferences,
create_video,
get_3rd_party_transcription_plans,
get_transcript_credentials_state_for_org,
update_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',
'transcript_credentials_handler'
]
LOGGER = logging.getLogger(__name__)
@@ -388,32 +384,6 @@ def transcript_preferences_handler(request, course_key_string):
return JsonResponse()
@expect_json
@login_required
@require_POST
def transcript_credentials_handler(request, course_key_string):
"""
JSON view handler to post the transcript organization credentials.
Arguments:
request: WSGI request object
course_key_string: string for course key
Returns: An empty success response or 404 if transcript feature is not enabled
"""
course_key = CourseKey.from_string(course_key_string)
if not VideoTranscriptEnabledFlag.feature_enabled(course_key):
return HttpResponseNotFound()
org = course_key.org
provider = request.json.get('provider')
# TODO: Send organization credentials to edx-pipeline end point.
credentials = update_transcript_credentials_state_for_org(org, provider, exists=True)
return JsonResponse()
@login_required
@require_GET
def video_encodings_download(request, course_key_string):