diff --git a/.eslintrc.json b/.eslintrc.json index 36d4a7a626..c76fcbe5bf 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": "eslint-config-edx", + "extends": "eslint-config-edx-es5", "globals": { // Try to avoid adding any new globals. // Old compatibility things and hacks "edx": true, diff --git a/cms/celery.py b/cms/celery.py index e35bf4d7c1..49f31fbfa0 100644 --- a/cms/celery.py +++ b/cms/celery.py @@ -33,4 +33,5 @@ class Router(AlternateEnvironmentRouter): """ return { 'openedx.core.djangoapps.content.block_structure.tasks.update_course_in_cache': 'lms', + 'openedx.core.djangoapps.content.block_structure.tasks.update_course_in_cache_v2': 'lms', } diff --git a/cms/djangoapps/contentstore/tests/test_import.py b/cms/djangoapps/contentstore/tests/test_import.py index e6c3e152f2..6b271d7c82 100644 --- a/cms/djangoapps/contentstore/tests/test_import.py +++ b/cms/djangoapps/contentstore/tests/test_import.py @@ -41,7 +41,7 @@ class ContentStoreImportTest(SignalDisconnectTestMixin, ModuleStoreTestCase): self.client.login(username=self.user.username, password=self.user_password) # block_structure.update_course_in_cache cannot succeed in tests, as it needs to be run async on an lms worker - self.task_patcher = patch('openedx.core.djangoapps.content.block_structure.tasks.update_course_in_cache') + self.task_patcher = patch('openedx.core.djangoapps.content.block_structure.tasks.update_course_in_cache_v2') self._mock_lms_task = self.task_patcher.start() def tearDown(self): diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 46e1ba2740..55da469595 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -6,6 +6,7 @@ from datetime import datetime import unittest import decimal import ddt +import httpretty import freezegun from mock import patch from nose.plugins.attrib import attr @@ -25,12 +26,14 @@ from student.models import CourseEnrollment from student.tests.factories import CourseEnrollmentFactory, UserFactory from util.testing import UrlResetMixin from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme +from util.tests.mixins.enterprise import EnterpriseServiceMockMixin +from util import organizations_helpers as organizations_api @attr(shard=3) @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): +class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin): """ Course Mode View tests """ @@ -44,6 +47,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): self.client.login(username=self.user.username, password="edx") @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + @httpretty.activate @ddt.data( # is_active?, enrollment_mode, redirect? (True, 'verified', True), @@ -69,6 +73,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): user=self.user ) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Configure whether we're upgrading or not url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) @@ -118,17 +130,101 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False) ecomm_test_utils.update_commerce_config(enabled=False) + @httpretty.activate def test_no_enrollment(self): # Create the course modes for mode in ('audit', 'honor', 'verified'): CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # User visits the track selection page directly without ever enrolling url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) self.assertEquals(response.status_code, 200) + @httpretty.activate + def test_enterprise_learner_context(self): + """ + Test: Track selection page should show the enterprise context message if user belongs to the Enterprise. + """ + # Create the course modes + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + + # User visits the track selection page directly without ever enrolling + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url) + self.assertEquals(response.status_code, 200) + self.assertContains( + response, + 'Welcome, {username}! You are about to enroll in {course_name}, from {partner_names}, ' + 'sponsored by TestShib. Please select your enrollment information below.'.format( + username=self.user.username, + course_name=self.course.display_name_with_default_escaped, + partner_names=self.course.org + ) + ) + + @httpretty.activate + def test_enterprise_learner_context_with_multiple_organizations(self): + """ + Test: Track selection page should show the enterprise context message with multiple organization names + if user belongs to the Enterprise. + """ + # Create the course modes + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + + # Creating organization + for i in xrange(2): + test_organization_data = { + 'name': 'test organization ' + str(i), + 'short_name': 'test_organization_' + str(i), + 'description': 'Test Organization Description', + 'active': True, + 'logo': '/logo_test1.png/' + } + test_org = organizations_api.add_organization(organization_data=test_organization_data) + organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id)) + + # User visits the track selection page directly without ever enrolling + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url) + self.assertEquals(response.status_code, 200) + self.assertContains( + response, + 'Welcome, {username}! You are about to enroll in {course_name}, from test organization 0 and ' + 'test organization 1, sponsored by TestShib. Please select your enrollment information below.'.format( + username=self.user.username, + course_name=self.course.display_name_with_default_escaped + ) + ) + + @httpretty.activate @ddt.data( '', '1,,2', @@ -155,6 +251,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): user=self.user ) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Verify that the prices render correctly response = self.client.get( reverse('course_modes_choose', args=[unicode(self.course.id)]), @@ -165,6 +269,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): # TODO: Fix it so that response.templates works w/ mako templates, and then assert # that the right template rendered + @httpretty.activate @ddt.data( (['honor', 'verified', 'credit'], True), (['honor', 'verified'], False), @@ -175,6 +280,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): for mode in available_modes: CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Check whether credit upsell is shown on the page # This should *only* be shown when a credit mode is available url = reverse('course_modes_choose', args=[unicode(self.course.id)]) @@ -375,11 +488,20 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @with_comprehensive_theme("edx.org") + @httpretty.activate def test_hide_nav(self): # Create the course modes for mode in ["honor", "verified"]: CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Load the track selection page url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) @@ -406,7 +528,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): +class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin): """Test embargo restrictions on the track selection page. """ URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] @@ -433,6 +555,15 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): response = self.client.get(self.url) self.assertRedirects(response, redirect_url) + @httpretty.activate def test_embargo_allow(self): + + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) response = self.client.get(self.url) self.assertEqual(response.status_code, 200) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 65a1934391..6f2eba5204 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -26,6 +26,8 @@ from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.embargo import api as embargo_api from student.models import CourseEnrollment from util.db import outer_atomic +from util import enterprise_helpers as enterprise_api +from util import organizations_helpers as organization_api class ChooseModeView(View): @@ -148,6 +150,20 @@ class ChooseModeView(View): "responsive": True, "nav_hidden": True, } + + enterprise_learner_data = enterprise_api.get_enterprise_learner_data(site=request.site, user=request.user) + if enterprise_learner_data: + context["show_enterprise_context"] = True + context["partner_names"] = partner_name = course.display_organization \ + if course.display_organization else course.org + context["enterprise_name"] = enterprise_learner_data[0]['enterprise_customer']['name'] + context["username"] = request.user.username + organizations = organization_api.get_course_organizations(course_id=course.id) + if organizations: + context["partner_names"] = ' and '.join([ + org.get('name', partner_name) for org in organizations + ]) + if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ diff --git a/common/djangoapps/newrelic_custom_metrics/middleware.py b/common/djangoapps/newrelic_custom_metrics/middleware.py index 3af8cb5ef1..3d6bcd89f9 100644 --- a/common/djangoapps/newrelic_custom_metrics/middleware.py +++ b/common/djangoapps/newrelic_custom_metrics/middleware.py @@ -6,7 +6,14 @@ This middleware will only call on the newrelic agent if there are any metrics to report for this request, so it will not incur any processing overhead for request handlers which do not record custom metrics. """ -import newrelic.agent +import logging +log = logging.getLogger(__name__) +try: + import newrelic.agent +except ImportError: + log.warning("Unable to load NewRelic agent module") + newrelic = None # pylint: disable=invalid-name + import request_cache REQUEST_CACHE_KEY = 'newrelic_custom_metrics' @@ -40,6 +47,8 @@ class NewRelicCustomMetrics(object): """ Report the collected custom metrics to New Relic. """ + if not newrelic: + return metrics_cache = cls._get_metrics_cache() for metric_name, metric_value in metrics_cache.iteritems(): newrelic.agent.add_custom_parameter(metric_name, metric_value) diff --git a/common/djangoapps/student/tests/test_recent_enrollments.py b/common/djangoapps/student/tests/test_recent_enrollments.py index 8dfd143cfe..209dd45796 100644 --- a/common/djangoapps/student/tests/test_recent_enrollments.py +++ b/common/djangoapps/student/tests/test_recent_enrollments.py @@ -18,6 +18,7 @@ from course_modes.tests.factories import CourseModeFactory from student.models import CourseEnrollment, DashboardConfiguration from student.views import get_course_enrollments, _get_recently_enrolled_courses from common.test.utils import XssTestMixin +from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context @attr(shard=3) @@ -211,3 +212,29 @@ class TestRecentEnrollments(ModuleStoreTestCase, XssTestMixin): self.client.login(username=self.student.username, password=self.PASSWORD) response = self.client.get(reverse("dashboard")) self.assertNotContains(response, "donate-container") + + @ddt.data( + (True, False,), + (True, True,), + (False, False,), + (False, True,), + ) + @ddt.unpack + def test_donate_button_with_enabled_site_configuration(self, enable_donation_config, enable_donation_site_config): + # Enable the enrollment success message and donations + self._configure_message_timeout(10000) + + # DonationConfiguration has low precedence if 'ENABLE_DONATIONS' is enable in SiteConfiguration + DonationConfiguration(enabled=enable_donation_config).save() + + CourseModeFactory.create(mode_slug="audit", course_id=self.course.id, min_price=0) + self.enrollment.mode = "audit" + self.enrollment.save() + self.client.login(username=self.student.username, password=self.PASSWORD) + + with with_site_configuration_context(configuration={'ENABLE_DONATIONS': enable_donation_site_config}): + response = self.client.get(reverse("dashboard")) + if enable_donation_site_config: + self.assertContains(response, "donate-container") + else: + self.assertNotContains(response, "donate-container") diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 777d58d6ad..bc1c82d9f1 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -936,7 +936,10 @@ def _allow_donation(course_modes, course_id, enrollment): flat_unexpired_modes, flat_all_modes ) - donations_enabled = DonationConfiguration.current().enabled + donations_enabled = configuration_helpers.get_value( + 'ENABLE_DONATIONS', + DonationConfiguration.current().enabled + ) return ( donations_enabled and enrollment.mode in course_modes[course_id] and diff --git a/common/djangoapps/terrain/stubs/tests/test_video.py b/common/djangoapps/terrain/stubs/tests/test_video.py new file mode 100644 index 0000000000..de7324bf4d --- /dev/null +++ b/common/djangoapps/terrain/stubs/tests/test_video.py @@ -0,0 +1,44 @@ +""" +Unit tests for Video stub server implementation. +""" +import unittest +import requests +from terrain.stubs.video_source import VideoSourceHttpService +from django.conf import settings + +HLS_MANIFEST_TEXT = """ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=264787,RESOLUTION=1280x720 +history_264kbit/history_264kbit.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=328415,RESOLUTION=1920x1080 +history_328kbit/history_328kbit.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=70750,RESOLUTION=640x360 +history_70kbit/history_70kbit.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=148269,RESOLUTION=960x540 +history_148kbit/history_148kbit.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=41276,RESOLUTION=640x360 +history_41kbit/history_41kbit.m3u8 +""" + + +class StubVideoServiceTest(unittest.TestCase): + """ + Test cases for the video stub service. + """ + def setUp(self): + """ + Start the stub server. + """ + super(StubVideoServiceTest, self).setUp() + self.server = VideoSourceHttpService() + self.server.config['root_dir'] = '{}/data/video'.format(settings.TEST_ROOT) + self.addCleanup(self.server.shutdown) + + def test_get_hls_manifest(self): + """ + Verify that correct hls manifest is received. + """ + response = requests.get("http://127.0.0.1:{port}/hls/history.m3u8".format(port=self.server.port)) + self.assertTrue(response.ok) + self.assertEqual(response.text, HLS_MANIFEST_TEXT.lstrip()) + self.assertEqual(response.headers['Access-Control-Allow-Origin'], '*') diff --git a/common/djangoapps/terrain/stubs/video_source.py b/common/djangoapps/terrain/stubs/video_source.py index fa48834546..1265ba286d 100644 --- a/common/djangoapps/terrain/stubs/video_source.py +++ b/common/djangoapps/terrain/stubs/video_source.py @@ -24,6 +24,13 @@ class VideoSourceRequestHandler(SimpleHTTPRequestHandler): path = '{}{}'.format(root_dir, path) return path.split('?')[0] + def end_headers(self): + """ + This is required by hls.js to play hls videos. + """ + self.send_header('Access-Control-Allow-Origin', '*') + SimpleHTTPRequestHandler.end_headers(self) + class VideoSourceHttpService(StubHttpService): """ diff --git a/common/djangoapps/util/enterprise_helpers.py b/common/djangoapps/util/enterprise_helpers.py index be3ff19751..6d5ff1788e 100644 --- a/common/djangoapps/util/enterprise_helpers.py +++ b/common/djangoapps/util/enterprise_helpers.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.shortcuts import redirect from django.utils.http import urlencode +from django.core.cache import cache from edx_rest_api_client.client import EdxRestApiClient try: from enterprise import utils as enterprise_utils @@ -18,6 +19,8 @@ except ImportError: from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.token_utils import JwtBuilder from slumber.exceptions import HttpClientError, HttpServerError +import hashlib +import six ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details' @@ -71,6 +74,108 @@ class EnterpriseApiClient(object): LOGGER.exception(message) raise EnterpriseApiException(message) + def fetch_enterprise_learner_data(self, site, user): + """ + Fetch information related to enterprise from the Enterprise Service. + + Example: + fetch_enterprise_learner_data(site, user) + + Argument: + site: (Site) site instance + user: (User) django auth user + + Returns: + dict: { + "enterprise_api_response_for_learner": { + "count": 1, + "num_pages": 1, + "current_page": 1, + "results": [ + { + "enterprise_customer": { + "uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59", + "name": "TestShib", + "catalog": 2, + "active": true, + "site": { + "domain": "example.com", + "name": "example.com" + }, + "enable_data_sharing_consent": true, + "enforce_data_sharing_consent": "at_login", + "enterprise_customer_users": [ + 1 + ], + "branding_configuration": { + "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59", + "logo": "https://open.edx.org/sites/all/themes/edx_open/logo.png" + }, + "enterprise_customer_entitlements": [ + { + "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59", + "entitlement_id": 69 + } + ] + }, + "user_id": 5, + "user": { + "username": "staff", + "first_name": "", + "last_name": "", + "email": "staff@example.com", + "is_staff": true, + "is_active": true, + "date_joined": "2016-09-01T19:18:26.026495Z" + }, + "data_sharing_consent": [ + { + "user": 1, + "state": "enabled", + "enabled": true + } + ] + } + ], + "next": null, + "start": 0, + "previous": null + } + } + + Raises: + ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect + to enterprise api server. + SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains + http error status like 4xx, 5xx etc. + Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning + a response. This exception is raised for both connection timeout and read timeout. + + """ + api_resource_name = 'enterprise-learner' + + cache_key = get_cache_key( + site_domain=site.domain, + resource=api_resource_name, + username=user.username + ) + + response = cache.get(cache_key) + if not response: + try: + endpoint = getattr(self.client, api_resource_name) + querystring = {'username': user.username} + response = endpoint().get(**querystring) + cache.set(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT) + except (HttpClientError, HttpServerError): + message = ("An error occurred while getting EnterpriseLearner data for user {username}".format( + username=user.username + )) + LOGGER.exception(message) + return None + + return response + def data_sharing_consent_required(view_func): """ @@ -225,3 +330,39 @@ def get_enterprise_branding_filter_param(request): """ return request.session.get(ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS, None) + + +def get_cache_key(**kwargs): + """ + Get MD5 encoded cache key for given arguments. + + Here is the format of key before MD5 encryption. + key1:value1__key2:value2 ... + + Example: + >>> get_cache_key(site_domain="example.com", resource="enterprise-learner") + # Here is key format for above call + # "site_domain:example.com__resource:enterprise-learner" + a54349175618ff1659dee0978e3149ca + + Arguments: + **kwargs: Key word arguments that need to be present in cache key. + + Returns: + An MD5 encoded key uniquely identified by the key word arguments. + """ + key = '__'.join(['{}:{}'.format(item, value) for item, value in six.iteritems(kwargs)]) + + return hashlib.md5(key).hexdigest() + + +def get_enterprise_learner_data(site, user): + """ + Client API operation adapter/wrapper + """ + if not enterprise_enabled(): + return None + + enterprise_learner_data = EnterpriseApiClient().fetch_enterprise_learner_data(site=site, user=user) + if enterprise_learner_data: + return enterprise_learner_data['results'] diff --git a/common/djangoapps/util/help_context_processor.py b/common/djangoapps/util/help_context_processor.py index 5aa56e7bae..2f6fd134b3 100644 --- a/common/djangoapps/util/help_context_processor.py +++ b/common/djangoapps/util/help_context_processor.py @@ -4,9 +4,11 @@ Online Contextual Help. """ import ConfigParser -from django.conf import settings import logging +from django.conf import settings + +from openedx.core.release import doc_version log = logging.getLogger(__name__) @@ -76,7 +78,7 @@ def common_doc_url(request, config_file_object): # pylint: disable=unused-argum return "{url_base}/{language}/{version}/{page_path}".format( url_base=doc_base_url, language=get_config_value_with_default("locales", settings.LANGUAGE_CODE), - version=config_file_object.get("help_settings", "version"), + version=doc_version(), page_path=get_config_value_with_default("pages", page_token), ) @@ -102,7 +104,7 @@ def common_doc_url(request, config_file_object): # pylint: disable=unused-argum # Construct and return the URL for the PDF link. return "{pdf_base}/{version}/{pdf_file}".format( pdf_base=pdf_base_url, - version=config_file_object.get("help_settings", "version"), + version=doc_version(), pdf_file=config_file_object.get("pdf_settings", "pdf_file"), ) diff --git a/common/djangoapps/util/tests/mixins/enterprise.py b/common/djangoapps/util/tests/mixins/enterprise.py index 48285fb39d..7dcfc88363 100644 --- a/common/djangoapps/util/tests/mixins/enterprise.py +++ b/common/djangoapps/util/tests/mixins/enterprise.py @@ -57,6 +57,80 @@ class EnterpriseServiceMockMixin(object): status=500 ) + def mock_enterprise_learner_api( + self, + catalog_id=1, + entitlement_id=1, + learner_id=1, + enterprise_customer_uuid='cf246b88-d5f6-4908-a522-fc307e0b0c59' + ): + """ + Helper function to register enterprise learner API endpoint. + """ + enterprise_learner_api_response = { + 'count': 1, + 'num_pages': 1, + 'current_page': 1, + 'results': [ + { + 'id': learner_id, + 'enterprise_customer': { + 'uuid': enterprise_customer_uuid, + 'name': 'TestShib', + 'catalog': catalog_id, + 'active': True, + 'site': { + 'domain': 'example.com', + 'name': 'example.com' + }, + 'enable_data_sharing_consent': True, + 'enforce_data_sharing_consent': 'at_login', + 'enterprise_customer_users': [ + 1 + ], + 'branding_configuration': { + 'enterprise_customer': enterprise_customer_uuid, + 'logo': 'https://open.edx.org/sites/all/themes/edx_open/logo.png' + }, + 'enterprise_customer_entitlements': [ + { + 'enterprise_customer': enterprise_customer_uuid, + 'entitlement_id': entitlement_id + } + ] + }, + 'user_id': 5, + 'user': { + 'username': 'verified', + 'first_name': '', + 'last_name': '', + 'email': 'verified@example.com', + 'is_staff': True, + 'is_active': True, + 'date_joined': '2016-09-01T19:18:26.026495Z' + }, + 'data_sharing_consent': [ + { + 'user': 1, + 'state': 'enabled', + 'enabled': True + } + ] + } + ], + 'next': None, + 'start': 0, + 'previous': None + } + enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response) + + httpretty.register_uri( + method=httpretty.GET, + uri=self.get_enterprise_url('enterprise-learner'), + body=enterprise_learner_api_response_json, + content_type='application/json' + ) + class EnterpriseTestConsentRequired(object): """ diff --git a/common/djangoapps/util/tests/test_help_context_processor.py b/common/djangoapps/util/tests/test_help_context_processor.py index 3b388f3421..b4c2e26a5a 100644 --- a/common/djangoapps/util/tests/test_help_context_processor.py +++ b/common/djangoapps/util/tests/test_help_context_processor.py @@ -8,6 +8,7 @@ from mock import patch from django.conf import settings from django.test import TestCase +from openedx.core.release import doc_version from util.help_context_processor import common_doc_url @@ -33,34 +34,24 @@ class HelpContextProcessorTest(TestCase): def test_get_doc_url(self): # Test default values. - self.assertRegexpMatches( - self._get_doc_url(), - "http://edx.readthedocs.io/projects/open-edx-learner-guide/en/.*/index.html" - ) + doc = "http://edx.readthedocs.io/projects/open-edx-learner-guide/en/{}/index.html" + self.assertEqual(self._get_doc_url(), doc.format(doc_version())) # Provide a known page_token. - self.assertRegexpMatches( - self._get_doc_url('profile'), - "http://edx.readthedocs.io/projects/open-edx-learner-guide/en/.*/sfd_dashboard_profile/index.html" - ) + doc = "http://edx.readthedocs.io/projects/open-edx-learner-guide/en/{}/sfd_dashboard_profile/index.html" + self.assertEqual(self._get_doc_url('profile'), doc.format(doc_version())) # Use settings.DOC_LINK_BASE_URL to override default base_url. + doc = "settings_base_url/en/{}/SFD_instructor_dash_help.html" with patch('django.conf.settings.DOC_LINK_BASE_URL', 'settings_base_url'): - self.assertRegexpMatches( - self._get_doc_url('instructor'), - "settings_base_url/en/.*/SFD_instructor_dash_help.html" - ) + self.assertEqual(self._get_doc_url('instructor'), doc.format(doc_version())) def test_get_pdf_url(self): # Test default values. - self.assertRegexpMatches( - self._get_pdf_url(), - "https://media.readthedocs.org/pdf/open-edx-learner-guide/.*/open-edx-learner-guide.pdf" - ) + doc = "https://media.readthedocs.org/pdf/open-edx-learner-guide/{}/open-edx-learner-guide.pdf" + self.assertEqual(self._get_pdf_url(), doc.format(doc_version())) # Use settings.DOC_LINK_BASE_URL to override default base_url. + doc = "settings_base_url/{}/open-edx-learner-guide.pdf" with patch('django.conf.settings.DOC_LINK_BASE_URL', 'settings_base_url'): - self.assertRegexpMatches( - self._get_pdf_url(), - "settings_base_url/.*/open-edx-learner-guide.pdf" - ) + self.assertEqual(self._get_pdf_url(), doc.format(doc_version())) diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index aa327dc285..fe47e9b597 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -15,6 +15,8 @@ XQUEUE_METRIC_NAME = 'edxapp.xqueue' # Wait time for response from Xqueue. XQUEUE_TIMEOUT = 35 # seconds +CONNECT_TIMEOUT = 3.05 # seconds +READ_TIMEOUT = 10 # seconds def make_hashkey(seed): @@ -134,12 +136,18 @@ class XQueueInterface(object): def _http_post(self, url, data, files=None): try: - r = self.session.post(url, data=data, files=files) + response = self.session.post( + url, data=data, files=files, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT) + ) except requests.exceptions.ConnectionError, err: log.error(err) return (1, 'cannot connect to server') - if r.status_code not in [200]: - return (1, 'unexpected HTTP status code [%d]' % r.status_code) + except requests.exceptions.ReadTimeout, err: + log.error(err) + return (1, 'failed to read from the server') - return parse_xreply(r.text) + if response.status_code not in [200]: + return (1, 'unexpected HTTP status code [%d]' % response.status_code) + + return parse_xreply(response.text) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 95af81a9b9..9c39c7b549 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -675,6 +675,9 @@ class CourseFields(object): scope=Scope.settings, ) + # Note: Although users enter the entrance exam minimum score + # as a percentage value, it is internally converted and stored + # as a decimal value less than 1. entrance_exam_minimum_score_pct = Float( display_name=_("Entrance Exam Minimum Score (%)"), help=_( diff --git a/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html b/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html index a0250c5685..bedbadf61f 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html +++ b/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html @@ -1,4 +1,4 @@ -