diff --git a/cms/djangoapps/contentstore/views/tests/test_programs.py b/cms/djangoapps/contentstore/views/tests/test_programs.py index 480a9cab01..4ec26edac8 100644 --- a/cms/djangoapps/contentstore/views/tests/test_programs.py +++ b/cms/djangoapps/contentstore/views/tests/test_programs.py @@ -29,7 +29,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule @httpretty.activate def test_programs_config_disabled(self): """Verify that the programs tab and creation button aren't rendered when config is disabled.""" - self.create_config(enable_studio_tab=False) + self.create_programs_config(enable_studio_tab=False) self.mock_programs_api() response = self.client.get(self.studio_home) @@ -48,7 +48,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule student = UserFactory(is_staff=False) self.client.login(username=student.username, password='test') - self.create_config() + self.create_programs_config() self.mock_programs_api() response = self.client.get(self.studio_home) @@ -57,9 +57,9 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule @httpretty.activate def test_programs_displayed(self): """Verify that the programs tab and creation button can be rendered when config is enabled.""" - self.create_config() # When no data is provided, expect creation prompt. + self.create_programs_config() self.mock_programs_api(data={'results': []}) response = self.client.get(self.studio_home) @@ -102,7 +102,7 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase def test_authoring_header(self): """Verify that the header contains the expected text.""" self.client.login(username=self.staff.username, password='test') - self.create_config() + self.create_programs_config() response = self._assert_status(200) self.assertIn("Program Administration", response.content) @@ -116,7 +116,7 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase self._assert_status(404) # Enable Programs authoring interface - self.create_config() + self.create_programs_config() student = UserFactory(is_staff=False) self.client.login(username=student.username, password='test') @@ -134,13 +134,13 @@ class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase) def test_config_disabled(self): """Ensure the endpoint returns 404 when Programs authoring is disabled.""" - self.create_config(enable_studio_tab=False) + self.create_programs_config(enable_studio_tab=False) response = self.client.get(self.path) self.assertEqual(response.status_code, 404) def test_not_logged_in(self): """Ensure the endpoint denies access to unauthenticated users.""" - self.create_config() + self.create_programs_config() self.client.logout() response = self.client.get(self.path) self.assertEqual(response.status_code, 302) @@ -152,7 +152,7 @@ class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase) Ensure the endpoint responds with a valid JSON payload when authoring is enabled. """ - self.create_config() + self.create_programs_config() response = self.client.get(self.path) self.assertEqual(response.status_code, 200) payload = json.loads(response.content) diff --git a/cms/envs/common.py b/cms/envs/common.py index 9648cdf496..281c42baa7 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -840,6 +840,8 @@ INSTALLED_APPS = ( # Microsite configuration application 'microsite_configuration', + # Credentials support + 'openedx.core.djangoapps.credentials', ) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index b346e16bcd..986a97f3c4 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -1035,7 +1035,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin): CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode) self.client.login(username="jack", password="test") - self.create_config() + self.create_programs_config() with patch('student.views.get_programs_for_dashboard') as mock_data: mock_data.return_value = self._create_program_data( @@ -1068,7 +1068,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin): CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified') self.client.login(username="jack", password="test") - self.create_config() + self.create_programs_config() with patch( 'student.views.get_programs_for_dashboard', @@ -1098,7 +1098,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin): CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor') self.client.login(username="jack", password="test") - self.create_config() + self.create_programs_config() with patch('student.views.get_programs_for_dashboard') as mock_data: mock_data.return_value = self._create_program_data( @@ -1119,7 +1119,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin): CourseEnrollment.enroll(self.user, self.course_1.id) self.client.login(username="jack", password="test") - self.create_config() + self.create_programs_config() program_data = self._create_program_data([(self.course_1.id, 'active')]) if key_remove and key_remove in program_data[unicode(self.course_1.id)]: diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 4abf192f22..c79f6bb187 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -123,6 +123,7 @@ from eventtracking import tracker from notification_prefs.views import enable_notifications # Note that this lives in openedx, so this dependency should be refactored. +from openedx.core.djangoapps.credentials.utils import get_user_program_credentials from openedx.core.djangoapps.user_api.preferences import api as preferences_api from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard @@ -609,6 +610,7 @@ def dashboard(request): # This is passed along in the template context to allow rendering of # program-related information on the dashboard. course_programs = _get_course_programs(user, [enrollment.course_id for enrollment in course_enrollments]) + xseries_credentials = _get_xseries_credentials(user) # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict @@ -732,6 +734,7 @@ def dashboard(request): 'nav_hidden': True, 'course_programs': course_programs, 'disable_courseware_js': True, + 'xseries_credentials': xseries_credentials, } return render_to_response('dashboard.html', context) @@ -2410,3 +2413,34 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali log.warning('Program structure is invalid, skipping display: %r', program) return programs_data + + +def _get_xseries_credentials(user): + """Return program credentials data required for display on + the learner dashboard. + + Given a user, find all programs for which certificates have been earned + and return list of dictionaries of required program data. + + Arguments: + user (User): user object for getting programs credentials. + + Returns: + list of dict, containing data corresponding to the programs for which + the user has been awarded a credential. + """ + programs_credentials = get_user_program_credentials(user) + credentials_data = [] + for program in programs_credentials: + if program.get('category') == 'xseries': + try: + program_data = { + 'display_name': program['name'], + 'subtitle': program['subtitle'], + 'credential_url': program['credential_url'], + } + credentials_data.append(program_data) + except KeyError: + log.warning('Program structure is invalid: %r', program) + + return credentials_data diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 793a6e9434..f8dac1dd51 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -735,6 +735,7 @@ CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_U #### JWT configuration #### JWT_ISSUER = ENV_TOKENS.get('JWT_ISSUER', JWT_ISSUER) JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION) +JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {})) ################# PROCTORING CONFIGURATION ################## diff --git a/lms/envs/common.py b/lms/envs/common.py index 31cd0a3f69..e49faa4880 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1930,6 +1930,9 @@ INSTALLED_APPS = ( 'openedx.core.djangoapps.self_paced', 'sorl.thumbnail', + + # Credentials support + 'openedx.core.djangoapps.credentials', ) # Migrations which are not in the standard module "migrations" @@ -2006,6 +2009,18 @@ SOCIAL_MEDIA_FOOTER_NAMES = [ "reddit", ] +# JWT Settings +JWT_AUTH = { + 'JWT_SECRET_KEY': None, + 'JWT_ALGORITHM': 'HS256', + 'JWT_VERIFY_EXPIRATION': True, + 'JWT_ISSUER': None, + 'JWT_PAYLOAD_GET_USERNAME_HANDLER': lambda d: d.get('username'), + 'JWT_AUDIENCE': None, + 'JWT_LEEWAY': 1, + 'JWT_DECODE_HANDLER': 'openedx.core.lib.api.jwt_decode_handler.decode', +} + # The footer URLs dictionary maps social footer names # to URLs defined in configuration. SOCIAL_MEDIA_FOOTER_URLS = {} diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 95a8572a67..3445bf21a5 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -224,6 +224,13 @@ CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_WHITELIST = () CORS_ORIGIN_ALLOW_ALL = True +# JWT settings for devstack +JWT_AUTH.update({ + 'JWT_ALGORITHM': 'HS256', + 'JWT_SECRET_KEY': 'lms-secret', + 'JWT_ISSUER': 'http://127.0.0.1:8000/oauth2', + 'JWT_AUDIENCE': 'lms-key', +}) ##################################################################### # See if the developer has any local overrides. diff --git a/lms/envs/test.py b/lms/envs/test.py index e036dd2565..2a5e025107 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -563,3 +563,9 @@ FEATURES['ORGANIZATIONS_APP'] = True # Financial assistance page FEATURES['ENABLE_FINANCIAL_ASSISTANCE_FORM'] = True + +JWT_AUTH.update({ + 'JWT_SECRET_KEY': 'test-secret', + 'JWT_ISSUER': 'https://test-provider/oauth2', + 'JWT_AUDIENCE': 'test-key', +}) diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 1901dcae37..a1eab8972b 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -32,6 +32,32 @@ } } + .wrapper-xseries-certificates{ + @include float(right); + @include margin-left(flex-gutter()); + width: flex-grid(3); + + .title{ + @extend %t-title7; + @extend %t-weight4; + } + + ul{ + @include padding-left(0); + margin-top: ($baseline/2); + } + + li{ + @include line-height(20); + list-style-type: none; + } + + .copy { + @extend %t-copy-sub1; + margin-top: ($baseline/2); + } + } + .profile-sidebar { background: transparent; @include float(right); diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index eee0f7cb84..337018224c 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -172,6 +172,19 @@ import json + % if xseries_credentials: +
+

${_("XSeries Program Certificates")}

+

${_("You have received a certificate for the following XSeries programs:")}

+ +
+ % endif