Skip to content
Snippets Groups Projects
Commit 7157ec2e authored by Muzaffar yousaf's avatar Muzaffar yousaf Committed by muzaffaryousaf
Browse files

Revert "Revert "Transcript secure credentials""

parent e503ed86
No related branches found
No related tags found
No related merge requests found
Showing
with 1191 additions and 196 deletions
......@@ -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 *
......
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
......@@ -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_or_update_transcript_preferences,
create_video,
get_videos_for_course,
remove_video_for_course,
update_video_status,
update_video_image,
get_3rd_party_transcription_plans,
get_transcript_credentials_state_for_org,
get_transcript_preferences,
create_or_update_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)
......
......@@ -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',
......
......@@ -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) {
......
......@@ -43,7 +43,7 @@ define(
activeTranscriptPreferences: {},
videoTranscriptSettings: {
transcript_preferences_handler_url: '',
transcription_plans: {}
transcription_plans: null
},
isVideoTranscriptEnabled: isVideoTranscriptEnabled
});
......
This diff is collapsed.
......@@ -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();
......
This diff is collapsed.
......@@ -83,6 +83,15 @@
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 {
......@@ -99,6 +108,7 @@
.error-info {
@include font-size(16);
@include margin-left($baseline/2);
}
.transcript-preferance-label {
......@@ -108,10 +118,15 @@
display: block;
}
.transcript-provider-group, .transcript-turnaround, .transcript-fidelity, .video-source-language {
.transcript-provider-group, .transcript-turnaround, .transcript-fidelity, .video-source-language, .selected-transcript-provider {
margin-top: ($baseline*0.8);
}
.selected-transcript-provider {
.action-change-provider {
@include margin-left($baseline/2);
}
}
.transcript-provider-group {
input[type=radio] {
......@@ -128,6 +143,10 @@
display: none;
}
.transcript-languages-wrapper .transcript-preferance-label {
display: inline-block;
}
.transcript-languages-container .languages-container {
margin-top: ($baseline*0.8);
.transcript-language-container {
......@@ -148,6 +167,9 @@
.action-add-language {
@include margin-left($baseline/4);
}
.error-info {
display: inline-block;
}
}
}
.transcript-language-menu, .video-source-language {
......@@ -155,11 +177,28 @@
}
}
.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);
@include margin-left($baseline/4);
display: block;
margin-top: ($baseline/2);
}
}
......
<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,49 +11,8 @@
<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'>
<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 class='transcript-preferance-wrapper transcript-provider-wrapper'></div>
<div class='course-video-settings-content'></div>
<div class='course-video-settings-footer'></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,6 +39,7 @@
${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,6 +141,8 @@ 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),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment