diff --git a/lms/djangoapps/learner_dashboard/programs.py b/lms/djangoapps/learner_dashboard/programs.py index 4374d02cea..ed73c580fa 100644 --- a/lms/djangoapps/learner_dashboard/programs.py +++ b/lms/djangoapps/learner_dashboard/programs.py @@ -12,6 +12,7 @@ from web_fragments.fragment import Fragment from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.learner_dashboard.utils import FAKE_COURSE_KEY, strip_course_id +from openedx.core.djangoapps.credentials.utils import get_credentials_records_url from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.utils import ( @@ -97,13 +98,22 @@ class ProgramDetailsFragmentView(EdxFragmentView): skus = program_data.get('skus') ecommerce_service = EcommerceService() + # TODO: Don't have business logic of course-certificate==record-available here in LMS. + # Eventually, the UI should ask Credentials if there is a record available and get a URL from it. + # But this is here for now so that we can gate this URL behind both this business logic and + # a waffle flag. This feature is in active developoment. + program_record_url = get_credentials_records_url(program_uuid=program_uuid) + if not certificate_data: + program_record_url = None + urls = { 'program_listing_url': reverse('program_listing_view'), 'track_selection_url': strip_course_id( reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY}) ), 'commerce_api_url': reverse('commerce_api:v0:baskets:create'), - 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus) + 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus), + 'program_record_url': program_record_url, } context = { diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 4075dd88c2..cc6a1a82d8 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -12,7 +12,9 @@ from bs4 import BeautifulSoup from django.conf import settings from django.core.urlresolvers import reverse, reverse_lazy from django.test import override_settings +from waffle.testutils import override_switch +from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL from openedx.core.djangoapps.catalog.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin @@ -172,6 +174,7 @@ class TestProgramListing(ProgramsApiConfigMixin, SharedModuleStoreTestCase): @skip_unless_lms @mock.patch(PROGRAMS_UTILS_MODULE + '.get_programs') +@override_switch('student_records', True) class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, SharedModuleStoreTestCase): """Unit tests for the program details page.""" program_uuid = str(uuid4()) @@ -198,6 +201,8 @@ class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, Shared """Verify that program data is present.""" self.assertContains(response, 'programData') self.assertContains(response, 'urls') + self.assertContains(response, + '"program_record_url": "{}/records/programs/'.format(CREDENTIALS_PUBLIC_SERVICE_URL)) self.assertContains(response, 'program_listing_url') self.assertContains(response, self.data['title']) self.assert_programs_tab_present(response) @@ -230,7 +235,10 @@ class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, Shared self.client.login(username=self.user.username, password=self.password) - response = self.client.get(self.url) + with mock.patch('lms.djangoapps.learner_dashboard.programs.get_certificates') as certs: + certs.return_value = [{'type': 'program', 'url': '/'}] + response = self.client.get(self.url) + self.assert_program_data_present(response) def test_404_if_disabled(self, _mock_get_programs): diff --git a/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js index 77f884f065..a9b1866375 100644 --- a/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js @@ -52,6 +52,7 @@ describe('Program Progress View', () => { model: programModel, courseModel: courseData, certificateCollection, + programRecordUrl: '/foo/bar' }); beforeEach(() => { @@ -94,6 +95,13 @@ describe('Program Progress View', () => { expect(view.$('.certificate-heading')).toHaveText('Your XSeries Certificate'); }); + it('should render the program record link', () => { + view = initView(); + + expect(view.$('.program-record-link button')).toBeInDOM(); + expect(view.$('.program-record-link').attr('href')).toEqual('/foo/bar'); + }); + it('should render the course certificate list', () => { view = initView(); const $certificates = view.$('.certificate-list .certificate'); diff --git a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js index 7f70193ee3..ce1e998f50 100644 --- a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js @@ -468,6 +468,7 @@ describe('Program Details Header View', () => { program_listing_url: '/dashboard/programs/', commerce_api_url: '/api/commerce/v0/baskets/', track_selection_url: '/course_modes/choose/', + program_record_url: 'http://credentials.example.com/records/programs/UUID', }, userPreferences: { 'pref-lang': 'en', diff --git a/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js b/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js index 699550cfdf..412c34ec31 100644 --- a/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js @@ -16,6 +16,7 @@ class ProgramDetailsSidebarView extends Backbone.View { this.courseModel = options.courseModel || {}; this.certificateCollection = options.certificateCollection || []; this.programCertificate = this.getProgramCertificate(); + this.programRecordUrl = options.programRecordUrl; this.render(); } @@ -23,6 +24,7 @@ class ProgramDetailsSidebarView extends Backbone.View { const data = $.extend({}, this.model.toJSON(), { programCertificate: this.programCertificate ? this.programCertificate.toJSON() : {}, + programRecordUrl: this.programRecordUrl, }); HtmlUtils.setHtml(this.$el, this.tpl(data)); diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js index 1557079043..09adbd4f92 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -112,6 +112,7 @@ class ProgramDetailsView extends Backbone.View { model: this.programModel, courseModel: this.courseData, certificateCollection: this.certificateCollection, + programRecordUrl: this.options.urls.program_record_url, }); } diff --git a/lms/static/sass/views/_program-details.scss b/lms/static/sass/views/_program-details.scss index a9be0be568..6ded24172b 100644 --- a/lms/static/sass/views/_program-details.scss +++ b/lms/static/sass/views/_program-details.scss @@ -630,6 +630,10 @@ .program-sidebar { padding: 30px 10px; + .program-record { + text-align: center; + } + @media (min-width: $bp-screen-md) { @include float(right); diff --git a/lms/templates/learner_dashboard/program_details_sidebar.underscore b/lms/templates/learner_dashboard/program_details_sidebar.underscore index 10b3a4eb89..24f0ce1911 100644 --- a/lms/templates/learner_dashboard/program_details_sidebar.underscore +++ b/lms/templates/learner_dashboard/program_details_sidebar.underscore @@ -7,4 +7,11 @@ <% } %> +<% if (programRecordUrl) { %> + +<% } %> diff --git a/openedx/core/djangoapps/credentials/models.py b/openedx/core/djangoapps/credentials/models.py index e435d20c97..a1005ec4cb 100644 --- a/openedx/core/djangoapps/credentials/models.py +++ b/openedx/core/djangoapps/credentials/models.py @@ -2,6 +2,7 @@ Models for credentials support for the LMS and Studio. """ +import waffle from urlparse import urljoin from config_models.models import ConfigurationModel @@ -77,6 +78,17 @@ class CredentialsApiConfig(ConfigurationModel): root = helpers.get_value('CREDENTIALS_PUBLIC_SERVICE_URL', settings.CREDENTIALS_PUBLIC_SERVICE_URL) return urljoin(root, '/api/{}/'.format(API_VERSION)) + @property + def public_records_url(self): + """ + Publicly-accessible Records URL root. + """ + # Temporarily disable this feature while we work on it + if not waffle.switch_is_active('student_records'): + return None + root = helpers.get_value('CREDENTIALS_PUBLIC_SERVICE_URL', settings.CREDENTIALS_PUBLIC_SERVICE_URL) + return urljoin(root, '/records/') + @property def is_learner_issuance_enabled(self): """ diff --git a/openedx/core/djangoapps/credentials/utils.py b/openedx/core/djangoapps/credentials/utils.py index 45348afdef..be2775dd53 100644 --- a/openedx/core/djangoapps/credentials/utils.py +++ b/openedx/core/djangoapps/credentials/utils.py @@ -9,6 +9,19 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.token_utils import JwtBuilder +def get_credentials_records_url(program_uuid=None): + """ + Returns a URL for a given records page (or general records list if given no UUID). + May return None if this feature is disabled. + """ + base_url = CredentialsApiConfig.current().public_records_url + if base_url is None: + return None + if program_uuid: + return base_url + 'programs/{}/'.format(program_uuid) + return base_url + + def get_credentials_api_client(user): """ Returns an authenticated Credentials API client. """ diff --git a/openedx/features/learner_profile/templates/learner_profile/learner_profile.html b/openedx/features/learner_profile/templates/learner_profile/learner_profile.html index 2160c7ecae..2dc6248773 100644 --- a/openedx/features/learner_profile/templates/learner_profile/learner_profile.html +++ b/openedx/features/learner_profile/templates/learner_profile/learner_profile.html @@ -33,6 +33,10 @@ from openedx.core.djangolib.markup import HTML ${_('Build out your profile to personalize your identity on {platform_name}.').format( platform_name=platform_name, )} + % if records_url: + ## We don't translate this yet because we know it's not the final string +
To view and share your program records, go to My Records.
+ % endif % endif diff --git a/openedx/features/learner_profile/tests/views/test_learner_profile.py b/openedx/features/learner_profile/tests/views/test_learner_profile.py index 1d7fed0bb1..061e1717cd 100644 --- a/openedx/features/learner_profile/tests/views/test_learner_profile.py +++ b/openedx/features/learner_profile/tests/views/test_learner_profile.py @@ -4,8 +4,10 @@ import datetime import ddt import mock +from waffle.testutils import override_switch from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error +from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL from course_modes.models import CourseMode from django.conf import settings from django.core.urlresolvers import reverse @@ -20,6 +22,7 @@ from xmodule.modulestore.tests.factories import CourseFactory @ddt.ddt +@override_switch('student_records', True) class LearnerProfileViewTest(UrlResetMixin, ModuleStoreTestCase): """ Tests for the student profile view. """ @@ -110,6 +113,11 @@ class LearnerProfileViewTest(UrlResetMixin, ModuleStoreTestCase): for attribute in self.CONTEXT_DATA: self.assertIn(attribute, response.content) + def test_records_link(self): + profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME}) + response = self.client.get(path=profile_path) + self.assertContains(response, ''.format(CREDENTIALS_PUBLIC_SERVICE_URL)) + def test_undefined_profile_page(self): """ Verify that a 404 is returned for a non-existent profile page. diff --git a/openedx/features/learner_profile/views/learner_profile.py b/openedx/features/learner_profile/views/learner_profile.py index db95234d8e..8f56d42c6f 100644 --- a/openedx/features/learner_profile/views/learner_profile.py +++ b/openedx/features/learner_profile/views/learner_profile.py @@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _ from django.views.decorators.http import require_http_methods from django_countries import countries from edxmako.shortcuts import marketing_link +from openedx.core.djangoapps.credentials.utils import get_credentials_records_url from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_api.accounts.api import get_account_settings @@ -139,6 +140,7 @@ def learner_profile_context(request, profile_username, user_is_staff): 'show_dashboard_tabs': True, 'disable_courseware_js': True, 'nav_hidden': True, + 'records_url': get_credentials_records_url(), } if badges_enabled():