Commit 4c51f873 by Will Daly

Merge pull request #5883 from edx/will/logistration-enrollment-cleanup

Logistration: enrollment cleanup and a few fixes
parents 1a5cc867 e629ce2b
...@@ -14,7 +14,7 @@ from student.models import NonExistentCourseError, CourseEnrollmentException ...@@ -14,7 +14,7 @@ from student.models import NonExistentCourseError, CourseEnrollmentException
class EnrollmentUserThrottle(UserRateThrottle): class EnrollmentUserThrottle(UserRateThrottle):
rate = '50/second' # TODO Limit significantly after performance testing. rate = '50/second' # TODO Limit significantly after performance testing.
@api_view(['GET']) @api_view(['GET'])
......
...@@ -1034,7 +1034,9 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins ...@@ -1034,7 +1034,9 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins
student_account_js = [ student_account_js = [
'js/utils/rwd_header_footer.js', 'js/utils/rwd_header_footer.js',
'js/utils/edx.utils.validate.js', 'js/utils/edx.utils.validate.js',
'js/student_account/enrollment_interface.js', 'js/src/utility.js',
'js/student_account/enrollment.js',
'js/student_account/shoppingcart.js',
'js/student_account/models/LoginModel.js', 'js/student_account/models/LoginModel.js',
'js/student_account/models/RegisterModel.js', 'js/student_account/models/RegisterModel.js',
'js/student_account/models/PasswordResetModel.js', 'js/student_account/models/PasswordResetModel.js',
......
...@@ -289,9 +289,13 @@ ...@@ -289,9 +289,13 @@
exports: 'NotificationView', exports: 'NotificationView',
deps: ['backbone', 'jquery', 'underscore'] deps: ['backbone', 'jquery', 'underscore']
}, },
'js/student_account/enrollment_interface': { 'js/student_account/enrollment': {
exports: 'edx.student.account.EnrollmentInterface', exports: 'edx.student.account.EnrollmentInterface',
deps: ['jquery', 'jquery.cookie', 'underscore', 'gettext'] deps: ['jquery', 'jquery.cookie']
},
'js/student_account/shoppingcart': {
exports: 'edx.student.account.ShoppingCartInterface',
deps: ['jquery', 'jquery.cookie', 'underscore']
}, },
// Student account registration/login // Student account registration/login
// Loaded explicitly until these are converted to RequireJS // Loaded explicitly until these are converted to RequireJS
...@@ -350,13 +354,16 @@ ...@@ -350,13 +354,16 @@
'underscore', 'underscore',
'backbone', 'backbone',
'gettext', 'gettext',
'utility',
'js/student_account/views/LoginView', 'js/student_account/views/LoginView',
'js/student_account/views/PasswordResetView', 'js/student_account/views/PasswordResetView',
'js/student_account/views/RegisterView', 'js/student_account/views/RegisterView',
'js/student_account/models/LoginModel', 'js/student_account/models/LoginModel',
'js/student_account/models/PasswordResetModel', 'js/student_account/models/PasswordResetModel',
'js/student_account/models/RegisterModel', 'js/student_account/models/RegisterModel',
'js/student_account/views/FormView' 'js/student_account/views/FormView',
'js/student_account/enrollment',
'js/student_account/shoppingcart',
] ]
} }
} }
...@@ -375,7 +382,8 @@ ...@@ -375,7 +382,8 @@
'lms/include/js/spec/student_account/login_spec.js', 'lms/include/js/spec/student_account/login_spec.js',
'lms/include/js/spec/student_account/register_spec.js', 'lms/include/js/spec/student_account/register_spec.js',
'lms/include/js/spec/student_account/password_reset_spec.js', 'lms/include/js/spec/student_account/password_reset_spec.js',
'lms/include/js/spec/student_account/enrollment_interface_spec.js', 'lms/include/js/spec/student_account/enrollment_spec.js',
'lms/include/js/spec/student_account/shoppingcart_spec.js',
'lms/include/js/spec/student_profile/profile_spec.js' 'lms/include/js/spec/student_profile/profile_spec.js'
]); ]);
......
...@@ -3,8 +3,10 @@ define([ ...@@ -3,8 +3,10 @@ define([
'js/common_helpers/template_helpers', 'js/common_helpers/template_helpers',
'js/common_helpers/ajax_helpers', 'js/common_helpers/ajax_helpers',
'js/student_account/views/AccessView', 'js/student_account/views/AccessView',
'js/student_account/views/FormView' 'js/student_account/views/FormView',
], function($, TemplateHelpers, AjaxHelpers, AccessView) { 'js/student_account/enrollment',
'js/student_account/shoppingcart'
], function($, TemplateHelpers, AjaxHelpers, AccessView, FormView, EnrollmentInterface, ShoppingCartInterface) {
describe('edx.student.account.AccessView', function() { describe('edx.student.account.AccessView', function() {
'use strict'; 'use strict';
...@@ -51,7 +53,9 @@ define([ ...@@ -51,7 +53,9 @@ define([
} }
} }
] ]
}; },
FORWARD_URL = '/courseware/next',
COURSE_KEY = 'edx/DemoX/Fall';
var ajaxAssertAndRespond = function(url, requestIndex) { var ajaxAssertAndRespond = function(url, requestIndex) {
// Verify that the client contacts the server as expected // Verify that the client contacts the server as expected
...@@ -77,6 +81,14 @@ define([ ...@@ -77,6 +81,14 @@ define([
platformName: 'edX' platformName: 'edX'
}); });
// Mock the redirect call
spyOn( view, 'redirect' ).andCallFake( function() {} );
// Mock the enrollment and shopping cart interfaces
spyOn( EnrollmentInterface, 'enroll' ).andCallFake( function() {} );
spyOn( ShoppingCartInterface, 'addCourseToCart' ).andCallFake( function() {} );
// Initialize the subview
ajaxAssertAndRespond(AJAX_INFO[mode].url); ajaxAssertAndRespond(AJAX_INFO[mode].url);
}; };
...@@ -97,6 +109,20 @@ define([ ...@@ -97,6 +109,20 @@ define([
ajaxAssertAndRespond(AJAX_INFO[type].url, AJAX_INFO[type].requestIndex); ajaxAssertAndRespond(AJAX_INFO[type].url, AJAX_INFO[type].requestIndex);
}; };
/**
* Simulate query string params.
*
* @param {object} params Parameters to set, each of which
* should be prefixed with '?'
*/
var setFakeQueryParams = function( params ) {
spyOn( $, 'url' ).andCallFake(function( requestedParam ) {
if ( params.hasOwnProperty(requestedParam) ) {
return params[requestedParam];
}
});
};
beforeEach(function() { beforeEach(function() {
setFixtures('<div id="login-and-registration-container"></div>'); setFixtures('<div id="login-and-registration-container"></div>');
TemplateHelpers.installTemplate('templates/student_account/access'); TemplateHelpers.installTemplate('templates/student_account/access');
...@@ -104,6 +130,11 @@ define([ ...@@ -104,6 +130,11 @@ define([
TemplateHelpers.installTemplate('templates/student_account/register'); TemplateHelpers.installTemplate('templates/student_account/register');
TemplateHelpers.installTemplate('templates/student_account/password_reset'); TemplateHelpers.installTemplate('templates/student_account/password_reset');
TemplateHelpers.installTemplate('templates/student_account/form_field'); TemplateHelpers.installTemplate('templates/student_account/form_field');
// Stub analytics tracking
// TODO: use RequireJS to ensure that this is loaded correctly
window.analytics = window.analytics || {};
window.analytics.track = window.analytics.track || function() {};
}); });
it('can initially display the login form', function() { it('can initially display the login form', function() {
...@@ -143,14 +174,83 @@ define([ ...@@ -143,14 +174,83 @@ define([
view.resetPassword(); view.resetPassword();
ajaxAssertAndRespond( ajaxAssertAndRespond(
AJAX_INFO['password_reset'].url, AJAX_INFO.password_reset.url,
AJAX_INFO['password_reset'].requestIndex AJAX_INFO.password_reset.requestIndex
); );
// Verify that the password reset wrapper is populated // Verify that the password reset wrapper is populated
expect($('#password-reset-wrapper')).not.toBeEmpty(); expect($('#password-reset-wrapper')).not.toBeEmpty();
}); });
it('enrolls the user on auth complete', function() {
ajaxSpyAndInitialize(this, 'login');
// Simulate providing enrollment query string params
setFakeQueryParams({
'?enrollment_action': 'enroll',
'?course_id': COURSE_KEY
});
// Trigger auth complete on the login view
view.subview.login.trigger('auth-complete');
// Expect that the view tried to enroll the student
expect( EnrollmentInterface.enroll ).toHaveBeenCalledWith( COURSE_KEY );
});
it('adds a white-label course to the shopping cart on auth complete', function() {
ajaxSpyAndInitialize(this, 'register');
// Simulate providing "add to cart" query string params
setFakeQueryParams({
'?enrollment_action': 'add_to_cart',
'?course_id': COURSE_KEY
});
// Trigger auth complete on the register view
view.subview.register.trigger('auth-complete');
// Expect that the view tried to add the course to the user's shopping cart
expect( ShoppingCartInterface.addCourseToCart ).toHaveBeenCalledWith( COURSE_KEY );
});
it('redirects the user to the dashboard on auth complete', function() {
ajaxSpyAndInitialize(this, 'register');
// Trigger auth complete
view.subview.register.trigger('auth-complete');
// Since we did not provide a ?next query param, expect a redirect to the dashboard.
expect( view.redirect ).toHaveBeenCalledWith( '/dashboard' );
});
it('redirects the user to the next page on auth complete', function() {
ajaxSpyAndInitialize(this, 'register');
// Simulate providing a ?next query string parameter
setFakeQueryParams({ '?next': FORWARD_URL });
// Trigger auth complete
view.subview.register.trigger('auth-complete');
// Verify that we were redirected
expect( view.redirect ).toHaveBeenCalledWith( FORWARD_URL );
});
it('ignores redirect to external URLs', function() {
ajaxSpyAndInitialize(this, 'register');
// Simulate providing a ?next query string parameter
// that goes to an external URL
setFakeQueryParams({ '?next': "http://www.example.com" });
// Trigger auth complete
view.subview.register.trigger('auth-complete');
// Expect that we ignore the external URL and redirect to the dashboard
expect( view.redirect ).toHaveBeenCalledWith( "/dashboard" );
});
it('displays an error if a form definition could not be loaded', function() { it('displays an error if a form definition could not be loaded', function() {
// Spy on AJAX requests // Spy on AJAX requests
requests = AjaxHelpers.requests(this); requests = AjaxHelpers.requests(this);
......
define(['js/student_account/enrollment_interface'],
function(EnrollmentInterface) {
describe("edx.student.account.EnrollmentInterface", function() {
'use strict';
it('checks if a given course mode slug exists in an array of mode objects', function() {
var courseModes = [ { slug: 'honor' }, { slug: 'professional' } ]
expect( EnrollmentInterface.modeInArray( courseModes, 'professional' ) ).toBe(true);
expect( EnrollmentInterface.modeInArray( courseModes, 'audit' ) ).toBe(false);
});
});
}
);
define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
function( AjaxHelpers, EnrollmentInterface ) {
'use strict';
describe( 'edx.student.account.EnrollmentInterface', function() {
var COURSE_KEY = 'edX/DemoX/Fall',
ENROLL_URL = '/enrollment/v0/course/edX/DemoX/Fall',
FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/';
beforeEach(function() {
// Mock the redirect call
spyOn(EnrollmentInterface, 'redirect').andCallFake(function() {});
});
it('enrolls a user in a course', function() {
// Spy on Ajax requests
var requests = AjaxHelpers.requests( this );
// Attempt to enroll the user
EnrollmentInterface.enroll( COURSE_KEY );
// Expect that the correct request was made to the server
AjaxHelpers.expectRequest( requests, 'POST', ENROLL_URL );
// Simulate a successful response from the server
AjaxHelpers.respondWithJson(requests, {});
// Verify that the user was redirected correctly
expect( EnrollmentInterface.redirect ).toHaveBeenCalledWith( FORWARD_URL );
});
it('redirects the user if enrollment fails', function() {
// Spy on Ajax requests
var requests = AjaxHelpers.requests( this );
// Attempt to enroll the user
EnrollmentInterface.enroll( COURSE_KEY );
// Simulate an error response from the server
AjaxHelpers.respondWithError(requests);
// Verify that the user was still redirected
expect(EnrollmentInterface.redirect).toHaveBeenCalledWith( FORWARD_URL );
});
});
}
);
...@@ -12,6 +12,7 @@ define([ ...@@ -12,6 +12,7 @@ define([
var model = null, var model = null,
view = null, view = null,
requests = null, requests = null,
authComplete = false,
PLATFORM_NAME = 'edX', PLATFORM_NAME = 'edX',
USER_DATA = { USER_DATA = {
email: 'xsy@edx.org', email: 'xsy@edx.org',
...@@ -72,7 +73,10 @@ define([ ...@@ -72,7 +73,10 @@ define([
var createLoginView = function(test) { var createLoginView = function(test) {
// Initialize the login model // Initialize the login model
model = new LoginModel({ url: FORM_DESCRIPTION.submit_url }); model = new LoginModel({}, {
url: FORM_DESCRIPTION.submit_url,
method: FORM_DESCRIPTION.method
});
// Initialize the login view // Initialize the login view
view = new LoginView({ view = new LoginView({
...@@ -85,9 +89,10 @@ define([ ...@@ -85,9 +89,10 @@ define([
// Spy on AJAX requests // Spy on AJAX requests
requests = AjaxHelpers.requests(test); requests = AjaxHelpers.requests(test);
// Mock out redirection logic // Intercept events from the view
spyOn(view, 'redirect').andCallFake(function() { authComplete = false;
return true; view.on("auth-complete", function() {
authComplete = true;
}); });
}; };
...@@ -130,16 +135,16 @@ define([ ...@@ -130,16 +135,16 @@ define([
// Verify that the client contacts the server with the expected data // Verify that the client contacts the server with the expected data
AjaxHelpers.expectRequest( AjaxHelpers.expectRequest(
requests, 'POST', FORM_DESCRIPTION.submit_url, $.param( requests, 'POST',
$.extend({url: FORM_DESCRIPTION.submit_url}, USER_DATA) FORM_DESCRIPTION.submit_url,
) $.param( USER_DATA )
); );
// Respond with status code 200 // Respond with status code 200
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
// Verify that the user is redirected to the dashboard // Verify that auth-complete is triggered
expect(view.redirect).toHaveBeenCalledWith('/dashboard'); expect(authComplete).toBe(true);
}); });
it('displays third-party auth login buttons', function() { it('displays third-party auth login buttons', function() {
...@@ -175,6 +180,9 @@ define([ ...@@ -175,6 +180,9 @@ define([
// Verify that submission errors are visible // Verify that submission errors are visible
expect(view.$errors).not.toHaveClass('hidden'); expect(view.$errors).not.toHaveClass('hidden');
// Expect auth complete NOT to have been triggered
expect(authComplete).toBe(false);
}); });
it('displays an error if the server returns an error while logging in', function() { it('displays an error if the server returns an error while logging in', function() {
...@@ -186,9 +194,9 @@ define([ ...@@ -186,9 +194,9 @@ define([
// Simulate an error from the LMS servers // Simulate an error from the LMS servers
AjaxHelpers.respondWithError(requests); AjaxHelpers.respondWithError(requests);
// Expect that an error is displayed, and that we haven't been redirected // Expect that an error is displayed and that auth complete is not triggered
expect(view.$errors).not.toHaveClass('hidden'); expect(view.$errors).not.toHaveClass('hidden');
expect(view.redirect).not.toHaveBeenCalled(); expect(authComplete).toBe(false);
// If we try again and succeed, the error should go away // If we try again and succeed, the error should go away
submitForm(); submitForm();
...@@ -196,8 +204,9 @@ define([ ...@@ -196,8 +204,9 @@ define([
// This time, respond with status code 200 // This time, respond with status code 200
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
// Expect that the error is hidden // Expect that the error is hidden and auth complete is triggered
expect(view.$errors).toHaveClass('hidden'); expect(view.$errors).toHaveClass('hidden');
expect(authComplete).toBe(true);
}); });
}); });
} }
......
...@@ -30,7 +30,10 @@ define([ ...@@ -30,7 +30,10 @@ define([
var createPasswordResetView = function(that) { var createPasswordResetView = function(that) {
// Initialize the password reset model // Initialize the password reset model
model = new PasswordResetModel({ url: FORM_DESCRIPTION.submit_url }); model = new PasswordResetModel({}, {
url: FORM_DESCRIPTION.submit_url,
method: FORM_DESCRIPTION.method
});
// Initialize the password reset view // Initialize the password reset view
view = new PasswordResetView({ view = new PasswordResetView({
...@@ -77,10 +80,9 @@ define([ ...@@ -77,10 +80,9 @@ define([
// Verify that the client contacts the server with the expected data // Verify that the client contacts the server with the expected data
AjaxHelpers.expectRequest( AjaxHelpers.expectRequest(
requests, 'POST', FORM_DESCRIPTION.submit_url, $.param({ requests, 'POST',
url: FORM_DESCRIPTION.submit_url, FORM_DESCRIPTION.submit_url,
email: EMAIL $.param({ email: EMAIL })
})
); );
// Respond with status code 200 // Respond with status code 200
...@@ -125,10 +127,10 @@ define([ ...@@ -125,10 +127,10 @@ define([
// If we try again and succeed, the error should go away // If we try again and succeed, the error should go away
submitEmail(); submitEmail();
// This time, respond with status code 200 // This time, respond with status code 200
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
// Expect that the error is hidden // Expect that the error is hidden
expect(view.$errors).toHaveClass('hidden'); expect(view.$errors).toHaveClass('hidden');
}); });
......
...@@ -12,6 +12,7 @@ define([ ...@@ -12,6 +12,7 @@ define([
var model = null, var model = null,
view = null, view = null,
requests = null, requests = null,
authComplete = false,
PLATFORM_NAME = 'edX', PLATFORM_NAME = 'edX',
USER_DATA = { USER_DATA = {
email: 'xsy@edx.org', email: 'xsy@edx.org',
...@@ -160,7 +161,10 @@ define([ ...@@ -160,7 +161,10 @@ define([
var createRegisterView = function(that) { var createRegisterView = function(that) {
// Initialize the register model // Initialize the register model
model = new RegisterModel({ url: FORM_DESCRIPTION.submit_url }); model = new RegisterModel({}, {
url: FORM_DESCRIPTION.submit_url,
method: FORM_DESCRIPTION.method
});
// Initialize the register view // Initialize the register view
view = new RegisterView({ view = new RegisterView({
...@@ -173,9 +177,10 @@ define([ ...@@ -173,9 +177,10 @@ define([
// Spy on AJAX requests // Spy on AJAX requests
requests = AjaxHelpers.requests(that); requests = AjaxHelpers.requests(that);
// Mock out redirection logic // Intercept events from the view
spyOn(view, 'redirect').andCallFake(function() { authComplete = false;
return true; view.on("auth-complete", function() {
authComplete = true;
}); });
}; };
...@@ -225,16 +230,16 @@ define([ ...@@ -225,16 +230,16 @@ define([
// Verify that the client contacts the server with the expected data // Verify that the client contacts the server with the expected data
AjaxHelpers.expectRequest( AjaxHelpers.expectRequest(
requests, 'POST', FORM_DESCRIPTION.submit_url, $.param( requests, 'POST',
$.extend({url: FORM_DESCRIPTION.submit_url}, USER_DATA) FORM_DESCRIPTION.submit_url,
) $.param( USER_DATA )
); );
// Respond with status code 200 // Respond with status code 200
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
// Verify that the user is redirected to the dashboard // Verify that auth complete is triggered
expect(view.redirect).toHaveBeenCalledWith('/dashboard'); expect(authComplete).toBe(true);
}); });
it('displays third-party auth registration buttons', function() { it('displays third-party auth registration buttons', function() {
...@@ -269,6 +274,9 @@ define([ ...@@ -269,6 +274,9 @@ define([
// Verify that submission errors are visible // Verify that submission errors are visible
expect(view.$errors).not.toHaveClass('hidden'); expect(view.$errors).not.toHaveClass('hidden');
// Expect that auth complete is NOT triggered
expect(authComplete).toBe(false);
}); });
it('displays an error if the server returns an error while registering', function() { it('displays an error if the server returns an error while registering', function() {
...@@ -280,8 +288,9 @@ define([ ...@@ -280,8 +288,9 @@ define([
// Simulate an error from the LMS servers // Simulate an error from the LMS servers
AjaxHelpers.respondWithError(requests); AjaxHelpers.respondWithError(requests);
// Expect that an error is displayed // Expect that an error is displayed and that auth complete is NOT triggered
expect(view.$errors).not.toHaveClass('hidden'); expect(view.$errors).not.toHaveClass('hidden');
expect(authComplete).toBe(false);
// If we try again and succeed, the error should go away // If we try again and succeed, the error should go away
submitForm(); submitForm();
...@@ -289,8 +298,9 @@ define([ ...@@ -289,8 +298,9 @@ define([
// This time, respond with status code 200 // This time, respond with status code 200
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
// Expect that the error is hidden // Expect that the error is hidden and that auth complete is triggered
expect(view.$errors).toHaveClass('hidden'); expect(view.$errors).toHaveClass('hidden');
expect(authComplete).toBe(true);
}); });
}); });
} }
......
define(['js/common_helpers/ajax_helpers', 'js/student_account/shoppingcart'],
function(AjaxHelpers, ShoppingCartInterface) {
'use strict';
describe( 'edx.student.account.ShoppingCartInterface', function() {
var COURSE_KEY = "edX/DemoX/Fall",
ADD_COURSE_URL = "/shoppingcart/add/course/edX/DemoX/Fall/",
FORWARD_URL = "/shoppingcart/";
beforeEach(function() {
// Mock the redirect call
spyOn(ShoppingCartInterface, 'redirect').andCallFake(function() {});
});
it('adds a course to the cart', function() {
// Spy on Ajax requests
var requests = AjaxHelpers.requests( this );
// Attempt to add a course to the cart
ShoppingCartInterface.addCourseToCart( COURSE_KEY );
// Expect that the correct request was made to the server
AjaxHelpers.expectRequest( requests, 'POST', ADD_COURSE_URL );
// Simulate a successful response from the server
AjaxHelpers.respondWithJson( requests, {} );
// Expect that the user was redirected to the shopping cart
expect( ShoppingCartInterface.redirect ).toHaveBeenCalledWith( FORWARD_URL );
});
it('redirects the user on a server error', function() {
// Spy on Ajax requests
var requests = AjaxHelpers.requests( this );
// Attempt to add a course to the cart
ShoppingCartInterface.addCourseToCart( COURSE_KEY );
// Simulate an error response from the server
AjaxHelpers.respondWithError( requests );
// Expect that the user was redirected to the shopping cart
expect( ShoppingCartInterface.redirect ).toHaveBeenCalledWith( FORWARD_URL );
});
});
}
);
var edx = edx || {};
(function($) {
'use strict';
edx.student = edx.student || {};
edx.student.account = edx.student.account || {};
edx.student.account.EnrollmentInterface = {
urls: {
course: '/enrollment/v0/course/',
trackSelection: '/course_modes/choose/'
},
headers: {
'X-CSRFToken': $.cookie('csrftoken')
},
/**
* Enroll a user in a course, then redirect the user
* to the track selection page.
* @param {string} courseKey Slash-separated course key.
*/
enroll: function( courseKey ) {
$.ajax({
url: this.courseEnrollmentUrl( courseKey ),
type: 'POST',
data: {},
headers: this.headers,
context: this
}).always(function() {
this.redirect( this.trackSelectionUrl( courseKey ) );
});
},
/**
* Construct the URL to the track selection page for a course.
* @param {string} courseKey Slash-separated course key.
* @return {string} The URL to the track selection page.
*/
trackSelectionUrl: function( courseKey ) {
return this.urls.trackSelection + courseKey + '/';
},
/**
* Construct a URL to enroll in a course.
* @param {string} courseKey Slash-separated course key.
* @return {string} The URL to enroll in a course.
*/
courseEnrollmentUrl: function( courseKey ) {
return this.urls.course + courseKey;
},
/**
* Redirect to a URL. Mainly useful for mocking out in tests.
* @param {string} url The URL to redirect to.
*/
redirect: function(url) {
window.location.href = url;
}
};
})(jQuery);
var edx = edx || {};
(function($, _, gettext) {
'use strict';
edx.student = edx.student || {};
edx.student.account = edx.student.account || {};
edx.student.account.EnrollmentInterface = {
courseUrl: '/enrollment/v0/course/',
studentUrl: '/enrollment/v0/student',
trackSelectionUrl: '/course_modes/choose/',
headers: {
'X-CSRFToken': $.cookie('csrftoken')
},
studentInformation: function(courseKey) {
// retrieve student enrollment information
},
courseInformation: function(courseKey) {
// retrieve course information from the enrollment API
},
modeInArray: function(modeObjects, targetMode) {
// Check if a given course mode slug exists in an array of mode objects
var result = _.find(modeObjects, function(mode) {
return mode.slug === targetMode;
});
/* _.find returns the first value which passes the provided truth test,
/* or undefined if no values pass the test
*/
return !_.isUndefined(result);
},
enroll: function(courseKey, forwardUrl){
var me = this;
// attempt to enroll a student in a course
$.ajax({
url: this.courseUrl + courseKey,
type: 'POST',
data: {},
headers: this.headers
}).done(function(data){
me.postEnrollmentHandler(courseKey, data, forwardUrl);
}
).fail(function(data, textStatus) {
me.enrollmentFailureHandler(courseKey, data, forwardUrl);
});
},
enrollmentFailureHandler: function(courseKey, data, forwardUrl) {
// handle failures to enroll via the API
if(data.status == 400) {
/* This status code probably means we don't have permissions to register
/* for this course; look at the contents of the response
*/
var course = $.parseJSON(data.responseText);
// see if it's a professional ed course
if( 'course_modes' in course && this.modeInArray(course.course_modes, 'professional') ) {
// forward appropriately
forwardUrl = this.trackSelectionUrl + courseKey;
}
}
// TODO: if we have a paid registration mode, add item to the cart and send them along
// TODO: we should figure out how to handle errors here
window.location.href = forwardUrl;
},
postEnrollmentHandler: function(courseKey, data, forwardUrl) {
// Determine whether or not the course needs to be redirected to
// a particular page.
var course = data.course,
course_modes = course.course_modes;
// send the user to the track selection page, because it will do the right thing
forwardUrl = this.trackSelectionUrl + courseKey;
window.location.href = forwardUrl;
}
};
})(jQuery, _, gettext);
...@@ -18,9 +18,9 @@ var edx = edx || {}; ...@@ -18,9 +18,9 @@ var edx = edx || {};
urlRoot: '', urlRoot: '',
initialize: function( obj ) { initialize: function( attributes, options ) {
this.ajaxType = obj.method; this.ajaxType = options.method;
this.urlRoot = obj.url; this.urlRoot = options.url;
}, },
sync: function(method, model) { sync: function(method, model) {
......
...@@ -16,9 +16,9 @@ var edx = edx || {}; ...@@ -16,9 +16,9 @@ var edx = edx || {};
urlRoot: '', urlRoot: '',
initialize: function( obj ) { initialize: function( attributes, options ) {
this.ajaxType = obj.method; this.ajaxType = options.method;
this.urlRoot = obj.url; this.urlRoot = options.url;
}, },
sync: function(method, model) { sync: function(method, model) {
......
...@@ -18,16 +18,15 @@ var edx = edx || {}; ...@@ -18,16 +18,15 @@ var edx = edx || {};
year_of_birth: '', year_of_birth: '',
mailing_address: '', mailing_address: '',
goals: '', goals: '',
honor_code: false
}, },
ajaxType: '', ajaxType: '',
urlRoot: '', urlRoot: '',
initialize: function( obj ) { initialize: function( attributes, options ) {
this.ajaxType = obj.method; this.ajaxType = options.method;
this.urlRoot = obj.url; this.urlRoot = options.url;
}, },
sync: function(method, model) { sync: function(method, model) {
......
/**
* Use the shopping cart to purchase courses.
*/
var edx = edx || {};
(function($) {
'use strict';
edx.student = edx.student || {};
edx.student.account = edx.student.account || {};
edx.student.account.ShoppingCartInterface = {
urls: {
viewCart: "/shoppingcart/",
addCourse: "/shoppingcart/add/course/"
},
headers: {
'X-CSRFToken': $.cookie('csrftoken')
},
/**
* Add a course to a cart, then redirect to the view cart page.
* @param {string} courseId The slash-separated course ID to add to the cart.
*/
addCourseToCart: function( courseId ) {
$.ajax({
url: this.urls.addCourse + courseId + "/",
type: 'POST',
data: {},
headers: this.headers,
context: this
}).always(function() {
this.redirect( this.urls.viewCart );
});
},
/**
* Redirect to a URL. Mainly useful for mocking out in tests.
* @param {string} url The URL to redirect to.
*/
redirect: function( url ) {
window.location.href = url;
}
};
})(jQuery);
...@@ -64,7 +64,7 @@ var edx = edx || {}; ...@@ -64,7 +64,7 @@ var edx = edx || {};
load: { load: {
login: function( data, context ) { login: function( data, context ) {
var model = new edx.student.account.LoginModel({ var model = new edx.student.account.LoginModel({}, {
method: data.method, method: data.method,
url: data.submit_url url: data.submit_url
}); });
...@@ -78,10 +78,14 @@ var edx = edx || {}; ...@@ -78,10 +78,14 @@ var edx = edx || {};
// Listen for 'password-help' event to toggle sub-views // Listen for 'password-help' event to toggle sub-views
context.listenTo( context.subview.login, 'password-help', context.resetPassword ); context.listenTo( context.subview.login, 'password-help', context.resetPassword );
// Listen for 'auth-complete' event so we can enroll/redirect the user appropriately.
context.listenTo( context.subview.login, 'auth-complete', context.authComplete );
}, },
reset: function( data, context ) { reset: function( data, context ) {
var model = new edx.student.account.PasswordResetModel({ var model = new edx.student.account.PasswordResetModel({}, {
method: data.method, method: data.method,
url: data.submit_url url: data.submit_url
}); });
...@@ -93,7 +97,7 @@ var edx = edx || {}; ...@@ -93,7 +97,7 @@ var edx = edx || {};
}, },
register: function( data, context ) { register: function( data, context ) {
var model = new edx.student.account.RegisterModel({ var model = new edx.student.account.RegisterModel({}, {
method: data.method, method: data.method,
url: data.submit_url url: data.submit_url
}); });
...@@ -104,6 +108,9 @@ var edx = edx || {}; ...@@ -104,6 +108,9 @@ var edx = edx || {};
thirdPartyAuth: context.thirdPartyAuth, thirdPartyAuth: context.thirdPartyAuth,
platformName: context.platformName platformName: context.platformName
}); });
// Listen for 'auth-complete' event so we can enroll/redirect the user appropriately.
context.listenTo( context.subview.register, 'auth-complete', context.authComplete );
} }
}, },
...@@ -162,6 +169,92 @@ var edx = edx || {}; ...@@ -162,6 +169,92 @@ var edx = edx || {};
},'slow'); },'slow');
}, },
/**
* Once authentication has completed successfully, a user may need to:
*
* - Enroll in a course.
* - Add a course to the shopping cart.
* - Be redirected to the dashboard / track selection page / shopping cart.
*
* This handler is triggered upon successful authentication,
* either from the login or registration form. It checks
* query string params, performs enrollment/shopping cart actions,
* then redirects the user to the next page.
*
* The optional query string params are:
*
* ?next: If provided, redirect to this page upon successful auth.
* Django uses this when an unauthenticated user accesses a view
* decorated with @login_required.
*
* ?enrollment_action: Can be either "enroll" or "add_to_cart".
* If you provide this param, you must also provide a `course_id` param;
* otherwise, no action will be taken.
*
* ?course_id: The slash-separated course ID to enroll in or add to the cart.
*
*/
authComplete: function() {
var enrollment = edx.student.account.EnrollmentInterface,
shoppingcart = edx.student.account.ShoppingCartInterface,
redirectUrl = '/dashboard',
queryParams = this.queryParams();
if ( queryParams.enrollmentAction === 'enroll' && queryParams.courseId) {
/*
If we need to enroll in a course, mark as enrolled.
The enrollment interface will redirect the student once enrollment completes.
*/
enrollment.enroll( decodeURIComponent( queryParams.courseId ) );
} else if ( queryParams.enrollmentAction === 'add_to_cart' && queryParams.courseId) {
/*
If this is a paid course, add it to the shopping cart and redirect
the user to the "view cart" page.
*/
shoppingcart.addCourseToCart( decodeURIComponent( queryParams.courseId ) );
} else {
/*
Otherwise, redirect the user to the next page
Check for forwarding url and ensure that it isn't external.
If not, use the default forwarding URL.
*/
if ( !_.isNull( queryParams.next ) ) {
var next = decodeURIComponent( queryParams.next );
// Ensure that the URL is internal for security reasons
if ( !window.isExternal( next ) ) {
redirectUrl = next;
}
}
this.redirect( redirectUrl );
}
},
/**
* Redirect to a URL. Mainly useful for mocking out in tests.
* @param {string} url The URL to redirect to.
*/
redirect: function( url ) {
window.location.href = url;
},
/**
* Retrieve query params that we use post-authentication
* to decide whether to enroll a student in a course, add
* an item to the cart, or redirect.
*
* @return {object} The query params. If any param is not
* provided, it will default to null.
*/
queryParams: function() {
return {
next: $.url( '?next' ),
enrollmentAction: $.url( '?enrollment_action' ),
courseId: $.url( '?course_id' )
};
},
form: { form: {
isLoaded: function( $form ) { isLoaded: function( $form ) {
return $form.html().length > 0; return $form.html().length > 0;
......
...@@ -79,29 +79,7 @@ var edx = edx || {}; ...@@ -79,29 +79,7 @@ var edx = edx || {};
}, },
saveSuccess: function () { saveSuccess: function () {
var enrollment = edx.student.account.EnrollmentInterface, this.trigger('auth-complete');
redirectUrl = '/dashboard',
next = null;
// Check for forwarding url
if ( !_.isNull( $.url('?next') ) ) {
next = decodeURIComponent( $.url('?next') );
if ( !window.isExternal(next) ) {
redirectUrl = next;
}
}
// If we need to enroll in a course, mark as enrolled
if ( $.url('?enrollment_action') === 'enroll' ) {
enrollment.enroll( decodeURIComponent( $.url('?course_id') ), redirectUrl );
} else {
this.redirect(redirectUrl);
}
},
redirect: function( url ) {
window.location.href = url;
}, },
saveError: function( error ) { saveError: function( error ) {
......
var edx = edx || {}; var edx = edx || {};
(function($, _, gettext) { (function($, gettext) {
'use strict'; 'use strict';
edx.student = edx.student || {}; edx.student = edx.student || {};
...@@ -39,4 +39,4 @@ var edx = edx || {}; ...@@ -39,4 +39,4 @@ var edx = edx || {};
} }
}); });
})(jQuery, _, gettext); })(jQuery, gettext);
...@@ -55,25 +55,7 @@ var edx = edx || {}; ...@@ -55,25 +55,7 @@ var edx = edx || {};
}, },
saveSuccess: function() { saveSuccess: function() {
var enrollment = edx.student.account.EnrollmentInterface, this.trigger('auth-complete');
redirectUrl = '/dashboard',
next = null;
// Check for forwarding url
if ( !_.isNull( $.url('?next') ) ) {
next = decodeURIComponent( $.url('?next') );
if ( !window.isExternal(next) ) {
redirectUrl = next;
}
}
// If we need to enroll in a course, mark as enrolled
if ( $.url('?enrollment_action') === 'enroll' ) {
enrollment.enroll( decodeURIComponent( $.url('?course_id') ), redirectUrl );
} else {
this.redirect(redirectUrl);
}
}, },
redirect: function( url ) { redirect: function( url ) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment