Merge pull request #12221 from edx/alasdair/ECOM-3200-program-card-learner-progress
Adding program progress bar to program cards
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone'
|
||||
],
|
||||
function (Backbone) {
|
||||
return Backbone.Collection.extend({});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -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({
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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('<div class="program-cards-container"></div>');
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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('<div class="program-card"></div>');
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
<section class="empty-programs-message">
|
||||
<p><%- gettext('You are not enrolled in any XSeries Programs yet.') %></p>
|
||||
<a class="find-xseries-programs" href="<%- xseriesUrl %>">
|
||||
<i class="action-xseries-icon" aria-hidden="true"></i>
|
||||
<span><%- gettext('Explore XSeries Programs') %></span>
|
||||
</a>
|
||||
</section>
|
||||
<section class="empty-programs-message">
|
||||
<h2 class="text"><%- gettext('You are not enrolled in any XSeries Programs yet.') %></h2>
|
||||
<a class="find-xseries-programs" href="<%- xseriesUrl %>">
|
||||
<span class="action-xseries-icon" aria-hidden="true"></span>
|
||||
<span><%- gettext('Explore XSeries Programs') %></span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,4 +1,45 @@
|
||||
|
||||
<div class="text-section">
|
||||
<h3 id="program-<%- id %>" class="title"><%- gettext(name) %></h3>
|
||||
<div class="meta-info">
|
||||
<div class="organization"><%- orgList %></div>
|
||||
<div class="category">
|
||||
<span class="category-text"><%- gettext(type) %></span>
|
||||
<span class="xseries-icon" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<% if (progress) { %>
|
||||
<p id="status-<%- id %>" class="certificate-status"><%= 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
|
||||
) %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (progress) { %>
|
||||
<div class="progress">
|
||||
<div class="bar complete" style="width:<%- progress.percentage.completed %>;"></div>
|
||||
<div class="bar in-progress" style="width:<%- progress.percentage.in_progress %>;">
|
||||
<span class="sr"><%= interpolate(
|
||||
ngettext(
|
||||
'%(count)s course are in progress.',
|
||||
'%(count)s courses are in progress.',
|
||||
progress.total.in_progress
|
||||
),
|
||||
{count: progress.total.in_progress}, true
|
||||
) %></span>
|
||||
</div>
|
||||
<div class="bar not-started">
|
||||
<span class="sr"><%= interpolate(
|
||||
ngettext(
|
||||
'%(count)s course have not been started.',
|
||||
'%(count)s courses have not been started.',
|
||||
progress.total.not_started
|
||||
),
|
||||
{count: progress.total.not_started}, true
|
||||
) %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<a href="<%- marketingUrl %>" class="card-link">
|
||||
<div class="banner-image-container">
|
||||
<picture>
|
||||
@@ -6,25 +47,7 @@
|
||||
<source srcset="<%- mediumBannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
|
||||
<source srcset="<%- largeBannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
|
||||
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
|
||||
<img class="banner-image" srcset="<%- mediumBannerUrl %>" alt="<%- gettext(name)%>">
|
||||
<img class="banner-image" srcset="<%- mediumBannerUrl %>" alt="<%= interpolate(gettext('Learn more about %(programName)s.'), {programName: name}, true)%>">
|
||||
</picture>
|
||||
</div>
|
||||
</a>
|
||||
<div class="text-section">
|
||||
<div class="meta-info">
|
||||
<div class="organization">
|
||||
<% _.each(organizations, function(org){ %>
|
||||
<%- gettext(org.key) %>
|
||||
<% }); %>
|
||||
</div>
|
||||
<div class="category">
|
||||
<span class="category-text"><%- gettext(type) %></span>
|
||||
<i class="xseries-icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title" aria-hidden="true">
|
||||
<%- gettext(name) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
</div>
|
||||
|
||||
@@ -21,8 +21,10 @@ ProgramListFactory({
|
||||
</%block>
|
||||
|
||||
<%block name="pagetitle">${_("Programs")}</%block>
|
||||
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="program-list-wrapper">
|
||||
<h2 class="sr">${_("Your Programs")}</h2>
|
||||
<div class="program-cards-container"></div>
|
||||
<div class="sidebar"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user