From 8d672bc8322fec8992640b7ec966368e2a6d96c6 Mon Sep 17 00:00:00 2001 From: Simon Chen Date: Mon, 13 Jun 2016 15:08:00 -0400 Subject: [PATCH] ECOM-3206 allow run selection and enrollment (#12685) --- .../learner_dashboard/tests/test_programs.py | 3 +- .../learner_dashboard/tests/test_utils.py | 23 ++ lms/djangoapps/learner_dashboard/utils.py | 16 ++ lms/djangoapps/learner_dashboard/views.py | 13 +- .../models/course_card_model.js | 56 +++-- .../models/course_enroll_model.js | 19 ++ .../views/collection_list_view.js | 1 + .../views/course_card_view.js | 15 +- .../views/course_enroll_view.js | 78 ++++++- .../views/program_details_view.js | 2 +- .../course_card_view_spec.js | 36 ++-- .../course_enroll_view_spec.js | 201 +++++++++++++++--- .../program_details_header_spec.js | 7 +- lms/static/sass/elements/_course-card.scss | 25 ++- .../learner_dashboard/course_card.underscore | 7 +- .../course_enroll.underscore | 21 ++ .../learner_dashboard/program_details.html | 2 +- .../program_header_view.underscore | 2 +- 18 files changed, 438 insertions(+), 89 deletions(-) create mode 100644 lms/djangoapps/learner_dashboard/tests/test_utils.py create mode 100644 lms/djangoapps/learner_dashboard/utils.py create mode 100644 lms/static/js/learner_dashboard/models/course_enroll_model.js diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 4824e6c1be..5843c0b91c 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -284,7 +284,8 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): def _assert_program_data_present(self, response): """Verify that program data is present.""" self.assertContains(response, 'programData') - self.assertContains(response, 'programListingUrl') + self.assertContains(response, 'urls') + self.assertContains(response, 'program_listing_url') self.assertContains(response, self.data['name']) self._assert_programs_tab_present(response) diff --git a/lms/djangoapps/learner_dashboard/tests/test_utils.py b/lms/djangoapps/learner_dashboard/tests/test_utils.py new file mode 100644 index 0000000000..aeca0b7805 --- /dev/null +++ b/lms/djangoapps/learner_dashboard/tests/test_utils.py @@ -0,0 +1,23 @@ +""" +Unit test module covering utils module +""" + +import ddt +from django.test import TestCase + +from lms.djangoapps.learner_dashboard import utils + + +@ddt.ddt +class TestUtils(TestCase): + """ + The test case class covering the all the utils functions + """ + @ddt.data('path1/', '/path1/path2/', '/', '') + def test_strip_course_id(self, path): + """ + Test to make sure the function 'strip_course_id' + handles various url input + """ + actual = utils.strip_course_id(path + unicode(utils.FAKE_COURSE_KEY)) + self.assertEqual(actual, path) diff --git a/lms/djangoapps/learner_dashboard/utils.py b/lms/djangoapps/learner_dashboard/utils.py new file mode 100644 index 0000000000..d214b03a51 --- /dev/null +++ b/lms/djangoapps/learner_dashboard/utils.py @@ -0,0 +1,16 @@ +""" +The utility methods and functions to help the djangoapp logic +""" +from opaque_keys.edx.keys import CourseKey + + +FAKE_COURSE_KEY = CourseKey.from_string('course-v1:fake+course+run') + + +def strip_course_id(path): + """ + The utility function to help remove the fake + course ID from the url path + """ + course_id = unicode(FAKE_COURSE_KEY) + return path.split(course_id)[0] diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 3a14cd5b24..9b210d8e5c 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -11,6 +11,10 @@ from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.credentials.utils import get_programs_credentials from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs import utils +from lms.djangoapps.learner_dashboard.utils import ( + FAKE_COURSE_KEY, + strip_course_id +) @login_required @@ -63,9 +67,16 @@ def program_details(request, program_id): program_data = utils.supplement_program_data(program_data, request.user) show_program_listing = ProgramsApiConfig.current().show_program_listing + urls = { + 'program_listing_url': reverse('program_listing_view'), + 'track_selection_url': strip_course_id( + reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})), + 'commerce_api_url': reverse('commerce_api:v0:baskets:create') + } + context = { 'program_data': program_data, - 'program_listing_url': reverse('program_listing_view'), + 'urls': urls, 'show_program_listing': show_program_listing, 'nav_hidden': True, 'disable_courseware_js': True, 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 e3d4c744bc..93f4cfc6f3 100644 --- a/lms/static/js/learner_dashboard/models/course_card_model.js +++ b/lms/static/js/learner_dashboard/models/course_card_model.js @@ -11,27 +11,49 @@ initialize: function(data) { if (data){ this.context = data; - //we should populate our model by looking at the run_modes - if (data.run_modes.length > 0){ - //We only have 1 run mode for this program - this.setActiveRunMode(data.run_modes[0]); - } + this.setActiveRunMode(this.getRunMode(data.run_modes)); } }, + getRunMode: function(runModes){ + //we should populate our model by looking at the run_modes + if (runModes.length > 0){ + if(runModes.length === 1){ + return runModes[0]; + }else{ + //We need to implement logic here to select the + //most relevant run mode for the student to enroll + return runModes[0]; + } + }else{ + return null; + } + }, + setActiveRunMode: function(runMode){ - this.set({ - display_name: this.context.display_name, - key: this.context.key, - marketing_url: runMode.marketing_url || '', - start_date: runMode.start_date, - end_date: runMode.end_date, - is_enrolled: runMode.is_enrolled, - is_enrollment_open: runMode.is_enrollment_open, - course_url: runMode.course_url || '', - course_image_url: runMode.course_image_url || '', - mode_slug: runMode.mode_slug - }); + if (runMode){ + this.set({ + display_name: this.context.display_name, + key: this.context.key, + marketing_url: runMode.marketing_url || '', + start_date: runMode.start_date, + end_date: runMode.end_date, + is_enrolled: runMode.is_enrolled, + is_enrollment_open: runMode.is_enrollment_open, + course_key: runMode.course_key, + course_url: runMode.course_url || '', + course_image_url: runMode.course_image_url || '', + mode_slug: runMode.mode_slug, + run_key: runMode.run_key + }); + } + }, + + updateRun: function(runKey){ + var selectedRun = _.findWhere(this.get('run_modes'), {run_key: runKey}); + if (selectedRun){ + this.setActiveRunMode(selectedRun); + } } }); }); diff --git a/lms/static/js/learner_dashboard/models/course_enroll_model.js b/lms/static/js/learner_dashboard/models/course_enroll_model.js new file mode 100644 index 0000000000..5a2a26aecb --- /dev/null +++ b/lms/static/js/learner_dashboard/models/course_enroll_model.js @@ -0,0 +1,19 @@ +/** + * Store data to enroll learners into the course + */ +;(function (define) { + 'use strict'; + + define([ + 'backbone' + ], + function( Backbone) { + return Backbone.Model.extend({ + defaults: { + course_id: '', + optIn: false, + } + }); + } + ); +}).call(this, define || RequireJS.define); 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 6ff7970e6b..f0e2af1cf0 100644 --- a/lms/static/js/learner_dashboard/views/collection_list_view.js +++ b/lms/static/js/learner_dashboard/views/collection_list_view.js @@ -46,6 +46,7 @@ if (this.titleContext){ this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString()); } + this.$el.html(childList); } }, 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 69f17c1cee..b77536bb32 100644 --- a/lms/static/js/learner_dashboard/views/course_card_view.js +++ b/lms/static/js/learner_dashboard/views/course_card_view.js @@ -6,6 +6,7 @@ 'underscore', 'gettext', 'edx-ui-toolkit/js/utils/html-utils', + 'js/learner_dashboard/models/course_enroll_model', 'js/learner_dashboard/views/course_enroll_view', 'text!../../../templates/learner_dashboard/course_card.underscore' ], @@ -15,6 +16,7 @@ _, gettext, HtmlUtils, + EnrollModel, CourseEnrollView, pageTpl ) { @@ -23,8 +25,14 @@ tpl: HtmlUtils.template(pageTpl), - initialize: function() { + initialize: function(options) { + this.enrollModel = new EnrollModel(); + if(options.context && options.context.urls){ + this.urlModel = new Backbone.Model(options.context.urls); + this.enrollModel.urlRoot = this.urlModel.get('commerce_api_url'); + } this.render(); + this.listenTo(this.model, 'change', this.render); }, render: function() { @@ -35,9 +43,10 @@ postRender: function(){ this.enrollView = new CourseEnrollView({ - $el: this.$('.course-actions'), + $parentEl: this.$('.course-actions'), model: this.model, - context: this.context + urlModel: this.urlModel, + enrollModel: this.enrollModel }); } }); 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 3f45779b6e..f7d61e9e74 100644 --- a/lms/static/js/learner_dashboard/views/course_enroll_view.js +++ b/lms/static/js/learner_dashboard/views/course_enroll_view.js @@ -20,23 +20,89 @@ tpl: HtmlUtils.template(pageTpl), events: { - 'click .enroll-button': 'handleEnroll' + 'click .enroll-button': 'handleEnroll', + 'change .run-select': 'handleRunSelect', }, initialize: function(options) { - if (options.$el){ - this.$el = options.$el; - this.render(); + this.$parentEl = options.$parentEl; + this.enrollModel = options.enrollModel; + this.urlModel = options.urlModel; + this.render(); + if(this.urlModel){ + this.trackSelectionUrl = this.urlModel.get('track_selection_url'); } }, render: function() { - var filledTemplate = this.tpl(this.model.toJSON()); - HtmlUtils.setHtml(this.$el, filledTemplate); + var filledTemplate; + if (this.$parentEl && + this.enrollModel && + this.model.get('course_key')){ + + filledTemplate = this.tpl(this.model.toJSON()); + HtmlUtils.setHtml(this.$el, filledTemplate); + HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el)); + } }, handleEnroll: function(){ //Enrollment click event handled here + if (!this.model.get('is_enrolled')){ + // actually enroll + this.enrollModel.save({ + course_id: this.model.get('course_key') + }, { + success: _.bind(this.enrollSuccess, this), + error: _.bind(this.enrollError, this) + }); + } + }, + + handleRunSelect: function(event){ + var runKey; + if (event.target){ + runKey = $(event.target).val(); + if (runKey){ + this.model.updateRun(runKey); + } + } + }, + + enrollSuccess: function(){ + var courseKey = this.model.get('course_key'); + if (this.trackSelectionUrl) { + // Go to track selection page + this.redirect( this.trackSelectionUrl + courseKey ); + }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_key') ); + } + }, + + redirect: function( url ) { + window.location.href = url; } }); } 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 4295d72847..06e53c3ae1 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -51,7 +51,7 @@ el: '.js-course-list', childView: CourseCardView, collection: this.courseCardCollection, - context: this.programModel.toJSON(), + context: this.options, titleContext: { el: 'h2', title: 'Course List' 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 index b7de0bc487..7f9db28149 100644 --- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js @@ -10,7 +10,6 @@ define([ describe('Course Card View', function () { var view = null, courseCardModel, - setupView, context = { course_modes: [], display_name: 'Astrophysics: Exploring Exoplanets', @@ -32,7 +31,7 @@ define([ is_enrolled: true, certificate_url: '', }] - }; + }, setupView = function(isEnrolled){ context.run_modes[0].is_enrolled = isEnrolled; @@ -41,6 +40,17 @@ define([ view = new CourseCardView({ model: courseCardModel }); + }, + + validateCourseInfoDisplay = function(){ + //DRY validation for course card in enrolled state + expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); + expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name); + expect(view.$('.course-details .course-title-link').attr('href')).toEqual( + context.run_modes[0].course_url); + expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); + expect(view.$('.course-details .course-text .run-period').html()) + .toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date); }; beforeEach(function() { @@ -58,22 +68,18 @@ define([ it('should render the course card based on the data enrolled', function() { view.remove(); setupView(true); - expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); - expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name); - expect(view.$('.course-details .course-title-link').attr('href')).toEqual( - context.run_modes[0].course_url); - expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); - expect(view.$('.course-details .course-text .run-period').html()) - .toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date); + validateCourseInfoDisplay(); }); it('should render the course card based on the data not enrolled', function() { - expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); - expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name); - expect(view.$('.course-details .course-title-link').attr('href')).toEqual( - context.run_modes[0].course_url); - expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); - expect(view.$('.course-details .course-text .run-period').html()).not.toBeDefined(); + validateCourseInfoDisplay(); + }); + + it('should update render if the course card is_enrolled updated', function(){ + courseCardModel.set({ + is_enrolled: true + }); + validateCourseInfoDisplay(); }); }); } 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 index 9638312867..a618e4b029 100644 --- a/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js @@ -2,70 +2,215 @@ 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, CourseEnrollView) { + ], function (Backbone, $, CourseCardModel, CourseEnrollModel, CourseEnrollView) { 'use strict'; describe('Course Enroll View', function () { var view = null, courseCardModel, + courseEnrollModel, + urlModel, setupView, + singleRunModeList = [{ + start_date: 'Apr 25, 2016', + end_date: 'Jun 13, 2016', + course_key: 'course-v1:course-v1:edX+DemoX+Demo_Course', + course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', + course_image_url: 'http://test.com/image1', + marketing_url: 'http://test.com/image2', + mode_slug: 'audit', + run_key: '2T2016', + is_enrolled: false + }], + multiRunModeList = [{ + start_date: 'May 21, 2015', + end_date: 'Sep 21, 2015', + course_key: 'course-v1:course-v1:edX+DemoX+Demo_Course', + course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', + course_image_url: 'http://test.com/run_2_image_1', + marketing_url: 'http://test.com/run_2_image_2', + mode_slug: 'verified', + run_key: '1T2015', + is_enrolled: false + },{ + start_date: 'Sep 22, 2015', + end_date: 'Dec 28, 2015', + course_key: 'course-v1:course-v1:edX+DemoX+Demo_Course', + course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', + course_image_url: 'http://test.com/run_3_image_1', + marketing_url: 'http://test.com/run_3_image_2', + mode_slug: 'verified', + run_key: '2T2015', + is_enrolled: false + }], context = { - display_name: 'Astrophysics: Exploring Exoplanets', - key: 'ANU-ASTRO1x', + display_name: 'Edx Demo course', + key: 'edX+DemoX+Demo_Course', organization: { - display_name: 'Australian National University', - key: 'ANUx' - }, - run_modes: [{ - start_date: 'Apr 25, 2016', - end_date: 'Jun 13, 2016', - course_key: 'course-v1:ANUx+ANU-ASTRO1x+3T2015', - course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', - course_image_url: 'http://test.com/image1', - marketing_url: 'http://test.com/image2', - mode_slug: 'verified', - run_key: '2T2016', - is_enrolled: false, - certificate_url: '', - }] + display_name: 'edx.org', + key: 'edX' + } + }, + urls = { + dashboard_url: '/dashboard', + id_verification_url: '/verify_student/start_flow/', + track_selection_url: '/select_track/course/' }; - setupView = function(isEnrolled){ - context.run_modes[0].is_enrolled = isEnrolled; + setupView = function(runModes, urls){ + context.run_modes = runModes; setFixtures('
'); courseCardModel = new CourseCardModel(context); + courseEnrollModel = new CourseEnrollModel({}, { + courseId: courseCardModel.get('course_key') + }); + if(urls){ + urlModel = new Backbone.Model(urls); + } view = new CourseEnrollView({ - $el: $('.course-actions'), - model: courseCardModel + $parentEl: $('.course-actions'), + model: courseCardModel, + enrollModel: courseEnrollModel, + urlModel: urlModel }); }; - beforeEach(function() { - setupView(false); - }); - afterEach(function() { view.remove(); + urlModel = null; + courseCardModel = null; + courseEnrollModel = null; }); it('should exist', function() { + setupView(singleRunModeList); expect(view).toBeDefined(); }); it('should render the course enroll view based on not enrolled data', function() { + setupView(singleRunModeList); expect(view.$('.enrollment-info').html().trim()).toEqual('not enrolled'); expect(view.$('.enroll-button').text().trim()).toEqual('Enroll Now'); + expect(view.$('.run-select').length).toBe(0); }); it('should render the course enroll view based on enrolled data', function(){ - view.remove(); - setupView(true); + singleRunModeList[0].is_enrolled = true; + setupView(singleRunModeList); expect(view.$('.enrollment-info').html().trim()).toEqual('enrolled'); expect(view.$('.view-course-link').attr('href')).toEqual( context.run_modes[0].course_url); expect(view.$('.view-course-link').text().trim()).toEqual('View Course'); + expect(view.$('.run-select').length).toBe(0); + }); + + it('should not render anything if run modes is 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 drop down if mulitple run available', function(){ + setupView(multiRunModeList); + expect(view.$('.run-select').length).toBe(1); + expect(view.$('.run-select').val()).toEqual(multiRunModeList[0].run_key); + }); + + it('should switch run context if dropdown selection changed', function(){ + setupView(multiRunModeList); + spyOn(courseCardModel, 'updateRun').and.callThrough(); + expect(view.$('.run-select').val()).toEqual(multiRunModeList[0].run_key); + view.$('.run-select').val(multiRunModeList[1].run_key); + view.$('.run-select').trigger("change"); + expect(view.$('.run-select').val()).toEqual(multiRunModeList[1].run_key); + expect(courseCardModel.updateRun) + .toHaveBeenCalledWith(multiRunModeList[1].run_key); + expect(courseCardModel.get('run_key')).toEqual(multiRunModeList[1].run_key); + }); + + it('should enroll learner when enroll button clicked', function(){ + singleRunModeList[0].is_enrolled = false; + setupView(singleRunModeList); + expect(view.$('.enroll-button').length).toBe(1); + spyOn(courseEnrollModel, 'save'); + view.$('.enroll-button').click(); + expect(courseEnrollModel.save).toHaveBeenCalled(); + }); + + it('should enroll learner into the updated run with button click', function(){ + setupView(multiRunModeList); + spyOn(courseEnrollModel, 'save'); + view.$('.run-select').val(multiRunModeList[1].run_key); + view.$('.run-select').trigger("change"); + view.$('.enroll-button').click(); + expect(courseEnrollModel.save).toHaveBeenCalled(); + }); + + it('should redirect to trackSelectionUrl when enrollment success for audit track', function(){ + singleRunModeList[0].is_enrolled = false; + singleRunModeList[0].mode_slug = 'audit'; + setupView(singleRunModeList, 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_key')); + }); + + + it('should redirect when enrollment success for no track', function(){ + singleRunModeList[0].is_enrolled = false; + singleRunModeList[0].mode_slug = null; + setupView(singleRunModeList, 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_key')); + }); + + it('should not redirect when urls not provided', function(){ + singleRunModeList[0].is_enrolled = false; + singleRunModeList[0].mode_slug = 'verified'; + setupView(singleRunModeList); + 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(singleRunModeList, 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_key')); + }); + + it('should redirect to login on 403 error', function(){ + var response = { + status: 403, + responseJSON:{ + user_message_url: 'test_url/haha' + }}; + setupView(singleRunModeList, 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); }); }); } 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 index bb8a63fb52..db8c560160 100644 --- a/lms/static/js/spec/learner_dashboard/program_details_header_spec.js +++ b/lms/static/js/spec/learner_dashboard/program_details_header_spec.js @@ -8,9 +8,10 @@ define([ describe('Program Details Header View', function () { var view = null, - programModel, context = { - programListingUrl: '/dashboard/programs', + urls: { + program_listing_url: '/dashboard/programs' + }, programData: { uuid: '12-ab', name: 'Astrophysics', @@ -58,7 +59,7 @@ define([ expect(view.$('.org-logo').attr('alt')).toEqual( context.programData.organizations[0].display_name + '\'s logo' ); - expect(programListUrl).toEqual(context.programListingUrl); + expect(programListUrl).toEqual(context.urls.program_listing_url); }); }); } diff --git a/lms/static/sass/elements/_course-card.scss b/lms/static/sass/elements/_course-card.scss index 4578317749..bfece9319b 100644 --- a/lms/static/sass/elements/_course-card.scss +++ b/lms/static/sass/elements/_course-card.scss @@ -1,7 +1,7 @@ .course-card{ @include span(10); - margin-left: $baseline*2 + px; - margin-bottom: $baseline + px; + margin-left: $baseline*2; + margin-bottom: $baseline; .course-image-link{ @include float(left); .header-img{ @@ -12,25 +12,34 @@ @include float(right); width: 100%; @include susy-media($bp-screen-sm) { width: calc(100% - 191px); } - padding-left: $baseline*1.5 + px; + padding-left: $baseline*1.5; .course-title{ font-size: font-size(x-large); font-weight: font-weight(normal); - margin-bottom: $baseline/4 + px; + margin-bottom: $baseline/4; } .course-text{ color: palette(grayscale, dark); + .run-period{ + color: palette(grayscale, black); + } } } .course-actions{ .enrollment-info{ - width: $baseline*10 + px; + width: $baseline*10; text-align: center; - margin-bottom: $baseline/2 + px; + margin-bottom: $baseline/2; text-transform: uppercase; } + .run-select-container{ + margin-bottom: $baseline; + .run-select{ + width: $baseline*10; + } + } .enroll-button{ - width: $baseline*10 + px; + width: $baseline*10; text-align: center; background-color: palette(success, dark); border-color: palette(success, dark); @@ -42,7 +51,7 @@ } .view-course-link{ - width: $baseline*10 + px; + width: $baseline*10; text-align: center; } } diff --git a/lms/templates/learner_dashboard/course_card.underscore b/lms/templates/learner_dashboard/course_card.underscore index 01ba2dc4f4..d70a6e07ab 100644 --- a/lms/templates/learner_dashboard/course_card.underscore +++ b/lms/templates/learner_dashboard/course_card.underscore @@ -4,7 +4,7 @@ <%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true)%> + alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true) %>"/>

@@ -13,9 +13,8 @@

- <% if (is_enrolled){ %> - <%- start_date %> - <%- end_date %> - <% } %> + <%- start_date %> - <%- end_date %> + - <%- key %>
diff --git a/lms/templates/learner_dashboard/course_enroll.underscore b/lms/templates/learner_dashboard/course_enroll.underscore index 4b36fa0ab7..c2488ebb87 100644 --- a/lms/templates/learner_dashboard/course_enroll.underscore +++ b/lms/templates/learner_dashboard/course_enroll.underscore @@ -5,6 +5,27 @@ <% }else{ %>
<%- gettext('not enrolled') %>
+ <% if (run_modes.length > 1){ %> +
+ + +
+ <% } %> diff --git a/lms/templates/learner_dashboard/program_details.html b/lms/templates/learner_dashboard/program_details.html index b290783a9b..8af6668543 100644 --- a/lms/templates/learner_dashboard/program_details.html +++ b/lms/templates/learner_dashboard/program_details.html @@ -15,7 +15,7 @@ from openedx.core.djangolib.js_utils import ( <%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory"> ProgramDetailsFactory({ programData: ${program_data | n, dump_js_escaped_json}, - programListingUrl: '${program_listing_url | n, js_escaped_string}', + urls: ${urls | n, dump_js_escaped_json}, }); diff --git a/lms/templates/learner_dashboard/program_header_view.underscore b/lms/templates/learner_dashboard/program_header_view.underscore index 932989609f..e4664d2545 100644 --- a/lms/templates/learner_dashboard/program_header_view.underscore +++ b/lms/templates/learner_dashboard/program_header_view.underscore @@ -29,7 +29,7 @@
  • - <%- gettext('Programs') %> + <%- gettext('Programs') %>