ECOM-3206 allow run selection and enrollment (#12685)

This commit is contained in:
Simon Chen
2016-06-13 15:08:00 -04:00
committed by GitHub
parent 781a20497b
commit 8d672bc832
18 changed files with 438 additions and 89 deletions

View File

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

View File

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

View File

@@ -46,6 +46,7 @@
if (this.titleContext){
this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString());
}
this.$el.html(childList);
}
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('<div class="course-actions"></div>');
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);
});
});
}

View File

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

View File

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