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": [
|
||||
[
|
||||
"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) {
|
||||
var getFrameworkFiles = function() {
|
||||
var files = [
|
||||
'node_modules/jquery/dist/jquery.js',
|
||||
'common/static/common/js/vendor/jquery.js',
|
||||
'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
|
||||
'common/static/common/js/jasmine_stack_trace.js',
|
||||
'node_modules/karma-jasmine/lib/boot.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) {
|
||||
|
||||
@@ -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/paging_header_spec.js',
|
||||
'common/js/spec/components/paging_footer_spec.js',
|
||||
'common/js/spec/components/progress_circle_view_spec.js',
|
||||
'common/js/spec/components/search_field_spec.js',
|
||||
'common/js/spec/components/view_utils_spec.js',
|
||||
'common/js/spec/utils/edx.utils.validate_spec.js'
|
||||
|
||||
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) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone',
|
||||
'js/learner_dashboard/models/course_card_model'
|
||||
],
|
||||
function(Backbone, CourseCard) {
|
||||
return Backbone.Collection.extend({
|
||||
model: CourseCard
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
import Backbone from 'backbone';
|
||||
import CourseCard from '../models/course_card_model';
|
||||
|
||||
class CourseCardCollection extends Backbone.Collection {
|
||||
constructor(models, options) {
|
||||
const defaults = {
|
||||
model: CourseCard,
|
||||
};
|
||||
super(models, Object.assign({}, defaults, options));
|
||||
}
|
||||
}
|
||||
|
||||
export default CourseCardCollection;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone',
|
||||
'js/learner_dashboard/models/program_model'
|
||||
],
|
||||
function(Backbone, Program) {
|
||||
return Backbone.Collection.extend({
|
||||
model: Program
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
import Backbone from 'backbone';
|
||||
import Program from '../models/program_model';
|
||||
|
||||
class ProgramCollection extends Backbone.Collection {
|
||||
constructor(models, options) {
|
||||
const defaults = {
|
||||
model: Program,
|
||||
};
|
||||
super(models, Object.assign({}, defaults, options));
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramCollection;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone'
|
||||
],
|
||||
function(Backbone) {
|
||||
return Backbone.Collection.extend({});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
import Backbone from 'backbone';
|
||||
|
||||
class ProgramProgressCollection extends Backbone.Collection {
|
||||
}
|
||||
|
||||
export default ProgramProgressCollection;
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
import CourseEntitlementView from './views/course_entitlement_view';
|
||||
|
||||
define([
|
||||
'js/learner_dashboard/views/course_entitlement_view'
|
||||
],
|
||||
function(EntitlementView) {
|
||||
return function(options) {
|
||||
return new EntitlementView(options);
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
function EntitlementFactory(options) {
|
||||
return new CourseEntitlementView(options);
|
||||
}
|
||||
|
||||
export { EntitlementFactory }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
import EntitlementUnenrollmentView from './views/entitlement_unenrollment_view';
|
||||
|
||||
define([
|
||||
'js/learner_dashboard/views/entitlement_unenrollment_view'
|
||||
],
|
||||
function(EntitlementUnenrollmentView) {
|
||||
return function(options) {
|
||||
return new EntitlementUnenrollmentView(options);
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
function EntitlementUnenrollmentFactory(options) {
|
||||
return new EntitlementUnenrollmentView(options);
|
||||
}
|
||||
|
||||
export { EntitlementUnenrollmentFactory }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'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);
|
||||
}
|
||||
},
|
||||
class CourseCardModel extends Backbone.Model {
|
||||
initialize(data) {
|
||||
if (data) {
|
||||
this.context = data;
|
||||
this.setActiveCourseRun(this.getCourseRun(data), data.user_preferences);
|
||||
}
|
||||
}
|
||||
|
||||
getCourseRun: function(course) {
|
||||
var enrolledCourseRun = _.findWhere(course.course_runs, {is_enrolled: true}),
|
||||
openEnrollmentCourseRuns = this.getEnrollableCourseRuns(),
|
||||
desiredCourseRun;
|
||||
getCourseRun(course) {
|
||||
const enrolledCourseRun = _.findWhere(course.course_runs, { is_enrolled: true });
|
||||
const openEnrollmentCourseRuns = this.getEnrollableCourseRuns();
|
||||
let desiredCourseRun;
|
||||
|
||||
// If the learner has an existing, unexpired enrollment,
|
||||
// use it to populate the model.
|
||||
if (enrolledCourseRun && !course.expired) {
|
||||
desiredCourseRun = enrolledCourseRun;
|
||||
} else if (openEnrollmentCourseRuns.length > 0) {
|
||||
if (openEnrollmentCourseRuns.length === 1) {
|
||||
desiredCourseRun = openEnrollmentCourseRuns[0];
|
||||
} else {
|
||||
desiredCourseRun = this.getUnselectedCourseRun(openEnrollmentCourseRuns);
|
||||
}
|
||||
} else {
|
||||
desiredCourseRun = this.getUnselectedCourseRun(course.course_runs);
|
||||
}
|
||||
// If the learner has an existing, unexpired enrollment,
|
||||
// use it to populate the model.
|
||||
if (enrolledCourseRun && !course.expired) {
|
||||
desiredCourseRun = enrolledCourseRun;
|
||||
} else if (openEnrollmentCourseRuns.length > 0) {
|
||||
if (openEnrollmentCourseRuns.length === 1) {
|
||||
desiredCourseRun = openEnrollmentCourseRuns[0];
|
||||
} else {
|
||||
desiredCourseRun = CourseCardModel.getUnselectedCourseRun(openEnrollmentCourseRuns);
|
||||
}
|
||||
} else {
|
||||
desiredCourseRun = CourseCardModel.getUnselectedCourseRun(course.course_runs);
|
||||
}
|
||||
|
||||
return desiredCourseRun;
|
||||
},
|
||||
return desiredCourseRun;
|
||||
}
|
||||
|
||||
isEnrolledInSession: function() {
|
||||
// Returns true if the user is currently enrolled in a session of the course
|
||||
return _.findWhere(this.context.course_runs, {is_enrolled: true}) !== undefined;
|
||||
},
|
||||
isEnrolledInSession() {
|
||||
// Returns true if the user is currently enrolled in a session of the course
|
||||
return _.findWhere(this.context.course_runs, { is_enrolled: true }) !== undefined;
|
||||
}
|
||||
|
||||
getUnselectedCourseRun: function(courseRuns) {
|
||||
var unselectedRun = {},
|
||||
courseRun;
|
||||
static getUnselectedCourseRun(courseRuns) {
|
||||
const unselectedRun = {};
|
||||
|
||||
if (courseRuns && courseRuns.length > 0) {
|
||||
courseRun = courseRuns[0];
|
||||
if (courseRuns && courseRuns.length > 0) {
|
||||
const courseRun = courseRuns[0];
|
||||
|
||||
$.extend(unselectedRun, {
|
||||
marketing_url: courseRun.marketing_url,
|
||||
is_enrollment_open: courseRun.is_enrollment_open,
|
||||
key: courseRun.key || '',
|
||||
is_mobile_only: courseRun.is_mobile_only || false
|
||||
});
|
||||
}
|
||||
$.extend(unselectedRun, {
|
||||
marketing_url: courseRun.marketing_url,
|
||||
is_enrollment_open: courseRun.is_enrollment_open,
|
||||
key: courseRun.key || '',
|
||||
is_mobile_only: courseRun.is_mobile_only || false,
|
||||
});
|
||||
}
|
||||
|
||||
return unselectedRun;
|
||||
},
|
||||
return unselectedRun;
|
||||
}
|
||||
|
||||
getEnrollableCourseRuns: function() {
|
||||
var rawCourseRuns,
|
||||
enrollableCourseRuns;
|
||||
getEnrollableCourseRuns() {
|
||||
const rawCourseRuns = _.where(this.context.course_runs, {
|
||||
is_enrollment_open: true,
|
||||
is_enrolled: false,
|
||||
is_course_ended: false,
|
||||
status: 'published',
|
||||
});
|
||||
|
||||
rawCourseRuns = _.where(this.context.course_runs, {
|
||||
is_enrollment_open: true,
|
||||
is_enrolled: false,
|
||||
is_course_ended: false,
|
||||
status: 'published'
|
||||
});
|
||||
// Deep copy to avoid mutating this.context.
|
||||
const enrollableCourseRuns = $.extend(true, [], rawCourseRuns);
|
||||
|
||||
// Deep copy to avoid mutating this.context.
|
||||
enrollableCourseRuns = $.extend(true, [], rawCourseRuns);
|
||||
// These are raw course runs from the server. The start
|
||||
// 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
|
||||
// dates are ISO-8601 formatted strings that need to be
|
||||
// prepped for display.
|
||||
_.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
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
courseRun.dateString = this.formatDateString(courseRun);
|
||||
});
|
||||
|
||||
// This is used to render the date when selecting a course run to enroll in
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
courseRun.dateString = this.formatDateString(courseRun);
|
||||
}).bind(this));
|
||||
return enrollableCourseRuns;
|
||||
}
|
||||
|
||||
return enrollableCourseRuns;
|
||||
},
|
||||
getUpcomingCourseRuns() {
|
||||
return _.where(this.context.course_runs, {
|
||||
is_enrollment_open: false,
|
||||
is_enrolled: false,
|
||||
is_course_ended: false,
|
||||
status: 'published',
|
||||
});
|
||||
}
|
||||
|
||||
getUpcomingCourseRuns: function() {
|
||||
return _.where(this.context.course_runs, {
|
||||
is_enrollment_open: false,
|
||||
is_enrolled: false,
|
||||
is_course_ended: false,
|
||||
status: 'published'
|
||||
});
|
||||
},
|
||||
static formatDate(date, userPreferences) {
|
||||
let userTimezone = '';
|
||||
let userLanguage = '';
|
||||
if (userPreferences !== undefined) {
|
||||
userTimezone = userPreferences.time_zone;
|
||||
userLanguage = userPreferences['pref-lang'];
|
||||
}
|
||||
const context = {
|
||||
datetime: date,
|
||||
timezone: userTimezone,
|
||||
language: userLanguage,
|
||||
format: DateUtils.dateFormatEnum.shortDate,
|
||||
};
|
||||
return DateUtils.localize(context);
|
||||
}
|
||||
|
||||
formatDate: function(date, userPreferences) {
|
||||
var context,
|
||||
userTimezone = '',
|
||||
userLanguage = '';
|
||||
if (userPreferences !== undefined) {
|
||||
userTimezone = userPreferences.time_zone;
|
||||
userLanguage = userPreferences['pref-lang'];
|
||||
}
|
||||
context = {
|
||||
datetime: date,
|
||||
timezone: userTimezone,
|
||||
language: userLanguage,
|
||||
format: DateUtils.dateFormatEnum.shortDate
|
||||
};
|
||||
return DateUtils.localize(context);
|
||||
},
|
||||
static getCertificatePriceString(run) {
|
||||
if ('seats' in run && run.seats.length) {
|
||||
// eslint-disable-next-line consistent-return
|
||||
const upgradeableSeats = _.filter(run.seats, (seat) => {
|
||||
const upgradeableSeatTypes = ['verified', 'professional', 'no-id-professional', 'credit'];
|
||||
if (upgradeableSeatTypes.indexOf(seat.type) >= 0) {
|
||||
return seat;
|
||||
}
|
||||
});
|
||||
if (upgradeableSeats.length > 0) {
|
||||
const upgradeableSeat = upgradeableSeats[0];
|
||||
if (upgradeableSeat) {
|
||||
const currency = upgradeableSeat.currency;
|
||||
if (currency === 'USD') {
|
||||
return `$${upgradeableSeat.price}`;
|
||||
}
|
||||
return `${upgradeableSeat.price} ${currency}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getCertificatePriceString: function(run) {
|
||||
var upgradeableSeat, upgradeableSeats, currency;
|
||||
if ('seats' in run && run.seats.length) {
|
||||
// eslint-disable-next-line consistent-return
|
||||
upgradeableSeats = _.filter(run.seats, function(seat) {
|
||||
var upgradeableSeatTypes = ['verified', 'professional', 'no-id-professional', 'credit'];
|
||||
if (upgradeableSeatTypes.indexOf(seat.type) >= 0) {
|
||||
return seat;
|
||||
}
|
||||
});
|
||||
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(run) {
|
||||
const pacingType = run.pacing_type;
|
||||
let dateString;
|
||||
const start = CourseCardModel.valueIsDefined(run.start_date) ?
|
||||
run.advertised_start || run.start_date :
|
||||
this.get('start_date');
|
||||
const end = CourseCardModel.valueIsDefined(run.end_date) ? run.end_date : this.get('end_date');
|
||||
const now = new Date();
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
|
||||
formatDateString: function(run) {
|
||||
var pacingType = run.pacing_type,
|
||||
dateString,
|
||||
start = this.valueIsDefined(run.start_date) ? run.advertised_start || run.start_date :
|
||||
this.get('start_date'),
|
||||
end = this.valueIsDefined(run.end_date) ? run.end_date : this.get('end_date'),
|
||||
now = new Date(),
|
||||
startDate = new Date(start),
|
||||
endDate = new Date(end);
|
||||
if (pacingType === 'self_paced') {
|
||||
if (start) {
|
||||
dateString = startDate > now ?
|
||||
StringUtils.interpolate(gettext('(Self-paced) Starts {start}'), { start }) :
|
||||
StringUtils.interpolate(gettext('(Self-paced) Started {start}'), { start });
|
||||
} else if (end && endDate > now) {
|
||||
dateString = StringUtils.interpolate(gettext('(Self-paced) Ends {end}'), { end });
|
||||
} else if (end && endDate < now) {
|
||||
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') {
|
||||
if (start) {
|
||||
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;
|
||||
},
|
||||
static valueIsDefined(val) {
|
||||
return !([undefined, 'None', null].indexOf(val) >= 0);
|
||||
}
|
||||
|
||||
valueIsDefined: function(val) {
|
||||
return !([undefined, 'None', null].indexOf(val) >= 0);
|
||||
},
|
||||
setActiveCourseRun(courseRun, userPreferences) {
|
||||
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) {
|
||||
var startDateString,
|
||||
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
|
||||
this.set({ dateString: this.formatDateString(courseRun) });
|
||||
}
|
||||
}
|
||||
|
||||
// This is used to render the date for completed and in progress courses
|
||||
this.set({dateString: this.formatDateString(courseRun)});
|
||||
}
|
||||
},
|
||||
setUnselected() {
|
||||
// Called to reset the model back to the unselected state.
|
||||
const unselectedCourseRun = CourseCardModel.getUnselectedCourseRun(this.get('enrollable_course_runs'));
|
||||
this.setActiveCourseRun(unselectedCourseRun);
|
||||
}
|
||||
|
||||
setUnselected: function() {
|
||||
// Called to reset the model back to the unselected state.
|
||||
var unselectedCourseRun = this.getUnselectedCourseRun(this.get('enrollable_course_runs'));
|
||||
this.setActiveCourseRun(unselectedCourseRun);
|
||||
},
|
||||
updateCourseRun(courseRunKey) {
|
||||
const selectedCourseRun = _.findWhere(this.get('course_runs'), { key: courseRunKey });
|
||||
if (selectedCourseRun) {
|
||||
// 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) {
|
||||
var selectedCourseRun = _.findWhere(this.get('course_runs'), {key: courseRunKey});
|
||||
if (selectedCourseRun) {
|
||||
// Update the current context to set the course run to the enrolled state
|
||||
_.each(this.context.course_runs, function(run) {
|
||||
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
|
||||
static updateMarketingUrl(courseRun) {
|
||||
if (courseRun.is_mobile_only === true) {
|
||||
const marketingUrl = courseRun.marketing_url;
|
||||
let href = marketingUrl;
|
||||
|
||||
// update marketing url for deep linking if is_mobile_only true
|
||||
updateMarketingUrl: function(courseRun) {
|
||||
if (courseRun.is_mobile_only === true) {
|
||||
var marketingUrl = courseRun.marketing_url, // eslint-disable-line vars-on-top
|
||||
href = marketingUrl,
|
||||
path,
|
||||
start;
|
||||
if (marketingUrl.indexOf('course_info?path_id') < 0) {
|
||||
const start = marketingUrl.indexOf('course/');
|
||||
let path;
|
||||
|
||||
if (marketingUrl.indexOf('course_info?path_id') < 0) {
|
||||
start = marketingUrl.indexOf('course/');
|
||||
if (start > -1) {
|
||||
path = marketingUrl.substr(start);
|
||||
}
|
||||
|
||||
if (start > -1) {
|
||||
path = marketingUrl.substr(start);
|
||||
}
|
||||
href = `edxapp://course_info?path_id=${path}`;
|
||||
}
|
||||
|
||||
href = 'edxapp://course_info?path_id=' + path;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
return courseRun.marketing_url;
|
||||
}
|
||||
}
|
||||
|
||||
return href;
|
||||
} else {
|
||||
return courseRun.marketing_url;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
export default CourseCardModel;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
/**
|
||||
* Store data to enroll learners into the course
|
||||
*/
|
||||
(function(define) {
|
||||
'use strict';
|
||||
class CourseEnrollModel extends Backbone.Model {
|
||||
constructor(attrs, ...args) {
|
||||
const defaults = {
|
||||
course_id: '',
|
||||
optIn: false,
|
||||
};
|
||||
super(Object.assign({}, defaults, attrs), ...args);
|
||||
}
|
||||
}
|
||||
|
||||
define([
|
||||
'backbone'
|
||||
],
|
||||
function(Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
defaults: {
|
||||
course_id: '',
|
||||
optIn: false
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
export default CourseEnrollModel;
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
/**
|
||||
* Store data for the current entitlement.
|
||||
*/
|
||||
(function(define) {
|
||||
'use strict';
|
||||
class CourseEntitlementModel extends Backbone.Model {
|
||||
constructor(attrs, ...args) {
|
||||
const defaults = {
|
||||
availableSessions: [],
|
||||
entitlementUUID: '',
|
||||
currentSessionId: '',
|
||||
courseName: '',
|
||||
expiredAt: null,
|
||||
daysUntilExpiration: Number.MAX_VALUE,
|
||||
};
|
||||
super(Object.assign({}, defaults, attrs), ...args);
|
||||
}
|
||||
}
|
||||
|
||||
define([
|
||||
'backbone'
|
||||
],
|
||||
function(Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
defaults: {
|
||||
availableSessions: [],
|
||||
entitlementUUID: '',
|
||||
currentSessionId: '',
|
||||
courseName: '',
|
||||
expiredAt: null,
|
||||
daysUntilExpiration: Number.MAX_VALUE
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
export default CourseEntitlementModel;
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import Backbone from 'backbone';
|
||||
|
||||
/**
|
||||
* Model for Course Programs.
|
||||
*/
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone'
|
||||
],
|
||||
function(Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
initialize: function(data) {
|
||||
if (data) {
|
||||
this.set({
|
||||
title: data.title,
|
||||
type: data.type,
|
||||
subtitle: data.subtitle,
|
||||
authoring_organizations: data.authoring_organizations,
|
||||
detailUrl: data.detail_url,
|
||||
xsmallBannerUrl: data.banner_image['x-small'].url,
|
||||
smallBannerUrl: data.banner_image.small.url,
|
||||
mediumBannerUrl: data.banner_image.medium.url,
|
||||
breakpoints: {
|
||||
max: {
|
||||
xsmall: '320px',
|
||||
small: '540px',
|
||||
medium: '768px',
|
||||
large: '979px'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
class ProgramModel extends Backbone.Model {
|
||||
initialize(data) {
|
||||
if (data) {
|
||||
this.set({
|
||||
title: data.title,
|
||||
type: data.type,
|
||||
subtitle: data.subtitle,
|
||||
authoring_organizations: data.authoring_organizations,
|
||||
detailUrl: data.detail_url,
|
||||
xsmallBannerUrl: data.banner_image['x-small'].url,
|
||||
smallBannerUrl: data.banner_image.small.url,
|
||||
mediumBannerUrl: data.banner_image.medium.url,
|
||||
breakpoints: {
|
||||
max: {
|
||||
xsmall: '320px',
|
||||
small: '540px',
|
||||
medium: '768px',
|
||||
large: '979px',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramModel;
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
import ProgramDetailsView from './views/program_details_view';
|
||||
|
||||
define([
|
||||
'js/learner_dashboard/views/program_details_view'
|
||||
],
|
||||
function(ProgramDetailsView) {
|
||||
return function(options) {
|
||||
var ProgramDetails = new ProgramDetailsView(options);
|
||||
return ProgramDetails;
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
function ProgramDetailsFactory(options) {
|
||||
return new ProgramDetailsView(options);
|
||||
}
|
||||
|
||||
export { ProgramDetailsFactory }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
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';
|
||||
import SidebarView from './views/sidebar_view';
|
||||
|
||||
define([
|
||||
'js/learner_dashboard/views/collection_list_view',
|
||||
'js/learner_dashboard/views/sidebar_view',
|
||||
'js/learner_dashboard/views/program_card_view',
|
||||
'js/learner_dashboard/collections/program_collection',
|
||||
'js/learner_dashboard/collections/program_progress_collection'
|
||||
],
|
||||
function(CollectionListView, SidebarView, ProgramCardView, ProgramCollection, ProgressCollection) {
|
||||
return function(options) {
|
||||
var progressCollection = new ProgressCollection();
|
||||
function ProgramListFactory(options) {
|
||||
const progressCollection = new ProgressCollection();
|
||||
|
||||
if (options.userProgress) {
|
||||
progressCollection.set(options.userProgress);
|
||||
options.progressCollection = progressCollection;
|
||||
}
|
||||
if (options.userProgress) {
|
||||
progressCollection.set(options.userProgress);
|
||||
options.progressCollection = progressCollection; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
new CollectionListView({
|
||||
el: '.program-cards-container',
|
||||
childView: ProgramCardView,
|
||||
collection: new ProgramCollection(options.programsData),
|
||||
context: options,
|
||||
titleContext: {
|
||||
el: 'h2',
|
||||
title: 'Your Programs'
|
||||
}
|
||||
}).render();
|
||||
new CollectionListView({
|
||||
el: '.program-cards-container',
|
||||
childView: ProgramCardView,
|
||||
collection: new ProgramCollection(options.programsData),
|
||||
context: options,
|
||||
titleContext: {
|
||||
el: 'h2',
|
||||
title: 'Your Programs',
|
||||
},
|
||||
}).render();
|
||||
|
||||
if (options.programsData.length) {
|
||||
new SidebarView({
|
||||
el: '.sidebar',
|
||||
context: options
|
||||
}).render();
|
||||
}
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
if (options.programsData.length) {
|
||||
new SidebarView({
|
||||
el: '.sidebar',
|
||||
context: options,
|
||||
}).render();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
'use strict';
|
||||
import UnenrollView from './views/unenroll_view';
|
||||
|
||||
define([
|
||||
'js/learner_dashboard/views/unenroll_view'
|
||||
],
|
||||
function(UnenrollView) {
|
||||
return function(options) {
|
||||
var Unenroll = new UnenrollView(options);
|
||||
return Unenroll;
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
function UnenrollmentFactory(options) {
|
||||
return new UnenrollView(options);
|
||||
}
|
||||
|
||||
export { UnenrollmentFactory }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'text!../../../templates/learner_dashboard/certificate_list.underscore'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext,
|
||||
certificateTpl
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
tpl: _.template(certificateTpl),
|
||||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
initialize: function(options) {
|
||||
this.title = options.title || false;
|
||||
this.render();
|
||||
},
|
||||
import certificateTpl from '../../../templates/learner_dashboard/certificate_list.underscore';
|
||||
|
||||
render: function() {
|
||||
var data = {
|
||||
title: this.title,
|
||||
certificateList: this.collection.toJSON()
|
||||
};
|
||||
class CertificateListView extends Backbone.View {
|
||||
initialize(options) {
|
||||
this.tpl = _.template(certificateTpl);
|
||||
this.title = options.title || false;
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.$el.html(this.tpl(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
render() {
|
||||
const data = {
|
||||
title: this.title,
|
||||
certificateList: this.collection.toJSON(),
|
||||
};
|
||||
|
||||
this.$el.html(this.tpl(data));
|
||||
}
|
||||
}
|
||||
|
||||
export default CertificateListView;
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
(function(define) {
|
||||
'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),
|
||||
import Backbone from 'backbone';
|
||||
|
||||
initialize: function(options) {
|
||||
this.$el = options.$el;
|
||||
this.render();
|
||||
},
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
render: function() {
|
||||
var data = this.model.toJSON();
|
||||
import certificateStatusTpl from '../../../templates/learner_dashboard/certificate_status.underscore';
|
||||
import certificateIconTpl from '../../../templates/learner_dashboard/certificate_icon.underscore';
|
||||
|
||||
data = $.extend(data, {certificateSvg: this.iconTpl()});
|
||||
HtmlUtils.setHtml(this.$el, this.statusTpl(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
class CertificateStatusView extends Backbone.View {
|
||||
initialize(options) {
|
||||
this.statusTpl = HtmlUtils.template(certificateStatusTpl);
|
||||
this.iconTpl = HtmlUtils.template(certificateIconTpl);
|
||||
this.$el = options.$el;
|
||||
this.render();
|
||||
}
|
||||
|
||||
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) {
|
||||
'use strict';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'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({
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||
|
||||
initialize: function(data) {
|
||||
this.childView = data.childView;
|
||||
this.context = data.context;
|
||||
this.titleContext = data.titleContext;
|
||||
},
|
||||
import emptyProgramsListTpl from '../../../templates/learner_dashboard/empty_programs_list.underscore';
|
||||
|
||||
render: function() {
|
||||
var childList;
|
||||
class CollectionListView extends Backbone.View {
|
||||
initialize(data) {
|
||||
this.childView = data.childView;
|
||||
this.context = data.context;
|
||||
this.titleContext = data.titleContext;
|
||||
}
|
||||
|
||||
if (!this.collection.length) {
|
||||
if (this.context.marketingUrl) {
|
||||
// Only show the advertising panel if the link is passed in
|
||||
HtmlUtils.setHtml(this.$el, HtmlUtils.template(emptyProgramsListTpl)(this.context));
|
||||
}
|
||||
} else {
|
||||
childList = [];
|
||||
render() {
|
||||
if (!this.collection.length) {
|
||||
if (this.context.marketingUrl) {
|
||||
// Only show the advertising panel if the link is passed in
|
||||
HtmlUtils.setHtml(this.$el, HtmlUtils.template(emptyProgramsListTpl)(this.context));
|
||||
}
|
||||
} else {
|
||||
const childList = [];
|
||||
|
||||
this.collection.each(function(model) {
|
||||
var child = new this.childView({
|
||||
model: model,
|
||||
context: this.context
|
||||
});
|
||||
childList.push(child.el);
|
||||
}, this);
|
||||
this.collection.each((model) => {
|
||||
const child = new this.childView({ // eslint-disable-line new-cap
|
||||
model,
|
||||
context: this.context,
|
||||
});
|
||||
childList.push(child.el);
|
||||
}, this);
|
||||
|
||||
if (this.titleContext) {
|
||||
this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString());
|
||||
}
|
||||
if (this.titleContext) {
|
||||
this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString());
|
||||
}
|
||||
|
||||
this.$el.html(childList);
|
||||
}
|
||||
},
|
||||
this.$el.html(childList);
|
||||
}
|
||||
}
|
||||
|
||||
getTitleHtml: function() {
|
||||
var titleHtml = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<'),
|
||||
this.titleContext.el,
|
||||
HtmlUtils.HTML(' class="sr-only collection-title">'),
|
||||
StringUtils.interpolate(this.titleContext.title),
|
||||
HtmlUtils.HTML('</'),
|
||||
this.titleContext.el,
|
||||
HtmlUtils.HTML('>'));
|
||||
return titleHtml;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
getTitleHtml() {
|
||||
const titleHtml = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<'),
|
||||
this.titleContext.el,
|
||||
HtmlUtils.HTML(' class="sr-only collection-title">'),
|
||||
StringUtils.interpolate(this.titleContext.title),
|
||||
HtmlUtils.HTML('</'),
|
||||
this.titleContext.el,
|
||||
HtmlUtils.HTML('>'));
|
||||
return titleHtml;
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectionListView;
|
||||
|
||||
@@ -1,130 +1,116 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
define(['backbone',
|
||||
'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',
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
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) {
|
||||
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');
|
||||
import pageTpl from '../../../templates/learner_dashboard/course_card.underscore';
|
||||
|
||||
this.render();
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
class CourseCardView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
className: 'program-course-card',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
render: function() {
|
||||
var data = $.extend(this.model.toJSON(), {
|
||||
enrolled: this.context.enrolled || ''
|
||||
});
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
this.postRender();
|
||||
},
|
||||
initialize(options) {
|
||||
this.tpl = HtmlUtils.template(pageTpl);
|
||||
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');
|
||||
|
||||
postRender: function() {
|
||||
var $upgradeMessage = this.$('.upgrade-message'),
|
||||
$certStatus = this.$('.certificate-status'),
|
||||
$expiredNotification = this.$('.expired-notification'),
|
||||
expired = this.model.get('expired'),
|
||||
courseUUID = this.model.get('uuid'),
|
||||
containerSelector = '#course-' + courseUUID;
|
||||
this.render();
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
}
|
||||
|
||||
this.enrollView = new CourseEnrollView({
|
||||
$parentEl: this.$('.course-actions'),
|
||||
model: this.model,
|
||||
grade: this.grade,
|
||||
collectionCourseStatus: this.collectionCourseStatus,
|
||||
urlModel: this.urlModel,
|
||||
enrollModel: this.enrollModel
|
||||
});
|
||||
render() {
|
||||
const data = $.extend(this.model.toJSON(), {
|
||||
enrolled: this.context.enrolled || '',
|
||||
});
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
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() ?
|
||||
postRender() {
|
||||
const $upgradeMessage = this.$('.upgrade-message');
|
||||
const $certStatus = this.$('.certificate-status');
|
||||
const $expiredNotification = this.$('.expired-notification');
|
||||
const expired = this.model.get('expired');
|
||||
const courseUUID = this.model.get('uuid');
|
||||
const containerSelector = `#course-${courseUUID}`;
|
||||
|
||||
this.enrollView = new CourseEnrollView({
|
||||
$parentEl: this.$('.course-actions'),
|
||||
model: this.model,
|
||||
grade: this.grade,
|
||||
collectionCourseStatus: this.collectionCourseStatus,
|
||||
urlModel: this.urlModel,
|
||||
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,
|
||||
enrollUrl: this.model.get('enroll_url'),
|
||||
courseHomeUrl: this.model.get('course_url'),
|
||||
expiredAt: this.entitlement.expired_at,
|
||||
daysUntilExpiration: this.entitlement.days_until_expiration
|
||||
});
|
||||
}
|
||||
enrollUrl: this.model.get('enroll_url'),
|
||||
courseHomeUrl: this.model.get('course_url'),
|
||||
expiredAt: this.entitlement.expired_at,
|
||||
daysUntilExpiration: this.entitlement.days_until_expiration,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.model.get('upgrade_url') && !(expired === true)) {
|
||||
this.upgradeMessage = new UpgradeMessageView({
|
||||
$el: $upgradeMessage,
|
||||
model: this.model
|
||||
});
|
||||
if (this.model.get('upgrade_url') && !(expired === true)) {
|
||||
this.upgradeMessage = new UpgradeMessageView({
|
||||
$el: $upgradeMessage,
|
||||
model: this.model,
|
||||
});
|
||||
|
||||
$certStatus.remove();
|
||||
} else if (this.model.get('certificate_url') && !(expired === true)) {
|
||||
this.certificateStatus = new CertificateStatusView({
|
||||
$el: $certStatus,
|
||||
model: this.model
|
||||
});
|
||||
$certStatus.remove();
|
||||
} else if (this.model.get('certificate_url') && !(expired === true)) {
|
||||
this.certificateStatus = new CertificateStatusView({
|
||||
$el: $certStatus,
|
||||
model: this.model,
|
||||
});
|
||||
|
||||
$upgradeMessage.remove();
|
||||
} else {
|
||||
// Styles are applied to these elements which will be visible if they're empty.
|
||||
$upgradeMessage.remove();
|
||||
$certStatus.remove();
|
||||
}
|
||||
$upgradeMessage.remove();
|
||||
} else {
|
||||
// Styles are applied to these elements which will be visible if they're empty.
|
||||
$upgradeMessage.remove();
|
||||
$certStatus.remove();
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
this.expiredNotification = new ExpiredNotificationView({
|
||||
$el: $expiredNotification,
|
||||
model: this.model
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
if (expired) {
|
||||
this.expiredNotification = new ExpiredNotificationView({
|
||||
$el: $expiredNotification,
|
||||
model: this.model,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CourseCardView;
|
||||
|
||||
@@ -1,120 +1,111 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
define(['backbone',
|
||||
'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',
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
tpl: HtmlUtils.template(pageTpl),
|
||||
import pageTpl from '../../../templates/learner_dashboard/course_enroll.underscore';
|
||||
|
||||
events: {
|
||||
'click .enroll-button': 'handleEnroll',
|
||||
'change .run-select': 'updateEnrollUrl'
|
||||
},
|
||||
class CourseEnrollView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
className: 'course-enroll-view',
|
||||
events: {
|
||||
'click .enroll-button': 'handleEnroll',
|
||||
'change .run-select': 'updateEnrollUrl',
|
||||
},
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize: function(options) {
|
||||
this.$parentEl = options.$parentEl;
|
||||
this.enrollModel = options.enrollModel;
|
||||
this.urlModel = options.urlModel;
|
||||
this.grade = options.grade;
|
||||
this.collectionCourseStatus = options.collectionCourseStatus;
|
||||
this.render();
|
||||
},
|
||||
initialize(options) {
|
||||
this.tpl = HtmlUtils.template(pageTpl);
|
||||
this.$parentEl = options.$parentEl;
|
||||
this.enrollModel = options.enrollModel;
|
||||
this.urlModel = options.urlModel;
|
||||
this.grade = options.grade;
|
||||
this.collectionCourseStatus = options.collectionCourseStatus;
|
||||
this.render();
|
||||
}
|
||||
|
||||
render: function() {
|
||||
var filledTemplate,
|
||||
context = this.model.toJSON();
|
||||
if (this.$parentEl && this.enrollModel) {
|
||||
context.grade = this.grade;
|
||||
context.collectionCourseStatus = this.collectionCourseStatus;
|
||||
filledTemplate = this.tpl(context);
|
||||
HtmlUtils.setHtml(this.$el, filledTemplate);
|
||||
HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el));
|
||||
}
|
||||
this.postRender();
|
||||
},
|
||||
render() {
|
||||
let filledTemplate;
|
||||
const context = this.model.toJSON();
|
||||
if (this.$parentEl && this.enrollModel) {
|
||||
context.grade = this.grade;
|
||||
context.collectionCourseStatus = this.collectionCourseStatus;
|
||||
filledTemplate = this.tpl(context);
|
||||
HtmlUtils.setHtml(this.$el, filledTemplate);
|
||||
HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el));
|
||||
}
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
postRender: function() {
|
||||
if (this.urlModel) {
|
||||
this.trackSelectionUrl = this.urlModel.get('track_selection_url');
|
||||
}
|
||||
},
|
||||
postRender() {
|
||||
if (this.urlModel) {
|
||||
this.trackSelectionUrl = this.urlModel.get('track_selection_url');
|
||||
}
|
||||
}
|
||||
|
||||
handleEnroll: function() {
|
||||
// Enrollment click event handled here
|
||||
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
|
||||
this.model.updateCourseRun(courseRunKey);
|
||||
if (this.model.get('is_enrolled')) {
|
||||
// Create the enrollment.
|
||||
this.enrollModel.save({
|
||||
course_id: courseRunKey
|
||||
}, {
|
||||
success: _.bind(this.enrollSuccess, this),
|
||||
error: _.bind(this.enrollError, this)
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
handleEnroll() {
|
||||
// Enrollment click event handled here
|
||||
if (this.model.get('is_mobile_only') !== true) {
|
||||
const courseRunKey = $('.run-select').val() || this.model.get('course_run_key');
|
||||
this.model.updateCourseRun(courseRunKey);
|
||||
if (this.model.get('is_enrolled')) {
|
||||
// Create the enrollment.
|
||||
this.enrollModel.save({
|
||||
course_id: courseRunKey,
|
||||
}, {
|
||||
success: _.bind(this.enrollSuccess, this),
|
||||
error: _.bind(this.enrollError, this),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enrollSuccess: function() {
|
||||
var courseRunKey = this.model.get('course_run_key');
|
||||
window.analytics.track('edx.bi.user.program-details.enrollment');
|
||||
if (this.trackSelectionUrl) {
|
||||
// Go to track selection page
|
||||
this.redirect(this.trackSelectionUrl + courseRunKey);
|
||||
} else {
|
||||
this.model.set({
|
||||
is_enrolled: true
|
||||
});
|
||||
}
|
||||
},
|
||||
enrollSuccess() {
|
||||
const courseRunKey = this.model.get('course_run_key');
|
||||
window.analytics.track('edx.bi.user.program-details.enrollment');
|
||||
if (this.trackSelectionUrl) {
|
||||
// Go to track selection page
|
||||
CourseEnrollView.redirect(this.trackSelectionUrl + courseRunKey);
|
||||
} else {
|
||||
this.model.set({
|
||||
is_enrolled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enrollError: function(model, response) {
|
||||
if (response.status === 403 && response.responseJSON.user_message_url) {
|
||||
/**
|
||||
* Check if we've been blocked from the course
|
||||
* because of country access rules.
|
||||
* If so, redirect to a page explaining to the user
|
||||
* why they were blocked.
|
||||
*/
|
||||
this.redirect(response.responseJSON.user_message_url);
|
||||
} else if (this.trackSelectionUrl) {
|
||||
/**
|
||||
* Otherwise, go to the track selection page as usual.
|
||||
* This can occur, for example, when a course does not
|
||||
* have a free enrollment mode, so we can't auto-enroll.
|
||||
*/
|
||||
this.redirect(this.trackSelectionUrl + this.model.get('course_run_key'));
|
||||
}
|
||||
},
|
||||
enrollError(model, response) {
|
||||
if (response.status === 403 && response.responseJSON.user_message_url) {
|
||||
/**
|
||||
* Check if we've been blocked from the course
|
||||
* because of country access rules.
|
||||
* If so, redirect to a page explaining to the user
|
||||
* why they were blocked.
|
||||
*/
|
||||
CourseEnrollView.redirect(response.responseJSON.user_message_url);
|
||||
} else if (this.trackSelectionUrl) {
|
||||
/**
|
||||
* Otherwise, go to the track selection page as usual.
|
||||
* This can occur, for example, when a course does not
|
||||
* have a free enrollment mode, so we can't auto-enroll.
|
||||
*/
|
||||
CourseEnrollView.redirect(this.trackSelectionUrl + this.model.get('course_run_key'));
|
||||
}
|
||||
}
|
||||
|
||||
updateEnrollUrl: function() {
|
||||
if (this.model.get('is_mobile_only') === true) {
|
||||
var courseRunKey = $('.run-select').val(), // eslint-disable-line vars-on-top
|
||||
href = 'edxapp://enroll?course_id=' + courseRunKey + '&email_opt_in=true';
|
||||
$('.enroll-course-button').attr('href', href);
|
||||
}
|
||||
},
|
||||
updateEnrollUrl() {
|
||||
if (this.model.get('is_mobile_only') === true) {
|
||||
const courseRunKey = $('.run-select').val();
|
||||
const href = `edxapp://enroll?course_id=${courseRunKey}&email_opt_in=true`;
|
||||
$('.enroll-course-button').attr('href', href);
|
||||
}
|
||||
}
|
||||
|
||||
redirect: function(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
static redirect(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export default CourseEnrollView;
|
||||
|
||||
@@ -1,419 +1,409 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
/* globals gettext */
|
||||
|
||||
define(['backbone',
|
||||
'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),
|
||||
import 'bootstrap';
|
||||
|
||||
events: {
|
||||
'change .session-select': 'updateEnrollBtn',
|
||||
'click .enroll-btn': 'handleEnrollChange',
|
||||
'keydown .final-confirmation-btn': 'handleVerificationPopoverA11y',
|
||||
'click .popover-dismiss': 'hideDialog'
|
||||
},
|
||||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import moment from 'moment';
|
||||
|
||||
initialize: function(options) {
|
||||
// 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);
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
// Grab URLs that handle changing of enrollment and entering a newly selected session.
|
||||
this.enrollUrl = options.enrollUrl;
|
||||
this.courseHomeUrl = options.courseHomeUrl;
|
||||
import EntitlementModel from '../models/course_entitlement_model';
|
||||
import CourseCardModel from '../models/course_card_model';
|
||||
|
||||
// Grab elements from the parent card that work with this view
|
||||
this.$parentEl = options.$parentEl; // Containing course card (must be a backbone view root el)
|
||||
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
|
||||
import pageTpl from '../../../templates/learner_dashboard/course_entitlement.underscore';
|
||||
import verificationPopoverTpl from '../../../templates/learner_dashboard/verification_popover.underscore';
|
||||
|
||||
// Bind action elements with associated events to objects outside this view
|
||||
this.$dateDisplayField = this.$parentEl ? this.$parentEl.find(options.dateDisplayField) :
|
||||
$(options.dateDisplayField); // Displays current session dates
|
||||
this.$triggerOpenBtn = this.$parentEl ? this.$parentEl.find(options.triggerOpenBtn) :
|
||||
$(options.triggerOpenBtn); // Opens/closes session selection view
|
||||
this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this));
|
||||
class CourseEntitlementView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
events: {
|
||||
'change .session-select': 'updateEnrollBtn',
|
||||
'click .enroll-btn': 'handleEnrollChange',
|
||||
'keydown .final-confirmation-btn': 'handleVerificationPopoverA11y',
|
||||
'click .popover-dismiss': 'hideDialog',
|
||||
},
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
this.render(options);
|
||||
this.postRender();
|
||||
},
|
||||
initialize(options) {
|
||||
this.tpl = HtmlUtils.template(pageTpl);
|
||||
this.verificationTpl = HtmlUtils.template(verificationPopoverTpl);
|
||||
|
||||
render: function() {
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(this.entitlementModel.toJSON()));
|
||||
this.delegateEvents();
|
||||
this.updateEnrollBtn();
|
||||
return this;
|
||||
},
|
||||
// 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: 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() {
|
||||
// Close any visible popovers on click-away
|
||||
$(document).on('click', function(e) {
|
||||
if (this.$('.popover:visible').length &&
|
||||
!($(e.target).closest('.enroll-btn-initial, .popover').length)) {
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
}
|
||||
}.bind(this));
|
||||
// Grab URLs that handle changing of enrollment and entering a newly selected session.
|
||||
this.enrollUrl = options.enrollUrl;
|
||||
this.courseHomeUrl = options.courseHomeUrl;
|
||||
|
||||
// Initialize focus to cancel button on popover load
|
||||
$(document).on('shown.bs.popover', function() {
|
||||
this.$('.final-confirmation-btn:first').focus();
|
||||
}.bind(this));
|
||||
},
|
||||
// Grab elements from the parent card that work with this view
|
||||
this.$parentEl = options.$parentEl; // Containing course card (must be a backbone view root el)
|
||||
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
|
||||
|
||||
handleEnrollChange: function() {
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
var isLeavingSession;
|
||||
// Bind action elements with associated events to objects outside this view
|
||||
this.$dateDisplayField = this.$parentEl ? this.$parentEl.find(options.dateDisplayField) :
|
||||
$(options.dateDisplayField); // Displays current session dates
|
||||
this.$triggerOpenBtn = this.$parentEl ? this.$parentEl.find(options.triggerOpenBtn) :
|
||||
$(options.triggerOpenBtn); // Opens/closes session selection view
|
||||
this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this));
|
||||
|
||||
// Do not allow for enrollment when button is disabled
|
||||
if (this.$('.enroll-btn-initial').hasClass('disabled')) return;
|
||||
this.render(options);
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
// Grab the id for the desired session, an leave session event will return null
|
||||
this.currentSessionSelection = this.$('.session-select')
|
||||
.find('option:selected').data('session_id');
|
||||
isLeavingSession = !this.currentSessionSelection;
|
||||
render() {
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(this.entitlementModel.toJSON()));
|
||||
this.delegateEvents();
|
||||
this.updateEnrollBtn();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Display the indicator icon
|
||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||
HtmlUtils.HTML('<span class="fa fa-spinner fa-spin" aria-hidden="true"></span>')
|
||||
);
|
||||
postRender() {
|
||||
// Close any visible popovers on click-away
|
||||
$(document).on('click', (e) => {
|
||||
if (this.$('.popover:visible').length &&
|
||||
!($(e.target).closest('.enroll-btn-initial, .popover').length)) {
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
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)
|
||||
});
|
||||
},
|
||||
// Initialize focus to cancel button on popover load
|
||||
$(document).on('shown.bs.popover', () => {
|
||||
this.$('.final-confirmation-btn:first').focus();
|
||||
});
|
||||
}
|
||||
|
||||
enrollSuccess: function(data) {
|
||||
/*
|
||||
Update external elements on the course card to represent the now available course session.
|
||||
handleEnrollChange() {
|
||||
/*
|
||||
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.
|
||||
2) Add the new session's dates to the date field on the main course card.
|
||||
3) Hide the 'View Course' button to the course card.
|
||||
*/
|
||||
var successIconEl = '<span class="fa fa-check" aria-hidden="true"></span>';
|
||||
// Grab the id for the desired session, an leave session event will return null
|
||||
this.currentSessionSelection = this.$('.session-select')
|
||||
.find('option:selected').data('session_id');
|
||||
const isLeavingSession = !this.currentSessionSelection;
|
||||
|
||||
// With a containing backbone view, we can simply re-render the parent card
|
||||
if (this.$parentEl) {
|
||||
this.courseCardModel.updateCourseRun(this.currentSessionSelection);
|
||||
return;
|
||||
}
|
||||
// Display the indicator icon
|
||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||
HtmlUtils.HTML('<span class="fa fa-spinner fa-spin" aria-hidden="true"></span>'),
|
||||
);
|
||||
|
||||
// Update the model with the new session Id
|
||||
this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
|
||||
$.ajax({
|
||||
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
|
||||
this.$triggerOpenBtn.removeClass('hidden');
|
||||
enrollSuccess(data) {
|
||||
/*
|
||||
Update external elements on the course card to represent the now available course session.
|
||||
|
||||
// Display a success indicator
|
||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML(successIconEl),
|
||||
this.getAvailableSessionWithId(data.course_run_id).session_dates
|
||||
)
|
||||
);
|
||||
1) Show the change session toggle button.
|
||||
2) Add the new session's dates to the date field on the main course card.
|
||||
3) Hide the 'View Course' button to the course card.
|
||||
*/
|
||||
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
|
||||
this.$enterCourseBtn
|
||||
.attr('href', this.formatCourseHomeUrl(data.course_run_id))
|
||||
.removeClass('hidden')
|
||||
.focus();
|
||||
this.toggleSessionSelectionPanel();
|
||||
},
|
||||
// With a containing backbone view, we can simply re-render the parent card
|
||||
if (this.$parentEl) {
|
||||
this.courseCardModel.updateCourseRun(this.currentSessionSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
unenrollSuccess: function() {
|
||||
/*
|
||||
Update external elements on the course card to represent the unenrolled state.
|
||||
// Update the model with the new session Id
|
||||
this.entitlementModel.set({ currentSessionId: this.currentSessionSelection });
|
||||
|
||||
1) Hide the change session button and the date field.
|
||||
2) Hide the 'View Course' button.
|
||||
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;
|
||||
}
|
||||
// Allow user to change session
|
||||
this.$triggerOpenBtn.removeClass('hidden');
|
||||
|
||||
// Update the model with the new session Id;
|
||||
this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
|
||||
// Display a success indicator
|
||||
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
|
||||
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.'))
|
||||
)
|
||||
);
|
||||
// Ensure the view course button links to new session home page and place focus there
|
||||
this.$enterCourseBtn
|
||||
.attr('href', this.formatCourseHomeUrl(data.course_run_id))
|
||||
.removeClass('hidden')
|
||||
.focus();
|
||||
this.toggleSessionSelectionPanel();
|
||||
}
|
||||
|
||||
// Remove links to previously enrolled sessions
|
||||
this.$courseImageLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
||||
HtmlUtils.joinHtml(
|
||||
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
|
||||
);
|
||||
},
|
||||
unenrollSuccess() {
|
||||
/*
|
||||
Update external elements on the course card to represent the unenrolled state.
|
||||
|
||||
enrollError: function() {
|
||||
// Display a success indicator
|
||||
var errorMsgEl = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<span class="enroll-error">'),
|
||||
gettext('There was an error. Please reload the page and try again.'),
|
||||
HtmlUtils.HTML('</spandiv>')
|
||||
).text;
|
||||
1) Hide the change session button and the date field.
|
||||
2) Hide the 'View Course' button.
|
||||
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;
|
||||
}
|
||||
|
||||
this.$dateDisplayField
|
||||
.find('.fa.fa-spin')
|
||||
.removeClass('fa-spin fa-spinner')
|
||||
.addClass('fa-close');
|
||||
// Update the model with the new session Id;
|
||||
this.entitlementModel.set({ currentSessionId: this.currentSessionSelection });
|
||||
|
||||
this.$dateDisplayField.append(errorMsgEl);
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
},
|
||||
// Reset the card contents to the unenrolled state
|
||||
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() {
|
||||
/*
|
||||
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
|
||||
*/
|
||||
var enrollText,
|
||||
currentSessionId = this.entitlementModel.get('currentSessionId'),
|
||||
newSessionId = this.$('.session-select').find('option:selected').data('session_id'),
|
||||
enrollBtnInitial = this.$('.enroll-btn-initial');
|
||||
// Remove links to previously enrolled sessions
|
||||
this.$courseImageLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
||||
HtmlUtils.joinHtml(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
// Disable the button if the user is already enrolled in that session.
|
||||
if (currentSessionId === newSessionId) {
|
||||
enrollBtnInitial.addClass('disabled');
|
||||
this.removeDialog(enrollBtnInitial);
|
||||
return;
|
||||
}
|
||||
enrollBtnInitial.removeClass('disabled');
|
||||
enrollError() {
|
||||
// Display a success indicator
|
||||
const errorMsgEl = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<span class="enroll-error">'),
|
||||
gettext('There was an error. Please reload the page and try again.'),
|
||||
HtmlUtils.HTML('</spandiv>'),
|
||||
).text;
|
||||
|
||||
// Update button text specifying if the user is initially enrolling, changing or leaving a session.
|
||||
if (newSessionId) {
|
||||
enrollText = currentSessionId ? gettext('Change Session') : gettext('Select Session');
|
||||
} else {
|
||||
enrollText = gettext('Leave Current Session');
|
||||
}
|
||||
enrollBtnInitial.text(enrollText);
|
||||
this.initializeVerificationDialog(enrollBtnInitial);
|
||||
},
|
||||
this.$dateDisplayField
|
||||
.find('.fa.fa-spin')
|
||||
.removeClass('fa-spin fa-spinner')
|
||||
.addClass('fa-close');
|
||||
|
||||
toggleSessionSelectionPanel: function() {
|
||||
/*
|
||||
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();
|
||||
},
|
||||
this.$dateDisplayField.append(errorMsgEl);
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
}
|
||||
|
||||
initializeVerificationDialog: function(invokingElement) {
|
||||
/*
|
||||
Instantiates an instance of the Bootstrap v4 dialog modal and attaches it to the passed in element.
|
||||
updateEnrollBtn() {
|
||||
/*
|
||||
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
|
||||
available course session.
|
||||
*/
|
||||
var confirmationMsgTitle,
|
||||
confirmationMsgBody,
|
||||
currentSessionId = this.entitlementModel.get('currentSessionId'),
|
||||
newSessionId = this.$('.session-select').find('option:selected').data('session_id');
|
||||
// Disable the button if the user is already enrolled in that session.
|
||||
if (currentSessionId === newSessionId) {
|
||||
enrollBtnInitial.addClass('disabled');
|
||||
this.removeDialog(enrollBtnInitial);
|
||||
return;
|
||||
}
|
||||
enrollBtnInitial.removeClass('disabled');
|
||||
|
||||
// Update the button popover text to enable two step authentication.
|
||||
if (newSessionId) {
|
||||
confirmationMsgTitle = !currentSessionId ?
|
||||
gettext('Are you sure you want to select this session?') :
|
||||
gettext('Are you sure you want to change to a different session?');
|
||||
confirmationMsgBody = !currentSessionId ? '' :
|
||||
gettext('Any course progress or grades from your current session will be lost.');
|
||||
} else {
|
||||
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
|
||||
}
|
||||
// Update button text specifying if the user is initially enrolling,
|
||||
// changing or leaving a session.
|
||||
if (newSessionId) {
|
||||
enrollText = currentSessionId ? gettext('Change Session') : gettext('Select Session');
|
||||
} else {
|
||||
enrollText = gettext('Leave Current Session');
|
||||
}
|
||||
enrollBtnInitial.text(enrollText);
|
||||
this.initializeVerificationDialog(enrollBtnInitial);
|
||||
}
|
||||
|
||||
// Re-initialize the popover
|
||||
invokingElement.popover({
|
||||
placement: 'bottom',
|
||||
container: this.$el,
|
||||
html: true,
|
||||
trigger: 'click',
|
||||
content: this.verificationTpl({
|
||||
confirmationMsgTitle: confirmationMsgTitle,
|
||||
confirmationMsgBody: confirmationMsgBody
|
||||
}).text
|
||||
});
|
||||
},
|
||||
toggleSessionSelectionPanel() {
|
||||
/*
|
||||
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();
|
||||
}
|
||||
|
||||
removeDialog: function(el) {
|
||||
/* Removes the Bootstrap v4 dialog modal from the update session enrollment button. */
|
||||
var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
||||
if (this.$('popover').length) {
|
||||
$el.popover('dispose');
|
||||
}
|
||||
},
|
||||
initializeVerificationDialog(invokingElement) {
|
||||
/*
|
||||
Instantiates an instance of the Bootstrap v4 dialog modal and attaches it to the
|
||||
passed in element.
|
||||
|
||||
hideDialog: function(el, returnFocus) {
|
||||
/* Hides the modal if it is visible without removing it from the DOM. */
|
||||
var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
||||
if (this.$('.popover:visible').length) {
|
||||
$el.popover('hide');
|
||||
if (returnFocus) {
|
||||
$el.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
This dialog acts as the second step in verifying the user's action to select, change
|
||||
or leave an available course session.
|
||||
*/
|
||||
let confirmationMsgTitle;
|
||||
let confirmationMsgBody;
|
||||
const currentSessionId = this.entitlementModel.get('currentSessionId');
|
||||
const newSessionId = this.$('.session-select').find('option:selected').data('session_id');
|
||||
|
||||
handleVerificationPopoverA11y: function(e) {
|
||||
/* Ensure that the second step verification popover is treated as an a11y compliant dialog */
|
||||
var $nextButton,
|
||||
$verificationOption = $(e.target),
|
||||
openButton = $(e.target).closest('.course-entitlement-selection-container')
|
||||
.find('.enroll-btn-initial');
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
$nextButton = $verificationOption.is(':first-child') ?
|
||||
// Update the button popover text to enable two step authentication.
|
||||
if (newSessionId) {
|
||||
confirmationMsgTitle = !currentSessionId ?
|
||||
gettext('Are you sure you want to select this session?') :
|
||||
gettext('Are you sure you want to change to a different session?');
|
||||
confirmationMsgBody = !currentSessionId ? '' :
|
||||
gettext('Any course progress or grades from your current session will be lost.');
|
||||
} else {
|
||||
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.prev('.final-confirmation-btn');
|
||||
$nextButton.focus();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.hideDialog(openButton);
|
||||
openButton.focus();
|
||||
}
|
||||
},
|
||||
$nextButton.focus();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.hideDialog(openButton);
|
||||
openButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
formatCourseHomeUrl: function(sessionKey) {
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
var oldSessionKey = this.courseHomeUrl.split('/')
|
||||
formatCourseHomeUrl(sessionKey) {
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
const oldSessionKey = this.courseHomeUrl.split('/')
|
||||
.filter(
|
||||
function(urlParam) {
|
||||
return urlParam.indexOf('+') > 0;
|
||||
}
|
||||
urlParam => urlParam.indexOf('+') > 0,
|
||||
)[0];
|
||||
return this.courseHomeUrl.replace(oldSessionKey, sessionKey);
|
||||
},
|
||||
return this.courseHomeUrl.replace(oldSessionKey, sessionKey);
|
||||
}
|
||||
|
||||
formatDates: function(sessionData) {
|
||||
/*
|
||||
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 start and end dates of the particular session.
|
||||
*/
|
||||
var formattedSessionData = sessionData,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat;
|
||||
// Set the date format string to the user's selected language
|
||||
moment.locale(document.documentElement.lang);
|
||||
dateFormat = moment.localeData().longDateFormat('L').indexOf('DD') >
|
||||
moment.localeData().longDateFormat('L').indexOf('MM') ? 'MMMM D, YYYY' : 'D MMMM, YYYY';
|
||||
formatDates(sessionData) {
|
||||
/*
|
||||
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 start and end dates of the particular session.
|
||||
*/
|
||||
const formattedSessionData = sessionData;
|
||||
let startDate;
|
||||
let endDate;
|
||||
// Set the date format string to the user's selected language
|
||||
moment.locale(document.documentElement.lang);
|
||||
const dateFormat = moment.localeData().longDateFormat('L').indexOf('DD') >
|
||||
moment.localeData().longDateFormat('L').indexOf('MM') ? 'MMMM D, YYYY' : 'D MMMM, YYYY';
|
||||
|
||||
return _.map(formattedSessionData, function(session) {
|
||||
var formattedSession = session;
|
||||
startDate = this.formatDate(formattedSession.start, dateFormat);
|
||||
endDate = this.formatDate(formattedSession.end, dateFormat);
|
||||
formattedSession.enrollment_end = this.formatDate(formattedSession.enrollment_end, dateFormat);
|
||||
formattedSession.session_dates = this.courseCardModel.formatDateString({
|
||||
start_date: startDate,
|
||||
advertised_start: session.advertised_start,
|
||||
end_date: endDate,
|
||||
pacing_type: formattedSession.pacing_type
|
||||
});
|
||||
return formattedSession;
|
||||
}, this);
|
||||
},
|
||||
return _.map(formattedSessionData, (session) => {
|
||||
const formattedSession = session;
|
||||
startDate = CourseEntitlementView.formatDate(formattedSession.start, dateFormat);
|
||||
endDate = CourseEntitlementView.formatDate(formattedSession.end, dateFormat);
|
||||
formattedSession.enrollment_end = CourseEntitlementView.formatDate(
|
||||
formattedSession.enrollment_end,
|
||||
dateFormat);
|
||||
formattedSession.session_dates = this.courseCardModel.formatDateString({
|
||||
start_date: startDate,
|
||||
advertised_start: session.advertised_start,
|
||||
end_date: endDate,
|
||||
pacing_type: formattedSession.pacing_type,
|
||||
});
|
||||
return formattedSession;
|
||||
}, this);
|
||||
}
|
||||
|
||||
formatDate: function(date, dateFormat) {
|
||||
return date ? moment((new Date(date))).format(dateFormat) : '';
|
||||
},
|
||||
static formatDate(date, dateFormat) {
|
||||
return date ? moment((new Date(date))).format(dateFormat) : '';
|
||||
}
|
||||
|
||||
getAvailableSessionWithId: function(sessionId) {
|
||||
/* Returns an available session given a sessionId */
|
||||
return this.entitlementModel.get('availableSessions').find(function(session) {
|
||||
return session.session_id === sessionId;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
getAvailableSessionWithId(sessionId) {
|
||||
/* Returns an available session given a sessionId */
|
||||
return this.entitlementModel.get('availableSessions').find(session => session.session_id === sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export default CourseEntitlementView;
|
||||
|
||||
@@ -1,130 +1,134 @@
|
||||
(function(define) {
|
||||
'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.'),
|
||||
/* globals gettext */
|
||||
|
||||
initialize: function(options) {
|
||||
var view = this;
|
||||
this.dashboardPath = options.dashboardPath;
|
||||
this.signInPath = options.signInPath;
|
||||
import Backbone from 'backbone';
|
||||
|
||||
this.$submitButton = $(this.submitButtonSelector);
|
||||
this.$headerText = $(this.headerTextSelector);
|
||||
this.$errorText = $(this.errorTextSelector);
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
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() {
|
||||
var $trigger = $(this);
|
||||
initialize(options) {
|
||||
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) {
|
||||
window.accessible_modal(
|
||||
'#' + $trigger.attr('id'),
|
||||
view.closeButtonSelector,
|
||||
'#' + view.$el.attr('id'),
|
||||
view.mainPageSelector
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
this.dashboardPath = options.dashboardPath;
|
||||
this.signInPath = options.signInPath;
|
||||
|
||||
handleTrigger: function(event) {
|
||||
var $trigger = $(event.target),
|
||||
courseName = $trigger.data('courseName'),
|
||||
courseNumber = $trigger.data('courseNumber'),
|
||||
apiEndpoint = $trigger.data('entitlementApiEndpoint');
|
||||
this.$submitButton = $(this.submitButtonSelector);
|
||||
this.$headerText = $(this.headerTextSelector);
|
||||
this.$errorText = $(this.errorTextSelector);
|
||||
|
||||
this.resetModal();
|
||||
this.setHeaderText(courseName, courseNumber);
|
||||
this.setSubmitData(apiEndpoint);
|
||||
this.$el.css('position', 'fixed');
|
||||
},
|
||||
this.$submitButton.on('click', this.handleSubmit.bind(this));
|
||||
|
||||
handleSubmit: function() {
|
||||
var apiEndpoint = this.$submitButton.data('entitlementApiEndpoint');
|
||||
$(this.triggerSelector).each(function setUpTrigger() {
|
||||
const $trigger = $(this);
|
||||
|
||||
if (apiEndpoint === undefined) {
|
||||
this.setError(this.genericErrorMsg);
|
||||
return;
|
||||
}
|
||||
$trigger.on('click', view.handleTrigger.bind(view));
|
||||
|
||||
this.$submitButton.prop('disabled', true);
|
||||
$.ajax({
|
||||
url: apiEndpoint,
|
||||
method: 'DELETE',
|
||||
complete: this.onComplete.bind(this)
|
||||
});
|
||||
},
|
||||
if (window.accessible_modal) {
|
||||
window.accessible_modal(
|
||||
`#${$trigger.attr('id')}`,
|
||||
view.closeButtonSelector,
|
||||
`#${view.$el.attr('id')}`,
|
||||
view.mainPageSelector,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetModal: function() {
|
||||
this.$submitButton.removeData();
|
||||
this.$submitButton.prop('disabled', false);
|
||||
this.$headerText.empty();
|
||||
this.$errorText.removeClass('entitlement-unenrollment-modal-error-text-visible');
|
||||
this.$errorText.empty();
|
||||
},
|
||||
handleTrigger(event) {
|
||||
const $trigger = $(event.target);
|
||||
const courseName = $trigger.data('courseName');
|
||||
const courseNumber = $trigger.data('courseNumber');
|
||||
const apiEndpoint = $trigger.data('entitlementApiEndpoint');
|
||||
|
||||
setError: function(message) {
|
||||
this.$submitButton.prop('disabled', true);
|
||||
this.$errorText.empty();
|
||||
HtmlUtils.setHtml(
|
||||
this.resetModal();
|
||||
this.setHeaderText(courseName, courseNumber);
|
||||
this.setSubmitData(apiEndpoint);
|
||||
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,
|
||||
message
|
||||
message,
|
||||
);
|
||||
this.$errorText.addClass('entitlement-unenrollment-modal-error-text-visible');
|
||||
},
|
||||
this.$errorText.addClass('entitlement-unenrollment-modal-error-text-visible');
|
||||
}
|
||||
|
||||
setHeaderText: function(courseName, courseNumber) {
|
||||
this.$headerText.empty();
|
||||
HtmlUtils.setHtml(
|
||||
this.$headerText,
|
||||
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
|
||||
{
|
||||
courseName: courseName,
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
setHeaderText(courseName, courseNumber) {
|
||||
this.$headerText.empty();
|
||||
HtmlUtils.setHtml(
|
||||
this.$headerText,
|
||||
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
|
||||
{
|
||||
courseName,
|
||||
courseNumber,
|
||||
},
|
||||
),
|
||||
);
|
||||
}).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) {
|
||||
'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),
|
||||
import Backbone from 'backbone';
|
||||
|
||||
initialize: function(options) {
|
||||
this.$el = options.$el;
|
||||
this.render();
|
||||
},
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
render: function() {
|
||||
var data = this.model.toJSON();
|
||||
HtmlUtils.setHtml(this.$el, this.expiredNotificationTpl(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
import expiredNotificationTpl from '../../../templates/learner_dashboard/expired_notification.underscore';
|
||||
|
||||
class ExpiredNotificationView extends Backbone.View {
|
||||
initialize(options) {
|
||||
this.expiredNotificationTpl = HtmlUtils.template(expiredNotificationTpl);
|
||||
this.$el = options.$el;
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.model.toJSON();
|
||||
HtmlUtils.setHtml(this.$el, this.expiredNotificationTpl(data));
|
||||
}
|
||||
}
|
||||
|
||||
export default ExpiredNotificationView;
|
||||
|
||||
@@ -1,44 +1,33 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'text!../../../templates/learner_dashboard/explore_new_programs.underscore'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext,
|
||||
exploreTpl
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
el: '.program-advertise',
|
||||
import exploreTpl from '../../../templates/learner_dashboard/explore_new_programs.underscore';
|
||||
|
||||
tpl: _.template(exploreTpl),
|
||||
class ExploreNewProgramsView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.program-advertise',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize: function(data) {
|
||||
this.context = data.context;
|
||||
this.$parentEl = $(this.parentEl);
|
||||
initialize(data) {
|
||||
this.tpl = _.template(exploreTpl);
|
||||
this.context = data.context;
|
||||
this.$parentEl = $(this.parentEl);
|
||||
|
||||
if (this.context.marketingUrl) {
|
||||
// Only render if there is a link
|
||||
this.render();
|
||||
} else {
|
||||
/**
|
||||
* If not rendering remove el because
|
||||
* styles are applied to it
|
||||
*/
|
||||
this.remove();
|
||||
}
|
||||
},
|
||||
if (this.context.marketingUrl) {
|
||||
// Only render if there is a link
|
||||
this.render();
|
||||
} else {
|
||||
// If not rendering, remove el because styles are applied to it
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.tpl(this.context));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
render() {
|
||||
this.$el.html(this.tpl(this.context));
|
||||
}
|
||||
}
|
||||
|
||||
export default ExploreNewProgramsView;
|
||||
|
||||
@@ -1,114 +1,102 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
/* globals gettext */
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'text!../../../templates/learner_dashboard/program_card.underscore',
|
||||
'picturefill'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext,
|
||||
programCardTpl,
|
||||
picturefill
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import picturefill from 'picturefill';
|
||||
|
||||
className: 'program-card',
|
||||
import programCardTpl from '../../../templates/learner_dashboard/program_card.underscore';
|
||||
|
||||
attributes: function() {
|
||||
return {
|
||||
'aria-labelledby': 'program-' + this.model.get('uuid'),
|
||||
role: 'group'
|
||||
};
|
||||
},
|
||||
class ProgramCardView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
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) {
|
||||
this.progressCollection = data.context.progressCollection;
|
||||
if (this.progressCollection) {
|
||||
this.progressModel = this.progressCollection.findWhere({
|
||||
uuid: this.model.get('uuid')
|
||||
});
|
||||
}
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const orgList = _.map(this.model.get('authoring_organizations'), org => gettext(org.key));
|
||||
const data = $.extend(
|
||||
this.model.toJSON(),
|
||||
this.getProgramProgress(),
|
||||
{ orgList: orgList.join(' ') },
|
||||
);
|
||||
}).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) {
|
||||
'use strict';
|
||||
/* globals gettext */
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'common/js/components/views/progress_circle_view',
|
||||
'js/learner_dashboard/views/certificate_list_view',
|
||||
'text!../../../templates/learner_dashboard/program_details_sidebar.underscore'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext,
|
||||
HtmlUtils,
|
||||
StringUtils,
|
||||
ProgramProgressView,
|
||||
CertificateView,
|
||||
sidebarTpl
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
tpl: HtmlUtils.template(sidebarTpl),
|
||||
import Backbone from 'backbone';
|
||||
|
||||
initialize: function(options) {
|
||||
this.courseModel = options.courseModel || {};
|
||||
this.certificateCollection = options.certificateCollection || [];
|
||||
this.programCertificate = this.getProgramCertificate();
|
||||
this.render();
|
||||
},
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||
|
||||
render: function() {
|
||||
var data = $.extend({}, this.model.toJSON(), {
|
||||
programCertificate: this.programCertificate ?
|
||||
this.programCertificate.toJSON() : {}
|
||||
});
|
||||
import CertificateView from './certificate_list_view';
|
||||
import ProgramProgressView from './progress_circle_view';
|
||||
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
this.postRender();
|
||||
},
|
||||
import sidebarTpl from '../../../templates/learner_dashboard/program_details_sidebar.underscore';
|
||||
|
||||
postRender: function() {
|
||||
if (!this.programCertificate) {
|
||||
this.progressModel = new Backbone.Model({
|
||||
title: StringUtils.interpolate(
|
||||
gettext('{type} Progress'),
|
||||
{type: this.model.get('type')}
|
||||
),
|
||||
label: gettext('Earned Certificates'),
|
||||
progress: {
|
||||
completed: this.courseModel.get('completed').length,
|
||||
in_progress: this.courseModel.get('in_progress').length,
|
||||
not_started: this.courseModel.get('not_started').length
|
||||
}
|
||||
});
|
||||
class ProgramDetailsSidebarView extends Backbone.View {
|
||||
initialize(options) {
|
||||
this.tpl = HtmlUtils.template(sidebarTpl);
|
||||
this.courseModel = options.courseModel || {};
|
||||
this.certificateCollection = options.certificateCollection || [];
|
||||
this.programCertificate = this.getProgramCertificate();
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.programProgressView = new ProgramProgressView({
|
||||
el: '.js-program-progress',
|
||||
model: this.progressModel
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const data = $.extend({}, this.model.toJSON(), {
|
||||
programCertificate: this.programCertificate ?
|
||||
this.programCertificate.toJSON() : {},
|
||||
});
|
||||
|
||||
if (this.certificateCollection.length) {
|
||||
this.certificateView = new CertificateView({
|
||||
el: '.js-course-certificates',
|
||||
collection: this.certificateCollection,
|
||||
title: gettext('Earned Certificates')
|
||||
});
|
||||
}
|
||||
},
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
getProgramCertificate: function() {
|
||||
var certificate = this.certificateCollection.findWhere({type: 'program'}),
|
||||
base = '/static/images/programs/program-certificate-';
|
||||
postRender() {
|
||||
if (!this.programCertificate) {
|
||||
this.progressModel = new Backbone.Model({
|
||||
title: StringUtils.interpolate(
|
||||
gettext('{type} Progress'),
|
||||
{ type: this.model.get('type') },
|
||||
),
|
||||
label: gettext('Earned Certificates'),
|
||||
progress: {
|
||||
completed: this.courseModel.get('completed').length,
|
||||
in_progress: this.courseModel.get('in_progress').length,
|
||||
not_started: this.courseModel.get('not_started').length,
|
||||
},
|
||||
});
|
||||
|
||||
if (certificate) {
|
||||
certificate.set({
|
||||
img: base + this.getType() + '.gif'
|
||||
});
|
||||
}
|
||||
this.programProgressView = new ProgramProgressView({
|
||||
el: '.js-program-progress',
|
||||
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() {
|
||||
var type = this.model.get('type').toLowerCase();
|
||||
getProgramCertificate() {
|
||||
const certificate = this.certificateCollection.findWhere({ type: 'program' });
|
||||
const base = '/static/images/programs/program-certificate-';
|
||||
|
||||
return type.replace(/\s+/g, '-');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
if (certificate) {
|
||||
certificate.set({
|
||||
img: `${base + this.getType()}.gif`,
|
||||
});
|
||||
}
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
getType() {
|
||||
const type = this.model.get('type').toLowerCase();
|
||||
|
||||
return type.replace(/\s+/g, '-');
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramDetailsSidebarView;
|
||||
|
||||
@@ -1,137 +1,128 @@
|
||||
(function(define) {
|
||||
'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',
|
||||
/* globals gettext */
|
||||
|
||||
tpl: HtmlUtils.template(pageTpl),
|
||||
import Backbone from 'backbone';
|
||||
|
||||
events: {
|
||||
'click .complete-program': 'trackPurchase'
|
||||
},
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
initialize: function(options) {
|
||||
this.options = options;
|
||||
this.programModel = new Backbone.Model(this.options.programData);
|
||||
this.courseData = new Backbone.Model(this.options.courseData);
|
||||
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
|
||||
this.completedCourseCollection = new CourseCardCollection(
|
||||
this.courseData.get('completed') || [],
|
||||
this.options.userPreferences
|
||||
);
|
||||
this.inProgressCourseCollection = new CourseCardCollection(
|
||||
this.courseData.get('in_progress') || [],
|
||||
this.options.userPreferences
|
||||
);
|
||||
this.remainingCourseCollection = new CourseCardCollection(
|
||||
this.courseData.get('not_started') || [],
|
||||
this.options.userPreferences
|
||||
);
|
||||
import CollectionListView from './collection_list_view';
|
||||
import CourseCardCollection from '../collections/course_card_collection';
|
||||
import CourseCardView from './course_card_view';
|
||||
import HeaderView from './program_header_view';
|
||||
import SidebarView from './program_details_sidebar_view';
|
||||
|
||||
this.render();
|
||||
},
|
||||
import pageTpl from '../../../templates/learner_dashboard/program_details_view.underscore';
|
||||
|
||||
getUrl: function(base, programData) {
|
||||
if (programData.uuid) {
|
||||
return base + '&bundle=' + encodeURIComponent(programData.uuid);
|
||||
}
|
||||
return base;
|
||||
},
|
||||
class ProgramDetailsView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.js-program-details-wrapper',
|
||||
events: {
|
||||
'click .complete-program': 'trackPurchase',
|
||||
},
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
render: function() {
|
||||
var completedCount = this.completedCourseCollection.length,
|
||||
inProgressCount = this.inProgressCourseCollection.length,
|
||||
remainingCount = this.remainingCourseCollection.length,
|
||||
totalCount = completedCount + inProgressCount + remainingCount,
|
||||
buyButtonUrl = this.getUrl(this.options.urls.buy_button_url, this.options.programData),
|
||||
data = {
|
||||
totalCount: totalCount,
|
||||
inProgressCount: inProgressCount,
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
initialize(options) {
|
||||
this.options = options;
|
||||
this.tpl = HtmlUtils.template(pageTpl);
|
||||
this.programModel = new Backbone.Model(this.options.programData);
|
||||
this.courseData = new Backbone.Model(this.options.courseData);
|
||||
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
|
||||
this.completedCourseCollection = new CourseCardCollection(
|
||||
this.courseData.get('completed') || [],
|
||||
this.options.userPreferences,
|
||||
);
|
||||
}).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) {
|
||||
'use strict';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
define(['backbone',
|
||||
'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'
|
||||
}
|
||||
},
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
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() {
|
||||
this.render();
|
||||
},
|
||||
initialize() {
|
||||
this.breakpoints = {
|
||||
min: {
|
||||
medium: '768px',
|
||||
large: '1180px',
|
||||
},
|
||||
};
|
||||
this.tpl = HtmlUtils.template(pageTpl);
|
||||
this.render();
|
||||
}
|
||||
|
||||
getLogo: function() {
|
||||
var logo = false,
|
||||
type = this.model.get('programData').type;
|
||||
getLogo() {
|
||||
const type = this.model.get('programData').type;
|
||||
let logo = false;
|
||||
|
||||
if (type === 'MicroMasters') {
|
||||
logo = MicroMastersLogo;
|
||||
} else if (type === 'XSeries') {
|
||||
logo = XSeriesLogo;
|
||||
} else if (type === 'Professional Certificate') {
|
||||
logo = ProfessionalCertificateLogo;
|
||||
}
|
||||
return logo;
|
||||
},
|
||||
if (type === 'MicroMasters') {
|
||||
logo = MicroMastersLogo;
|
||||
} else if (type === 'XSeries') {
|
||||
logo = XSeriesLogo;
|
||||
} else if (type === 'Professional Certificate') {
|
||||
logo = ProfessionalCertificateLogo;
|
||||
}
|
||||
return logo;
|
||||
}
|
||||
|
||||
render: function() {
|
||||
var data = $.extend(this.model.toJSON(), {
|
||||
breakpoints: this.breakpoints,
|
||||
logo: this.getLogo()
|
||||
});
|
||||
render() {
|
||||
const data = $.extend(this.model.toJSON(), {
|
||||
breakpoints: this.breakpoints,
|
||||
logo: this.getLogo(),
|
||||
});
|
||||
|
||||
if (this.model.get('programData')) {
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
if (this.model.get('programData')) {
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgramHeaderView;
|
||||
|
||||
@@ -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) {
|
||||
'use strict';
|
||||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
define(['backbone',
|
||||
'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',
|
||||
import NewProgramsView from './explore_new_programs_view';
|
||||
|
||||
tpl: _.template(sidebarTpl),
|
||||
import sidebarTpl from '../../../templates/learner_dashboard/sidebar.underscore';
|
||||
|
||||
initialize: function(data) {
|
||||
this.context = data.context;
|
||||
},
|
||||
class SidebarView extends Backbone.View {
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.sidebar',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.tpl(this.context));
|
||||
this.postRender();
|
||||
},
|
||||
initialize(data) {
|
||||
this.tpl = _.template(sidebarTpl);
|
||||
this.context = data.context;
|
||||
}
|
||||
|
||||
postRender: function() {
|
||||
this.newProgramsView = new NewProgramsView({
|
||||
context: this.context
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
render() {
|
||||
this.$el.html(this.tpl(this.context));
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
postRender() {
|
||||
this.newProgramsView = new NewProgramsView({
|
||||
context: this.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarView;
|
||||
|
||||
@@ -1,77 +1,72 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
el: '.unenroll-modal',
|
||||
/* globals gettext */
|
||||
|
||||
switchToSlideOne: function() {
|
||||
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');
|
||||
},
|
||||
import Backbone from 'backbone';
|
||||
|
||||
switchToSlideTwo: function() {
|
||||
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);
|
||||
},
|
||||
class UnenrollView extends Backbone.View {
|
||||
|
||||
unenrollComplete: function(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');
|
||||
}
|
||||
},
|
||||
constructor(options) {
|
||||
const defaults = {
|
||||
el: '.unenroll-modal',
|
||||
};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
|
||||
initialize: function(options) {
|
||||
this.urls = options.urls;
|
||||
this.isEdx = options.isEdx;
|
||||
switchToSlideOne() {
|
||||
// Randomize survey option order
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
switchToSlideTwo() {
|
||||
let 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(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) {
|
||||
'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),
|
||||
import Backbone from 'backbone';
|
||||
|
||||
initialize: function(options) {
|
||||
this.$el = options.$el;
|
||||
this.render();
|
||||
},
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
render: function() {
|
||||
var data = this.model.toJSON();
|
||||
import upgradeMessageTpl from '../../../templates/learner_dashboard/upgrade_message.underscore';
|
||||
|
||||
HtmlUtils.setHtml(this.$el, this.messageTpl(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
class UpgradeMessageView extends Backbone.View {
|
||||
initialize(options) {
|
||||
this.messageTpl = HtmlUtils.template(upgradeMessageTpl);
|
||||
this.$el = options.$el;
|
||||
this.render();
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ var options = {
|
||||
specFiles: [
|
||||
// Define the Webpack-built spec files first
|
||||
{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
|
||||
{pattern: '../**/*spec.js'}
|
||||
|
||||
@@ -32,11 +32,6 @@
|
||||
'js/groups/views/cohorts_dashboard_factory',
|
||||
'js/discussions_management/views/discussions_dashboard_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/views/account_settings_factory',
|
||||
'js/student_account/views/finish_auth_factory',
|
||||
|
||||
@@ -699,6 +699,18 @@
|
||||
'discussion/js/spec/discussion_board_view_spec.js',
|
||||
'discussion/js/spec/views/discussion_user_profile_view_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/ccx/schedule_spec.js',
|
||||
'js/spec/commerce/receipt_view_spec.js',
|
||||
@@ -757,17 +769,6 @@
|
||||
'js/spec/instructor_dashboard/ecommerce_spec.js',
|
||||
'js/spec/instructor_dashboard/membership_auth_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/dateutil_factory_spec.js',
|
||||
'js/spec/navigation_spec.js',
|
||||
|
||||
@@ -49,7 +49,7 @@ from student.models import CourseEnrollment
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<%static:require_module module_name="js/learner_dashboard/unenrollment_factory" class_name="UnenrollmentFactory">
|
||||
<%static:webpack entry="UnenrollmentFactory">
|
||||
UnenrollmentFactory({
|
||||
urls: {
|
||||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||||
@@ -59,8 +59,8 @@ from student.models import CourseEnrollment
|
||||
},
|
||||
isEdx: false
|
||||
});
|
||||
</%static:require_module>
|
||||
<%static:require_module module_name="js/learner_dashboard/entitlement_unenrollment_factory" class_name="EntitlementUnenrollmentFactory">
|
||||
</%static:webpack>
|
||||
<%static:webpack entry="EntitlementUnenrollmentFactory">
|
||||
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
||||
## to ensure events are setup correctly.
|
||||
$(document).ready(function() {
|
||||
@@ -69,7 +69,7 @@ from student.models import CourseEnrollment
|
||||
signInPath: "${reverse('signin_user') | n, js_escaped_string}"
|
||||
});
|
||||
});
|
||||
</%static:require_module>
|
||||
</%static:webpack>
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="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:
|
||||
<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({
|
||||
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 }',
|
||||
@@ -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 }',
|
||||
daysUntilExpiration: '${ entitlement.get_days_until_expiration() | n, js_escaped_string }'
|
||||
});
|
||||
</%static:require_module>
|
||||
</%static:webpack>
|
||||
%endif
|
||||
|
||||
% 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">
|
||||
<%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory">
|
||||
<%static:webpack entry="ProgramDetailsFactory">
|
||||
ProgramDetailsFactory({
|
||||
programData: ${program_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},
|
||||
userPreferences: ${user_preferences | n, dump_js_escaped_json},
|
||||
});
|
||||
</%static:require_module>
|
||||
</%static:webpack>
|
||||
</%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>
|
||||
<section class="program-details-content">
|
||||
<!-- TODO: consider if article is the most appropriate element here -->
|
||||
<article class="program-details-content">
|
||||
<div class="program-heading">
|
||||
<% if (completedCount === totalCount) { %>
|
||||
<h3 class="program-heading-title"><%- gettext('Congratulations!') %></h3>
|
||||
@@ -73,5 +74,5 @@
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
<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">
|
||||
<%static:require_module module_name="js/learner_dashboard/program_list_factory" class_name="ProgramListFactory">
|
||||
<%static:webpack entry="ProgramListFactory">
|
||||
ProgramListFactory({
|
||||
marketingUrl: '${marketing_url | n, js_escaped_string}',
|
||||
programsData: ${programs | n, dump_js_escaped_json},
|
||||
userProgress: ${progress | n, dump_js_escaped_json}
|
||||
});
|
||||
</%static:require_module>
|
||||
</%static:webpack>
|
||||
</%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-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": {
|
||||
"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",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"babel-core": "6.26.0",
|
||||
"babel-loader": "6.4.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-polyfill": "6.26.0",
|
||||
"babel-preset-env": "1.6.1",
|
||||
|
||||
@@ -3,5 +3,5 @@ set -e
|
||||
|
||||
export LOWER_PYLINT_THRESHOLD=1000
|
||||
export UPPER_PYLINT_THRESHOLD=5900
|
||||
export ESLINT_THRESHOLD=5700
|
||||
export ESLINT_THRESHOLD=5580
|
||||
export STYLELINT_THRESHOLD=973
|
||||
|
||||
@@ -56,7 +56,7 @@ from student.models import CourseEnrollment
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<%static:require_module module_name="js/learner_dashboard/unenrollment_factory" class_name="UnenrollmentFactory">
|
||||
<%static:webpack entry="UnenrollmentFactory">
|
||||
UnenrollmentFactory({
|
||||
urls: {
|
||||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||||
@@ -66,8 +66,8 @@ from student.models import CourseEnrollment
|
||||
},
|
||||
isEdx: true
|
||||
});
|
||||
</%static:require_module>
|
||||
<%static:require_module module_name="js/learner_dashboard/entitlement_unenrollment_factory" class_name="EntitlementUnenrollmentFactory">
|
||||
</%static:webpack>
|
||||
<%static:webpack entry="EntitlementUnenrollmentFactory">
|
||||
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
||||
## to ensure events are setup correctly.
|
||||
$(document).ready(function() {
|
||||
@@ -76,7 +76,7 @@ from student.models import CourseEnrollment
|
||||
signInPath: "${reverse('signin_user') | n, js_escaped_string}"
|
||||
});
|
||||
});
|
||||
</%static:require_module>
|
||||
</%static:webpack>
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
||||
DashboardSearchFactory();
|
||||
|
||||
@@ -33,6 +33,13 @@ module.exports = {
|
||||
UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.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
|
||||
CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
|
||||
CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js',
|
||||
@@ -68,7 +75,8 @@ module.exports = {
|
||||
_: 'underscore',
|
||||
$: '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
|
||||
@@ -174,6 +182,7 @@ module.exports = {
|
||||
'node_modules',
|
||||
'common/static/js/vendor/',
|
||||
'cms/static',
|
||||
'common/static/',
|
||||
'common/static/js/src'
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user