diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index eea296c..c2e401d 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -14,7 +14,7 @@ from student.models import NonExistentCourseError, CourseEnrollmentException class EnrollmentUserThrottle(UserRateThrottle): - rate = '50/second' # TODO Limit significantly after performance testing. + rate = '50/second' # TODO Limit significantly after performance testing. @api_view(['GET']) diff --git a/lms/envs/common.py b/lms/envs/common.py index 308a068..77468e4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1034,7 +1034,9 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins student_account_js = [ 'js/utils/rwd_header_footer.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/RegisterModel.js', 'js/student_account/models/PasswordResetModel.js', diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 722692a..f081eb3 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -289,9 +289,13 @@ exports: 'NotificationView', deps: ['backbone', 'jquery', 'underscore'] }, - 'js/student_account/enrollment_interface': { + 'js/student_account/enrollment': { 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 // Loaded explicitly until these are converted to RequireJS @@ -350,13 +354,16 @@ 'underscore', 'backbone', 'gettext', + 'utility', 'js/student_account/views/LoginView', 'js/student_account/views/PasswordResetView', 'js/student_account/views/RegisterView', 'js/student_account/models/LoginModel', 'js/student_account/models/PasswordResetModel', '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 @@ 'lms/include/js/spec/student_account/login_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/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' ]); diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js index 8a9cfb7..896ef75 100644 --- a/lms/static/js/spec/student_account/access_spec.js +++ b/lms/static/js/spec/student_account/access_spec.js @@ -3,8 +3,10 @@ define([ 'js/common_helpers/template_helpers', 'js/common_helpers/ajax_helpers', 'js/student_account/views/AccessView', - 'js/student_account/views/FormView' -], function($, TemplateHelpers, AjaxHelpers, AccessView) { + 'js/student_account/views/FormView', + 'js/student_account/enrollment', + 'js/student_account/shoppingcart' +], function($, TemplateHelpers, AjaxHelpers, AccessView, FormView, EnrollmentInterface, ShoppingCartInterface) { describe('edx.student.account.AccessView', function() { 'use strict'; @@ -51,7 +53,9 @@ define([ } } ] - }; + }, + FORWARD_URL = '/courseware/next', + COURSE_KEY = 'edx/DemoX/Fall'; var ajaxAssertAndRespond = function(url, requestIndex) { // Verify that the client contacts the server as expected @@ -77,6 +81,14 @@ define([ 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); }; @@ -97,6 +109,20 @@ define([ 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() { setFixtures('<div id="login-and-registration-container"></div>'); TemplateHelpers.installTemplate('templates/student_account/access'); @@ -104,6 +130,11 @@ define([ TemplateHelpers.installTemplate('templates/student_account/register'); TemplateHelpers.installTemplate('templates/student_account/password_reset'); 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() { @@ -143,14 +174,83 @@ define([ view.resetPassword(); ajaxAssertAndRespond( - AJAX_INFO['password_reset'].url, - AJAX_INFO['password_reset'].requestIndex + AJAX_INFO.password_reset.url, + AJAX_INFO.password_reset.requestIndex ); // Verify that the password reset wrapper is populated 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() { // Spy on AJAX requests requests = AjaxHelpers.requests(this); diff --git a/lms/static/js/spec/student_account/enrollment_interface_spec.js b/lms/static/js/spec/student_account/enrollment_interface_spec.js deleted file mode 100644 index 204861e..0000000 --- a/lms/static/js/spec/student_account/enrollment_interface_spec.js +++ /dev/null @@ -1,14 +0,0 @@ -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); - }); - }); - } -); diff --git a/lms/static/js/spec/student_account/enrollment_spec.js b/lms/static/js/spec/student_account/enrollment_spec.js new file mode 100644 index 0000000..05119f1 --- /dev/null +++ b/lms/static/js/spec/student_account/enrollment_spec.js @@ -0,0 +1,49 @@ +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 ); + }); + + }); + } +); diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js index ec75902..32fd8e1 100644 --- a/lms/static/js/spec/student_account/login_spec.js +++ b/lms/static/js/spec/student_account/login_spec.js @@ -12,6 +12,7 @@ define([ var model = null, view = null, requests = null, + authComplete = false, PLATFORM_NAME = 'edX', USER_DATA = { email: 'xsy@edx.org', @@ -72,7 +73,10 @@ define([ var createLoginView = function(test) { // 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 view = new LoginView({ @@ -85,9 +89,10 @@ define([ // Spy on AJAX requests requests = AjaxHelpers.requests(test); - // Mock out redirection logic - spyOn(view, 'redirect').andCallFake(function() { - return true; + // Intercept events from the view + authComplete = false; + view.on("auth-complete", function() { + authComplete = true; }); }; @@ -130,16 +135,16 @@ define([ // Verify that the client contacts the server with the expected data AjaxHelpers.expectRequest( - requests, 'POST', FORM_DESCRIPTION.submit_url, $.param( - $.extend({url: FORM_DESCRIPTION.submit_url}, USER_DATA) - ) + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( USER_DATA ) ); // Respond with status code 200 AjaxHelpers.respondWithJson(requests, {}); - // Verify that the user is redirected to the dashboard - expect(view.redirect).toHaveBeenCalledWith('/dashboard'); + // Verify that auth-complete is triggered + expect(authComplete).toBe(true); }); it('displays third-party auth login buttons', function() { @@ -175,6 +180,9 @@ define([ // Verify that submission errors are visible 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() { @@ -186,9 +194,9 @@ define([ // Simulate an error from the LMS servers 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.redirect).not.toHaveBeenCalled(); + expect(authComplete).toBe(false); // If we try again and succeed, the error should go away submitForm(); @@ -196,8 +204,9 @@ define([ // This time, respond with status code 200 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(authComplete).toBe(true); }); }); } diff --git a/lms/static/js/spec/student_account/password_reset_spec.js b/lms/static/js/spec/student_account/password_reset_spec.js index 78128c9..37a6ab4 100644 --- a/lms/static/js/spec/student_account/password_reset_spec.js +++ b/lms/static/js/spec/student_account/password_reset_spec.js @@ -30,7 +30,10 @@ define([ var createPasswordResetView = function(that) { // 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 view = new PasswordResetView({ @@ -77,10 +80,9 @@ define([ // Verify that the client contacts the server with the expected data AjaxHelpers.expectRequest( - requests, 'POST', FORM_DESCRIPTION.submit_url, $.param({ - url: FORM_DESCRIPTION.submit_url, - email: EMAIL - }) + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param({ email: EMAIL }) ); // Respond with status code 200 @@ -125,10 +127,10 @@ define([ // If we try again and succeed, the error should go away submitEmail(); - + // This time, respond with status code 200 AjaxHelpers.respondWithJson(requests, {}); - + // Expect that the error is hidden expect(view.$errors).toHaveClass('hidden'); }); diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js index fcc0ceb..2c0d849 100644 --- a/lms/static/js/spec/student_account/register_spec.js +++ b/lms/static/js/spec/student_account/register_spec.js @@ -12,6 +12,7 @@ define([ var model = null, view = null, requests = null, + authComplete = false, PLATFORM_NAME = 'edX', USER_DATA = { email: 'xsy@edx.org', @@ -160,7 +161,10 @@ define([ var createRegisterView = function(that) { // 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 view = new RegisterView({ @@ -173,9 +177,10 @@ define([ // Spy on AJAX requests requests = AjaxHelpers.requests(that); - // Mock out redirection logic - spyOn(view, 'redirect').andCallFake(function() { - return true; + // Intercept events from the view + authComplete = false; + view.on("auth-complete", function() { + authComplete = true; }); }; @@ -225,16 +230,16 @@ define([ // Verify that the client contacts the server with the expected data AjaxHelpers.expectRequest( - requests, 'POST', FORM_DESCRIPTION.submit_url, $.param( - $.extend({url: FORM_DESCRIPTION.submit_url}, USER_DATA) - ) + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( USER_DATA ) ); // Respond with status code 200 AjaxHelpers.respondWithJson(requests, {}); - // Verify that the user is redirected to the dashboard - expect(view.redirect).toHaveBeenCalledWith('/dashboard'); + // Verify that auth complete is triggered + expect(authComplete).toBe(true); }); it('displays third-party auth registration buttons', function() { @@ -269,6 +274,9 @@ define([ // Verify that submission errors are visible 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() { @@ -280,8 +288,9 @@ define([ // Simulate an error from the LMS servers 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(authComplete).toBe(false); // If we try again and succeed, the error should go away submitForm(); @@ -289,8 +298,9 @@ define([ // This time, respond with status code 200 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(authComplete).toBe(true); }); }); } diff --git a/lms/static/js/spec/student_account/shoppingcart_spec.js b/lms/static/js/spec/student_account/shoppingcart_spec.js new file mode 100644 index 0000000..5936f76 --- /dev/null +++ b/lms/static/js/spec/student_account/shoppingcart_spec.js @@ -0,0 +1,48 @@ +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 ); + }); + }); + } +); diff --git a/lms/static/js/student_account/enrollment.js b/lms/static/js/student_account/enrollment.js new file mode 100644 index 0000000..f16e78f --- /dev/null +++ b/lms/static/js/student_account/enrollment.js @@ -0,0 +1,63 @@ +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); diff --git a/lms/static/js/student_account/enrollment_interface.js b/lms/static/js/student_account/enrollment_interface.js deleted file mode 100644 index 3572458..0000000 --- a/lms/static/js/student_account/enrollment_interface.js +++ /dev/null @@ -1,87 +0,0 @@ -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); diff --git a/lms/static/js/student_account/models/LoginModel.js b/lms/static/js/student_account/models/LoginModel.js index e131695..0b0124f 100644 --- a/lms/static/js/student_account/models/LoginModel.js +++ b/lms/static/js/student_account/models/LoginModel.js @@ -18,9 +18,9 @@ var edx = edx || {}; urlRoot: '', - initialize: function( obj ) { - this.ajaxType = obj.method; - this.urlRoot = obj.url; + initialize: function( attributes, options ) { + this.ajaxType = options.method; + this.urlRoot = options.url; }, sync: function(method, model) { diff --git a/lms/static/js/student_account/models/PasswordResetModel.js b/lms/static/js/student_account/models/PasswordResetModel.js index d43e591..60858dc 100644 --- a/lms/static/js/student_account/models/PasswordResetModel.js +++ b/lms/static/js/student_account/models/PasswordResetModel.js @@ -16,9 +16,9 @@ var edx = edx || {}; urlRoot: '', - initialize: function( obj ) { - this.ajaxType = obj.method; - this.urlRoot = obj.url; + initialize: function( attributes, options ) { + this.ajaxType = options.method; + this.urlRoot = options.url; }, sync: function(method, model) { diff --git a/lms/static/js/student_account/models/RegisterModel.js b/lms/static/js/student_account/models/RegisterModel.js index e5a009f..7e07e85 100644 --- a/lms/static/js/student_account/models/RegisterModel.js +++ b/lms/static/js/student_account/models/RegisterModel.js @@ -18,16 +18,15 @@ var edx = edx || {}; year_of_birth: '', mailing_address: '', goals: '', - honor_code: false }, ajaxType: '', urlRoot: '', - initialize: function( obj ) { - this.ajaxType = obj.method; - this.urlRoot = obj.url; + initialize: function( attributes, options ) { + this.ajaxType = options.method; + this.urlRoot = options.url; }, sync: function(method, model) { diff --git a/lms/static/js/student_account/shoppingcart.js b/lms/static/js/student_account/shoppingcart.js new file mode 100644 index 0000000..18ec197 --- /dev/null +++ b/lms/static/js/student_account/shoppingcart.js @@ -0,0 +1,49 @@ +/** +* 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); diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 4060d53..8b5debc 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -64,7 +64,7 @@ var edx = edx || {}; load: { login: function( data, context ) { - var model = new edx.student.account.LoginModel({ + var model = new edx.student.account.LoginModel({}, { method: data.method, url: data.submit_url }); @@ -78,10 +78,14 @@ var edx = edx || {}; // Listen for 'password-help' event to toggle sub-views 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 ) { - var model = new edx.student.account.PasswordResetModel({ + var model = new edx.student.account.PasswordResetModel({}, { method: data.method, url: data.submit_url }); @@ -93,7 +97,7 @@ var edx = edx || {}; }, register: function( data, context ) { - var model = new edx.student.account.RegisterModel({ + var model = new edx.student.account.RegisterModel({}, { method: data.method, url: data.submit_url }); @@ -104,6 +108,9 @@ var edx = edx || {}; thirdPartyAuth: context.thirdPartyAuth, 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 || {}; },'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: { isLoaded: function( $form ) { return $form.html().length > 0; diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js index b10e4d6..23df7bf 100644 --- a/lms/static/js/student_account/views/LoginView.js +++ b/lms/static/js/student_account/views/LoginView.js @@ -79,29 +79,7 @@ var edx = edx || {}; }, saveSuccess: function () { - var enrollment = edx.student.account.EnrollmentInterface, - 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; + this.trigger('auth-complete'); }, saveError: function( error ) { diff --git a/lms/static/js/student_account/views/PasswordResetView.js b/lms/static/js/student_account/views/PasswordResetView.js index da85293..40af682 100644 --- a/lms/static/js/student_account/views/PasswordResetView.js +++ b/lms/static/js/student_account/views/PasswordResetView.js @@ -1,6 +1,6 @@ var edx = edx || {}; -(function($, _, gettext) { +(function($, gettext) { 'use strict'; edx.student = edx.student || {}; @@ -39,4 +39,4 @@ var edx = edx || {}; } }); -})(jQuery, _, gettext); +})(jQuery, gettext); diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js index afa250d..2fd857f 100644 --- a/lms/static/js/student_account/views/RegisterView.js +++ b/lms/static/js/student_account/views/RegisterView.js @@ -55,25 +55,7 @@ var edx = edx || {}; }, saveSuccess: function() { - var enrollment = edx.student.account.EnrollmentInterface, - 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); - } + this.trigger('auth-complete'); }, redirect: function( url ) {