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:
@@ -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 *
|
||||
|
||||
@@ -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)
|
||||
110
cms/djangoapps/contentstore/views/transcript_settings.py
Normal file
110
cms/djangoapps/contentstore/views/transcript_settings.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user