diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index ae42b4a45e6f8be409de49b3b2aeecff9ab00342..42271d5f8eb0ba161c02ded58ad4122a8a149518 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -14,6 +14,8 @@ from edx_oauth2_provider.tests.factories import ClientFactory from opaque_keys.edx import locator from provider.constants import CONFIDENTIAL +from openedx.core.djangoapps.credentials.models import CredentialsApiConfig +from openedx.core.djangoapps.credentials.tests.mixins import CredentialsDataMixin, CredentialsApiConfigMixin from openedx.core.djangoapps.programs.tests.mixins import ( ProgramsApiConfigMixin, ProgramsDataMixin) @@ -29,7 +31,9 @@ from xmodule.modulestore.tests.factories import CourseFactory class TestProgramListing( ModuleStoreTestCase, ProgramsApiConfigMixin, - ProgramsDataMixin): + ProgramsDataMixin, + CredentialsDataMixin, + CredentialsApiConfigMixin): """ Unit tests for getting the list of programs enrolled by a logged in user @@ -41,6 +45,7 @@ class TestProgramListing( Add a student """ super(TestProgramListing, self).setUp() + ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) self.student = UserFactory() self.create_programs_config(xseries_ad_enabled=True, program_listing_enabled=True) @@ -139,3 +144,57 @@ class TestProgramListing( self.assertEqual(response.status_code, 302) self.assertIsInstance(response, HttpResponseRedirect) self.assertIn('login', response.url) # pylint: disable=no-member + + def _expected_credetials_data(self): + """ Dry method for getting expected credentials.""" + + return [ + { + "display_name": "Test Program A", + "credential_url": "http://credentials.edx.org/credentials/dummy-uuid-1/" + }, + { + "display_name": "Test Program B", + "credential_url": "http://credentials.edx.org/credentials/dummy-uuid-2/" + } + ] + + @httpretty.activate + def test_get_xseries_certificates_with_data(self): + + self.create_programs_config(program_listing_enabled=True) + self.create_credentials_config(is_learner_issuance_enabled=True) + + self.client.login(username=self.student.username, password=self.PASSWORD) + + # mock programs and credentials apis + self.mock_programs_api() + self.mock_credentials_api(self.student, data=self.CREDENTIALS_API_RESPONSE, reset_url=False) + + response = self.client.get(reverse("program_listing_view")) + self.assertEqual(response.status_code, 200) + + for certificate in self._expected_credetials_data(): + self.assertIn(certificate['display_name'], response.content) + self.assertIn(certificate['credential_url'], response.content) + + self.assertIn('images/xseries-certificate-visual.png', response.content) + + @httpretty.activate + def test_get_xseries_certificates_without_data(self): + + self.create_programs_config(program_listing_enabled=True) + self.create_credentials_config(is_learner_issuance_enabled=True) + + self.client.login(username=self.student.username, password=self.PASSWORD) + + # mock programs and credentials apis + self.mock_programs_api() + self.mock_credentials_api(self.student, data={"results": []}, reset_url=False) + + response = self.client.get(reverse("program_listing_view")) + self.assertEqual(response.status_code, 200) + + for certificate in self._expected_credetials_data(): + self.assertNotIn(certificate['display_name'], response.content) + self.assertNotIn(certificate['credential_url'], response.content) diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 503d4bfcc3a61cb31d34cf6377b4c8618247b170..427bfa6ed3215d0246046f1ff9d1203f0654ee8e 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -9,7 +9,7 @@ from django.http import Http404 from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.programs.utils import get_engaged_programs from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from student.views import get_course_enrollments +from student.views import get_course_enrollments, _get_xseries_credentials @login_required @@ -35,5 +35,6 @@ def view_programs(request): 'programs': programs, 'xseries_url': marketing_root if ProgramsApiConfig.current().show_xseries_ad else None, 'nav_hidden': True, - 'show_program_listing': show_program_listing + 'show_program_listing': show_program_listing, + 'credentials': _get_xseries_credentials(request.user) }) diff --git a/lms/static/images/xseries-certificate-visual.png b/lms/static/images/xseries-certificate-visual.png new file mode 100644 index 0000000000000000000000000000000000000000..122a485a5a28eaf3350ab7e1fca23912c9b860c8 Binary files /dev/null and b/lms/static/images/xseries-certificate-visual.png differ diff --git a/lms/static/js/learner_dashboard/views/certificate_view.js b/lms/static/js/learner_dashboard/views/certificate_view.js new file mode 100644 index 0000000000000000000000000000000000000000..35856030534b0e058b89ee9e4c63c8bcccd365fe --- /dev/null +++ b/lms/static/js/learner_dashboard/views/certificate_view.js @@ -0,0 +1,31 @@ +;(function (define) { + 'use strict'; + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'text!../../../templates/learner_dashboard/certificate.underscore' + ], + function( + Backbone, + $, + _, + gettext, + certificateTpl + ) { + return Backbone.View.extend({ + el: '.certificates-list', + tpl: _.template(certificateTpl), + initialize: function(data) { + this.context = data.context; + this.render(); + }, + render: function() { + if (this.context.certificatesData.length > 0) { + this.$el.html(this.tpl(this.context)); + } + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/sidebar_view.js b/lms/static/js/learner_dashboard/views/sidebar_view.js index 3534eb3e022571bf903a936565de3d8c83b5c803..e2c9502556f854be1765ab963aebd538c7d9de49 100644 --- a/lms/static/js/learner_dashboard/views/sidebar_view.js +++ b/lms/static/js/learner_dashboard/views/sidebar_view.js @@ -6,6 +6,7 @@ 'underscore', 'gettext', 'js/learner_dashboard/views/explore_new_programs_view', + 'js/learner_dashboard/views/certificate_view', 'text!../../../templates/learner_dashboard/sidebar.underscore' ], function( @@ -14,6 +15,7 @@ _, gettext, NewProgramsView, + CertificateView, sidebarTpl ) { return Backbone.View.extend({ @@ -34,6 +36,10 @@ this.newProgramsView = new NewProgramsView({ context: this.context }); + + this.newCertificateView = new CertificateView({ + context: this.context + }); } }); } diff --git a/lms/static/js/spec/learner_dashboard/certificate_view_spec.js b/lms/static/js/spec/learner_dashboard/certificate_view_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..92e61daa072255527050a3e23675dd0cd2bde93e --- /dev/null +++ b/lms/static/js/spec/learner_dashboard/certificate_view_spec.js @@ -0,0 +1,65 @@ +define([ + 'backbone', + 'jquery', + 'js/learner_dashboard/views/certificate_view' + ], function (Backbone, $, CertificateView) { + + 'use strict'; + describe('Certificate View', function () { + var view = null, + data = { + context: { + certificatesData: [ + { + "display_name": "Testing", + "credential_url": "https://credentials.stage.edx.org/credentials/dummy-uuid-1/" + }, + { + "display_name": "Testing2", + "credential_url": "https://credentials.stage.edx.org/credentials/dummy-uuid-2/" + } + ], + xseriesImage: "/images/testing.png" + } + }; + + beforeEach(function() { + setFixtures('<div class="certificates-list"></div>'); + view = new CertificateView(data); + view.render(); + }); + + afterEach(function() { + view.remove(); + }); + + it('should exist', function() { + expect(view).toBeDefined(); + }); + + it('should load the certificates based on passed in certificates list', function() { + var $certificates = view.$el.find('.certificate-box'); + expect($certificates.length).toBe(2); + + $certificates.each(function(index, el){ + expect($(el).html().trim()).toEqual(data.context.certificatesData[index].display_name); + expect($(el).attr('href')).toEqual(data.context.certificatesData[index].credential_url); + }); + expect(view.$el.find('.title').html().trim()).toEqual('XSeries Program Certificates:'); + expect(view.$el.find('img').attr('src')).toEqual('/images/testing.png'); + }); + + it('should display no certificate box if certificates list is empty', function() { + var $certificate; + view.remove(); + setFixtures('<div class="certificates-list"></div>'); + view = new CertificateView({ + context: {certificatesData: []} + }); + view.render(); + $certificate = view.$el.find('.certificate-box'); + expect($certificate.length).toBe(0); + }); + }); + } +); diff --git a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js index 295d25c3beb0e8da8afe6b4eed72332e9a965011..14f57bd91ffc0704278de444a9a9950d800a71db 100644 --- a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js @@ -10,7 +10,14 @@ define([ describe('Sidebar View', function () { var view = null, context = { - xseriesUrl: 'http://www.edx.org/xseries' + xseriesUrl: 'http://www.edx.org/xseries', + certificatesData: [ + { + "display_name": "Testing", + "credential_url": "https://credentials.stage.edx.org/credentials/dummy-uuid-1/" + } + ], + xseriesImage: '/image/test.png' }; beforeEach(function() { @@ -38,17 +45,23 @@ define([ expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.xseriesUrl); }); + it('should load the certificates based on passed in certificates list', function() { + expect(view.$('.certificate-box').length).toBe(1); + }); + it('should not load the xseries advertising if no xseriesUrl passed in', function(){ var $ad; view.remove(); view = new SidebarView({ el: '.sidebar', - context: {} + context: {certificatesData: []} }); view.render(); $ad = view.$el.find('.program-advertise'); expect($ad.length).toBe(0); + expect(view.$('.certificate-box').length).toBe(0); }); + }); } ); diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 0287a265e7a6bc392dc285c554bcc9913145f76e..f25edd870b63f0f120234a880b6799ddf762d8fb 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -754,7 +754,8 @@ 'lms/include/js/spec/markdown_editor_spec.js', 'lms/include/js/spec/learner_dashboard/collection_list_view_spec.js', 'lms/include/js/spec/learner_dashboard/sidebar_view_spec.js', - 'lms/include/js/spec/learner_dashboard/program_card_view_spec.js' + 'lms/include/js/spec/learner_dashboard/program_card_view_spec.js', + 'lms/include/js/spec/learner_dashboard/certificate_view_spec.js' ]); }).call(this, requirejs, define); diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index ae3dbb281f873900c5b0bd682787921b2ddbd37c..bfa3ea5859e03ee1600d7f4b635cf72fc91ad2f5 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -59,6 +59,7 @@ @import "views/financial-assistance"; @import 'views/bookmarks'; @import 'course/auto-cert'; +@import 'xseries_certificates'; @import 'views/program-list'; @import 'views/api-access'; diff --git a/lms/static/sass/_xseries_certificates.scss b/lms/static/sass/_xseries_certificates.scss new file mode 100644 index 0000000000000000000000000000000000000000..7cc9dab92408fef3b2cdbd4f7f099f3a77a18f9e --- /dev/null +++ b/lms/static/sass/_xseries_certificates.scss @@ -0,0 +1,17 @@ +@mixin xseries-certificate-container { + border: 1px solid $gray-l3; + box-sizing: border-box; + padding: $baseline; + background: $gray-l6; + margin-top: $baseline; + .title{ + @extend %t-title6; + @extend %t-weight3; + margin-bottom:$baseline; + color: $gray; + } + .certificate-box{ + padding-top: $baseline; + display: block; + } +} diff --git a/lms/static/sass/views/_program-list.scss b/lms/static/sass/views/_program-list.scss index b555e13ca82cca4e4d40c642696c6431c55b6605..5dad8787228fb92ddfb6b2f59a68113252953326 100644 --- a/lms/static/sass/views/_program-list.scss +++ b/lms/static/sass/views/_program-list.scss @@ -19,6 +19,11 @@ $pl-button-color: #0079bc; .sidebar{ @include outer-container; @include span-columns(12); + float: right !important; + margin-bottom: $baseline; + .certificate-container{ + @include xseries-certificate-container(); + } .program-advertise{ padding: $baseline; background-color: $body-bg; diff --git a/lms/templates/learner_dashboard/certificate.underscore b/lms/templates/learner_dashboard/certificate.underscore new file mode 100644 index 0000000000000000000000000000000000000000..b1b6a9c3fc243902cae318278023ba9f3917d0a7 --- /dev/null +++ b/lms/templates/learner_dashboard/certificate.underscore @@ -0,0 +1,7 @@ +<div class="certificate-container"> + <p class="title"><%- gettext('XSeries Program Certificates') %>:</p> + <img src="<%- xseriesImage %>" alt=""> + <% _.each(certificatesData, function(certificate){ %> + <a class="certificate-box" href="<%- gettext(certificate.credential_url) %>"><%- gettext(certificate.display_name) %></a> + <% }); %> +</div> diff --git a/lms/templates/learner_dashboard/programs.html b/lms/templates/learner_dashboard/programs.html index d563522aa2eef8e94192b783563b7444263a3d15..3a7221e95d7376be0459c8c8004fbfcd30fda054 100644 --- a/lms/templates/learner_dashboard/programs.html +++ b/lms/templates/learner_dashboard/programs.html @@ -12,7 +12,9 @@ from openedx.core.djangolib.js_utils import ( <%static:require_module module_name="js/learner_dashboard/program_list_factory" class_name="ProgramListFactory"> ProgramListFactory({ programsData: ${programs | n, dump_js_escaped_json}, - xseriesUrl: '${xseries_url | n, js_escaped_string}' + certificatesData: ${credentials | n, dump_js_escaped_json}, + xseriesUrl: '${xseries_url | n, js_escaped_string}', + xseriesImage: '${static.url('images/xseries-certificate-visual.png')}' }); </%static:require_module> </%block> diff --git a/lms/templates/learner_dashboard/sidebar.underscore b/lms/templates/learner_dashboard/sidebar.underscore index 6ddfcc18a5df4c6e124f3ae1bdc3932cac057a15..c93b1b2f32208384164e4f499c3131c537cbab3d 100644 --- a/lms/templates/learner_dashboard/sidebar.underscore +++ b/lms/templates/learner_dashboard/sidebar.underscore @@ -1,2 +1,2 @@ <div class="program-advertise"></div> -<div class="certificate-container"></div> +<div class="certificates-list"></div>