Convert learner_dashboard to es2015
This commit is contained in:
committed by
Michael Terry
parent
acf7de7c02
commit
c9318c3e51
3
.babelrc
3
.babelrc
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"plugins": [
|
||||||
|
"transform-object-assign"
|
||||||
|
],
|
||||||
"presets": [
|
"presets": [
|
||||||
[
|
[
|
||||||
"env",
|
"env",
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
(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);
|
|
||||||
@@ -235,12 +235,17 @@ function setDefaults(files) {
|
|||||||
function getBaseConfig(config, useRequireJs) {
|
function getBaseConfig(config, useRequireJs) {
|
||||||
var getFrameworkFiles = function() {
|
var getFrameworkFiles = function() {
|
||||||
var files = [
|
var files = [
|
||||||
'node_modules/jquery/dist/jquery.js',
|
'common/static/common/js/vendor/jquery.js',
|
||||||
'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
|
'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
|
||||||
'common/static/common/js/jasmine_stack_trace.js',
|
'common/static/common/js/jasmine_stack_trace.js',
|
||||||
'node_modules/karma-jasmine/lib/boot.js',
|
'node_modules/karma-jasmine/lib/boot.js',
|
||||||
'node_modules/karma-jasmine/lib/adapter.js',
|
'node_modules/karma-jasmine/lib/adapter.js',
|
||||||
'node_modules/jasmine-jquery/lib/jasmine-jquery.js'
|
'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
|
||||||
|
'node_modules/popper.js/dist/umd/popper.js',
|
||||||
|
'node_modules/bootstrap/dist/js/bootstrap.js',
|
||||||
|
'node_modules/underscore/underscore.js',
|
||||||
|
'node_modules/backbone/backbone.js',
|
||||||
|
'common/static/js/test/i18n.js',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (useRequireJs) {
|
if (useRequireJs) {
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -165,7 +165,6 @@
|
|||||||
'common/js/spec/components/paginated_view_spec.js',
|
'common/js/spec/components/paginated_view_spec.js',
|
||||||
'common/js/spec/components/paging_header_spec.js',
|
'common/js/spec/components/paging_header_spec.js',
|
||||||
'common/js/spec/components/paging_footer_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/search_field_spec.js',
|
||||||
'common/js/spec/components/view_utils_spec.js',
|
'common/js/spec/components/view_utils_spec.js',
|
||||||
'common/js/spec/utils/edx.utils.validate_spec.js'
|
'common/js/spec/utils/edx.utils.validate_spec.js'
|
||||||
|
|||||||
11
lms/static/js/learner_dashboard/.eslintrc.js
Normal file
11
lms/static/js/learner_dashboard/.eslintrc.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: 'eslint-config-edx',
|
||||||
|
root: true,
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
webpack: {
|
||||||
|
config: 'webpack.dev.config.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
import CourseCard from '../models/course_card_model';
|
||||||
define([
|
|
||||||
'backbone',
|
class CourseCardCollection extends Backbone.Collection {
|
||||||
'js/learner_dashboard/models/course_card_model'
|
constructor(models, options) {
|
||||||
],
|
const defaults = {
|
||||||
function(Backbone, CourseCard) {
|
model: CourseCard,
|
||||||
return Backbone.Collection.extend({
|
};
|
||||||
model: CourseCard
|
super(models, Object.assign({}, defaults, options));
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
export default CourseCardCollection;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
import Program from '../models/program_model';
|
||||||
define([
|
|
||||||
'backbone',
|
class ProgramCollection extends Backbone.Collection {
|
||||||
'js/learner_dashboard/models/program_model'
|
constructor(models, options) {
|
||||||
],
|
const defaults = {
|
||||||
function(Backbone, Program) {
|
model: Program,
|
||||||
return Backbone.Collection.extend({
|
};
|
||||||
model: Program
|
super(models, Object.assign({}, defaults, options));
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
export default ProgramCollection;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
define([
|
class ProgramProgressCollection extends Backbone.Collection {
|
||||||
'backbone'
|
}
|
||||||
],
|
|
||||||
function(Backbone) {
|
export default ProgramProgressCollection;
|
||||||
return Backbone.Collection.extend({});
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
(function(define) {
|
import CourseEntitlementView from './views/course_entitlement_view';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
function EntitlementFactory(options) {
|
||||||
'js/learner_dashboard/views/course_entitlement_view'
|
return new CourseEntitlementView(options);
|
||||||
],
|
}
|
||||||
function(EntitlementView) {
|
|
||||||
return function(options) {
|
export { EntitlementFactory }; // eslint-disable-line import/prefer-default-export
|
||||||
return new EntitlementView(options);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
(function(define) {
|
import EntitlementUnenrollmentView from './views/entitlement_unenrollment_view';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
function EntitlementUnenrollmentFactory(options) {
|
||||||
'js/learner_dashboard/views/entitlement_unenrollment_view'
|
return new EntitlementUnenrollmentView(options);
|
||||||
],
|
}
|
||||||
function(EntitlementUnenrollmentView) {
|
|
||||||
return function(options) {
|
export { EntitlementUnenrollmentFactory }; // eslint-disable-line import/prefer-default-export
|
||||||
return new EntitlementUnenrollmentView(options);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,273 +1,261 @@
|
|||||||
|
/* globals gettext */
|
||||||
|
|
||||||
|
import _ from 'underscore';
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
|
import DateUtils from 'edx-ui-toolkit/js/utils/date-utils';
|
||||||
|
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model for Course Programs.
|
* Model for Course Programs.
|
||||||
*/
|
*/
|
||||||
(function(define) {
|
class CourseCardModel extends Backbone.Model {
|
||||||
'use strict';
|
initialize(data) {
|
||||||
define([
|
if (data) {
|
||||||
'backbone',
|
this.context = data;
|
||||||
'underscore',
|
this.setActiveCourseRun(this.getCourseRun(data), data.user_preferences);
|
||||||
'gettext',
|
}
|
||||||
'jquery',
|
}
|
||||||
'edx-ui-toolkit/js/utils/date-utils',
|
|
||||||
'edx-ui-toolkit/js/utils/string-utils'
|
|
||||||
],
|
|
||||||
function(Backbone, _, gettext, $, DateUtils, StringUtils) {
|
|
||||||
return Backbone.Model.extend({
|
|
||||||
initialize: function(data) {
|
|
||||||
if (data) {
|
|
||||||
this.context = data;
|
|
||||||
this.setActiveCourseRun(this.getCourseRun(data), data.user_preferences);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getCourseRun: function(course) {
|
getCourseRun(course) {
|
||||||
var enrolledCourseRun = _.findWhere(course.course_runs, {is_enrolled: true}),
|
const enrolledCourseRun = _.findWhere(course.course_runs, { is_enrolled: true });
|
||||||
openEnrollmentCourseRuns = this.getEnrollableCourseRuns(),
|
const openEnrollmentCourseRuns = this.getEnrollableCourseRuns();
|
||||||
desiredCourseRun;
|
let desiredCourseRun;
|
||||||
|
|
||||||
// If the learner has an existing, unexpired enrollment,
|
// If the learner has an existing, unexpired enrollment,
|
||||||
// use it to populate the model.
|
// use it to populate the model.
|
||||||
if (enrolledCourseRun && !course.expired) {
|
if (enrolledCourseRun && !course.expired) {
|
||||||
desiredCourseRun = enrolledCourseRun;
|
desiredCourseRun = enrolledCourseRun;
|
||||||
} else if (openEnrollmentCourseRuns.length > 0) {
|
} else if (openEnrollmentCourseRuns.length > 0) {
|
||||||
if (openEnrollmentCourseRuns.length === 1) {
|
if (openEnrollmentCourseRuns.length === 1) {
|
||||||
desiredCourseRun = openEnrollmentCourseRuns[0];
|
desiredCourseRun = openEnrollmentCourseRuns[0];
|
||||||
} else {
|
} else {
|
||||||
desiredCourseRun = this.getUnselectedCourseRun(openEnrollmentCourseRuns);
|
desiredCourseRun = CourseCardModel.getUnselectedCourseRun(openEnrollmentCourseRuns);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
desiredCourseRun = this.getUnselectedCourseRun(course.course_runs);
|
desiredCourseRun = CourseCardModel.getUnselectedCourseRun(course.course_runs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return desiredCourseRun;
|
return desiredCourseRun;
|
||||||
},
|
}
|
||||||
|
|
||||||
isEnrolledInSession: function() {
|
isEnrolledInSession() {
|
||||||
// Returns true if the user is currently enrolled in a session of the course
|
// Returns true if the user is currently enrolled in a session of the course
|
||||||
return _.findWhere(this.context.course_runs, {is_enrolled: true}) !== undefined;
|
return _.findWhere(this.context.course_runs, { is_enrolled: true }) !== undefined;
|
||||||
},
|
}
|
||||||
|
|
||||||
getUnselectedCourseRun: function(courseRuns) {
|
static getUnselectedCourseRun(courseRuns) {
|
||||||
var unselectedRun = {},
|
const unselectedRun = {};
|
||||||
courseRun;
|
|
||||||
|
|
||||||
if (courseRuns && courseRuns.length > 0) {
|
if (courseRuns && courseRuns.length > 0) {
|
||||||
courseRun = courseRuns[0];
|
const courseRun = courseRuns[0];
|
||||||
|
|
||||||
$.extend(unselectedRun, {
|
$.extend(unselectedRun, {
|
||||||
marketing_url: courseRun.marketing_url,
|
marketing_url: courseRun.marketing_url,
|
||||||
is_enrollment_open: courseRun.is_enrollment_open,
|
is_enrollment_open: courseRun.is_enrollment_open,
|
||||||
key: courseRun.key || '',
|
key: courseRun.key || '',
|
||||||
is_mobile_only: courseRun.is_mobile_only || false
|
is_mobile_only: courseRun.is_mobile_only || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return unselectedRun;
|
return unselectedRun;
|
||||||
},
|
}
|
||||||
|
|
||||||
getEnrollableCourseRuns: function() {
|
getEnrollableCourseRuns() {
|
||||||
var rawCourseRuns,
|
const rawCourseRuns = _.where(this.context.course_runs, {
|
||||||
enrollableCourseRuns;
|
is_enrollment_open: true,
|
||||||
|
is_enrolled: false,
|
||||||
|
is_course_ended: false,
|
||||||
|
status: 'published',
|
||||||
|
});
|
||||||
|
|
||||||
rawCourseRuns = _.where(this.context.course_runs, {
|
// Deep copy to avoid mutating this.context.
|
||||||
is_enrollment_open: true,
|
const enrollableCourseRuns = $.extend(true, [], rawCourseRuns);
|
||||||
is_enrolled: false,
|
|
||||||
is_course_ended: false,
|
|
||||||
status: 'published'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Deep copy to avoid mutating this.context.
|
// These are raw course runs from the server. The start
|
||||||
enrollableCourseRuns = $.extend(true, [], rawCourseRuns);
|
// dates are ISO-8601 formatted strings that need to be
|
||||||
|
// prepped for display.
|
||||||
|
_.each(enrollableCourseRuns, (courseRun) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
courseRun.start_date = CourseCardModel.formatDate(courseRun.start);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
courseRun.end_date = CourseCardModel.formatDate(courseRun.end);
|
||||||
|
|
||||||
// These are raw course runs from the server. The start
|
// This is used to render the date when selecting a course run to enroll in
|
||||||
// dates are ISO-8601 formatted strings that need to be
|
// eslint-disable-next-line no-param-reassign
|
||||||
// prepped for display.
|
courseRun.dateString = this.formatDateString(courseRun);
|
||||||
_.each(enrollableCourseRuns, (function(courseRun) {
|
});
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
courseRun.start_date = this.formatDate(courseRun.start);
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
courseRun.end_date = this.formatDate(courseRun.end);
|
|
||||||
|
|
||||||
// This is used to render the date when selecting a course run to enroll in
|
return enrollableCourseRuns;
|
||||||
// eslint-disable-next-line no-param-reassign
|
}
|
||||||
courseRun.dateString = this.formatDateString(courseRun);
|
|
||||||
}).bind(this));
|
|
||||||
|
|
||||||
return enrollableCourseRuns;
|
getUpcomingCourseRuns() {
|
||||||
},
|
return _.where(this.context.course_runs, {
|
||||||
|
is_enrollment_open: false,
|
||||||
|
is_enrolled: false,
|
||||||
|
is_course_ended: false,
|
||||||
|
status: 'published',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getUpcomingCourseRuns: function() {
|
static formatDate(date, userPreferences) {
|
||||||
return _.where(this.context.course_runs, {
|
let userTimezone = '';
|
||||||
is_enrollment_open: false,
|
let userLanguage = '';
|
||||||
is_enrolled: false,
|
if (userPreferences !== undefined) {
|
||||||
is_course_ended: false,
|
userTimezone = userPreferences.time_zone;
|
||||||
status: 'published'
|
userLanguage = userPreferences['pref-lang'];
|
||||||
});
|
}
|
||||||
},
|
const context = {
|
||||||
|
datetime: date,
|
||||||
|
timezone: userTimezone,
|
||||||
|
language: userLanguage,
|
||||||
|
format: DateUtils.dateFormatEnum.shortDate,
|
||||||
|
};
|
||||||
|
return DateUtils.localize(context);
|
||||||
|
}
|
||||||
|
|
||||||
formatDate: function(date, userPreferences) {
|
static getCertificatePriceString(run) {
|
||||||
var context,
|
if ('seats' in run && run.seats.length) {
|
||||||
userTimezone = '',
|
// eslint-disable-next-line consistent-return
|
||||||
userLanguage = '';
|
const upgradeableSeats = _.filter(run.seats, (seat) => {
|
||||||
if (userPreferences !== undefined) {
|
const upgradeableSeatTypes = ['verified', 'professional', 'no-id-professional', 'credit'];
|
||||||
userTimezone = userPreferences.time_zone;
|
if (upgradeableSeatTypes.indexOf(seat.type) >= 0) {
|
||||||
userLanguage = userPreferences['pref-lang'];
|
return seat;
|
||||||
}
|
}
|
||||||
context = {
|
});
|
||||||
datetime: date,
|
if (upgradeableSeats.length > 0) {
|
||||||
timezone: userTimezone,
|
const upgradeableSeat = upgradeableSeats[0];
|
||||||
language: userLanguage,
|
if (upgradeableSeat) {
|
||||||
format: DateUtils.dateFormatEnum.shortDate
|
const currency = upgradeableSeat.currency;
|
||||||
};
|
if (currency === 'USD') {
|
||||||
return DateUtils.localize(context);
|
return `$${upgradeableSeat.price}`;
|
||||||
},
|
}
|
||||||
|
return `${upgradeableSeat.price} ${currency}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getCertificatePriceString: function(run) {
|
formatDateString(run) {
|
||||||
var upgradeableSeat, upgradeableSeats, currency;
|
const pacingType = run.pacing_type;
|
||||||
if ('seats' in run && run.seats.length) {
|
let dateString;
|
||||||
// eslint-disable-next-line consistent-return
|
const start = CourseCardModel.valueIsDefined(run.start_date) ?
|
||||||
upgradeableSeats = _.filter(run.seats, function(seat) {
|
run.advertised_start || run.start_date :
|
||||||
var upgradeableSeatTypes = ['verified', 'professional', 'no-id-professional', 'credit'];
|
this.get('start_date');
|
||||||
if (upgradeableSeatTypes.indexOf(seat.type) >= 0) {
|
const end = CourseCardModel.valueIsDefined(run.end_date) ? run.end_date : this.get('end_date');
|
||||||
return seat;
|
const now = new Date();
|
||||||
}
|
const startDate = new Date(start);
|
||||||
});
|
const endDate = new Date(end);
|
||||||
if (upgradeableSeats.length > 0) {
|
|
||||||
upgradeableSeat = upgradeableSeats[0];
|
|
||||||
if (upgradeableSeat) {
|
|
||||||
currency = upgradeableSeat.currency;
|
|
||||||
if (currency === 'USD') {
|
|
||||||
return '$' + upgradeableSeat.price;
|
|
||||||
} else {
|
|
||||||
return upgradeableSeat.price + ' ' + currency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDateString: function(run) {
|
if (pacingType === 'self_paced') {
|
||||||
var pacingType = run.pacing_type,
|
if (start) {
|
||||||
dateString,
|
dateString = startDate > now ?
|
||||||
start = this.valueIsDefined(run.start_date) ? run.advertised_start || run.start_date :
|
StringUtils.interpolate(gettext('(Self-paced) Starts {start}'), { start }) :
|
||||||
this.get('start_date'),
|
StringUtils.interpolate(gettext('(Self-paced) Started {start}'), { start });
|
||||||
end = this.valueIsDefined(run.end_date) ? run.end_date : this.get('end_date'),
|
} else if (end && endDate > now) {
|
||||||
now = new Date(),
|
dateString = StringUtils.interpolate(gettext('(Self-paced) Ends {end}'), { end });
|
||||||
startDate = new Date(start),
|
} else if (end && endDate < now) {
|
||||||
endDate = new Date(end);
|
dateString = StringUtils.interpolate(gettext('(Self-paced) Ended {end}'), { end });
|
||||||
|
}
|
||||||
|
} else if (start && end) {
|
||||||
|
dateString = `${start} - ${end}`;
|
||||||
|
} else if (start) {
|
||||||
|
dateString = startDate > now ?
|
||||||
|
StringUtils.interpolate(gettext('Starts {start}'), { start }) :
|
||||||
|
StringUtils.interpolate(gettext('Started {start}'), { start });
|
||||||
|
} else if (end) {
|
||||||
|
dateString = StringUtils.interpolate(gettext('Ends {end}'), { end });
|
||||||
|
}
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
|
||||||
if (pacingType === 'self_paced') {
|
static valueIsDefined(val) {
|
||||||
if (start) {
|
return !([undefined, 'None', null].indexOf(val) >= 0);
|
||||||
dateString = startDate > now ?
|
}
|
||||||
StringUtils.interpolate(gettext('(Self-paced) Starts {start}'), {start: start}) :
|
|
||||||
StringUtils.interpolate(gettext('(Self-paced) Started {start}'), {start: start});
|
|
||||||
} else if (end && endDate > now) {
|
|
||||||
dateString = StringUtils.interpolate(gettext('(Self-paced) Ends {end}'), {end: end});
|
|
||||||
} else if (end && endDate < now) {
|
|
||||||
dateString = StringUtils.interpolate(gettext('(Self-paced) Ended {end}'), {end: end});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (start && end) {
|
|
||||||
dateString = start + ' - ' + end;
|
|
||||||
} else if (start) {
|
|
||||||
dateString = startDate > now ?
|
|
||||||
StringUtils.interpolate(gettext('Starts {start}'), {start: start}) :
|
|
||||||
StringUtils.interpolate(gettext('Started {start}'), {start: start});
|
|
||||||
} else if (end) {
|
|
||||||
dateString = StringUtils.interpolate(gettext('Ends {end}'), {end: end});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dateString;
|
|
||||||
},
|
|
||||||
|
|
||||||
valueIsDefined: function(val) {
|
setActiveCourseRun(courseRun, userPreferences) {
|
||||||
return !([undefined, 'None', null].indexOf(val) >= 0);
|
let startDateString;
|
||||||
},
|
let courseTitleLink = '';
|
||||||
|
const isEnrolled = this.isEnrolledInSession() && courseRun.key;
|
||||||
|
if (courseRun) {
|
||||||
|
if (CourseCardModel.valueIsDefined(courseRun.advertised_start)) {
|
||||||
|
startDateString = courseRun.advertised_start;
|
||||||
|
} else {
|
||||||
|
startDateString = CourseCardModel.formatDate(courseRun.start, userPreferences);
|
||||||
|
}
|
||||||
|
if (isEnrolled && courseRun.course_url) {
|
||||||
|
courseTitleLink = courseRun.course_url;
|
||||||
|
} else if (!isEnrolled && courseRun.marketing_url) {
|
||||||
|
courseTitleLink = CourseCardModel.updateMarketingUrl(courseRun);
|
||||||
|
}
|
||||||
|
this.set({
|
||||||
|
certificate_url: courseRun.certificate_url,
|
||||||
|
course_run_key: courseRun.key || '',
|
||||||
|
course_url: courseRun.course_url || '',
|
||||||
|
title: this.context.title,
|
||||||
|
end_date: CourseCardModel.formatDate(courseRun.end, userPreferences),
|
||||||
|
enrollable_course_runs: this.getEnrollableCourseRuns(),
|
||||||
|
is_course_ended: courseRun.is_course_ended,
|
||||||
|
is_enrolled: isEnrolled,
|
||||||
|
is_enrollment_open: courseRun.is_enrollment_open,
|
||||||
|
course_key: this.context.key,
|
||||||
|
user_entitlement: this.context.user_entitlement,
|
||||||
|
is_unfulfilled_entitlement: this.context.user_entitlement && !isEnrolled,
|
||||||
|
marketing_url: courseRun.marketing_url,
|
||||||
|
mode_slug: courseRun.type,
|
||||||
|
start_date: startDateString,
|
||||||
|
upcoming_course_runs: this.getUpcomingCourseRuns(),
|
||||||
|
upgrade_url: courseRun.upgrade_url,
|
||||||
|
price: CourseCardModel.getCertificatePriceString(courseRun),
|
||||||
|
course_title_link: courseTitleLink,
|
||||||
|
is_mobile_only: courseRun.is_mobile_only || false,
|
||||||
|
});
|
||||||
|
|
||||||
setActiveCourseRun: function(courseRun, userPreferences) {
|
// This is used to render the date for completed and in progress courses
|
||||||
var startDateString,
|
this.set({ dateString: this.formatDateString(courseRun) });
|
||||||
courseTitleLink = '',
|
}
|
||||||
isEnrolled = this.isEnrolledInSession() && courseRun.key;
|
}
|
||||||
if (courseRun) {
|
|
||||||
if (this.valueIsDefined(courseRun.advertised_start)) {
|
|
||||||
startDateString = courseRun.advertised_start;
|
|
||||||
} else {
|
|
||||||
startDateString = this.formatDate(courseRun.start, userPreferences);
|
|
||||||
}
|
|
||||||
if (isEnrolled && courseRun.course_url) {
|
|
||||||
courseTitleLink = courseRun.course_url;
|
|
||||||
} else if (!isEnrolled && courseRun.marketing_url) {
|
|
||||||
courseTitleLink = this.updateMarketingUrl(courseRun);
|
|
||||||
}
|
|
||||||
this.set({
|
|
||||||
certificate_url: courseRun.certificate_url,
|
|
||||||
course_run_key: courseRun.key || '',
|
|
||||||
course_url: courseRun.course_url || '',
|
|
||||||
title: this.context.title,
|
|
||||||
end_date: this.formatDate(courseRun.end, userPreferences),
|
|
||||||
enrollable_course_runs: this.getEnrollableCourseRuns(),
|
|
||||||
is_course_ended: courseRun.is_course_ended,
|
|
||||||
is_enrolled: isEnrolled,
|
|
||||||
is_enrollment_open: courseRun.is_enrollment_open,
|
|
||||||
course_key: this.context.key,
|
|
||||||
user_entitlement: this.context.user_entitlement,
|
|
||||||
is_unfulfilled_entitlement: this.context.user_entitlement && !isEnrolled,
|
|
||||||
marketing_url: courseRun.marketing_url,
|
|
||||||
mode_slug: courseRun.type,
|
|
||||||
start_date: startDateString,
|
|
||||||
upcoming_course_runs: this.getUpcomingCourseRuns(),
|
|
||||||
upgrade_url: courseRun.upgrade_url,
|
|
||||||
price: this.getCertificatePriceString(courseRun),
|
|
||||||
course_title_link: courseTitleLink,
|
|
||||||
is_mobile_only: courseRun.is_mobile_only || false
|
|
||||||
});
|
|
||||||
|
|
||||||
// This is used to render the date for completed and in progress courses
|
setUnselected() {
|
||||||
this.set({dateString: this.formatDateString(courseRun)});
|
// Called to reset the model back to the unselected state.
|
||||||
}
|
const unselectedCourseRun = CourseCardModel.getUnselectedCourseRun(this.get('enrollable_course_runs'));
|
||||||
},
|
this.setActiveCourseRun(unselectedCourseRun);
|
||||||
|
}
|
||||||
|
|
||||||
setUnselected: function() {
|
updateCourseRun(courseRunKey) {
|
||||||
// Called to reset the model back to the unselected state.
|
const selectedCourseRun = _.findWhere(this.get('course_runs'), { key: courseRunKey });
|
||||||
var unselectedCourseRun = this.getUnselectedCourseRun(this.get('enrollable_course_runs'));
|
if (selectedCourseRun) {
|
||||||
this.setActiveCourseRun(unselectedCourseRun);
|
// Update the current context to set the course run to the enrolled state
|
||||||
},
|
_.each(this.context.course_runs, (run) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
if (run.key === selectedCourseRun.key) run.is_enrolled = true;
|
||||||
|
});
|
||||||
|
this.setActiveCourseRun(selectedCourseRun);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateCourseRun: function(courseRunKey) {
|
// update marketing url for deep linking if is_mobile_only true
|
||||||
var selectedCourseRun = _.findWhere(this.get('course_runs'), {key: courseRunKey});
|
static updateMarketingUrl(courseRun) {
|
||||||
if (selectedCourseRun) {
|
if (courseRun.is_mobile_only === true) {
|
||||||
// Update the current context to set the course run to the enrolled state
|
const marketingUrl = courseRun.marketing_url;
|
||||||
_.each(this.context.course_runs, function(run) {
|
let href = marketingUrl;
|
||||||
if (run.key === selectedCourseRun.key) run.is_enrolled = true; // eslint-disable-line no-param-reassign, max-len
|
|
||||||
});
|
|
||||||
this.setActiveCourseRun(selectedCourseRun);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// update marketing url for deep linking if is_mobile_only true
|
if (marketingUrl.indexOf('course_info?path_id') < 0) {
|
||||||
updateMarketingUrl: function(courseRun) {
|
const start = marketingUrl.indexOf('course/');
|
||||||
if (courseRun.is_mobile_only === true) {
|
let path;
|
||||||
var marketingUrl = courseRun.marketing_url, // eslint-disable-line vars-on-top
|
|
||||||
href = marketingUrl,
|
|
||||||
path,
|
|
||||||
start;
|
|
||||||
|
|
||||||
if (marketingUrl.indexOf('course_info?path_id') < 0) {
|
if (start > -1) {
|
||||||
start = marketingUrl.indexOf('course/');
|
path = marketingUrl.substr(start);
|
||||||
|
}
|
||||||
|
|
||||||
if (start > -1) {
|
href = `edxapp://course_info?path_id=${path}`;
|
||||||
path = marketingUrl.substr(start);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
href = 'edxapp://course_info?path_id=' + path;
|
return href;
|
||||||
}
|
}
|
||||||
|
return courseRun.marketing_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return href;
|
export default CourseCardModel;
|
||||||
} else {
|
|
||||||
return courseRun.marketing_url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store data to enroll learners into the course
|
* Store data to enroll learners into the course
|
||||||
*/
|
*/
|
||||||
(function(define) {
|
class CourseEnrollModel extends Backbone.Model {
|
||||||
'use strict';
|
constructor(attrs, ...args) {
|
||||||
|
const defaults = {
|
||||||
|
course_id: '',
|
||||||
|
optIn: false,
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, attrs), ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
define([
|
export default CourseEnrollModel;
|
||||||
'backbone'
|
|
||||||
],
|
|
||||||
function(Backbone) {
|
|
||||||
return Backbone.Model.extend({
|
|
||||||
defaults: {
|
|
||||||
course_id: '',
|
|
||||||
optIn: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store data for the current entitlement.
|
* Store data for the current entitlement.
|
||||||
*/
|
*/
|
||||||
(function(define) {
|
class CourseEntitlementModel extends Backbone.Model {
|
||||||
'use strict';
|
constructor(attrs, ...args) {
|
||||||
|
const defaults = {
|
||||||
|
availableSessions: [],
|
||||||
|
entitlementUUID: '',
|
||||||
|
currentSessionId: '',
|
||||||
|
courseName: '',
|
||||||
|
expiredAt: null,
|
||||||
|
daysUntilExpiration: Number.MAX_VALUE,
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, attrs), ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
define([
|
export default CourseEntitlementModel;
|
||||||
'backbone'
|
|
||||||
],
|
|
||||||
function(Backbone) {
|
|
||||||
return Backbone.Model.extend({
|
|
||||||
defaults: {
|
|
||||||
availableSessions: [],
|
|
||||||
entitlementUUID: '',
|
|
||||||
currentSessionId: '',
|
|
||||||
courseName: '',
|
|
||||||
expiredAt: null,
|
|
||||||
daysUntilExpiration: Number.MAX_VALUE
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model for Course Programs.
|
* Model for Course Programs.
|
||||||
*/
|
*/
|
||||||
(function(define) {
|
class ProgramModel extends Backbone.Model {
|
||||||
'use strict';
|
initialize(data) {
|
||||||
define([
|
if (data) {
|
||||||
'backbone'
|
this.set({
|
||||||
],
|
title: data.title,
|
||||||
function(Backbone) {
|
type: data.type,
|
||||||
return Backbone.Model.extend({
|
subtitle: data.subtitle,
|
||||||
initialize: function(data) {
|
authoring_organizations: data.authoring_organizations,
|
||||||
if (data) {
|
detailUrl: data.detail_url,
|
||||||
this.set({
|
xsmallBannerUrl: data.banner_image['x-small'].url,
|
||||||
title: data.title,
|
smallBannerUrl: data.banner_image.small.url,
|
||||||
type: data.type,
|
mediumBannerUrl: data.banner_image.medium.url,
|
||||||
subtitle: data.subtitle,
|
breakpoints: {
|
||||||
authoring_organizations: data.authoring_organizations,
|
max: {
|
||||||
detailUrl: data.detail_url,
|
xsmall: '320px',
|
||||||
xsmallBannerUrl: data.banner_image['x-small'].url,
|
small: '540px',
|
||||||
smallBannerUrl: data.banner_image.small.url,
|
medium: '768px',
|
||||||
mediumBannerUrl: data.banner_image.medium.url,
|
large: '979px',
|
||||||
breakpoints: {
|
},
|
||||||
max: {
|
},
|
||||||
xsmall: '320px',
|
});
|
||||||
small: '540px',
|
}
|
||||||
medium: '768px',
|
}
|
||||||
large: '979px'
|
}
|
||||||
}
|
|
||||||
}
|
export default ProgramModel;
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
(function(define) {
|
import ProgramDetailsView from './views/program_details_view';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
function ProgramDetailsFactory(options) {
|
||||||
'js/learner_dashboard/views/program_details_view'
|
return new ProgramDetailsView(options);
|
||||||
],
|
}
|
||||||
function(ProgramDetailsView) {
|
|
||||||
return function(options) {
|
export { ProgramDetailsFactory }; // eslint-disable-line import/prefer-default-export
|
||||||
var ProgramDetails = new ProgramDetailsView(options);
|
|
||||||
return ProgramDetails;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,39 +1,34 @@
|
|||||||
(function(define) {
|
import CollectionListView from './views/collection_list_view';
|
||||||
'use strict';
|
import ProgramCardView from './views/program_card_view';
|
||||||
|
import ProgramCollection from './collections/program_collection';
|
||||||
|
import ProgressCollection from './collections/program_progress_collection';
|
||||||
|
import SidebarView from './views/sidebar_view';
|
||||||
|
|
||||||
define([
|
function ProgramListFactory(options) {
|
||||||
'js/learner_dashboard/views/collection_list_view',
|
const progressCollection = new ProgressCollection();
|
||||||
'js/learner_dashboard/views/sidebar_view',
|
|
||||||
'js/learner_dashboard/views/program_card_view',
|
|
||||||
'js/learner_dashboard/collections/program_collection',
|
|
||||||
'js/learner_dashboard/collections/program_progress_collection'
|
|
||||||
],
|
|
||||||
function(CollectionListView, SidebarView, ProgramCardView, ProgramCollection, ProgressCollection) {
|
|
||||||
return function(options) {
|
|
||||||
var progressCollection = new ProgressCollection();
|
|
||||||
|
|
||||||
if (options.userProgress) {
|
if (options.userProgress) {
|
||||||
progressCollection.set(options.userProgress);
|
progressCollection.set(options.userProgress);
|
||||||
options.progressCollection = progressCollection;
|
options.progressCollection = progressCollection; // eslint-disable-line no-param-reassign
|
||||||
}
|
}
|
||||||
|
|
||||||
new CollectionListView({
|
new CollectionListView({
|
||||||
el: '.program-cards-container',
|
el: '.program-cards-container',
|
||||||
childView: ProgramCardView,
|
childView: ProgramCardView,
|
||||||
collection: new ProgramCollection(options.programsData),
|
collection: new ProgramCollection(options.programsData),
|
||||||
context: options,
|
context: options,
|
||||||
titleContext: {
|
titleContext: {
|
||||||
el: 'h2',
|
el: 'h2',
|
||||||
title: 'Your Programs'
|
title: 'Your Programs',
|
||||||
}
|
},
|
||||||
}).render();
|
}).render();
|
||||||
|
|
||||||
if (options.programsData.length) {
|
if (options.programsData.length) {
|
||||||
new SidebarView({
|
new SidebarView({
|
||||||
el: '.sidebar',
|
el: '.sidebar',
|
||||||
context: options
|
context: options,
|
||||||
}).render();
|
}).render();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
export { ProgramListFactory }; // eslint-disable-line import/prefer-default-export
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import CollectionListView from '../views/collection_list_view';
|
||||||
|
import ProgramCardView from '../views/program_card_view';
|
||||||
|
import ProgramCollection from '../collections/program_collection';
|
||||||
|
import ProgressCollection from '../collections/program_progress_collection';
|
||||||
|
|
||||||
|
describe('Collection List View', () => {
|
||||||
|
let view = null;
|
||||||
|
let programCollection;
|
||||||
|
let progressCollection;
|
||||||
|
const context = {
|
||||||
|
programsData: [
|
||||||
|
{
|
||||||
|
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||||
|
title: 'Food Security and Sustainability',
|
||||||
|
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
||||||
|
type: 'XSeries',
|
||||||
|
detail_url: 'https://www.edx.org/foo/bar',
|
||||||
|
banner_image: {
|
||||||
|
medium: {
|
||||||
|
height: 242,
|
||||||
|
width: 726,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg',
|
||||||
|
},
|
||||||
|
'x-small': {
|
||||||
|
height: 116,
|
||||||
|
width: 348,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg',
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
height: 145,
|
||||||
|
width: 435,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
height: 480,
|
||||||
|
width: 1440,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authoring_organizations: [
|
||||||
|
{
|
||||||
|
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||||
|
key: 'WageningenX',
|
||||||
|
name: 'Wageningen University & Research',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
||||||
|
title: 'edX Course Creator',
|
||||||
|
subtitle: 'Become an expert in creating courses for the edX platform.',
|
||||||
|
type: 'XSeries',
|
||||||
|
detail_url: 'https://www.edx.org/foo/bar',
|
||||||
|
banner_image: {
|
||||||
|
medium: {
|
||||||
|
height: 242,
|
||||||
|
width: 726,
|
||||||
|
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.medium.jpg',
|
||||||
|
},
|
||||||
|
'x-small': {
|
||||||
|
height: 116,
|
||||||
|
width: 348,
|
||||||
|
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.x-small.jpg',
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
height: 145,
|
||||||
|
width: 435,
|
||||||
|
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.small.jpg',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
height: 480,
|
||||||
|
width: 1440,
|
||||||
|
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.large.jpg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authoring_organizations: [
|
||||||
|
{
|
||||||
|
uuid: '4f8cb2c9-589b-4d1e-88c1-b01a02db3a9c',
|
||||||
|
key: 'edX',
|
||||||
|
name: 'edX',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
userProgress: [
|
||||||
|
{
|
||||||
|
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||||
|
completed: 4,
|
||||||
|
in_progress: 2,
|
||||||
|
not_started: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
||||||
|
completed: 1,
|
||||||
|
in_progress: 0,
|
||||||
|
not_started: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the collection items based on passed in collection', () => {
|
||||||
|
const $cards = view.$el.find('.program-card');
|
||||||
|
expect($cards.length).toBe(2);
|
||||||
|
$cards.each((index, el) => {
|
||||||
|
// eslint-disable-next-line newline-per-chained-call
|
||||||
|
expect($(el).find('.title').html().trim()).toEqual(context.programsData[index].title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display no item if collection is empty', () => {
|
||||||
|
view.remove();
|
||||||
|
programCollection = new ProgramCollection([]);
|
||||||
|
view = new CollectionListView({
|
||||||
|
el: '.program-cards-container',
|
||||||
|
childView: ProgramCardView,
|
||||||
|
context: {},
|
||||||
|
collection: programCollection,
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
const $cards = view.$el.find('.program-card');
|
||||||
|
expect($cards.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no title when title not provided', () => {
|
||||||
|
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
|
||||||
|
view.remove();
|
||||||
|
view.render();
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
const $title = view.$el.parent().find('.collection-title');
|
||||||
|
expect($title.html()).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display screen reader header when provided', () => {
|
||||||
|
const titleContext = { el: 'h2', title: 'list start' };
|
||||||
|
|
||||||
|
view.remove();
|
||||||
|
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
|
||||||
|
programCollection = new ProgramCollection(context.programsData);
|
||||||
|
view = new CollectionListView({
|
||||||
|
el: '.program-cards-container',
|
||||||
|
childView: ProgramCardView,
|
||||||
|
context,
|
||||||
|
collection: programCollection,
|
||||||
|
titleContext,
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
const $title = view.$el.parent().find('.collection-title');
|
||||||
|
expect($title.html()).toBe(titleContext.title);
|
||||||
|
});
|
||||||
|
});
|
||||||
267
lms/static/js/learner_dashboard/spec/course_card_view_spec.js
Normal file
267
lms/static/js/learner_dashboard/spec/course_card_view_spec.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import CourseCardModel from '../models/course_card_model';
|
||||||
|
import CourseCardView from '../views/course_card_view';
|
||||||
|
|
||||||
|
describe('Course Card View', () => {
|
||||||
|
let view = null;
|
||||||
|
let courseCardModel;
|
||||||
|
let course;
|
||||||
|
const startDate = 'Feb 28, 2017';
|
||||||
|
const endDate = 'May 30, 2017';
|
||||||
|
|
||||||
|
const setupView = (data, isEnrolled, collectionCourseStatus) => {
|
||||||
|
const programData = $.extend({}, data);
|
||||||
|
const context = {
|
||||||
|
courseData: {
|
||||||
|
grades: {
|
||||||
|
'course-v1:WageningenX+FFESx+1T2017': 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collectionCourseStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof collectionCourseStatus === 'undefined') {
|
||||||
|
context.collectionCourseStatus = 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
programData.course_runs[0].is_enrolled = isEnrolled;
|
||||||
|
setFixtures('<div class="program-course-card"></div>');
|
||||||
|
courseCardModel = new CourseCardModel(programData);
|
||||||
|
view = new CourseCardView({
|
||||||
|
model: courseCardModel,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateCourseInfoDisplay = () => {
|
||||||
|
// DRY validation for course card in enrolled state
|
||||||
|
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(course.title);
|
||||||
|
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
|
||||||
|
course.course_runs[0].marketing_url,
|
||||||
|
);
|
||||||
|
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
||||||
|
`${startDate} - ${endDate}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// NOTE: This data is redefined prior to each test case so that tests
|
||||||
|
// can't break each other by modifying data copied by reference.
|
||||||
|
course = {
|
||||||
|
key: 'WageningenX+FFESx',
|
||||||
|
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',
|
||||||
|
title: 'Systems thinking and environmental sustainability',
|
||||||
|
course_runs: [
|
||||||
|
{
|
||||||
|
key: 'course-v1:WageningenX+FFESx+1T2017',
|
||||||
|
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||||
|
image: {
|
||||||
|
src: 'https://example.com/9f8562eb-f99b-45c7-b437-799fd0c15b6a.jpg',
|
||||||
|
},
|
||||||
|
marketing_url: 'https://www.edx.org/course/food-security-sustainability',
|
||||||
|
start: '2017-02-28T05:00:00Z',
|
||||||
|
end: '2017-05-30T23:00:00Z',
|
||||||
|
enrollment_start: '2017-01-18T00:00:00Z',
|
||||||
|
enrollment_end: null,
|
||||||
|
type: 'verified',
|
||||||
|
certificate_url: '',
|
||||||
|
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017',
|
||||||
|
enrollment_open_date: 'Jan 18, 2016',
|
||||||
|
is_course_ended: false,
|
||||||
|
is_enrolled: true,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
status: 'published',
|
||||||
|
upgrade_url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
setupView(course, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render final grade if course is completed', () => {
|
||||||
|
view.remove();
|
||||||
|
setupView(course, true);
|
||||||
|
expect(view.$('.grade-display').text()).toEqual('80%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render final grade if course has not been completed', () => {
|
||||||
|
view.remove();
|
||||||
|
setupView(course, true, 'in_progress');
|
||||||
|
expect(view.$('.final-grade').length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the course card based on the data not enrolled', () => {
|
||||||
|
view.remove();
|
||||||
|
setupView(course, false);
|
||||||
|
validateCourseInfoDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update render if the course card is_enrolled updated', () => {
|
||||||
|
setupView(course, false);
|
||||||
|
courseCardModel.set({
|
||||||
|
is_enrolled: true,
|
||||||
|
});
|
||||||
|
validateCourseInfoDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the course advertised start date', () => {
|
||||||
|
const advertisedStart = 'A long time ago...';
|
||||||
|
course.course_runs[0].advertised_start = advertisedStart;
|
||||||
|
|
||||||
|
setupView(course, false);
|
||||||
|
|
||||||
|
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
||||||
|
`${advertisedStart} - ${endDate}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only show certificate status section if a certificate has been earned', () => {
|
||||||
|
const certUrl = 'sample-certificate';
|
||||||
|
|
||||||
|
expect(view.$('.course-certificate .certificate-status').length).toEqual(0);
|
||||||
|
view.remove();
|
||||||
|
|
||||||
|
course.course_runs[0].certificate_url = certUrl;
|
||||||
|
setupView(course, false);
|
||||||
|
expect(view.$('.course-certificate .certificate-status').length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only show upgrade message section if an upgrade is required', () => {
|
||||||
|
const upgradeUrl = '/path/to/upgrade';
|
||||||
|
|
||||||
|
expect(view.$('.upgrade-message').length).toEqual(0);
|
||||||
|
view.remove();
|
||||||
|
|
||||||
|
course.course_runs[0].upgrade_url = upgradeUrl;
|
||||||
|
setupView(course, false);
|
||||||
|
expect(view.$('.upgrade-message').length).toEqual(1);
|
||||||
|
expect(view.$('.upgrade-message .cta-primary').attr('href')).toEqual(upgradeUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show both the upgrade message and certificate status sections', () => {
|
||||||
|
// Verify that no empty elements are left in the DOM.
|
||||||
|
course.course_runs[0].upgrade_url = '';
|
||||||
|
course.course_runs[0].certificate_url = '';
|
||||||
|
setupView(course, false);
|
||||||
|
expect(view.$('.upgrade-message').length).toEqual(0);
|
||||||
|
expect(view.$('.course-certificate .certificate-status').length).toEqual(0);
|
||||||
|
view.remove();
|
||||||
|
|
||||||
|
// Verify that the upgrade message takes priority.
|
||||||
|
course.course_runs[0].upgrade_url = '/path/to/upgrade';
|
||||||
|
course.course_runs[0].certificate_url = '/path/to/certificate';
|
||||||
|
setupView(course, false);
|
||||||
|
expect(view.$('.upgrade-message').length).toEqual(1);
|
||||||
|
expect(view.$('.course-certificate .certificate-status').length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow enrollment in future runs when the user has an expired enrollment', () => {
|
||||||
|
const newRun = $.extend({}, course.course_runs[0]);
|
||||||
|
const newRunKey = 'course-v1:foo+bar+baz';
|
||||||
|
const advertisedStart = 'Summer';
|
||||||
|
|
||||||
|
newRun.key = newRunKey;
|
||||||
|
newRun.is_enrolled = false;
|
||||||
|
newRun.advertised_start = advertisedStart;
|
||||||
|
course.course_runs.push(newRun);
|
||||||
|
|
||||||
|
course.expired = true;
|
||||||
|
|
||||||
|
setupView(course, true);
|
||||||
|
|
||||||
|
expect(courseCardModel.get('course_run_key')).toEqual(newRunKey);
|
||||||
|
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
||||||
|
`${advertisedStart} - ${endDate}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a message if an there is an upcoming course run', () => {
|
||||||
|
course.course_runs[0].is_enrollment_open = false;
|
||||||
|
|
||||||
|
setupView(course, false);
|
||||||
|
|
||||||
|
expect(view.$('.course-details .course-title').text().trim()).toEqual(course.title);
|
||||||
|
expect(view.$('.course-details .course-text .run-period').length).toBe(0);
|
||||||
|
expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon');
|
||||||
|
expect(view.$('.enrollment-open-date').text().trim()).toEqual(
|
||||||
|
course.course_runs[0].enrollment_open_date,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a message if there are no upcoming course runs', () => {
|
||||||
|
course.course_runs[0].is_enrollment_open = false;
|
||||||
|
course.course_runs[0].is_course_ended = true;
|
||||||
|
|
||||||
|
setupView(course, false);
|
||||||
|
|
||||||
|
expect(view.$('.course-details .course-title').text().trim()).toEqual(course.title);
|
||||||
|
expect(view.$('.course-details .course-text .run-period').length).toBe(0);
|
||||||
|
expect(view.$('.no-action-message').text().trim()).toBe('Not Currently Available');
|
||||||
|
expect(view.$('.enrollment-opens').length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should link to the marketing site when the user is not enrolled', () => {
|
||||||
|
setupView(course, false);
|
||||||
|
expect(view.$('.course-title-link').attr('href')).toEqual(course.course_runs[0].marketing_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should link to the course home when the user is enrolled', () => {
|
||||||
|
setupView(course, true);
|
||||||
|
expect(view.$('.course-title-link').attr('href')).toEqual(course.course_runs[0].course_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not link to the marketing site if the URL is not available', () => {
|
||||||
|
course.course_runs[0].marketing_url = null;
|
||||||
|
setupView(course, false);
|
||||||
|
|
||||||
|
expect(view.$('.course-title-link').length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not link to the course home if the URL is not available', () => {
|
||||||
|
course.course_runs[0].course_url = null;
|
||||||
|
setupView(course, true);
|
||||||
|
|
||||||
|
expect(view.$('.course-title-link').length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show an unfulfilled user entitlement allows you to select a session', () => {
|
||||||
|
course.user_entitlement = {
|
||||||
|
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
||||||
|
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
||||||
|
expiration_date: '2017-12-05 01:06:12',
|
||||||
|
};
|
||||||
|
setupView(course, false);
|
||||||
|
expect(view.$('.info-expires-at').text().trim()).toContain('You must select a session by');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a fulfilled expired user entitlement does not allow the changing of sessions', () => {
|
||||||
|
course.user_entitlement = {
|
||||||
|
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
||||||
|
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
||||||
|
expired_at: '2017-12-06 01:06:12',
|
||||||
|
expiration_date: '2017-12-05 01:06:12',
|
||||||
|
};
|
||||||
|
setupView(course, true);
|
||||||
|
expect(view.$('.info-expires-at').text().trim()).toContain('You can no longer change sessions.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a fulfilled user entitlement allows the changing of sessions', () => {
|
||||||
|
course.user_entitlement = {
|
||||||
|
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
||||||
|
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
||||||
|
expiration_date: '2017-12-05 01:06:12',
|
||||||
|
};
|
||||||
|
setupView(course, true);
|
||||||
|
expect(view.$('.info-expires-at').text().trim()).toContain('You can change sessions until');
|
||||||
|
});
|
||||||
|
});
|
||||||
303
lms/static/js/learner_dashboard/spec/course_enroll_view_spec.js
Normal file
303
lms/static/js/learner_dashboard/spec/course_enroll_view_spec.js
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
|
import CourseCardModel from '../models/course_card_model';
|
||||||
|
import CourseEnrollModel from '../models/course_enroll_model';
|
||||||
|
import CourseEnrollView from '../views/course_enroll_view';
|
||||||
|
|
||||||
|
describe('Course Enroll View', () => {
|
||||||
|
let view = null;
|
||||||
|
let courseCardModel;
|
||||||
|
let courseEnrollModel;
|
||||||
|
let urlModel;
|
||||||
|
let singleCourseRunList;
|
||||||
|
let multiCourseRunList;
|
||||||
|
const course = {
|
||||||
|
key: 'WageningenX+FFESx',
|
||||||
|
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',
|
||||||
|
title: 'Systems thinking and environmental sustainability',
|
||||||
|
owners: [
|
||||||
|
{
|
||||||
|
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||||
|
key: 'WageningenX',
|
||||||
|
name: 'Wageningen University & Research',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const urls = {
|
||||||
|
commerce_api_url: '/commerce',
|
||||||
|
track_selection_url: '/select_track/course/',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Stub analytics tracking
|
||||||
|
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||||
|
|
||||||
|
// NOTE: This data is redefined prior to each test case so that tests
|
||||||
|
// can't break each other by modifying data copied by reference.
|
||||||
|
singleCourseRunList = [{
|
||||||
|
key: 'course-v1:WageningenX+FFESx+1T2017',
|
||||||
|
uuid: '2f2edf03-79e6-4e39-aef0-65436a6ee344',
|
||||||
|
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||||
|
image: {
|
||||||
|
src: 'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg',
|
||||||
|
},
|
||||||
|
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx',
|
||||||
|
start: '2017-02-28T05:00:00Z',
|
||||||
|
end: '2017-05-30T23:00:00Z',
|
||||||
|
enrollment_start: '2017-01-18T00:00:00Z',
|
||||||
|
enrollment_end: null,
|
||||||
|
type: 'verified',
|
||||||
|
certificate_url: '',
|
||||||
|
course_url: 'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course',
|
||||||
|
enrollment_open_date: 'Jan 18, 2016',
|
||||||
|
is_course_ended: false,
|
||||||
|
is_enrolled: false,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
status: 'published',
|
||||||
|
upgrade_url: '',
|
||||||
|
}];
|
||||||
|
|
||||||
|
multiCourseRunList = [{
|
||||||
|
key: 'course-v1:WageningenX+FFESx+2T2016',
|
||||||
|
uuid: '9bbb7844-4848-44ab-8e20-0be6604886e9',
|
||||||
|
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||||
|
image: {
|
||||||
|
src: 'https://example.com/9bbb7844-4848-44ab-8e20-0be6604886e9.jpg',
|
||||||
|
},
|
||||||
|
short_description: 'Learn how to apply systems thinking to improve food production systems.',
|
||||||
|
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-stesx',
|
||||||
|
start: '2016-09-08T04:00:00Z',
|
||||||
|
end: '2016-11-11T00:00:00Z',
|
||||||
|
enrollment_start: null,
|
||||||
|
enrollment_end: null,
|
||||||
|
pacing_type: 'instructor_paced',
|
||||||
|
type: 'verified',
|
||||||
|
certificate_url: '',
|
||||||
|
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+2T2016',
|
||||||
|
enrollment_open_date: 'Jan 18, 2016',
|
||||||
|
is_course_ended: false,
|
||||||
|
is_enrolled: false,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
status: 'published',
|
||||||
|
}, {
|
||||||
|
key: 'course-v1:WageningenX+FFESx+1T2017',
|
||||||
|
uuid: '2f2edf03-79e6-4e39-aef0-65436a6ee344',
|
||||||
|
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||||
|
image: {
|
||||||
|
src: 'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg',
|
||||||
|
},
|
||||||
|
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx',
|
||||||
|
start: '2017-02-28T05:00:00Z',
|
||||||
|
end: '2017-05-30T23:00:00Z',
|
||||||
|
enrollment_start: '2017-01-18T00:00:00Z',
|
||||||
|
enrollment_end: null,
|
||||||
|
type: 'verified',
|
||||||
|
certificate_url: '',
|
||||||
|
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017',
|
||||||
|
enrollment_open_date: 'Jan 18, 2016',
|
||||||
|
is_course_ended: false,
|
||||||
|
is_enrolled: false,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
status: 'published',
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupView = (courseRuns, urlMap) => {
|
||||||
|
course.course_runs = courseRuns;
|
||||||
|
setFixtures('<div class="course-actions"></div>');
|
||||||
|
courseCardModel = new CourseCardModel(course);
|
||||||
|
courseEnrollModel = new CourseEnrollModel({}, {
|
||||||
|
courseId: courseCardModel.get('course_run_key'),
|
||||||
|
});
|
||||||
|
if (urlMap) {
|
||||||
|
urlModel = new Backbone.Model(urlMap);
|
||||||
|
}
|
||||||
|
view = new CourseEnrollView({
|
||||||
|
$parentEl: $('.course-actions'),
|
||||||
|
model: courseCardModel,
|
||||||
|
enrollModel: courseEnrollModel,
|
||||||
|
urlModel,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
urlModel = null;
|
||||||
|
courseCardModel = null;
|
||||||
|
courseEnrollModel = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
setupView(singleCourseRunList);
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the course enroll view when not enrolled', () => {
|
||||||
|
setupView(singleCourseRunList);
|
||||||
|
expect(view.$('.enroll-button').text().trim()).toEqual('Enroll Now');
|
||||||
|
expect(view.$('.run-select').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the course enroll view when enrolled', () => {
|
||||||
|
singleCourseRunList[0].is_enrolled = true;
|
||||||
|
|
||||||
|
setupView(singleCourseRunList);
|
||||||
|
expect(view.$('.view-course-button').text().trim()).toEqual('View Course');
|
||||||
|
expect(view.$('.run-select').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render anything if course runs are empty', () => {
|
||||||
|
setupView([]);
|
||||||
|
|
||||||
|
expect(view.$('.run-select').length).toBe(0);
|
||||||
|
expect(view.$('.enroll-button').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render run selection dropdown if multiple course runs are available', () => {
|
||||||
|
setupView(multiCourseRunList);
|
||||||
|
|
||||||
|
expect(view.$('.run-select').length).toBe(1);
|
||||||
|
expect(view.$('.run-select').val()).toEqual(multiCourseRunList[0].key);
|
||||||
|
expect(view.$('.run-select option').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow enrollment in unpublished course runs', () => {
|
||||||
|
multiCourseRunList[0].status = 'unpublished';
|
||||||
|
|
||||||
|
setupView(multiCourseRunList);
|
||||||
|
expect(view.$('.run-select').length).toBe(0);
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow enrollment in course runs with a null status', () => {
|
||||||
|
multiCourseRunList[0].status = null;
|
||||||
|
|
||||||
|
setupView(multiCourseRunList);
|
||||||
|
expect(view.$('.run-select').length).toBe(0);
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enroll learner when enroll button is clicked with one course run available', () => {
|
||||||
|
setupView(singleCourseRunList);
|
||||||
|
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
|
||||||
|
spyOn(courseEnrollModel, 'save');
|
||||||
|
|
||||||
|
view.$('.enroll-button').click();
|
||||||
|
|
||||||
|
expect(courseEnrollModel.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enroll learner when enroll button is clicked with multiple course runs available', () => {
|
||||||
|
setupView(multiCourseRunList);
|
||||||
|
|
||||||
|
spyOn(courseEnrollModel, 'save');
|
||||||
|
|
||||||
|
view.$('.run-select').val(multiCourseRunList[1].key);
|
||||||
|
view.$('.run-select').trigger('change');
|
||||||
|
view.$('.enroll-button').click();
|
||||||
|
|
||||||
|
expect(courseEnrollModel.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to track selection when audit enrollment succeeds', () => {
|
||||||
|
singleCourseRunList[0].is_enrolled = false;
|
||||||
|
singleCourseRunList[0].mode_slug = 'audit';
|
||||||
|
|
||||||
|
setupView(singleCourseRunList, urls);
|
||||||
|
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
expect(view.trackSelectionUrl).toBeDefined();
|
||||||
|
|
||||||
|
spyOn(CourseEnrollView, 'redirect');
|
||||||
|
|
||||||
|
view.enrollSuccess();
|
||||||
|
|
||||||
|
expect(CourseEnrollView.redirect).toHaveBeenCalledWith(
|
||||||
|
view.trackSelectionUrl + courseCardModel.get('course_run_key'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to track selection when enrollment in an unspecified mode is attempted', () => {
|
||||||
|
singleCourseRunList[0].is_enrolled = false;
|
||||||
|
singleCourseRunList[0].mode_slug = null;
|
||||||
|
|
||||||
|
setupView(singleCourseRunList, urls);
|
||||||
|
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
expect(view.trackSelectionUrl).toBeDefined();
|
||||||
|
|
||||||
|
spyOn(CourseEnrollView, 'redirect');
|
||||||
|
|
||||||
|
view.enrollSuccess();
|
||||||
|
|
||||||
|
expect(CourseEnrollView.redirect).toHaveBeenCalledWith(
|
||||||
|
view.trackSelectionUrl + courseCardModel.get('course_run_key'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect when urls are not provided', () => {
|
||||||
|
singleCourseRunList[0].is_enrolled = false;
|
||||||
|
singleCourseRunList[0].mode_slug = 'verified';
|
||||||
|
|
||||||
|
setupView(singleCourseRunList);
|
||||||
|
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
expect(view.verificationUrl).not.toBeDefined();
|
||||||
|
expect(view.dashboardUrl).not.toBeDefined();
|
||||||
|
expect(view.trackSelectionUrl).not.toBeDefined();
|
||||||
|
|
||||||
|
spyOn(CourseEnrollView, 'redirect');
|
||||||
|
|
||||||
|
view.enrollSuccess();
|
||||||
|
|
||||||
|
expect(CourseEnrollView.redirect).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to track selection on error', () => {
|
||||||
|
setupView(singleCourseRunList, urls);
|
||||||
|
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
expect(view.trackSelectionUrl).toBeDefined();
|
||||||
|
|
||||||
|
spyOn(CourseEnrollView, 'redirect');
|
||||||
|
|
||||||
|
view.enrollError(courseEnrollModel, { status: 500 });
|
||||||
|
expect(CourseEnrollView.redirect).toHaveBeenCalledWith(
|
||||||
|
view.trackSelectionUrl + courseCardModel.get('course_run_key'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to login on 403 error', () => {
|
||||||
|
const response = {
|
||||||
|
status: 403,
|
||||||
|
responseJSON: {
|
||||||
|
user_message_url: 'redirect/to/this',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setupView(singleCourseRunList, urls);
|
||||||
|
|
||||||
|
expect(view.$('.enroll-button').length).toBe(1);
|
||||||
|
expect(view.trackSelectionUrl).toBeDefined();
|
||||||
|
|
||||||
|
spyOn(CourseEnrollView, 'redirect');
|
||||||
|
|
||||||
|
view.enrollError(courseEnrollModel, response);
|
||||||
|
|
||||||
|
expect(CourseEnrollView.redirect).toHaveBeenCalledWith(
|
||||||
|
response.responseJSON.user_message_url,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends analytics event when enrollment succeeds', () => {
|
||||||
|
setupView(singleCourseRunList, urls);
|
||||||
|
spyOn(CourseEnrollView, 'redirect');
|
||||||
|
view.enrollSuccess();
|
||||||
|
expect(window.analytics.track).toHaveBeenCalledWith(
|
||||||
|
'edx.bi.user.program-details.enrollment',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import _ from 'underscore';
|
||||||
|
|
||||||
|
import CourseEntitlementView from '../views/course_entitlement_view';
|
||||||
|
|
||||||
|
describe('Course Entitlement View', () => {
|
||||||
|
let view = null;
|
||||||
|
let sessionIndex;
|
||||||
|
let selectOptions;
|
||||||
|
let entitlementAvailableSessions;
|
||||||
|
let initialSessionId;
|
||||||
|
let alreadyEnrolled;
|
||||||
|
let hasSessions;
|
||||||
|
const entitlementUUID = 'a9aiuw76a4ijs43u18';
|
||||||
|
const testSessionIds = ['test_session_id_1', 'test_session_id_2'];
|
||||||
|
|
||||||
|
const setupView = (isAlreadyEnrolled, hasAvailableSessions, specificSessionIndex) => {
|
||||||
|
setFixtures('<div class="course-entitlement-selection-container"></div>');
|
||||||
|
alreadyEnrolled = (typeof isAlreadyEnrolled !== 'undefined') ? isAlreadyEnrolled : true;
|
||||||
|
hasSessions = (typeof hasAvailableSessions !== 'undefined') ? hasAvailableSessions : true;
|
||||||
|
sessionIndex = (typeof specificSessionIndex !== 'undefined') ? specificSessionIndex : 0;
|
||||||
|
|
||||||
|
initialSessionId = alreadyEnrolled ? testSessionIds[sessionIndex] : '';
|
||||||
|
entitlementAvailableSessions = [];
|
||||||
|
if (hasSessions) {
|
||||||
|
entitlementAvailableSessions = [{
|
||||||
|
enrollment_end: null,
|
||||||
|
start: '2016-02-05T05:00:00+00:00',
|
||||||
|
pacing_type: 'instructor_paced',
|
||||||
|
session_id: testSessionIds[0],
|
||||||
|
end: null,
|
||||||
|
}, {
|
||||||
|
enrollment_end: '2019-12-22T03:30:00Z',
|
||||||
|
start: '2020-01-03T13:00:00+00:00',
|
||||||
|
pacing_type: 'self_paced',
|
||||||
|
session_id: testSessionIds[1],
|
||||||
|
end: '2020-03-09T21:30:00+00:00',
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
view = new CourseEntitlementView({
|
||||||
|
el: '.course-entitlement-selection-container',
|
||||||
|
triggerOpenBtn: '#course-card-0 .change-session',
|
||||||
|
courseCardMessages: '#course-card-0 .messages-list > .message',
|
||||||
|
courseTitleLink: '#course-card-0 .course-title a',
|
||||||
|
courseImageLink: '#course-card-0 .wrapper-course-image > a',
|
||||||
|
dateDisplayField: '#course-card-0 .info-date-block',
|
||||||
|
enterCourseBtn: '#course-card-0 .enter-course',
|
||||||
|
availableSessions: JSON.stringify(entitlementAvailableSessions),
|
||||||
|
entitlementUUID,
|
||||||
|
currentSessionId: initialSessionId,
|
||||||
|
userId: '1',
|
||||||
|
enrollUrl: '/api/enrollment/v1/enrollment',
|
||||||
|
courseHomeUrl: '/courses/course-v1:edX+DemoX+Demo_Course/course/',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (view) view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initialization of view', () => {
|
||||||
|
it('Should create a entitlement view element', () => {
|
||||||
|
setupView(false);
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Available Sessions Select - Unfulfilled Entitlement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupView(false);
|
||||||
|
selectOptions = view.$('.session-select').find('option');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Select session dropdown should show all available course runs and a coming soon option.', () => {
|
||||||
|
expect(selectOptions.length).toEqual(entitlementAvailableSessions.length + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Self paced courses should have visual indication in the selection option.', () => {
|
||||||
|
const selfPacedOptionIndex = _.findIndex(entitlementAvailableSessions, session => session.pacing_type === 'self_paced');
|
||||||
|
const selfPacedOption = selectOptions[selfPacedOptionIndex];
|
||||||
|
expect(selfPacedOption && selfPacedOption.text.includes('(Self-paced)')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Courses with an an enroll by date should indicate so on the selection option.', () => {
|
||||||
|
const enrollEndSetOptionIndex = _.findIndex(entitlementAvailableSessions,
|
||||||
|
session => session.enrollment_end !== null);
|
||||||
|
const enrollEndSetOption = selectOptions[enrollEndSetOptionIndex];
|
||||||
|
expect(enrollEndSetOption && enrollEndSetOption.text.includes('Open until')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Title element should correctly indicate the expected behavior.', () => {
|
||||||
|
expect(view.$('.action-header').text().includes(
|
||||||
|
'To access the course, select a session.',
|
||||||
|
)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Available Sessions Select - Unfulfilled Entitlement without available sessions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupView(false, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should notify user that more sessions are coming soon if none available.', () => {
|
||||||
|
expect(view.$('.action-header').text().includes('More sessions coming soon.')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Available Sessions Select - Fulfilled Entitlement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupView(true);
|
||||||
|
selectOptions = view.$('.session-select').find('option');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Select session dropdown should show available course runs, coming soon and leave options.', () => {
|
||||||
|
expect(selectOptions.length).toEqual(entitlementAvailableSessions.length + 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Select session dropdown should allow user to leave the current session.', () => {
|
||||||
|
const leaveSessionOption = selectOptions[selectOptions.length - 1];
|
||||||
|
expect(leaveSessionOption.text.includes('Leave the current session and decide later')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Currently selected session should be specified in the dropdown options.', () => {
|
||||||
|
const selectedSessionIndex = _.findIndex(entitlementAvailableSessions,
|
||||||
|
session => initialSessionId === session.session_id);
|
||||||
|
expect(selectOptions[selectedSessionIndex].text.includes('Currently Selected')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Title element should correctly indicate the expected behavior.', () => {
|
||||||
|
expect(view.$('.action-header').text().includes(
|
||||||
|
'Change to a different session or leave the current session.',
|
||||||
|
)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Available Sessions Select - Fulfilled Entitlement (session in the future)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupView(true, true, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Currently selected session should initialize to selected in the dropdown options.', () => {
|
||||||
|
const selectedOption = view.$('.session-select').find('option:selected');
|
||||||
|
expect(selectedOption.data('session_id')).toEqual(testSessionIds[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Select Session Action Button and popover behavior - Unfulfilled Entitlement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupView(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Change session button should have the correct text.', () => {
|
||||||
|
expect(view.$('.enroll-btn-initial').text() === 'Select Session').toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Select session button should show popover when clicked.', () => {
|
||||||
|
view.$('.enroll-btn-initial').click();
|
||||||
|
expect(view.$('.verification-modal').length > 0).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Change Session Action Button and popover behavior - Fulfilled Entitlement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupView(true);
|
||||||
|
selectOptions = view.$('.session-select').find('option');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Change session button should show correct text.', () => {
|
||||||
|
expect(view.$('.enroll-btn-initial').text().trim() === 'Change Session').toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Switch session button should be disabled when on the currently enrolled session.', () => {
|
||||||
|
expect(view.$('.enroll-btn-initial')).toHaveClass('disabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import EntitlementUnenrollmentView from '../views/entitlement_unenrollment_view';
|
||||||
|
|
||||||
|
describe('EntitlementUnenrollmentView', () => {
|
||||||
|
let view = null;
|
||||||
|
const options = {
|
||||||
|
dashboardPath: '/dashboard',
|
||||||
|
signInPath: '/login',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initView = () => new EntitlementUnenrollmentView(options);
|
||||||
|
|
||||||
|
const modalHtml = '<a id="link1" class="js-entitlement-action-unenroll" ' +
|
||||||
|
' data-course-name="Test Course 1" ' +
|
||||||
|
' data-course-number="test1" ' +
|
||||||
|
' data-entitlement-api-endpoint="/test/api/endpoint/1">Unenroll</a> ' +
|
||||||
|
'<a id="link2" class="js-entitlement-action-unenroll" ' +
|
||||||
|
' data-course-name="Test Course 2" ' +
|
||||||
|
' data-course-number="test2" ' +
|
||||||
|
' data-entitlement-api-endpoint="/test/api/endpoint/2">Unenroll</a> ' +
|
||||||
|
'<div class="js-entitlement-unenrollment-modal"> ' +
|
||||||
|
' <span class="js-entitlement-unenrollment-modal-header-text"></span> ' +
|
||||||
|
' <span class="js-entitlement-unenrollment-modal-error-text"></span> ' +
|
||||||
|
' <button class="js-entitlement-unenrollment-modal-submit">Unenroll</button> ' +
|
||||||
|
'</div> ';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures(modalHtml);
|
||||||
|
view = initView();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an unenroll link is clicked', () => {
|
||||||
|
it('should reset the modal and set the correct values for header/submit', () => {
|
||||||
|
const $link1 = $('#link1');
|
||||||
|
const $link2 = $('#link2');
|
||||||
|
const $headerTxt = $('.js-entitlement-unenrollment-modal-header-text');
|
||||||
|
const $errorTxt = $('.js-entitlement-unenrollment-modal-error-text');
|
||||||
|
const $submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
||||||
|
|
||||||
|
$link1.trigger('click');
|
||||||
|
expect($headerTxt.html().startsWith('Are you sure you want to unenroll from Test Course 1')).toBe(true);
|
||||||
|
expect($submitBtn.data()).toEqual({ entitlementApiEndpoint: '/test/api/endpoint/1' });
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(false);
|
||||||
|
expect($errorTxt.html()).toEqual('');
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
||||||
|
|
||||||
|
// Set an error so that we can see that the modal is reset properly when clicked again
|
||||||
|
view.setError('This is an error');
|
||||||
|
expect($errorTxt.html()).toEqual('This is an error');
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(true);
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(true);
|
||||||
|
|
||||||
|
$link2.trigger('click');
|
||||||
|
expect($headerTxt.html().startsWith('Are you sure you want to unenroll from Test Course 2')).toBe(true);
|
||||||
|
expect($submitBtn.data()).toEqual({ entitlementApiEndpoint: '/test/api/endpoint/2' });
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(false);
|
||||||
|
expect($errorTxt.html()).toEqual('');
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the unenroll submit button is clicked', () => {
|
||||||
|
it('should send a DELETE request to the configured apiEndpoint', () => {
|
||||||
|
const $submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
||||||
|
const apiEndpoint = '/test/api/endpoint/1';
|
||||||
|
|
||||||
|
view.setSubmitData(apiEndpoint);
|
||||||
|
|
||||||
|
spyOn($, 'ajax').and.callFake((opts) => {
|
||||||
|
expect(opts.url).toEqual(apiEndpoint);
|
||||||
|
expect(opts.method).toEqual('DELETE');
|
||||||
|
expect(opts.complete).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
$submitBtn.trigger('click');
|
||||||
|
expect($.ajax).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set an error and disable submit if the apiEndpoint has not been properly set', () => {
|
||||||
|
const $errorTxt = $('.js-entitlement-unenrollment-modal-error-text');
|
||||||
|
const $submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
||||||
|
|
||||||
|
expect($submitBtn.data()).toEqual({});
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(false);
|
||||||
|
expect($errorTxt.html()).toEqual('');
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
||||||
|
|
||||||
|
spyOn($, 'ajax');
|
||||||
|
$submitBtn.trigger('click');
|
||||||
|
expect($.ajax).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect($submitBtn.data()).toEqual({});
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(true);
|
||||||
|
expect($errorTxt.html()).toEqual(view.genericErrorMsg);
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the unenroll request is complete', () => {
|
||||||
|
it('should redirect to the dashboard if the request was successful', () => {
|
||||||
|
const $submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
||||||
|
const apiEndpoint = '/test/api/endpoint/1';
|
||||||
|
|
||||||
|
view.setSubmitData(apiEndpoint);
|
||||||
|
|
||||||
|
spyOn($, 'ajax').and.callFake((opts) => {
|
||||||
|
expect(opts.url).toEqual(apiEndpoint);
|
||||||
|
expect(opts.method).toEqual('DELETE');
|
||||||
|
expect(opts.complete).toBeTruthy();
|
||||||
|
|
||||||
|
opts.complete({
|
||||||
|
status: 204,
|
||||||
|
responseJSON: { detail: 'success' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
spyOn(EntitlementUnenrollmentView, 'redirectTo');
|
||||||
|
|
||||||
|
$submitBtn.trigger('click');
|
||||||
|
expect($.ajax).toHaveBeenCalled();
|
||||||
|
expect(EntitlementUnenrollmentView.redirectTo).toHaveBeenCalledWith(view.dashboardPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to the login page if the request failed with an auth error', () => {
|
||||||
|
const $submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
||||||
|
const apiEndpoint = '/test/api/endpoint/1';
|
||||||
|
|
||||||
|
view.setSubmitData(apiEndpoint);
|
||||||
|
|
||||||
|
spyOn($, 'ajax').and.callFake((opts) => {
|
||||||
|
expect(opts.url).toEqual(apiEndpoint);
|
||||||
|
expect(opts.method).toEqual('DELETE');
|
||||||
|
expect(opts.complete).toBeTruthy();
|
||||||
|
|
||||||
|
opts.complete({
|
||||||
|
status: 401,
|
||||||
|
responseJSON: { detail: 'Authentication credentials were not provided.' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
spyOn(EntitlementUnenrollmentView, 'redirectTo');
|
||||||
|
|
||||||
|
$submitBtn.trigger('click');
|
||||||
|
expect($.ajax).toHaveBeenCalled();
|
||||||
|
expect(EntitlementUnenrollmentView.redirectTo).toHaveBeenCalledWith(
|
||||||
|
`${view.signInPath}?next=${encodeURIComponent(view.dashboardPath)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set an error and disable submit if a non-auth error occurs', () => {
|
||||||
|
const $errorTxt = $('.js-entitlement-unenrollment-modal-error-text');
|
||||||
|
const $submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
||||||
|
const apiEndpoint = '/test/api/endpoint/1';
|
||||||
|
|
||||||
|
view.setSubmitData(apiEndpoint);
|
||||||
|
|
||||||
|
spyOn($, 'ajax').and.callFake((opts) => {
|
||||||
|
expect(opts.url).toEqual(apiEndpoint);
|
||||||
|
expect(opts.method).toEqual('DELETE');
|
||||||
|
expect(opts.complete).toBeTruthy();
|
||||||
|
|
||||||
|
opts.complete({
|
||||||
|
status: 400,
|
||||||
|
responseJSON: { detail: 'Bad request.' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
spyOn(EntitlementUnenrollmentView, 'redirectTo');
|
||||||
|
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(false);
|
||||||
|
expect($errorTxt.html()).toEqual('');
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
||||||
|
|
||||||
|
$submitBtn.trigger('click');
|
||||||
|
|
||||||
|
expect($submitBtn.prop('disabled')).toBe(true);
|
||||||
|
expect($errorTxt.html()).toEqual(view.genericErrorMsg);
|
||||||
|
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(true);
|
||||||
|
|
||||||
|
expect($.ajax).toHaveBeenCalled();
|
||||||
|
expect(EntitlementUnenrollmentView.redirectTo).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
130
lms/static/js/learner_dashboard/spec/program_card_view_spec.js
Normal file
130
lms/static/js/learner_dashboard/spec/program_card_view_spec.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import ProgramCardView from '../views/program_card_view';
|
||||||
|
import ProgramModel from '../models/program_model';
|
||||||
|
import ProgressCollection from '../collections/program_progress_collection';
|
||||||
|
|
||||||
|
describe('Program card View', () => {
|
||||||
|
let view = null;
|
||||||
|
let programModel;
|
||||||
|
const program = {
|
||||||
|
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||||
|
title: 'Food Security and Sustainability',
|
||||||
|
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
||||||
|
type: 'XSeries',
|
||||||
|
detail_url: 'https://www.edx.org/foo/bar',
|
||||||
|
banner_image: {
|
||||||
|
medium: {
|
||||||
|
height: 242,
|
||||||
|
width: 726,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg',
|
||||||
|
},
|
||||||
|
'x-small': {
|
||||||
|
height: 116,
|
||||||
|
width: 348,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg',
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
height: 145,
|
||||||
|
width: 435,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
height: 480,
|
||||||
|
width: 1440,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authoring_organizations: [
|
||||||
|
{
|
||||||
|
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||||
|
key: 'WageningenX',
|
||||||
|
name: 'Wageningen University & Research',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const userProgress = [
|
||||||
|
{
|
||||||
|
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||||
|
completed: 4,
|
||||||
|
in_progress: 2,
|
||||||
|
not_started: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
||||||
|
completed: 1,
|
||||||
|
in_progress: 0,
|
||||||
|
not_started: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const progressCollection = new ProgressCollection();
|
||||||
|
const cardRenders = ($card) => {
|
||||||
|
expect($card).toBeDefined();
|
||||||
|
expect($card.find('.title').html().trim()).toEqual(program.title);
|
||||||
|
expect($card.find('.category span').html().trim()).toEqual(program.type);
|
||||||
|
expect($card.find('.organization').html().trim()).toEqual(program.authoring_organizations[0].key);
|
||||||
|
expect($card.find('.card-link').attr('href')).toEqual(program.detail_url);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div class="program-card"></div>');
|
||||||
|
programModel = new ProgramModel(program);
|
||||||
|
progressCollection.set(userProgress);
|
||||||
|
view = new ProgramCardView({
|
||||||
|
model: programModel,
|
||||||
|
context: {
|
||||||
|
progressCollection,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the program-card based on passed in context', () => {
|
||||||
|
cardRenders(view.$el);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call reEvaluatePicture if reLoadBannerImage is called', () => {
|
||||||
|
spyOn(ProgramCardView, 'reEvaluatePicture');
|
||||||
|
view.reLoadBannerImage();
|
||||||
|
expect(ProgramCardView.reEvaluatePicture).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle exceptions from reEvaluatePicture', () => {
|
||||||
|
const message = 'Picturefill had exceptions';
|
||||||
|
|
||||||
|
spyOn(ProgramCardView, 'reEvaluatePicture').and.callFake(() => {
|
||||||
|
const error = { name: message };
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
view.reLoadBannerImage();
|
||||||
|
expect(ProgramCardView.reEvaluatePicture).toHaveBeenCalled();
|
||||||
|
expect(view.reLoadBannerImage).not.toThrow(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the right number of progress bar segments', () => {
|
||||||
|
expect(view.$('.progress-bar .completed').length).toEqual(4);
|
||||||
|
expect(view.$('.progress-bar .enrolled').length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the correct course status numbers', () => {
|
||||||
|
expect(view.$('.number-circle').text()).toEqual('424');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render cards if there is no progressData', () => {
|
||||||
|
view.remove();
|
||||||
|
view = new ProgramCardView({
|
||||||
|
model: programModel,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
cardRenders(view.$el);
|
||||||
|
expect(view.$('.progress').length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
|
import ProgramHeaderView from '../views/program_header_view';
|
||||||
|
|
||||||
|
describe('Program Details Header View', () => {
|
||||||
|
let view = null;
|
||||||
|
const context = {
|
||||||
|
programData: {
|
||||||
|
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||||
|
title: 'Food Security and Sustainability',
|
||||||
|
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
||||||
|
type: 'XSeries',
|
||||||
|
detail_url: 'https://www.edx.org/foo/bar',
|
||||||
|
banner_image: {
|
||||||
|
medium: {
|
||||||
|
height: 242,
|
||||||
|
width: 726,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg',
|
||||||
|
},
|
||||||
|
'x-small': {
|
||||||
|
height: 116,
|
||||||
|
width: 348,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg',
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
height: 145,
|
||||||
|
width: 435,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
height: 480,
|
||||||
|
width: 1440,
|
||||||
|
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authoring_organizations: [
|
||||||
|
{
|
||||||
|
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||||
|
key: 'WageningenX',
|
||||||
|
name: 'Wageningen University & Research',
|
||||||
|
certificate_logo_image_url: 'https://example.com/org-certificate-logo.jpg',
|
||||||
|
logo_image_url: 'https://example.com/org-logo.jpg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div class="js-program-header"></div>');
|
||||||
|
view = new ProgramHeaderView({
|
||||||
|
model: new Backbone.Model(context),
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the header based on the passed in model', () => {
|
||||||
|
expect(view.$('.program-title').html()).toEqual(context.programData.title);
|
||||||
|
expect(view.$('.org-logo').length).toEqual(context.programData.authoring_organizations.length);
|
||||||
|
expect(view.$('.org-logo').attr('src'))
|
||||||
|
.toEqual(context.programData.authoring_organizations[0].certificate_logo_image_url);
|
||||||
|
expect(view.$('.org-logo').attr('alt'))
|
||||||
|
.toEqual(`${context.programData.authoring_organizations[0].name}'s logo`);
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,626 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import ProgramDetailsView from '../views/program_details_view';
|
||||||
|
|
||||||
|
describe('Program Details Header View', () => {
|
||||||
|
let view = null;
|
||||||
|
const options = {
|
||||||
|
programData: {
|
||||||
|
subtitle: '',
|
||||||
|
overview: '',
|
||||||
|
weeks_to_complete: null,
|
||||||
|
corporate_endorsements: [],
|
||||||
|
video: null,
|
||||||
|
type: 'Test',
|
||||||
|
max_hours_effort_per_week: null,
|
||||||
|
transcript_languages: [
|
||||||
|
'en-us',
|
||||||
|
],
|
||||||
|
expected_learning_items: [],
|
||||||
|
uuid: '0ffff5d6-0177-4690-9a48-aa2fecf94610',
|
||||||
|
title: 'Test Course Title',
|
||||||
|
languages: [
|
||||||
|
'en-us',
|
||||||
|
],
|
||||||
|
subjects: [],
|
||||||
|
individual_endorsements: [],
|
||||||
|
staff: [
|
||||||
|
{
|
||||||
|
family_name: 'Tester',
|
||||||
|
uuid: '11ee1afb-5750-4185-8434-c9ae8297f0f1',
|
||||||
|
bio: 'Dr. Tester, PhD, RD, is an Associate Professor at the School of Nutrition.',
|
||||||
|
profile_image: {},
|
||||||
|
profile_image_url: 'some image',
|
||||||
|
given_name: 'Bob',
|
||||||
|
urls: {
|
||||||
|
blog: null,
|
||||||
|
twitter: null,
|
||||||
|
facebook: null,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
organization_name: 'Test University',
|
||||||
|
title: 'Associate Professor of Nutrition',
|
||||||
|
},
|
||||||
|
works: [],
|
||||||
|
slug: 'dr-tester',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
marketing_slug: 'testing',
|
||||||
|
marketing_url: 'someurl',
|
||||||
|
status: 'active',
|
||||||
|
credit_redemption_overview: '',
|
||||||
|
discount_data: {
|
||||||
|
currency: 'USD',
|
||||||
|
discount_value: 0,
|
||||||
|
is_discounted: false,
|
||||||
|
total_incl_tax: 300,
|
||||||
|
total_incl_tax_excl_discounts: 300,
|
||||||
|
},
|
||||||
|
full_program_price: 300,
|
||||||
|
card_image_url: 'some image',
|
||||||
|
faq: [],
|
||||||
|
price_ranges: [
|
||||||
|
{
|
||||||
|
max: 378,
|
||||||
|
total: 109,
|
||||||
|
min: 10,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
banner_image: {
|
||||||
|
large: {
|
||||||
|
url: 'someurl',
|
||||||
|
width: 1440,
|
||||||
|
height: 480,
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
url: 'someurl',
|
||||||
|
width: 435,
|
||||||
|
height: 145,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
url: 'someurl',
|
||||||
|
width: 726,
|
||||||
|
height: 242,
|
||||||
|
},
|
||||||
|
'x-small': {
|
||||||
|
url: 'someurl',
|
||||||
|
width: 348,
|
||||||
|
height: 116,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authoring_organizations: [
|
||||||
|
{
|
||||||
|
description: '<p>Learning University is home to leading creators, entrepreneurs.</p>',
|
||||||
|
tags: [
|
||||||
|
'contributor',
|
||||||
|
],
|
||||||
|
name: 'Learning University',
|
||||||
|
homepage_url: null,
|
||||||
|
key: 'LearnX',
|
||||||
|
certificate_logo_image_url: null,
|
||||||
|
marketing_url: 'someurl',
|
||||||
|
logo_image_url: 'https://stage.edx.org/sites/default/files/school/image/logo/learnx.png',
|
||||||
|
uuid: 'de3e9ff0-477d-4496-8cfa-a98f902e5830',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: '<p>The Test University was chartered in 1868.</p>',
|
||||||
|
tags: [
|
||||||
|
'charter',
|
||||||
|
'contributor',
|
||||||
|
],
|
||||||
|
name: 'Test University',
|
||||||
|
homepage_url: null,
|
||||||
|
key: 'TestX',
|
||||||
|
certificate_logo_image_url: null,
|
||||||
|
marketing_url: 'someurl',
|
||||||
|
logo_image_url: 'https://stage.edx.org/sites/default/files/school/image/logo/ritx.png',
|
||||||
|
uuid: '54bc81cb-b736-4505-aa51-dd2b18c61d84',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
job_outlook_items: [],
|
||||||
|
credit_backing_organizations: [],
|
||||||
|
weeks_to_complete_min: 8,
|
||||||
|
weeks_to_complete_max: 8,
|
||||||
|
min_hours_effort_per_week: null,
|
||||||
|
is_learner_eligible_for_one_click_purchase: false,
|
||||||
|
},
|
||||||
|
courseData: {
|
||||||
|
completed: [
|
||||||
|
{
|
||||||
|
owners: [
|
||||||
|
{
|
||||||
|
uuid: '766a3716-f962-425b-b56e-e214c019b229',
|
||||||
|
key: 'Testx',
|
||||||
|
name: 'Test University',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uuid: '4be8dceb-3454-4fbf-8993-17d563ab41d4',
|
||||||
|
title: 'Who let the dogs out',
|
||||||
|
image: null,
|
||||||
|
key: 'Testx+DOGx002',
|
||||||
|
course_runs: [
|
||||||
|
{
|
||||||
|
upgrade_url: null,
|
||||||
|
image: {
|
||||||
|
src: 'someurl',
|
||||||
|
width: null,
|
||||||
|
description: null,
|
||||||
|
height: null,
|
||||||
|
},
|
||||||
|
max_effort: null,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
course: 'Testx+DOGx002',
|
||||||
|
content_language: null,
|
||||||
|
eligible_for_financial_aid: true,
|
||||||
|
seats: [
|
||||||
|
{
|
||||||
|
sku: '4250900',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '89.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: '',
|
||||||
|
type: 'verified',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
course_url: '/courses/course-v1:Testx+DOGx002+1T2016/',
|
||||||
|
availability: 'Archived',
|
||||||
|
transcript_languages: [],
|
||||||
|
staff: [],
|
||||||
|
announcement: null,
|
||||||
|
end: '2016-10-01T23:59:00Z',
|
||||||
|
uuid: 'f0ac45f5-f0d6-44bc-aeb9-a14e36e963a5',
|
||||||
|
title: 'Who let the dogs out',
|
||||||
|
certificate_url: '/certificates/1730700d89434b718d0d91f8b5d339bf',
|
||||||
|
enrollment_start: null,
|
||||||
|
start: '2017-03-21T22:18:15Z',
|
||||||
|
min_effort: null,
|
||||||
|
short_description: null,
|
||||||
|
hidden: false,
|
||||||
|
level_type: null,
|
||||||
|
type: 'verified',
|
||||||
|
enrollment_open_date: 'Jan 01, 1900',
|
||||||
|
marketing_url: null,
|
||||||
|
is_course_ended: false,
|
||||||
|
instructors: [],
|
||||||
|
full_description: null,
|
||||||
|
key: 'course-v1:Testx+DOGx002+1T2016',
|
||||||
|
enrollment_end: null,
|
||||||
|
reporting_type: 'mooc',
|
||||||
|
advertised_start: null,
|
||||||
|
mobile_available: false,
|
||||||
|
modified: '2017-03-24T14:22:15.609907Z',
|
||||||
|
is_enrolled: true,
|
||||||
|
pacing_type: 'self_paced',
|
||||||
|
video: null,
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
in_progress: [
|
||||||
|
{
|
||||||
|
owners: [
|
||||||
|
{
|
||||||
|
uuid: 'c484a523-d396-4aff-90f4-bb7e82e16bf6',
|
||||||
|
key: 'LearnX',
|
||||||
|
name: 'Learning University',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uuid: '872ec14c-3b7d-44b8-9cf2-9fa62182e1dd',
|
||||||
|
title: 'Star Trek: The Next Generation',
|
||||||
|
image: null,
|
||||||
|
key: 'LearnX+NGIx',
|
||||||
|
course_runs: [
|
||||||
|
{
|
||||||
|
upgrade_url: 'someurl',
|
||||||
|
image: {
|
||||||
|
src: '',
|
||||||
|
width: null,
|
||||||
|
description: null,
|
||||||
|
height: null,
|
||||||
|
},
|
||||||
|
max_effort: null,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
course: 'LearnX+NGx',
|
||||||
|
content_language: null,
|
||||||
|
eligible_for_financial_aid: true,
|
||||||
|
seats: [
|
||||||
|
{
|
||||||
|
sku: '44EEB26',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '0.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: null,
|
||||||
|
type: 'audit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sku: '64AAFBA',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '10.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: '2017-04-29T00:00:00Z',
|
||||||
|
credit_provider: null,
|
||||||
|
type: 'verified',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
course_url: 'someurl',
|
||||||
|
availability: 'Current',
|
||||||
|
transcript_languages: [],
|
||||||
|
staff: [],
|
||||||
|
announcement: null,
|
||||||
|
end: '2017-03-31T12:00:00Z',
|
||||||
|
uuid: 'ce841f5b-f5a9-428f-b187-e6372b532266',
|
||||||
|
title: 'Star Trek: The Next Generation',
|
||||||
|
certificate_url: null,
|
||||||
|
enrollment_start: '2014-03-31T20:00:00Z',
|
||||||
|
start: '2017-03-20T20:50:14Z',
|
||||||
|
min_effort: null,
|
||||||
|
short_description: null,
|
||||||
|
hidden: false,
|
||||||
|
level_type: null,
|
||||||
|
type: 'verified',
|
||||||
|
enrollment_open_date: 'Jan 01, 1900',
|
||||||
|
marketing_url: 'someurl',
|
||||||
|
is_course_ended: false,
|
||||||
|
instructors: [],
|
||||||
|
full_description: null,
|
||||||
|
key: 'course-v1:LearnX+NGIx+3T2016',
|
||||||
|
enrollment_end: null,
|
||||||
|
reporting_type: 'mooc',
|
||||||
|
advertised_start: null,
|
||||||
|
mobile_available: false,
|
||||||
|
modified: '2017-03-24T14:16:47.547643Z',
|
||||||
|
is_enrolled: true,
|
||||||
|
pacing_type: 'instructor_paced',
|
||||||
|
video: null,
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uuid: '0ffff5d6-0177-4690-9a48-aa2fecf94610',
|
||||||
|
not_started: [
|
||||||
|
{
|
||||||
|
owners: [
|
||||||
|
{
|
||||||
|
uuid: '766a3716-f962-425b-b56e-e214c019b229',
|
||||||
|
key: 'Testx',
|
||||||
|
name: 'Test University',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uuid: '88da08e4-e9ef-406e-95d7-7a178f9f9695',
|
||||||
|
title: 'Introduction to Health and Wellness',
|
||||||
|
image: null,
|
||||||
|
key: 'Testx+EXW100x',
|
||||||
|
course_runs: [
|
||||||
|
{
|
||||||
|
upgrade_url: null,
|
||||||
|
image: {
|
||||||
|
src: 'someurl',
|
||||||
|
width: null,
|
||||||
|
description: null,
|
||||||
|
height: null,
|
||||||
|
},
|
||||||
|
max_effort: null,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
course: 'Testx+EXW100x',
|
||||||
|
content_language: 'en-us',
|
||||||
|
eligible_for_financial_aid: true,
|
||||||
|
seats: [
|
||||||
|
{
|
||||||
|
sku: '',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '0.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: '',
|
||||||
|
type: 'audit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sku: '',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '10.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: '',
|
||||||
|
type: 'verified',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
course_url: 'someurl',
|
||||||
|
availability: 'Archived',
|
||||||
|
transcript_languages: [
|
||||||
|
'en-us',
|
||||||
|
],
|
||||||
|
staff: [
|
||||||
|
{
|
||||||
|
family_name: 'Tester',
|
||||||
|
uuid: '11ee1afb-5750-4185-8434-c9ae8297f0f1',
|
||||||
|
bio: 'Dr. Tester, PhD, RD, is a Professor at the School of Nutrition.',
|
||||||
|
profile_image: {},
|
||||||
|
profile_image_url: 'someimage.jpg',
|
||||||
|
given_name: 'Bob',
|
||||||
|
urls: {
|
||||||
|
blog: null,
|
||||||
|
twitter: null,
|
||||||
|
facebook: null,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
organization_name: 'Test University',
|
||||||
|
title: 'Associate Professor of Nutrition',
|
||||||
|
},
|
||||||
|
works: [],
|
||||||
|
slug: 'dr-tester',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
announcement: null,
|
||||||
|
end: '2017-03-25T22:18:33Z',
|
||||||
|
uuid: 'a36efd39-6637-11e6-a8e3-22000bdde520',
|
||||||
|
title: 'Introduction to Jedi',
|
||||||
|
certificate_url: null,
|
||||||
|
enrollment_start: null,
|
||||||
|
start: '2016-01-11T05:00:00Z',
|
||||||
|
min_effort: null,
|
||||||
|
short_description: null,
|
||||||
|
hidden: false,
|
||||||
|
level_type: null,
|
||||||
|
type: 'verified',
|
||||||
|
enrollment_open_date: 'Jan 01, 1900',
|
||||||
|
marketing_url: 'someurl',
|
||||||
|
is_course_ended: false,
|
||||||
|
instructors: [],
|
||||||
|
full_description: null,
|
||||||
|
key: 'course-v1:Testx+EXW100x+1T2016',
|
||||||
|
enrollment_end: null,
|
||||||
|
reporting_type: 'mooc',
|
||||||
|
advertised_start: null,
|
||||||
|
mobile_available: true,
|
||||||
|
modified: '2017-03-24T14:18:08.693748Z',
|
||||||
|
is_enrolled: false,
|
||||||
|
pacing_type: 'instructor_paced',
|
||||||
|
video: null,
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upgrade_url: null,
|
||||||
|
image: {
|
||||||
|
src: 'someurl',
|
||||||
|
width: null,
|
||||||
|
description: null,
|
||||||
|
height: null,
|
||||||
|
},
|
||||||
|
max_effort: null,
|
||||||
|
is_enrollment_open: true,
|
||||||
|
course: 'Testx+EXW100x',
|
||||||
|
content_language: null,
|
||||||
|
eligible_for_financial_aid: true,
|
||||||
|
seats: [
|
||||||
|
{
|
||||||
|
sku: '77AA8F2',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '0.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: null,
|
||||||
|
type: 'audit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sku: '7EC7BB0',
|
||||||
|
credit_hours: null,
|
||||||
|
price: '100.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: null,
|
||||||
|
type: 'verified',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sku: 'BD436CC',
|
||||||
|
credit_hours: 10,
|
||||||
|
price: '378.00',
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_deadline: null,
|
||||||
|
credit_provider: 'asu',
|
||||||
|
type: 'credit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
course_url: 'someurl',
|
||||||
|
availability: 'Archived',
|
||||||
|
transcript_languages: [],
|
||||||
|
staff: [],
|
||||||
|
announcement: null,
|
||||||
|
end: '2016-07-29T00:00:00Z',
|
||||||
|
uuid: '03b34748-19b1-4732-9ea2-e68da95024e6',
|
||||||
|
title: 'Introduction to Jedi',
|
||||||
|
certificate_url: null,
|
||||||
|
enrollment_start: null,
|
||||||
|
start: '2017-03-22T18:10:39Z',
|
||||||
|
min_effort: null,
|
||||||
|
short_description: null,
|
||||||
|
hidden: false,
|
||||||
|
level_type: null,
|
||||||
|
type: 'credit',
|
||||||
|
enrollment_open_date: 'Jan 01, 1900',
|
||||||
|
marketing_url: null,
|
||||||
|
is_course_ended: false,
|
||||||
|
instructors: [],
|
||||||
|
full_description: null,
|
||||||
|
key: 'course-v1:Testx+EXW100x+2164C',
|
||||||
|
enrollment_end: '2016-06-18T19:00:00Z',
|
||||||
|
reporting_type: 'mooc',
|
||||||
|
advertised_start: null,
|
||||||
|
mobile_available: false,
|
||||||
|
modified: '2017-03-23T16:47:37.108260Z',
|
||||||
|
is_enrolled: false,
|
||||||
|
pacing_type: 'self_paced',
|
||||||
|
video: null,
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grades: {
|
||||||
|
'course-v1:Testx+DOGx002+1T2016': 0.9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
urls: {
|
||||||
|
program_listing_url: '/dashboard/programs/',
|
||||||
|
commerce_api_url: '/api/commerce/v0/baskets/',
|
||||||
|
track_selection_url: '/course_modes/choose/',
|
||||||
|
},
|
||||||
|
userPreferences: {
|
||||||
|
'pref-lang': 'en',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const data = options.programData;
|
||||||
|
|
||||||
|
const initView = (updates) => {
|
||||||
|
const viewOptions = $.extend({}, options, updates);
|
||||||
|
|
||||||
|
return new ProgramDetailsView(viewOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div class="js-program-details-wrapper"></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the header', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect(view.$('.js-program-header h2').html()).toEqual(data.title);
|
||||||
|
expect(view.$('.js-program-header .org-logo')[0].src).toEqual(
|
||||||
|
data.authoring_organizations[0].logo_image_url,
|
||||||
|
);
|
||||||
|
expect(view.$('.js-program-header .org-logo')[1].src).toEqual(
|
||||||
|
data.authoring_organizations[1].logo_image_url,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the program heading program journey message if program not completed', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect(view.$('.program-heading-title').text()).toEqual('Your Program Journey');
|
||||||
|
expect(view.$('.program-heading-message').text().trim()
|
||||||
|
.replace(/\s+/g, ' ')).toEqual(
|
||||||
|
'Track and plan your progress through the 3 courses in this program. ' +
|
||||||
|
'To complete the program, you must earn a verified certificate for each course.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the program heading congratulations message if all courses completed', () => {
|
||||||
|
view = initView({
|
||||||
|
// Remove remaining courses so all courses are complete
|
||||||
|
courseData: $.extend({}, options.courseData, {
|
||||||
|
in_progress: [],
|
||||||
|
not_started: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
|
||||||
|
expect(view.$('.program-heading-title').text()).toEqual('Congratulations!');
|
||||||
|
expect(view.$('.program-heading-message').text().trim()
|
||||||
|
.replace(/\s+/g, ' ')).toEqual(
|
||||||
|
'You have successfully completed all the requirements for the Test Course Title Test.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the course list headings', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect(view.$('.course-list-heading .status').text()).toEqual(
|
||||||
|
'COURSES IN PROGRESSREMAINING COURSESCOMPLETED COURSES',
|
||||||
|
);
|
||||||
|
expect(view.$('.course-list-heading .count').text()).toEqual('111');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the basic course card information', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect($(view.$('.course-title')[0]).text().trim()).toEqual('Star Trek: The Next Generation');
|
||||||
|
expect($(view.$('.enrolled')[0]).text().trim()).toEqual('Enrolled:');
|
||||||
|
expect($(view.$('.run-period')[0]).text().trim()).toEqual('Mar 20, 2017 - Mar 31, 2017');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render certificate information', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect($(view.$('.upgrade-message .card-msg')).text().trim()).toEqual('Certificate Status:');
|
||||||
|
expect($(view.$('.upgrade-message .price')).text().trim()).toEqual('$10.00');
|
||||||
|
expect($(view.$('.upgrade-button.single-course-run')[0]).text().trim()).toEqual('Upgrade to Verified');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render full program purchase link', () => {
|
||||||
|
view = initView({
|
||||||
|
programData: $.extend({}, options.programData, {
|
||||||
|
is_learner_eligible_for_one_click_purchase: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
expect($(view.$('.upgrade-button.complete-program')).text().trim()
|
||||||
|
.replace(/\s+/g, ' '))
|
||||||
|
.toEqual(
|
||||||
|
'Upgrade All Remaining Courses ( $300.00 USD )',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render partial program purchase link', () => {
|
||||||
|
view = initView({
|
||||||
|
programData: $.extend({}, options.programData, {
|
||||||
|
is_learner_eligible_for_one_click_purchase: true,
|
||||||
|
discount_data: {
|
||||||
|
currency: 'USD',
|
||||||
|
discount_value: 30,
|
||||||
|
is_discounted: true,
|
||||||
|
total_incl_tax: 300,
|
||||||
|
total_incl_tax_excl_discounts: 270,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
expect($(view.$('.upgrade-button.complete-program')).text().trim()
|
||||||
|
.replace(/\s+/g, ' '))
|
||||||
|
.toEqual(
|
||||||
|
'Upgrade All Remaining Courses ( $270.00 $300.00 USD )',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render enrollment information', () => {
|
||||||
|
view = initView();
|
||||||
|
view.render();
|
||||||
|
expect(view.$('.run-select')[0].options.length).toEqual(2);
|
||||||
|
expect($(view.$('.select-choice')[0]).attr('for')).toEqual($(view.$('.run-select')[0]).attr('id'));
|
||||||
|
expect($(view.$('.enroll-button button')[0]).text().trim()).toEqual('Enroll Now');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send analytic event when purchase button clicked', () => {
|
||||||
|
const properties = {
|
||||||
|
category: 'partial bundle',
|
||||||
|
label: 'Test Course Title',
|
||||||
|
uuid: '0ffff5d6-0177-4690-9a48-aa2fecf94610',
|
||||||
|
};
|
||||||
|
view = initView({
|
||||||
|
programData: $.extend({}, options.programData, {
|
||||||
|
is_learner_eligible_for_one_click_purchase: true,
|
||||||
|
variant: 'partial',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
$('.complete-program').click();
|
||||||
|
// Verify that analytics event fires when the purchase button is clicked.
|
||||||
|
expect(window.analytics.track).toHaveBeenCalledWith(
|
||||||
|
'edx.bi.user.dashboard.program.purchase',
|
||||||
|
properties,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
|
import SpecHelpers from 'edx-ui-toolkit/js/utils/spec-helpers/spec-helpers';
|
||||||
|
|
||||||
|
import ProgressCircleView from '../views/progress_circle_view';
|
||||||
|
|
||||||
|
describe('Progress Circle View', () => {
|
||||||
|
let view = null;
|
||||||
|
const context = {
|
||||||
|
title: 'XSeries Progress',
|
||||||
|
label: 'Earned Certificates',
|
||||||
|
progress: {
|
||||||
|
completed: 2,
|
||||||
|
in_progress: 1,
|
||||||
|
not_started: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCircle = (progress) => {
|
||||||
|
const $circle = view.$('.progress-circle');
|
||||||
|
|
||||||
|
expect($circle.find('.complete').length).toEqual(progress.completed);
|
||||||
|
expect($circle.find('.incomplete').length).toEqual(progress.in_progress + progress.not_started);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testText = (progress) => {
|
||||||
|
const $numbers = view.$('.numbers');
|
||||||
|
const 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgress = (x, y, z) => ({
|
||||||
|
completed: x,
|
||||||
|
in_progress: y,
|
||||||
|
not_started: z,
|
||||||
|
});
|
||||||
|
|
||||||
|
const initView = (progress) => {
|
||||||
|
const data = $.extend({}, context, {
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ProgressCircleView({
|
||||||
|
el: '.js-program-progress',
|
||||||
|
model: new Backbone.Model(data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testProgress = (x, y, z) => {
|
||||||
|
const progress = getProgress(x, y, z);
|
||||||
|
|
||||||
|
view = initView(progress);
|
||||||
|
view.render();
|
||||||
|
|
||||||
|
testCircle(progress);
|
||||||
|
testText(progress);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div class="js-program-progress"></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
const 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', () => {
|
||||||
|
const progress = getProgress(2, 1, 3);
|
||||||
|
|
||||||
|
view = initView(progress);
|
||||||
|
view.render();
|
||||||
|
testCircle(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the progress text based on the passed in model', () => {
|
||||||
|
const 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);
|
||||||
|
});
|
||||||
46
lms/static/js/learner_dashboard/spec/sidebar_view_spec.js
Normal file
46
lms/static/js/learner_dashboard/spec/sidebar_view_spec.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import SidebarView from '../views/sidebar_view';
|
||||||
|
|
||||||
|
describe('Sidebar View', () => {
|
||||||
|
let view = null;
|
||||||
|
const context = {
|
||||||
|
marketingUrl: 'https://www.example.org/programs',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div class="sidebar"></div>');
|
||||||
|
|
||||||
|
view = new SidebarView({
|
||||||
|
el: '.sidebar',
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the exploration panel given a marketing URL', () => {
|
||||||
|
const $sidebar = view.$el;
|
||||||
|
expect($sidebar.find('.program-advertise .advertise-message').html().trim())
|
||||||
|
.toEqual('Browse recently launched courses and see what\'s new in your favorite subjects');
|
||||||
|
expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.marketingUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load the advertising panel if no marketing URL is provided', () => {
|
||||||
|
view.remove();
|
||||||
|
view = new SidebarView({
|
||||||
|
el: '.sidebar',
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
view.render();
|
||||||
|
const $ad = view.$el.find('.program-advertise');
|
||||||
|
expect($ad.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
lms/static/js/learner_dashboard/spec/unenroll_view_spec.js
Normal file
39
lms/static/js/learner_dashboard/spec/unenroll_view_spec.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import UnenrollView from '../views/unenroll_view';
|
||||||
|
|
||||||
|
describe('Unenroll View', () => {
|
||||||
|
let view = null;
|
||||||
|
const options = {
|
||||||
|
urls: {
|
||||||
|
dashboard: '/dashboard',
|
||||||
|
browseCourses: '/courses',
|
||||||
|
},
|
||||||
|
isEdx: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initView = () => new UnenrollView(options);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div class="unenroll-modal"><div class="wrapper-action-more" data-course-key="course-v1:edX+DemoX+Demo_Course"> <button type="button" class="action action-more" id="actions-dropdown-link-0" aria-haspopup="true" aria-expanded="true" aria-controls="actions-dropdown-0" data-course-number="DemoX" data-course-name="edX Demonstration Course" data-dashboard-index="0"> <span class="sr">Course options for</span> <span class="sr"> edX Demonstration Course </span> <span class="fa fa-cog" aria-hidden="true"></span> </button> <div class="actions-dropdown is-visible" id="actions-dropdown-0" tabindex="-1"> <ul class="actions-dropdown-list" id="actions-dropdown-list-0" aria-label="Available Actions" role="menu"> <div class="reasons_survey"> <div class="slide1 hidden"> <h3>We\'re sorry to see you go! Please share your main reason for unenrolling.</h3><br> <ul class="options"> <li><label class="option"><input type="radio" name="reason" val="I don\'t have enough support">I don\'t have enough support</label></li><li><label class="option"><input type="radio" name="reason" val="I don’t have the academic or language prerequisites">I don\'t have the academic or language prerequisites</label></li><li><label class="option"><input type="radio" name="reason" val="Something was broken">Something was broken</label></li><li><label class="option"><input type="radio" name="reason" val="I just wanted to browse the material">I just wanted to browse the material</label></li><li><label class="option"><input type="radio" name="reason" val="This won’t help me reach my goals">This won\'t help me reach my goals</label></li><li><label class="option"><input type="radio" name="reason" val="I am not happy with the quality of the content">I am not happy with the quality of the content</label></li><li><label class="option"><input type="radio" name="reason" val="The course material was too hard">The course material was too hard</label></li><li><label class="option"><input type="radio" name="reason" val="I don\'t have the time">I don\'t have the time</label></li><li><label class="option"><input type="radio" name="reason" val="The course material was too easy">The course material was too easy</label></li><li><label class="option"><input class="other_radio" type="radio" name="reason" val="Other">Other <input type="text" class="other_text"></label></li></ul> <button class="submit_reasons">Submit</button> </div> </div> <div class="slide2 hidden"> Thank you for sharing your reasons for unenrolling.<br> You are unenrolled from edX Demonstration Course. <a class="button survey_button return_to_dashboard"> Return To Dashboard </a> <a class="button survey_button browse_courses"> Browse Courses </a> </div> <li class="actions-item" id="actions-item-unenroll-0"> <a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="course-v1:edX+DemoX+Demo_Course" data-course-number="DemoX" data-course-name="edX Demonstration Course" data-dashboard-index="0" data-track-info="Are you sure you want to unenroll from %(course_name)s (%(course_number)s)?" id="unenroll-0"> Unenroll </a> </li> <li class="actions-item" id="actions-item-email-settings-0"> </li> </ul> </div> </div></div>'); // eslint-disable-line max-len
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
view.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist', () => {
|
||||||
|
view = initView();
|
||||||
|
expect(view).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switch between slides', () => {
|
||||||
|
view = initView();
|
||||||
|
expect($('.slide1').hasClass('hidden')).toEqual(true);
|
||||||
|
view.switchToSlideOne();
|
||||||
|
expect($('.slide1').hasClass('hidden')).toEqual(false);
|
||||||
|
expect($('.slide2').hasClass('hidden')).toEqual(true);
|
||||||
|
view.switchToSlideTwo();
|
||||||
|
expect($('.slide2').hasClass('hidden')).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
(function(define) {
|
import UnenrollView from './views/unenroll_view';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
function UnenrollmentFactory(options) {
|
||||||
'js/learner_dashboard/views/unenroll_view'
|
return new UnenrollView(options);
|
||||||
],
|
}
|
||||||
function(UnenrollView) {
|
|
||||||
return function(options) {
|
export { UnenrollmentFactory }; // eslint-disable-line import/prefer-default-export
|
||||||
var Unenroll = new UnenrollView(options);
|
|
||||||
return Unenroll;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
(function(define) {
|
import _ from 'underscore';
|
||||||
'use strict';
|
import Backbone from 'backbone';
|
||||||
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) {
|
import certificateTpl from '../../../templates/learner_dashboard/certificate_list.underscore';
|
||||||
this.title = options.title || false;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
class CertificateListView extends Backbone.View {
|
||||||
var data = {
|
initialize(options) {
|
||||||
title: this.title,
|
this.tpl = _.template(certificateTpl);
|
||||||
certificateList: this.collection.toJSON()
|
this.title = options.title || false;
|
||||||
};
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
this.$el.html(this.tpl(data));
|
render() {
|
||||||
}
|
const data = {
|
||||||
});
|
title: this.title,
|
||||||
}
|
certificateList: this.collection.toJSON(),
|
||||||
);
|
};
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
this.$el.html(this.tpl(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CertificateListView;
|
||||||
|
|||||||
@@ -1,38 +1,24 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
define(['backbone',
|
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'text!../../../templates/learner_dashboard/certificate_status.underscore',
|
|
||||||
'text!../../../templates/learner_dashboard/certificate_icon.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
HtmlUtils,
|
|
||||||
certificateStatusTpl,
|
|
||||||
certificateIconTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
statusTpl: HtmlUtils.template(certificateStatusTpl),
|
|
||||||
iconTpl: HtmlUtils.template(certificateIconTpl),
|
|
||||||
|
|
||||||
initialize: function(options) {
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
this.$el = options.$el;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
import certificateStatusTpl from '../../../templates/learner_dashboard/certificate_status.underscore';
|
||||||
var data = this.model.toJSON();
|
import certificateIconTpl from '../../../templates/learner_dashboard/certificate_icon.underscore';
|
||||||
|
|
||||||
data = $.extend(data, {certificateSvg: this.iconTpl()});
|
class CertificateStatusView extends Backbone.View {
|
||||||
HtmlUtils.setHtml(this.$el, this.statusTpl(data));
|
initialize(options) {
|
||||||
}
|
this.statusTpl = HtmlUtils.template(certificateStatusTpl);
|
||||||
});
|
this.iconTpl = HtmlUtils.template(certificateIconTpl);
|
||||||
}
|
this.$el = options.$el;
|
||||||
);
|
this.render();
|
||||||
}).call(this, define || RequireJS.define);
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let data = this.model.toJSON();
|
||||||
|
|
||||||
|
data = $.extend(data, { certificateSvg: this.iconTpl() });
|
||||||
|
HtmlUtils.setHtml(this.$el, this.statusTpl(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CertificateStatusView;
|
||||||
|
|||||||
@@ -1,68 +1,53 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define(['backbone',
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
'jquery',
|
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/string-utils',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'text!../../../templates/learner_dashboard/empty_programs_list.underscore'
|
|
||||||
],
|
|
||||||
function(Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
StringUtils,
|
|
||||||
HtmlUtils,
|
|
||||||
emptyProgramsListTpl) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
|
|
||||||
initialize: function(data) {
|
import emptyProgramsListTpl from '../../../templates/learner_dashboard/empty_programs_list.underscore';
|
||||||
this.childView = data.childView;
|
|
||||||
this.context = data.context;
|
|
||||||
this.titleContext = data.titleContext;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
class CollectionListView extends Backbone.View {
|
||||||
var childList;
|
initialize(data) {
|
||||||
|
this.childView = data.childView;
|
||||||
|
this.context = data.context;
|
||||||
|
this.titleContext = data.titleContext;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.collection.length) {
|
render() {
|
||||||
if (this.context.marketingUrl) {
|
if (!this.collection.length) {
|
||||||
// Only show the advertising panel if the link is passed in
|
if (this.context.marketingUrl) {
|
||||||
HtmlUtils.setHtml(this.$el, HtmlUtils.template(emptyProgramsListTpl)(this.context));
|
// Only show the advertising panel if the link is passed in
|
||||||
}
|
HtmlUtils.setHtml(this.$el, HtmlUtils.template(emptyProgramsListTpl)(this.context));
|
||||||
} else {
|
}
|
||||||
childList = [];
|
} else {
|
||||||
|
const childList = [];
|
||||||
|
|
||||||
this.collection.each(function(model) {
|
this.collection.each((model) => {
|
||||||
var child = new this.childView({
|
const child = new this.childView({ // eslint-disable-line new-cap
|
||||||
model: model,
|
model,
|
||||||
context: this.context
|
context: this.context,
|
||||||
});
|
});
|
||||||
childList.push(child.el);
|
childList.push(child.el);
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
if (this.titleContext) {
|
if (this.titleContext) {
|
||||||
this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString());
|
this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$el.html(childList);
|
this.$el.html(childList);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
getTitleHtml: function() {
|
getTitleHtml() {
|
||||||
var titleHtml = HtmlUtils.joinHtml(
|
const titleHtml = HtmlUtils.joinHtml(
|
||||||
HtmlUtils.HTML('<'),
|
HtmlUtils.HTML('<'),
|
||||||
this.titleContext.el,
|
this.titleContext.el,
|
||||||
HtmlUtils.HTML(' class="sr-only collection-title">'),
|
HtmlUtils.HTML(' class="sr-only collection-title">'),
|
||||||
StringUtils.interpolate(this.titleContext.title),
|
StringUtils.interpolate(this.titleContext.title),
|
||||||
HtmlUtils.HTML('</'),
|
HtmlUtils.HTML('</'),
|
||||||
this.titleContext.el,
|
this.titleContext.el,
|
||||||
HtmlUtils.HTML('>'));
|
HtmlUtils.HTML('>'));
|
||||||
return titleHtml;
|
return titleHtml;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
);
|
export default CollectionListView;
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,130 +1,116 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define(['backbone',
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'js/learner_dashboard/models/course_enroll_model',
|
|
||||||
'js/learner_dashboard/views/upgrade_message_view',
|
|
||||||
'js/learner_dashboard/views/certificate_status_view',
|
|
||||||
'js/learner_dashboard/views/expired_notification_view',
|
|
||||||
'js/learner_dashboard/views/course_enroll_view',
|
|
||||||
'js/learner_dashboard/views/course_entitlement_view',
|
|
||||||
'text!../../../templates/learner_dashboard/course_card.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
HtmlUtils,
|
|
||||||
EnrollModel,
|
|
||||||
UpgradeMessageView,
|
|
||||||
CertificateStatusView,
|
|
||||||
ExpiredNotificationView,
|
|
||||||
CourseEnrollView,
|
|
||||||
EntitlementView,
|
|
||||||
pageTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
className: 'program-course-card',
|
|
||||||
|
|
||||||
tpl: HtmlUtils.template(pageTpl),
|
import EnrollModel from '../models/course_enroll_model';
|
||||||
|
import UpgradeMessageView from './upgrade_message_view';
|
||||||
|
import CertificateStatusView from './certificate_status_view';
|
||||||
|
import ExpiredNotificationView from './expired_notification_view';
|
||||||
|
import CourseEnrollView from './course_enroll_view';
|
||||||
|
import EntitlementView from './course_entitlement_view';
|
||||||
|
|
||||||
initialize: function(options) {
|
import pageTpl from '../../../templates/learner_dashboard/course_card.underscore';
|
||||||
this.enrollModel = new EnrollModel();
|
|
||||||
if (options.context) {
|
|
||||||
this.urlModel = new Backbone.Model(options.context.urls);
|
|
||||||
this.enrollModel.urlRoot = this.urlModel.get('commerce_api_url');
|
|
||||||
}
|
|
||||||
this.context = options.context || {};
|
|
||||||
this.grade = this.context.courseData.grades[this.model.get('course_run_key')];
|
|
||||||
this.grade = this.grade * 100;
|
|
||||||
this.collectionCourseStatus = this.context.collectionCourseStatus || '';
|
|
||||||
this.entitlement = this.model.get('user_entitlement');
|
|
||||||
|
|
||||||
this.render();
|
class CourseCardView extends Backbone.View {
|
||||||
this.listenTo(this.model, 'change', this.render);
|
constructor(options) {
|
||||||
},
|
const defaults = {
|
||||||
|
className: 'program-course-card',
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
render: function() {
|
initialize(options) {
|
||||||
var data = $.extend(this.model.toJSON(), {
|
this.tpl = HtmlUtils.template(pageTpl);
|
||||||
enrolled: this.context.enrolled || ''
|
this.enrollModel = new EnrollModel();
|
||||||
});
|
if (options.context) {
|
||||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
this.urlModel = new Backbone.Model(options.context.urls);
|
||||||
this.postRender();
|
this.enrollModel.urlRoot = this.urlModel.get('commerce_api_url');
|
||||||
},
|
}
|
||||||
|
this.context = options.context || {};
|
||||||
|
this.grade = this.context.courseData.grades[this.model.get('course_run_key')];
|
||||||
|
this.grade = this.grade * 100;
|
||||||
|
this.collectionCourseStatus = this.context.collectionCourseStatus || '';
|
||||||
|
this.entitlement = this.model.get('user_entitlement');
|
||||||
|
|
||||||
postRender: function() {
|
this.render();
|
||||||
var $upgradeMessage = this.$('.upgrade-message'),
|
this.listenTo(this.model, 'change', this.render);
|
||||||
$certStatus = this.$('.certificate-status'),
|
}
|
||||||
$expiredNotification = this.$('.expired-notification'),
|
|
||||||
expired = this.model.get('expired'),
|
|
||||||
courseUUID = this.model.get('uuid'),
|
|
||||||
containerSelector = '#course-' + courseUUID;
|
|
||||||
|
|
||||||
this.enrollView = new CourseEnrollView({
|
render() {
|
||||||
$parentEl: this.$('.course-actions'),
|
const data = $.extend(this.model.toJSON(), {
|
||||||
model: this.model,
|
enrolled: this.context.enrolled || '',
|
||||||
grade: this.grade,
|
});
|
||||||
collectionCourseStatus: this.collectionCourseStatus,
|
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||||
urlModel: this.urlModel,
|
this.postRender();
|
||||||
enrollModel: this.enrollModel
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (this.entitlement) {
|
postRender() {
|
||||||
this.sessionSelectionView = new EntitlementView({
|
const $upgradeMessage = this.$('.upgrade-message');
|
||||||
el: this.$(containerSelector + ' .course-entitlement-selection-container'),
|
const $certStatus = this.$('.certificate-status');
|
||||||
$parentEl: this.$el,
|
const $expiredNotification = this.$('.expired-notification');
|
||||||
courseCardModel: this.model,
|
const expired = this.model.get('expired');
|
||||||
enrollModel: this.enrollModel,
|
const courseUUID = this.model.get('uuid');
|
||||||
triggerOpenBtn: '.course-details .change-session',
|
const containerSelector = `#course-${courseUUID}`;
|
||||||
courseCardMessages: '',
|
|
||||||
courseImageLink: '',
|
this.enrollView = new CourseEnrollView({
|
||||||
courseTitleLink: containerSelector + ' .course-details .course-title',
|
$parentEl: this.$('.course-actions'),
|
||||||
dateDisplayField: containerSelector + ' .course-details .course-text',
|
model: this.model,
|
||||||
enterCourseBtn: containerSelector + ' .view-course-button',
|
grade: this.grade,
|
||||||
availableSessions: JSON.stringify(this.model.get('course_runs')),
|
collectionCourseStatus: this.collectionCourseStatus,
|
||||||
entitlementUUID: this.entitlement.uuid,
|
urlModel: this.urlModel,
|
||||||
currentSessionId: this.model.isEnrolledInSession() ?
|
enrollModel: this.enrollModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.entitlement) {
|
||||||
|
this.sessionSelectionView = new EntitlementView({
|
||||||
|
el: this.$(`${containerSelector} .course-entitlement-selection-container`),
|
||||||
|
$parentEl: this.$el,
|
||||||
|
courseCardModel: this.model,
|
||||||
|
enrollModel: this.enrollModel,
|
||||||
|
triggerOpenBtn: '.course-details .change-session',
|
||||||
|
courseCardMessages: '',
|
||||||
|
courseImageLink: '',
|
||||||
|
courseTitleLink: `${containerSelector} .course-details .course-title`,
|
||||||
|
dateDisplayField: `${containerSelector} .course-details .course-text`,
|
||||||
|
enterCourseBtn: `${containerSelector} .view-course-button`,
|
||||||
|
availableSessions: JSON.stringify(this.model.get('course_runs')),
|
||||||
|
entitlementUUID: this.entitlement.uuid,
|
||||||
|
currentSessionId: this.model.isEnrolledInSession() ?
|
||||||
this.model.get('course_run_key') : null,
|
this.model.get('course_run_key') : null,
|
||||||
enrollUrl: this.model.get('enroll_url'),
|
enrollUrl: this.model.get('enroll_url'),
|
||||||
courseHomeUrl: this.model.get('course_url'),
|
courseHomeUrl: this.model.get('course_url'),
|
||||||
expiredAt: this.entitlement.expired_at,
|
expiredAt: this.entitlement.expired_at,
|
||||||
daysUntilExpiration: this.entitlement.days_until_expiration
|
daysUntilExpiration: this.entitlement.days_until_expiration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.model.get('upgrade_url') && !(expired === true)) {
|
if (this.model.get('upgrade_url') && !(expired === true)) {
|
||||||
this.upgradeMessage = new UpgradeMessageView({
|
this.upgradeMessage = new UpgradeMessageView({
|
||||||
$el: $upgradeMessage,
|
$el: $upgradeMessage,
|
||||||
model: this.model
|
model: this.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
$certStatus.remove();
|
$certStatus.remove();
|
||||||
} else if (this.model.get('certificate_url') && !(expired === true)) {
|
} else if (this.model.get('certificate_url') && !(expired === true)) {
|
||||||
this.certificateStatus = new CertificateStatusView({
|
this.certificateStatus = new CertificateStatusView({
|
||||||
$el: $certStatus,
|
$el: $certStatus,
|
||||||
model: this.model
|
model: this.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
$upgradeMessage.remove();
|
$upgradeMessage.remove();
|
||||||
} else {
|
} else {
|
||||||
// Styles are applied to these elements which will be visible if they're empty.
|
// Styles are applied to these elements which will be visible if they're empty.
|
||||||
$upgradeMessage.remove();
|
$upgradeMessage.remove();
|
||||||
$certStatus.remove();
|
$certStatus.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expired) {
|
if (expired) {
|
||||||
this.expiredNotification = new ExpiredNotificationView({
|
this.expiredNotification = new ExpiredNotificationView({
|
||||||
$el: $expiredNotification,
|
$el: $expiredNotification,
|
||||||
model: this.model
|
model: this.model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
);
|
export default CourseCardView;
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,120 +1,111 @@
|
|||||||
(function(define) {
|
import _ from 'underscore';
|
||||||
'use strict';
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
define(['backbone',
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'text!../../../templates/learner_dashboard/course_enroll.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
HtmlUtils,
|
|
||||||
pageTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
className: 'course-enroll-view',
|
|
||||||
|
|
||||||
tpl: HtmlUtils.template(pageTpl),
|
import pageTpl from '../../../templates/learner_dashboard/course_enroll.underscore';
|
||||||
|
|
||||||
events: {
|
class CourseEnrollView extends Backbone.View {
|
||||||
'click .enroll-button': 'handleEnroll',
|
constructor(options) {
|
||||||
'change .run-select': 'updateEnrollUrl'
|
const defaults = {
|
||||||
},
|
className: 'course-enroll-view',
|
||||||
|
events: {
|
||||||
|
'click .enroll-button': 'handleEnroll',
|
||||||
|
'change .run-select': 'updateEnrollUrl',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
initialize: function(options) {
|
initialize(options) {
|
||||||
this.$parentEl = options.$parentEl;
|
this.tpl = HtmlUtils.template(pageTpl);
|
||||||
this.enrollModel = options.enrollModel;
|
this.$parentEl = options.$parentEl;
|
||||||
this.urlModel = options.urlModel;
|
this.enrollModel = options.enrollModel;
|
||||||
this.grade = options.grade;
|
this.urlModel = options.urlModel;
|
||||||
this.collectionCourseStatus = options.collectionCourseStatus;
|
this.grade = options.grade;
|
||||||
this.render();
|
this.collectionCourseStatus = options.collectionCourseStatus;
|
||||||
},
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
var filledTemplate,
|
let filledTemplate;
|
||||||
context = this.model.toJSON();
|
const context = this.model.toJSON();
|
||||||
if (this.$parentEl && this.enrollModel) {
|
if (this.$parentEl && this.enrollModel) {
|
||||||
context.grade = this.grade;
|
context.grade = this.grade;
|
||||||
context.collectionCourseStatus = this.collectionCourseStatus;
|
context.collectionCourseStatus = this.collectionCourseStatus;
|
||||||
filledTemplate = this.tpl(context);
|
filledTemplate = this.tpl(context);
|
||||||
HtmlUtils.setHtml(this.$el, filledTemplate);
|
HtmlUtils.setHtml(this.$el, filledTemplate);
|
||||||
HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el));
|
HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el));
|
||||||
}
|
}
|
||||||
this.postRender();
|
this.postRender();
|
||||||
},
|
}
|
||||||
|
|
||||||
postRender: function() {
|
postRender() {
|
||||||
if (this.urlModel) {
|
if (this.urlModel) {
|
||||||
this.trackSelectionUrl = this.urlModel.get('track_selection_url');
|
this.trackSelectionUrl = this.urlModel.get('track_selection_url');
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
handleEnroll: function() {
|
handleEnroll() {
|
||||||
// Enrollment click event handled here
|
// Enrollment click event handled here
|
||||||
if (this.model.get('is_mobile_only') !== true) {
|
if (this.model.get('is_mobile_only') !== true) {
|
||||||
var courseRunKey = $('.run-select').val() || this.model.get('course_run_key'); // eslint-disable-line vars-on-top, max-len
|
const courseRunKey = $('.run-select').val() || this.model.get('course_run_key');
|
||||||
this.model.updateCourseRun(courseRunKey);
|
this.model.updateCourseRun(courseRunKey);
|
||||||
if (this.model.get('is_enrolled')) {
|
if (this.model.get('is_enrolled')) {
|
||||||
// Create the enrollment.
|
// Create the enrollment.
|
||||||
this.enrollModel.save({
|
this.enrollModel.save({
|
||||||
course_id: courseRunKey
|
course_id: courseRunKey,
|
||||||
}, {
|
}, {
|
||||||
success: _.bind(this.enrollSuccess, this),
|
success: _.bind(this.enrollSuccess, this),
|
||||||
error: _.bind(this.enrollError, this)
|
error: _.bind(this.enrollError, this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
enrollSuccess: function() {
|
enrollSuccess() {
|
||||||
var courseRunKey = this.model.get('course_run_key');
|
const courseRunKey = this.model.get('course_run_key');
|
||||||
window.analytics.track('edx.bi.user.program-details.enrollment');
|
window.analytics.track('edx.bi.user.program-details.enrollment');
|
||||||
if (this.trackSelectionUrl) {
|
if (this.trackSelectionUrl) {
|
||||||
// Go to track selection page
|
// Go to track selection page
|
||||||
this.redirect(this.trackSelectionUrl + courseRunKey);
|
CourseEnrollView.redirect(this.trackSelectionUrl + courseRunKey);
|
||||||
} else {
|
} else {
|
||||||
this.model.set({
|
this.model.set({
|
||||||
is_enrolled: true
|
is_enrolled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
enrollError: function(model, response) {
|
enrollError(model, response) {
|
||||||
if (response.status === 403 && response.responseJSON.user_message_url) {
|
if (response.status === 403 && response.responseJSON.user_message_url) {
|
||||||
/**
|
/**
|
||||||
* Check if we've been blocked from the course
|
* Check if we've been blocked from the course
|
||||||
* because of country access rules.
|
* because of country access rules.
|
||||||
* If so, redirect to a page explaining to the user
|
* If so, redirect to a page explaining to the user
|
||||||
* why they were blocked.
|
* why they were blocked.
|
||||||
*/
|
*/
|
||||||
this.redirect(response.responseJSON.user_message_url);
|
CourseEnrollView.redirect(response.responseJSON.user_message_url);
|
||||||
} else if (this.trackSelectionUrl) {
|
} else if (this.trackSelectionUrl) {
|
||||||
/**
|
/**
|
||||||
* Otherwise, go to the track selection page as usual.
|
* Otherwise, go to the track selection page as usual.
|
||||||
* This can occur, for example, when a course does not
|
* This can occur, for example, when a course does not
|
||||||
* have a free enrollment mode, so we can't auto-enroll.
|
* have a free enrollment mode, so we can't auto-enroll.
|
||||||
*/
|
*/
|
||||||
this.redirect(this.trackSelectionUrl + this.model.get('course_run_key'));
|
CourseEnrollView.redirect(this.trackSelectionUrl + this.model.get('course_run_key'));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
updateEnrollUrl: function() {
|
updateEnrollUrl() {
|
||||||
if (this.model.get('is_mobile_only') === true) {
|
if (this.model.get('is_mobile_only') === true) {
|
||||||
var courseRunKey = $('.run-select').val(), // eslint-disable-line vars-on-top
|
const courseRunKey = $('.run-select').val();
|
||||||
href = 'edxapp://enroll?course_id=' + courseRunKey + '&email_opt_in=true';
|
const href = `edxapp://enroll?course_id=${courseRunKey}&email_opt_in=true`;
|
||||||
$('.enroll-course-button').attr('href', href);
|
$('.enroll-course-button').attr('href', href);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
redirect: function(url) {
|
static redirect(url) {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
);
|
export default CourseEnrollView;
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,419 +1,409 @@
|
|||||||
(function(define) {
|
/* globals gettext */
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define(['backbone',
|
import 'bootstrap';
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'moment',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'js/learner_dashboard/models/course_entitlement_model',
|
|
||||||
'js/learner_dashboard/models/course_card_model',
|
|
||||||
'text!../../../templates/learner_dashboard/course_entitlement.underscore',
|
|
||||||
'text!../../../templates/learner_dashboard/verification_popover.underscore',
|
|
||||||
'bootstrap'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
moment,
|
|
||||||
HtmlUtils,
|
|
||||||
EntitlementModel,
|
|
||||||
CourseCardModel,
|
|
||||||
pageTpl,
|
|
||||||
verificationPopoverTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
tpl: HtmlUtils.template(pageTpl),
|
|
||||||
verificationTpl: HtmlUtils.template(verificationPopoverTpl),
|
|
||||||
|
|
||||||
events: {
|
import _ from 'underscore';
|
||||||
'change .session-select': 'updateEnrollBtn',
|
import Backbone from 'backbone';
|
||||||
'click .enroll-btn': 'handleEnrollChange',
|
import moment from 'moment';
|
||||||
'keydown .final-confirmation-btn': 'handleVerificationPopoverA11y',
|
|
||||||
'click .popover-dismiss': 'hideDialog'
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function(options) {
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
// Set up models and reload view on change
|
|
||||||
this.courseCardModel = options.courseCardModel || new CourseCardModel();
|
|
||||||
this.enrollModel = options.enrollModel;
|
|
||||||
this.entitlementModel = new EntitlementModel({
|
|
||||||
availableSessions: this.formatDates(JSON.parse(options.availableSessions)),
|
|
||||||
entitlementUUID: options.entitlementUUID,
|
|
||||||
currentSessionId: options.currentSessionId,
|
|
||||||
expiredAt: options.expiredAt,
|
|
||||||
expiresAtDate: this.courseCardModel.formatDate(
|
|
||||||
new moment().utc().add(options.daysUntilExpiration, 'days')
|
|
||||||
),
|
|
||||||
courseName: options.courseName
|
|
||||||
});
|
|
||||||
this.listenTo(this.entitlementModel, 'change', this.render);
|
|
||||||
|
|
||||||
// Grab URLs that handle changing of enrollment and entering a newly selected session.
|
import EntitlementModel from '../models/course_entitlement_model';
|
||||||
this.enrollUrl = options.enrollUrl;
|
import CourseCardModel from '../models/course_card_model';
|
||||||
this.courseHomeUrl = options.courseHomeUrl;
|
|
||||||
|
|
||||||
// Grab elements from the parent card that work with this view
|
import pageTpl from '../../../templates/learner_dashboard/course_entitlement.underscore';
|
||||||
this.$parentEl = options.$parentEl; // Containing course card (must be a backbone view root el)
|
import verificationPopoverTpl from '../../../templates/learner_dashboard/verification_popover.underscore';
|
||||||
this.$enterCourseBtn = $(options.enterCourseBtn); // Button link to course home page
|
|
||||||
this.$courseCardMessages = $(options.courseCardMessages); // Additional session messages
|
|
||||||
this.$courseTitleLink = $(options.courseTitleLink); // Title link to course home page
|
|
||||||
this.$courseImageLink = $(options.courseImageLink); // Image link to course home page
|
|
||||||
this.$policyMsg = $(options.policyMsg); // Message for policy information
|
|
||||||
|
|
||||||
// Bind action elements with associated events to objects outside this view
|
class CourseEntitlementView extends Backbone.View {
|
||||||
this.$dateDisplayField = this.$parentEl ? this.$parentEl.find(options.dateDisplayField) :
|
constructor(options) {
|
||||||
$(options.dateDisplayField); // Displays current session dates
|
const defaults = {
|
||||||
this.$triggerOpenBtn = this.$parentEl ? this.$parentEl.find(options.triggerOpenBtn) :
|
events: {
|
||||||
$(options.triggerOpenBtn); // Opens/closes session selection view
|
'change .session-select': 'updateEnrollBtn',
|
||||||
this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this));
|
'click .enroll-btn': 'handleEnrollChange',
|
||||||
|
'keydown .final-confirmation-btn': 'handleVerificationPopoverA11y',
|
||||||
|
'click .popover-dismiss': 'hideDialog',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
this.render(options);
|
initialize(options) {
|
||||||
this.postRender();
|
this.tpl = HtmlUtils.template(pageTpl);
|
||||||
},
|
this.verificationTpl = HtmlUtils.template(verificationPopoverTpl);
|
||||||
|
|
||||||
render: function() {
|
// Set up models and reload view on change
|
||||||
HtmlUtils.setHtml(this.$el, this.tpl(this.entitlementModel.toJSON()));
|
this.courseCardModel = options.courseCardModel || new CourseCardModel();
|
||||||
this.delegateEvents();
|
this.enrollModel = options.enrollModel;
|
||||||
this.updateEnrollBtn();
|
this.entitlementModel = new EntitlementModel({
|
||||||
return this;
|
availableSessions: this.formatDates(JSON.parse(options.availableSessions)),
|
||||||
},
|
entitlementUUID: options.entitlementUUID,
|
||||||
|
currentSessionId: options.currentSessionId,
|
||||||
|
expiredAt: options.expiredAt,
|
||||||
|
expiresAtDate: CourseCardModel.formatDate(
|
||||||
|
new moment().utc().add(options.daysUntilExpiration, 'days'), // eslint-disable-line new-cap
|
||||||
|
),
|
||||||
|
courseName: options.courseName,
|
||||||
|
});
|
||||||
|
this.listenTo(this.entitlementModel, 'change', this.render);
|
||||||
|
|
||||||
postRender: function() {
|
// Grab URLs that handle changing of enrollment and entering a newly selected session.
|
||||||
// Close any visible popovers on click-away
|
this.enrollUrl = options.enrollUrl;
|
||||||
$(document).on('click', function(e) {
|
this.courseHomeUrl = options.courseHomeUrl;
|
||||||
if (this.$('.popover:visible').length &&
|
|
||||||
!($(e.target).closest('.enroll-btn-initial, .popover').length)) {
|
|
||||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
// Initialize focus to cancel button on popover load
|
// Grab elements from the parent card that work with this view
|
||||||
$(document).on('shown.bs.popover', function() {
|
this.$parentEl = options.$parentEl; // Containing course card (must be a backbone view root el)
|
||||||
this.$('.final-confirmation-btn:first').focus();
|
this.$enterCourseBtn = $(options.enterCourseBtn); // Button link to course home page
|
||||||
}.bind(this));
|
this.$courseCardMessages = $(options.courseCardMessages); // Additional session messages
|
||||||
},
|
this.$courseTitleLink = $(options.courseTitleLink); // Title link to course home page
|
||||||
|
this.$courseImageLink = $(options.courseImageLink); // Image link to course home page
|
||||||
|
this.$policyMsg = $(options.policyMsg); // Message for policy information
|
||||||
|
|
||||||
handleEnrollChange: function() {
|
// Bind action elements with associated events to objects outside this view
|
||||||
/*
|
this.$dateDisplayField = this.$parentEl ? this.$parentEl.find(options.dateDisplayField) :
|
||||||
Handles enrolling in a course, unenrolling in a session and changing session.
|
$(options.dateDisplayField); // Displays current session dates
|
||||||
The new session id is stored as a data attribute on the option in the session-select element.
|
this.$triggerOpenBtn = this.$parentEl ? this.$parentEl.find(options.triggerOpenBtn) :
|
||||||
*/
|
$(options.triggerOpenBtn); // Opens/closes session selection view
|
||||||
var isLeavingSession;
|
this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this));
|
||||||
|
|
||||||
// Do not allow for enrollment when button is disabled
|
this.render(options);
|
||||||
if (this.$('.enroll-btn-initial').hasClass('disabled')) return;
|
this.postRender();
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the id for the desired session, an leave session event will return null
|
render() {
|
||||||
this.currentSessionSelection = this.$('.session-select')
|
HtmlUtils.setHtml(this.$el, this.tpl(this.entitlementModel.toJSON()));
|
||||||
.find('option:selected').data('session_id');
|
this.delegateEvents();
|
||||||
isLeavingSession = !this.currentSessionSelection;
|
this.updateEnrollBtn();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
// Display the indicator icon
|
postRender() {
|
||||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
// Close any visible popovers on click-away
|
||||||
HtmlUtils.HTML('<span class="fa fa-spinner fa-spin" aria-hidden="true"></span>')
|
$(document).on('click', (e) => {
|
||||||
);
|
if (this.$('.popover:visible').length &&
|
||||||
|
!($(e.target).closest('.enroll-btn-initial, .popover').length)) {
|
||||||
|
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
// Initialize focus to cancel button on popover load
|
||||||
type: isLeavingSession ? 'DELETE' : 'POST',
|
$(document).on('shown.bs.popover', () => {
|
||||||
url: this.enrollUrl,
|
this.$('.final-confirmation-btn:first').focus();
|
||||||
contentType: 'application/json',
|
});
|
||||||
dataType: 'json',
|
}
|
||||||
data: JSON.stringify({
|
|
||||||
course_run_id: this.currentSessionSelection
|
|
||||||
}),
|
|
||||||
statusCode: {
|
|
||||||
201: _.bind(this.enrollSuccess, this),
|
|
||||||
204: _.bind(this.unenrollSuccess, this)
|
|
||||||
},
|
|
||||||
error: _.bind(this.enrollError, this)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
enrollSuccess: function(data) {
|
handleEnrollChange() {
|
||||||
/*
|
/*
|
||||||
Update external elements on the course card to represent the now available course session.
|
Handles enrolling in a course, unenrolling in a session and changing session.
|
||||||
|
The new session id is stored as a data attribute on the option in the session-select element.
|
||||||
|
*/
|
||||||
|
// Do not allow for enrollment when button is disabled
|
||||||
|
if (this.$('.enroll-btn-initial').hasClass('disabled')) return;
|
||||||
|
|
||||||
1) Show the change session toggle button.
|
// Grab the id for the desired session, an leave session event will return null
|
||||||
2) Add the new session's dates to the date field on the main course card.
|
this.currentSessionSelection = this.$('.session-select')
|
||||||
3) Hide the 'View Course' button to the course card.
|
.find('option:selected').data('session_id');
|
||||||
*/
|
const isLeavingSession = !this.currentSessionSelection;
|
||||||
var successIconEl = '<span class="fa fa-check" aria-hidden="true"></span>';
|
|
||||||
|
|
||||||
// With a containing backbone view, we can simply re-render the parent card
|
// Display the indicator icon
|
||||||
if (this.$parentEl) {
|
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||||
this.courseCardModel.updateCourseRun(this.currentSessionSelection);
|
HtmlUtils.HTML('<span class="fa fa-spinner fa-spin" aria-hidden="true"></span>'),
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Update the model with the new session Id
|
$.ajax({
|
||||||
this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
|
type: isLeavingSession ? 'DELETE' : 'POST',
|
||||||
|
url: this.enrollUrl,
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
course_run_id: this.currentSessionSelection,
|
||||||
|
}),
|
||||||
|
statusCode: {
|
||||||
|
201: _.bind(this.enrollSuccess, this),
|
||||||
|
204: _.bind(this.unenrollSuccess, this),
|
||||||
|
},
|
||||||
|
error: _.bind(this.enrollError, this),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Allow user to change session
|
enrollSuccess(data) {
|
||||||
this.$triggerOpenBtn.removeClass('hidden');
|
/*
|
||||||
|
Update external elements on the course card to represent the now available course session.
|
||||||
|
|
||||||
// Display a success indicator
|
1) Show the change session toggle button.
|
||||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
2) Add the new session's dates to the date field on the main course card.
|
||||||
HtmlUtils.joinHtml(
|
3) Hide the 'View Course' button to the course card.
|
||||||
HtmlUtils.HTML(successIconEl),
|
*/
|
||||||
this.getAvailableSessionWithId(data.course_run_id).session_dates
|
const successIconEl = '<span class="fa fa-check" aria-hidden="true"></span>';
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure the view course button links to new session home page and place focus there
|
// With a containing backbone view, we can simply re-render the parent card
|
||||||
this.$enterCourseBtn
|
if (this.$parentEl) {
|
||||||
.attr('href', this.formatCourseHomeUrl(data.course_run_id))
|
this.courseCardModel.updateCourseRun(this.currentSessionSelection);
|
||||||
.removeClass('hidden')
|
return;
|
||||||
.focus();
|
}
|
||||||
this.toggleSessionSelectionPanel();
|
|
||||||
},
|
|
||||||
|
|
||||||
unenrollSuccess: function() {
|
// Update the model with the new session Id
|
||||||
/*
|
this.entitlementModel.set({ currentSessionId: this.currentSessionSelection });
|
||||||
Update external elements on the course card to represent the unenrolled state.
|
|
||||||
|
|
||||||
1) Hide the change session button and the date field.
|
// Allow user to change session
|
||||||
2) Hide the 'View Course' button.
|
this.$triggerOpenBtn.removeClass('hidden');
|
||||||
3) Remove the messages associated with the enrolled state.
|
|
||||||
4) Remove the link from the course card image and title.
|
|
||||||
*/
|
|
||||||
// With a containing backbone view, we can simply re-render the parent card
|
|
||||||
if (this.$parentEl) {
|
|
||||||
this.courseCardModel.setUnselected();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the model with the new session Id;
|
// Display a success indicator
|
||||||
this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
|
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||||
|
HtmlUtils.joinHtml(
|
||||||
|
HtmlUtils.HTML(successIconEl),
|
||||||
|
this.getAvailableSessionWithId(data.course_run_id).session_dates,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Reset the card contents to the unenrolled state
|
// Ensure the view course button links to new session home page and place focus there
|
||||||
this.$triggerOpenBtn.addClass('hidden');
|
this.$enterCourseBtn
|
||||||
this.$enterCourseBtn.addClass('hidden');
|
.attr('href', this.formatCourseHomeUrl(data.course_run_id))
|
||||||
// Remove all message except for related programs, which should always be shown
|
.removeClass('hidden')
|
||||||
// (Even other messages might need to be shown again in future: LEARNER-3523.)
|
.focus();
|
||||||
this.$courseCardMessages.filter(':not(.message-related-programs)').remove();
|
this.toggleSessionSelectionPanel();
|
||||||
this.$policyMsg.remove();
|
}
|
||||||
this.$('.enroll-btn-initial').focus();
|
|
||||||
HtmlUtils.setHtml(
|
|
||||||
this.$dateDisplayField,
|
|
||||||
HtmlUtils.joinHtml(
|
|
||||||
HtmlUtils.HTML('<span class="icon fa fa-warning" aria-hidden="true"></span>'),
|
|
||||||
HtmlUtils.HTML(gettext('You must select a session to access the course.'))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove links to previously enrolled sessions
|
unenrollSuccess() {
|
||||||
this.$courseImageLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
/*
|
||||||
HtmlUtils.joinHtml(
|
Update external elements on the course card to represent the unenrolled state.
|
||||||
HtmlUtils.HTML('<div class="'),
|
|
||||||
this.$courseImageLink.attr('class'),
|
|
||||||
HtmlUtils.HTML('" tabindex="-1">'),
|
|
||||||
HtmlUtils.HTML(this.$courseImageLink.html()),
|
|
||||||
HtmlUtils.HTML('</div>')
|
|
||||||
).text
|
|
||||||
);
|
|
||||||
this.$courseTitleLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
|
||||||
HtmlUtils.joinHtml(
|
|
||||||
HtmlUtils.HTML('<span>'),
|
|
||||||
this.$courseTitleLink.text(),
|
|
||||||
HtmlUtils.HTML('</span>')
|
|
||||||
).text
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
enrollError: function() {
|
1) Hide the change session button and the date field.
|
||||||
// Display a success indicator
|
2) Hide the 'View Course' button.
|
||||||
var errorMsgEl = HtmlUtils.joinHtml(
|
3) Remove the messages associated with the enrolled state.
|
||||||
HtmlUtils.HTML('<span class="enroll-error">'),
|
4) Remove the link from the course card image and title.
|
||||||
gettext('There was an error. Please reload the page and try again.'),
|
*/
|
||||||
HtmlUtils.HTML('</spandiv>')
|
// With a containing backbone view, we can simply re-render the parent card
|
||||||
).text;
|
if (this.$parentEl) {
|
||||||
|
this.courseCardModel.setUnselected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$dateDisplayField
|
// Update the model with the new session Id;
|
||||||
.find('.fa.fa-spin')
|
this.entitlementModel.set({ currentSessionId: this.currentSessionSelection });
|
||||||
.removeClass('fa-spin fa-spinner')
|
|
||||||
.addClass('fa-close');
|
|
||||||
|
|
||||||
this.$dateDisplayField.append(errorMsgEl);
|
// Reset the card contents to the unenrolled state
|
||||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
this.$triggerOpenBtn.addClass('hidden');
|
||||||
},
|
this.$enterCourseBtn.addClass('hidden');
|
||||||
|
// Remove all message except for related programs, which should always be shown
|
||||||
|
// (Even other messages might need to be shown again in future: LEARNER-3523.)
|
||||||
|
this.$courseCardMessages.filter(':not(.message-related-programs)').remove();
|
||||||
|
this.$policyMsg.remove();
|
||||||
|
this.$('.enroll-btn-initial').focus();
|
||||||
|
HtmlUtils.setHtml(
|
||||||
|
this.$dateDisplayField,
|
||||||
|
HtmlUtils.joinHtml(
|
||||||
|
HtmlUtils.HTML('<span class="icon fa fa-warning" aria-hidden="true"></span>'),
|
||||||
|
HtmlUtils.HTML(gettext('You must select a session to access the course.')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
updateEnrollBtn: function() {
|
// Remove links to previously enrolled sessions
|
||||||
/*
|
this.$courseImageLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
||||||
This function is invoked on load, on opening the view and on changing the option on the session
|
HtmlUtils.joinHtml(
|
||||||
selection dropdown. It plays three roles:
|
HtmlUtils.HTML('<div class="'),
|
||||||
1) Enables and disables enroll button
|
this.$courseImageLink.attr('class'),
|
||||||
2) Changes text to describe the action taken
|
HtmlUtils.HTML('" tabindex="-1">'),
|
||||||
3) Formats the confirmation popover to allow for two step authentication
|
HtmlUtils.HTML(this.$courseImageLink.html()),
|
||||||
*/
|
HtmlUtils.HTML('</div>'),
|
||||||
var enrollText,
|
).text,
|
||||||
currentSessionId = this.entitlementModel.get('currentSessionId'),
|
);
|
||||||
newSessionId = this.$('.session-select').find('option:selected').data('session_id'),
|
this.$courseTitleLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
||||||
enrollBtnInitial = this.$('.enroll-btn-initial');
|
HtmlUtils.joinHtml(
|
||||||
|
HtmlUtils.HTML('<span>'),
|
||||||
|
this.$courseTitleLink.text(),
|
||||||
|
HtmlUtils.HTML('</span>'),
|
||||||
|
).text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Disable the button if the user is already enrolled in that session.
|
enrollError() {
|
||||||
if (currentSessionId === newSessionId) {
|
// Display a success indicator
|
||||||
enrollBtnInitial.addClass('disabled');
|
const errorMsgEl = HtmlUtils.joinHtml(
|
||||||
this.removeDialog(enrollBtnInitial);
|
HtmlUtils.HTML('<span class="enroll-error">'),
|
||||||
return;
|
gettext('There was an error. Please reload the page and try again.'),
|
||||||
}
|
HtmlUtils.HTML('</spandiv>'),
|
||||||
enrollBtnInitial.removeClass('disabled');
|
).text;
|
||||||
|
|
||||||
// Update button text specifying if the user is initially enrolling, changing or leaving a session.
|
this.$dateDisplayField
|
||||||
if (newSessionId) {
|
.find('.fa.fa-spin')
|
||||||
enrollText = currentSessionId ? gettext('Change Session') : gettext('Select Session');
|
.removeClass('fa-spin fa-spinner')
|
||||||
} else {
|
.addClass('fa-close');
|
||||||
enrollText = gettext('Leave Current Session');
|
|
||||||
}
|
|
||||||
enrollBtnInitial.text(enrollText);
|
|
||||||
this.initializeVerificationDialog(enrollBtnInitial);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleSessionSelectionPanel: function() {
|
this.$dateDisplayField.append(errorMsgEl);
|
||||||
/*
|
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||||
Opens and closes the session selection panel.
|
}
|
||||||
*/
|
|
||||||
this.$el.toggleClass('hidden');
|
|
||||||
if (!this.$el.hasClass('hidden')) {
|
|
||||||
// Set focus to the session selection for a11y purposes
|
|
||||||
this.$('.session-select').focus();
|
|
||||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
|
||||||
}
|
|
||||||
this.updateEnrollBtn();
|
|
||||||
},
|
|
||||||
|
|
||||||
initializeVerificationDialog: function(invokingElement) {
|
updateEnrollBtn() {
|
||||||
/*
|
/*
|
||||||
Instantiates an instance of the Bootstrap v4 dialog modal and attaches it to the passed in element.
|
This function is invoked on load, on opening the view and on changing the option on the session
|
||||||
|
selection dropdown. It plays three roles:
|
||||||
|
1) Enables and disables enroll button
|
||||||
|
2) Changes text to describe the action taken
|
||||||
|
3) Formats the confirmation popover to allow for two step authentication
|
||||||
|
*/
|
||||||
|
let enrollText;
|
||||||
|
const currentSessionId = this.entitlementModel.get('currentSessionId');
|
||||||
|
const newSessionId = this.$('.session-select').find('option:selected').data('session_id');
|
||||||
|
const enrollBtnInitial = this.$('.enroll-btn-initial');
|
||||||
|
|
||||||
This dialog acts as the second step in verifying the user's action to select, change or leave an
|
// Disable the button if the user is already enrolled in that session.
|
||||||
available course session.
|
if (currentSessionId === newSessionId) {
|
||||||
*/
|
enrollBtnInitial.addClass('disabled');
|
||||||
var confirmationMsgTitle,
|
this.removeDialog(enrollBtnInitial);
|
||||||
confirmationMsgBody,
|
return;
|
||||||
currentSessionId = this.entitlementModel.get('currentSessionId'),
|
}
|
||||||
newSessionId = this.$('.session-select').find('option:selected').data('session_id');
|
enrollBtnInitial.removeClass('disabled');
|
||||||
|
|
||||||
// Update the button popover text to enable two step authentication.
|
// Update button text specifying if the user is initially enrolling,
|
||||||
if (newSessionId) {
|
// changing or leaving a session.
|
||||||
confirmationMsgTitle = !currentSessionId ?
|
if (newSessionId) {
|
||||||
gettext('Are you sure you want to select this session?') :
|
enrollText = currentSessionId ? gettext('Change Session') : gettext('Select Session');
|
||||||
gettext('Are you sure you want to change to a different session?');
|
} else {
|
||||||
confirmationMsgBody = !currentSessionId ? '' :
|
enrollText = gettext('Leave Current Session');
|
||||||
gettext('Any course progress or grades from your current session will be lost.');
|
}
|
||||||
} else {
|
enrollBtnInitial.text(enrollText);
|
||||||
confirmationMsgTitle = gettext('Are you sure that you want to leave this session?');
|
this.initializeVerificationDialog(enrollBtnInitial);
|
||||||
confirmationMsgBody = gettext('Any course progress or grades from your current session will be lost.'); // eslint-disable-line max-len
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Re-initialize the popover
|
toggleSessionSelectionPanel() {
|
||||||
invokingElement.popover({
|
/*
|
||||||
placement: 'bottom',
|
Opens and closes the session selection panel.
|
||||||
container: this.$el,
|
*/
|
||||||
html: true,
|
this.$el.toggleClass('hidden');
|
||||||
trigger: 'click',
|
if (!this.$el.hasClass('hidden')) {
|
||||||
content: this.verificationTpl({
|
// Set focus to the session selection for a11y purposes
|
||||||
confirmationMsgTitle: confirmationMsgTitle,
|
this.$('.session-select').focus();
|
||||||
confirmationMsgBody: confirmationMsgBody
|
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||||
}).text
|
}
|
||||||
});
|
this.updateEnrollBtn();
|
||||||
},
|
}
|
||||||
|
|
||||||
removeDialog: function(el) {
|
initializeVerificationDialog(invokingElement) {
|
||||||
/* Removes the Bootstrap v4 dialog modal from the update session enrollment button. */
|
/*
|
||||||
var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
Instantiates an instance of the Bootstrap v4 dialog modal and attaches it to the
|
||||||
if (this.$('popover').length) {
|
passed in element.
|
||||||
$el.popover('dispose');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hideDialog: function(el, returnFocus) {
|
This dialog acts as the second step in verifying the user's action to select, change
|
||||||
/* Hides the modal if it is visible without removing it from the DOM. */
|
or leave an available course session.
|
||||||
var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
*/
|
||||||
if (this.$('.popover:visible').length) {
|
let confirmationMsgTitle;
|
||||||
$el.popover('hide');
|
let confirmationMsgBody;
|
||||||
if (returnFocus) {
|
const currentSessionId = this.entitlementModel.get('currentSessionId');
|
||||||
$el.focus();
|
const newSessionId = this.$('.session-select').find('option:selected').data('session_id');
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleVerificationPopoverA11y: function(e) {
|
// Update the button popover text to enable two step authentication.
|
||||||
/* Ensure that the second step verification popover is treated as an a11y compliant dialog */
|
if (newSessionId) {
|
||||||
var $nextButton,
|
confirmationMsgTitle = !currentSessionId ?
|
||||||
$verificationOption = $(e.target),
|
gettext('Are you sure you want to select this session?') :
|
||||||
openButton = $(e.target).closest('.course-entitlement-selection-container')
|
gettext('Are you sure you want to change to a different session?');
|
||||||
.find('.enroll-btn-initial');
|
confirmationMsgBody = !currentSessionId ? '' :
|
||||||
if (e.key === 'Tab') {
|
gettext('Any course progress or grades from your current session will be lost.');
|
||||||
e.preventDefault();
|
} else {
|
||||||
$nextButton = $verificationOption.is(':first-child') ?
|
confirmationMsgTitle = gettext('Are you sure that you want to leave this session?');
|
||||||
|
confirmationMsgBody = gettext('Any course progress or grades from your current session will be lost.'); // eslint-disable-line max-len
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-initialize the popover
|
||||||
|
invokingElement.popover({
|
||||||
|
placement: 'bottom',
|
||||||
|
container: this.$el,
|
||||||
|
html: true,
|
||||||
|
trigger: 'click',
|
||||||
|
content: this.verificationTpl({
|
||||||
|
confirmationMsgTitle,
|
||||||
|
confirmationMsgBody,
|
||||||
|
}).text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDialog(el) {
|
||||||
|
/* Removes the Bootstrap v4 dialog modal from the update session enrollment button. */
|
||||||
|
const $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
||||||
|
if (this.$('popover').length) {
|
||||||
|
$el.popover('dispose');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDialog(el, returnFocus) {
|
||||||
|
/* Hides the modal if it is visible without removing it from the DOM. */
|
||||||
|
const $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
||||||
|
if (this.$('.popover:visible').length) {
|
||||||
|
$el.popover('hide');
|
||||||
|
if (returnFocus) {
|
||||||
|
$el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVerificationPopoverA11y(e) {
|
||||||
|
/* Ensure that the second step verification popover is treated as an a11y compliant dialog */
|
||||||
|
let $nextButton;
|
||||||
|
const $verificationOption = $(e.target);
|
||||||
|
const openButton = $(e.target).closest('.course-entitlement-selection-container')
|
||||||
|
.find('.enroll-btn-initial');
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
$nextButton = $verificationOption.is(':first-child') ?
|
||||||
$verificationOption.next('.final-confirmation-btn') :
|
$verificationOption.next('.final-confirmation-btn') :
|
||||||
$verificationOption.prev('.final-confirmation-btn');
|
$verificationOption.prev('.final-confirmation-btn');
|
||||||
$nextButton.focus();
|
$nextButton.focus();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
this.hideDialog(openButton);
|
this.hideDialog(openButton);
|
||||||
openButton.focus();
|
openButton.focus();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
formatCourseHomeUrl: function(sessionKey) {
|
formatCourseHomeUrl(sessionKey) {
|
||||||
/*
|
/*
|
||||||
Takes the base course home URL and updates it with the new session id, leveraging the
|
Takes the base course home URL and updates it with the new session id, leveraging the
|
||||||
the fact that all course keys contain a '+' symbol.
|
the fact that all course keys contain a '+' symbol.
|
||||||
*/
|
*/
|
||||||
var oldSessionKey = this.courseHomeUrl.split('/')
|
const oldSessionKey = this.courseHomeUrl.split('/')
|
||||||
.filter(
|
.filter(
|
||||||
function(urlParam) {
|
urlParam => urlParam.indexOf('+') > 0,
|
||||||
return urlParam.indexOf('+') > 0;
|
|
||||||
}
|
|
||||||
)[0];
|
)[0];
|
||||||
return this.courseHomeUrl.replace(oldSessionKey, sessionKey);
|
return this.courseHomeUrl.replace(oldSessionKey, sessionKey);
|
||||||
},
|
}
|
||||||
|
|
||||||
formatDates: function(sessionData) {
|
formatDates(sessionData) {
|
||||||
/*
|
/*
|
||||||
Takes a data object containing the upcoming available sessions for an entitlement and returns
|
Takes a data object containing the upcoming available sessions for an entitlement and returns
|
||||||
the object with a session_dates attribute representing a formatted date string that highlights
|
the object with a session_dates attribute representing a formatted date string that highlights
|
||||||
the start and end dates of the particular session.
|
the start and end dates of the particular session.
|
||||||
*/
|
*/
|
||||||
var formattedSessionData = sessionData,
|
const formattedSessionData = sessionData;
|
||||||
startDate,
|
let startDate;
|
||||||
endDate,
|
let endDate;
|
||||||
dateFormat;
|
// Set the date format string to the user's selected language
|
||||||
// Set the date format string to the user's selected language
|
moment.locale(document.documentElement.lang);
|
||||||
moment.locale(document.documentElement.lang);
|
const dateFormat = moment.localeData().longDateFormat('L').indexOf('DD') >
|
||||||
dateFormat = moment.localeData().longDateFormat('L').indexOf('DD') >
|
moment.localeData().longDateFormat('L').indexOf('MM') ? 'MMMM D, YYYY' : 'D MMMM, YYYY';
|
||||||
moment.localeData().longDateFormat('L').indexOf('MM') ? 'MMMM D, YYYY' : 'D MMMM, YYYY';
|
|
||||||
|
|
||||||
return _.map(formattedSessionData, function(session) {
|
return _.map(formattedSessionData, (session) => {
|
||||||
var formattedSession = session;
|
const formattedSession = session;
|
||||||
startDate = this.formatDate(formattedSession.start, dateFormat);
|
startDate = CourseEntitlementView.formatDate(formattedSession.start, dateFormat);
|
||||||
endDate = this.formatDate(formattedSession.end, dateFormat);
|
endDate = CourseEntitlementView.formatDate(formattedSession.end, dateFormat);
|
||||||
formattedSession.enrollment_end = this.formatDate(formattedSession.enrollment_end, dateFormat);
|
formattedSession.enrollment_end = CourseEntitlementView.formatDate(
|
||||||
formattedSession.session_dates = this.courseCardModel.formatDateString({
|
formattedSession.enrollment_end,
|
||||||
start_date: startDate,
|
dateFormat);
|
||||||
advertised_start: session.advertised_start,
|
formattedSession.session_dates = this.courseCardModel.formatDateString({
|
||||||
end_date: endDate,
|
start_date: startDate,
|
||||||
pacing_type: formattedSession.pacing_type
|
advertised_start: session.advertised_start,
|
||||||
});
|
end_date: endDate,
|
||||||
return formattedSession;
|
pacing_type: formattedSession.pacing_type,
|
||||||
}, this);
|
});
|
||||||
},
|
return formattedSession;
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
|
||||||
formatDate: function(date, dateFormat) {
|
static formatDate(date, dateFormat) {
|
||||||
return date ? moment((new Date(date))).format(dateFormat) : '';
|
return date ? moment((new Date(date))).format(dateFormat) : '';
|
||||||
},
|
}
|
||||||
|
|
||||||
getAvailableSessionWithId: function(sessionId) {
|
getAvailableSessionWithId(sessionId) {
|
||||||
/* Returns an available session given a sessionId */
|
/* Returns an available session given a sessionId */
|
||||||
return this.entitlementModel.get('availableSessions').find(function(session) {
|
return this.entitlementModel.get('availableSessions').find(session => session.session_id === sessionId);
|
||||||
return session.session_id === sessionId;
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
});
|
export default CourseEntitlementView;
|
||||||
}
|
|
||||||
);
|
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,130 +1,134 @@
|
|||||||
(function(define) {
|
/* globals gettext */
|
||||||
'use strict';
|
|
||||||
define(['backbone',
|
|
||||||
'jquery',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils'
|
|
||||||
],
|
|
||||||
function(Backbone, $, gettext, HtmlUtils) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
el: '.js-entitlement-unenrollment-modal',
|
|
||||||
closeButtonSelector: '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-close-btn',
|
|
||||||
headerTextSelector: '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-header-text',
|
|
||||||
errorTextSelector: '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-error-text',
|
|
||||||
submitButtonSelector: '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-submit',
|
|
||||||
triggerSelector: '.js-entitlement-action-unenroll',
|
|
||||||
mainPageSelector: '#dashboard-main',
|
|
||||||
genericErrorMsg: gettext('Your unenrollment request could not be processed. Please try again later.'),
|
|
||||||
|
|
||||||
initialize: function(options) {
|
import Backbone from 'backbone';
|
||||||
var view = this;
|
|
||||||
this.dashboardPath = options.dashboardPath;
|
|
||||||
this.signInPath = options.signInPath;
|
|
||||||
|
|
||||||
this.$submitButton = $(this.submitButtonSelector);
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
this.$headerText = $(this.headerTextSelector);
|
|
||||||
this.$errorText = $(this.errorTextSelector);
|
|
||||||
|
|
||||||
this.$submitButton.on('click', this.handleSubmit.bind(this));
|
class EntitlementUnenrollmentView extends Backbone.View {
|
||||||
|
constructor(options) {
|
||||||
|
const defaults = {
|
||||||
|
el: '.js-entitlement-unenrollment-modal',
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
$(this.triggerSelector).each(function() {
|
initialize(options) {
|
||||||
var $trigger = $(this);
|
const view = this;
|
||||||
|
|
||||||
$trigger.on('click', view.handleTrigger.bind(view));
|
this.closeButtonSelector = '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-close-btn';
|
||||||
|
this.headerTextSelector = '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-header-text';
|
||||||
|
this.errorTextSelector = '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-error-text';
|
||||||
|
this.submitButtonSelector = '.js-entitlement-unenrollment-modal .js-entitlement-unenrollment-modal-submit';
|
||||||
|
this.triggerSelector = '.js-entitlement-action-unenroll';
|
||||||
|
this.mainPageSelector = '#dashboard-main';
|
||||||
|
this.genericErrorMsg = gettext('Your unenrollment request could not be processed. Please try again later.');
|
||||||
|
|
||||||
if (window.accessible_modal) {
|
this.dashboardPath = options.dashboardPath;
|
||||||
window.accessible_modal(
|
this.signInPath = options.signInPath;
|
||||||
'#' + $trigger.attr('id'),
|
|
||||||
view.closeButtonSelector,
|
|
||||||
'#' + view.$el.attr('id'),
|
|
||||||
view.mainPageSelector
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTrigger: function(event) {
|
this.$submitButton = $(this.submitButtonSelector);
|
||||||
var $trigger = $(event.target),
|
this.$headerText = $(this.headerTextSelector);
|
||||||
courseName = $trigger.data('courseName'),
|
this.$errorText = $(this.errorTextSelector);
|
||||||
courseNumber = $trigger.data('courseNumber'),
|
|
||||||
apiEndpoint = $trigger.data('entitlementApiEndpoint');
|
|
||||||
|
|
||||||
this.resetModal();
|
this.$submitButton.on('click', this.handleSubmit.bind(this));
|
||||||
this.setHeaderText(courseName, courseNumber);
|
|
||||||
this.setSubmitData(apiEndpoint);
|
|
||||||
this.$el.css('position', 'fixed');
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSubmit: function() {
|
$(this.triggerSelector).each(function setUpTrigger() {
|
||||||
var apiEndpoint = this.$submitButton.data('entitlementApiEndpoint');
|
const $trigger = $(this);
|
||||||
|
|
||||||
if (apiEndpoint === undefined) {
|
$trigger.on('click', view.handleTrigger.bind(view));
|
||||||
this.setError(this.genericErrorMsg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$submitButton.prop('disabled', true);
|
if (window.accessible_modal) {
|
||||||
$.ajax({
|
window.accessible_modal(
|
||||||
url: apiEndpoint,
|
`#${$trigger.attr('id')}`,
|
||||||
method: 'DELETE',
|
view.closeButtonSelector,
|
||||||
complete: this.onComplete.bind(this)
|
`#${view.$el.attr('id')}`,
|
||||||
});
|
view.mainPageSelector,
|
||||||
},
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
resetModal: function() {
|
handleTrigger(event) {
|
||||||
this.$submitButton.removeData();
|
const $trigger = $(event.target);
|
||||||
this.$submitButton.prop('disabled', false);
|
const courseName = $trigger.data('courseName');
|
||||||
this.$headerText.empty();
|
const courseNumber = $trigger.data('courseNumber');
|
||||||
this.$errorText.removeClass('entitlement-unenrollment-modal-error-text-visible');
|
const apiEndpoint = $trigger.data('entitlementApiEndpoint');
|
||||||
this.$errorText.empty();
|
|
||||||
},
|
|
||||||
|
|
||||||
setError: function(message) {
|
this.resetModal();
|
||||||
this.$submitButton.prop('disabled', true);
|
this.setHeaderText(courseName, courseNumber);
|
||||||
this.$errorText.empty();
|
this.setSubmitData(apiEndpoint);
|
||||||
HtmlUtils.setHtml(
|
this.$el.css('position', 'fixed');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
const apiEndpoint = this.$submitButton.data('entitlementApiEndpoint');
|
||||||
|
|
||||||
|
if (apiEndpoint === undefined) {
|
||||||
|
this.setError(this.genericErrorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$submitButton.prop('disabled', true);
|
||||||
|
$.ajax({
|
||||||
|
url: apiEndpoint,
|
||||||
|
method: 'DELETE',
|
||||||
|
complete: this.onComplete.bind(this),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetModal() {
|
||||||
|
this.$submitButton.removeData();
|
||||||
|
this.$submitButton.prop('disabled', false);
|
||||||
|
this.$headerText.empty();
|
||||||
|
this.$errorText.removeClass('entitlement-unenrollment-modal-error-text-visible');
|
||||||
|
this.$errorText.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(message) {
|
||||||
|
this.$submitButton.prop('disabled', true);
|
||||||
|
this.$errorText.empty();
|
||||||
|
HtmlUtils.setHtml(
|
||||||
this.$errorText,
|
this.$errorText,
|
||||||
message
|
message,
|
||||||
);
|
);
|
||||||
this.$errorText.addClass('entitlement-unenrollment-modal-error-text-visible');
|
this.$errorText.addClass('entitlement-unenrollment-modal-error-text-visible');
|
||||||
},
|
}
|
||||||
|
|
||||||
setHeaderText: function(courseName, courseNumber) {
|
setHeaderText(courseName, courseNumber) {
|
||||||
this.$headerText.empty();
|
this.$headerText.empty();
|
||||||
HtmlUtils.setHtml(
|
HtmlUtils.setHtml(
|
||||||
this.$headerText,
|
this.$headerText,
|
||||||
HtmlUtils.interpolateHtml(
|
HtmlUtils.interpolateHtml(
|
||||||
gettext('Are you sure you want to unenroll from {courseName} ({courseNumber})? You will be refunded the amount you paid.'), // eslint-disable-line max-len
|
gettext('Are you sure you want to unenroll from {courseName} ({courseNumber})? You will be refunded the amount you paid.'), // eslint-disable-line max-len
|
||||||
{
|
{
|
||||||
courseName: courseName,
|
courseName,
|
||||||
courseNumber: courseNumber
|
courseNumber,
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
setSubmitData: function(apiEndpoint) {
|
|
||||||
this.$submitButton.removeData();
|
|
||||||
this.$submitButton.data('entitlementApiEndpoint', apiEndpoint);
|
|
||||||
},
|
|
||||||
|
|
||||||
onComplete: function(xhr) {
|
|
||||||
var status = xhr.status,
|
|
||||||
message = xhr.responseJSON && xhr.responseJSON.detail;
|
|
||||||
|
|
||||||
if (status === 204) {
|
|
||||||
this.redirectTo(this.dashboardPath);
|
|
||||||
} else if (status === 401 && message === 'Authentication credentials were not provided.') {
|
|
||||||
this.redirectTo(this.signInPath + '?next=' + encodeURIComponent(this.dashboardPath));
|
|
||||||
} else {
|
|
||||||
this.setError(this.genericErrorMsg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
redirectTo: function(path) {
|
|
||||||
window.location.href = path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}).call(this, define || RequireJS.define);
|
}
|
||||||
|
|
||||||
|
setSubmitData(apiEndpoint) {
|
||||||
|
this.$submitButton.removeData();
|
||||||
|
this.$submitButton.data('entitlementApiEndpoint', apiEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete(xhr) {
|
||||||
|
const status = xhr.status;
|
||||||
|
const message = xhr.responseJSON && xhr.responseJSON.detail;
|
||||||
|
|
||||||
|
if (status === 204) {
|
||||||
|
EntitlementUnenrollmentView.redirectTo(this.dashboardPath);
|
||||||
|
} else if (status === 401 && message === 'Authentication credentials were not provided.') {
|
||||||
|
EntitlementUnenrollmentView.redirectTo(`${this.signInPath}?next=${encodeURIComponent(this.dashboardPath)}`);
|
||||||
|
} else {
|
||||||
|
this.setError(this.genericErrorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static redirectTo(path) {
|
||||||
|
window.location.href = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EntitlementUnenrollmentView;
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
define(['backbone',
|
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'text!../../../templates/learner_dashboard/expired_notification.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
HtmlUtils,
|
|
||||||
expiredNotificationTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
expiredNotificationTpl: HtmlUtils.template(expiredNotificationTpl),
|
|
||||||
|
|
||||||
initialize: function(options) {
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
this.$el = options.$el;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
import expiredNotificationTpl from '../../../templates/learner_dashboard/expired_notification.underscore';
|
||||||
var data = this.model.toJSON();
|
|
||||||
HtmlUtils.setHtml(this.$el, this.expiredNotificationTpl(data));
|
class ExpiredNotificationView extends Backbone.View {
|
||||||
}
|
initialize(options) {
|
||||||
});
|
this.expiredNotificationTpl = HtmlUtils.template(expiredNotificationTpl);
|
||||||
}
|
this.$el = options.$el;
|
||||||
);
|
this.render();
|
||||||
}).call(this, define || RequireJS.define);
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const data = this.model.toJSON();
|
||||||
|
HtmlUtils.setHtml(this.$el, this.expiredNotificationTpl(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpiredNotificationView;
|
||||||
|
|||||||
@@ -1,44 +1,33 @@
|
|||||||
(function(define) {
|
import _ from 'underscore';
|
||||||
'use strict';
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
define(['backbone',
|
import exploreTpl from '../../../templates/learner_dashboard/explore_new_programs.underscore';
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'text!../../../templates/learner_dashboard/explore_new_programs.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
exploreTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
el: '.program-advertise',
|
|
||||||
|
|
||||||
tpl: _.template(exploreTpl),
|
class ExploreNewProgramsView extends Backbone.View {
|
||||||
|
constructor(options) {
|
||||||
|
const defaults = {
|
||||||
|
el: '.program-advertise',
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
initialize: function(data) {
|
initialize(data) {
|
||||||
this.context = data.context;
|
this.tpl = _.template(exploreTpl);
|
||||||
this.$parentEl = $(this.parentEl);
|
this.context = data.context;
|
||||||
|
this.$parentEl = $(this.parentEl);
|
||||||
|
|
||||||
if (this.context.marketingUrl) {
|
if (this.context.marketingUrl) {
|
||||||
// Only render if there is a link
|
// Only render if there is a link
|
||||||
this.render();
|
this.render();
|
||||||
} else {
|
} else {
|
||||||
/**
|
// If not rendering, remove el because styles are applied to it
|
||||||
* If not rendering remove el because
|
this.remove();
|
||||||
* styles are applied to it
|
}
|
||||||
*/
|
}
|
||||||
this.remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
this.$el.html(this.tpl(this.context));
|
this.$el.html(this.tpl(this.context));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
);
|
export default ExploreNewProgramsView;
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -1,114 +1,102 @@
|
|||||||
(function(define) {
|
/* globals gettext */
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define(['backbone',
|
import _ from 'underscore';
|
||||||
'jquery',
|
import Backbone from 'backbone';
|
||||||
'underscore',
|
import picturefill from 'picturefill';
|
||||||
'gettext',
|
|
||||||
'text!../../../templates/learner_dashboard/program_card.underscore',
|
|
||||||
'picturefill'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
programCardTpl,
|
|
||||||
picturefill
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
|
|
||||||
className: 'program-card',
|
import programCardTpl from '../../../templates/learner_dashboard/program_card.underscore';
|
||||||
|
|
||||||
attributes: function() {
|
class ProgramCardView extends Backbone.View {
|
||||||
return {
|
constructor(options) {
|
||||||
'aria-labelledby': 'program-' + this.model.get('uuid'),
|
const defaults = {
|
||||||
role: 'group'
|
className: 'program-card',
|
||||||
};
|
attributes: function attr() {
|
||||||
},
|
return {
|
||||||
|
'aria-labelledby': `program-${this.model.get('uuid')}`,
|
||||||
|
role: 'group',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
tpl: _.template(programCardTpl),
|
initialize(data) {
|
||||||
|
this.tpl = _.template(programCardTpl);
|
||||||
|
this.progressCollection = data.context.progressCollection;
|
||||||
|
if (this.progressCollection) {
|
||||||
|
this.progressModel = this.progressCollection.findWhere({
|
||||||
|
uuid: this.model.get('uuid'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
initialize: function(data) {
|
render() {
|
||||||
this.progressCollection = data.context.progressCollection;
|
const orgList = _.map(this.model.get('authoring_organizations'), org => gettext(org.key));
|
||||||
if (this.progressCollection) {
|
const data = $.extend(
|
||||||
this.progressModel = this.progressCollection.findWhere({
|
this.model.toJSON(),
|
||||||
uuid: this.model.get('uuid')
|
this.getProgramProgress(),
|
||||||
});
|
{ orgList: orgList.join(' ') },
|
||||||
}
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var orgList = _.map(this.model.get('authoring_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() {
|
|
||||||
if (navigator.userAgent.indexOf('MSIE') !== -1 ||
|
|
||||||
navigator.appVersion.indexOf('Trident/') > 0) {
|
|
||||||
/* Microsoft Internet Explorer detected in. */
|
|
||||||
window.setTimeout(function() {
|
|
||||||
this.reLoadBannerImage();
|
|
||||||
}.bind(this), 100);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Calculate counts for progress and percentages for styling
|
|
||||||
getProgramProgress: function() {
|
|
||||||
var progress = this.progressModel ? this.progressModel.toJSON() : false;
|
|
||||||
|
|
||||||
if (progress) {
|
|
||||||
progress.total = progress.completed +
|
|
||||||
progress.in_progress +
|
|
||||||
progress.not_started;
|
|
||||||
|
|
||||||
progress.percentage = {
|
|
||||||
completed: this.getWidth(progress.completed, progress.total),
|
|
||||||
in_progress: this.getWidth(progress.in_progress, progress.total)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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'),
|
|
||||||
imgSrcAttr = $img ? $img.attr('src') : {};
|
|
||||||
|
|
||||||
if (!imgSrcAttr || imgSrcAttr.length < 0) {
|
|
||||||
try {
|
|
||||||
this.reEvaluatePicture();
|
|
||||||
} catch (err) {
|
|
||||||
// Swallow the error here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reEvaluatePicture: function() {
|
|
||||||
picturefill({
|
|
||||||
reevaluate: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
this.$el.html(this.tpl(data));
|
||||||
|
this.postRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
postRender() {
|
||||||
|
if (navigator.userAgent.indexOf('MSIE') !== -1 ||
|
||||||
|
navigator.appVersion.indexOf('Trident/') > 0) {
|
||||||
|
/* Microsoft Internet Explorer detected in. */
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.reLoadBannerImage();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate counts for progress and percentages for styling
|
||||||
|
getProgramProgress() {
|
||||||
|
const progress = this.progressModel ? this.progressModel.toJSON() : false;
|
||||||
|
|
||||||
|
if (progress) {
|
||||||
|
progress.total = progress.completed +
|
||||||
|
progress.in_progress +
|
||||||
|
progress.not_started;
|
||||||
|
|
||||||
|
progress.percentage = {
|
||||||
|
completed: ProgramCardView.getWidth(progress.completed, progress.total),
|
||||||
|
in_progress: ProgramCardView.getWidth(progress.in_progress, progress.total),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
progress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWidth(val, total) {
|
||||||
|
const int = (val / total) * 100;
|
||||||
|
return `${int}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer loading the rest of the page to limit FOUC
|
||||||
|
reLoadBannerImage() {
|
||||||
|
const $img = this.$('.program_card .banner-image');
|
||||||
|
const imgSrcAttr = $img ? $img.attr('src') : {};
|
||||||
|
|
||||||
|
if (!imgSrcAttr || imgSrcAttr.length < 0) {
|
||||||
|
try {
|
||||||
|
ProgramCardView.reEvaluatePicture();
|
||||||
|
} catch (err) {
|
||||||
|
// Swallow the error here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static reEvaluatePicture() {
|
||||||
|
picturefill({
|
||||||
|
reevaluate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgramCardView;
|
||||||
|
|||||||
@@ -1,97 +1,82 @@
|
|||||||
(function(define) {
|
/* globals gettext */
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
import Backbone from 'backbone';
|
||||||
'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) {
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
this.courseModel = options.courseModel || {};
|
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||||
this.certificateCollection = options.certificateCollection || [];
|
|
||||||
this.programCertificate = this.getProgramCertificate();
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
import CertificateView from './certificate_list_view';
|
||||||
var data = $.extend({}, this.model.toJSON(), {
|
import ProgramProgressView from './progress_circle_view';
|
||||||
programCertificate: this.programCertificate ?
|
|
||||||
this.programCertificate.toJSON() : {}
|
|
||||||
});
|
|
||||||
|
|
||||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
import sidebarTpl from '../../../templates/learner_dashboard/program_details_sidebar.underscore';
|
||||||
this.postRender();
|
|
||||||
},
|
|
||||||
|
|
||||||
postRender: function() {
|
class ProgramDetailsSidebarView extends Backbone.View {
|
||||||
if (!this.programCertificate) {
|
initialize(options) {
|
||||||
this.progressModel = new Backbone.Model({
|
this.tpl = HtmlUtils.template(sidebarTpl);
|
||||||
title: StringUtils.interpolate(
|
this.courseModel = options.courseModel || {};
|
||||||
gettext('{type} Progress'),
|
this.certificateCollection = options.certificateCollection || [];
|
||||||
{type: this.model.get('type')}
|
this.programCertificate = this.getProgramCertificate();
|
||||||
),
|
this.render();
|
||||||
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({
|
render() {
|
||||||
el: '.js-program-progress',
|
const data = $.extend({}, this.model.toJSON(), {
|
||||||
model: this.progressModel
|
programCertificate: this.programCertificate ?
|
||||||
});
|
this.programCertificate.toJSON() : {},
|
||||||
}
|
});
|
||||||
|
|
||||||
if (this.certificateCollection.length) {
|
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||||
this.certificateView = new CertificateView({
|
this.postRender();
|
||||||
el: '.js-course-certificates',
|
}
|
||||||
collection: this.certificateCollection,
|
|
||||||
title: gettext('Earned Certificates')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getProgramCertificate: function() {
|
postRender() {
|
||||||
var certificate = this.certificateCollection.findWhere({type: 'program'}),
|
if (!this.programCertificate) {
|
||||||
base = '/static/images/programs/program-certificate-';
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (certificate) {
|
this.programProgressView = new ProgramProgressView({
|
||||||
certificate.set({
|
el: '.js-program-progress',
|
||||||
img: base + this.getType() + '.gif'
|
model: this.progressModel,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return certificate;
|
if (this.certificateCollection.length) {
|
||||||
},
|
this.certificateView = new CertificateView({
|
||||||
|
el: '.js-course-certificates',
|
||||||
|
collection: this.certificateCollection,
|
||||||
|
title: gettext('Earned Certificates'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getType: function() {
|
getProgramCertificate() {
|
||||||
var type = this.model.get('type').toLowerCase();
|
const certificate = this.certificateCollection.findWhere({ type: 'program' });
|
||||||
|
const base = '/static/images/programs/program-certificate-';
|
||||||
|
|
||||||
return type.replace(/\s+/g, '-');
|
if (certificate) {
|
||||||
}
|
certificate.set({
|
||||||
});
|
img: `${base + this.getType()}.gif`,
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
const type = this.model.get('type').toLowerCase();
|
||||||
|
|
||||||
|
return type.replace(/\s+/g, '-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgramDetailsSidebarView;
|
||||||
|
|||||||
@@ -1,137 +1,128 @@
|
|||||||
(function(define) {
|
/* globals gettext */
|
||||||
'use strict';
|
|
||||||
define(['backbone',
|
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'js/learner_dashboard/collections/course_card_collection',
|
|
||||||
'js/learner_dashboard/views/program_header_view',
|
|
||||||
'js/learner_dashboard/views/collection_list_view',
|
|
||||||
'js/learner_dashboard/views/course_card_view',
|
|
||||||
'js/learner_dashboard/views/program_details_sidebar_view',
|
|
||||||
'text!../../../templates/learner_dashboard/program_details_view.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
HtmlUtils,
|
|
||||||
CourseCardCollection,
|
|
||||||
HeaderView,
|
|
||||||
CollectionListView,
|
|
||||||
CourseCardView,
|
|
||||||
SidebarView,
|
|
||||||
pageTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
el: '.js-program-details-wrapper',
|
|
||||||
|
|
||||||
tpl: HtmlUtils.template(pageTpl),
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
events: {
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
'click .complete-program': 'trackPurchase'
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function(options) {
|
import CollectionListView from './collection_list_view';
|
||||||
this.options = options;
|
import CourseCardCollection from '../collections/course_card_collection';
|
||||||
this.programModel = new Backbone.Model(this.options.programData);
|
import CourseCardView from './course_card_view';
|
||||||
this.courseData = new Backbone.Model(this.options.courseData);
|
import HeaderView from './program_header_view';
|
||||||
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
|
import SidebarView from './program_details_sidebar_view';
|
||||||
this.completedCourseCollection = new CourseCardCollection(
|
|
||||||
this.courseData.get('completed') || [],
|
|
||||||
this.options.userPreferences
|
|
||||||
);
|
|
||||||
this.inProgressCourseCollection = new CourseCardCollection(
|
|
||||||
this.courseData.get('in_progress') || [],
|
|
||||||
this.options.userPreferences
|
|
||||||
);
|
|
||||||
this.remainingCourseCollection = new CourseCardCollection(
|
|
||||||
this.courseData.get('not_started') || [],
|
|
||||||
this.options.userPreferences
|
|
||||||
);
|
|
||||||
|
|
||||||
this.render();
|
import pageTpl from '../../../templates/learner_dashboard/program_details_view.underscore';
|
||||||
},
|
|
||||||
|
|
||||||
getUrl: function(base, programData) {
|
class ProgramDetailsView extends Backbone.View {
|
||||||
if (programData.uuid) {
|
constructor(options) {
|
||||||
return base + '&bundle=' + encodeURIComponent(programData.uuid);
|
const defaults = {
|
||||||
}
|
el: '.js-program-details-wrapper',
|
||||||
return base;
|
events: {
|
||||||
},
|
'click .complete-program': 'trackPurchase',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
render: function() {
|
initialize(options) {
|
||||||
var completedCount = this.completedCourseCollection.length,
|
this.options = options;
|
||||||
inProgressCount = this.inProgressCourseCollection.length,
|
this.tpl = HtmlUtils.template(pageTpl);
|
||||||
remainingCount = this.remainingCourseCollection.length,
|
this.programModel = new Backbone.Model(this.options.programData);
|
||||||
totalCount = completedCount + inProgressCount + remainingCount,
|
this.courseData = new Backbone.Model(this.options.courseData);
|
||||||
buyButtonUrl = this.getUrl(this.options.urls.buy_button_url, this.options.programData),
|
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
|
||||||
data = {
|
this.completedCourseCollection = new CourseCardCollection(
|
||||||
totalCount: totalCount,
|
this.courseData.get('completed') || [],
|
||||||
inProgressCount: inProgressCount,
|
this.options.userPreferences,
|
||||||
remainingCount: remainingCount,
|
|
||||||
completedCount: completedCount,
|
|
||||||
completeProgramURL: buyButtonUrl
|
|
||||||
};
|
|
||||||
data = $.extend(data, this.programModel.toJSON());
|
|
||||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
|
||||||
this.postRender();
|
|
||||||
},
|
|
||||||
|
|
||||||
postRender: function() {
|
|
||||||
this.headerView = new HeaderView({
|
|
||||||
model: new Backbone.Model(this.options)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.remainingCourseCollection.length > 0) {
|
|
||||||
new CollectionListView({
|
|
||||||
el: '.js-course-list-remaining',
|
|
||||||
childView: CourseCardView,
|
|
||||||
collection: this.remainingCourseCollection,
|
|
||||||
context: $.extend(this.options, {collectionCourseStatus: 'remaining'})
|
|
||||||
}).render();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.completedCourseCollection.length > 0) {
|
|
||||||
new CollectionListView({
|
|
||||||
el: '.js-course-list-completed',
|
|
||||||
childView: CourseCardView,
|
|
||||||
collection: this.completedCourseCollection,
|
|
||||||
context: $.extend(this.options, {collectionCourseStatus: 'completed'})
|
|
||||||
}).render();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.inProgressCourseCollection.length > 0) {
|
|
||||||
// This is last because the context is modified below
|
|
||||||
new CollectionListView({
|
|
||||||
el: '.js-course-list-in-progress',
|
|
||||||
childView: CourseCardView,
|
|
||||||
collection: this.inProgressCourseCollection,
|
|
||||||
context: $.extend(this.options,
|
|
||||||
{enrolled: gettext('Enrolled'), collectionCourseStatus: 'in_progress'}
|
|
||||||
)
|
|
||||||
}).render();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sidebarView = new SidebarView({
|
|
||||||
el: '.js-program-sidebar',
|
|
||||||
model: this.programModel,
|
|
||||||
courseModel: this.courseData,
|
|
||||||
certificateCollection: this.certificateCollection
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
trackPurchase: function() {
|
|
||||||
var data = this.options.programData;
|
|
||||||
window.analytics.track('edx.bi.user.dashboard.program.purchase', {
|
|
||||||
category: data.variant + ' bundle',
|
|
||||||
label: data.title,
|
|
||||||
uuid: data.uuid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}).call(this, define || RequireJS.define);
|
this.inProgressCourseCollection = new CourseCardCollection(
|
||||||
|
this.courseData.get('in_progress') || [],
|
||||||
|
this.options.userPreferences,
|
||||||
|
);
|
||||||
|
this.remainingCourseCollection = new CourseCardCollection(
|
||||||
|
this.courseData.get('not_started') || [],
|
||||||
|
this.options.userPreferences,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUrl(base, programData) {
|
||||||
|
if (programData.uuid) {
|
||||||
|
return `${base}&bundle=${encodeURIComponent(programData.uuid)}`;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const completedCount = this.completedCourseCollection.length;
|
||||||
|
const inProgressCount = this.inProgressCourseCollection.length;
|
||||||
|
const remainingCount = this.remainingCourseCollection.length;
|
||||||
|
const totalCount = completedCount + inProgressCount + remainingCount;
|
||||||
|
const buyButtonUrl = ProgramDetailsView.getUrl(
|
||||||
|
this.options.urls.buy_button_url,
|
||||||
|
this.options.programData);
|
||||||
|
let data = {
|
||||||
|
totalCount,
|
||||||
|
inProgressCount,
|
||||||
|
remainingCount,
|
||||||
|
completedCount,
|
||||||
|
completeProgramURL: buyButtonUrl,
|
||||||
|
};
|
||||||
|
data = $.extend(data, this.programModel.toJSON());
|
||||||
|
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||||
|
this.postRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
postRender() {
|
||||||
|
this.headerView = new HeaderView({
|
||||||
|
model: new Backbone.Model(this.options),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.remainingCourseCollection.length > 0) {
|
||||||
|
new CollectionListView({
|
||||||
|
el: '.js-course-list-remaining',
|
||||||
|
childView: CourseCardView,
|
||||||
|
collection: this.remainingCourseCollection,
|
||||||
|
context: $.extend(this.options, { collectionCourseStatus: 'remaining' }),
|
||||||
|
}).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.completedCourseCollection.length > 0) {
|
||||||
|
new CollectionListView({
|
||||||
|
el: '.js-course-list-completed',
|
||||||
|
childView: CourseCardView,
|
||||||
|
collection: this.completedCourseCollection,
|
||||||
|
context: $.extend(this.options, { collectionCourseStatus: 'completed' }),
|
||||||
|
}).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inProgressCourseCollection.length > 0) {
|
||||||
|
// This is last because the context is modified below
|
||||||
|
new CollectionListView({
|
||||||
|
el: '.js-course-list-in-progress',
|
||||||
|
childView: CourseCardView,
|
||||||
|
collection: this.inProgressCourseCollection,
|
||||||
|
context: $.extend(this.options,
|
||||||
|
{ enrolled: gettext('Enrolled'), collectionCourseStatus: 'in_progress' },
|
||||||
|
),
|
||||||
|
}).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sidebarView = new SidebarView({
|
||||||
|
el: '.js-program-sidebar',
|
||||||
|
model: this.programModel,
|
||||||
|
courseModel: this.courseData,
|
||||||
|
certificateCollection: this.certificateCollection,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
trackPurchase() {
|
||||||
|
const data = this.options.programData;
|
||||||
|
window.analytics.track('edx.bi.user.dashboard.program.purchase', {
|
||||||
|
category: `${data.variant} bundle`,
|
||||||
|
label: data.title,
|
||||||
|
uuid: data.uuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgramDetailsView;
|
||||||
|
|||||||
@@ -1,57 +1,55 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
|
|
||||||
define(['backbone',
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
'jquery',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'text!../../../templates/learner_dashboard/program_header_view.underscore',
|
|
||||||
'text!../../../images/programs/micromasters-program-details.svg',
|
|
||||||
'text!../../../images/programs/xseries-program-details.svg',
|
|
||||||
'text!../../../images/programs/professional-certificate-program-details.svg'
|
|
||||||
],
|
|
||||||
function(Backbone, $, HtmlUtils, pageTpl, MicroMastersLogo,
|
|
||||||
XSeriesLogo, ProfessionalCertificateLogo) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
breakpoints: {
|
|
||||||
min: {
|
|
||||||
medium: '768px',
|
|
||||||
large: '1180px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
el: '.js-program-header',
|
import pageTpl from '../../../templates/learner_dashboard/program_header_view.underscore';
|
||||||
|
import MicroMastersLogo from '../../../images/programs/micromasters-program-details.svg';
|
||||||
|
import XSeriesLogo from '../../../images/programs/xseries-program-details.svg';
|
||||||
|
import ProfessionalCertificateLogo from '../../../images/programs/professional-certificate-program-details.svg';
|
||||||
|
|
||||||
tpl: HtmlUtils.template(pageTpl),
|
class ProgramHeaderView extends Backbone.View {
|
||||||
|
constructor(options) {
|
||||||
|
const defaults = {
|
||||||
|
el: '.js-program-header',
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
initialize: function() {
|
initialize() {
|
||||||
this.render();
|
this.breakpoints = {
|
||||||
},
|
min: {
|
||||||
|
medium: '768px',
|
||||||
|
large: '1180px',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.tpl = HtmlUtils.template(pageTpl);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
getLogo: function() {
|
getLogo() {
|
||||||
var logo = false,
|
const type = this.model.get('programData').type;
|
||||||
type = this.model.get('programData').type;
|
let logo = false;
|
||||||
|
|
||||||
if (type === 'MicroMasters') {
|
if (type === 'MicroMasters') {
|
||||||
logo = MicroMastersLogo;
|
logo = MicroMastersLogo;
|
||||||
} else if (type === 'XSeries') {
|
} else if (type === 'XSeries') {
|
||||||
logo = XSeriesLogo;
|
logo = XSeriesLogo;
|
||||||
} else if (type === 'Professional Certificate') {
|
} else if (type === 'Professional Certificate') {
|
||||||
logo = ProfessionalCertificateLogo;
|
logo = ProfessionalCertificateLogo;
|
||||||
}
|
}
|
||||||
return logo;
|
return logo;
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
var data = $.extend(this.model.toJSON(), {
|
const data = $.extend(this.model.toJSON(), {
|
||||||
breakpoints: this.breakpoints,
|
breakpoints: this.breakpoints,
|
||||||
logo: this.getLogo()
|
logo: this.getLogo(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.model.get('programData')) {
|
if (this.model.get('programData')) {
|
||||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
);
|
export default ProgramHeaderView;
|
||||||
}).call(this, define || RequireJS.define);
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import _ from 'underscore';
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
|
import progressViewTpl from '../../../templates/learner_dashboard//progress_circle_view.underscore';
|
||||||
|
import progressSegmentTpl from '../../../templates/learner_dashboard/progress_circle_segment.underscore';
|
||||||
|
|
||||||
|
class ProgressCircleView extends Backbone.View {
|
||||||
|
initialize() {
|
||||||
|
this.x = 22;
|
||||||
|
this.y = 22;
|
||||||
|
this.radius = 16;
|
||||||
|
this.degrees = 180;
|
||||||
|
this.strokeWidth = 1.2;
|
||||||
|
|
||||||
|
this.viewTpl = _.template(progressViewTpl);
|
||||||
|
this.segmentTpl = _.template(progressSegmentTpl);
|
||||||
|
|
||||||
|
const progress = this.model.get('progress');
|
||||||
|
|
||||||
|
this.model.set({
|
||||||
|
totalCourses: progress.completed + progress.in_progress + progress.not_started,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDegreeIncrement(total) {
|
||||||
|
return 360 / total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOffset(total) {
|
||||||
|
return 100 - ((1 / total) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProgressSegments() {
|
||||||
|
const progressHTML = [];
|
||||||
|
const total = this.model.get('totalCourses');
|
||||||
|
const segmentDash = 2 * Math.PI * this.radius;
|
||||||
|
const degreeInc = ProgressCircleView.getDegreeIncrement(total);
|
||||||
|
const data = {
|
||||||
|
// Remove strokeWidth to show a gap between the segments
|
||||||
|
dashArray: segmentDash - this.strokeWidth,
|
||||||
|
degrees: this.degrees,
|
||||||
|
offset: ProgressCircleView.getOffset(total),
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
radius: this.radius,
|
||||||
|
strokeWidth: this.strokeWidth,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < total; i += 1) {
|
||||||
|
const 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('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgressCircleView;
|
||||||
@@ -1,41 +1,33 @@
|
|||||||
(function(define) {
|
import _ from 'underscore';
|
||||||
'use strict';
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
define(['backbone',
|
import NewProgramsView from './explore_new_programs_view';
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'js/learner_dashboard/views/explore_new_programs_view',
|
|
||||||
'text!../../../templates/learner_dashboard/sidebar.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
NewProgramsView,
|
|
||||||
sidebarTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
el: '.sidebar',
|
|
||||||
|
|
||||||
tpl: _.template(sidebarTpl),
|
import sidebarTpl from '../../../templates/learner_dashboard/sidebar.underscore';
|
||||||
|
|
||||||
initialize: function(data) {
|
class SidebarView extends Backbone.View {
|
||||||
this.context = data.context;
|
constructor(options) {
|
||||||
},
|
const defaults = {
|
||||||
|
el: '.sidebar',
|
||||||
|
};
|
||||||
|
super(Object.assign({}, defaults, options));
|
||||||
|
}
|
||||||
|
|
||||||
render: function() {
|
initialize(data) {
|
||||||
this.$el.html(this.tpl(this.context));
|
this.tpl = _.template(sidebarTpl);
|
||||||
this.postRender();
|
this.context = data.context;
|
||||||
},
|
}
|
||||||
|
|
||||||
postRender: function() {
|
render() {
|
||||||
this.newProgramsView = new NewProgramsView({
|
this.$el.html(this.tpl(this.context));
|
||||||
context: this.context
|
this.postRender();
|
||||||
});
|
}
|
||||||
}
|
|
||||||
});
|
postRender() {
|
||||||
}
|
this.newProgramsView = new NewProgramsView({
|
||||||
);
|
context: this.context,
|
||||||
}).call(this, define || RequireJS.define);
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarView;
|
||||||
|
|||||||
@@ -1,77 +1,72 @@
|
|||||||
(function(define) {
|
/* globals gettext */
|
||||||
'use strict';
|
|
||||||
define(['backbone',
|
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
el: '.unenroll-modal',
|
|
||||||
|
|
||||||
switchToSlideOne: function() {
|
import Backbone from 'backbone';
|
||||||
var survey, i;
|
|
||||||
// Randomize survey option order
|
|
||||||
survey = document.querySelector('.options');
|
|
||||||
for (i = survey.children.length - 1; i >= 0; i--) {
|
|
||||||
survey.appendChild(survey.children[Math.random() * i | 0]);
|
|
||||||
}
|
|
||||||
this.$('.inner-wrapper header').hide();
|
|
||||||
this.$('#unenroll_form').hide();
|
|
||||||
this.$('.slide1').removeClass('hidden');
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToSlideTwo: function() {
|
class UnenrollView extends Backbone.View {
|
||||||
var reason = this.$(".reasons_survey input[name='reason']:checked").attr('val');
|
|
||||||
if (reason === 'Other') {
|
|
||||||
reason = this.$('.other_text').val();
|
|
||||||
}
|
|
||||||
if (reason) {
|
|
||||||
window.analytics.track('unenrollment_reason.selected', {
|
|
||||||
category: 'user-engagement',
|
|
||||||
label: reason,
|
|
||||||
displayName: 'v1'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.$('.slide1').addClass('hidden');
|
|
||||||
this.$('.survey_course_name').text(this.$('#unenroll_course_name').text());
|
|
||||||
this.$('.slide2').removeClass('hidden');
|
|
||||||
this.$('.reasons_survey .return_to_dashboard').attr('href', this.urls.dashboard);
|
|
||||||
this.$('.reasons_survey .browse_courses').attr('href', this.urls.browseCourses);
|
|
||||||
},
|
|
||||||
|
|
||||||
unenrollComplete: function(event, xhr) {
|
constructor(options) {
|
||||||
if (xhr.status === 200) {
|
const defaults = {
|
||||||
if (!this.isEdx) {
|
el: '.unenroll-modal',
|
||||||
location.href = this.urls.dashboard;
|
};
|
||||||
} else {
|
super(Object.assign({}, defaults, options));
|
||||||
this.switchToSlideOne();
|
}
|
||||||
this.$('.reasons_survey:first .submit_reasons').click(this.switchToSlideTwo.bind(this));
|
|
||||||
}
|
|
||||||
} else if (xhr.status === 403) {
|
|
||||||
location.href = this.urls.signInUser + '?course_id=' +
|
|
||||||
encodeURIComponent($('#unenroll_course_id').val()) + '&enrollment_action=unenroll';
|
|
||||||
} else {
|
|
||||||
$('#unenroll_error').text(
|
|
||||||
gettext('Unable to determine whether we should give you a refund because' +
|
|
||||||
' of System Error. Please try again later.')
|
|
||||||
).stop()
|
|
||||||
.css('display', 'block');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function(options) {
|
switchToSlideOne() {
|
||||||
this.urls = options.urls;
|
// Randomize survey option order
|
||||||
this.isEdx = options.isEdx;
|
const survey = document.querySelector('.options');
|
||||||
|
for (let i = survey.children.length - 1; i >= 0; i -= 1) {
|
||||||
|
survey.appendChild(survey.children[Math.trunc(Math.random() * i)]);
|
||||||
|
}
|
||||||
|
this.$('.inner-wrapper header').hide();
|
||||||
|
this.$('#unenroll_form').hide();
|
||||||
|
this.$('.slide1').removeClass('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
$('#unenroll_form').on('ajax:complete', this.unenrollComplete.bind(this));
|
switchToSlideTwo() {
|
||||||
}
|
let reason = this.$(".reasons_survey input[name='reason']:checked").attr('val');
|
||||||
});
|
if (reason === 'Other') {
|
||||||
}
|
reason = this.$('.other_text').val();
|
||||||
);
|
}
|
||||||
}).call(this, define || RequireJS.define);
|
if (reason) {
|
||||||
|
window.analytics.track('unenrollment_reason.selected', {
|
||||||
|
category: 'user-engagement',
|
||||||
|
label: reason,
|
||||||
|
displayName: 'v1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.$('.slide1').addClass('hidden');
|
||||||
|
this.$('.survey_course_name').text(this.$('#unenroll_course_name').text());
|
||||||
|
this.$('.slide2').removeClass('hidden');
|
||||||
|
this.$('.reasons_survey .return_to_dashboard').attr('href', this.urls.dashboard);
|
||||||
|
this.$('.reasons_survey .browse_courses').attr('href', this.urls.browseCourses);
|
||||||
|
}
|
||||||
|
|
||||||
|
unenrollComplete(event, xhr) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
if (!this.isEdx) {
|
||||||
|
location.href = this.urls.dashboard;
|
||||||
|
} else {
|
||||||
|
this.switchToSlideOne();
|
||||||
|
this.$('.reasons_survey:first .submit_reasons').click(this.switchToSlideTwo.bind(this));
|
||||||
|
}
|
||||||
|
} else if (xhr.status === 403) {
|
||||||
|
location.href = `${this.urls.signInUser}?course_id=${
|
||||||
|
encodeURIComponent($('#unenroll_course_id').val())}&enrollment_action=unenroll`;
|
||||||
|
} else {
|
||||||
|
$('#unenroll_error').text(
|
||||||
|
gettext('Unable to determine whether we should give you a refund because' +
|
||||||
|
' of System Error. Please try again later.'),
|
||||||
|
).stop()
|
||||||
|
.css('display', 'block');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(options) {
|
||||||
|
this.urls = options.urls;
|
||||||
|
this.isEdx = options.isEdx;
|
||||||
|
|
||||||
|
$('#unenroll_form').on('ajax:complete', this.unenrollComplete.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnenrollView;
|
||||||
|
|||||||
@@ -1,34 +1,20 @@
|
|||||||
(function(define) {
|
import Backbone from 'backbone';
|
||||||
'use strict';
|
|
||||||
define(['backbone',
|
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'gettext',
|
|
||||||
'edx-ui-toolkit/js/utils/html-utils',
|
|
||||||
'text!../../../templates/learner_dashboard/upgrade_message.underscore'
|
|
||||||
],
|
|
||||||
function(
|
|
||||||
Backbone,
|
|
||||||
$,
|
|
||||||
_,
|
|
||||||
gettext,
|
|
||||||
HtmlUtils,
|
|
||||||
upgradeMessageTpl
|
|
||||||
) {
|
|
||||||
return Backbone.View.extend({
|
|
||||||
messageTpl: HtmlUtils.template(upgradeMessageTpl),
|
|
||||||
|
|
||||||
initialize: function(options) {
|
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||||
this.$el = options.$el;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
import upgradeMessageTpl from '../../../templates/learner_dashboard/upgrade_message.underscore';
|
||||||
var data = this.model.toJSON();
|
|
||||||
|
|
||||||
HtmlUtils.setHtml(this.$el, this.messageTpl(data));
|
class UpgradeMessageView extends Backbone.View {
|
||||||
}
|
initialize(options) {
|
||||||
});
|
this.messageTpl = HtmlUtils.template(upgradeMessageTpl);
|
||||||
}
|
this.$el = options.$el;
|
||||||
);
|
this.render();
|
||||||
}).call(this, define || RequireJS.define);
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const data = this.model.toJSON();
|
||||||
|
HtmlUtils.setHtml(this.$el, this.messageTpl(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpgradeMessageView;
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/views/program_card_view',
|
|
||||||
'js/learner_dashboard/collections/program_collection',
|
|
||||||
'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 */
|
|
||||||
|
|
||||||
describe('Collection List View', function() {
|
|
||||||
var view = null,
|
|
||||||
programCollection,
|
|
||||||
progressCollection,
|
|
||||||
context = {
|
|
||||||
programsData: [
|
|
||||||
{
|
|
||||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
|
||||||
title: 'Food Security and Sustainability',
|
|
||||||
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
|
||||||
type: 'XSeries',
|
|
||||||
detail_url: 'https://www.edx.org/foo/bar',
|
|
||||||
banner_image: {
|
|
||||||
medium: {
|
|
||||||
height: 242,
|
|
||||||
width: 726,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
|
|
||||||
},
|
|
||||||
'x-small': {
|
|
||||||
height: 116,
|
|
||||||
width: 348,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
height: 145,
|
|
||||||
width: 435,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
height: 480,
|
|
||||||
width: 1440,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authoring_organizations: [
|
|
||||||
{
|
|
||||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
|
||||||
key: 'WageningenX',
|
|
||||||
name: 'Wageningen University & Research'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
|
||||||
title: 'edX Course Creator',
|
|
||||||
subtitle: 'Become an expert in creating courses for the edX platform.',
|
|
||||||
type: 'XSeries',
|
|
||||||
detail_url: 'https://www.edx.org/foo/bar',
|
|
||||||
banner_image: {
|
|
||||||
medium: {
|
|
||||||
height: 242,
|
|
||||||
width: 726,
|
|
||||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.medium.jpg'
|
|
||||||
},
|
|
||||||
'x-small': {
|
|
||||||
height: 116,
|
|
||||||
width: 348,
|
|
||||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.x-small.jpg'
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
height: 145,
|
|
||||||
width: 435,
|
|
||||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.small.jpg'
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
height: 480,
|
|
||||||
width: 1440,
|
|
||||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.large.jpg'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authoring_organizations: [
|
|
||||||
{
|
|
||||||
uuid: '4f8cb2c9-589b-4d1e-88c1-b01a02db3a9c',
|
|
||||||
key: 'edX',
|
|
||||||
name: 'edX'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
userProgress: [
|
|
||||||
{
|
|
||||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
|
||||||
completed: 4,
|
|
||||||
in_progress: 2,
|
|
||||||
not_started: 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
|
||||||
completed: 1,
|
|
||||||
in_progress: 0,
|
|
||||||
not_started: 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
context: context
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load the collection items based on passed in collection', function() {
|
|
||||||
var $cards = view.$el.find('.program-card');
|
|
||||||
expect($cards.length).toBe(2);
|
|
||||||
$cards.each(function(index, el) {
|
|
||||||
// eslint-disable-next-line newline-per-chained-call
|
|
||||||
expect($(el).find('.title').html().trim()).toEqual(context.programsData[index].title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display no item if collection is empty', function() {
|
|
||||||
var $cards;
|
|
||||||
view.remove();
|
|
||||||
programCollection = new ProgramCollection([]);
|
|
||||||
view = new CollectionListView({
|
|
||||||
el: '.program-cards-container',
|
|
||||||
childView: ProgramCardView,
|
|
||||||
context: {},
|
|
||||||
collection: programCollection
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
$cards = view.$el.find('.program-card');
|
|
||||||
expect($cards.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have no title when title not provided', function() {
|
|
||||||
var $title;
|
|
||||||
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
|
|
||||||
view.remove();
|
|
||||||
view.render();
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
$title = view.$el.parent().find('.collection-title');
|
|
||||||
expect($title.html()).not.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display screen reader header when provided', function() {
|
|
||||||
var titleContext = {el: 'h2', title: 'list start'},
|
|
||||||
$title;
|
|
||||||
|
|
||||||
view.remove();
|
|
||||||
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
|
|
||||||
programCollection = new ProgramCollection(context.programsData);
|
|
||||||
view = new CollectionListView({
|
|
||||||
el: '.program-cards-container',
|
|
||||||
childView: ProgramCardView,
|
|
||||||
context: context,
|
|
||||||
collection: programCollection,
|
|
||||||
titleContext: titleContext
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
$title = view.$el.parent().find('.collection-title');
|
|
||||||
expect($title.html()).toBe(titleContext.title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/models/course_card_model',
|
|
||||||
'js/learner_dashboard/views/course_card_view'
|
|
||||||
], function(Backbone, $, CourseCardModel, CourseCardView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Course Card View', function() {
|
|
||||||
var view = null,
|
|
||||||
courseCardModel,
|
|
||||||
course,
|
|
||||||
startDate = 'Feb 28, 2017',
|
|
||||||
endDate = 'May 30, 2017',
|
|
||||||
|
|
||||||
setupView = function(data, isEnrolled, collectionCourseStatus) {
|
|
||||||
var programData = $.extend({}, data),
|
|
||||||
context = {
|
|
||||||
courseData: {
|
|
||||||
grades: {
|
|
||||||
'course-v1:WageningenX+FFESx+1T2017': 0.8
|
|
||||||
}
|
|
||||||
},
|
|
||||||
collectionCourseStatus: collectionCourseStatus
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof collectionCourseStatus === 'undefined') {
|
|
||||||
context.collectionCourseStatus = 'completed';
|
|
||||||
}
|
|
||||||
|
|
||||||
programData.course_runs[0].is_enrolled = isEnrolled;
|
|
||||||
setFixtures('<div class="program-course-card"></div>');
|
|
||||||
courseCardModel = new CourseCardModel(programData);
|
|
||||||
view = new CourseCardView({
|
|
||||||
model: courseCardModel,
|
|
||||||
context: context
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
validateCourseInfoDisplay = function() {
|
|
||||||
// DRY validation for course card in enrolled state
|
|
||||||
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(course.title);
|
|
||||||
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
|
|
||||||
course.course_runs[0].marketing_url
|
|
||||||
);
|
|
||||||
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
|
||||||
startDate + ' - ' + endDate
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
// NOTE: This data is redefined prior to each test case so that tests
|
|
||||||
// can't break each other by modifying data copied by reference.
|
|
||||||
course = {
|
|
||||||
key: 'WageningenX+FFESx',
|
|
||||||
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',
|
|
||||||
title: 'Systems thinking and environmental sustainability',
|
|
||||||
course_runs: [
|
|
||||||
{
|
|
||||||
key: 'course-v1:WageningenX+FFESx+1T2017',
|
|
||||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
|
||||||
image: {
|
|
||||||
src: 'https://example.com/9f8562eb-f99b-45c7-b437-799fd0c15b6a.jpg'
|
|
||||||
},
|
|
||||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability',
|
|
||||||
start: '2017-02-28T05:00:00Z',
|
|
||||||
end: '2017-05-30T23:00:00Z',
|
|
||||||
enrollment_start: '2017-01-18T00:00:00Z',
|
|
||||||
enrollment_end: null,
|
|
||||||
type: 'verified',
|
|
||||||
certificate_url: '',
|
|
||||||
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017',
|
|
||||||
enrollment_open_date: 'Jan 18, 2016',
|
|
||||||
is_course_ended: false,
|
|
||||||
is_enrolled: true,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
status: 'published',
|
|
||||||
upgrade_url: ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
setupView(course, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render final grade if course is completed', function() {
|
|
||||||
view.remove();
|
|
||||||
setupView(course, true);
|
|
||||||
expect(view.$('.grade-display').text()).toEqual('80%');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not render final grade if course has not been completed', function() {
|
|
||||||
view.remove();
|
|
||||||
setupView(course, true, 'in_progress');
|
|
||||||
expect(view.$('.final-grade').length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the course card based on the data not enrolled', function() {
|
|
||||||
view.remove();
|
|
||||||
setupView(course, false);
|
|
||||||
validateCourseInfoDisplay();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update render if the course card is_enrolled updated', function() {
|
|
||||||
setupView(course, false);
|
|
||||||
courseCardModel.set({
|
|
||||||
is_enrolled: true
|
|
||||||
});
|
|
||||||
validateCourseInfoDisplay();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show the course advertised start date', function() {
|
|
||||||
var advertisedStart = 'A long time ago...';
|
|
||||||
course.course_runs[0].advertised_start = advertisedStart;
|
|
||||||
|
|
||||||
setupView(course, false);
|
|
||||||
|
|
||||||
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
|
||||||
advertisedStart + ' - ' + endDate
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only show certificate status section if a certificate has been earned', function() {
|
|
||||||
var certUrl = 'sample-certificate';
|
|
||||||
|
|
||||||
expect(view.$('.course-certificate .certificate-status').length).toEqual(0);
|
|
||||||
view.remove();
|
|
||||||
|
|
||||||
course.course_runs[0].certificate_url = certUrl;
|
|
||||||
setupView(course, false);
|
|
||||||
expect(view.$('.course-certificate .certificate-status').length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only show upgrade message section if an upgrade is required', function() {
|
|
||||||
var upgradeUrl = '/path/to/upgrade';
|
|
||||||
|
|
||||||
expect(view.$('.upgrade-message').length).toEqual(0);
|
|
||||||
view.remove();
|
|
||||||
|
|
||||||
course.course_runs[0].upgrade_url = upgradeUrl;
|
|
||||||
setupView(course, false);
|
|
||||||
expect(view.$('.upgrade-message').length).toEqual(1);
|
|
||||||
expect(view.$('.upgrade-message .cta-primary').attr('href')).toEqual(upgradeUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show both the upgrade message and certificate status sections', function() {
|
|
||||||
// Verify that no empty elements are left in the DOM.
|
|
||||||
course.course_runs[0].upgrade_url = '';
|
|
||||||
course.course_runs[0].certificate_url = '';
|
|
||||||
setupView(course, false);
|
|
||||||
expect(view.$('.upgrade-message').length).toEqual(0);
|
|
||||||
expect(view.$('.course-certificate .certificate-status').length).toEqual(0);
|
|
||||||
view.remove();
|
|
||||||
|
|
||||||
// Verify that the upgrade message takes priority.
|
|
||||||
course.course_runs[0].upgrade_url = '/path/to/upgrade';
|
|
||||||
course.course_runs[0].certificate_url = '/path/to/certificate';
|
|
||||||
setupView(course, false);
|
|
||||||
expect(view.$('.upgrade-message').length).toEqual(1);
|
|
||||||
expect(view.$('.course-certificate .certificate-status').length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow enrollment in future runs when the user has an expired enrollment', function() {
|
|
||||||
var newRun = $.extend({}, course.course_runs[0]),
|
|
||||||
newRunKey = 'course-v1:foo+bar+baz',
|
|
||||||
advertisedStart = 'Summer';
|
|
||||||
|
|
||||||
newRun.key = newRunKey;
|
|
||||||
newRun.is_enrolled = false;
|
|
||||||
newRun.advertised_start = advertisedStart;
|
|
||||||
course.course_runs.push(newRun);
|
|
||||||
|
|
||||||
course.expired = true;
|
|
||||||
|
|
||||||
setupView(course, true);
|
|
||||||
|
|
||||||
expect(courseCardModel.get('course_run_key')).toEqual(newRunKey);
|
|
||||||
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
|
||||||
advertisedStart + ' - ' + endDate
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a message if an there is an upcoming course run', function() {
|
|
||||||
course.course_runs[0].is_enrollment_open = false;
|
|
||||||
|
|
||||||
setupView(course, false);
|
|
||||||
|
|
||||||
expect(view.$('.course-details .course-title').text().trim()).toEqual(course.title);
|
|
||||||
expect(view.$('.course-details .course-text .run-period').length).toBe(0);
|
|
||||||
expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon');
|
|
||||||
expect(view.$('.enrollment-open-date').text().trim()).toEqual(
|
|
||||||
course.course_runs[0].enrollment_open_date
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a message if there are no upcoming course runs', function() {
|
|
||||||
course.course_runs[0].is_enrollment_open = false;
|
|
||||||
course.course_runs[0].is_course_ended = true;
|
|
||||||
|
|
||||||
setupView(course, false);
|
|
||||||
|
|
||||||
expect(view.$('.course-details .course-title').text().trim()).toEqual(course.title);
|
|
||||||
expect(view.$('.course-details .course-text .run-period').length).toBe(0);
|
|
||||||
expect(view.$('.no-action-message').text().trim()).toBe('Not Currently Available');
|
|
||||||
expect(view.$('.enrollment-opens').length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should link to the marketing site when the user is not enrolled', function() {
|
|
||||||
setupView(course, false);
|
|
||||||
expect(view.$('.course-title-link').attr('href')).toEqual(course.course_runs[0].marketing_url);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should link to the course home when the user is enrolled', function() {
|
|
||||||
setupView(course, true);
|
|
||||||
expect(view.$('.course-title-link').attr('href')).toEqual(course.course_runs[0].course_url);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not link to the marketing site if the URL is not available', function() {
|
|
||||||
course.course_runs[0].marketing_url = null;
|
|
||||||
setupView(course, false);
|
|
||||||
|
|
||||||
expect(view.$('.course-title-link').length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not link to the course home if the URL is not available', function() {
|
|
||||||
course.course_runs[0].course_url = null;
|
|
||||||
setupView(course, true);
|
|
||||||
|
|
||||||
expect(view.$('.course-title-link').length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show an unfulfilled user entitlement allows you to select a session', function() {
|
|
||||||
course.user_entitlement = {
|
|
||||||
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
|
||||||
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
|
||||||
expiration_date: '2017-12-05 01:06:12'
|
|
||||||
};
|
|
||||||
setupView(course, false);
|
|
||||||
expect(view.$('.info-expires-at').text().trim()).toContain('You must select a session by');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a fulfilled expired user entitlement does not allow the changing of sessions', function() {
|
|
||||||
course.user_entitlement = {
|
|
||||||
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
|
||||||
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
|
||||||
expired_at: '2017-12-06 01:06:12',
|
|
||||||
expiration_date: '2017-12-05 01:06:12'
|
|
||||||
};
|
|
||||||
setupView(course, true);
|
|
||||||
expect(view.$('.info-expires-at').text().trim()).toContain('You can no longer change sessions.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a fulfilled user entitlement allows the changing of sessions', function() {
|
|
||||||
course.user_entitlement = {
|
|
||||||
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
|
||||||
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
|
|
||||||
expiration_date: '2017-12-05 01:06:12'
|
|
||||||
};
|
|
||||||
setupView(course, true);
|
|
||||||
expect(view.$('.info-expires-at').text().trim()).toContain('You can change sessions until');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/models/course_card_model',
|
|
||||||
'js/learner_dashboard/models/course_enroll_model',
|
|
||||||
'js/learner_dashboard/views/course_enroll_view'
|
|
||||||
], function(Backbone, $, CourseCardModel, CourseEnrollModel, CourseEnrollView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Course Enroll View', function() {
|
|
||||||
var view = null,
|
|
||||||
courseCardModel,
|
|
||||||
courseEnrollModel,
|
|
||||||
urlModel,
|
|
||||||
setupView,
|
|
||||||
singleCourseRunList,
|
|
||||||
multiCourseRunList,
|
|
||||||
course = {
|
|
||||||
key: 'WageningenX+FFESx',
|
|
||||||
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',
|
|
||||||
title: 'Systems thinking and environmental sustainability',
|
|
||||||
owners: [
|
|
||||||
{
|
|
||||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
|
||||||
key: 'WageningenX',
|
|
||||||
name: 'Wageningen University & Research'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
urls = {
|
|
||||||
commerce_api_url: '/commerce',
|
|
||||||
track_selection_url: '/select_track/course/'
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
// Stub analytics tracking
|
|
||||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
|
||||||
|
|
||||||
// NOTE: This data is redefined prior to each test case so that tests
|
|
||||||
// can't break each other by modifying data copied by reference.
|
|
||||||
singleCourseRunList = [{
|
|
||||||
key: 'course-v1:WageningenX+FFESx+1T2017',
|
|
||||||
uuid: '2f2edf03-79e6-4e39-aef0-65436a6ee344',
|
|
||||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
|
||||||
image: {
|
|
||||||
src: 'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg'
|
|
||||||
},
|
|
||||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx',
|
|
||||||
start: '2017-02-28T05:00:00Z',
|
|
||||||
end: '2017-05-30T23:00:00Z',
|
|
||||||
enrollment_start: '2017-01-18T00:00:00Z',
|
|
||||||
enrollment_end: null,
|
|
||||||
type: 'verified',
|
|
||||||
certificate_url: '',
|
|
||||||
course_url: 'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course',
|
|
||||||
enrollment_open_date: 'Jan 18, 2016',
|
|
||||||
is_course_ended: false,
|
|
||||||
is_enrolled: false,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
status: 'published',
|
|
||||||
upgrade_url: ''
|
|
||||||
}];
|
|
||||||
|
|
||||||
multiCourseRunList = [{
|
|
||||||
key: 'course-v1:WageningenX+FFESx+2T2016',
|
|
||||||
uuid: '9bbb7844-4848-44ab-8e20-0be6604886e9',
|
|
||||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
|
||||||
image: {
|
|
||||||
src: 'https://example.com/9bbb7844-4848-44ab-8e20-0be6604886e9.jpg'
|
|
||||||
},
|
|
||||||
short_description: 'Learn how to apply systems thinking to improve food production systems.',
|
|
||||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-stesx',
|
|
||||||
start: '2016-09-08T04:00:00Z',
|
|
||||||
end: '2016-11-11T00:00:00Z',
|
|
||||||
enrollment_start: null,
|
|
||||||
enrollment_end: null,
|
|
||||||
pacing_type: 'instructor_paced',
|
|
||||||
type: 'verified',
|
|
||||||
certificate_url: '',
|
|
||||||
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+2T2016',
|
|
||||||
enrollment_open_date: 'Jan 18, 2016',
|
|
||||||
is_course_ended: false,
|
|
||||||
is_enrolled: false,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
status: 'published'
|
|
||||||
}, {
|
|
||||||
key: 'course-v1:WageningenX+FFESx+1T2017',
|
|
||||||
uuid: '2f2edf03-79e6-4e39-aef0-65436a6ee344',
|
|
||||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
|
||||||
image: {
|
|
||||||
src: 'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg'
|
|
||||||
},
|
|
||||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx',
|
|
||||||
start: '2017-02-28T05:00:00Z',
|
|
||||||
end: '2017-05-30T23:00:00Z',
|
|
||||||
enrollment_start: '2017-01-18T00:00:00Z',
|
|
||||||
enrollment_end: null,
|
|
||||||
type: 'verified',
|
|
||||||
certificate_url: '',
|
|
||||||
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017',
|
|
||||||
enrollment_open_date: 'Jan 18, 2016',
|
|
||||||
is_course_ended: false,
|
|
||||||
is_enrolled: false,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
status: 'published'
|
|
||||||
}];
|
|
||||||
});
|
|
||||||
|
|
||||||
setupView = function(courseRuns, urlMap) {
|
|
||||||
course.course_runs = courseRuns;
|
|
||||||
setFixtures('<div class="course-actions"></div>');
|
|
||||||
courseCardModel = new CourseCardModel(course);
|
|
||||||
courseEnrollModel = new CourseEnrollModel({}, {
|
|
||||||
courseId: courseCardModel.get('course_run_key')
|
|
||||||
});
|
|
||||||
if (urlMap) {
|
|
||||||
urlModel = new Backbone.Model(urlMap);
|
|
||||||
}
|
|
||||||
view = new CourseEnrollView({
|
|
||||||
$parentEl: $('.course-actions'),
|
|
||||||
model: courseCardModel,
|
|
||||||
enrollModel: courseEnrollModel,
|
|
||||||
urlModel: urlModel
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
urlModel = null;
|
|
||||||
courseCardModel = null;
|
|
||||||
courseEnrollModel = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
setupView(singleCourseRunList);
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the course enroll view when not enrolled', function() {
|
|
||||||
setupView(singleCourseRunList);
|
|
||||||
expect(view.$('.enroll-button').text().trim()).toEqual('Enroll Now');
|
|
||||||
expect(view.$('.run-select').length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the course enroll view when enrolled', function() {
|
|
||||||
singleCourseRunList[0].is_enrolled = true;
|
|
||||||
|
|
||||||
setupView(singleCourseRunList);
|
|
||||||
expect(view.$('.view-course-button').text().trim()).toEqual('View Course');
|
|
||||||
expect(view.$('.run-select').length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not render anything if course runs are empty', function() {
|
|
||||||
setupView([]);
|
|
||||||
|
|
||||||
expect(view.$('.run-select').length).toBe(0);
|
|
||||||
expect(view.$('.enroll-button').length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render run selection dropdown if multiple course runs are available', function() {
|
|
||||||
setupView(multiCourseRunList);
|
|
||||||
|
|
||||||
expect(view.$('.run-select').length).toBe(1);
|
|
||||||
expect(view.$('.run-select').val()).toEqual(multiCourseRunList[0].key);
|
|
||||||
expect(view.$('.run-select option').length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow enrollment in unpublished course runs', function() {
|
|
||||||
multiCourseRunList[0].status = 'unpublished';
|
|
||||||
|
|
||||||
setupView(multiCourseRunList);
|
|
||||||
expect(view.$('.run-select').length).toBe(0);
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow enrollment in course runs with a null status', function() {
|
|
||||||
multiCourseRunList[0].status = null;
|
|
||||||
|
|
||||||
setupView(multiCourseRunList);
|
|
||||||
expect(view.$('.run-select').length).toBe(0);
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should enroll learner when enroll button is clicked with one course run available', function() {
|
|
||||||
setupView(singleCourseRunList);
|
|
||||||
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
|
|
||||||
spyOn(courseEnrollModel, 'save');
|
|
||||||
|
|
||||||
view.$('.enroll-button').click();
|
|
||||||
|
|
||||||
expect(courseEnrollModel.save).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should enroll learner when enroll button is clicked with multiple course runs available', function() {
|
|
||||||
setupView(multiCourseRunList);
|
|
||||||
|
|
||||||
spyOn(courseEnrollModel, 'save');
|
|
||||||
|
|
||||||
view.$('.run-select').val(multiCourseRunList[1].key);
|
|
||||||
view.$('.run-select').trigger('change');
|
|
||||||
view.$('.enroll-button').click();
|
|
||||||
|
|
||||||
expect(courseEnrollModel.save).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to track selection when audit enrollment succeeds', function() {
|
|
||||||
singleCourseRunList[0].is_enrolled = false;
|
|
||||||
singleCourseRunList[0].mode_slug = 'audit';
|
|
||||||
|
|
||||||
setupView(singleCourseRunList, urls);
|
|
||||||
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
expect(view.trackSelectionUrl).toBeDefined();
|
|
||||||
|
|
||||||
spyOn(view, 'redirect');
|
|
||||||
|
|
||||||
view.enrollSuccess();
|
|
||||||
|
|
||||||
expect(view.redirect).toHaveBeenCalledWith(
|
|
||||||
view.trackSelectionUrl + courseCardModel.get('course_run_key'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to track selection when enrollment in an unspecified mode is attempted', function() {
|
|
||||||
singleCourseRunList[0].is_enrolled = false;
|
|
||||||
singleCourseRunList[0].mode_slug = null;
|
|
||||||
|
|
||||||
setupView(singleCourseRunList, urls);
|
|
||||||
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
expect(view.trackSelectionUrl).toBeDefined();
|
|
||||||
|
|
||||||
spyOn(view, 'redirect');
|
|
||||||
|
|
||||||
view.enrollSuccess();
|
|
||||||
|
|
||||||
expect(view.redirect).toHaveBeenCalledWith(
|
|
||||||
view.trackSelectionUrl + courseCardModel.get('course_run_key')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect when urls are not provided', function() {
|
|
||||||
singleCourseRunList[0].is_enrolled = false;
|
|
||||||
singleCourseRunList[0].mode_slug = 'verified';
|
|
||||||
|
|
||||||
setupView(singleCourseRunList);
|
|
||||||
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
expect(view.verificationUrl).not.toBeDefined();
|
|
||||||
expect(view.dashboardUrl).not.toBeDefined();
|
|
||||||
expect(view.trackSelectionUrl).not.toBeDefined();
|
|
||||||
|
|
||||||
spyOn(view, 'redirect');
|
|
||||||
|
|
||||||
view.enrollSuccess();
|
|
||||||
|
|
||||||
expect(view.redirect).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to track selection on error', function() {
|
|
||||||
setupView(singleCourseRunList, urls);
|
|
||||||
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
expect(view.trackSelectionUrl).toBeDefined();
|
|
||||||
|
|
||||||
spyOn(view, 'redirect');
|
|
||||||
|
|
||||||
view.enrollError(courseEnrollModel, {status: 500});
|
|
||||||
expect(view.redirect).toHaveBeenCalledWith(
|
|
||||||
view.trackSelectionUrl + courseCardModel.get('course_run_key')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to login on 403 error', function() {
|
|
||||||
var response = {
|
|
||||||
status: 403,
|
|
||||||
responseJSON: {
|
|
||||||
user_message_url: 'redirect/to/this'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setupView(singleCourseRunList, urls);
|
|
||||||
|
|
||||||
expect(view.$('.enroll-button').length).toBe(1);
|
|
||||||
expect(view.trackSelectionUrl).toBeDefined();
|
|
||||||
|
|
||||||
spyOn(view, 'redirect');
|
|
||||||
|
|
||||||
view.enrollError(courseEnrollModel, response);
|
|
||||||
|
|
||||||
expect(view.redirect).toHaveBeenCalledWith(
|
|
||||||
response.responseJSON.user_message_url
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sends analytics event when enrollment succeeds', function() {
|
|
||||||
setupView(singleCourseRunList, urls);
|
|
||||||
spyOn(view, 'redirect');
|
|
||||||
view.enrollSuccess();
|
|
||||||
expect(window.analytics.track).toHaveBeenCalledWith(
|
|
||||||
'edx.bi.user.program-details.enrollment'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'underscore',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/models/course_entitlement_model',
|
|
||||||
'js/learner_dashboard/views/course_entitlement_view'
|
|
||||||
], function(Backbone, _, $, CourseEntitlementModel, CourseEntitlementView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Course Entitlement View', function() {
|
|
||||||
var view = null,
|
|
||||||
setupView,
|
|
||||||
sessionIndex,
|
|
||||||
selectOptions,
|
|
||||||
entitlementAvailableSessions,
|
|
||||||
initialSessionId,
|
|
||||||
alreadyEnrolled,
|
|
||||||
hasSessions,
|
|
||||||
entitlementUUID = 'a9aiuw76a4ijs43u18',
|
|
||||||
testSessionIds = ['test_session_id_1', 'test_session_id_2'];
|
|
||||||
|
|
||||||
setupView = function(isAlreadyEnrolled, hasAvailableSessions, specificSessionIndex) {
|
|
||||||
setFixtures('<div class="course-entitlement-selection-container"></div>');
|
|
||||||
alreadyEnrolled = (typeof isAlreadyEnrolled !== 'undefined') ? isAlreadyEnrolled : true;
|
|
||||||
hasSessions = (typeof hasAvailableSessions !== 'undefined') ? hasAvailableSessions : true;
|
|
||||||
sessionIndex = (typeof specificSessionIndex !== 'undefined') ? specificSessionIndex : 0;
|
|
||||||
|
|
||||||
initialSessionId = alreadyEnrolled ? testSessionIds[sessionIndex] : '';
|
|
||||||
entitlementAvailableSessions = [];
|
|
||||||
if (hasSessions) {
|
|
||||||
entitlementAvailableSessions = [{
|
|
||||||
enrollment_end: null,
|
|
||||||
start: '2016-02-05T05:00:00+00:00',
|
|
||||||
pacing_type: 'instructor_paced',
|
|
||||||
session_id: testSessionIds[0],
|
|
||||||
end: null
|
|
||||||
}, {
|
|
||||||
enrollment_end: '2019-12-22T03:30:00Z',
|
|
||||||
start: '2020-01-03T13:00:00+00:00',
|
|
||||||
pacing_type: 'self_paced',
|
|
||||||
session_id: testSessionIds[1],
|
|
||||||
end: '2020-03-09T21:30:00+00:00'
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
view = new CourseEntitlementView({
|
|
||||||
el: '.course-entitlement-selection-container',
|
|
||||||
triggerOpenBtn: '#course-card-0 .change-session',
|
|
||||||
courseCardMessages: '#course-card-0 .messages-list > .message',
|
|
||||||
courseTitleLink: '#course-card-0 .course-title a',
|
|
||||||
courseImageLink: '#course-card-0 .wrapper-course-image > a',
|
|
||||||
dateDisplayField: '#course-card-0 .info-date-block',
|
|
||||||
enterCourseBtn: '#course-card-0 .enter-course',
|
|
||||||
availableSessions: JSON.stringify(entitlementAvailableSessions),
|
|
||||||
entitlementUUID: entitlementUUID,
|
|
||||||
currentSessionId: initialSessionId,
|
|
||||||
userId: '1',
|
|
||||||
enrollUrl: '/api/enrollment/v1/enrollment',
|
|
||||||
courseHomeUrl: '/courses/course-v1:edX+DemoX+Demo_Course/course/'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
if (view) view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Initialization of view', function() {
|
|
||||||
it('Should create a entitlement view element', function() {
|
|
||||||
setupView(false);
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Available Sessions Select - Unfulfilled Entitlement', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
setupView(false);
|
|
||||||
selectOptions = view.$('.session-select').find('option');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Select session dropdown should show all available course runs and a coming soon option.', function() {
|
|
||||||
expect(selectOptions.length).toEqual(entitlementAvailableSessions.length + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Self paced courses should have visual indication in the selection option.', function() {
|
|
||||||
var selfPacedOptionIndex = _.findIndex(entitlementAvailableSessions, function(session) {
|
|
||||||
return session.pacing_type === 'self_paced';
|
|
||||||
});
|
|
||||||
var selfPacedOption = selectOptions[selfPacedOptionIndex];
|
|
||||||
expect(selfPacedOption && selfPacedOption.text.includes('(Self-paced)')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Courses with an an enroll by date should indicate so on the selection option.', function() {
|
|
||||||
var enrollEndSetOptionIndex = _.findIndex(entitlementAvailableSessions, function(session) {
|
|
||||||
return session.enrollment_end !== null;
|
|
||||||
});
|
|
||||||
var enrollEndSetOption = selectOptions[enrollEndSetOptionIndex];
|
|
||||||
expect(enrollEndSetOption && enrollEndSetOption.text.includes('Open until')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Title element should correctly indicate the expected behavior.', function() {
|
|
||||||
expect(view.$('.action-header').text().includes(
|
|
||||||
'To access the course, select a session.'
|
|
||||||
)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Available Sessions Select - Unfulfilled Entitlement without available sessions', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
setupView(false, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should notify user that more sessions are coming soon if none available.', function() {
|
|
||||||
expect(view.$('.action-header').text().includes('More sessions coming soon.')).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Available Sessions Select - Fulfilled Entitlement', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
setupView(true);
|
|
||||||
selectOptions = view.$('.session-select').find('option');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Select session dropdown should show available course runs, coming soon and leave options.', function() {
|
|
||||||
expect(selectOptions.length).toEqual(entitlementAvailableSessions.length + 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Select session dropdown should allow user to leave the current session.', function() {
|
|
||||||
var leaveSessionOption = selectOptions[selectOptions.length - 1];
|
|
||||||
expect(leaveSessionOption.text.includes('Leave the current session and decide later')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Currently selected session should be specified in the dropdown options.', function() {
|
|
||||||
var selectedSessionIndex = _.findIndex(entitlementAvailableSessions, function(session) {
|
|
||||||
return initialSessionId === session.session_id;
|
|
||||||
});
|
|
||||||
expect(selectOptions[selectedSessionIndex].text.includes('Currently Selected')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Title element should correctly indicate the expected behavior.', function() {
|
|
||||||
expect(view.$('.action-header').text().includes(
|
|
||||||
'Change to a different session or leave the current session.'
|
|
||||||
)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Available Sessions Select - Fulfilled Entitlement (session in the future)', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
setupView(true, true, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Currently selected session should initialize to selected in the dropdown options.', function() {
|
|
||||||
var selectedOption = view.$('.session-select').find('option:selected');
|
|
||||||
expect(selectedOption.data('session_id')).toEqual(testSessionIds[1]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Select Session Action Button and popover behavior - Unfulfilled Entitlement', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
setupView(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Change session button should have the correct text.', function() {
|
|
||||||
expect(view.$('.enroll-btn-initial').text() === 'Select Session').toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Select session button should show popover when clicked.', function() {
|
|
||||||
view.$('.enroll-btn-initial').click();
|
|
||||||
expect(view.$('.verification-modal').length > 0).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Change Session Action Button and popover behavior - Fulfilled Entitlement', function() {
|
|
||||||
beforeEach(function() {
|
|
||||||
setupView(true);
|
|
||||||
selectOptions = view.$('.session-select').find('option');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Change session button should show correct text.', function() {
|
|
||||||
expect(view.$('.enroll-btn-initial').text().trim() === 'Change Session').toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Switch session button should be disabled when on the currently enrolled session.', function() {
|
|
||||||
expect(view.$('.enroll-btn-initial')).toHaveClass('disabled');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/views/entitlement_unenrollment_view'
|
|
||||||
], function(Backbone, $, EntitlementUnenrollmentView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('EntitlementUnenrollmentView', function() {
|
|
||||||
var view = null,
|
|
||||||
options = {
|
|
||||||
dashboardPath: '/dashboard',
|
|
||||||
signInPath: '/login'
|
|
||||||
},
|
|
||||||
|
|
||||||
initView = function() {
|
|
||||||
return new EntitlementUnenrollmentView(options);
|
|
||||||
},
|
|
||||||
|
|
||||||
modalHtml = '<a id="link1" class="js-entitlement-action-unenroll" ' +
|
|
||||||
' data-course-name="Test Course 1" ' +
|
|
||||||
' data-course-number="test1" ' +
|
|
||||||
' data-entitlement-api-endpoint="/test/api/endpoint/1">Unenroll</a> ' +
|
|
||||||
'<a id="link2" class="js-entitlement-action-unenroll" ' +
|
|
||||||
' data-course-name="Test Course 2" ' +
|
|
||||||
' data-course-number="test2" ' +
|
|
||||||
' data-entitlement-api-endpoint="/test/api/endpoint/2">Unenroll</a> ' +
|
|
||||||
'<div class="js-entitlement-unenrollment-modal"> ' +
|
|
||||||
' <span class="js-entitlement-unenrollment-modal-header-text"></span> ' +
|
|
||||||
' <span class="js-entitlement-unenrollment-modal-error-text"></span> ' +
|
|
||||||
' <button class="js-entitlement-unenrollment-modal-submit">Unenroll</button> ' +
|
|
||||||
'</div> ';
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
setFixtures(modalHtml);
|
|
||||||
view = initView();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when an unenroll link is clicked', function() {
|
|
||||||
it('should reset the modal and set the correct values for header/submit', function() {
|
|
||||||
var $link1 = $('#link1'),
|
|
||||||
$link2 = $('#link2'),
|
|
||||||
$headerTxt = $('.js-entitlement-unenrollment-modal-header-text'),
|
|
||||||
$errorTxt = $('.js-entitlement-unenrollment-modal-error-text'),
|
|
||||||
$submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
|
||||||
|
|
||||||
$link1.trigger('click');
|
|
||||||
expect($headerTxt.html().startsWith('Are you sure you want to unenroll from Test Course 1')).toBe(true);
|
|
||||||
expect($submitBtn.data()).toEqual({entitlementApiEndpoint: '/test/api/endpoint/1'});
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(false);
|
|
||||||
expect($errorTxt.html()).toEqual('');
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
|
||||||
|
|
||||||
// Set an error so that we can see that the modal is reset properly when clicked again
|
|
||||||
view.setError('This is an error');
|
|
||||||
expect($errorTxt.html()).toEqual('This is an error');
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(true);
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(true);
|
|
||||||
|
|
||||||
$link2.trigger('click');
|
|
||||||
expect($headerTxt.html().startsWith('Are you sure you want to unenroll from Test Course 2')).toBe(true);
|
|
||||||
expect($submitBtn.data()).toEqual({entitlementApiEndpoint: '/test/api/endpoint/2'});
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(false);
|
|
||||||
expect($errorTxt.html()).toEqual('');
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the unenroll submit button is clicked', function() {
|
|
||||||
it('should send a DELETE request to the configured apiEndpoint', function() {
|
|
||||||
var $submitBtn = $('.js-entitlement-unenrollment-modal-submit'),
|
|
||||||
apiEndpoint = '/test/api/endpoint/1';
|
|
||||||
|
|
||||||
view.setSubmitData(apiEndpoint);
|
|
||||||
|
|
||||||
spyOn($, 'ajax').and.callFake(function(opts) {
|
|
||||||
expect(opts.url).toEqual(apiEndpoint);
|
|
||||||
expect(opts.method).toEqual('DELETE');
|
|
||||||
expect(opts.complete).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
$submitBtn.trigger('click');
|
|
||||||
expect($.ajax).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set an error and disable submit if the apiEndpoint has not been properly set', function() {
|
|
||||||
var $errorTxt = $('.js-entitlement-unenrollment-modal-error-text'),
|
|
||||||
$submitBtn = $('.js-entitlement-unenrollment-modal-submit');
|
|
||||||
|
|
||||||
expect($submitBtn.data()).toEqual({});
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(false);
|
|
||||||
expect($errorTxt.html()).toEqual('');
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
|
||||||
|
|
||||||
spyOn($, 'ajax');
|
|
||||||
$submitBtn.trigger('click');
|
|
||||||
expect($.ajax).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect($submitBtn.data()).toEqual({});
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(true);
|
|
||||||
expect($errorTxt.html()).toEqual(view.genericErrorMsg);
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the unenroll request is complete', function() {
|
|
||||||
it('should redirect to the dashboard if the request was successful', function() {
|
|
||||||
var $submitBtn = $('.js-entitlement-unenrollment-modal-submit'),
|
|
||||||
apiEndpoint = '/test/api/endpoint/1';
|
|
||||||
|
|
||||||
view.setSubmitData(apiEndpoint);
|
|
||||||
|
|
||||||
spyOn($, 'ajax').and.callFake(function(opts) {
|
|
||||||
expect(opts.url).toEqual(apiEndpoint);
|
|
||||||
expect(opts.method).toEqual('DELETE');
|
|
||||||
expect(opts.complete).toBeTruthy();
|
|
||||||
|
|
||||||
opts.complete({
|
|
||||||
status: 204,
|
|
||||||
responseJSON: {detail: 'success'}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
spyOn(view, 'redirectTo');
|
|
||||||
|
|
||||||
$submitBtn.trigger('click');
|
|
||||||
expect($.ajax).toHaveBeenCalled();
|
|
||||||
expect(view.redirectTo).toHaveBeenCalledWith(view.dashboardPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the login page if the request failed with an auth error', function() {
|
|
||||||
var $submitBtn = $('.js-entitlement-unenrollment-modal-submit'),
|
|
||||||
apiEndpoint = '/test/api/endpoint/1';
|
|
||||||
|
|
||||||
view.setSubmitData(apiEndpoint);
|
|
||||||
|
|
||||||
spyOn($, 'ajax').and.callFake(function(opts) {
|
|
||||||
expect(opts.url).toEqual(apiEndpoint);
|
|
||||||
expect(opts.method).toEqual('DELETE');
|
|
||||||
expect(opts.complete).toBeTruthy();
|
|
||||||
|
|
||||||
opts.complete({
|
|
||||||
status: 401,
|
|
||||||
responseJSON: {detail: 'Authentication credentials were not provided.'}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
spyOn(view, 'redirectTo');
|
|
||||||
|
|
||||||
$submitBtn.trigger('click');
|
|
||||||
expect($.ajax).toHaveBeenCalled();
|
|
||||||
expect(view.redirectTo).toHaveBeenCalledWith(
|
|
||||||
view.signInPath + '?next=' + encodeURIComponent(view.dashboardPath)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set an error and disable submit if a non-auth error occurs', function() {
|
|
||||||
var $errorTxt = $('.js-entitlement-unenrollment-modal-error-text'),
|
|
||||||
$submitBtn = $('.js-entitlement-unenrollment-modal-submit'),
|
|
||||||
apiEndpoint = '/test/api/endpoint/1';
|
|
||||||
|
|
||||||
view.setSubmitData(apiEndpoint);
|
|
||||||
|
|
||||||
spyOn($, 'ajax').and.callFake(function(opts) {
|
|
||||||
expect(opts.url).toEqual(apiEndpoint);
|
|
||||||
expect(opts.method).toEqual('DELETE');
|
|
||||||
expect(opts.complete).toBeTruthy();
|
|
||||||
|
|
||||||
opts.complete({
|
|
||||||
status: 400,
|
|
||||||
responseJSON: {detail: 'Bad request.'}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
spyOn(view, 'redirectTo');
|
|
||||||
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(false);
|
|
||||||
expect($errorTxt.html()).toEqual('');
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(false);
|
|
||||||
|
|
||||||
$submitBtn.trigger('click');
|
|
||||||
|
|
||||||
expect($submitBtn.prop('disabled')).toBe(true);
|
|
||||||
expect($errorTxt.html()).toEqual(view.genericErrorMsg);
|
|
||||||
expect($errorTxt.hasClass('entitlement-unenrollment-modal-error-text-visible')).toBe(true);
|
|
||||||
|
|
||||||
expect($.ajax).toHaveBeenCalled();
|
|
||||||
expect(view.redirectTo).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'underscore',
|
|
||||||
'jquery',
|
|
||||||
'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 */
|
|
||||||
|
|
||||||
describe('Program card View', function() {
|
|
||||||
var view = null,
|
|
||||||
programModel,
|
|
||||||
program = {
|
|
||||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
|
||||||
title: 'Food Security and Sustainability',
|
|
||||||
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
|
||||||
type: 'XSeries',
|
|
||||||
detail_url: 'https://www.edx.org/foo/bar',
|
|
||||||
banner_image: {
|
|
||||||
medium: {
|
|
||||||
height: 242,
|
|
||||||
width: 726,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
|
|
||||||
},
|
|
||||||
'x-small': {
|
|
||||||
height: 116,
|
|
||||||
width: 348,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
height: 145,
|
|
||||||
width: 435,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
height: 480,
|
|
||||||
width: 1440,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authoring_organizations: [
|
|
||||||
{
|
|
||||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
|
||||||
key: 'WageningenX',
|
|
||||||
name: 'Wageningen University & Research'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
userProgress = [
|
|
||||||
{
|
|
||||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
|
||||||
completed: 4,
|
|
||||||
in_progress: 2,
|
|
||||||
not_started: 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
|
||||||
completed: 1,
|
|
||||||
in_progress: 0,
|
|
||||||
not_started: 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
progressCollection = new ProgressCollection(),
|
|
||||||
cardRenders = function($card) {
|
|
||||||
expect($card).toBeDefined();
|
|
||||||
expect($card.find('.title').html().trim()).toEqual(program.title);
|
|
||||||
expect($card.find('.category span').html().trim()).toEqual(program.type);
|
|
||||||
expect($card.find('.organization').html().trim()).toEqual(program.authoring_organizations[0].key);
|
|
||||||
expect($card.find('.card-link').attr('href')).toEqual(program.detail_url);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
setFixtures('<div class="program-card"></div>');
|
|
||||||
programModel = new ProgramModel(program);
|
|
||||||
progressCollection.set(userProgress);
|
|
||||||
view = new ProgramCardView({
|
|
||||||
model: programModel,
|
|
||||||
context: {
|
|
||||||
progressCollection: progressCollection
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load the program-card based on passed in context', function() {
|
|
||||||
cardRenders(view.$el);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call reEvaluatePicture if reLoadBannerImage is called', function() {
|
|
||||||
spyOn(view, 'reEvaluatePicture');
|
|
||||||
view.reLoadBannerImage();
|
|
||||||
expect(view.reEvaluatePicture).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle exceptions from reEvaluatePicture', function() {
|
|
||||||
var message = 'Picturefill had exceptions';
|
|
||||||
|
|
||||||
spyOn(view, 'reEvaluatePicture').and.callFake(function() {
|
|
||||||
var error = {name: message};
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
view.reLoadBannerImage();
|
|
||||||
expect(view.reEvaluatePicture).toHaveBeenCalled();
|
|
||||||
expect(view.reLoadBannerImage).not.toThrow(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show the right number of progress bar segments', function() {
|
|
||||||
expect(view.$('.progress-bar .completed').length).toEqual(4);
|
|
||||||
expect(view.$('.progress-bar .enrolled').length).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display the correct course status numbers', function() {
|
|
||||||
expect(view.$('.number-circle').text()).toEqual('424');
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/views/program_header_view'
|
|
||||||
], function(Backbone, $, ProgramHeaderView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Program Details Header View', function() {
|
|
||||||
var view = null,
|
|
||||||
context = {
|
|
||||||
programData: {
|
|
||||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
|
||||||
title: 'Food Security and Sustainability',
|
|
||||||
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
|
||||||
type: 'XSeries',
|
|
||||||
detail_url: 'https://www.edx.org/foo/bar',
|
|
||||||
banner_image: {
|
|
||||||
medium: {
|
|
||||||
height: 242,
|
|
||||||
width: 726,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
|
|
||||||
},
|
|
||||||
'x-small': {
|
|
||||||
height: 116,
|
|
||||||
width: 348,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
height: 145,
|
|
||||||
width: 435,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
height: 480,
|
|
||||||
width: 1440,
|
|
||||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authoring_organizations: [
|
|
||||||
{
|
|
||||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
|
||||||
key: 'WageningenX',
|
|
||||||
name: 'Wageningen University & Research',
|
|
||||||
certificate_logo_image_url: 'https://example.com/org-certificate-logo.jpg',
|
|
||||||
logo_image_url: 'https://example.com/org-logo.jpg'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
setFixtures('<div class="js-program-header"></div>');
|
|
||||||
view = new ProgramHeaderView({
|
|
||||||
model: new Backbone.Model(context)
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the header based on the passed in model', function() {
|
|
||||||
expect(view.$('.program-title').html()).toEqual(context.programData.title);
|
|
||||||
expect(view.$('.org-logo').length).toEqual(context.programData.authoring_organizations.length);
|
|
||||||
expect(view.$('.org-logo').attr('src'))
|
|
||||||
.toEqual(context.programData.authoring_organizations[0].certificate_logo_image_url);
|
|
||||||
expect(view.$('.org-logo').attr('alt'))
|
|
||||||
.toEqual(context.programData.authoring_organizations[0].name + '\'s logo');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,632 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/views/program_details_view'
|
|
||||||
], function(Backbone, $, ProgramDetailsView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Program Details Header View', function() {
|
|
||||||
var view = null,
|
|
||||||
options = {
|
|
||||||
programData: {
|
|
||||||
subtitle: '',
|
|
||||||
overview: '',
|
|
||||||
weeks_to_complete: null,
|
|
||||||
corporate_endorsements: [],
|
|
||||||
video: null,
|
|
||||||
type: 'Test',
|
|
||||||
max_hours_effort_per_week: null,
|
|
||||||
transcript_languages: [
|
|
||||||
'en-us'
|
|
||||||
],
|
|
||||||
expected_learning_items: [],
|
|
||||||
uuid: '0ffff5d6-0177-4690-9a48-aa2fecf94610',
|
|
||||||
title: 'Test Course Title',
|
|
||||||
languages: [
|
|
||||||
'en-us'
|
|
||||||
],
|
|
||||||
subjects: [],
|
|
||||||
individual_endorsements: [],
|
|
||||||
staff: [
|
|
||||||
{
|
|
||||||
family_name: 'Tester',
|
|
||||||
uuid: '11ee1afb-5750-4185-8434-c9ae8297f0f1',
|
|
||||||
bio: 'Dr. Tester, PhD, RD, is an Associate Professor at the School of Nutrition.',
|
|
||||||
profile_image: {},
|
|
||||||
profile_image_url: 'some image',
|
|
||||||
given_name: 'Bob',
|
|
||||||
urls: {
|
|
||||||
blog: null,
|
|
||||||
twitter: null,
|
|
||||||
facebook: null
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
organization_name: 'Test University',
|
|
||||||
title: 'Associate Professor of Nutrition'
|
|
||||||
},
|
|
||||||
works: [],
|
|
||||||
slug: 'dr-tester'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
marketing_slug: 'testing',
|
|
||||||
marketing_url: 'someurl',
|
|
||||||
status: 'active',
|
|
||||||
credit_redemption_overview: '',
|
|
||||||
discount_data: {
|
|
||||||
currency: 'USD',
|
|
||||||
discount_value: 0,
|
|
||||||
is_discounted: false,
|
|
||||||
total_incl_tax: 300,
|
|
||||||
total_incl_tax_excl_discounts: 300
|
|
||||||
},
|
|
||||||
full_program_price: 300,
|
|
||||||
card_image_url: 'some image',
|
|
||||||
faq: [],
|
|
||||||
price_ranges: [
|
|
||||||
{
|
|
||||||
max: 378,
|
|
||||||
total: 109,
|
|
||||||
min: 10,
|
|
||||||
currency: 'USD'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
banner_image: {
|
|
||||||
large: {
|
|
||||||
url: 'someurl',
|
|
||||||
width: 1440,
|
|
||||||
height: 480
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
url: 'someurl',
|
|
||||||
width: 435,
|
|
||||||
height: 145
|
|
||||||
},
|
|
||||||
medium: {
|
|
||||||
url: 'someurl',
|
|
||||||
width: 726,
|
|
||||||
height: 242
|
|
||||||
},
|
|
||||||
'x-small': {
|
|
||||||
url: 'someurl',
|
|
||||||
width: 348,
|
|
||||||
height: 116
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authoring_organizations: [
|
|
||||||
{
|
|
||||||
description: '<p>Learning University is home to leading creators, entrepreneurs.</p>',
|
|
||||||
tags: [
|
|
||||||
'contributor'
|
|
||||||
],
|
|
||||||
name: 'Learning University',
|
|
||||||
homepage_url: null,
|
|
||||||
key: 'LearnX',
|
|
||||||
certificate_logo_image_url: null,
|
|
||||||
marketing_url: 'someurl',
|
|
||||||
logo_image_url: 'https://stage.edx.org/sites/default/files/school/image/logo/learnx.png',
|
|
||||||
uuid: 'de3e9ff0-477d-4496-8cfa-a98f902e5830'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '<p>The Test University was chartered in 1868.</p>',
|
|
||||||
tags: [
|
|
||||||
'charter',
|
|
||||||
'contributor'
|
|
||||||
],
|
|
||||||
name: 'Test University',
|
|
||||||
homepage_url: null,
|
|
||||||
key: 'TestX',
|
|
||||||
certificate_logo_image_url: null,
|
|
||||||
marketing_url: 'someurl',
|
|
||||||
logo_image_url: 'https://stage.edx.org/sites/default/files/school/image/logo/ritx.png',
|
|
||||||
uuid: '54bc81cb-b736-4505-aa51-dd2b18c61d84'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
job_outlook_items: [],
|
|
||||||
credit_backing_organizations: [],
|
|
||||||
weeks_to_complete_min: 8,
|
|
||||||
weeks_to_complete_max: 8,
|
|
||||||
min_hours_effort_per_week: null,
|
|
||||||
is_learner_eligible_for_one_click_purchase: false
|
|
||||||
},
|
|
||||||
courseData: {
|
|
||||||
completed: [
|
|
||||||
{
|
|
||||||
owners: [
|
|
||||||
{
|
|
||||||
uuid: '766a3716-f962-425b-b56e-e214c019b229',
|
|
||||||
key: 'Testx',
|
|
||||||
name: 'Test University'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
uuid: '4be8dceb-3454-4fbf-8993-17d563ab41d4',
|
|
||||||
title: 'Who let the dogs out',
|
|
||||||
image: null,
|
|
||||||
key: 'Testx+DOGx002',
|
|
||||||
course_runs: [
|
|
||||||
{
|
|
||||||
upgrade_url: null,
|
|
||||||
image: {
|
|
||||||
src: 'someurl',
|
|
||||||
width: null,
|
|
||||||
description: null,
|
|
||||||
height: null
|
|
||||||
},
|
|
||||||
max_effort: null,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
course: 'Testx+DOGx002',
|
|
||||||
content_language: null,
|
|
||||||
eligible_for_financial_aid: true,
|
|
||||||
seats: [
|
|
||||||
{
|
|
||||||
sku: '4250900',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '89.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: '',
|
|
||||||
type: 'verified'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
course_url: '/courses/course-v1:Testx+DOGx002+1T2016/',
|
|
||||||
availability: 'Archived',
|
|
||||||
transcript_languages: [],
|
|
||||||
staff: [],
|
|
||||||
announcement: null,
|
|
||||||
end: '2016-10-01T23:59:00Z',
|
|
||||||
uuid: 'f0ac45f5-f0d6-44bc-aeb9-a14e36e963a5',
|
|
||||||
title: 'Who let the dogs out',
|
|
||||||
certificate_url: '/certificates/1730700d89434b718d0d91f8b5d339bf',
|
|
||||||
enrollment_start: null,
|
|
||||||
start: '2017-03-21T22:18:15Z',
|
|
||||||
min_effort: null,
|
|
||||||
short_description: null,
|
|
||||||
hidden: false,
|
|
||||||
level_type: null,
|
|
||||||
type: 'verified',
|
|
||||||
enrollment_open_date: 'Jan 01, 1900',
|
|
||||||
marketing_url: null,
|
|
||||||
is_course_ended: false,
|
|
||||||
instructors: [],
|
|
||||||
full_description: null,
|
|
||||||
key: 'course-v1:Testx+DOGx002+1T2016',
|
|
||||||
enrollment_end: null,
|
|
||||||
reporting_type: 'mooc',
|
|
||||||
advertised_start: null,
|
|
||||||
mobile_available: false,
|
|
||||||
modified: '2017-03-24T14:22:15.609907Z',
|
|
||||||
is_enrolled: true,
|
|
||||||
pacing_type: 'self_paced',
|
|
||||||
video: null,
|
|
||||||
status: 'published'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
in_progress: [
|
|
||||||
{
|
|
||||||
owners: [
|
|
||||||
{
|
|
||||||
uuid: 'c484a523-d396-4aff-90f4-bb7e82e16bf6',
|
|
||||||
key: 'LearnX',
|
|
||||||
name: 'Learning University'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
uuid: '872ec14c-3b7d-44b8-9cf2-9fa62182e1dd',
|
|
||||||
title: 'Star Trek: The Next Generation',
|
|
||||||
image: null,
|
|
||||||
key: 'LearnX+NGIx',
|
|
||||||
course_runs: [
|
|
||||||
{
|
|
||||||
upgrade_url: 'someurl',
|
|
||||||
image: {
|
|
||||||
src: '',
|
|
||||||
width: null,
|
|
||||||
description: null,
|
|
||||||
height: null
|
|
||||||
},
|
|
||||||
max_effort: null,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
course: 'LearnX+NGx',
|
|
||||||
content_language: null,
|
|
||||||
eligible_for_financial_aid: true,
|
|
||||||
seats: [
|
|
||||||
{
|
|
||||||
sku: '44EEB26',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '0.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: null,
|
|
||||||
type: 'audit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sku: '64AAFBA',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '10.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: '2017-04-29T00:00:00Z',
|
|
||||||
credit_provider: null,
|
|
||||||
type: 'verified'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
course_url: 'someurl',
|
|
||||||
availability: 'Current',
|
|
||||||
transcript_languages: [],
|
|
||||||
staff: [],
|
|
||||||
announcement: null,
|
|
||||||
end: '2017-03-31T12:00:00Z',
|
|
||||||
uuid: 'ce841f5b-f5a9-428f-b187-e6372b532266',
|
|
||||||
title: 'Star Trek: The Next Generation',
|
|
||||||
certificate_url: null,
|
|
||||||
enrollment_start: '2014-03-31T20:00:00Z',
|
|
||||||
start: '2017-03-20T20:50:14Z',
|
|
||||||
min_effort: null,
|
|
||||||
short_description: null,
|
|
||||||
hidden: false,
|
|
||||||
level_type: null,
|
|
||||||
type: 'verified',
|
|
||||||
enrollment_open_date: 'Jan 01, 1900',
|
|
||||||
marketing_url: 'someurl',
|
|
||||||
is_course_ended: false,
|
|
||||||
instructors: [],
|
|
||||||
full_description: null,
|
|
||||||
key: 'course-v1:LearnX+NGIx+3T2016',
|
|
||||||
enrollment_end: null,
|
|
||||||
reporting_type: 'mooc',
|
|
||||||
advertised_start: null,
|
|
||||||
mobile_available: false,
|
|
||||||
modified: '2017-03-24T14:16:47.547643Z',
|
|
||||||
is_enrolled: true,
|
|
||||||
pacing_type: 'instructor_paced',
|
|
||||||
video: null,
|
|
||||||
status: 'published'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
uuid: '0ffff5d6-0177-4690-9a48-aa2fecf94610',
|
|
||||||
not_started: [
|
|
||||||
{
|
|
||||||
owners: [
|
|
||||||
{
|
|
||||||
uuid: '766a3716-f962-425b-b56e-e214c019b229',
|
|
||||||
key: 'Testx',
|
|
||||||
name: 'Test University'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
uuid: '88da08e4-e9ef-406e-95d7-7a178f9f9695',
|
|
||||||
title: 'Introduction to Health and Wellness',
|
|
||||||
image: null,
|
|
||||||
key: 'Testx+EXW100x',
|
|
||||||
course_runs: [
|
|
||||||
{
|
|
||||||
upgrade_url: null,
|
|
||||||
image: {
|
|
||||||
src: 'someurl',
|
|
||||||
width: null,
|
|
||||||
description: null,
|
|
||||||
height: null
|
|
||||||
},
|
|
||||||
max_effort: null,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
course: 'Testx+EXW100x',
|
|
||||||
content_language: 'en-us',
|
|
||||||
eligible_for_financial_aid: true,
|
|
||||||
seats: [
|
|
||||||
{
|
|
||||||
sku: '',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '0.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: '',
|
|
||||||
type: 'audit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sku: '',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '10.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: '',
|
|
||||||
type: 'verified'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
course_url: 'someurl',
|
|
||||||
availability: 'Archived',
|
|
||||||
transcript_languages: [
|
|
||||||
'en-us'
|
|
||||||
],
|
|
||||||
staff: [
|
|
||||||
{
|
|
||||||
family_name: 'Tester',
|
|
||||||
uuid: '11ee1afb-5750-4185-8434-c9ae8297f0f1',
|
|
||||||
bio: 'Dr. Tester, PhD, RD, is a Professor at the School of Nutrition.',
|
|
||||||
profile_image: {},
|
|
||||||
profile_image_url: 'someimage.jpg',
|
|
||||||
given_name: 'Bob',
|
|
||||||
urls: {
|
|
||||||
blog: null,
|
|
||||||
twitter: null,
|
|
||||||
facebook: null
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
organization_name: 'Test University',
|
|
||||||
title: 'Associate Professor of Nutrition'
|
|
||||||
},
|
|
||||||
works: [],
|
|
||||||
slug: 'dr-tester'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
announcement: null,
|
|
||||||
end: '2017-03-25T22:18:33Z',
|
|
||||||
uuid: 'a36efd39-6637-11e6-a8e3-22000bdde520',
|
|
||||||
title: 'Introduction to Jedi',
|
|
||||||
certificate_url: null,
|
|
||||||
enrollment_start: null,
|
|
||||||
start: '2016-01-11T05:00:00Z',
|
|
||||||
min_effort: null,
|
|
||||||
short_description: null,
|
|
||||||
hidden: false,
|
|
||||||
level_type: null,
|
|
||||||
type: 'verified',
|
|
||||||
enrollment_open_date: 'Jan 01, 1900',
|
|
||||||
marketing_url: 'someurl',
|
|
||||||
is_course_ended: false,
|
|
||||||
instructors: [],
|
|
||||||
full_description: null,
|
|
||||||
key: 'course-v1:Testx+EXW100x+1T2016',
|
|
||||||
enrollment_end: null,
|
|
||||||
reporting_type: 'mooc',
|
|
||||||
advertised_start: null,
|
|
||||||
mobile_available: true,
|
|
||||||
modified: '2017-03-24T14:18:08.693748Z',
|
|
||||||
is_enrolled: false,
|
|
||||||
pacing_type: 'instructor_paced',
|
|
||||||
video: null,
|
|
||||||
status: 'published'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
upgrade_url: null,
|
|
||||||
image: {
|
|
||||||
src: 'someurl',
|
|
||||||
width: null,
|
|
||||||
description: null,
|
|
||||||
height: null
|
|
||||||
},
|
|
||||||
max_effort: null,
|
|
||||||
is_enrollment_open: true,
|
|
||||||
course: 'Testx+EXW100x',
|
|
||||||
content_language: null,
|
|
||||||
eligible_for_financial_aid: true,
|
|
||||||
seats: [
|
|
||||||
{
|
|
||||||
sku: '77AA8F2',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '0.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: null,
|
|
||||||
type: 'audit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sku: '7EC7BB0',
|
|
||||||
credit_hours: null,
|
|
||||||
price: '100.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: null,
|
|
||||||
type: 'verified'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sku: 'BD436CC',
|
|
||||||
credit_hours: 10,
|
|
||||||
price: '378.00',
|
|
||||||
currency: 'USD',
|
|
||||||
upgrade_deadline: null,
|
|
||||||
credit_provider: 'asu',
|
|
||||||
type: 'credit'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
course_url: 'someurl',
|
|
||||||
availability: 'Archived',
|
|
||||||
transcript_languages: [],
|
|
||||||
staff: [],
|
|
||||||
announcement: null,
|
|
||||||
end: '2016-07-29T00:00:00Z',
|
|
||||||
uuid: '03b34748-19b1-4732-9ea2-e68da95024e6',
|
|
||||||
title: 'Introduction to Jedi',
|
|
||||||
certificate_url: null,
|
|
||||||
enrollment_start: null,
|
|
||||||
start: '2017-03-22T18:10:39Z',
|
|
||||||
min_effort: null,
|
|
||||||
short_description: null,
|
|
||||||
hidden: false,
|
|
||||||
level_type: null,
|
|
||||||
type: 'credit',
|
|
||||||
enrollment_open_date: 'Jan 01, 1900',
|
|
||||||
marketing_url: null,
|
|
||||||
is_course_ended: false,
|
|
||||||
instructors: [],
|
|
||||||
full_description: null,
|
|
||||||
key: 'course-v1:Testx+EXW100x+2164C',
|
|
||||||
enrollment_end: '2016-06-18T19:00:00Z',
|
|
||||||
reporting_type: 'mooc',
|
|
||||||
advertised_start: null,
|
|
||||||
mobile_available: false,
|
|
||||||
modified: '2017-03-23T16:47:37.108260Z',
|
|
||||||
is_enrolled: false,
|
|
||||||
pacing_type: 'self_paced',
|
|
||||||
video: null,
|
|
||||||
status: 'published'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
grades: {
|
|
||||||
'course-v1:Testx+DOGx002+1T2016': 0.9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
urls: {
|
|
||||||
program_listing_url: '/dashboard/programs/',
|
|
||||||
commerce_api_url: '/api/commerce/v0/baskets/',
|
|
||||||
track_selection_url: '/course_modes/choose/'
|
|
||||||
},
|
|
||||||
userPreferences: {
|
|
||||||
'pref-lang': 'en'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data = options.programData,
|
|
||||||
initView;
|
|
||||||
|
|
||||||
initView = function(updates) {
|
|
||||||
var viewOptions = $.extend({}, options, updates);
|
|
||||||
|
|
||||||
return new ProgramDetailsView(viewOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
setFixtures('<div class="js-program-details-wrapper"></div>');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the header', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect(view.$('.js-program-header h2').html()).toEqual(data.title);
|
|
||||||
expect(view.$('.js-program-header .org-logo')[0].src).toEqual(
|
|
||||||
data.authoring_organizations[0].logo_image_url
|
|
||||||
);
|
|
||||||
expect(view.$('.js-program-header .org-logo')[1].src).toEqual(
|
|
||||||
data.authoring_organizations[1].logo_image_url
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the program heading program journey message if program not completed', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect(view.$('.program-heading-title').text()).toEqual('Your Program Journey');
|
|
||||||
expect(view.$('.program-heading-message').text().trim()
|
|
||||||
.replace(/\s+/g, ' ')).toEqual(
|
|
||||||
'Track and plan your progress through the 3 courses in this program. ' +
|
|
||||||
'To complete the program, you must earn a verified certificate for each course.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the program heading congratulations message if all courses completed', function() {
|
|
||||||
view = initView({
|
|
||||||
// Remove remaining courses so all courses are complete
|
|
||||||
courseData: $.extend({}, options.courseData, {
|
|
||||||
in_progress: [],
|
|
||||||
not_started: []
|
|
||||||
})
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
|
|
||||||
expect(view.$('.program-heading-title').text()).toEqual('Congratulations!');
|
|
||||||
expect(view.$('.program-heading-message').text().trim()
|
|
||||||
.replace(/\s+/g, ' ')).toEqual(
|
|
||||||
'You have successfully completed all the requirements for the Test Course Title Test.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the course list headings', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect(view.$('.course-list-heading .status').text()).toEqual(
|
|
||||||
'COURSES IN PROGRESSREMAINING COURSESCOMPLETED COURSES'
|
|
||||||
);
|
|
||||||
expect(view.$('.course-list-heading .count').text()).toEqual('111');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the basic course card information', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect($(view.$('.course-title')[0]).text().trim()).toEqual('Star Trek: The Next Generation');
|
|
||||||
expect($(view.$('.enrolled')[0]).text().trim()).toEqual('Enrolled:');
|
|
||||||
expect($(view.$('.run-period')[0]).text().trim()).toEqual('Mar 20, 2017 - Mar 31, 2017');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render certificate information', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect($(view.$('.upgrade-message .card-msg')).text().trim()).toEqual('Certificate Status:');
|
|
||||||
expect($(view.$('.upgrade-message .price')).text().trim()).toEqual('$10.00');
|
|
||||||
expect($(view.$('.upgrade-button.single-course-run')[0]).text().trim()).toEqual('Upgrade to Verified');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render full program purchase link', function() {
|
|
||||||
view = initView({
|
|
||||||
programData: $.extend({}, options.programData, {
|
|
||||||
is_learner_eligible_for_one_click_purchase: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
expect($(view.$('.upgrade-button.complete-program')).text().trim().
|
|
||||||
replace(/\s+/g, ' ')).
|
|
||||||
toEqual(
|
|
||||||
'Upgrade All Remaining Courses ( $300.00 USD )'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render partial program purchase link', function() {
|
|
||||||
view = initView({
|
|
||||||
programData: $.extend({}, options.programData, {
|
|
||||||
is_learner_eligible_for_one_click_purchase: true,
|
|
||||||
discount_data: {
|
|
||||||
currency: 'USD',
|
|
||||||
discount_value: 30,
|
|
||||||
is_discounted: true,
|
|
||||||
total_incl_tax: 300,
|
|
||||||
total_incl_tax_excl_discounts: 270
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
expect($(view.$('.upgrade-button.complete-program')).text().trim().
|
|
||||||
replace(/\s+/g, ' ')).
|
|
||||||
toEqual(
|
|
||||||
'Upgrade All Remaining Courses ( $270.00 $300.00 USD )'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render enrollment information', function() {
|
|
||||||
view = initView();
|
|
||||||
view.render();
|
|
||||||
expect(view.$('.run-select')[0].options.length).toEqual(2);
|
|
||||||
expect($(view.$('.select-choice')[0]).attr('for')).toEqual($(view.$('.run-select')[0]).attr('id'));
|
|
||||||
expect($(view.$('.enroll-button button')[0]).text().trim()).toEqual('Enroll Now');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send analytic event when purchase button clicked', function() {
|
|
||||||
var properties = {
|
|
||||||
category: 'partial bundle',
|
|
||||||
label: 'Test Course Title',
|
|
||||||
uuid: '0ffff5d6-0177-4690-9a48-aa2fecf94610'
|
|
||||||
};
|
|
||||||
view = initView({
|
|
||||||
programData: $.extend({}, options.programData, {
|
|
||||||
is_learner_eligible_for_one_click_purchase: true,
|
|
||||||
variant: 'partial'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
$('.complete-program').click();
|
|
||||||
// Verify that analytics event fires when the purchase button is clicked.
|
|
||||||
expect(window.analytics.track).toHaveBeenCalledWith(
|
|
||||||
'edx.bi.user.dashboard.program.purchase',
|
|
||||||
properties
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'jquery',
|
|
||||||
'js/learner_dashboard/views/sidebar_view'
|
|
||||||
], function(Backbone, $, SidebarView) {
|
|
||||||
'use strict';
|
|
||||||
/* jslint maxlen: 500 */
|
|
||||||
|
|
||||||
describe('Sidebar View', function() {
|
|
||||||
var view = null,
|
|
||||||
context = {
|
|
||||||
marketingUrl: 'https://www.example.org/programs'
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
setFixtures('<div class="sidebar"></div>');
|
|
||||||
|
|
||||||
view = new SidebarView({
|
|
||||||
el: '.sidebar',
|
|
||||||
context: context
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load the exploration panel given a marketing URL', function() {
|
|
||||||
var $sidebar = view.$el;
|
|
||||||
expect($sidebar.find('.program-advertise .advertise-message').html().trim())
|
|
||||||
.toEqual('Browse recently launched courses and see what\'s new in your favorite subjects');
|
|
||||||
expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.marketingUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not load the advertising panel if no marketing URL is provided', function() {
|
|
||||||
var $ad;
|
|
||||||
view.remove();
|
|
||||||
view = new SidebarView({
|
|
||||||
el: '.sidebar',
|
|
||||||
context: {}
|
|
||||||
});
|
|
||||||
view.render();
|
|
||||||
$ad = view.$el.find('.program-advertise');
|
|
||||||
expect($ad.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
define([
|
|
||||||
'backbone',
|
|
||||||
'js/learner_dashboard/views/unenroll_view'
|
|
||||||
], function(Backbone, UnenrollView) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Unenroll View', function() {
|
|
||||||
var view = null,
|
|
||||||
options = {
|
|
||||||
urls: {
|
|
||||||
dashboard: '/dashboard',
|
|
||||||
browseCourses: '/courses'
|
|
||||||
},
|
|
||||||
isEdx: true
|
|
||||||
},
|
|
||||||
initView;
|
|
||||||
|
|
||||||
initView = function() {
|
|
||||||
return new UnenrollView(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
setFixtures('<div class="unenroll-modal"><div class="wrapper-action-more" data-course-key="course-v1:edX+DemoX+Demo_Course"> <button type="button" class="action action-more" id="actions-dropdown-link-0" aria-haspopup="true" aria-expanded="true" aria-controls="actions-dropdown-0" data-course-number="DemoX" data-course-name="edX Demonstration Course" data-dashboard-index="0"> <span class="sr">Course options for</span> <span class="sr"> edX Demonstration Course </span> <span class="fa fa-cog" aria-hidden="true"></span> </button> <div class="actions-dropdown is-visible" id="actions-dropdown-0" tabindex="-1"> <ul class="actions-dropdown-list" id="actions-dropdown-list-0" aria-label="Available Actions" role="menu"> <div class="reasons_survey"> <div class="slide1 hidden"> <h3>We\'re sorry to see you go! Please share your main reason for unenrolling.</h3><br> <ul class="options"> <li><label class="option"><input type="radio" name="reason" val="I don\'t have enough support">I don\'t have enough support</label></li><li><label class="option"><input type="radio" name="reason" val="I don’t have the academic or language prerequisites">I don\'t have the academic or language prerequisites</label></li><li><label class="option"><input type="radio" name="reason" val="Something was broken">Something was broken</label></li><li><label class="option"><input type="radio" name="reason" val="I just wanted to browse the material">I just wanted to browse the material</label></li><li><label class="option"><input type="radio" name="reason" val="This won’t help me reach my goals">This won\'t help me reach my goals</label></li><li><label class="option"><input type="radio" name="reason" val="I am not happy with the quality of the content">I am not happy with the quality of the content</label></li><li><label class="option"><input type="radio" name="reason" val="The course material was too hard">The course material was too hard</label></li><li><label class="option"><input type="radio" name="reason" val="I don\'t have the time">I don\'t have the time</label></li><li><label class="option"><input type="radio" name="reason" val="The course material was too easy">The course material was too easy</label></li><li><label class="option"><input class="other_radio" type="radio" name="reason" val="Other">Other <input type="text" class="other_text"></label></li></ul> <button class="submit_reasons">Submit</button> </div> </div> <div class="slide2 hidden"> Thank you for sharing your reasons for unenrolling.<br> You are unenrolled from edX Demonstration Course. <a class="button survey_button return_to_dashboard"> Return To Dashboard </a> <a class="button survey_button browse_courses"> Browse Courses </a> </div> <li class="actions-item" id="actions-item-unenroll-0"> <a href="#unenroll-modal" class="action action-unenroll" rel="leanModal" data-course-id="course-v1:edX+DemoX+Demo_Course" data-course-number="DemoX" data-course-name="edX Demonstration Course" data-dashboard-index="0" data-track-info="Are you sure you want to unenroll from %(course_name)s (%(course_number)s)?" id="unenroll-0"> Unenroll </a> </li> <li class="actions-item" id="actions-item-email-settings-0"> </li> </ul> </div> </div></div>'); // eslint-disable-line max-len
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
view.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exist', function() {
|
|
||||||
view = initView();
|
|
||||||
expect(view).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('switch between slides', function() {
|
|
||||||
view = initView();
|
|
||||||
expect($('.slide1').hasClass('hidden')).toEqual(true);
|
|
||||||
view.switchToSlideOne();
|
|
||||||
expect($('.slide1').hasClass('hidden')).toEqual(false);
|
|
||||||
expect($('.slide2').hasClass('hidden')).toEqual(true);
|
|
||||||
view.switchToSlideTwo();
|
|
||||||
expect($('.slide2').hasClass('hidden')).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -97,7 +97,7 @@ define([
|
|||||||
};
|
};
|
||||||
StaffDebug.doInstructorDashAction(action);
|
StaffDebug.doInstructorDashAction(action);
|
||||||
AjaxHelpers.respondWithTextError(requests);
|
AjaxHelpers.respondWithTextError(requests);
|
||||||
expect($('#idash_msg').text()).toBe('Failed to reset attempts for user. ');
|
expect($('#idash_msg').text()).toBe('Failed to reset attempts for user. Unknown Error Occurred.');
|
||||||
$('#result_' + locationName).remove();
|
$('#result_' + locationName).remove();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ var options = {
|
|||||||
specFiles: [
|
specFiles: [
|
||||||
// Define the Webpack-built spec files first
|
// Define the Webpack-built spec files first
|
||||||
{pattern: 'course_experience/js/**/*_spec.js', webpack: true},
|
{pattern: 'course_experience/js/**/*_spec.js', webpack: true},
|
||||||
|
{pattern: 'js/learner_dashboard/**/*_spec.js', webpack: true},
|
||||||
|
|
||||||
// Add all remaining spec files to be used without Webpack
|
// Add all remaining spec files to be used without Webpack
|
||||||
{pattern: '../**/*spec.js'}
|
{pattern: '../**/*spec.js'}
|
||||||
|
|||||||
@@ -32,11 +32,6 @@
|
|||||||
'js/groups/views/cohorts_dashboard_factory',
|
'js/groups/views/cohorts_dashboard_factory',
|
||||||
'js/discussions_management/views/discussions_dashboard_factory',
|
'js/discussions_management/views/discussions_dashboard_factory',
|
||||||
'js/header_factory',
|
'js/header_factory',
|
||||||
'js/learner_dashboard/course_entitlement_factory',
|
|
||||||
'js/learner_dashboard/unenrollment_factory',
|
|
||||||
'js/learner_dashboard/entitlement_unenrollment_factory',
|
|
||||||
'js/learner_dashboard/program_details_factory',
|
|
||||||
'js/learner_dashboard/program_list_factory',
|
|
||||||
'js/student_account/logistration_factory',
|
'js/student_account/logistration_factory',
|
||||||
'js/student_account/views/account_settings_factory',
|
'js/student_account/views/account_settings_factory',
|
||||||
'js/student_account/views/finish_auth_factory',
|
'js/student_account/views/finish_auth_factory',
|
||||||
|
|||||||
@@ -699,6 +699,18 @@
|
|||||||
'discussion/js/spec/discussion_board_view_spec.js',
|
'discussion/js/spec/discussion_board_view_spec.js',
|
||||||
'discussion/js/spec/views/discussion_user_profile_view_spec.js',
|
'discussion/js/spec/views/discussion_user_profile_view_spec.js',
|
||||||
'lms/js/spec/preview/preview_factory_spec.js',
|
'lms/js/spec/preview/preview_factory_spec.js',
|
||||||
|
'js/learner_dashboard/spec/collection_list_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/course_card_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/course_enroll_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/course_entitlement_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/entitlement_unenrollment_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/program_card_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/program_details_header_spec.js',
|
||||||
|
'js/learner_dashboard/spec/program_details_sidebar_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/program_details_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/progress_circle_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/sidebar_view_spec.js',
|
||||||
|
'js/learner_dashboard/spec/unenroll_view_spec.js',
|
||||||
'js/spec/api_admin/catalog_preview_spec.js',
|
'js/spec/api_admin/catalog_preview_spec.js',
|
||||||
'js/spec/ccx/schedule_spec.js',
|
'js/spec/ccx/schedule_spec.js',
|
||||||
'js/spec/commerce/receipt_view_spec.js',
|
'js/spec/commerce/receipt_view_spec.js',
|
||||||
@@ -757,17 +769,6 @@
|
|||||||
'js/spec/instructor_dashboard/ecommerce_spec.js',
|
'js/spec/instructor_dashboard/ecommerce_spec.js',
|
||||||
'js/spec/instructor_dashboard/membership_auth_spec.js',
|
'js/spec/instructor_dashboard/membership_auth_spec.js',
|
||||||
'js/spec/instructor_dashboard/student_admin_spec.js',
|
'js/spec/instructor_dashboard/student_admin_spec.js',
|
||||||
'js/spec/learner_dashboard/collection_list_view_spec.js',
|
|
||||||
'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_view_spec.js',
|
|
||||||
'js/spec/learner_dashboard/program_details_sidebar_view_spec.js',
|
|
||||||
'js/spec/learner_dashboard/unenroll_view_spec.js',
|
|
||||||
'js/spec/learner_dashboard/entitlement_unenrollment_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_entitlement_view_spec.js',
|
|
||||||
'js/spec/markdown_editor_spec.js',
|
'js/spec/markdown_editor_spec.js',
|
||||||
'js/spec/dateutil_factory_spec.js',
|
'js/spec/dateutil_factory_spec.js',
|
||||||
'js/spec/navigation_spec.js',
|
'js/spec/navigation_spec.js',
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from student.models import CourseEnrollment
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<%static:require_module module_name="js/learner_dashboard/unenrollment_factory" class_name="UnenrollmentFactory">
|
<%static:webpack entry="UnenrollmentFactory">
|
||||||
UnenrollmentFactory({
|
UnenrollmentFactory({
|
||||||
urls: {
|
urls: {
|
||||||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||||||
@@ -59,8 +59,8 @@ from student.models import CourseEnrollment
|
|||||||
},
|
},
|
||||||
isEdx: false
|
isEdx: false
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
<%static:require_module module_name="js/learner_dashboard/entitlement_unenrollment_factory" class_name="EntitlementUnenrollmentFactory">
|
<%static:webpack entry="EntitlementUnenrollmentFactory">
|
||||||
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
||||||
## to ensure events are setup correctly.
|
## to ensure events are setup correctly.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@@ -69,7 +69,7 @@ from student.models import CourseEnrollment
|
|||||||
signInPath: "${reverse('signin_user') | n, js_escaped_string}"
|
signInPath: "${reverse('signin_user') | n, js_escaped_string}"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||||
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
||||||
DashboardSearchFactory();
|
DashboardSearchFactory();
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
|||||||
|
|
||||||
% if entitlement and not entitlement_expired_at:
|
% if entitlement and not entitlement_expired_at:
|
||||||
<div class="course-entitlement-selection-container ${'' if is_unfulfilled_entitlement else 'hidden'}"></div>
|
<div class="course-entitlement-selection-container ${'' if is_unfulfilled_entitlement else 'hidden'}"></div>
|
||||||
<%static:require_module module_name="js/learner_dashboard/course_entitlement_factory" class_name="EntitlementFactory">
|
<%static:webpack entry="EntitlementFactory">
|
||||||
EntitlementFactory({
|
EntitlementFactory({
|
||||||
el: '${ '#course-card-' + str(course_card_index) + ' .course-entitlement-selection-container' | n, js_escaped_string }',
|
el: '${ '#course-card-' + str(course_card_index) + ' .course-entitlement-selection-container' | n, js_escaped_string }',
|
||||||
triggerOpenBtn: '${ '#course-card-' + str(course_card_index) + ' .change-session' | n, js_escaped_string }',
|
triggerOpenBtn: '${ '#course-card-' + str(course_card_index) + ' .change-session' | n, js_escaped_string }',
|
||||||
@@ -321,7 +321,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
|||||||
expiredAt: '${ entitlement.expired_at_datetime | n, js_escaped_string }',
|
expiredAt: '${ entitlement.expired_at_datetime | n, js_escaped_string }',
|
||||||
daysUntilExpiration: '${ entitlement.get_days_until_expiration() | n, js_escaped_string }'
|
daysUntilExpiration: '${ entitlement.get_days_until_expiration() | n, js_escaped_string }'
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
% if related_programs:
|
% if related_programs:
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from openedx.core.djangolib.js_utils import (
|
|||||||
)
|
)
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
<div class="js-program-details-wrapper program-details-wrapper"></div>
|
||||||
|
|
||||||
<%block name="js_extra">
|
<%block name="js_extra">
|
||||||
<%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory">
|
<%static:webpack entry="ProgramDetailsFactory">
|
||||||
ProgramDetailsFactory({
|
ProgramDetailsFactory({
|
||||||
programData: ${program_data | n, dump_js_escaped_json},
|
programData: ${program_data | n, dump_js_escaped_json},
|
||||||
courseData: ${course_data | n, dump_js_escaped_json},
|
courseData: ${course_data | n, dump_js_escaped_json},
|
||||||
@@ -17,7 +19,5 @@ ProgramDetailsFactory({
|
|||||||
urls: ${urls | n, dump_js_escaped_json},
|
urls: ${urls | n, dump_js_escaped_json},
|
||||||
userPreferences: ${user_preferences | n, dump_js_escaped_json},
|
userPreferences: ${user_preferences | n, dump_js_escaped_json},
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<div class="js-program-details-wrapper program-details-wrapper"></div>
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<header class="js-program-header program-header full-width-banner"></header>
|
<header class="js-program-header program-header full-width-banner"></header>
|
||||||
<section class="program-details-content">
|
<!-- TODO: consider if article is the most appropriate element here -->
|
||||||
|
<article class="program-details-content">
|
||||||
<div class="program-heading">
|
<div class="program-heading">
|
||||||
<% if (completedCount === totalCount) { %>
|
<% if (completedCount === totalCount) { %>
|
||||||
<h3 class="program-heading-title"><%- gettext('Congratulations!') %></h3>
|
<h3 class="program-heading-title"><%- gettext('Congratulations!') %></h3>
|
||||||
@@ -73,5 +74,5 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</article>
|
||||||
<aside class="js-program-sidebar program-sidebar"></aside>
|
<aside class="js-program-sidebar program-sidebar"></aside>
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ from openedx.core.djangolib.js_utils import (
|
|||||||
)
|
)
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
<div class="program-list-wrapper grid-container">
|
||||||
|
<div class="program-cards-container col"></div>
|
||||||
|
<div class="sidebar col col-last"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%block name="js_extra">
|
<%block name="js_extra">
|
||||||
<%static:require_module module_name="js/learner_dashboard/program_list_factory" class_name="ProgramListFactory">
|
<%static:webpack entry="ProgramListFactory">
|
||||||
ProgramListFactory({
|
ProgramListFactory({
|
||||||
marketingUrl: '${marketing_url | n, js_escaped_string}',
|
marketingUrl: '${marketing_url | n, js_escaped_string}',
|
||||||
programsData: ${programs | n, dump_js_escaped_json},
|
programsData: ${programs | n, dump_js_escaped_json},
|
||||||
userProgress: ${progress | n, dump_js_escaped_json}
|
userProgress: ${progress | n, dump_js_escaped_json}
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<div class="program-list-wrapper grid-container">
|
|
||||||
<div class="program-cards-container col"></div>
|
|
||||||
<div class="sidebar col col-last"></div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1059,6 +1059,14 @@
|
|||||||
"babel-runtime": "6.26.0"
|
"babel-runtime": "6.26.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-plugin-transform-object-assign": {
|
||||||
|
"version": "6.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-object-assign/-/babel-plugin-transform-object-assign-6.22.0.tgz",
|
||||||
|
"integrity": "sha1-+Z0vZvGgsNSY40bFNZaEdAyqILo=",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.26.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"babel-plugin-transform-object-rest-spread": {
|
"babel-plugin-transform-object-rest-spread": {
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"babel-core": "6.26.0",
|
"babel-core": "6.26.0",
|
||||||
"babel-loader": "6.4.1",
|
"babel-loader": "6.4.1",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
|
"babel-plugin-transform-object-assign": "6.22.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "6.26.0",
|
"babel-plugin-transform-object-rest-spread": "6.26.0",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"babel-preset-env": "1.6.1",
|
"babel-preset-env": "1.6.1",
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ set -e
|
|||||||
|
|
||||||
export LOWER_PYLINT_THRESHOLD=1000
|
export LOWER_PYLINT_THRESHOLD=1000
|
||||||
export UPPER_PYLINT_THRESHOLD=5900
|
export UPPER_PYLINT_THRESHOLD=5900
|
||||||
export ESLINT_THRESHOLD=5700
|
export ESLINT_THRESHOLD=5580
|
||||||
export STYLELINT_THRESHOLD=973
|
export STYLELINT_THRESHOLD=973
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from student.models import CourseEnrollment
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<%static:require_module module_name="js/learner_dashboard/unenrollment_factory" class_name="UnenrollmentFactory">
|
<%static:webpack entry="UnenrollmentFactory">
|
||||||
UnenrollmentFactory({
|
UnenrollmentFactory({
|
||||||
urls: {
|
urls: {
|
||||||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||||||
@@ -66,8 +66,8 @@ from student.models import CourseEnrollment
|
|||||||
},
|
},
|
||||||
isEdx: true
|
isEdx: true
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
<%static:require_module module_name="js/learner_dashboard/entitlement_unenrollment_factory" class_name="EntitlementUnenrollmentFactory">
|
<%static:webpack entry="EntitlementUnenrollmentFactory">
|
||||||
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
||||||
## to ensure events are setup correctly.
|
## to ensure events are setup correctly.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@@ -76,7 +76,7 @@ from student.models import CourseEnrollment
|
|||||||
signInPath: "${reverse('signin_user') | n, js_escaped_string}"
|
signInPath: "${reverse('signin_user') | n, js_escaped_string}"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</%static:require_module>
|
</%static:webpack>
|
||||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||||
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
||||||
DashboardSearchFactory();
|
DashboardSearchFactory();
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ module.exports = {
|
|||||||
UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx',
|
UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx',
|
||||||
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
|
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
|
||||||
|
|
||||||
|
// Learner Dashboard
|
||||||
|
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',
|
||||||
|
EntitlementUnenrollmentFactory: './lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js',
|
||||||
|
ProgramDetailsFactory: './lms/static/js/learner_dashboard/program_details_factory.js',
|
||||||
|
ProgramListFactory: './lms/static/js/learner_dashboard/program_list_factory.js',
|
||||||
|
UnenrollmentFactory: './lms/static/js/learner_dashboard/unenrollment_factory.js',
|
||||||
|
|
||||||
// Features
|
// Features
|
||||||
CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
|
CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
|
||||||
CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js',
|
CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js',
|
||||||
@@ -68,7 +75,8 @@ module.exports = {
|
|||||||
_: 'underscore',
|
_: 'underscore',
|
||||||
$: 'jquery',
|
$: 'jquery',
|
||||||
jQuery: 'jquery',
|
jQuery: 'jquery',
|
||||||
'window.jQuery': 'jquery'
|
'window.jQuery': 'jquery',
|
||||||
|
Popper: 'popper.js' // used by bootstrap
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Note: Until karma-webpack releases v3, it doesn't play well with
|
// Note: Until karma-webpack releases v3, it doesn't play well with
|
||||||
@@ -174,6 +182,7 @@ module.exports = {
|
|||||||
'node_modules',
|
'node_modules',
|
||||||
'common/static/js/vendor/',
|
'common/static/js/vendor/',
|
||||||
'cms/static',
|
'cms/static',
|
||||||
|
'common/static/',
|
||||||
'common/static/js/src'
|
'common/static/js/src'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user