Merge branch 'release'
Conflicts: common/lib/xmodule/xmodule/tests/test_video.py
This commit is contained in:
0
common/djangoapps/geoinfo/__init__.py
Normal file
0
common/djangoapps/geoinfo/__init__.py
Normal file
39
common/djangoapps/geoinfo/middleware.py
Normal file
39
common/djangoapps/geoinfo/middleware.py
Normal 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)
|
||||
0
common/djangoapps/geoinfo/tests/__init__.py
Normal file
0
common/djangoapps/geoinfo/tests/__init__.py
Normal file
94
common/djangoapps/geoinfo/tests/test_middleware.py
Normal file
94
common/djangoapps/geoinfo/tests/test_middleware.py
Normal 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'))
|
||||
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user