From c9318c3e515ca7f3fd23287a42fd3bbfcf78b3bd Mon Sep 17 00:00:00 2001 From: Michael Terry Date: Thu, 18 Jan 2018 13:07:44 -0500 Subject: [PATCH] Convert learner_dashboard to es2015 --- .babelrc | 3 + .../components/views/progress_circle_view.js | 96 --- common/static/common/js/karma.common.conf.js | 9 +- .../components/progress_circle_view_spec.js | 112 --- .../static/common/js/spec/main_requirejs.js | 1 - lms/static/js/learner_dashboard/.eslintrc.js | 11 + .../collections/course_card_collection.js | 25 +- .../collections/program_collection.js | 25 +- .../program_progress_collection.js | 15 +- .../course_entitlement_factory.js | 17 +- .../entitlement_unenrollment_factory.js | 17 +- .../models/course_card_model.js | 470 ++++++----- .../models/course_enroll_model.js | 27 +- .../models/course_entitlement_model.js | 35 +- .../learner_dashboard/models/program_model.js | 60 +- .../program_details_factory.js | 18 +- .../learner_dashboard/program_list_factory.js | 65 +- .../spec/collection_list_view_spec.js | 176 +++++ .../spec/course_card_view_spec.js | 267 +++++++ .../spec/course_enroll_view_spec.js | 303 ++++++++ .../spec/course_entitlement_view_spec.js | 178 +++++ .../entitlement_unenrollment_view_spec.js | 186 +++++ .../spec/program_card_view_spec.js | 130 ++++ .../spec/program_details_header_spec.js | 74 ++ .../spec/program_details_sidebar_view_spec.js | 118 +++ .../spec/program_details_view_spec.js | 626 +++++++++++++++ .../spec/progress_circle_view_spec.js | 104 +++ .../spec/sidebar_view_spec.js | 46 ++ .../spec/unenroll_view_spec.js | 39 + .../learner_dashboard/unenrollment_factory.js | 18 +- .../views/certificate_list_view.js | 52 +- .../views/certificate_status_view.js | 56 +- .../views/collection_list_view.js | 105 ++- .../views/course_card_view.js | 220 +++--- .../views/course_enroll_view.js | 209 +++-- .../views/course_entitlement_view.js | 728 +++++++++--------- .../views/entitlement_unenrollment_view.js | 230 +++--- .../views/expired_notification_view.js | 49 +- .../views/explore_new_programs_view.js | 67 +- .../views/program_card_view.js | 204 +++-- .../views/program_details_sidebar_view.js | 155 ++-- .../views/program_details_view.js | 251 +++--- .../views/program_header_view.js | 96 ++- .../views/progress_circle_view.js | 81 ++ .../learner_dashboard/views/sidebar_view.js | 64 +- .../learner_dashboard/views/unenroll_view.js | 139 ++-- .../views/upgrade_message_view.js | 48 +- .../collection_list_view_spec.js | 185 ----- .../course_card_view_spec.js | 272 ------- .../course_enroll_view_spec.js | 307 -------- .../course_entitlement_view_spec.js | 188 ----- .../entitlement_unenrollment_view_spec.js | 193 ----- .../program_card_view_spec.js | 137 ---- .../program_details_header_spec.js | 77 -- .../program_details_sidebar_view_spec.js | 127 --- .../program_details_view_spec.js | 632 --------------- .../learner_dashboard/sidebar_view_spec.js | 53 -- .../learner_dashboard/unenroll_view_spec.js | 46 -- .../js/spec/staff_debug_actions_spec.js | 2 +- lms/static/karma_lms.conf.js | 1 + lms/static/lms/js/build.js | 5 - lms/static/lms/js/spec/main.js | 23 +- lms/templates/dashboard.html | 8 +- .../dashboard/_dashboard_course_listing.html | 4 +- .../program_details_fragment.html | 8 +- .../program_details_view.underscore | 5 +- .../learner_dashboard/programs_fragment.html | 14 +- .../progress_circle_segment.underscore | 0 .../progress_circle_view.underscore | 0 package-lock.json | 8 + package.json | 1 + scripts/thresholds.sh | 2 +- themes/edx.org/lms/templates/dashboard.html | 8 +- webpack.common.config.js | 11 +- 74 files changed, 4035 insertions(+), 4307 deletions(-) delete mode 100644 common/static/common/js/components/views/progress_circle_view.js delete mode 100644 common/static/common/js/spec/components/progress_circle_view_spec.js create mode 100644 lms/static/js/learner_dashboard/.eslintrc.js create mode 100644 lms/static/js/learner_dashboard/spec/collection_list_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/course_card_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/course_enroll_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/course_entitlement_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/entitlement_unenrollment_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/program_card_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/program_details_header_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/program_details_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/progress_circle_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/sidebar_view_spec.js create mode 100644 lms/static/js/learner_dashboard/spec/unenroll_view_spec.js create mode 100644 lms/static/js/learner_dashboard/views/progress_circle_view.js delete mode 100644 lms/static/js/spec/learner_dashboard/collection_list_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/course_card_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/entitlement_unenrollment_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/program_card_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/program_details_header_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/program_details_sidebar_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/program_details_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/sidebar_view_spec.js delete mode 100644 lms/static/js/spec/learner_dashboard/unenroll_view_spec.js rename {common/static/common/templates/components => lms/templates/learner_dashboard}/progress_circle_segment.underscore (100%) rename {common/static/common/templates/components => lms/templates/learner_dashboard}/progress_circle_view.underscore (100%) diff --git a/.babelrc b/.babelrc index 6c6df4b2b9..20a87235f4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,7 @@ { + "plugins": [ + "transform-object-assign" + ], "presets": [ [ "env", diff --git a/common/static/common/js/components/views/progress_circle_view.js b/common/static/common/js/components/views/progress_circle_view.js deleted file mode 100644 index fbcac9c036..0000000000 --- a/common/static/common/js/components/views/progress_circle_view.js +++ /dev/null @@ -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); diff --git a/common/static/common/js/karma.common.conf.js b/common/static/common/js/karma.common.conf.js index 97e739e01f..938dbe61c4 100644 --- a/common/static/common/js/karma.common.conf.js +++ b/common/static/common/js/karma.common.conf.js @@ -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) { diff --git a/common/static/common/js/spec/components/progress_circle_view_spec.js b/common/static/common/js/spec/components/progress_circle_view_spec.js deleted file mode 100644 index d0eb70f417..0000000000 --- a/common/static/common/js/spec/components/progress_circle_view_spec.js +++ /dev/null @@ -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('
'); - }); - - 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); - }); -}); diff --git a/common/static/common/js/spec/main_requirejs.js b/common/static/common/js/spec/main_requirejs.js index 03d49a2ace..a155145984 100644 --- a/common/static/common/js/spec/main_requirejs.js +++ b/common/static/common/js/spec/main_requirejs.js @@ -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' diff --git a/lms/static/js/learner_dashboard/.eslintrc.js b/lms/static/js/learner_dashboard/.eslintrc.js new file mode 100644 index 0000000000..838b853a82 --- /dev/null +++ b/lms/static/js/learner_dashboard/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: 'eslint-config-edx', + root: true, + settings: { + 'import/resolver': { + webpack: { + config: 'webpack.dev.config.js', + }, + }, + }, +}; diff --git a/lms/static/js/learner_dashboard/collections/course_card_collection.js b/lms/static/js/learner_dashboard/collections/course_card_collection.js index d9d26dbb96..e6b0fb834a 100644 --- a/lms/static/js/learner_dashboard/collections/course_card_collection.js +++ b/lms/static/js/learner_dashboard/collections/course_card_collection.js @@ -1,12 +1,13 @@ -(function(define) { - 'use strict'; - define([ - 'backbone', - 'js/learner_dashboard/models/course_card_model' - ], - function(Backbone, CourseCard) { - return Backbone.Collection.extend({ - model: CourseCard - }); - }); -}).call(this, define || RequireJS.define); +import Backbone from 'backbone'; +import CourseCard from '../models/course_card_model'; + +class CourseCardCollection extends Backbone.Collection { + constructor(models, options) { + const defaults = { + model: CourseCard, + }; + super(models, Object.assign({}, defaults, options)); + } +} + +export default CourseCardCollection; diff --git a/lms/static/js/learner_dashboard/collections/program_collection.js b/lms/static/js/learner_dashboard/collections/program_collection.js index 8644f40fb9..11599255b7 100644 --- a/lms/static/js/learner_dashboard/collections/program_collection.js +++ b/lms/static/js/learner_dashboard/collections/program_collection.js @@ -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; diff --git a/lms/static/js/learner_dashboard/collections/program_progress_collection.js b/lms/static/js/learner_dashboard/collections/program_progress_collection.js index bbf9bce290..705acd8e9e 100644 --- a/lms/static/js/learner_dashboard/collections/program_progress_collection.js +++ b/lms/static/js/learner_dashboard/collections/program_progress_collection.js @@ -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; diff --git a/lms/static/js/learner_dashboard/course_entitlement_factory.js b/lms/static/js/learner_dashboard/course_entitlement_factory.js index 50b18841d2..d1daf2c8c4 100644 --- a/lms/static/js/learner_dashboard/course_entitlement_factory.js +++ b/lms/static/js/learner_dashboard/course_entitlement_factory.js @@ -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 diff --git a/lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js b/lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js index 92828b90cf..7594545f9f 100644 --- a/lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js +++ b/lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js @@ -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 diff --git a/lms/static/js/learner_dashboard/models/course_card_model.js b/lms/static/js/learner_dashboard/models/course_card_model.js index e8459973b5..1c328eacf9 100644 --- a/lms/static/js/learner_dashboard/models/course_card_model.js +++ b/lms/static/js/learner_dashboard/models/course_card_model.js @@ -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; diff --git a/lms/static/js/learner_dashboard/models/course_enroll_model.js b/lms/static/js/learner_dashboard/models/course_enroll_model.js index 6eb58b03b0..9c31b3bf9c 100644 --- a/lms/static/js/learner_dashboard/models/course_enroll_model.js +++ b/lms/static/js/learner_dashboard/models/course_enroll_model.js @@ -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; diff --git a/lms/static/js/learner_dashboard/models/course_entitlement_model.js b/lms/static/js/learner_dashboard/models/course_entitlement_model.js index d80e4fc3e0..6bd257f431 100644 --- a/lms/static/js/learner_dashboard/models/course_entitlement_model.js +++ b/lms/static/js/learner_dashboard/models/course_entitlement_model.js @@ -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; diff --git a/lms/static/js/learner_dashboard/models/program_model.js b/lms/static/js/learner_dashboard/models/program_model.js index 66f937edea..46fdcc78d3 100644 --- a/lms/static/js/learner_dashboard/models/program_model.js +++ b/lms/static/js/learner_dashboard/models/program_model.js @@ -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; diff --git a/lms/static/js/learner_dashboard/program_details_factory.js b/lms/static/js/learner_dashboard/program_details_factory.js index dea1494c9b..7e4df17ac0 100644 --- a/lms/static/js/learner_dashboard/program_details_factory.js +++ b/lms/static/js/learner_dashboard/program_details_factory.js @@ -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 diff --git a/lms/static/js/learner_dashboard/program_list_factory.js b/lms/static/js/learner_dashboard/program_list_factory.js index f5e3bb13ad..c23577576c 100644 --- a/lms/static/js/learner_dashboard/program_list_factory.js +++ b/lms/static/js/learner_dashboard/program_list_factory.js @@ -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 diff --git a/lms/static/js/learner_dashboard/spec/collection_list_view_spec.js b/lms/static/js/learner_dashboard/spec/collection_list_view_spec.js new file mode 100644 index 0000000000..eb5a60d3b0 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/collection_list_view_spec.js @@ -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('
'); + 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('
'); + 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('
'); + 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); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/course_card_view_spec.js b/lms/static/js/learner_dashboard/spec/course_card_view_spec.js new file mode 100644 index 0000000000..726dc2b70b --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/course_card_view_spec.js @@ -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('
'); + 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'); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/course_enroll_view_spec.js b/lms/static/js/learner_dashboard/spec/course_enroll_view_spec.js new file mode 100644 index 0000000000..4f07849843 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/course_enroll_view_spec.js @@ -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('
'); + 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', + ); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/course_entitlement_view_spec.js b/lms/static/js/learner_dashboard/spec/course_entitlement_view_spec.js new file mode 100644 index 0000000000..253a6acfaa --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/course_entitlement_view_spec.js @@ -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('
'); + 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'); + }); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/entitlement_unenrollment_view_spec.js b/lms/static/js/learner_dashboard/spec/entitlement_unenrollment_view_spec.js new file mode 100644 index 0000000000..26149b31a3 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/entitlement_unenrollment_view_spec.js @@ -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 = 'Unenroll ' + + 'Unenroll ' + + '
' + + ' ' + + ' ' + + ' ' + + '
'; + + 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(); + }); + }); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/program_card_view_spec.js b/lms/static/js/learner_dashboard/spec/program_card_view_spec.js new file mode 100644 index 0000000000..e08e5778d2 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/program_card_view_spec.js @@ -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('
'); + 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); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/program_details_header_spec.js b/lms/static/js/learner_dashboard/spec/program_details_header_spec.js new file mode 100644 index 0000000000..3be603accd --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/program_details_header_spec.js @@ -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('
'); + 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`); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js new file mode 100644 index 0000000000..77f884f065 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/program_details_sidebar_view_spec.js @@ -0,0 +1,118 @@ +/* globals setFixtures */ + +import Backbone from 'backbone'; + +import ProgramSidebarView from '../views/program_details_sidebar_view'; + +describe('Program Progress View', () => { + let view = null; + // Don't bother linting the format of the test data + /* eslint-disable */ + const data = { + programData: {"subtitle": "Explore water management concepts and technologies.", "overview": "\u003ch3\u003eXSeries Program Overview\u003c/h3\u003e\n\u003cp\u003eSafe water supply and hygienic water treatment are prerequisites for the well-being of communities all over the world. This Water XSeries, offered by the water management experts of TU Delft, will give you a unique opportunity to gain access to world-class knowledge and expertise in this field.\u003c/p\u003e\n\u003cp\u003eThis 3-course series will cover questions such as: How does climate change affect water cycle and public safety? How to use existing technologies to treat groundwater and surface water so we have safe drinking water? How do we take care of sewage produced in the cities on a daily basis? You will learn what are the physical, chemical and biological processes involved; carry out simple experiments at home; and have the chance to make a basic design of a drinking water treatment plant\u003c/p\u003e", "weeks_to_complete": null, "corporate_endorsements": [], "video": null, "type": "XSeries", "applicable_seat_types": ["verified", "professional", "credit"], "max_hours_effort_per_week": null, "transcript_languages": ["en-us"], "expected_learning_items": [], "uuid": "988e7ea8-f5e2-4d2e-998a-eae4ad3af322", "title": "Water Management", "languages": ["en-us"], "subjects": [{"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/engineering.jpg", "name": "Engineering", "subtitle": "Learn about engineering and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/engineering-1440x210.jpg", "slug": "engineering", "description": "Enroll in an online introduction to engineering course or explore specific areas such as structural, mechanical, electrical, software or aeronautical engineering. EdX offers free online courses in thermodynamics, robot mechanics, aerodynamics and more from top engineering universities."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/biology.jpg", "name": "Biology \u0026 Life Sciences", "subtitle": "Learn about biology and life sciences and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/plant-stomas-1440x210.jpg", "slug": "biology-life-sciences", "description": "Take free online biology courses in genetics, biotechnology, biochemistry, neurobiology and other disciplines. Courses include Fundamentals of Neuroscience from Harvard University, Molecular Biology from MIT and an Introduction to Bioethics from Georgetown."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/science.jpg", "name": "Science", "subtitle": "Learn about science and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/neuron-1440x210.jpg", "slug": "science", "description": "Science is one of the most popular subjects on edX and online courses range from beginner to advanced levels. Areas of study include neuroscience, genotyping, DNA methylation, innovations in environmental science, modern astrophysics and more from top universities and institutions worldwide."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/physics.jpg", "name": "Physics", "subtitle": "Learn about physics and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/header-bg-physics.png", "slug": "physics", "description": "Find online courses in quantum mechanics and magnetism the likes of MIT and Rice University or get an introduction to the violent universe from Australian National University."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/engery.jpg", "name": "Energy \u0026 Earth Sciences", "subtitle": "Learn about energy and earth sciences and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/energy-1440x210.jpg", "slug": "energy-earth-sciences", "description": "EdX\u2019s online Earth sciences courses cover very timely and important issues such as climate change and energy sustainability. Learn about natural disasters and our ability to predict them. Explore the universe with online courses in astrophysics, space plasmas and fusion energy."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/environmental-studies.jpg", "name": "Environmental Studies", "subtitle": "Learn about environmental studies, and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/environment-studies-1440x210.jpg", "slug": "environmental-studies", "description": "Take online courses in environmental science, natural resource management, environmental policy and civic ecology. Learn how to solve complex problems related to pollution control, water treatment and environmental sustainability with free online courses from leading universities worldwide."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/health.jpg", "name": "Health \u0026 Safety", "subtitle": "Learn about health and safety and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/health-and-safety-1440x210.jpg", "slug": "health-safety", "description": "From public health initiatives to personal wellbeing, find online courses covering a wide variety of health and medical subjects. Enroll in free courses from major universities on topics like epidemics, global healthcare and the fundamentals of clinical trials."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/electronics.jpg", "name": "Electronics", "subtitle": "Learn about electronics and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/electronics-a-1440x210.jpg", "slug": "electronics", "description": "The online courses in electrical engineering explore computation structures, electronic interfaces and the principles of electric circuits. Learn the engineering behind drones and autonomous robots or find out how organic electronic devices are changing the way humans interact with machines."}], "individual_endorsements": [], "staff": [{"family_name": "Smets", "uuid": "6078b3dd-ade4-457d-9262-7439a5f4b07e", "bio": "Dr. Arno H.M. Smets is Professor in Solar Energy in the Photovoltaics Material and Devices group at the faculty of Electrical Engineering, Mathematics and Computer Science, Delft University of Technology. From 2005-2010 he worked at the Research Center for Photovoltaics at the National Institute of Advanced Industrial Science and Technology (AIST) in Tsukuba Japan. His research work is focused on processing of thin silicon films, innovative materials and new concepts for photovoltaic applications. He is lecturer for BSc and MSc courses on Photovoltaics and Sustainable Energy at TU Delft. His online edX course on Solar Energy attracted over 150,000 students worldwide. He is co-author of the book \u003cem\u003e\u201cSolar Energy. The physics and engineering of photovoltaic conversion technologies and systems.\u201d\u003c/em\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/arno-smets_x110.jpg", "given_name": "Arno", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Electrical Engineering, Mathematics and Computer Science"}, "works": [], "slug": "arno-smets"}, {"family_name": "van de Giesen", "uuid": "0e28153f-4e9f-4080-b56f-43480600ecd7", "bio": "Since July 2004, Nick van de Giesen has held the Van Kuffeler Chair of Water Resources Management of the Faculty of Civil Engineering and Geosciences. He teaches Integrated Water Resources Management and Water Management. His main interests are the modeling of complex water resources systems and the development of science-based decision support systems. The interaction between water systems and their users is the core theme in both research portfolio and teaching curriculum. Since 1 April 2009, he is chairman of the \u003ca href=\"http://www.environment.tudelft.nl\"\u003eDelft Research Initiative Environment\u003c/a\u003e.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/giesen_vd_nick_110p.jpg", "given_name": "Nick", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "nick-van-de-giesen"}, {"family_name": "Russchenberg", "uuid": "8a94bdb9-ac44-4bc1-a3d2-306f391682b4", "bio": "Herman Russchenberg is engaged in intensive and extensive research into the causes of climate change. His own research involves investigating the role played by clouds and dust particles in the atmosphere, but he is also head of the TU Delft Climate Institute, established in March 2012 to bring together TU Delft researchers working on all aspects of climate and climate change. Russchenberg started out in the faculty of Electrical Engineering, conducting research into the influence of the atmosphere (rain, clouds) on satellite signals. After obtaining his PhD in 1992, he shifted his attention to the physics of water vapour, water droplets, dust particles, sunlight, radiation and emissions in the atmosphere. He is now based in the faculty of Civil Engineering and Geosciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/russchenberg_herman_110p.jpg", "given_name": "Herman", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "herman-russchenberg"}, {"family_name": "Savenije", "uuid": "4ebdcd93-bb4e-4c0c-9faf-4e513b1a2e33", "bio": "Prof. Savenije was born in 1952 in the Netherlands and studied at the Delft University of Technology, in the Netherlands, where he obtained his MSc in 1977 in Hydrology. As a young graduate hydrologist he worked for six years in Mozambique where he developed a theory on salt intrusion in estuaries and studied the hydrology of international rivers. From 1985-1990 he worked as an international consultant mostly in Asia and Africa. He joined academia in 1990 to complete his PhD in 1992. In 1994 he was appointed Professor of Water Resources Management at the IHE (now UNESCO-IHE, Institute for Water Education) in Delft, the Netherlands. Since 1999, he is Professor of Hydrology at the Delft University of Technology, where he is the head of the Water Resources Section. He is President of the International Association of Hydrological Sciences and Executive Editor of the journal Hydrology and Earth System Sciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/savenije_hubert_110p.jpg", "given_name": "Hubert", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "hubert-savenije"}, {"family_name": "Stive", "uuid": "a7364bab-8e9c-4265-bd14-598afac1f086", "bio": "Marcel Stive studied Civil engineering at the Delft University of Technology, where he graduated in 1977 and received his doctorate in 1988. After graduating in 1977 Stive started working at WL-Delft Hydraulics, where he worked until 1992. In 1992 he became a professor at the Polytechnic University of Catalonia in Barcelona, Spain. In 1994 her returned to WL-Delft Hydraulics and at the same time began to work as a professor of Coastal Morphodynamics at the Delft University of Technology. Since 2001 Stive is a professor of Coastal Engineering at Delft University of Technology and he is the scientific director of the Water Research Centre Delft since 2003.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/stive_marcel_110p.jpg", "given_name": "Marcel", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "TU Delft", "title": "Professor"}, "works": [], "slug": "marcel-stive"}, {"family_name": "Rietveld", "uuid": "1b70c71d-20cc-487d-be10-4b31baeff559", "bio": "\u003cp\u003eLuuk Rietveld is professor of Urban Water Cycle Technology at Delft University of Technology. After finalizing his studies in Civil Engineering at Delft University of Technology in 1987, he worked, until 1991, as lecturer/researcher in Sanitary Engineering at the Eduardo Mondlane University, Maputo, Mozambique. Between 1991 and 1994, he was employed at the Management Centre for International Co-operation, and since 1994 he has had an appointment at the Department of Water Management of Delft University of Technology. In 2005, he defended his PhD thesis entitled \"Improving Operation of Drinking Water Treatment through Modelling\".\u003c/p\u003e\n\u003cp\u003eLuuk Rietveld\u2019s main research interests are modelling and optimisation of processes in the urban water cycle, and technological innovations in drinking water treatment and water reclamation for industrial purposes. In addition, he has extensive experience in education, in various cultural contexts, and is interested to explore the use of new ways of teaching through activated and blended learning and MOOCs.\u003c/p\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/rietveld_luuk_110p.jpg", "given_name": "Luuk", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "luuk-rietveld-0"}, {"family_name": "van Halem", "uuid": "4ce9ef2a-19e9-46de-9f34-5d755f26736a", "bio": "Doris van Halem is a tenure track Assistant Professor within the Department of Water Management, section Sanitary Engineering of Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering and Geosciences with a cum laude MSc degree (2007). During her studies she developed an interest in global drinking water challenges, illustrated by her internships in Sri Lanka and Benin, resulting in an MSc thesis \u201cCeramic silver impregnated pot filter for household drinking water treatment in developing countries\u201d. In 2011 she completed her PhD research (with honours) on subsurface iron and arsenic removal for drinking water supply in Bangladesh under the guidance of prof. J.C. van Dijk (TU Delft) and prof. dr. G.L. Amy (Unesco-IHE). Currently she supervises BSc, MSc and PhD students, focusing on inorganic constituent behaviour and trace compound removal during soil passage and drinking water treatment - with a particular interest in smart, pro-poor drinking water solutions.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/doris_van_halem_1.jpg", "given_name": "Doris", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Assistant Professor, Sanitary Engineering"}, "works": [], "slug": "doris-van-halem-0"}, {"family_name": "Grefte", "uuid": "463c3f1a-95fc-45aa-b7c0-d01b14126f02", "bio": "Anke Grefte is project manager open, online and blended education for the Faculty of Civil Engineering and Geosciences, Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering with a master\u2019s thesis entitled \"Behaviour of particles in a drinking water distribution network; test rig results\". For this thesis Anke was awarded the Gijs Oskam award for best young researcher. In November 2013, she finished her Ph.D. research on the removal of Natural Organic Matter (NOM) fractions by ion exchange and the impact on drinking water treatment processes and biological stability.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/grefte_anke_110p.jpg", "given_name": "Anke", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "anke-grefte-0"}, {"family_name": "Lier", "uuid": "349aa2cc-0107-4632-ad10-869f23966049", "bio": "Jules van Lier is full professor of Environmental Engineering and Wastewater Treatment at the Sanitary Engineering Section of Delft University of Technology and has a 1 day per week posted position at the Unesco-IHE Institute for Water Education, also in Delft Jules van Lier accomplished his PhD on Thermophilic Anaerobic Wastewater Treatment under the supervision of Prof. Gatze Lettinga (1995) at Wageningen University. Throughout his career he has been involved as a senior researcher / project manager in various (inter)national research projects, working on cost-effective water treatment for resource recovery (water, nutrients, biogas, elements). His research projects are focused on closing water cycles in industries and sewage water recovery for irrigated agriculture. The further development of anaerobic treatment technology is his prime focus. In addition to university work he is an Executive Board Member and Scientific Advisor to the LeAF Foundation; regional representative for Western Europe Anaerobic Digestion Specialist group of the International Water Association (IWA); editor of scientific journals (e.g Water Science Technology and Advances in Environmental Research and Development); member of the Paques Technological Advisory Commission; and member of the Advisory Board of World-Waternet, Amsterdam.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/lier_van_jules_110p.jpg", "given_name": "Jules van", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Sanitary Engineering"}, "works": [], "slug": "jules-van-lier"}, {"family_name": "Kreuk", "uuid": "c1e50a84-1b09-47b5-b704-5e16309d0cba", "bio": "Merle de Kreuk is a wastewater Associate Professor at the Sanitary Engineering department of the Delft University of Technology. Her research focus is on (municipal and industrial) wastewater treatment systems and anaerobic processes, aiming to link the world of Biotechnology to the Civil Engineering, as well as fundamental research to industrial applications. Her main research topics are hydrolysis processes in anaerobic treatment and granule formation and deterioration. Merle\u2019s PhD and Post-Doc research involved the development of aerobic granular sludge technology and up scaling the technology from a three litre lab scale reactor to the full scale Nereda\u00ae process\u00ae. The first application of aerobic granular sludge technology in the Netherlands was opened in May 2012, and currently many more installations are being built, due to its compactness, low energy use and good effluent characteristics. Her previous work experience also involved the position of water treatment technology innovator at Water authority Hollandse Delta on projects such as the Energy Factory in which 14 water authorities cooperated to develop an energy producing sewage treatment plant.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/kreuk_de_merle_110p.jpg", "given_name": "Merle de", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Associate Professor, Sanitary Engineering"}, "works": [], "slug": "merle-de-kreuk"}], "marketing_slug": "water-management", "marketing_url": "https://stage.edx.org/xseries/water-management", "status": "active", "credit_redemption_overview": "These courses can be taken in any order.", "card_image_url": "https://stage.edx.org/sites/default/files/card/images/waterxseries_course0.png", "faq": [], "price_ranges": [{"currency": "USD", "max": 15.0, "total": 35.0, "min": 10.0}], "banner_image": {"small": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.small.jpg", "width": 435, "height": 145}, "large": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.large.jpg", "width": 1440, "height": 480}, "medium": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.medium.jpg", "width": 726, "height": 242}, "x-small": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.x-small.jpg", "width": 348, "height": 116}}, "authoring_organizations": [{"description": "Delft University of Technology is the largest and oldest technological university in the Netherlands. Our research is inspired by the desire to increase fundamental understanding, as well as by societal challenges. We encourage our students to be independent thinkers so they will become engineers capable of solving complex problems. Our students have chosen Delft University of Technology because of our reputation for quality education and research.", "tags": ["charter", "contributor"], "name": "Delft University of Technology (TU Delft)", "homepage_url": null, "key": "DelftX", "certificate_logo_image_url": null, "marketing_url": "https://stage.edx.org/school/delftx", "logo_image_url": "https://stage.edx.org/sites/default/files/school/image/banner/delft_logo_200x101_0.png", "uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6"}], "job_outlook_items": [], "credit_backing_organizations": [], "weeks_to_complete_min": 4, "weeks_to_complete_max": 8, "min_hours_effort_per_week": null}, + courseData: { + "completed": [{"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "4ce7a648-3172-475a-84f3-9f843b2157f3", "title": "Introduction to Water and Climate", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/wc_home_378x225.jpg", "height": null, "description": null, "width": null}, "key": "Delftx+CTB3300WCx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/wc_home_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "Delftx+CTB3300WCx", "content_language": "en-us", "eligible_for_financial_aid": true, "seats": [{"sku": "18AC1BC", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}, {"sku": "86A734B", "credit_hours": null, "price": "10.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "verified"}], "course_url": "/courses/course-v1:Delftx+CTB3300WCx+2015_T3/", "availability": "Archived", "transcript_languages": ["en-us"], "staff": [{"family_name": "van de Giesen", "uuid": "0e28153f-4e9f-4080-b56f-43480600ecd7", "bio": "Since July 2004, Nick van de Giesen has held the Van Kuffeler Chair of Water Resources Management of the Faculty of Civil Engineering and Geosciences. He teaches Integrated Water Resources Management and Water Management. His main interests are the modeling of complex water resources systems and the development of science-based decision support systems. The interaction between water systems and their users is the core theme in both research portfolio and teaching curriculum. Since 1 April 2009, he is chairman of the \u003ca href=\"http://www.environment.tudelft.nl\"\u003eDelft Research Initiative Environment\u003c/a\u003e.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/giesen_vd_nick_110p.jpg", "given_name": "Nick", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "nick-van-de-giesen"}, {"family_name": "Russchenberg", "uuid": "8a94bdb9-ac44-4bc1-a3d2-306f391682b4", "bio": "Herman Russchenberg is engaged in intensive and extensive research into the causes of climate change. His own research involves investigating the role played by clouds and dust particles in the atmosphere, but he is also head of the TU Delft Climate Institute, established in March 2012 to bring together TU Delft researchers working on all aspects of climate and climate change. Russchenberg started out in the faculty of Electrical Engineering, conducting research into the influence of the atmosphere (rain, clouds) on satellite signals. After obtaining his PhD in 1992, he shifted his attention to the physics of water vapour, water droplets, dust particles, sunlight, radiation and emissions in the atmosphere. He is now based in the faculty of Civil Engineering and Geosciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/russchenberg_herman_110p.jpg", "given_name": "Herman", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "herman-russchenberg"}, {"family_name": "Savenije", "uuid": "4ebdcd93-bb4e-4c0c-9faf-4e513b1a2e33", "bio": "Prof. Savenije was born in 1952 in the Netherlands and studied at the Delft University of Technology, in the Netherlands, where he obtained his MSc in 1977 in Hydrology. As a young graduate hydrologist he worked for six years in Mozambique where he developed a theory on salt intrusion in estuaries and studied the hydrology of international rivers. From 1985-1990 he worked as an international consultant mostly in Asia and Africa. He joined academia in 1990 to complete his PhD in 1992. In 1994 he was appointed Professor of Water Resources Management at the IHE (now UNESCO-IHE, Institute for Water Education) in Delft, the Netherlands. Since 1999, he is Professor of Hydrology at the Delft University of Technology, where he is the head of the Water Resources Section. He is President of the International Association of Hydrological Sciences and Executive Editor of the journal Hydrology and Earth System Sciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/savenije_hubert_110p.jpg", "given_name": "Hubert", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "hubert-savenije"}, {"family_name": "Stive", "uuid": "a7364bab-8e9c-4265-bd14-598afac1f086", "bio": "Marcel Stive studied Civil engineering at the Delft University of Technology, where he graduated in 1977 and received his doctorate in 1988. After graduating in 1977 Stive started working at WL-Delft Hydraulics, where he worked until 1992. In 1992 he became a professor at the Polytechnic University of Catalonia in Barcelona, Spain. In 1994 her returned to WL-Delft Hydraulics and at the same time began to work as a professor of Coastal Morphodynamics at the Delft University of Technology. Since 2001 Stive is a professor of Coastal Engineering at Delft University of Technology and he is the scientific director of the Water Research Centre Delft since 2003.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/stive_marcel_110p.jpg", "given_name": "Marcel", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "TU Delft", "title": "Professor"}, "works": [], "slug": "marcel-stive"}], "announcement": "2015-06-09T00:00:00Z", "end": "2015-11-04T12:00:00Z", "uuid": "a36f5673-6637-11e6-a8e3-22000bdde520", "title": "Introduction to Water and Climate", "certificate_url": "/certificates/a37c59143d9d422eb6ab11e1053b8eb5", "enrollment_start": null, "start": "2015-09-01T04:00:00Z", "min_effort": null, "short_description": "Explore how climate change, water availability, and engineering innovation are key challenges for our planet.", "hidden": false, "level_type": "Intermediate", "type": "verified", "enrollment_open_date": "Jan 01, 1900", "marketing_url": "https://stage.edx.org/course/introduction-water-climate-delftx-ctb3300wcx-0", "is_course_ended": true, "instructors": [], "full_description": "\u003cp\u003eWater is essential for life on earth and of crucial importance for society. Cycling across the planet and the atmosphere, it also has a major influence on our climate.\u003c/p\u003e\n\u003cp\u003eWeekly modules are hosted by four different professors, all of them being international experts in their field. The course consists of knowledge clips, movies, exercises, discussion and homework assignments. It finishes with an examination.\u003c/p\u003e\n\u003cp\u003eThis course combined with the courses \"Introduction to Drinking Water Treatment\" (new edition to start in January 2016) and \"Introduction to the Treatment of Urban Sewage\" (new edition to start in April 2016) forms the Water XSeries, Faculty of Civil Engineering and Geosciences, TU Delft.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003eLICENSE\u003c/strong\u003e\u003cbr /\u003e\nThe course materials of this course are Copyright Delft University of Technology and are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA) 4.0 International License.\u003c/em\u003e\u003c/p\u003e", "key": "course-v1:Delftx+CTB3300WCx+2015_T3", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": true, "modified": "2017-04-06T12:26:52.594942Z", "is_enrolled": false, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=dJEhwq0sXiQ", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/featured-card/wc_home_378x225.jpg", "width": null, "description": null, "height": null}, "description": null}}]}, {"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "a0aade38-7a50-4afb-97cd-2214c572cc86", "title": "Urban Sewage Treatment", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/sewage_home_378x225.jpg", "height": null, "description": null, "width": null}, "key": "DelftX+CTB3365STx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/sewage_home_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "DelftX+CTB3365STx", "content_language": "en-us", "eligible_for_financial_aid": true, "seats": [{"sku": "01CDD4F", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}, {"sku": "B4F253D", "credit_hours": null, "price": "10.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "verified"}], "course_url": "/courses/course-v1:Delftx+CTB3365STx+1T2016/", "availability": "Archived", "transcript_languages": ["en-us"], "staff": [{"family_name": "Lier", "uuid": "349aa2cc-0107-4632-ad10-869f23966049", "bio": "Jules van Lier is full professor of Environmental Engineering and Wastewater Treatment at the Sanitary Engineering Section of Delft University of Technology and has a 1 day per week posted position at the Unesco-IHE Institute for Water Education, also in Delft Jules van Lier accomplished his PhD on Thermophilic Anaerobic Wastewater Treatment under the supervision of Prof. Gatze Lettinga (1995) at Wageningen University. Throughout his career he has been involved as a senior researcher / project manager in various (inter)national research projects, working on cost-effective water treatment for resource recovery (water, nutrients, biogas, elements). His research projects are focused on closing water cycles in industries and sewage water recovery for irrigated agriculture. The further development of anaerobic treatment technology is his prime focus. In addition to university work he is an Executive Board Member and Scientific Advisor to the LeAF Foundation; regional representative for Western Europe Anaerobic Digestion Specialist group of the International Water Association (IWA); editor of scientific journals (e.g Water Science Technology and Advances in Environmental Research and Development); member of the Paques Technological Advisory Commission; and member of the Advisory Board of World-Waternet, Amsterdam.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/lier_van_jules_110p.jpg", "given_name": "Jules van", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Sanitary Engineering"}, "works": [], "slug": "jules-van-lier"}, {"family_name": "Kreuk", "uuid": "c1e50a84-1b09-47b5-b704-5e16309d0cba", "bio": "Merle de Kreuk is a wastewater Associate Professor at the Sanitary Engineering department of the Delft University of Technology. Her research focus is on (municipal and industrial) wastewater treatment systems and anaerobic processes, aiming to link the world of Biotechnology to the Civil Engineering, as well as fundamental research to industrial applications. Her main research topics are hydrolysis processes in anaerobic treatment and granule formation and deterioration. Merle\u2019s PhD and Post-Doc research involved the development of aerobic granular sludge technology and up scaling the technology from a three litre lab scale reactor to the full scale Nereda\u00ae process\u00ae. The first application of aerobic granular sludge technology in the Netherlands was opened in May 2012, and currently many more installations are being built, due to its compactness, low energy use and good effluent characteristics. Her previous work experience also involved the position of water treatment technology innovator at Water authority Hollandse Delta on projects such as the Energy Factory in which 14 water authorities cooperated to develop an energy producing sewage treatment plant.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/kreuk_de_merle_110p.jpg", "given_name": "Merle de", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Associate Professor, Sanitary Engineering"}, "works": [], "slug": "merle-de-kreuk"}], "announcement": "2015-07-24T00:00:00Z", "end": "2016-07-01T22:30:00Z", "uuid": "a36f70c1-6637-11e6-a8e3-22000bdde520", "title": "Introduction to the Treatment of Urban Sewage", "certificate_url": "/certificates/bed3980e67ca40f0b31e309d9dfe9e7e", "enrollment_start": null, "start": "2016-04-12T04:00:00Z", "min_effort": null, "short_description": "Learn about urban water services, focusing on basic sewage treatment technologies.", "hidden": false, "level_type": "Intermediate", "type": "verified", "enrollment_open_date": "Jan 01, 1900", "marketing_url": "https://stage.edx.org/course/introduction-treatment-urban-sewage-delftx-ctb3365stx-0", "is_course_ended": true, "instructors": [], "full_description": "\u003cp\u003eThis course will focus on basic technologies for the treatment of urban sewage. Unit processes involved in the treatment chain will be described as well as the physical, chemical and biological processes involved. There will be an emphasis on water quality and the functionality of each unit process within the treatment chain. After the course one should be able to recognise the process units, describe their function and make simple design calculations on urban sewage treatment plants.\u003c/p\u003e\n\u003cp\u003eThe course consists of 6 modules:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSewage treatment plant overview. In this module you will learn what major pollutants are present in the sewage and why we need to treat sewage prior to discharge to surface waters. The functional units will be briefly discussed\u003c/li\u003e\n\u003cli\u003ePrimary treatment. In this module you learn how coarse material, sand \u0026 grit are removed from the sewage and how to design primary clarification tanks\u003c/li\u003e\n\u003cli\u003eBiological treatment. In this module you learn the basics of the carbon, nitrogen and phosphorous cycle and how biological processes are used to treat the main pollutants of concern.\u003c/li\u003e\n\u003cli\u003eActivated sludge process. In this module you learn the design principles of conventional activated sludge processes including the secondary clarifiers and aeration demand of aeration tanks.\u003c/li\u003e\n\u003cli\u003eNitrogen and phosphorus removal. In this module you learn the principles of biological nitrogen removal as well as phosphorus removal by biological and/or chemical means.\u003c/li\u003e\n\u003cli\u003eSludge treatment. In this module you will the design principles of sludge thickeners, digesters and dewatering facilities for the concentration and stabilisation of excess sewage sludge. Potentials for energy recovery via the produced biogas will be discussed as well as the direct anaerobic treatment of urban sewage in UASB reactors when climate conditions allow.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis course in combination with the courses \"\u003ca href=\"https://www.edx.org/course/introduction-water-climate-delftx-ctb3300wcx-0\"\u003eIntroduction to Water and Climate\u003c/a\u003e\" and \"\u003ca href=\"https://www.edx.org/course/introduction-drinking-water-treatment-delftx-ctb3365dwx-0\"\u003eIntroduction to Drinking Water Treatment\u003c/a\u003e\" forms the Water XSeries, by DelftX.\u003c/p\u003e\n\u003chr /\u003e\n\u003cp\u003e\u003cstrong\u003e\u003cem\u003eLICENSE\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThe course materials of this course are Copyright Delft University of Technology and are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA) 4.0 International License.\u003c/em\u003e\u003c/p\u003e", "key": "course-v1:Delftx+CTB3365STx+1T2016", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": true, "modified": "2017-04-06T12:26:52.679900Z", "is_enrolled": true, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=pcSsOE-F4e8", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/featured-card/sewage_home_378x225.jpg", "width": null, "description": null, "height": null}, "description": null}}]}], + "in_progress": [], "uuid": "988e7ea8-f5e2-4d2e-998a-eae4ad3af322", + "not_started": [{"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "51275d00-1f3f-462f-8231-ce42821cc1dd", "title": "Solar Energy", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/solar-energy_378x225.jpg", "height": null, "description": null, "width": null}, "key": "DelftX+ET3034TUx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/solar-energy_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "DelftX+ET3034TUx", "content_language": null, "eligible_for_financial_aid": true, "seats": [{"sku": "E433FA8", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}], "course_url": "/courses/DelftX/ET3034TUx/2013_Fall/", "availability": "Archived", "transcript_languages": [], "staff": [{"family_name": "Smets", "uuid": "6078b3dd-ade4-457d-9262-7439a5f4b07e", "bio": "Dr. Arno H.M. Smets is Professor in Solar Energy in the Photovoltaics Material and Devices group at the faculty of Electrical Engineering, Mathematics and Computer Science, Delft University of Technology. From 2005-2010 he worked at the Research Center for Photovoltaics at the National Institute of Advanced Industrial Science and Technology (AIST) in Tsukuba Japan. His research work is focused on processing of thin silicon films, innovative materials and new concepts for photovoltaic applications. He is lecturer for BSc and MSc courses on Photovoltaics and Sustainable Energy at TU Delft. His online edX course on Solar Energy attracted over 150,000 students worldwide. He is co-author of the book \u003cem\u003e\u201cSolar Energy. The physics and engineering of photovoltaic conversion technologies and systems.\u201d\u003c/em\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/arno-smets_x110.jpg", "given_name": "Arno", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Electrical Engineering, Mathematics and Computer Science"}, "works": [], "slug": "arno-smets"}], "announcement": "2013-05-08T00:00:00Z", "end": "2013-12-06T10:30:00Z", "uuid": "f33a9660-b5d0-47a9-9bfa-a326d9ed4ef2", "title": "Solar Energy", "certificate_url": null, "enrollment_start": null, "start": "2013-09-16T04:00:00Z", "min_effort": null, "short_description": "Discover the power of solar energy and design a complete photovoltaic system.", "hidden": false, "level_type": null, "type": "honor", "enrollment_open_date": "Jan 01, 1900", "marketing_url": "https://stage.edx.org/course/solar-energy-delftx-et3034tux", "is_course_ended": true, "instructors": [], "full_description": "", "key": "DelftX/ET3034TUx/2013_Fall", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": false, "modified": "2017-04-06T12:26:54.345710Z", "is_enrolled": false, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=LLiNzrIubF0", "image": null, "description": null}}]}, {"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "7c430382-d477-4bac-9c29-f36c24f1935f", "title": "Drinking Water Treatment", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/drinking_water_home_378x225.jpg", "height": null, "description": null, "width": null}, "key": "DelftX+CTB3365DWx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/drinking_water_home_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "DelftX+CTB3365DWx", "content_language": "en-us", "eligible_for_financial_aid": true, "seats": [{"sku": "74AC06B", "credit_hours": 100, "price": "15.00", "currency": "USD", "upgrade_deadline": "2016-04-30T00:00:00Z", "credit_provider": "mit", "type": "credit"}, {"sku": "0BBAE34", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}, {"sku": "8E52FAE", "credit_hours": null, "price": "10.00", "currency": "USD", "upgrade_deadline": "2016-03-25T01:06:00Z", "credit_provider": null, "type": "verified"}], "course_url": "/courses/course-v1:DelftX+CTB3365DWx+1T2016/", "availability": "Current", "transcript_languages": ["en-us"], "staff": [{"family_name": "Rietveld", "uuid": "1b70c71d-20cc-487d-be10-4b31baeff559", "bio": "\u003cp\u003eLuuk Rietveld is professor of Urban Water Cycle Technology at Delft University of Technology. After finalizing his studies in Civil Engineering at Delft University of Technology in 1987, he worked, until 1991, as lecturer/researcher in Sanitary Engineering at the Eduardo Mondlane University, Maputo, Mozambique. Between 1991 and 1994, he was employed at the Management Centre for International Co-operation, and since 1994 he has had an appointment at the Department of Water Management of Delft University of Technology. In 2005, he defended his PhD thesis entitled \"Improving Operation of Drinking Water Treatment through Modelling\".\u003c/p\u003e\n\u003cp\u003eLuuk Rietveld\u2019s main research interests are modelling and optimisation of processes in the urban water cycle, and technological innovations in drinking water treatment and water reclamation for industrial purposes. In addition, he has extensive experience in education, in various cultural contexts, and is interested to explore the use of new ways of teaching through activated and blended learning and MOOCs.\u003c/p\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/rietveld_luuk_110p.jpg", "given_name": "Luuk", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "luuk-rietveld-0"}, {"family_name": "van Halem", "uuid": "4ce9ef2a-19e9-46de-9f34-5d755f26736a", "bio": "Doris van Halem is a tenure track Assistant Professor within the Department of Water Management, section Sanitary Engineering of Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering and Geosciences with a cum laude MSc degree (2007). During her studies she developed an interest in global drinking water challenges, illustrated by her internships in Sri Lanka and Benin, resulting in an MSc thesis \u201cCeramic silver impregnated pot filter for household drinking water treatment in developing countries\u201d. In 2011 she completed her PhD research (with honours) on subsurface iron and arsenic removal for drinking water supply in Bangladesh under the guidance of prof. J.C. van Dijk (TU Delft) and prof. dr. G.L. Amy (Unesco-IHE). Currently she supervises BSc, MSc and PhD students, focusing on inorganic constituent behaviour and trace compound removal during soil passage and drinking water treatment - with a particular interest in smart, pro-poor drinking water solutions.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/doris_van_halem_1.jpg", "given_name": "Doris", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Assistant Professor, Sanitary Engineering"}, "works": [], "slug": "doris-van-halem-0"}, {"family_name": "Grefte", "uuid": "463c3f1a-95fc-45aa-b7c0-d01b14126f02", "bio": "Anke Grefte is project manager open, online and blended education for the Faculty of Civil Engineering and Geosciences, Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering with a master\u2019s thesis entitled \"Behaviour of particles in a drinking water distribution network; test rig results\". For this thesis Anke was awarded the Gijs Oskam award for best young researcher. In November 2013, she finished her Ph.D. research on the removal of Natural Organic Matter (NOM) fractions by ion exchange and the impact on drinking water treatment processes and biological stability.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/grefte_anke_110p.jpg", "given_name": "Anke", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "anke-grefte-0"}], "announcement": "2015-07-24T00:00:00Z", "end": "2017-07-20T21:30:00Z", "uuid": "a36ed16a-6637-11e6-a8e3-22000bdde520", "title": "Introduction to Drinking Water Treatment", "certificate_url": null, "enrollment_start": "2016-06-15T00:00:00Z", "start": "2016-01-12T05:00:00Z", "min_effort": null, "short_description": "Learn about urban water services, focusing on conventional technologies for drinking water treatment.", "hidden": false, "level_type": "Intermediate", "type": "credit", "enrollment_open_date": "Jun 15, 2016", "marketing_url": "https://stage.edx.org/course/introduction-drinking-water-treatment-delftx-ctb3365dwx-0", "is_course_ended": false, "instructors": [], "full_description": "\u003cp\u003eThis course focuses on conventional technologies for drinking water treatment. Unit processes, involved in the treatment chain, are discussed as well as the physical, chemical and biological processes involved. The emphasis is on the effect of treatment on water quality and the dimensions of the unit processes in the treatment chain. After the course one should be able to recognise the process units, describe their function, and make basic calculations for a preliminary design of a drinking water treatment plant.\u003c/p\u003e\n\u003cp\u003eThe course consists of 4 modules:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIntroduction to drinking water treatment. In this module you learn to describe the important disciplines, schemes and evaluation criteria involved in the design phase.\u003c/li\u003e\n\u003cli\u003eWater quality. In this module you learn to identify the drinking water quality parameters to be improved and explain what treatment train or scheme is needed.\u003c/li\u003e\n\u003cli\u003eGroundwater treatment. In this module you learn to calculate the dimensions of the groundwater treatment processes and draw groundwater treatment schemes.\u003c/li\u003e\n\u003cli\u003eSurface water treatment. In this module you learn to calculate the dimensions of the surface water treatment processes and draw surface water treatment schemes.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis course in combination with the courses \"\u003ca href=\"https://www.edx.org/course/introduction-water-climate-delftx-ctb3300wcx-0\"\u003eIntroduction to Water and Climate\u003c/a\u003e\" and \"\u003ca href=\"https://www.edx.org/course/introduction-treatment-urban-sewage-delftx-ctb3365stx\"\u003eIntroduction to the Treatment of Urban Sewage\u003c/a\u003e\" forms the Water XSeries, by DelftX.\u003c/p\u003e\n\u003chr /\u003e\n\u003cp\u003e\u003cstrong\u003e\u003cem\u003eLICENSE\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThe course materials of this course are Copyright Delft University of Technology and are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA) 4.0 International License.\u003c/em\u003e\u003c/p\u003e", "key": "course-v1:DelftX+CTB3365DWx+1T2016", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": true, "modified": "2017-04-06T12:26:52.652365Z", "is_enrolled": false, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=0xPZXLHtRJw", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/featured-card/h20_new_378x225.jpg", "width": null, "description": null, "height": null}, "description": null}}]}]}, + certificateData: [ + { + "url": "/certificates/a37c59143d9d422eb6ab11e1053b8eb5", "type": "course", "title": "Introduction to Water and Climate" + }, { + "url": "/certificates/bed3980e67ca40f0b31e309d9dfe9e7e", "type": "course", "title": "Introduction to the Treatment of Urban Sewage" + } + ], + urls: {"program_listing_url": "/dashboard/programs/", "commerce_api_url": "/api/commerce/v0/baskets/", "track_selection_url": "/course_modes/choose/"}, + userPreferences: {"pref-lang": "en"} + }; + /* eslint-enable */ + let programModel; + let courseData; + let certificateCollection; + + const testCircle = (progress) => { + const $circle = view.$('.progress-circle'); + const incomplete = progress.in_progress.length + progress.not_started.length; + + expect($circle.find('.complete').length).toEqual(progress.completed.length); + expect($circle.find('.incomplete').length).toEqual(incomplete); + }; + + const testText = (progress) => { + const $numbers = view.$('.numbers'); + const total = progress.completed.length + progress.in_progress.length + + progress.not_started.length; + + expect(view.$('.progress-heading').html()).toEqual('XSeries Progress'); + expect(parseInt($numbers.find('.complete').html(), 10)).toEqual(progress.completed.length); + expect(parseInt($numbers.find('.total').html(), 10)).toEqual(total); + }; + + const initView = () => new ProgramSidebarView({ + el: '.js-program-sidebar', + model: programModel, + courseModel: courseData, + certificateCollection, + }); + + beforeEach(() => { + setFixtures('
'); + programModel = new Backbone.Model(data.programData); + courseData = new Backbone.Model(data.courseData); + certificateCollection = new Backbone.Collection(data.certificateData); + }); + + afterEach(() => { + view.remove(); + }); + + it('should exist', () => { + view = initView(); + expect(view).toBeDefined(); + }); + + it('should render the progress view if there is no program certificate', () => { + view = initView(); + testCircle(data.courseData); + testText(data.courseData); + }); + + it('should render the program certificate if earned', () => { + const programCert = { + url: '/program-cert', + type: 'program', + title: 'And Justice For All...', + }; + const altText = `Open the certificate you earned for the ${programCert.title} program.`; + + certificateCollection.add(programCert); + view = initView(); + expect(view.$('.progress-circle-wrapper')[0]).not.toBeInDOM(); + const $certLink = view.$('.program-cert-link'); + expect($certLink[0]).toBeInDOM(); + expect($certLink.attr('href')).toEqual(programCert.url); + expect($certLink.find('.program-cert').attr('alt')).toEqual(altText); + expect(view.$('.certificate-heading')).toHaveText('Your XSeries Certificate'); + }); + + it('should render the course certificate list', () => { + view = initView(); + const $certificates = view.$('.certificate-list .certificate'); + + expect(view.$('.course-list-heading').html()).toEqual('Earned Certificates'); + expect($certificates).toHaveLength(certificateCollection.length); + $certificates.each((i, el) => { + const $link = $(el).find('.certificate-link'); + const model = certificateCollection.at(i); + + expect($link.attr('href')).toEqual(model.get('url')); + expect($link.html()).toEqual(model.get('title')); + }); + }); + + it('should not render the course certificate view if no certificates have been earned', () => { + certificateCollection.reset(); + view = initView(); + expect(view).toBeDefined(); + expect(view.$('.js-course-certificates')).toBeEmpty(); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js new file mode 100644 index 0000000000..7f70193ee3 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js @@ -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: '

Learning University is home to leading creators, entrepreneurs.

', + 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: '

The Test University was chartered in 1868.

', + 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('
'); + }); + + 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, + ); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/progress_circle_view_spec.js b/lms/static/js/learner_dashboard/spec/progress_circle_view_spec.js new file mode 100644 index 0000000000..9f8d31b0dc --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/progress_circle_view_spec.js @@ -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('
'); + }); + + 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); +}); diff --git a/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js b/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js new file mode 100644 index 0000000000..44d7e81ef6 --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js @@ -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(''); + + 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); + }); +}); diff --git a/lms/static/js/learner_dashboard/spec/unenroll_view_spec.js b/lms/static/js/learner_dashboard/spec/unenroll_view_spec.js new file mode 100644 index 0000000000..9f644563df --- /dev/null +++ b/lms/static/js/learner_dashboard/spec/unenroll_view_spec.js @@ -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('
'); // 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); + }); +}); diff --git a/lms/static/js/learner_dashboard/unenrollment_factory.js b/lms/static/js/learner_dashboard/unenrollment_factory.js index 4afca20bbb..b0e734ddb0 100644 --- a/lms/static/js/learner_dashboard/unenrollment_factory.js +++ b/lms/static/js/learner_dashboard/unenrollment_factory.js @@ -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 diff --git a/lms/static/js/learner_dashboard/views/certificate_list_view.js b/lms/static/js/learner_dashboard/views/certificate_list_view.js index 0e009d1868..3951315aae 100644 --- a/lms/static/js/learner_dashboard/views/certificate_list_view.js +++ b/lms/static/js/learner_dashboard/views/certificate_list_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/certificate_status_view.js b/lms/static/js/learner_dashboard/views/certificate_status_view.js index ccdfcec226..76b3d7e0e2 100644 --- a/lms/static/js/learner_dashboard/views/certificate_status_view.js +++ b/lms/static/js/learner_dashboard/views/certificate_status_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/collection_list_view.js b/lms/static/js/learner_dashboard/views/collection_list_view.js index 4bec4ba801..46a807ebc5 100644 --- a/lms/static/js/learner_dashboard/views/collection_list_view.js +++ b/lms/static/js/learner_dashboard/views/collection_list_view.js @@ -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('')); - 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('')); + return titleHtml; + } +} + +export default CollectionListView; diff --git a/lms/static/js/learner_dashboard/views/course_card_view.js b/lms/static/js/learner_dashboard/views/course_card_view.js index 34400b5559..9ad242ba92 100644 --- a/lms/static/js/learner_dashboard/views/course_card_view.js +++ b/lms/static/js/learner_dashboard/views/course_card_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/course_enroll_view.js b/lms/static/js/learner_dashboard/views/course_enroll_view.js index e0d5ecfb7f..b3906e0cfd 100644 --- a/lms/static/js/learner_dashboard/views/course_enroll_view.js +++ b/lms/static/js/learner_dashboard/views/course_enroll_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/course_entitlement_view.js b/lms/static/js/learner_dashboard/views/course_entitlement_view.js index f0d19c3280..272be27dc4 100644 --- a/lms/static/js/learner_dashboard/views/course_entitlement_view.js +++ b/lms/static/js/learner_dashboard/views/course_entitlement_view.js @@ -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('') - ); + 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 = ''; + // 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(''), + ); - // 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 = ''; - // 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(''), - 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('
'), - HtmlUtils.HTML(this.$courseImageLink.html()), - HtmlUtils.HTML('
') - ).text - ); - this.$courseTitleLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion - HtmlUtils.joinHtml( - HtmlUtils.HTML(''), - this.$courseTitleLink.text(), - HtmlUtils.HTML('') - ).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(''), - gettext('There was an error. Please reload the page and try again.'), - HtmlUtils.HTML('') - ).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(''), + 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('
'), + HtmlUtils.HTML(this.$courseImageLink.html()), + HtmlUtils.HTML('
'), + ).text, + ); + this.$courseTitleLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion + HtmlUtils.joinHtml( + HtmlUtils.HTML(''), + this.$courseTitleLink.text(), + HtmlUtils.HTML(''), + ).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(''), + gettext('There was an error. Please reload the page and try again.'), + HtmlUtils.HTML(''), + ).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; diff --git a/lms/static/js/learner_dashboard/views/entitlement_unenrollment_view.js b/lms/static/js/learner_dashboard/views/entitlement_unenrollment_view.js index 840dc09f1c..63dcc1d002 100644 --- a/lms/static/js/learner_dashboard/views/entitlement_unenrollment_view.js +++ b/lms/static/js/learner_dashboard/views/entitlement_unenrollment_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/expired_notification_view.js b/lms/static/js/learner_dashboard/views/expired_notification_view.js index 4b6fb2aaea..43824a86bc 100644 --- a/lms/static/js/learner_dashboard/views/expired_notification_view.js +++ b/lms/static/js/learner_dashboard/views/expired_notification_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/explore_new_programs_view.js b/lms/static/js/learner_dashboard/views/explore_new_programs_view.js index 104452fbec..436fb89b0c 100644 --- a/lms/static/js/learner_dashboard/views/explore_new_programs_view.js +++ b/lms/static/js/learner_dashboard/views/explore_new_programs_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/program_card_view.js b/lms/static/js/learner_dashboard/views/program_card_view.js index 8dafa84e04..d3571b2878 100644 --- a/lms/static/js/learner_dashboard/views/program_card_view.js +++ b/lms/static/js/learner_dashboard/views/program_card_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js b/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js index 275b3f7219..699550cfdf 100644 --- a/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_sidebar_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js index 88034df139..1557079043 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/program_header_view.js b/lms/static/js/learner_dashboard/views/program_header_view.js index 2138140cc5..2055895d03 100644 --- a/lms/static/js/learner_dashboard/views/program_header_view.js +++ b/lms/static/js/learner_dashboard/views/program_header_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/progress_circle_view.js b/lms/static/js/learner_dashboard/views/progress_circle_view.js new file mode 100644 index 0000000000..3ec35859ac --- /dev/null +++ b/lms/static/js/learner_dashboard/views/progress_circle_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/sidebar_view.js b/lms/static/js/learner_dashboard/views/sidebar_view.js index 0ffb750838..0af9b2a394 100644 --- a/lms/static/js/learner_dashboard/views/sidebar_view.js +++ b/lms/static/js/learner_dashboard/views/sidebar_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/unenroll_view.js b/lms/static/js/learner_dashboard/views/unenroll_view.js index 84a8d75612..a1615a5f11 100644 --- a/lms/static/js/learner_dashboard/views/unenroll_view.js +++ b/lms/static/js/learner_dashboard/views/unenroll_view.js @@ -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; diff --git a/lms/static/js/learner_dashboard/views/upgrade_message_view.js b/lms/static/js/learner_dashboard/views/upgrade_message_view.js index c57f91d122..1a66cfbef3 100644 --- a/lms/static/js/learner_dashboard/views/upgrade_message_view.js +++ b/lms/static/js/learner_dashboard/views/upgrade_message_view.js @@ -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; diff --git a/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js b/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js deleted file mode 100644 index c3f7a9b0c0..0000000000 --- a/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js +++ /dev/null @@ -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('
'); - 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('
'); - 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('
'); - 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); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js deleted file mode 100644 index ea7030b3b2..0000000000 --- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js +++ /dev/null @@ -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('
'); - 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'); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js b/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js deleted file mode 100644 index 7a2610f7ad..0000000000 --- a/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js +++ /dev/null @@ -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('
'); - 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' - ); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js b/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js deleted file mode 100644 index c04e935628..0000000000 --- a/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js +++ /dev/null @@ -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('
'); - 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'); - }); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/entitlement_unenrollment_view_spec.js b/lms/static/js/spec/learner_dashboard/entitlement_unenrollment_view_spec.js deleted file mode 100644 index 672d2e543b..0000000000 --- a/lms/static/js/spec/learner_dashboard/entitlement_unenrollment_view_spec.js +++ /dev/null @@ -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 = 'Unenroll ' + - 'Unenroll ' + - '
' + - ' ' + - ' ' + - ' ' + - '
'; - - 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(); - }); - }); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js deleted file mode 100644 index 66b68bae4d..0000000000 --- a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js +++ /dev/null @@ -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('
'); - 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); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/program_details_header_spec.js b/lms/static/js/spec/learner_dashboard/program_details_header_spec.js deleted file mode 100644 index 7829e0be1e..0000000000 --- a/lms/static/js/spec/learner_dashboard/program_details_header_spec.js +++ /dev/null @@ -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('
'); - 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'); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/program_details_sidebar_view_spec.js b/lms/static/js/spec/learner_dashboard/program_details_sidebar_view_spec.js deleted file mode 100644 index d4f9408e65..0000000000 --- a/lms/static/js/spec/learner_dashboard/program_details_sidebar_view_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -define([ - 'backbone', - 'js/learner_dashboard/views/program_details_sidebar_view' -], function(Backbone, ProgramSidebarView) { - 'use strict'; - - describe('Program Progress View', function() { - /* jslint maxlen: 500 */ - var view = null, - // Don't bother linting the format of the test data - /* eslint-disable */ - data = { - programData: {"subtitle": "Explore water management concepts and technologies.", "overview": "\u003ch3\u003eXSeries Program Overview\u003c/h3\u003e\n\u003cp\u003eSafe water supply and hygienic water treatment are prerequisites for the well-being of communities all over the world. This Water XSeries, offered by the water management experts of TU Delft, will give you a unique opportunity to gain access to world-class knowledge and expertise in this field.\u003c/p\u003e\n\u003cp\u003eThis 3-course series will cover questions such as: How does climate change affect water cycle and public safety? How to use existing technologies to treat groundwater and surface water so we have safe drinking water? How do we take care of sewage produced in the cities on a daily basis? You will learn what are the physical, chemical and biological processes involved; carry out simple experiments at home; and have the chance to make a basic design of a drinking water treatment plant\u003c/p\u003e", "weeks_to_complete": null, "corporate_endorsements": [], "video": null, "type": "XSeries", "applicable_seat_types": ["verified", "professional", "credit"], "max_hours_effort_per_week": null, "transcript_languages": ["en-us"], "expected_learning_items": [], "uuid": "988e7ea8-f5e2-4d2e-998a-eae4ad3af322", "title": "Water Management", "languages": ["en-us"], "subjects": [{"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/engineering.jpg", "name": "Engineering", "subtitle": "Learn about engineering and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/engineering-1440x210.jpg", "slug": "engineering", "description": "Enroll in an online introduction to engineering course or explore specific areas such as structural, mechanical, electrical, software or aeronautical engineering. EdX offers free online courses in thermodynamics, robot mechanics, aerodynamics and more from top engineering universities."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/biology.jpg", "name": "Biology \u0026 Life Sciences", "subtitle": "Learn about biology and life sciences and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/plant-stomas-1440x210.jpg", "slug": "biology-life-sciences", "description": "Take free online biology courses in genetics, biotechnology, biochemistry, neurobiology and other disciplines. Courses include Fundamentals of Neuroscience from Harvard University, Molecular Biology from MIT and an Introduction to Bioethics from Georgetown."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/science.jpg", "name": "Science", "subtitle": "Learn about science and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/neuron-1440x210.jpg", "slug": "science", "description": "Science is one of the most popular subjects on edX and online courses range from beginner to advanced levels. Areas of study include neuroscience, genotyping, DNA methylation, innovations in environmental science, modern astrophysics and more from top universities and institutions worldwide."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/physics.jpg", "name": "Physics", "subtitle": "Learn about physics and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/header-bg-physics.png", "slug": "physics", "description": "Find online courses in quantum mechanics and magnetism the likes of MIT and Rice University or get an introduction to the violent universe from Australian National University."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/engery.jpg", "name": "Energy \u0026 Earth Sciences", "subtitle": "Learn about energy and earth sciences and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/energy-1440x210.jpg", "slug": "energy-earth-sciences", "description": "EdX\u2019s online Earth sciences courses cover very timely and important issues such as climate change and energy sustainability. Learn about natural disasters and our ability to predict them. Explore the universe with online courses in astrophysics, space plasmas and fusion energy."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/environmental-studies.jpg", "name": "Environmental Studies", "subtitle": "Learn about environmental studies, and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/environment-studies-1440x210.jpg", "slug": "environmental-studies", "description": "Take online courses in environmental science, natural resource management, environmental policy and civic ecology. Learn how to solve complex problems related to pollution control, water treatment and environmental sustainability with free online courses from leading universities worldwide."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/health.jpg", "name": "Health \u0026 Safety", "subtitle": "Learn about health and safety and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/health-and-safety-1440x210.jpg", "slug": "health-safety", "description": "From public health initiatives to personal wellbeing, find online courses covering a wide variety of health and medical subjects. Enroll in free courses from major universities on topics like epidemics, global healthcare and the fundamentals of clinical trials."}, {"card_image_url": "https://stage.edx.org/sites/default/files/subject/image/card/electronics.jpg", "name": "Electronics", "subtitle": "Learn about electronics and more from the best universities and institutions around the world.", "banner_image_url": "https://stage.edx.org/sites/default/files/electronics-a-1440x210.jpg", "slug": "electronics", "description": "The online courses in electrical engineering explore computation structures, electronic interfaces and the principles of electric circuits. Learn the engineering behind drones and autonomous robots or find out how organic electronic devices are changing the way humans interact with machines."}], "individual_endorsements": [], "staff": [{"family_name": "Smets", "uuid": "6078b3dd-ade4-457d-9262-7439a5f4b07e", "bio": "Dr. Arno H.M. Smets is Professor in Solar Energy in the Photovoltaics Material and Devices group at the faculty of Electrical Engineering, Mathematics and Computer Science, Delft University of Technology. From 2005-2010 he worked at the Research Center for Photovoltaics at the National Institute of Advanced Industrial Science and Technology (AIST) in Tsukuba Japan. His research work is focused on processing of thin silicon films, innovative materials and new concepts for photovoltaic applications. He is lecturer for BSc and MSc courses on Photovoltaics and Sustainable Energy at TU Delft. His online edX course on Solar Energy attracted over 150,000 students worldwide. He is co-author of the book \u003cem\u003e\u201cSolar Energy. The physics and engineering of photovoltaic conversion technologies and systems.\u201d\u003c/em\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/arno-smets_x110.jpg", "given_name": "Arno", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Electrical Engineering, Mathematics and Computer Science"}, "works": [], "slug": "arno-smets"}, {"family_name": "van de Giesen", "uuid": "0e28153f-4e9f-4080-b56f-43480600ecd7", "bio": "Since July 2004, Nick van de Giesen has held the Van Kuffeler Chair of Water Resources Management of the Faculty of Civil Engineering and Geosciences. He teaches Integrated Water Resources Management and Water Management. His main interests are the modeling of complex water resources systems and the development of science-based decision support systems. The interaction between water systems and their users is the core theme in both research portfolio and teaching curriculum. Since 1 April 2009, he is chairman of the \u003ca href=\"http://www.environment.tudelft.nl\"\u003eDelft Research Initiative Environment\u003c/a\u003e.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/giesen_vd_nick_110p.jpg", "given_name": "Nick", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "nick-van-de-giesen"}, {"family_name": "Russchenberg", "uuid": "8a94bdb9-ac44-4bc1-a3d2-306f391682b4", "bio": "Herman Russchenberg is engaged in intensive and extensive research into the causes of climate change. His own research involves investigating the role played by clouds and dust particles in the atmosphere, but he is also head of the TU Delft Climate Institute, established in March 2012 to bring together TU Delft researchers working on all aspects of climate and climate change. Russchenberg started out in the faculty of Electrical Engineering, conducting research into the influence of the atmosphere (rain, clouds) on satellite signals. After obtaining his PhD in 1992, he shifted his attention to the physics of water vapour, water droplets, dust particles, sunlight, radiation and emissions in the atmosphere. He is now based in the faculty of Civil Engineering and Geosciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/russchenberg_herman_110p.jpg", "given_name": "Herman", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "herman-russchenberg"}, {"family_name": "Savenije", "uuid": "4ebdcd93-bb4e-4c0c-9faf-4e513b1a2e33", "bio": "Prof. Savenije was born in 1952 in the Netherlands and studied at the Delft University of Technology, in the Netherlands, where he obtained his MSc in 1977 in Hydrology. As a young graduate hydrologist he worked for six years in Mozambique where he developed a theory on salt intrusion in estuaries and studied the hydrology of international rivers. From 1985-1990 he worked as an international consultant mostly in Asia and Africa. He joined academia in 1990 to complete his PhD in 1992. In 1994 he was appointed Professor of Water Resources Management at the IHE (now UNESCO-IHE, Institute for Water Education) in Delft, the Netherlands. Since 1999, he is Professor of Hydrology at the Delft University of Technology, where he is the head of the Water Resources Section. He is President of the International Association of Hydrological Sciences and Executive Editor of the journal Hydrology and Earth System Sciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/savenije_hubert_110p.jpg", "given_name": "Hubert", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "hubert-savenije"}, {"family_name": "Stive", "uuid": "a7364bab-8e9c-4265-bd14-598afac1f086", "bio": "Marcel Stive studied Civil engineering at the Delft University of Technology, where he graduated in 1977 and received his doctorate in 1988. After graduating in 1977 Stive started working at WL-Delft Hydraulics, where he worked until 1992. In 1992 he became a professor at the Polytechnic University of Catalonia in Barcelona, Spain. In 1994 her returned to WL-Delft Hydraulics and at the same time began to work as a professor of Coastal Morphodynamics at the Delft University of Technology. Since 2001 Stive is a professor of Coastal Engineering at Delft University of Technology and he is the scientific director of the Water Research Centre Delft since 2003.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/stive_marcel_110p.jpg", "given_name": "Marcel", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "TU Delft", "title": "Professor"}, "works": [], "slug": "marcel-stive"}, {"family_name": "Rietveld", "uuid": "1b70c71d-20cc-487d-be10-4b31baeff559", "bio": "\u003cp\u003eLuuk Rietveld is professor of Urban Water Cycle Technology at Delft University of Technology. After finalizing his studies in Civil Engineering at Delft University of Technology in 1987, he worked, until 1991, as lecturer/researcher in Sanitary Engineering at the Eduardo Mondlane University, Maputo, Mozambique. Between 1991 and 1994, he was employed at the Management Centre for International Co-operation, and since 1994 he has had an appointment at the Department of Water Management of Delft University of Technology. In 2005, he defended his PhD thesis entitled \"Improving Operation of Drinking Water Treatment through Modelling\".\u003c/p\u003e\n\u003cp\u003eLuuk Rietveld\u2019s main research interests are modelling and optimisation of processes in the urban water cycle, and technological innovations in drinking water treatment and water reclamation for industrial purposes. In addition, he has extensive experience in education, in various cultural contexts, and is interested to explore the use of new ways of teaching through activated and blended learning and MOOCs.\u003c/p\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/rietveld_luuk_110p.jpg", "given_name": "Luuk", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "luuk-rietveld-0"}, {"family_name": "van Halem", "uuid": "4ce9ef2a-19e9-46de-9f34-5d755f26736a", "bio": "Doris van Halem is a tenure track Assistant Professor within the Department of Water Management, section Sanitary Engineering of Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering and Geosciences with a cum laude MSc degree (2007). During her studies she developed an interest in global drinking water challenges, illustrated by her internships in Sri Lanka and Benin, resulting in an MSc thesis \u201cCeramic silver impregnated pot filter for household drinking water treatment in developing countries\u201d. In 2011 she completed her PhD research (with honours) on subsurface iron and arsenic removal for drinking water supply in Bangladesh under the guidance of prof. J.C. van Dijk (TU Delft) and prof. dr. G.L. Amy (Unesco-IHE). Currently she supervises BSc, MSc and PhD students, focusing on inorganic constituent behaviour and trace compound removal during soil passage and drinking water treatment - with a particular interest in smart, pro-poor drinking water solutions.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/doris_van_halem_1.jpg", "given_name": "Doris", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Assistant Professor, Sanitary Engineering"}, "works": [], "slug": "doris-van-halem-0"}, {"family_name": "Grefte", "uuid": "463c3f1a-95fc-45aa-b7c0-d01b14126f02", "bio": "Anke Grefte is project manager open, online and blended education for the Faculty of Civil Engineering and Geosciences, Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering with a master\u2019s thesis entitled \"Behaviour of particles in a drinking water distribution network; test rig results\". For this thesis Anke was awarded the Gijs Oskam award for best young researcher. In November 2013, she finished her Ph.D. research on the removal of Natural Organic Matter (NOM) fractions by ion exchange and the impact on drinking water treatment processes and biological stability.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/grefte_anke_110p.jpg", "given_name": "Anke", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "anke-grefte-0"}, {"family_name": "Lier", "uuid": "349aa2cc-0107-4632-ad10-869f23966049", "bio": "Jules van Lier is full professor of Environmental Engineering and Wastewater Treatment at the Sanitary Engineering Section of Delft University of Technology and has a 1 day per week posted position at the Unesco-IHE Institute for Water Education, also in Delft Jules van Lier accomplished his PhD on Thermophilic Anaerobic Wastewater Treatment under the supervision of Prof. Gatze Lettinga (1995) at Wageningen University. Throughout his career he has been involved as a senior researcher / project manager in various (inter)national research projects, working on cost-effective water treatment for resource recovery (water, nutrients, biogas, elements). His research projects are focused on closing water cycles in industries and sewage water recovery for irrigated agriculture. The further development of anaerobic treatment technology is his prime focus. In addition to university work he is an Executive Board Member and Scientific Advisor to the LeAF Foundation; regional representative for Western Europe Anaerobic Digestion Specialist group of the International Water Association (IWA); editor of scientific journals (e.g Water Science Technology and Advances in Environmental Research and Development); member of the Paques Technological Advisory Commission; and member of the Advisory Board of World-Waternet, Amsterdam.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/lier_van_jules_110p.jpg", "given_name": "Jules van", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Sanitary Engineering"}, "works": [], "slug": "jules-van-lier"}, {"family_name": "Kreuk", "uuid": "c1e50a84-1b09-47b5-b704-5e16309d0cba", "bio": "Merle de Kreuk is a wastewater Associate Professor at the Sanitary Engineering department of the Delft University of Technology. Her research focus is on (municipal and industrial) wastewater treatment systems and anaerobic processes, aiming to link the world of Biotechnology to the Civil Engineering, as well as fundamental research to industrial applications. Her main research topics are hydrolysis processes in anaerobic treatment and granule formation and deterioration. Merle\u2019s PhD and Post-Doc research involved the development of aerobic granular sludge technology and up scaling the technology from a three litre lab scale reactor to the full scale Nereda\u00ae process\u00ae. The first application of aerobic granular sludge technology in the Netherlands was opened in May 2012, and currently many more installations are being built, due to its compactness, low energy use and good effluent characteristics. Her previous work experience also involved the position of water treatment technology innovator at Water authority Hollandse Delta on projects such as the Energy Factory in which 14 water authorities cooperated to develop an energy producing sewage treatment plant.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/kreuk_de_merle_110p.jpg", "given_name": "Merle de", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Associate Professor, Sanitary Engineering"}, "works": [], "slug": "merle-de-kreuk"}], "marketing_slug": "water-management", "marketing_url": "https://stage.edx.org/xseries/water-management", "status": "active", "credit_redemption_overview": "These courses can be taken in any order.", "card_image_url": "https://stage.edx.org/sites/default/files/card/images/waterxseries_course0.png", "faq": [], "price_ranges": [{"currency": "USD", "max": 15.0, "total": 35.0, "min": 10.0}], "banner_image": {"small": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.small.jpg", "width": 435, "height": 145}, "large": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.large.jpg", "width": 1440, "height": 480}, "medium": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.medium.jpg", "width": 726, "height": 242}, "x-small": {"url": "https://d385l2sek0vys7.cloudfront.net/media/programs/banner_images/988e7ea8-f5e2-4d2e-998a-eae4ad3af322.x-small.jpg", "width": 348, "height": 116}}, "authoring_organizations": [{"description": "Delft University of Technology is the largest and oldest technological university in the Netherlands. Our research is inspired by the desire to increase fundamental understanding, as well as by societal challenges. We encourage our students to be independent thinkers so they will become engineers capable of solving complex problems. Our students have chosen Delft University of Technology because of our reputation for quality education and research.", "tags": ["charter", "contributor"], "name": "Delft University of Technology (TU Delft)", "homepage_url": null, "key": "DelftX", "certificate_logo_image_url": null, "marketing_url": "https://stage.edx.org/school/delftx", "logo_image_url": "https://stage.edx.org/sites/default/files/school/image/banner/delft_logo_200x101_0.png", "uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6"}], "job_outlook_items": [], "credit_backing_organizations": [], "weeks_to_complete_min": 4, "weeks_to_complete_max": 8, "min_hours_effort_per_week": null}, - courseData: { - "completed": [{"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "4ce7a648-3172-475a-84f3-9f843b2157f3", "title": "Introduction to Water and Climate", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/wc_home_378x225.jpg", "height": null, "description": null, "width": null}, "key": "Delftx+CTB3300WCx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/wc_home_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "Delftx+CTB3300WCx", "content_language": "en-us", "eligible_for_financial_aid": true, "seats": [{"sku": "18AC1BC", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}, {"sku": "86A734B", "credit_hours": null, "price": "10.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "verified"}], "course_url": "/courses/course-v1:Delftx+CTB3300WCx+2015_T3/", "availability": "Archived", "transcript_languages": ["en-us"], "staff": [{"family_name": "van de Giesen", "uuid": "0e28153f-4e9f-4080-b56f-43480600ecd7", "bio": "Since July 2004, Nick van de Giesen has held the Van Kuffeler Chair of Water Resources Management of the Faculty of Civil Engineering and Geosciences. He teaches Integrated Water Resources Management and Water Management. His main interests are the modeling of complex water resources systems and the development of science-based decision support systems. The interaction between water systems and their users is the core theme in both research portfolio and teaching curriculum. Since 1 April 2009, he is chairman of the \u003ca href=\"http://www.environment.tudelft.nl\"\u003eDelft Research Initiative Environment\u003c/a\u003e.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/giesen_vd_nick_110p.jpg", "given_name": "Nick", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "nick-van-de-giesen"}, {"family_name": "Russchenberg", "uuid": "8a94bdb9-ac44-4bc1-a3d2-306f391682b4", "bio": "Herman Russchenberg is engaged in intensive and extensive research into the causes of climate change. His own research involves investigating the role played by clouds and dust particles in the atmosphere, but he is also head of the TU Delft Climate Institute, established in March 2012 to bring together TU Delft researchers working on all aspects of climate and climate change. Russchenberg started out in the faculty of Electrical Engineering, conducting research into the influence of the atmosphere (rain, clouds) on satellite signals. After obtaining his PhD in 1992, he shifted his attention to the physics of water vapour, water droplets, dust particles, sunlight, radiation and emissions in the atmosphere. He is now based in the faculty of Civil Engineering and Geosciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/russchenberg_herman_110p.jpg", "given_name": "Herman", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "herman-russchenberg"}, {"family_name": "Savenije", "uuid": "4ebdcd93-bb4e-4c0c-9faf-4e513b1a2e33", "bio": "Prof. Savenije was born in 1952 in the Netherlands and studied at the Delft University of Technology, in the Netherlands, where he obtained his MSc in 1977 in Hydrology. As a young graduate hydrologist he worked for six years in Mozambique where he developed a theory on salt intrusion in estuaries and studied the hydrology of international rivers. From 1985-1990 he worked as an international consultant mostly in Asia and Africa. He joined academia in 1990 to complete his PhD in 1992. In 1994 he was appointed Professor of Water Resources Management at the IHE (now UNESCO-IHE, Institute for Water Education) in Delft, the Netherlands. Since 1999, he is Professor of Hydrology at the Delft University of Technology, where he is the head of the Water Resources Section. He is President of the International Association of Hydrological Sciences and Executive Editor of the journal Hydrology and Earth System Sciences.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/savenije_hubert_110p.jpg", "given_name": "Hubert", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "hubert-savenije"}, {"family_name": "Stive", "uuid": "a7364bab-8e9c-4265-bd14-598afac1f086", "bio": "Marcel Stive studied Civil engineering at the Delft University of Technology, where he graduated in 1977 and received his doctorate in 1988. After graduating in 1977 Stive started working at WL-Delft Hydraulics, where he worked until 1992. In 1992 he became a professor at the Polytechnic University of Catalonia in Barcelona, Spain. In 1994 her returned to WL-Delft Hydraulics and at the same time began to work as a professor of Coastal Morphodynamics at the Delft University of Technology. Since 2001 Stive is a professor of Coastal Engineering at Delft University of Technology and he is the scientific director of the Water Research Centre Delft since 2003.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/stive_marcel_110p.jpg", "given_name": "Marcel", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "TU Delft", "title": "Professor"}, "works": [], "slug": "marcel-stive"}], "announcement": "2015-06-09T00:00:00Z", "end": "2015-11-04T12:00:00Z", "uuid": "a36f5673-6637-11e6-a8e3-22000bdde520", "title": "Introduction to Water and Climate", "certificate_url": "/certificates/a37c59143d9d422eb6ab11e1053b8eb5", "enrollment_start": null, "start": "2015-09-01T04:00:00Z", "min_effort": null, "short_description": "Explore how climate change, water availability, and engineering innovation are key challenges for our planet.", "hidden": false, "level_type": "Intermediate", "type": "verified", "enrollment_open_date": "Jan 01, 1900", "marketing_url": "https://stage.edx.org/course/introduction-water-climate-delftx-ctb3300wcx-0", "is_course_ended": true, "instructors": [], "full_description": "\u003cp\u003eWater is essential for life on earth and of crucial importance for society. Cycling across the planet and the atmosphere, it also has a major influence on our climate.\u003c/p\u003e\n\u003cp\u003eWeekly modules are hosted by four different professors, all of them being international experts in their field. The course consists of knowledge clips, movies, exercises, discussion and homework assignments. It finishes with an examination.\u003c/p\u003e\n\u003cp\u003eThis course combined with the courses \"Introduction to Drinking Water Treatment\" (new edition to start in January 2016) and \"Introduction to the Treatment of Urban Sewage\" (new edition to start in April 2016) forms the Water XSeries, Faculty of Civil Engineering and Geosciences, TU Delft.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003eLICENSE\u003c/strong\u003e\u003cbr /\u003e\nThe course materials of this course are Copyright Delft University of Technology and are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA) 4.0 International License.\u003c/em\u003e\u003c/p\u003e", "key": "course-v1:Delftx+CTB3300WCx+2015_T3", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": true, "modified": "2017-04-06T12:26:52.594942Z", "is_enrolled": false, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=dJEhwq0sXiQ", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/featured-card/wc_home_378x225.jpg", "width": null, "description": null, "height": null}, "description": null}}]}, {"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "a0aade38-7a50-4afb-97cd-2214c572cc86", "title": "Urban Sewage Treatment", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/sewage_home_378x225.jpg", "height": null, "description": null, "width": null}, "key": "DelftX+CTB3365STx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/sewage_home_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "DelftX+CTB3365STx", "content_language": "en-us", "eligible_for_financial_aid": true, "seats": [{"sku": "01CDD4F", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}, {"sku": "B4F253D", "credit_hours": null, "price": "10.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "verified"}], "course_url": "/courses/course-v1:Delftx+CTB3365STx+1T2016/", "availability": "Archived", "transcript_languages": ["en-us"], "staff": [{"family_name": "Lier", "uuid": "349aa2cc-0107-4632-ad10-869f23966049", "bio": "Jules van Lier is full professor of Environmental Engineering and Wastewater Treatment at the Sanitary Engineering Section of Delft University of Technology and has a 1 day per week posted position at the Unesco-IHE Institute for Water Education, also in Delft Jules van Lier accomplished his PhD on Thermophilic Anaerobic Wastewater Treatment under the supervision of Prof. Gatze Lettinga (1995) at Wageningen University. Throughout his career he has been involved as a senior researcher / project manager in various (inter)national research projects, working on cost-effective water treatment for resource recovery (water, nutrients, biogas, elements). His research projects are focused on closing water cycles in industries and sewage water recovery for irrigated agriculture. The further development of anaerobic treatment technology is his prime focus. In addition to university work he is an Executive Board Member and Scientific Advisor to the LeAF Foundation; regional representative for Western Europe Anaerobic Digestion Specialist group of the International Water Association (IWA); editor of scientific journals (e.g Water Science Technology and Advances in Environmental Research and Development); member of the Paques Technological Advisory Commission; and member of the Advisory Board of World-Waternet, Amsterdam.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/lier_van_jules_110p.jpg", "given_name": "Jules van", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Sanitary Engineering"}, "works": [], "slug": "jules-van-lier"}, {"family_name": "Kreuk", "uuid": "c1e50a84-1b09-47b5-b704-5e16309d0cba", "bio": "Merle de Kreuk is a wastewater Associate Professor at the Sanitary Engineering department of the Delft University of Technology. Her research focus is on (municipal and industrial) wastewater treatment systems and anaerobic processes, aiming to link the world of Biotechnology to the Civil Engineering, as well as fundamental research to industrial applications. Her main research topics are hydrolysis processes in anaerobic treatment and granule formation and deterioration. Merle\u2019s PhD and Post-Doc research involved the development of aerobic granular sludge technology and up scaling the technology from a three litre lab scale reactor to the full scale Nereda\u00ae process\u00ae. The first application of aerobic granular sludge technology in the Netherlands was opened in May 2012, and currently many more installations are being built, due to its compactness, low energy use and good effluent characteristics. Her previous work experience also involved the position of water treatment technology innovator at Water authority Hollandse Delta on projects such as the Energy Factory in which 14 water authorities cooperated to develop an energy producing sewage treatment plant.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/kreuk_de_merle_110p.jpg", "given_name": "Merle de", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Associate Professor, Sanitary Engineering"}, "works": [], "slug": "merle-de-kreuk"}], "announcement": "2015-07-24T00:00:00Z", "end": "2016-07-01T22:30:00Z", "uuid": "a36f70c1-6637-11e6-a8e3-22000bdde520", "title": "Introduction to the Treatment of Urban Sewage", "certificate_url": "/certificates/bed3980e67ca40f0b31e309d9dfe9e7e", "enrollment_start": null, "start": "2016-04-12T04:00:00Z", "min_effort": null, "short_description": "Learn about urban water services, focusing on basic sewage treatment technologies.", "hidden": false, "level_type": "Intermediate", "type": "verified", "enrollment_open_date": "Jan 01, 1900", "marketing_url": "https://stage.edx.org/course/introduction-treatment-urban-sewage-delftx-ctb3365stx-0", "is_course_ended": true, "instructors": [], "full_description": "\u003cp\u003eThis course will focus on basic technologies for the treatment of urban sewage. Unit processes involved in the treatment chain will be described as well as the physical, chemical and biological processes involved. There will be an emphasis on water quality and the functionality of each unit process within the treatment chain. After the course one should be able to recognise the process units, describe their function and make simple design calculations on urban sewage treatment plants.\u003c/p\u003e\n\u003cp\u003eThe course consists of 6 modules:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSewage treatment plant overview. In this module you will learn what major pollutants are present in the sewage and why we need to treat sewage prior to discharge to surface waters. The functional units will be briefly discussed\u003c/li\u003e\n\u003cli\u003ePrimary treatment. In this module you learn how coarse material, sand \u0026 grit are removed from the sewage and how to design primary clarification tanks\u003c/li\u003e\n\u003cli\u003eBiological treatment. In this module you learn the basics of the carbon, nitrogen and phosphorous cycle and how biological processes are used to treat the main pollutants of concern.\u003c/li\u003e\n\u003cli\u003eActivated sludge process. In this module you learn the design principles of conventional activated sludge processes including the secondary clarifiers and aeration demand of aeration tanks.\u003c/li\u003e\n\u003cli\u003eNitrogen and phosphorus removal. In this module you learn the principles of biological nitrogen removal as well as phosphorus removal by biological and/or chemical means.\u003c/li\u003e\n\u003cli\u003eSludge treatment. In this module you will the design principles of sludge thickeners, digesters and dewatering facilities for the concentration and stabilisation of excess sewage sludge. Potentials for energy recovery via the produced biogas will be discussed as well as the direct anaerobic treatment of urban sewage in UASB reactors when climate conditions allow.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis course in combination with the courses \"\u003ca href=\"https://www.edx.org/course/introduction-water-climate-delftx-ctb3300wcx-0\"\u003eIntroduction to Water and Climate\u003c/a\u003e\" and \"\u003ca href=\"https://www.edx.org/course/introduction-drinking-water-treatment-delftx-ctb3365dwx-0\"\u003eIntroduction to Drinking Water Treatment\u003c/a\u003e\" forms the Water XSeries, by DelftX.\u003c/p\u003e\n\u003chr /\u003e\n\u003cp\u003e\u003cstrong\u003e\u003cem\u003eLICENSE\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThe course materials of this course are Copyright Delft University of Technology and are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA) 4.0 International License.\u003c/em\u003e\u003c/p\u003e", "key": "course-v1:Delftx+CTB3365STx+1T2016", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": true, "modified": "2017-04-06T12:26:52.679900Z", "is_enrolled": true, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=pcSsOE-F4e8", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/featured-card/sewage_home_378x225.jpg", "width": null, "description": null, "height": null}, "description": null}}]}], - "in_progress": [], "uuid": "988e7ea8-f5e2-4d2e-998a-eae4ad3af322", - "not_started": [{"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "51275d00-1f3f-462f-8231-ce42821cc1dd", "title": "Solar Energy", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/solar-energy_378x225.jpg", "height": null, "description": null, "width": null}, "key": "DelftX+ET3034TUx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/solar-energy_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "DelftX+ET3034TUx", "content_language": null, "eligible_for_financial_aid": true, "seats": [{"sku": "E433FA8", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}], "course_url": "/courses/DelftX/ET3034TUx/2013_Fall/", "availability": "Archived", "transcript_languages": [], "staff": [{"family_name": "Smets", "uuid": "6078b3dd-ade4-457d-9262-7439a5f4b07e", "bio": "Dr. Arno H.M. Smets is Professor in Solar Energy in the Photovoltaics Material and Devices group at the faculty of Electrical Engineering, Mathematics and Computer Science, Delft University of Technology. From 2005-2010 he worked at the Research Center for Photovoltaics at the National Institute of Advanced Industrial Science and Technology (AIST) in Tsukuba Japan. His research work is focused on processing of thin silicon films, innovative materials and new concepts for photovoltaic applications. He is lecturer for BSc and MSc courses on Photovoltaics and Sustainable Energy at TU Delft. His online edX course on Solar Energy attracted over 150,000 students worldwide. He is co-author of the book \u003cem\u003e\u201cSolar Energy. The physics and engineering of photovoltaic conversion technologies and systems.\u201d\u003c/em\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/arno-smets_x110.jpg", "given_name": "Arno", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Professor, Electrical Engineering, Mathematics and Computer Science"}, "works": [], "slug": "arno-smets"}], "announcement": "2013-05-08T00:00:00Z", "end": "2013-12-06T10:30:00Z", "uuid": "f33a9660-b5d0-47a9-9bfa-a326d9ed4ef2", "title": "Solar Energy", "certificate_url": null, "enrollment_start": null, "start": "2013-09-16T04:00:00Z", "min_effort": null, "short_description": "Discover the power of solar energy and design a complete photovoltaic system.", "hidden": false, "level_type": null, "type": "honor", "enrollment_open_date": "Jan 01, 1900", "marketing_url": "https://stage.edx.org/course/solar-energy-delftx-et3034tux", "is_course_ended": true, "instructors": [], "full_description": "", "key": "DelftX/ET3034TUx/2013_Fall", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": false, "modified": "2017-04-06T12:26:54.345710Z", "is_enrolled": false, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=LLiNzrIubF0", "image": null, "description": null}}]}, {"owners": [{"uuid": "c484a523-d396-4aff-90f4-bb7e82e16bf6", "key": "DelftX", "name": "Delft University of Technology (TU Delft)"}], "uuid": "7c430382-d477-4bac-9c29-f36c24f1935f", "title": "Drinking Water Treatment", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/drinking_water_home_378x225.jpg", "height": null, "description": null, "width": null}, "key": "DelftX+CTB3365DWx", "course_runs": [{"upgrade_url": null, "image": {"src": "https://stage.edx.org/sites/default/files/course/image/promoted/drinking_water_home_378x225.jpg", "height": null, "description": null, "width": null}, "max_effort": null, "is_enrollment_open": true, "course": "DelftX+CTB3365DWx", "content_language": "en-us", "eligible_for_financial_aid": true, "seats": [{"sku": "74AC06B", "credit_hours": 100, "price": "15.00", "currency": "USD", "upgrade_deadline": "2016-04-30T00:00:00Z", "credit_provider": "mit", "type": "credit"}, {"sku": "0BBAE34", "credit_hours": null, "price": "0.00", "currency": "USD", "upgrade_deadline": null, "credit_provider": null, "type": "honor"}, {"sku": "8E52FAE", "credit_hours": null, "price": "10.00", "currency": "USD", "upgrade_deadline": "2016-03-25T01:06:00Z", "credit_provider": null, "type": "verified"}], "course_url": "/courses/course-v1:DelftX+CTB3365DWx+1T2016/", "availability": "Current", "transcript_languages": ["en-us"], "staff": [{"family_name": "Rietveld", "uuid": "1b70c71d-20cc-487d-be10-4b31baeff559", "bio": "\u003cp\u003eLuuk Rietveld is professor of Urban Water Cycle Technology at Delft University of Technology. After finalizing his studies in Civil Engineering at Delft University of Technology in 1987, he worked, until 1991, as lecturer/researcher in Sanitary Engineering at the Eduardo Mondlane University, Maputo, Mozambique. Between 1991 and 1994, he was employed at the Management Centre for International Co-operation, and since 1994 he has had an appointment at the Department of Water Management of Delft University of Technology. In 2005, he defended his PhD thesis entitled \"Improving Operation of Drinking Water Treatment through Modelling\".\u003c/p\u003e\n\u003cp\u003eLuuk Rietveld\u2019s main research interests are modelling and optimisation of processes in the urban water cycle, and technological innovations in drinking water treatment and water reclamation for industrial purposes. In addition, he has extensive experience in education, in various cultural contexts, and is interested to explore the use of new ways of teaching through activated and blended learning and MOOCs.\u003c/p\u003e", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/rietveld_luuk_110p.jpg", "given_name": "Luuk", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "luuk-rietveld-0"}, {"family_name": "van Halem", "uuid": "4ce9ef2a-19e9-46de-9f34-5d755f26736a", "bio": "Doris van Halem is a tenure track Assistant Professor within the Department of Water Management, section Sanitary Engineering of Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering and Geosciences with a cum laude MSc degree (2007). During her studies she developed an interest in global drinking water challenges, illustrated by her internships in Sri Lanka and Benin, resulting in an MSc thesis \u201cCeramic silver impregnated pot filter for household drinking water treatment in developing countries\u201d. In 2011 she completed her PhD research (with honours) on subsurface iron and arsenic removal for drinking water supply in Bangladesh under the guidance of prof. J.C. van Dijk (TU Delft) and prof. dr. G.L. Amy (Unesco-IHE). Currently she supervises BSc, MSc and PhD students, focusing on inorganic constituent behaviour and trace compound removal during soil passage and drinking water treatment - with a particular interest in smart, pro-poor drinking water solutions.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/doris_van_halem_1.jpg", "given_name": "Doris", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": {"organization_name": "Delft University of Technology", "title": "Assistant Professor, Sanitary Engineering"}, "works": [], "slug": "doris-van-halem-0"}, {"family_name": "Grefte", "uuid": "463c3f1a-95fc-45aa-b7c0-d01b14126f02", "bio": "Anke Grefte is project manager open, online and blended education for the Faculty of Civil Engineering and Geosciences, Delft University of Technology. She graduated from Delft University of Technology in Civil Engineering with a master\u2019s thesis entitled \"Behaviour of particles in a drinking water distribution network; test rig results\". For this thesis Anke was awarded the Gijs Oskam award for best young researcher. In November 2013, she finished her Ph.D. research on the removal of Natural Organic Matter (NOM) fractions by ion exchange and the impact on drinking water treatment processes and biological stability.", "profile_image": {}, "profile_image_url": "https://stage.edx.org/sites/default/files/person/image/grefte_anke_110p.jpg", "given_name": "Anke", "urls": {"blog": null, "twitter": null, "facebook": null}, "position": null, "works": [], "slug": "anke-grefte-0"}], "announcement": "2015-07-24T00:00:00Z", "end": "2017-07-20T21:30:00Z", "uuid": "a36ed16a-6637-11e6-a8e3-22000bdde520", "title": "Introduction to Drinking Water Treatment", "certificate_url": null, "enrollment_start": "2016-06-15T00:00:00Z", "start": "2016-01-12T05:00:00Z", "min_effort": null, "short_description": "Learn about urban water services, focusing on conventional technologies for drinking water treatment.", "hidden": false, "level_type": "Intermediate", "type": "credit", "enrollment_open_date": "Jun 15, 2016", "marketing_url": "https://stage.edx.org/course/introduction-drinking-water-treatment-delftx-ctb3365dwx-0", "is_course_ended": false, "instructors": [], "full_description": "\u003cp\u003eThis course focuses on conventional technologies for drinking water treatment. Unit processes, involved in the treatment chain, are discussed as well as the physical, chemical and biological processes involved. The emphasis is on the effect of treatment on water quality and the dimensions of the unit processes in the treatment chain. After the course one should be able to recognise the process units, describe their function, and make basic calculations for a preliminary design of a drinking water treatment plant.\u003c/p\u003e\n\u003cp\u003eThe course consists of 4 modules:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIntroduction to drinking water treatment. In this module you learn to describe the important disciplines, schemes and evaluation criteria involved in the design phase.\u003c/li\u003e\n\u003cli\u003eWater quality. In this module you learn to identify the drinking water quality parameters to be improved and explain what treatment train or scheme is needed.\u003c/li\u003e\n\u003cli\u003eGroundwater treatment. In this module you learn to calculate the dimensions of the groundwater treatment processes and draw groundwater treatment schemes.\u003c/li\u003e\n\u003cli\u003eSurface water treatment. In this module you learn to calculate the dimensions of the surface water treatment processes and draw surface water treatment schemes.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis course in combination with the courses \"\u003ca href=\"https://www.edx.org/course/introduction-water-climate-delftx-ctb3300wcx-0\"\u003eIntroduction to Water and Climate\u003c/a\u003e\" and \"\u003ca href=\"https://www.edx.org/course/introduction-treatment-urban-sewage-delftx-ctb3365stx\"\u003eIntroduction to the Treatment of Urban Sewage\u003c/a\u003e\" forms the Water XSeries, by DelftX.\u003c/p\u003e\n\u003chr /\u003e\n\u003cp\u003e\u003cstrong\u003e\u003cem\u003eLICENSE\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThe course materials of this course are Copyright Delft University of Technology and are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA) 4.0 International License.\u003c/em\u003e\u003c/p\u003e", "key": "course-v1:DelftX+CTB3365DWx+1T2016", "enrollment_end": null, "reporting_type": "mooc", "advertised_start": null, "mobile_available": true, "modified": "2017-04-06T12:26:52.652365Z", "is_enrolled": false, "pacing_type": "instructor_paced", "video": {"src": "http://www.youtube.com/watch?v=0xPZXLHtRJw", "image": {"src": "https://stage.edx.org/sites/default/files/course/image/featured-card/h20_new_378x225.jpg", "width": null, "description": null, "height": null}, "description": null}}]}]}, - certificateData: [ - { - "url": "/certificates/a37c59143d9d422eb6ab11e1053b8eb5", "type": "course", "title": "Introduction to Water and Climate" - }, { - "url": "/certificates/bed3980e67ca40f0b31e309d9dfe9e7e", "type": "course", "title": "Introduction to the Treatment of Urban Sewage" - } - ], - urls: {"program_listing_url": "/dashboard/programs/", "commerce_api_url": "/api/commerce/v0/baskets/", "track_selection_url": "/course_modes/choose/"}, - userPreferences: {"pref-lang": "en"} - }, - /* eslint-enable */ - programModel, - courseData, - certificateCollection, - testCircle, - testText, - initView; - - testCircle = function(progress) { - var $circle = view.$('.progress-circle'), - incomplete = progress.in_progress.length + progress.not_started.length; - - expect($circle.find('.complete').length).toEqual(progress.completed.length); - expect($circle.find('.incomplete').length).toEqual(incomplete); - }; - - testText = function(progress) { - var $numbers = view.$('.numbers'), - total = progress.completed.length + progress.in_progress.length + progress.not_started.length; - - expect(view.$('.progress-heading').html()).toEqual('XSeries Progress'); - expect(parseInt($numbers.find('.complete').html(), 10)).toEqual(progress.completed.length); - expect(parseInt($numbers.find('.total').html(), 10)).toEqual(total); - }; - - initView = function() { - return new ProgramSidebarView({ - el: '.js-program-sidebar', - model: programModel, - courseModel: courseData, - certificateCollection: certificateCollection - }); - }; - - beforeEach(function() { - setFixtures('
'); - programModel = new Backbone.Model(data.programData); - courseData = new Backbone.Model(data.courseData); - certificateCollection = new Backbone.Collection(data.certificateData); - }); - - afterEach(function() { - view.remove(); - }); - - it('should exist', function() { - view = initView(); - expect(view).toBeDefined(); - }); - - it('should render the progress view if there is no program certificate', function() { - view = initView(); - testCircle(data.courseData); - testText(data.courseData); - }); - - it('should render the program certificate if earned', function() { - var $certLink, - programCert = { - url: '/program-cert', - type: 'program', - title: 'And Justice For All...' - }, - altText = 'Open the certificate you earned for the ' + programCert.title + ' program.'; - - certificateCollection.add(programCert); - view = initView(); - expect(view.$('.progress-circle-wrapper')[0]).not.toBeInDOM(); - $certLink = view.$('.program-cert-link'); - expect($certLink[0]).toBeInDOM(); - expect($certLink.attr('href')).toEqual(programCert.url); - expect($certLink.find('.program-cert').attr('alt')).toEqual(altText); - expect(view.$('.certificate-heading')).toHaveText('Your XSeries Certificate'); - }); - - it('should render the course certificate list', function() { - var $certificates; - - view = initView(); - $certificates = view.$('.certificate-list .certificate'); - - expect(view.$('.course-list-heading').html()).toEqual('Earned Certificates'); - expect($certificates).toHaveLength(certificateCollection.length); - $certificates.each(function(i, el) { - var $link = $(el).find('.certificate-link'), - model = certificateCollection.at(i); - - expect($link.attr('href')).toEqual(model.get('url')); - expect($link.html()).toEqual(model.get('title')); - }); - }); - - it('should not render the course certificate view if no certificates have been earned', function() { - certificateCollection.reset(); - view = initView(); - expect(view).toBeDefined(); - expect(view.$('.js-course-certificates')).toBeEmpty(); - }); - }); -}); diff --git a/lms/static/js/spec/learner_dashboard/program_details_view_spec.js b/lms/static/js/spec/learner_dashboard/program_details_view_spec.js deleted file mode 100644 index 290f7db864..0000000000 --- a/lms/static/js/spec/learner_dashboard/program_details_view_spec.js +++ /dev/null @@ -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: '

Learning University is home to leading creators, entrepreneurs.

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

The Test University was chartered in 1868.

', - 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('
'); - }); - - 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 - ); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js deleted file mode 100644 index 67368ccaf5..0000000000 --- a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js +++ /dev/null @@ -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(''); - - 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); - }); - }); -} -); diff --git a/lms/static/js/spec/learner_dashboard/unenroll_view_spec.js b/lms/static/js/spec/learner_dashboard/unenroll_view_spec.js deleted file mode 100644 index 034eeaa808..0000000000 --- a/lms/static/js/spec/learner_dashboard/unenroll_view_spec.js +++ /dev/null @@ -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('
'); // 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); - }); - }); -} -); diff --git a/lms/static/js/spec/staff_debug_actions_spec.js b/lms/static/js/spec/staff_debug_actions_spec.js index e73b88d290..2ce26f7d55 100644 --- a/lms/static/js/spec/staff_debug_actions_spec.js +++ b/lms/static/js/spec/staff_debug_actions_spec.js @@ -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(); }); }); diff --git a/lms/static/karma_lms.conf.js b/lms/static/karma_lms.conf.js index a3bdc64bb0..eb228694a6 100644 --- a/lms/static/karma_lms.conf.js +++ b/lms/static/karma_lms.conf.js @@ -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'} diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index 20765e64dd..78b5b8403b 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -32,11 +32,6 @@ 'js/groups/views/cohorts_dashboard_factory', 'js/discussions_management/views/discussions_dashboard_factory', 'js/header_factory', - 'js/learner_dashboard/course_entitlement_factory', - 'js/learner_dashboard/unenrollment_factory', - 'js/learner_dashboard/entitlement_unenrollment_factory', - 'js/learner_dashboard/program_details_factory', - 'js/learner_dashboard/program_list_factory', 'js/student_account/logistration_factory', 'js/student_account/views/account_settings_factory', 'js/student_account/views/finish_auth_factory', diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index d81b26dae4..cd278f5cc6 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -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', diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index d3ca6622fb..0fea4927cc 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -49,7 +49,7 @@ from student.models import CourseEnrollment }); }); - <%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 module_name="js/learner_dashboard/entitlement_unenrollment_factory" class_name="EntitlementUnenrollmentFactory"> + + <%static:webpack entry="EntitlementUnenrollmentFactory"> ## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView ## 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}" }); }); - + % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): <%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory"> DashboardSearchFactory(); diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 2d900e2019..f0b1eb242e 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -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:
- <%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 }' }); - + %endif % if related_programs: diff --git a/lms/templates/learner_dashboard/program_details_fragment.html b/lms/templates/learner_dashboard/program_details_fragment.html index e3218e551e..f340bdefed 100644 --- a/lms/templates/learner_dashboard/program_details_fragment.html +++ b/lms/templates/learner_dashboard/program_details_fragment.html @@ -8,8 +8,10 @@ from openedx.core.djangolib.js_utils import ( ) %> +
+ <%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}, }); - + - -
diff --git a/lms/templates/learner_dashboard/program_details_view.underscore b/lms/templates/learner_dashboard/program_details_view.underscore index 9385e1469f..55676fcbbf 100644 --- a/lms/templates/learner_dashboard/program_details_view.underscore +++ b/lms/templates/learner_dashboard/program_details_view.underscore @@ -1,5 +1,6 @@
-
+ +
<% if (completedCount === totalCount) { %>

<%- gettext('Congratulations!') %>

@@ -73,5 +74,5 @@ <% } %>
-
+ diff --git a/lms/templates/learner_dashboard/programs_fragment.html b/lms/templates/learner_dashboard/programs_fragment.html index 01ee61061a..d24ec8e6a5 100644 --- a/lms/templates/learner_dashboard/programs_fragment.html +++ b/lms/templates/learner_dashboard/programs_fragment.html @@ -9,17 +9,17 @@ from openedx.core.djangolib.js_utils import ( ) %> +
+
+ +
+ <%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} }); - + - -
-
- -
diff --git a/common/static/common/templates/components/progress_circle_segment.underscore b/lms/templates/learner_dashboard/progress_circle_segment.underscore similarity index 100% rename from common/static/common/templates/components/progress_circle_segment.underscore rename to lms/templates/learner_dashboard/progress_circle_segment.underscore diff --git a/common/static/common/templates/components/progress_circle_view.underscore b/lms/templates/learner_dashboard/progress_circle_view.underscore similarity index 100% rename from common/static/common/templates/components/progress_circle_view.underscore rename to lms/templates/learner_dashboard/progress_circle_view.underscore diff --git a/package-lock.json b/package-lock.json index 0777ed041f..528f84986c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ee35a1a0f0..259b9e6952 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/thresholds.sh b/scripts/thresholds.sh index e5f49a7de3..c8ceb434ef 100755 --- a/scripts/thresholds.sh +++ b/scripts/thresholds.sh @@ -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 diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html index 2624dd1d1b..e8d140fa24 100644 --- a/themes/edx.org/lms/templates/dashboard.html +++ b/themes/edx.org/lms/templates/dashboard.html @@ -56,7 +56,7 @@ from student.models import CourseEnrollment }); }); - <%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 module_name="js/learner_dashboard/entitlement_unenrollment_factory" class_name="EntitlementUnenrollmentFactory"> + + <%static:webpack entry="EntitlementUnenrollmentFactory"> ## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView ## 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}" }); }); - + % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): <%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory"> DashboardSearchFactory(); diff --git a/webpack.common.config.js b/webpack.common.config.js index bc5693410b..b12902accf 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -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' ] },