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

This commit is contained in:
AlasdairSwan
2017-03-27 15:24:24 -04:00
parent 64372dd53e
commit a466b437f6
20 changed files with 762 additions and 109 deletions

View File

@@ -0,0 +1,96 @@
(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);

View File

@@ -0,0 +1,112 @@
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);
});
});

View File

@@ -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'

View File

@@ -0,0 +1,8 @@
<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>

View File

@@ -0,0 +1,15 @@
<% 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>