Commit 70fec213 by AlasdairSwan Committed by Clinton Blackburn

Adding program progress bar to program cards

parent f02b3f82
......@@ -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
(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();
});
......
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 {
......
<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>
<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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment