Merge branch 'release'

Conflicts:
	common/lib/xmodule/xmodule/tests/test_video.py
This commit is contained in:
Christine Lytwynec
2014-06-25 13:51:31 -04:00
29 changed files with 368 additions and 33163 deletions

View File

View File

@@ -0,0 +1,39 @@
"""
Middleware to identify the country of origin of page requests.
Middleware adds `country_code` in session.
Usage:
# To enable the Geoinfo feature on a per-view basis, use:
decorator `django.utils.decorators.decorator_from_middleware(middleware_class)`
"""
import logging
import pygeoip
from ipware.ip import get_real_ip
from django.conf import settings
log = logging.getLogger(__name__)
class CountryMiddleware(object):
"""
Identify the country by IP address.
"""
def process_request(self, request):
"""
Identify the country by IP address.
Store country code in session.
"""
new_ip_address = get_real_ip(request)
old_ip_address = request.session.get('ip_address', None)
if new_ip_address != old_ip_address:
country_code = pygeoip.GeoIP(settings.GEOIP_PATH).country_code_by_addr(new_ip_address)
request.session['country_code'] = country_code
request.session['ip_address'] = new_ip_address
log.debug('Country code for IP: %s is set to %s', new_ip_address, country_code)

View File

@@ -0,0 +1,94 @@
"""
Tests for CountryMiddleware.
"""
from mock import Mock, patch
import pygeoip
from django.test import TestCase
from django.test.utils import override_settings
from django.test.client import RequestFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.models import CourseEnrollment
from student.tests.factories import UserFactory, AnonymousUserFactory
from django.contrib.sessions.middleware import SessionMiddleware
from geoinfo.middleware import CountryMiddleware
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class CountryMiddlewareTests(TestCase):
"""
Tests of CountryMiddleware.
"""
def setUp(self):
self.country_middleware = CountryMiddleware()
self.session_middleware = SessionMiddleware()
self.authenticated_user = UserFactory.create()
self.anonymous_user = AnonymousUserFactory.create()
self.request_factory = RequestFactory()
self.patcher = patch.object(pygeoip.GeoIP, 'country_code_by_addr', self.mock_country_code_by_addr)
self.patcher.start()
def tearDown(self):
self.patcher.stop()
def mock_country_code_by_addr(self, ip_addr):
"""
Gives us a fake set of IPs
"""
ip_dict = {
'117.79.83.1': 'CN',
'117.79.83.100': 'CN',
'4.0.0.0': 'SD',
}
return ip_dict.get(ip_addr, 'US')
def test_country_code_added(self):
request = self.request_factory.get('/somewhere',
HTTP_X_FORWARDED_FOR='117.79.83.1')
request.user = self.authenticated_user
self.session_middleware.process_request(request)
# No country code exists before request.
self.assertNotIn('country_code', request.session)
self.assertNotIn('ip_address', request.session)
self.country_middleware.process_request(request)
# Country code added to session.
self.assertEqual('CN', request.session.get('country_code'))
self.assertEqual('117.79.83.1', request.session.get('ip_address'))
def test_ip_address_changed(self):
request = self.request_factory.get('/somewhere',
HTTP_X_FORWARDED_FOR='4.0.0.0')
request.user = self.anonymous_user
self.session_middleware.process_request(request)
request.session['country_code'] = 'CN'
request.session['ip_address'] = '117.79.83.1'
self.country_middleware.process_request(request)
# Country code is changed.
self.assertEqual('SD', request.session.get('country_code'))
self.assertEqual('4.0.0.0', request.session.get('ip_address'))
def test_ip_address_is_not_changed(self):
request = self.request_factory.get('/somewhere',
HTTP_X_FORWARDED_FOR='117.79.83.1')
request.user = self.anonymous_user
self.session_middleware.process_request(request)
request.session['country_code'] = 'CN'
request.session['ip_address'] = '117.79.83.1'
self.country_middleware.process_request(request)
# Country code is not changed.
self.assertEqual('CN', request.session.get('country_code'))
self.assertEqual('117.79.83.1', request.session.get('ip_address'))
def test_same_country_different_ip(self):
request = self.request_factory.get('/somewhere',
HTTP_X_FORWARDED_FOR='117.79.83.100')
request.user = self.anonymous_user
self.session_middleware.process_request(request)
request.session['country_code'] = 'CN'
request.session['ip_address'] = '117.79.83.1'
self.country_middleware.process_request(request)
# Country code is not changed.
self.assertEqual('CN', request.session.get('country_code'))
self.assertEqual('117.79.83.100', request.session.get('ip_address'))

View File

@@ -113,6 +113,11 @@ class InheritanceMixin(XBlockMixin):
default=[],
scope=Scope.settings
)
video_speed_optimizations = Boolean(
help="Enable Video CDN.",
default=True,
scope=Scope.settings
)
def compute_inherited_metadata(descriptor):

View File

@@ -91,6 +91,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
error_descriptor_class=ErrorDescriptor,
get_user_role=Mock(is_staff=False),
descriptor_runtime=get_test_descriptor_system(),
user_location=Mock(),
)

View File

@@ -14,12 +14,12 @@ the course, section, subsection, unit, etc.
"""
import unittest
import datetime
from mock import Mock
from mock import Mock, patch
from . import LogicTest
from lxml import etree
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.video_module import VideoDescriptor, create_youtube_string
from xmodule.video_module import VideoDescriptor, create_youtube_string, get_video_from_cdn
from .test_import import DummySystem
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
@@ -551,3 +551,33 @@ class VideoExportTestCase(VideoDescriptorTestBase):
xml = self.descriptor.definition_to_xml(None)
expected = '<video url_name="SampleProblem"/>\n'
self.assertEquals(expected, etree.tostring(xml, pretty_print=True))
class VideoCdnTest(unittest.TestCase):
"""
Tests for Video CDN.
"""
@patch('requests.get')
def test_get_video_success(self, cdn_response):
"""
Test successful CDN request.
"""
original_video_url = "http://www.original_video.com/original_video.mp4"
cdn_response_video_url = "http://www.cdn_video.com/cdn_video.mp4"
cdn_response_content = '{{"sources":["{cdn_url}"]}}'.format(cdn_url=cdn_response_video_url)
cdn_response.return_value=Mock(status_code=200, content=cdn_response_content)
fake_cdn_url = 'http://fake_cdn.com/'
self.assertEqual(
get_video_from_cdn(fake_cdn_url, original_video_url),
cdn_response_video_url
)
@patch('requests.get')
def test_get_no_video_exists(self, cdn_response):
"""
Test if no alternative video in CDN exists.
"""
original_video_url = "http://www.original_video.com/original_video.mp4"
cdn_response.return_value=Mock(status_code=404)
fake_cdn_url = 'http://fake_cdn.com/'
self.assertIsNone(get_video_from_cdn(fake_cdn_url, original_video_url))

View File

@@ -36,7 +36,7 @@ from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
from .video_utils import create_youtube_string
from .video_utils import create_youtube_string, get_video_from_cdn
from .video_xfields import VideoFields
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
@@ -93,12 +93,25 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
]}
js_module_name = "Video"
def get_html(self):
track_url = None
download_video_link = None
transcript_download_format = self.transcript_download_format
sources = filter(None, self.html5_sources)
# If the user comes from China use China CDN for html5 videos.
# 'CN' is China ISO 3166-1 country code.
# Video caching is disabled for Studio. User_location is always None in Studio.
# CountryMiddleware disabled for Studio.
cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(self.system.user_location)
if getattr(self, 'video_speed_optimizations', True) and cdn_url:
for index, source_url in enumerate(sources):
new_url = get_video_from_cdn(cdn_url, source_url)
if new_url:
sources[index] = new_url
if self.download_video:
if self.source:
download_video_link = self.source

View File

@@ -1,6 +1,14 @@
"""
Module containts utils specific for video_module but not for transcripts.
"""
import json
import logging
import urllib
import requests
from requests.exceptions import RequestException
log = logging.getLogger(__name__)
def create_youtube_string(module):
@@ -23,3 +31,41 @@ def create_youtube_string(module):
in zip(youtube_speeds, youtube_ids)
if pair[1]
])
def get_video_from_cdn(cdn_base_url, original_video_url):
"""
Get video URL from CDN.
`original_video_url` is the existing video url.
Currently `cdn_base_url` equals 'http://api.xuetangx.com/edx/video?s3_url='
Example of CDN outcome:
{
"sources":
[
"http://cm12.c110.play.bokecc.com/flvs/ca/QxcVl/u39EQbA0Ra-20.mp4",
"http://bm1.42.play.bokecc.com/flvs/ca/QxcVl/u39EQbA0Ra-20.mp4"
],
"s3_url": "http://s3.amazonaws.com/BESTech/CS169/download/CS169_v13_w5l2s3.mp4"
}
where `s3_url` is requested original video url and `sources` is the list of
alternative links.
"""
if not cdn_base_url:
return None
request_url = cdn_base_url + urllib.quote(original_video_url)
try:
cdn_response = requests.get(request_url, timeout=0.5)
except RequestException as err:
log.warning("Error requesting from CDN server at %s", request_url)
log.exception(err)
return None
if cdn_response.status_code == 200:
cdn_content = json.loads(cdn_response.content)
return cdn_content['sources'][0]
else:
return None

View File

@@ -1244,7 +1244,7 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
cache=None, can_execute_unsafe_code=None, replace_course_urls=None,
replace_jump_to_id_urls=None, error_descriptor_class=None, get_real_user=None,
field_data=None, get_user_role=None, rebind_noauth_module_to_user=None,
**kwargs):
user_location=None, **kwargs):
"""
Create a closure around the system environment.
@@ -1340,6 +1340,7 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
self.xmodule_instance = None
self.get_real_user = get_real_user
self.user_location = user_location
self.get_user_role = get_user_role
self.descriptor_runtime = descriptor_runtime