Convert learner_dashboard to es2015

This commit is contained in:
Michael Terry
2018-01-18 13:07:44 -05:00
committed by Michael Terry
parent acf7de7c02
commit c9318c3e51
74 changed files with 4035 additions and 4307 deletions

View File

@@ -1,4 +1,7 @@
{
"plugins": [
"transform-object-assign"
],
"presets": [
[
"env",

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);
});
});

View File

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

View File

@@ -0,0 +1,11 @@
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': {
webpack: {
config: 'webpack.dev.config.js',
},
},
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
});
});

View 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');
});
});

View 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',
);
});
});

View File

@@ -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');
});
});
});

View File

@@ -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();
});
});
});
});

View 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);
});
});

View File

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

View File

@@ -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,
);
});
});

View File

@@ -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);
});

View 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);
});
});

View 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">&nbsp; 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 dont 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 wont 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);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
});
});
}
);

View File

@@ -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');
});
});
}
);

View File

@@ -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'
);
});
});
}
);

View File

@@ -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');
});
});
});
}
);

View File

@@ -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();
});
});
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

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

View File

@@ -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
);
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

@@ -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">&nbsp; 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 dont 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 wont 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);
});
});
}
);

View File

@@ -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();
});
});

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -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();

View File

@@ -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'
]
},