diff --git a/cms/envs/common.py b/cms/envs/common.py index 31e45bde85..dc8ceb2209 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1263,3 +1263,7 @@ USER_TASKS_MAX_AGE = timedelta(days=7) ############## Settings for the Enterprise App ###################### ENTERPRISE_ENROLLMENT_API_URL = LMS_ROOT_URL + "/api/enrollment/v1/" + +############## Settings for the Discovery App ###################### + +COURSE_CATALOG_API_URL = None diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 98110ca655..cdddaa1299 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -27,13 +27,14 @@ 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.tests.mixins.discovery import CourseCatalogServiceMockMixin 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, EnterpriseServiceMockMixin): +class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin, CourseCatalogServiceMockMixin): """ Course Mode View tests """ @@ -156,6 +157,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMo self.mock_enterprise_learner_api() + self.mock_course_discovery_api_for_catalog_contains( + catalog_id=1, course_run_ids=[str(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) @@ -181,6 +186,9 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMo CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) self.mock_enterprise_learner_api() + self.mock_course_discovery_api_for_catalog_contains( + catalog_id=1, course_run_ids=[str(self.course.id)] + ) # Creating organization for i in xrange(2): diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index a9d67e60cc..7edb9817f2 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -20,6 +20,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey from xmodule.modulestore.django import modulestore from lms.djangoapps.commerce.utils import EcommerceService +from openedx.core.djangoapps.api_admin.utils import course_discovery_api_client from course_modes.models import CourseMode from courseware.access import has_access from edxmako.shortcuts import render_to_response @@ -132,11 +133,11 @@ class ChooseModeView(View): CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False) ) - + course_id = course_key.to_deprecated_string() context = { "course_modes_choose_url": reverse( "course_modes_choose", - kwargs={'course_id': course_key.to_deprecated_string()} + kwargs={'course_id': course_id} ), "modes": modes, "has_credit_upsell": has_credit_upsell, @@ -149,18 +150,35 @@ class ChooseModeView(View): "nav_hidden": True, } + title_content = _("Congratulations! You are now enrolled in {course_name}").format( + course_name=course.display_name_with_default_escaped + ) 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 - ]) + is_course_in_enterprise_catalog = enterprise_api.is_course_in_enterprise_catalog( + site=request.site, + course_id=course_id, + user=request.user, + enterprise_catalog_id=enterprise_learner_data[0]['enterprise_customer']['catalog'] + ) + + if is_course_in_enterprise_catalog: + partner_names = partner_name = course.display_organization \ + if course.display_organization else course.org + enterprise_name = enterprise_learner_data[0]['enterprise_customer']['name'] + organizations = organization_api.get_course_organizations(course_id=course.id) + if organizations: + partner_names = ' and '.join([org.get('name', partner_name) for org in organizations]) + + title_content = _("Welcome, {username}! You are about to enroll in {course_name}," + " from {partner_names}, sponsored by {enterprise_name}. Please select your enrollment" + " information below.").format( + username=request.user.username, + course_name=course.display_name_with_default_escaped, + partner_names=partner_names, + enterprise_name=enterprise_name + ) + context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] diff --git a/common/djangoapps/util/enterprise_helpers.py b/common/djangoapps/util/enterprise_helpers.py index 2fde41482d..248095bd5c 100644 --- a/common/djangoapps/util/enterprise_helpers.py +++ b/common/djangoapps/util/enterprise_helpers.py @@ -20,6 +20,10 @@ try: except ImportError: pass from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from slumber.exceptions import SlumberBaseException +from requests.exceptions import ConnectionError, Timeout +from openedx.core.djangoapps.api_admin.utils import course_discovery_api_client + from openedx.core.lib.token_utils import JwtBuilder from slumber.exceptions import HttpClientError, HttpServerError import hashlib @@ -442,3 +446,41 @@ def get_dashboard_consent_notification(request, user, course_enrollments): } ) return '' + + +def is_course_in_enterprise_catalog(site, course_id, user, enterprise_catalog_id): + """ + Verify that the provided course id exists in the site base list of course + run keys from the provided enterprise course catalog. + + Arguments: + course_id (str): The course ID. + site: (django.contrib.sites.Site) site instance + enterprise_catalog_id (Int): Course catalog id of enterprise + + Returns: + Boolean + + """ + cache_key = get_cache_key( + site_domain=site.domain, + resource='catalogs.contains', + course_id=course_id, + catalog_id=enterprise_catalog_id + ) + response = cache.get(cache_key) + if not response: + try: + # GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids} + response = course_discovery_api_client(user=user).catalogs(enterprise_catalog_id).contains.get( + course_run_id=course_id + ) + cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) + except (ConnectionError, SlumberBaseException, Timeout): + LOGGER.exception('Unable to connect to Course Catalog service for catalog contains endpoint.') + return False + + try: + return response['courses'][course_id] + except KeyError: + return False diff --git a/common/djangoapps/util/tests/mixins/discovery.py b/common/djangoapps/util/tests/mixins/discovery.py new file mode 100644 index 0000000000..ccd4083c09 --- /dev/null +++ b/common/djangoapps/util/tests/mixins/discovery.py @@ -0,0 +1,43 @@ +""" +Mixins for the CourseDiscoveryApiClient. +""" +import json + +import httpretty +from django.conf import settings +from django.core.cache import cache + + +class CourseCatalogServiceMockMixin(object): + """ + Mocks for the Open edX service 'Course Catalog Service' responses. + """ + COURSE_DISCOVERY_CATALOGS_URL = '{}/catalogs/'.format( + settings.COURSE_CATALOG_API_URL, + ) + + def setUp(self): + super(CourseCatalogServiceMockMixin, self).setUp() + cache.clear() + + def mock_course_discovery_api_for_catalog_contains(self, catalog_id=1, course_run_ids=None): + """ + Helper function to register course catalog contains API endpoint. + """ + course_run_ids = course_run_ids or [] + courses = {course_run_id: True for course_run_id in course_run_ids} + + course_discovery_api_response = { + 'courses': courses + } + course_discovery_api_response_json = json.dumps(course_discovery_api_response) + catalog_contains_uri = '{}{}/contains/?course_run_id={}'.format( + self.COURSE_DISCOVERY_CATALOGS_URL, catalog_id, ','.join(course_run_ids) + ) + + httpretty.register_uri( + method=httpretty.GET, + uri=catalog_contains_uri, + body=course_discovery_api_response_json, + content_type='application/json' + ) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index e05f15594d..db89ea53ae 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -916,3 +916,7 @@ ENTERPRISE_ENROLLMENT_API_URL = ENV_TOKENS.get( 'ENTERPRISE_ENROLLMENT_API_URL', ENTERPRISE_ENROLLMENT_API_URL ) + + +# Discovery App config +COURSES_API_CACHE_TIMEOUT = ENV_TOKENS.get('COURSES_API_CACHE_TIMEOUT', COURSES_API_CACHE_TIMEOUT) diff --git a/lms/envs/common.py b/lms/envs/common.py index 7e64efcd93..0f81a8c9c2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3081,3 +3081,7 @@ COURSE_ENROLLMENT_MODES = { "credit": 5, "honor": 6, } + +############## Settings for the Discovery App ###################### + +COURSES_API_CACHE_TIMEOUT = 3600 # Value is in seconds diff --git a/lms/templates/course_modes/choose.html b/lms/templates/course_modes/choose.html index 0bbc6a3913..0555183163 100644 --- a/lms/templates/course_modes/choose.html +++ b/lms/templates/course_modes/choose.html @@ -73,13 +73,7 @@ from openedx.core.djangolib.markup import HTML, Text