Merge pull request #9615 from edx/release-2015-09-02-merge-to-master
Release 2015 09 02 merge to master
This commit is contained in:
@@ -92,12 +92,11 @@ class StubYouTubeHandler(StubHttpRequestHandler):
|
||||
self._send_video_response(youtube_id, "I'm youtube.")
|
||||
|
||||
elif 'get_youtube_api' in self.path:
|
||||
# Delay the response to simulate network latency
|
||||
time.sleep(self.server.config.get('time_to_response', self.DEFAULT_DELAY_SEC))
|
||||
if self.server.config.get('youtube_api_blocked'):
|
||||
self.send_response(404, content='', headers={'Content-type': 'text/plain'})
|
||||
else:
|
||||
# Delay the response to simulate network latency
|
||||
time.sleep(self.server.config.get('time_to_response', self.DEFAULT_DELAY_SEC))
|
||||
|
||||
# Get the response to send from YouTube.
|
||||
# We need to do this every time because Google sometimes sends different responses
|
||||
# as part of their own experiments, which has caused our tests to become "flaky"
|
||||
|
||||
@@ -595,7 +595,11 @@ function (VideoPlayer, i18n, moment) {
|
||||
'[Video info]: YouTube returned an error for ' +
|
||||
'video with id "' + self.id + '".'
|
||||
);
|
||||
self.loadHtmlPlayer();
|
||||
// If the video is already loaded in `_waitForYoutubeApi` by the
|
||||
// time we get here, then we shouldn't load it again.
|
||||
if (!self.htmlPlayerLoaded) {
|
||||
self.loadHtmlPlayer();
|
||||
}
|
||||
});
|
||||
|
||||
window.Video.loadYouTubeIFrameAPI(scriptTag);
|
||||
|
||||
@@ -384,13 +384,36 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
|
||||
self.assertTrue(self.video.is_video_rendered('html5'))
|
||||
|
||||
def test_video_with_youtube_blocked(self):
|
||||
def test_video_with_youtube_blocked_with_default_response_time(self):
|
||||
"""
|
||||
Scenario: Video is rendered in HTML5 mode when the YouTube API is blocked
|
||||
Given the YouTube API is blocked
|
||||
And the course has a Video component in "Youtube_HTML5" mode
|
||||
Then the video has rendered in "HTML5" mode
|
||||
And only one video has rendered
|
||||
"""
|
||||
# configure youtube server
|
||||
self.youtube_configuration.update({
|
||||
'youtube_api_blocked': True,
|
||||
})
|
||||
|
||||
self.metadata = self.metadata_for_mode('youtube_html5')
|
||||
|
||||
self.navigate_to_video()
|
||||
|
||||
self.assertTrue(self.video.is_video_rendered('html5'))
|
||||
|
||||
# The video should only be loaded once
|
||||
self.assertEqual(len(self.video.q(css='video')), 1)
|
||||
|
||||
def test_video_with_youtube_blocked_delayed_response_time(self):
|
||||
"""
|
||||
Scenario: Video is rendered in HTML5 mode when the YouTube API is blocked
|
||||
Given the YouTube server response time is greater than 1.5 seconds
|
||||
And the YouTube API is blocked
|
||||
And the course has a Video component in "Youtube_HTML5" mode
|
||||
Then the video has rendered in "HTML5" mode
|
||||
And only one video has rendered
|
||||
"""
|
||||
# configure youtube server
|
||||
self.youtube_configuration.update({
|
||||
@@ -404,6 +427,9 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
|
||||
self.assertTrue(self.video.is_video_rendered('html5'))
|
||||
|
||||
# The video should only be loaded once
|
||||
self.assertEqual(len(self.video.q(css='video')), 1)
|
||||
|
||||
def test_html5_video_rendered_with_youtube_captions(self):
|
||||
"""
|
||||
Scenario: User should see Youtube captions for If there are no transcripts
|
||||
|
||||
@@ -26,7 +26,7 @@ class LtiConsumer(models.Model):
|
||||
consumer_name = models.CharField(max_length=255, unique=True)
|
||||
consumer_key = models.CharField(max_length=32, unique=True, db_index=True)
|
||||
consumer_secret = models.CharField(max_length=32, unique=True)
|
||||
instance_guid = models.CharField(max_length=255, null=True, unique=True)
|
||||
instance_guid = models.CharField(max_length=255, blank=True, null=True, unique=True)
|
||||
|
||||
@staticmethod
|
||||
def get_or_supplement(instance_guid, consumer_key):
|
||||
|
||||
@@ -3,19 +3,40 @@ Helper functions for managing interactions with the LTI outcomes service defined
|
||||
in LTI v1.1.
|
||||
"""
|
||||
|
||||
from hashlib import sha1
|
||||
from base64 import b64encode
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
from lxml.builder import ElementMaker
|
||||
from oauthlib.oauth1 import Client
|
||||
from oauthlib.common import to_unicode
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
import requests_oauthlib
|
||||
import uuid
|
||||
|
||||
from lti_provider.models import GradedAssignment, OutcomeService
|
||||
|
||||
log = logging.getLogger("edx.lti_provider")
|
||||
|
||||
|
||||
class BodyHashClient(Client):
|
||||
"""
|
||||
OAuth1 Client that adds body hash support (required by LTI).
|
||||
|
||||
The default Client doesn't support body hashes, so we have to add it ourselves.
|
||||
The spec:
|
||||
https://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
|
||||
"""
|
||||
def get_oauth_params(self, request):
|
||||
"""Override get_oauth_params to add the body hash."""
|
||||
params = super(BodyHashClient, self).get_oauth_params(request)
|
||||
digest = b64encode(sha1(request.body.encode('UTF-8')).digest())
|
||||
params.append((u'oauth_body_hash', to_unicode(digest)))
|
||||
return params
|
||||
|
||||
|
||||
def store_outcome_parameters(request_params, user, lti_consumer):
|
||||
"""
|
||||
Determine whether a set of LTI launch parameters contains information about
|
||||
@@ -168,7 +189,13 @@ def sign_and_send_replace_result(assignment, xml):
|
||||
# message. Testing with Canvas throws an error when this field is included.
|
||||
# This code may need to be revisited once we test with other LMS platforms,
|
||||
# and confirm whether there's a bug in Canvas.
|
||||
oauth = requests_oauthlib.OAuth1(consumer_key, consumer_secret)
|
||||
oauth = requests_oauthlib.OAuth1(
|
||||
consumer_key,
|
||||
consumer_secret,
|
||||
signature_method='HMAC-SHA1',
|
||||
client_class=BodyHashClient,
|
||||
force_include_body=True
|
||||
)
|
||||
|
||||
headers = {'content-type': 'application/xml'}
|
||||
response = requests.post(
|
||||
@@ -177,6 +204,7 @@ def sign_and_send_replace_result(assignment, xml):
|
||||
auth=oauth,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ def increment_assignment_versions(course_key, usage_key, user_id):
|
||||
return assignments
|
||||
|
||||
|
||||
@CELERY_APP.task
|
||||
@CELERY_APP.task(name='lti_provider.tasks.send_composite_outcome')
|
||||
def send_composite_outcome(user_id, course_id, assignment_id, version):
|
||||
"""
|
||||
Calculate and transmit the score for a composite module (such as a
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
"""
|
||||
Tests for the LTI outcome service handlers, both in outcomes.py and in tasks.py
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
from lxml import etree
|
||||
from mock import patch, MagicMock, ANY
|
||||
import requests_oauthlib
|
||||
import requests
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from lti_provider.models import GradedAssignment, LtiConsumer, OutcomeService
|
||||
import lti_provider.outcomes as outcomes
|
||||
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory, check_mongo_calls
|
||||
|
||||
|
||||
def create_score(earned, possible):
|
||||
"""
|
||||
Create a new mock Score object with specified earned and possible values
|
||||
"""
|
||||
score = MagicMock()
|
||||
score.possible = possible
|
||||
score.earned = earned
|
||||
return score
|
||||
|
||||
|
||||
class StoreOutcomeParametersTest(TestCase):
|
||||
"""
|
||||
Tests for the store_outcome_parameters method in outcomes.py
|
||||
@@ -301,6 +295,47 @@ class XmlHandlingTest(TestCase):
|
||||
self.assertFalse(outcomes.check_replace_result_response(response))
|
||||
|
||||
|
||||
class TestBodyHashClient(unittest.TestCase):
|
||||
"""
|
||||
Test our custom BodyHashClient
|
||||
|
||||
This Client should do everything a normal oauthlib.oauth1.Client would do,
|
||||
except it also adds oauth_body_hash to the Authorization headers.
|
||||
"""
|
||||
def test_simple_message(self):
|
||||
oauth = requests_oauthlib.OAuth1(
|
||||
'1000000000000000', # fake consumer key
|
||||
'2000000000000000', # fake consumer secret
|
||||
signature_method='HMAC-SHA1',
|
||||
client_class=outcomes.BodyHashClient,
|
||||
force_include_body=True
|
||||
)
|
||||
headers = {'content-type': 'application/xml'}
|
||||
req = requests.Request(
|
||||
'POST',
|
||||
"http://example.edx.org/fake",
|
||||
data="Hello world!",
|
||||
auth=oauth,
|
||||
headers=headers
|
||||
)
|
||||
prepped_req = req.prepare()
|
||||
|
||||
# Make sure that our body hash is now part of the test...
|
||||
self.assertIn(
|
||||
'oauth_body_hash="00hq6RNueFa8QiEjhep5cJRHWAI%3D"',
|
||||
prepped_req.headers['Authorization']
|
||||
)
|
||||
|
||||
# But make sure we haven't wiped out any of the other oauth values
|
||||
# that we would expect to be in the Authorization header as well
|
||||
expected_oauth_headers = [
|
||||
"oauth_nonce", "oauth_timestamp", "oauth_version",
|
||||
"oauth_signature_method", "oauth_consumer_key", "oauth_signature",
|
||||
]
|
||||
for oauth_header in expected_oauth_headers:
|
||||
self.assertIn(oauth_header, prepped_req.headers['Authorization'])
|
||||
|
||||
|
||||
class TestAssignmentsForProblem(ModuleStoreTestCase):
|
||||
"""
|
||||
Test cases for the assignments_for_problem method in outcomes.py
|
||||
|
||||
@@ -574,6 +574,7 @@
|
||||
padding: ($baseline/2) $baseline;
|
||||
background: $gray-l5;
|
||||
border: 1px solid $gray-l4;
|
||||
color: $base-font-color; // Overrides the normal white color in this one case
|
||||
|
||||
// STATE: shown
|
||||
&.is-shown {
|
||||
|
||||
Reference in New Issue
Block a user