diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ba7c6b9c9..e3cfe8dd7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Blades: Redirect Chinese students to a Chinese CDN for video. BLD-1052. + Studio: Move Peer Assessment into advanced problems menu. Studio: Support creation and editing of split_test instances (Content Experiments) diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index ac3078e359..cf34fa1067 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -10,6 +10,7 @@ class CourseMetadata(object): The objects have no predefined attrs but instead are obj encodings of the editable metadata. ''' + # The list of fields that wouldn't be shown in Advanced Settings. FILTERED_LIST = ['xml_attributes', 'start', 'end', @@ -21,6 +22,7 @@ class CourseMetadata(object): 'show_timezone', 'format', 'graded', + 'video_speed_optimizations', ] @classmethod diff --git a/common/djangoapps/geoinfo/__init__.py b/common/djangoapps/geoinfo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/geoinfo/middleware.py b/common/djangoapps/geoinfo/middleware.py new file mode 100644 index 0000000000..a6ff5289de --- /dev/null +++ b/common/djangoapps/geoinfo/middleware.py @@ -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) diff --git a/common/djangoapps/geoinfo/tests/__init__.py b/common/djangoapps/geoinfo/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/geoinfo/tests/test_middleware.py b/common/djangoapps/geoinfo/tests/test_middleware.py new file mode 100644 index 0000000000..3d90c76faa --- /dev/null +++ b/common/djangoapps/geoinfo/tests/test_middleware.py @@ -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')) diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 54f102a939..91eba5636a 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -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): diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index cbba120094..d79bd4366c 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -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(), ) diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index 4f546fe684..7e45826b72 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -15,12 +15,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 Location -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 @@ -563,3 +563,33 @@ class VideoExportTestCase(unittest.TestCase): expected = '