diff --git a/common/test/acceptance/pages/lms/programs.py b/common/test/acceptance/pages/lms/programs.py index 08efd8558a..ae43a0ba99 100644 --- a/common/test/acceptance/pages/lms/programs.py +++ b/common/test/acceptance/pages/lms/programs.py @@ -19,4 +19,4 @@ class ProgramListingPage(PageObject): @property def is_sidebar_present(self): """Check whether sidebar is present.""" - return self.q(css='.sidebar').present + return self.q(css='.sidebar').present and self.q(css='.certificates-list').present diff --git a/lms/static/js/learner_dashboard/collections/program_progress_collection.js b/lms/static/js/learner_dashboard/collections/program_progress_collection.js new file mode 100644 index 0000000000..562d58d7b9 --- /dev/null +++ b/lms/static/js/learner_dashboard/collections/program_progress_collection.js @@ -0,0 +1,9 @@ +(function (define) { + 'use strict'; + define([ + 'backbone' + ], + function (Backbone) { + return Backbone.Collection.extend({}); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/program_list_factory.js b/lms/static/js/learner_dashboard/program_list_factory.js index d204b7ff99..9b5cfcf414 100644 --- a/lms/static/js/learner_dashboard/program_list_factory.js +++ b/lms/static/js/learner_dashboard/program_list_factory.js @@ -5,15 +5,23 @@ 'js/learner_dashboard/views/collection_list_view', 'js/learner_dashboard/views/sidebar_view', 'js/learner_dashboard/views/program_card_view', - 'js/learner_dashboard/collections/program_collection' + 'js/learner_dashboard/collections/program_collection', + 'js/learner_dashboard/collections/program_progress_collection' ], - function (CollectionListView, SidebarView, ProgramCardView, ProgramCollection) { + function (CollectionListView, SidebarView, ProgramCardView, ProgramCollection, ProgressCollection) { return function (options) { + var progressCollection = new ProgressCollection(); + + if ( options.userProgress ) { + progressCollection.set(options.userProgress); + options.progressCollection = progressCollection; + } + new CollectionListView({ el: '.program-cards-container', childView: ProgramCardView, - context: options, - collection: new ProgramCollection(options.programsData) + collection: new ProgramCollection(options.programsData), + context: options }).render(); new SidebarView({ diff --git a/lms/static/js/learner_dashboard/views/certificate_view.js b/lms/static/js/learner_dashboard/views/certificate_view.js index 3585603053..e706bd1fc7 100644 --- a/lms/static/js/learner_dashboard/views/certificate_view.js +++ b/lms/static/js/learner_dashboard/views/certificate_view.js @@ -21,7 +21,9 @@ this.render(); }, render: function() { - if (this.context.certificatesData.length > 0) { + var certificatesData = this.context.certificatesData || []; + + if (certificatesData.length) { this.$el.html(this.tpl(this.context)); } } diff --git a/lms/static/js/learner_dashboard/views/collection_list_view.js b/lms/static/js/learner_dashboard/views/collection_list_view.js index 1f8e7a4438..42812c1946 100644 --- a/lms/static/js/learner_dashboard/views/collection_list_view.js +++ b/lms/static/js/learner_dashboard/views/collection_list_view.js @@ -13,6 +13,7 @@ gettext, emptyProgramsListTpl) { return Backbone.View.extend({ + initialize: function(data) { this.childView = data.childView; this.context = data.context; @@ -29,10 +30,15 @@ } } else { childList = []; - this.collection.each(function (program) { - var child = new this.childView({model: program}); + + this.collection.each(function(model) { + var child = new this.childView({ + model: model, + context: this.context + }); childList.push(child.el); }, this); + this.$el.html(childList); } } diff --git a/lms/static/js/learner_dashboard/views/program_card_view.js b/lms/static/js/learner_dashboard/views/program_card_view.js index d19bb7f609..ae5ac20965 100644 --- a/lms/static/js/learner_dashboard/views/program_card_view.js +++ b/lms/static/js/learner_dashboard/views/program_card_view.js @@ -20,19 +20,45 @@ className: 'program-card', + attributes: function() { + return { + 'aria-labelledby': 'program-' + this.model.get('id'), + 'role': 'group' + }; + }, + tpl: _.template(programCardTpl), - initialize: function() { + initialize: function(data) { + this.progressCollection = data.context.progressCollection; + if ( this.progressCollection ) { + this.progressModel = this.progressCollection.findWhere({ + programId: this.model.get('id') + }); + } this.render(); }, render: function() { - var templated = this.tpl(this.model.toJSON()); - this.$el.html(templated); + var orgList = _.map(this.model.get('organizations'), function(org) { + return gettext(org.key); + }), + data = $.extend( + this.model.toJSON(), + this.getProgramProgress(), + {orgList: orgList.join(' ')} + ); + + this.$el.html(this.tpl(data)); this.postRender(); }, postRender: function() { + // Add describedby to parent only if progess is present + if ( this.progressModel ) { + this.$el.attr('aria-describedby', 'status-' + this.model.get('id')); + } + if(navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0){ /* Microsoft Internet Explorer detected in. */ @@ -42,6 +68,38 @@ } }, + // Calculate counts for progress and percentages for styling + getProgramProgress: function() { + var progress = this.progressModel ? this.progressModel.toJSON() : false; + + if ( progress) { + progress.total = { + completed: progress.completed.length, + in_progress: progress.in_progress.length, + not_started: progress.not_started.length + }; + + progress.total.courses = progress.total.completed + + progress.total.in_progress + + progress.total.not_started; + + progress.percentage = { + completed: this.getWidth(progress.total.completed, progress.total.courses), + in_progress: this.getWidth(progress.total.in_progress, progress.total.courses) + }; + } + + return { + progress: progress + }; + }, + + getWidth: function(val, total) { + var int = ( val / total ) * 100; + + return int + '%'; + }, + // Defer loading the rest of the page to limit FOUC reLoadBannerImage: function() { var $img = this.$('.program_card .banner-image'), diff --git a/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js b/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js index fcfaf1ffbd..9618d9da50 100644 --- a/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js @@ -3,8 +3,10 @@ define([ 'jquery', 'js/learner_dashboard/views/program_card_view', 'js/learner_dashboard/collections/program_collection', - 'js/learner_dashboard/views/collection_list_view' - ], function (Backbone, $, ProgramCardView, ProgramCollection, CollectionListView) { + 'js/learner_dashboard/views/collection_list_view', + 'js/learner_dashboard/collections/program_progress_collection' + ], function (Backbone, $, ProgramCardView, ProgramCollection, CollectionListView, + ProgressCollection) { 'use strict'; /*jslint maxlen: 500 */ @@ -12,6 +14,7 @@ define([ describe('Collection List View', function () { var view = null, programCollection, + progressCollection, context = { programsData:[ { @@ -58,16 +61,35 @@ define([ w726h242: 'http://www.edx.org/images/org2/test3' } } + ], + userProgress: [ + { + programId: 146, + completed: ['courses', 'the', 'user', 'completed'], + in_progress: ['in', 'progress'], + not_started : ['courses', 'not', 'yet', 'started'] + }, + { + programId: 147, + completed: ['Course 1'], + in_progress: [], + not_started: ['Course 2', 'Course 3', 'Course 4'] + } ] }; beforeEach(function() { setFixtures('
'); programCollection = new ProgramCollection(context.programsData); + progressCollection = new ProgressCollection(); + progressCollection.set(context.userProgress); + context.progressCollection = progressCollection; + view = new CollectionListView({ el: '.program-cards-container', childView: ProgramCardView, - collection: programCollection + collection: programCollection, + context: context }); view.render(); }); diff --git a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js index 98b4ef1fee..e7b4466f55 100644 --- a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js @@ -1,9 +1,10 @@ define([ 'backbone', 'jquery', - 'js/learner_dashboard/views/program_card_view', - 'js/learner_dashboard/models/program_model' - ], function (Backbone, $, ProgramCardView, ProgramModel) { + 'js/learner_dashboard/collections/program_progress_collection', + 'js/learner_dashboard/models/program_model', + 'js/learner_dashboard/views/program_card_view' + ], function (Backbone, $, ProgressCollection, ProgramModel, ProgramCardView) { 'use strict'; /*jslint maxlen: 500 */ @@ -33,13 +34,39 @@ define([ w435h145: 'http://www.edx.org/images/test2', w726h242: 'http://www.edx.org/images/test3' } + }, + userProgress = [ + { + programId: 146, + completed: ['courses', 'the', 'user', 'completed'], + in_progress: ['in', 'progress'], + not_started : ['courses', 'not', 'yet', 'started'] + }, + { + programId: 147, + completed: ['Course 1'], + in_progress: [], + not_started: ['Course 2', 'Course 3', 'Course 4'] + } + ], + progressCollection = new ProgressCollection(), + cardRenders = function($card) { + expect($card).toBeDefined(); + expect($card.find('.title').html().trim()).toEqual(program.name); + expect($card.find('.category span').html().trim()).toEqual('XSeries Program'); + expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key); + expect($card.find('.card-link').attr('href')).toEqual(program.marketing_url); }; beforeEach(function() { setFixtures(''); programModel = new ProgramModel(program); + progressCollection.set(userProgress); view = new ProgramCardView({ - model: programModel + model: programModel, + context: { + progressCollection: progressCollection + } }); }); @@ -51,13 +78,8 @@ define([ expect(view).toBeDefined(); }); - it('should load the program-cards based on passed in context', function() { - var $cards = view.$el; - expect($cards).toBeDefined(); - expect($cards.find('.title').html().trim()).toEqual(program.name); - expect($cards.find('.category span').html().trim()).toEqual('XSeries Program'); - expect($cards.find('.organization').html().trim()).toEqual(program.organizations[0].key); - expect($cards.find('.card-link').attr('href')).toEqual(program.marketing_url); + it('should load the program-card based on passed in context', function() { + cardRenders(view.$el); }); it('should call reEvaluatePicture if reLoadBannerImage is called', function(){ @@ -75,6 +97,29 @@ define([ expect(view.reLoadBannerImage).not.toThrow('Picturefill had exceptions'); }); + + it('should calculate the correct percentages for progress bars', function() { + expect(view.$('.complete').css('width')).toEqual('40%'); + expect(view.$('.in-progress').css('width')).toEqual('20%'); + }); + + it('should display the correct completed courses message', function() { + var program = _.findWhere(userProgress, {programId: 146}), + completed = program.completed.length, + total = completed + program.in_progress.length + program.not_started.length; + + expect(view.$('.certificate-status').html()).toEqual('You have earned certificates in ' + completed + ' of the ' + total + ' courses so far.'); + }); + + it('should render cards if there is no progressData', function() { + view.remove(); + view = new ProgramCardView({ + model: programModel, + context: {} + }); + cardRenders(view.$el); + expect(view.$('.progress').length).toEqual(0); + }); }); } ); diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index f68c6b1e36..4f681edb0a 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -306,6 +306,10 @@ $credit-color-base: rgb(244,195,0); // accessible with black text // edx-specific: Studio/Staff actions $staff-color: $pink; +// from the edX Pattern Library +$x-light: #E5E9EB; +$success-dark: #1E8142; +$warning-base: #FDBC56; // ---------------------------- // #TYPOGRAPHY diff --git a/lms/static/sass/elements/_program-card.scss b/lms/static/sass/elements/_program-card.scss index 01ebe86490..430f20cf59 100644 --- a/lms/static/sass/elements/_program-card.scss +++ b/lms/static/sass/elements/_program-card.scss @@ -6,11 +6,12 @@ .program-card{ @include span-columns(12); border: 1px solid $border-color-l3; + border-bottom: none; box-sizing: border-box; - padding: $baseline; margin-bottom: $baseline; position: relative; display: inline; + .card-link{ position: absolute; top: 0; @@ -38,24 +39,34 @@ } } } + .text-section{ + padding: 40px $baseline $baseline; margin-top: 106px; + position: relative; + .meta-info{ @include outer-container; - margin-bottom: $baseline*0.25; font-size: em(12); color: $gray; + position: absolute; + top: $baseline; + width: calc(100% - 40px); + .organization{ @include span-columns(6); white-space: nowrap; overflow: hidden; } + .category{ @include span-columns(6); text-align: right; + .category-text{ @include float(right); } + .xseries-icon{ @include float(right); @include margin-right($baseline*0.25); @@ -67,24 +78,52 @@ } } } + .title{ + @extend %t-title4; font-size: em(24); color: $gray-d2; margin-bottom: 10px; line-height: 1.2; } } + + .certificate-status { + font-size: em(12); + color: $gray; + } + + .progress { + height: 5px; + background: $x-light; + + .bar { + height: 100%; + position: relative; + float: left; + + &.complete { + background: $success-dark; + } + + &.in-progress { + background: $warning-base; + } + } + } } @include media($bp-small) { .program-card{ @include omega(n); @include span-columns(4); + .card-link{ .banner-image-container{ height: 166px; } } + .text-section{ margin-top: 156px; } @@ -96,11 +135,13 @@ .program-card{ @include omega(n); @include span-columns(8); + .card-link{ .banner-image-container{ height: 242px; } } + .text-section{ margin-top: 232px; } @@ -113,11 +154,13 @@ .program-card{ @include omega(2n); @include span-columns(6); + .card-link{ .banner-image-container{ height: 116px; } } + .text-section{ margin-top: 106px; } @@ -128,11 +171,13 @@ .program-card{ @include omega(2n); @include span-columns(6); + .card-link{ .banner-image-container{ height: 145px; } } + .text-section{ margin-top: 135px; } diff --git a/lms/static/sass/views/_program-list.scss b/lms/static/sass/views/_program-list.scss index f098e435bb..47ae704fba 100644 --- a/lms/static/sass/views/_program-list.scss +++ b/lms/static/sass/views/_program-list.scss @@ -99,11 +99,17 @@ $pl-button-color: #0079bc; padding: ($baseline*2) 0; text-align: center; - p { + .text { @include font-size(24); - color: $lighter-base-font-color; - margin-bottom: $baseline; + margin: { + top: 0; + bottom: $baseline; + } text-shadow: 0 1px rgba(255,255,255, 0.6); + text-transform: none; + font-family: $sans-serif; + letter-spacing: initial; + color: $black; } a { diff --git a/lms/templates/learner_dashboard/empty_programs_list.underscore b/lms/templates/learner_dashboard/empty_programs_list.underscore index 0fa80cf0f2..521d68d23a 100644 --- a/lms/templates/learner_dashboard/empty_programs_list.underscore +++ b/lms/templates/learner_dashboard/empty_programs_list.underscore @@ -1,9 +1,8 @@ - - + diff --git a/lms/templates/learner_dashboard/program_card.underscore b/lms/templates/learner_dashboard/program_card.underscore index 061bd8eb52..75f8180eab 100644 --- a/lms/templates/learner_dashboard/program_card.underscore +++ b/lms/templates/learner_dashboard/program_card.underscore @@ -1,4 +1,45 @@ +<%= interpolate( + gettext('You have earned certificates in %(completed_courses)s of the %(total_courses)s courses so far.'), + {completed_courses: progress.total.completed, total_courses: progress.total.courses}, true + ) %>
+ <% } %> +