-
+
- <%- org %> +
- <%- content.number %> +
- <%- gettext("Starts") %> +
diff --git a/lms/static/images/programs/micromasters-program-details.svg b/lms/static/images/programs/micromasters-program-details.svg new file mode 100644 index 000000000000..6ac0a9bf6bad --- /dev/null +++ b/lms/static/images/programs/micromasters-program-details.svg @@ -0,0 +1 @@ + diff --git a/lms/static/images/programs/professional-certificate-program-details.svg b/lms/static/images/programs/professional-certificate-program-details.svg new file mode 100644 index 000000000000..eac90823a06d --- /dev/null +++ b/lms/static/images/programs/professional-certificate-program-details.svg @@ -0,0 +1 @@ + diff --git a/lms/static/images/programs/xseries-program-details.svg b/lms/static/images/programs/xseries-program-details.svg new file mode 100644 index 000000000000..a1aaaa35b4bf --- /dev/null +++ b/lms/static/images/programs/xseries-program-details.svg @@ -0,0 +1 @@ + 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 cc04551f5b23..a05dc96be0ba 100644 --- a/lms/static/js/learner_dashboard/models/course_card_model.js +++ b/lms/static/js/learner_dashboard/models/course_card_model.js @@ -85,6 +85,12 @@ _.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); }).bind(this)); return enrollableCourseRuns; @@ -115,12 +121,65 @@ return DateUtils.localize(context); }, + getCertificatePriceString: function(run) { + var verifiedSeat, currency; + if ('seats' in run && run.seats.length) { + // eslint-disable-next-line consistent-return + verifiedSeat = _.filter(run.seats, function(seat) { + if (['verified', 'professional', 'credit'].indexOf(seat.type) >= 0) { + return seat; + } + })[0]; + currency = verifiedSeat.currency; + if (currency === 'USD') { + return '$' + verifiedSeat.price; + } else { + return verifiedSeat.price + ' ' + currency; + } + } + return null; + }, + + formatDateString: function(run) { + var pacingType = run.pacing_type, + dateString = '', + start = run.start_date || this.get('start_date'), + end = run.end_date || this.get('end_date'), + now = new Date(), + startDate = new Date(start), + endDate = new Date(end); + + if (pacingType === 'self_paced') { + dateString = 'Self-paced'; + if (start && startDate > now) { + dateString += ' - Starts ' + start; + } else if (end && endDate > now) { + dateString += ' - Ends ' + end; + } else if (end && endDate < now) { + dateString += ' - Ended ' + end; + } + } else if (pacingType === 'instructor_paced') { + if (start && end) { + dateString = start + ' - ' + end; + } else if (start) { + dateString = 'Starts ' + start; + } else if (end) { + dateString = 'Ends ' + end; + } + } + return dateString; + }, + + valueIsDefined: function(val) { + return !([undefined, 'None', null].indexOf(val) >= 0); + }, + setActiveCourseRun: function(courseRun, userPreferences) { var startDateString, courseImageUrl; if (courseRun) { - if (courseRun.advertised_start !== undefined && courseRun.advertised_start !== 'None') { + if (this.valueIsDefined(courseRun.advertised_start)) { startDateString = courseRun.advertised_start; } else { startDateString = this.formatDate(courseRun.start, userPreferences); @@ -132,6 +191,7 @@ courseImageUrl = courseRun.course_image_url; } + this.set({ certificate_url: courseRun.certificate_url, course_image_url: courseImageUrl || '', @@ -148,8 +208,12 @@ mode_slug: courseRun.type, start_date: startDateString, upcoming_course_runs: this.getUpcomingCourseRuns(), - upgrade_url: courseRun.upgrade_url + upgrade_url: courseRun.upgrade_url, + price: this.getCertificatePriceString(courseRun) }); + + // This is used to render the date for completed and in progress courses + this.set({dateString: this.formatDateString(courseRun)}); } }, diff --git a/lms/static/js/learner_dashboard/views/certificate_status_view_2017.js b/lms/static/js/learner_dashboard/views/certificate_status_view_2017.js new file mode 100644 index 000000000000..4b9784b08a7a --- /dev/null +++ b/lms/static/js/learner_dashboard/views/certificate_status_view_2017.js @@ -0,0 +1,38 @@ +(function(define) { + 'use strict'; + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'edx-ui-toolkit/js/utils/html-utils', + 'text!../../../templates/learner_dashboard/certificate_status_2017.underscore', + 'text!../../../templates/learner_dashboard/certificate_icon.underscore' + ], + function( + Backbone, + $, + _, + gettext, + HtmlUtils, + certificateStatusTpl, + certificateIconTpl + ) { + return Backbone.View.extend({ + statusTpl: HtmlUtils.template(certificateStatusTpl), + iconTpl: HtmlUtils.template(certificateIconTpl), + + initialize: function(options) { + this.$el = options.$el; + this.render(); + }, + + render: function() { + var data = this.model.toJSON(); + + data = $.extend(data, {certificateSvg: this.iconTpl()}); + HtmlUtils.setHtml(this.$el, this.statusTpl(data)); + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/course_card_view_2017.js b/lms/static/js/learner_dashboard/views/course_card_view_2017.js new file mode 100644 index 000000000000..0d10dec0e8fa --- /dev/null +++ b/lms/static/js/learner_dashboard/views/course_card_view_2017.js @@ -0,0 +1,85 @@ +(function(define) { + 'use strict'; + + 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_2017', + 'js/learner_dashboard/views/certificate_status_view_2017', + 'js/learner_dashboard/views/course_enroll_view_2017', + 'text!../../../templates/learner_dashboard/course_card_2017.underscore' + ], + function( + Backbone, + $, + _, + gettext, + HtmlUtils, + EnrollModel, + UpgradeMessageView, + CertificateStatusView, + CourseEnrollView, + pageTpl + ) { + return Backbone.View.extend({ + className: 'program-course-card', + + tpl: HtmlUtils.template(pageTpl), + + 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.render(); + this.listenTo(this.model, 'change', this.render); + }, + + render: function() { + var data = $.extend(this.model.toJSON(), { + enrolled: this.context.enrolled || '' + }); + HtmlUtils.setHtml(this.$el, this.tpl(data)); + this.postRender(); + }, + + postRender: function() { + var $upgradeMessage = this.$('.upgrade-message'), + $certStatus = this.$('.certificate-status'); + + this.enrollView = new CourseEnrollView({ + $parentEl: this.$('.course-actions'), + model: this.model, + urlModel: this.urlModel, + enrollModel: this.enrollModel + }); + + if (this.model.get('upgrade_url')) { + this.upgradeMessage = new UpgradeMessageView({ + $el: $upgradeMessage, + model: this.model + }); + + $certStatus.remove(); + } else if (this.model.get('certificate_url')) { + 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(); + } + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/course_enroll_view_2017.js b/lms/static/js/learner_dashboard/views/course_enroll_view_2017.js new file mode 100644 index 000000000000..0565cc659ec4 --- /dev/null +++ b/lms/static/js/learner_dashboard/views/course_enroll_view_2017.js @@ -0,0 +1,104 @@ +(function(define) { + 'use strict'; + + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'edx-ui-toolkit/js/utils/html-utils', + 'text!../../../templates/learner_dashboard/course_enroll_2017.underscore' + ], + function( + Backbone, + $, + _, + gettext, + HtmlUtils, + pageTpl + ) { + return Backbone.View.extend({ + className: 'course-enroll-view', + + tpl: HtmlUtils.template(pageTpl), + + events: { + 'click .enroll-button': 'handleEnroll' + }, + + initialize: function(options) { + this.$parentEl = options.$parentEl; + this.enrollModel = options.enrollModel; + this.urlModel = options.urlModel; + this.render(); + }, + + render: function() { + var filledTemplate; + if (this.$parentEl && this.enrollModel) { + filledTemplate = this.tpl(this.model.toJSON()); + 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'); + } + }, + + handleEnroll: function() { + // Enrollment click event handled here + var courseRunKey = $('.run-select').val(); + 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 + }); + } + }, + + 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')); + } + }, + + redirect: function(url) { + window.location.href = url; + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/program_details_view_2017.js b/lms/static/js/learner_dashboard/views/program_details_view_2017.js index eb853632cd31..79e3a371a6a6 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view_2017.js +++ b/lms/static/js/learner_dashboard/views/program_details_view_2017.js @@ -1,18 +1,17 @@ (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' - ], + 'jquery', + 'underscore', + 'gettext', + 'edx-ui-toolkit/js/utils/html-utils', + 'js/learner_dashboard/collections/course_card_collection', + 'js/learner_dashboard/views/program_header_view_2017', + 'js/learner_dashboard/views/collection_list_view', + 'js/learner_dashboard/views/course_card_view_2017', + 'js/learner_dashboard/views/program_details_sidebar_view', + 'text!../../../templates/learner_dashboard/program_details_view_2017.underscore' + ], function( Backbone, $, @@ -34,15 +33,36 @@ initialize: function(options) { this.options = options; this.programModel = new Backbone.Model(this.options.programData); - this.courseCardCollection = new CourseCardCollection( - this.programModel.get('courses'), + this.courseData = new Backbone.Model(this.options.courseData); + this.completedCourseCollection = new CourseCardCollection( + this.courseData.get('completed') || [], + this.options.userPreferences + ); + this.inProgressCourseCollection = new CourseCardCollection( + this.courseData.get('in_progress') || [], this.options.userPreferences - ); + ); + this.remainingCourseCollection = new CourseCardCollection( + this.courseData.get('not_started') || [], + this.options.userPreferences + ); + this.render(); }, render: function() { - HtmlUtils.setHtml(this.$el, this.tpl()); + var completedCount = this.completedCourseCollection.length, + inProgressCount = this.inProgressCourseCollection.length, + remainingCount = this.remainingCourseCollection.length, + totalCount = completedCount + inProgressCount + remainingCount, + data = { + totalCount: totalCount, + inProgressCount: inProgressCount, + remainingCount: remainingCount, + completedCount: completedCount + }; + data = $.extend(data, this.options.programData); + HtmlUtils.setHtml(this.$el, this.tpl(data)); this.postRender(); }, @@ -50,16 +70,34 @@ this.headerView = new HeaderView({ model: new Backbone.Model(this.options) }); - new CollectionListView({ - el: '.js-course-list', - childView: CourseCardView, - collection: this.courseCardCollection, - context: this.options, - titleContext: { - el: 'h2', - title: 'Course List' - } - }).render(); + + if (this.remainingCourseCollection.length > 0) { + new CollectionListView({ + el: '.js-course-list-remaining', + childView: CourseCardView, + collection: this.remainingCourseCollection, + context: this.options + }).render(); + } + + if (this.completedCourseCollection.length > 0) { + new CollectionListView({ + el: '.js-course-list-completed', + childView: CourseCardView, + collection: this.completedCourseCollection, + context: this.options + }).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')}) + }).render(); + } new SidebarView({ el: '.sidebar', diff --git a/lms/static/js/learner_dashboard/views/program_header_view_2017.js b/lms/static/js/learner_dashboard/views/program_header_view_2017.js new file mode 100644 index 000000000000..76beef11e008 --- /dev/null +++ b/lms/static/js/learner_dashboard/views/program_header_view_2017.js @@ -0,0 +1,57 @@ +(function(define) { + 'use strict'; + + define(['backbone', + 'jquery', + 'edx-ui-toolkit/js/utils/html-utils', + 'text!../../../templates/learner_dashboard/program_header_view_2017.underscore', + 'text!/static/images/programs/micromasters-program-details.svg', + 'text!/static/images/programs/xseries-program-details.svg', + 'text!/static/images/programs/professional-certificate-program-details.svg' + ], + function(Backbone, $, HtmlUtils, pageTpl, MicroMastersLogo, + XSeriesLogo, ProfessionalCertificateLogo) { + return Backbone.View.extend({ + breakpoints: { + min: { + medium: '768px', + large: '1180px' + } + }, + + el: '.js-program-header', + + tpl: HtmlUtils.template(pageTpl), + + initialize: function() { + this.render(); + }, + + getLogo: function() { + var logo = false, + type = this.model.get('programData').type; + + 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() + }); + + if (this.model.get('programData')) { + HtmlUtils.setHtml(this.$el, this.tpl(data)); + } + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/upgrade_message_view_2017.js b/lms/static/js/learner_dashboard/views/upgrade_message_view_2017.js new file mode 100644 index 000000000000..53f94793ee56 --- /dev/null +++ b/lms/static/js/learner_dashboard/views/upgrade_message_view_2017.js @@ -0,0 +1,34 @@ +(function(define) { + 'use strict'; + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'edx-ui-toolkit/js/utils/html-utils', + 'text!../../../templates/learner_dashboard/upgrade_message_2017.underscore' + ], + function( + Backbone, + $, + _, + gettext, + HtmlUtils, + upgradeMessageTpl + ) { + return Backbone.View.extend({ + messageTpl: HtmlUtils.template(upgradeMessageTpl), + + initialize: function(options) { + this.$el = options.$el; + this.render(); + }, + + render: function() { + var data = this.model.toJSON(); + + HtmlUtils.setHtml(this.$el, this.messageTpl(data)); + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/learner_dashboard/course_enroll_view_spec_2017.js b/lms/static/js/spec/learner_dashboard/course_enroll_view_spec_2017.js new file mode 100644 index 000000000000..b9819b658a82 --- /dev/null +++ b/lms/static/js/spec/learner_dashboard/course_enroll_view_spec_2017.js @@ -0,0 +1,289 @@ +define([ + 'backbone', + 'jquery', + 'js/learner_dashboard/models/course_card_model', + 'js/learner_dashboard/models/course_enroll_model', + 'js/learner_dashboard/views/course_enroll_view_2017' +], 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, + 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 + }, { + 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 + }]; + }); + + 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.$el.html().trim()).toEqual(''); + expect(view.$('.run-select').length).toBe(0); + }); + + it('should not render anything if course runs are empty', function() { + setupView([]); + + expect(view.$('.enrollment-info').length).toBe(0); + 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 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/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 7adbc68f88dc..c02d81100a81 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -744,6 +744,7 @@ 'js/spec/learner_dashboard/program_details_header_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_enroll_view_spec_2017.js', 'js/spec/markdown_editor_spec.js', 'js/spec/dateutil_factory_spec.js', 'js/spec/navigation_spec.js', diff --git a/lms/static/sass/elements/_course-card.scss b/lms/static/sass/elements/_course-card.scss index a8acd371678c..a76a056f3a07 100644 --- a/lms/static/sass/elements/_course-card.scss +++ b/lms/static/sass/elements/_course-card.scss @@ -60,6 +60,10 @@ margin-bottom: $baseline/4; font-size: font-size(small); visibility: hidden; + + .visible { + visibility: visible; + } } .no-action-message { diff --git a/lms/static/sass/partials/base/_variables.scss b/lms/static/sass/partials/base/_variables.scss index a60a96187c3e..c4e4db398d8f 100644 --- a/lms/static/sass/partials/base/_variables.scss +++ b/lms/static/sass/partials/base/_variables.scss @@ -224,6 +224,13 @@ $success-color: rgb(0, 155, 0) !default; // #COLORS- EDX-SPECIFIC // ---------------------------- +// logo colors +$micromasters-color: #005585; +$xseries-color: #424242; +$professional-certificate-color: #9a1f60; +$zebra-stripe-color: rgb(249, 250, 252); +$divider-color: rgb(226,231,236); + // old color variables // DEPRECATED: Do not continue to use these colors, instead use pattern libary and base colors above. $dark-gray1: rgb(74,74,74) !default; @@ -326,6 +333,7 @@ $credit-color-base: rgb(244,195,0) !default; // accessible with black text $staff-color: $uxpl-pink-base !default; + // ---------------------------- // #TYPOGRAPHY // ---------------------------- diff --git a/lms/static/sass/views/_program-details.scss b/lms/static/sass/views/_program-details.scss index f7d5608cec0e..ae8e3e23c1ad 100644 --- a/lms/static/sass/views/_program-details.scss +++ b/lms/static/sass/views/_program-details.scss @@ -76,3 +76,425 @@ padding: 0 $full-width-banner-margin/4; } } + +// CSS for April 2017 version of Program Details Page + +.program-details { + .window-wrap { + background-color: $white; + } +} +.program-details-wrapper { + + .program-details-header { + background-color: $light-gray4; + display: flex; + color: black; + font-family: 'Open Sans'; + font-weight: normal; + flex-wrap: wrap; + padding-top: 40px; + padding-bottom: 35px; + margin-left: 10px; + margin-right: 10px; + + @media(min-width: $bp-screen-md) { + margin-left: 30px; + margin-right: 80px; + } + + .hd-1 { + font-size: 1.5em; + @media(min-width: $bp-screen-md) { + font-size: 2.375em; + } + } + + .program-details-icon { + margin-left: 3px; + margin-top: 10px; + height: auto; + + /* IE11 CSS styles */ + @media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) { + height: 50px; + } + } + + .micromasters { + fill: $micromasters-color; + width: 200px; + @media(min-width: $bp-screen-md) { + width: 250px; + } + } + + .xseries { + fill: xseries-color; + width: 150px; + @media(min-width: $bp-screen-md) { + width: 200px; + } + } + + .professional.certificate { + fill: $professional-certificate-color; + width: 250px; + @media(min-width: $bp-screen-md) { + width: 300px; + } + } + + .meta-info { + margin: 0; + @media(min-width: $bp-screen-md) { + width: 70%; + } + @media(min-width: $bp-screen-lg) { + width: 75%; + } + + .program-title { + font-weight: normal; + font-size: 2em; + } + } + + .authoring-organizations { + text-align: center; + + display: flex; + @media(min-width: $bp-screen-md) { + display: block; + } + + .heading { + font-family: "Open Sans"; + font-weight: bold; + color: palette(primary, dark); + font-size: 0.9375em; + + margin-top: auto; + margin-bottom: auto; + @media(min-width: $bp-screen-md) { + margin: 10px 0 0 0; + } + + } + + @media(min-width: $bp-screen-md) { + margin: initial; + width: 30%; + + .orgs .org-logo { + width: 46.5%; + margin-left: 2.5%; + height: auto; + } + } + + @media(min-width: $bp-screen-lg) { + width: 25%; + } + } + + } + + .program-details-content { + @media(min-width: $bp-screen-md) { + margin-left: 30px; + } + margin-left: 10px; + } + .course-list-heading { + font-family: "Open Sans"; + font-weight: bold; + color: palette(primary, dark); + font-size: 0.9375em; + line-height: normal; + margin-top: 10px; + margin-bottom: 0; + + .status { + margin-right: 7px; + } + } + + .course-list > div:nth-of-type(even) { + background-color: $zebra-stripe-color; + } + + .course-list-headings { + width: 700px; + + .divider { + margin-left: 0; + margin-bottom: 20px; + background-color: $divider-color; + margin-top: 5px; + height: 3px; + width: 315px; + @media(min-width: $bp-screen-sm) { + width: 550px; + } + @media(min-width: $bp-screen-md) { + width: 700px; + } + border: none; + } + + .motivating-section { + font-size: 0.9375em; + margin-left: 15px; + width: 310px; + @media(min-width: $bp-screen-sm) { + width: auto; + } + + .motivating-heading { + margin-bottom: 0px; + font-weight: 600; + } + + .motivating-message { + color: #414141; + } + + } + } + + .program-heading { + @media(min-width: $bp-screen-md) { + width: 70%; + } + width: 90%; + margin-top: 40px; + margin-bottom: 40px; + + .program-heading-title { + font-family: "Open Sans"; + font-weight: 600; + font-size: 1.3em; + color: palette(grayscale, base); + margin-bottom: 5px; + line-height: normal; + } + + .program-heading-message { + font-weight: 300; + } + } + + .course-enroll-view { + .enroll-button { + width: 100%; + + @media(min-width: $bp-screen-sm) { + width: initial; + margin-bottom: 0; + margin-top: 17px; + } + + @media(min-width: $bp-screen-md) { + width: initial; + margin-top: 0px; + margin-bottom: 0; + } + + button { + background-color: palette(primary, dark); + height: 37px; + width: 128px; + border-radius: 0; + padding: 0; + margin-bottom: 5px; + margin-top: 7px; + font-size: 0.9375em; + + /* IE11 CSS styles */ + @media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) { + float: right; + } + } + } + .select-choice { + font-family: "Open Sans"; + font-weight: bold; + font-size: 0.9375em; + color: palette(grayscale, base); + margin-top: 6px; + margin-right: 2px; + display: block; + + @media(min-width: $bp-screen-md) { + display: inline; + } + + } + .run-select-container { + @media(min-width: $bp-screen-md) { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + } + } + .run-select { + width: 95%; + @media(min-width: $bp-screen-sm) { + width: 300px; + } + height: 34px; + padding: 0; + margin-right: 10px; + } + } + + .program-course-card { + @media(min-width: $bp-screen-md) { + width: 100%; + } + + margin-bottom: 10px; + + @media(min-width: $bp-screen-md) { + height: 100px; + } + + .section { + display: flex; + justify-content: space-between; + margin-right: 40px; + margin-left: 15px; + + @media(min-width: $bp-screen-md) { + margin-left: 20px; + } + + @media(min-width: $bp-screen-md) { + flex-wrap: wrap; + } + + } + + .course-details { + float: none; + + .course-title { + font-size: 1em; + color: palette(primary, base); + font-weight: 600; + text-decoration: underline; + margin: 0; + } + + .run-period { + color: palette(grayscale, base); + font-size: 0.9375em; + } + + .course-text .enrolled { + color: palette(grayscale, base); + } + } + + .course-meta-container { + display: flex; + flex-direction: column; + + @media(min-width: $bp-screen-md) { + width: 100%; + } + } + + .course-actions { + display: flex; + flex-direction: column; + + .course-enroll-view { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + } + } + + .upgrade-message { + flex-wrap: wrap; + + .upgrade-button { + background: palette(success, text); + border-color: palette(success, text); + height: 37px; + width: 128px; + border-radius: 0; + padding: 0; + text-align: center; + font-size: 0.9375em; + + /* IE11 CSS styles */ + @media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) { + float: right; + } + } + + .action { + width: 100%; + margin: 5px 0; + + @media(min-width: $bp-screen-sm) { + width: initial; + } + @media(min-width: $bp-screen-md) { + margin-top: -25px; + } + } + + .certificate-status{ + padding-top: 0px; + + width: initial; + @media(min-width: $bp-screen-sm) { + width: 300px; + } + @media(min-width: $bp-screen-md) { + width: initial; + } + + .card-msg { + font-family: "Open Sans"; + font-weight: bold; + font-size: 0.9375em; + color: palette(grayscale, base); + display: block; + @media(min-width: $bp-screen-sm) { + display: inline; + } + } + + .price { + color: palette(success, text); + font-weight: bold; + } + } + + } + + .certificate-status { + .fa-check-circle { + color: palette(success, text); + } + + .card-msg { + font-family: "Open Sans"; + font-weight: bold; + font-size: 0.9375em; + color: palette(grayscale, base); + } + + .certificate-status-msg { + color: palette(grayscale, base); + font-size: 0.9375em; + } + } + + } +} diff --git a/lms/templates/discovery/course_card_2017.underscore b/lms/templates/discovery/course_card_2017.underscore new file mode 100644 index 000000000000..02324005c1ed --- /dev/null +++ b/lms/templates/discovery/course_card_2017.underscore @@ -0,0 +1,24 @@ ++ <%- gettext('Certificate Status:') %> + + <%- gettext('Certificate Purchased') %> +
diff --git a/lms/templates/learner_dashboard/course_card_2017.underscore b/lms/templates/learner_dashboard/course_card_2017.underscore new file mode 100644 index 000000000000..ec73da8d4cc7 --- /dev/null +++ b/lms/templates/learner_dashboard/course_card_2017.underscore @@ -0,0 +1,28 @@ +<%- gettext("As you complete courses, you will see them listed here.") %>
+ +