Commit a466b437 by AlasdairSwan

ECOM-7386 Added a program progress circle to program details page

parent 64372dd5
(function(define) {
'use strict';
define(['backbone',
'jquery',
'underscore',
'gettext',
'text!../../../templates/components/progress_circle_view.underscore',
'text!../../../templates/components/progress_circle_segment.underscore'
],
function(
Backbone,
$,
_,
gettext,
progressViewTpl,
progressSegmentTpl
) {
return Backbone.View.extend({
x: 22,
y: 22,
radius: 16,
degrees: 180,
strokeWidth: 1.2,
viewTpl: _.template(progressViewTpl),
segmentTpl: _.template(progressSegmentTpl),
initialize: function() {
var progress = this.model.get('progress');
this.model.set({
totalCourses: progress.completed + progress.in_progress + progress.not_started
});
this.render();
},
render: function() {
var data = $.extend({}, this.model.toJSON(), {
circleSegments: this.getProgressSegments(),
x: this.x,
y: this.y,
radius: this.radius,
strokeWidth: this.strokeWidth
});
this.$el.html(this.viewTpl(data));
},
getDegreeIncrement: function(total) {
return 360 / total;
},
getOffset: function(total) {
return 100 - ((1 / total) * 100);
},
getProgressSegments: function() {
var progressHTML = [],
total = this.model.get('totalCourses'),
segmentDash = 2 * Math.PI * this.radius,
degreeInc = this.getDegreeIncrement(total),
data = {
// Remove strokeWidth to show a gap between the segments
dashArray: segmentDash - this.strokeWidth,
degrees: this.degrees,
offset: this.getOffset(total),
x: this.x,
y: this.y,
radius: this.radius,
strokeWidth: this.strokeWidth
},
i,
segmentData;
for (i = 0; i < total; i++) {
segmentData = $.extend({}, data, {
classList: (i >= this.model.get('progress').completed) ? 'incomplete' : 'complete',
degrees: data.degrees + (i * degreeInc)
});
// Want the incomplete segments to have no gaps
if (segmentData.classList === 'incomplete' && (i + 1) < total) {
segmentData.dashArray = segmentDash;
}
progressHTML.push(this.segmentTpl(segmentData));
}
return progressHTML.join('');
}
});
}
);
}).call(this, define || RequireJS.define);
define([
'backbone',
'jquery',
'edx-ui-toolkit/js/utils/spec-helpers/spec-helpers',
'common/js/components/views/progress_circle_view'
], function(Backbone, $, SpecHelpers, ProgressCircleView) {
'use strict';
describe('Progress Circle View', function() {
var view = null,
context = {
title: 'XSeries Progress',
label: 'Earned Certificates',
progress: {
completed: 2,
in_progress: 1,
not_started: 3
}
},
testCircle,
testText,
initView,
getProgress,
testProgress;
testCircle = function(progress) {
var $circle = view.$('.progress-circle');
expect($circle.find('.complete').length).toEqual(progress.completed);
expect($circle.find('.incomplete').length).toEqual(progress.in_progress + progress.not_started);
};
testText = function(progress) {
var $numbers = view.$('.numbers'),
total = progress.completed + progress.in_progress + progress.not_started;
expect(view.$('.progress-heading').html()).toEqual('XSeries Progress');
expect(parseInt($numbers.find('.complete').html(), 10)).toEqual(progress.completed);
expect(parseInt($numbers.find('.total').html(), 10)).toEqual(total);
};
getProgress = function(x, y, z) {
return {
completed: x,
in_progress: y,
not_started: z
};
};
testProgress = function(x, y, z) {
var progress = getProgress(x, y, z);
view = initView(progress);
view.render();
testCircle(progress);
testText(progress);
};
initView = function(progress) {
var data = $.extend({}, context, {
progress: progress
});
return new ProgressCircleView({
el: '.js-program-progress',
model: new Backbone.Model(data)
});
};
beforeEach(function() {
setFixtures('<div class="js-program-progress"></div>');
});
afterEach(function() {
view.remove();
});
it('should exist', function() {
var progress = getProgress(2, 1, 3);
view = initView(progress);
view.render();
expect(view).toBeDefined();
});
it('should render the progress circle based on the passed in model', function() {
var progress = getProgress(2, 1, 3);
view = initView(progress);
view.render();
testCircle(progress);
});
it('should render the progress text based on the passed in model', function() {
var progress = getProgress(2, 1, 3);
view = initView(progress);
view.render();
testText(progress);
});
SpecHelpers.withData({
'should render the progress text with only completed courses': [5, 0, 0],
'should render the progress text with only in progress courses': [0, 4, 0],
'should render the progress circle with only not started courses': [0, 0, 5],
'should render the progress text with no completed courses': [0, 2, 3],
'should render the progress text with no in progress courses': [2, 0, 7],
'should render the progress text with no not started courses': [2, 4, 0]
}, testProgress);
});
});
......@@ -164,6 +164,7 @@
'common/js/spec/components/paginated_view_spec.js',
'common/js/spec/components/paging_header_spec.js',
'common/js/spec/components/paging_footer_spec.js',
'common/js/spec/components/progress_circle_view_spec.js',
'common/js/spec/components/search_field_spec.js',
'common/js/spec/components/view_utils_spec.js',
'common/js/spec/utils/edx.utils.validate_spec.js'
......
<circle class="<%- classList %>"
r="<%- radius %>" cx="<%- x %>" cy="<%- y %>"
transform="rotate(<%- degrees %>, <%- x %>, <%- y %>)"
stroke-width="<%- strokeWidth %>"
fill="none"
stroke-dasharray="<%- dashArray %>"
stroke-dashoffset="<%- offset %>">
</circle>
<% if (title) { %>
<h2 class="progress-heading"><%- title %></h2>
<% } %>
<div class="progress-circle-wrapper">
<svg class="progress-circle" viewBox="0 0 44 44" aria-hidden="true">
<circle class="js-circle bg" r="<%- radius %>" cx="<%- x %>" cy="<%- y %>" stroke-width="<%- strokeWidth %>" fill="none"></circle>
<%= circleSegments %>
</svg>
<div class="progress-label">
<div class="numbers">
<span class="complete"><%- progress.completed %></span>/<span class="total"><%- totalCourses %></span>
</div>
<div class="label"><% if (label) { %><%- label %><% } %></div>
</div>
</div>
(function(define) {
'use strict';
define(['backbone',
'jquery',
'underscore',
'gettext',
'text!../../../templates/learner_dashboard/certificate_list.underscore'
],
function(
Backbone,
$,
_,
gettext,
certificateTpl
) {
return Backbone.View.extend({
tpl: _.template(certificateTpl),
initialize: function(options) {
this.title = options.title || false;
this.render();
},
render: function() {
var data = {
title: this.title,
certificateList: this.collection.toJSON()
};
this.$el.html(this.tpl(data));
}
});
}
);
}).call(this, define || RequireJS.define);
(function(define) {
'use strict';
define(['backbone',
'jquery',
'underscore',
'gettext',
'js/learner_dashboard/views/explore_new_programs_view',
'js/learner_dashboard/views/certificate_view',
'text!../../../templates/learner_dashboard/sidebar.underscore'
],
function(
Backbone,
$,
_,
gettext,
NewProgramsView,
CertificateView,
sidebarTpl
) {
return Backbone.View.extend({
el: '.sidebar',
tpl: _.template(sidebarTpl),
initialize: function(data) {
this.context = data.context;
},
render: function() {
this.$el.html(this.tpl(this.context));
this.postRender();
},
postRender: function() {
this.newProgramsView = new NewProgramsView({
context: this.context
});
this.newCertificateView = new CertificateView({
context: this.context
});
}
});
}
define([
'backbone',
'jquery',
'underscore',
'gettext',
'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils',
'common/js/components/views/progress_circle_view',
'js/learner_dashboard/views/certificate_list_view',
'text!../../../templates/learner_dashboard/program_details_sidebar.underscore'
],
function(
Backbone,
$,
_,
gettext,
HtmlUtils,
StringUtils,
ProgramProgressView,
CertificateView,
sidebarTpl
) {
return Backbone.View.extend({
tpl: HtmlUtils.template(sidebarTpl),
initialize: function(options) {
this.courseModel = options.courseModel || {};
this.certificateCollection = options.certificateCollection || [];
this.programCertificate = this.getProgramCertificate();
this.render();
},
render: function() {
var data = $.extend({}, this.model.toJSON(), {
programCertificate: this.programCertificate ?
this.programCertificate.toJSON() : {}
});
HtmlUtils.setHtml(this.$el, this.tpl(data));
this.postRender();
},
postRender: function() {
if (!this.programCertificate) {
this.progressModel = new Backbone.Model({
title: StringUtils.interpolate(
gettext('{type} Progress'),
{type: this.model.get('type')}
),
label: gettext('Earned Certificates'),
progress: {
completed: this.courseModel.get('completed').length,
in_progress: this.courseModel.get('in_progress').length,
not_started: this.courseModel.get('not_started').length
}
});
this.programProgressView = new ProgramProgressView({
el: '.js-program-progress',
model: this.progressModel
});
}
if (this.certificateCollection.length) {
this.certificateView = new CertificateView({
el: '.js-course-certificates',
collection: this.certificateCollection,
title: gettext('Earned Certificates')
});
}
},
getProgramCertificate: function() {
var certificate = this.certificateCollection.findWhere({type: 'program'}),
base = '/static/images/programs/program-certificate-';
if (certificate) {
certificate.set({
img: base + this.getType() + '.gif'
});
}
return certificate;
},
getType: function() {
var type = this.model.get('type').toLowerCase();
return type.replace(/\s+/g, '-');
}
});
}
);
}).call(this, define || RequireJS.define);
......@@ -34,6 +34,7 @@
this.options = options;
this.programModel = new Backbone.Model(this.options.programData);
this.courseData = new Backbone.Model(this.options.courseData);
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
this.completedCourseCollection = new CourseCardCollection(
this.courseData.get('completed') || [],
this.options.userPreferences
......@@ -61,7 +62,7 @@
remainingCount: remainingCount,
completedCount: completedCount
};
data = $.extend(data, this.options.programData);
data = $.extend(data, this.programModel.toJSON());
HtmlUtils.setHtml(this.$el, this.tpl(data));
this.postRender();
},
......@@ -99,10 +100,12 @@
}).render();
}
new SidebarView({
el: '.sidebar',
context: this.options
}).render();
this.sidebarView = new SidebarView({
el: '.js-program-sidebar',
model: this.programModel,
courseModel: this.courseData,
certificateCollection: this.certificateCollection
});
}
});
}
......
......@@ -743,6 +743,7 @@
'js/spec/learner_dashboard/program_card_view_spec.js',
'js/spec/learner_dashboard/sidebar_view_spec.js',
'js/spec/learner_dashboard/program_details_header_spec.js',
'js/spec/learner_dashboard/program_details_sidebar_view_spec.js',
'js/spec/learner_dashboard/course_card_view_spec.js',
'js/spec/learner_dashboard/course_enroll_view_spec.js',
'js/spec/learner_dashboard/course_enroll_view_spec_2017.js',
......
......@@ -6,5 +6,6 @@
@import 'elements/course-card';
@import 'elements/program-card';
@import 'elements-v2/icons';
@import 'elements/progress-circle';
@import 'views/program-details';
@import 'views/program-list';
$progress-title-color: $blue-d1 !default;
$progress-complete-color: $blue-u1 !default;
$progress-incomplete-color: $gray-l3 !default;
$progress-complete-number-color: $blue-d1 !default;
$progress-incomplete-number-color: $gray !default;
$progress-number-label-color: palette(grayscale, base) !default;
.program-progress {
width: 300px;
margin: 0 auto 30px;
@media(min-width: $bp-screen-md) {
margin-left: 0;
}
}
.progress-heading {
color: $progress-title-color;
text-align: center;
margin-bottom: 0;
font: {
size: 1.1em;
weight: 700;
}
}
.progress-circle-wrapper {
position: relative;
margin-top: -20px;
width: 300px;
height: 300px;
.progress-label {
position: absolute;
width: 100%;
top: 92px;
text-align: center;
}
.numbers {
font-size: 3em;
color: $progress-incomplete-number-color;
.complete {
color: $progress-complete-number-color;
}
}
.label {
font: {
size: 1.1em;
weight: 600;
}
color: $progress-number-label-color;
}
}
.progress-circle {
.complete {
stroke: $progress-complete-color;
}
.incomplete {
stroke: $progress-incomplete-color;
}
}
......@@ -47,8 +47,8 @@
}
.crumb {
@include float(left);
position: relative;
float: left;
font-size: font-size(x-small);
line-height: line-height(x-small);
color: palette(grayscale, dark);
......@@ -78,14 +78,18 @@
}
// CSS for April 2017 version of Program Details Page
.program-details {
.window-wrap {
background-color: $white;
}
.wrapper-footer {
@include clearfix();
clear: both;
}
}
.program-details-wrapper {
.program-details-wrapper {
.program-details-header {
background-color: $light-gray4;
display: flex;
......@@ -93,14 +97,20 @@
font-family: 'Open Sans';
font-weight: normal;
flex-wrap: wrap;
padding-top: 40px;
padding-bottom: 35px;
margin-left: 10px;
margin-right: 10px;
padding: 40px 10px 35px;
@media(min-width: $bp-screen-md) {
margin-left: 30px;
margin-right: 80px;
padding: {
left: 30px;
right: 30px;
}
}
@media(min-width: $lms-max-width) {
padding: {
left: calc(((100% - 1180px) / 2) + 30px);
right: calc(((100% - 1180px) / 2) + 30px);
}
}
.hd-1 {
......@@ -111,7 +121,7 @@
}
.program-details-icon {
margin-left: 3px;
@include margin-left(3px);
margin-top: 10px;
height: auto;
......@@ -177,7 +187,7 @@
margin-top: auto;
margin-bottom: auto;
@media(min-width: $bp-screen-md) {
margin: 10px 0 0 0;
@include margin-right(10px 0 0 0);
}
}
......@@ -187,8 +197,8 @@
width: 30%;
.orgs .org-logo {
@include margin-left(2.5%);
width: 46.5%;
margin-left: 2.5%;
height: auto;
}
}
......@@ -197,26 +207,49 @@
width: 25%;
}
}
}
.program-details-content {
width: 100%;
margin-bottom: 30px;
padding: 30px 10px;
@media(min-width: $bp-screen-md) {
margin-left: 30px;
@include float(left);
padding: {
left: 30px;
right: 30px;
}
width: calc( 100% - 330px );
position: relative;
}
@media(min-width: $bp-screen-lg) {
width: calc( 100% - 510px );
max-width: 700px;
}
@media(min-width: $lms-max-width) {
@include margin-left(calc((100% - 1180px) / 2));
}
margin-left: 10px;
}
.course-list-heading {
font-family: "Open Sans";
font-weight: bold;
text-transform: uppercase;
color: palette(primary, dark);
font-size: 0.9375em;
line-height: normal;
margin-top: 10px;
margin-bottom: 0;
padding-bottom: 5px;
border-bottom: 3px solid $divider-color;
margin: {
top: 10px;
bottom: 20px;
}
.status {
margin-right: 7px;
@include margin-right(7px);
}
}
......@@ -225,27 +258,9 @@
}
.course-list-headings {
width: 700px;
.divider {
margin-left: 0;
margin-bottom: 20px;
background-color: $divider-color;
margin-top: 5px;
height: 3px;
width: 315px;
@media(min-width: $bp-screen-sm) {
width: 550px;
}
@media(min-width: $bp-screen-md) {
width: 700px;
}
border: none;
}
.motivating-section {
@include margin-left(15px);
font-size: 0.9375em;
margin-left: 15px;
width: 310px;
@media(min-width: $bp-screen-sm) {
width: auto;
......@@ -264,11 +279,7 @@
}
.program-heading {
@media(min-width: $bp-screen-md) {
width: 70%;
}
width: 90%;
margin-top: 40px;
width: 100%;
margin-bottom: 40px;
.program-heading-title {
......@@ -313,17 +324,17 @@
/* IE11 CSS styles */
@media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) {
float: right;
@include float(right);
}
}
}
.select-choice {
@include margin-right(2px);
font-family: "Open Sans";
font-weight: bold;
font-size: 0.9375em;
color: palette(grayscale, base);
margin-top: 6px;
margin-right: 2px;
display: block;
@media(min-width: $bp-screen-md) {
......@@ -339,21 +350,19 @@
}
}
.run-select {
@include margin-right(10px);
width: 95%;
@media(min-width: $bp-screen-sm) {
width: 300px;
}
height: 34px;
padding: 0;
margin-right: 10px;
}
}
.program-course-card {
@media(min-width: $bp-screen-md) {
width: 100%;
}
width: 100%;
padding: 15px;
margin-bottom: 10px;
@media(min-width: $bp-screen-md) {
......@@ -363,12 +372,6 @@
.section {
display: flex;
justify-content: space-between;
margin-right: 40px;
margin-left: 15px;
@media(min-width: $bp-screen-sm) {
margin-left: 20px;
}
@media(min-width: $bp-screen-md) {
flex-wrap: wrap;
......@@ -400,9 +403,12 @@
.course-meta-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
@media(min-width: $bp-screen-md) {
width: 100%;
flex-direction: row;
justify-content: space-between;
}
}
......@@ -417,6 +423,10 @@
}
}
.course-certificate {
width: 100%;
}
.upgrade-message {
flex-wrap: wrap;
......@@ -432,7 +442,7 @@
/* IE11 CSS styles */
@media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) {
float: right;
@include float(right);
}
}
......@@ -495,6 +505,109 @@
font-size: 0.9375em;
}
}
}
}
.program-sidebar {
padding: 30px 10px;
@media(min-width: $bp-screen-md) {
@include float(right);
width: 300px;
padding-right: 30px;
position: relative;
}
@media(min-width: $bp-screen-lg) {
width: 450px;
.program-progress {
@include margin-left(50px);
}
}
@media(min-width: $lms-max-width) {
@include margin-right(calc((100% - 1180px) / 2));
}
}
.certificate-heading {
margin-bottom: 10px;
@media(min-width: $bp-screen-md) {
@include margin-right(30px);
}
@media(min-width: $bp-screen-lg) {
@include margin-left(10px);
@include margin-right(0);
}
}
.program-cert-link {
display: inline-block;
&:active,
&:focus,
&:hover {
.program-cert {
border-color: $blue-d1;
}
}
}
.program-cert {
width: 100%;
border: 1px solid $divider-color;
@media(min-width: $bp-screen-md) {
width: calc(100% - 30px);
}
@media(min-width: $bp-screen-lg) {
width: 100%;
}
}
.certificate-list {
@include margin(0, 0, 0, 10px);
list-style: none;
.certificate {
display: flex;
flex-direction: row;
padding: 5px 0 10px;
}
.certificate-link {
@include margin-left(20px);
color: $black;
font: {
size: 1.1em;
weight: 600;
}
@media(min-width: $bp-screen-md) {
font-size: 0.9em;
}
@media(min-width: $bp-screen-lg) {
font-size: 1.1em;
}
&:active,
&:focus,
&:hover {
.sample-cert {
border-color: $blue-d1;
}
}
}
.sample-cert {
width: 120px;
border: 3px solid $gray-l3;
border-radius: 5px;
.expired-notification {
display: inline-block;
......@@ -511,7 +624,7 @@
}
.expired-icon {
float: left;
@include float(left);
color: palette(primary, dark);
}
......@@ -520,5 +633,12 @@
padding-left: 10px;
}
@media(min-width: $bp-screen-md) {
width: 100px;
}
@media(min-width: $bp-screen-lg) {
width: 120px;
}
}
}
<div class="certificate-container">
<% if (title) { %>
<h2 class="course-list-heading"><%- title %></h2>
<% } %>
<ul class="certificate-list">
<% _.each(certificateList, function(certificate){ %>
<li class="certificate">
<a class="image-link" href="<%- certificate.url %>" aria-hidden="true" tabindex="-1"><img src="/static/images/programs/sample-cert.png" class="sample-cert" alt=""></a>
<a class="certificate-link" href="<%- certificate.url %>"><%- certificate.title %></a>
</li>
<% }); %>
</ul>
</div>
......@@ -20,7 +20,7 @@
</div>
</div>
<div class="course-actions col-12 md-col-4 sm-col-12"></div>
<div class="certificate-status"></div>
<div class="course-certificate certificate-status"></div>
</div>
</div>
<div class="section action-msg-view"></div>
......
<aside class="aside js-program-progress program-progress">
<% if (programCertificate) { %>
<h2 class="progress-heading certificate-heading"><%- StringUtils.interpolate(gettext('Your {program} Certificate'), {program: type}, true) %></h2>
<a href="<%- programCertificate.url %>" class="program-cert-link">
<img src="<%- programCertificate.img %>" class="program-cert" alt="<%- interpolate(gettext('Open the certificate you earned for the %(title)s program.'), {title: programCertificate.title}, true) %>" />
</a>
<% } %>
</aside>
<aside class="aside js-course-certificates"></aside>
<header class="js-program-header program-header full-width-banner"></header>
<div class="program-details-content">
<div class="js-program-progress-view"></div>
<section class="program-details-content">
<div class="program-heading">
<% if (inProgressCount === totalCount) { %>
<h3 class="program-heading-title"><%- gettext('Congratulations!') %></h3>
......@@ -29,7 +28,6 @@
<span class="status"><%- gettext('COURSES IN PROGRESS') %></span>
<span class="count"><%- inProgressCount %></span>
</h4>
<div class="divider"></div>
<div class="course-list js-course-list-in-progress row"></div>
</div>
<% } %>
......@@ -39,7 +37,6 @@
<span class="status"><%- gettext('REMAINING COURSES') %></span>
<span class="count"><%- remainingCount %></span>
</h4>
<div class="divider"></div>
<div class="course-list js-course-list-remaining row"></div>
</div>
<% } %>
......@@ -48,7 +45,6 @@
<span class="status"><%- gettext('COMPLETED COURSES') %></span>
<span class="count"><%- completedCount %></span>
</h4>
<div class="divider"></div>
<% if (completedCount) { %>
<div class="course-list js-course-list-completed row"></div>
<% } else { %>
......@@ -59,6 +55,5 @@
<% } %>
</div>
</div>
<aside class="js-course-sidebar"></aside>
</div>
</section>
<aside class="js-program-sidebar program-sidebar"></aside>
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