From 95a435abc5fbb1e441df9b6acbb03d49f3f1bd6e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 10 Nov 2014 09:14:39 -0500 Subject: [PATCH] Include course ID in analytics events for logistration --- common/djangoapps/user_api/helpers.py | 14 + .../djangoapps/user_api/tests/test_helpers.py | 11 + common/djangoapps/user_api/views.py | 16 +- .../js/spec/student_account/login_spec.js | 427 +++++++------ .../js/spec/student_account/register_spec.js | 594 +++++++++--------- .../js/student_account/models/LoginModel.js | 21 +- .../student_account/models/RegisterModel.js | 21 +- 7 files changed, 614 insertions(+), 490 deletions(-) diff --git a/common/djangoapps/user_api/helpers.py b/common/djangoapps/user_api/helpers.py index bfd7d79d50..b09c72ac38 100644 --- a/common/djangoapps/user_api/helpers.py +++ b/common/djangoapps/user_api/helpers.py @@ -343,6 +343,20 @@ def shim_student_view(view_func, check_logged_in=False): if "course_id" in request.POST: del request.POST["course_id"] + # Include the course ID if it's specified in the analytics info + # so it can be included in analytics events. + if "analytics" in request.POST: + try: + analytics = json.loads(request.POST["analytics"]) + if "enroll_course_id" in analytics: + request.POST["course_id"] = analytics.get("enroll_course_id") + except (ValueError, TypeError): + LOGGER.error( + u"Could not parse analytics object sent to user API: {analytics}".format( + analytics=analytics + ) + ) + # Backwards compatibility: the student view expects both # terms of service and honor code values. Since we're combining # these into a single checkbox, the only value we may get diff --git a/common/djangoapps/user_api/tests/test_helpers.py b/common/djangoapps/user_api/tests/test_helpers.py index 4c7632f659..215957dd40 100644 --- a/common/djangoapps/user_api/tests/test_helpers.py +++ b/common/djangoapps/user_api/tests/test_helpers.py @@ -150,6 +150,17 @@ class StudentViewShimTest(TestCase): self.assertNotIn("enrollment_action", self.captured_request.POST) self.assertNotIn("course_id", self.captured_request.POST) + def test_include_analytics_info(self): + view = self._shimmed_view(HttpResponse()) + request = HttpRequest() + request.POST["analytics"] = json.dumps({ + "enroll_course_id": "edX/DemoX/Fall" + }) + view(request) + + # Expect that the analytics course ID was passed to the view + self.assertEqual(self.captured_request.POST.get("course_id"), "edX/DemoX/Fall") + def test_third_party_auth_login_failure(self): view = self._shimmed_view( HttpResponse(status=403), diff --git a/common/djangoapps/user_api/views.py b/common/djangoapps/user_api/views.py index 81b791f18f..1acf65c1aa 100644 --- a/common/djangoapps/user_api/views.py +++ b/common/djangoapps/user_api/views.py @@ -133,6 +133,13 @@ class LoginSessionView(APIView): def post(self, request): """Log in a user. + You must send all required form fields with the request. + + You can optionally send an `analytics` param with a JSON-encoded + object with additional info to include in the login analytics event. + Currently, the only supported field is "enroll_course_id" to indicate + that the user logged in while enrolling in a particular course. + Arguments: request (HttpRequest) @@ -148,7 +155,7 @@ class LoginSessionView(APIView): Example Usage: POST /user_api/v1/login_session - with POST params `email` and `password` + with POST params `email`, `password`, and `remember`. 200 OK @@ -246,6 +253,13 @@ class RegistrationView(APIView): def post(self, request): """Create the user's account. + You must send all required form fields with the request. + + You can optionally send an `analytics` param with a JSON-encoded + object with additional info to include in the registration analytics event. + Currently, the only supported field is "enroll_course_id" to indicate + that the user registered while enrolling in a particular course. + Arguments: request (HTTPRequest) diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js index 32fd8e1518..45004d07ac 100644 --- a/lms/static/js/spec/student_account/login_spec.js +++ b/lms/static/js/spec/student_account/login_spec.js @@ -6,208 +6,237 @@ define([ 'js/student_account/models/LoginModel', 'js/student_account/views/LoginView' ], function($, _, TemplateHelpers, AjaxHelpers, LoginModel, LoginView) { - describe('edx.student.account.LoginView', function() { - 'use strict'; + 'use strict'; + describe('edx.student.account.LoginView', function() { - var model = null, - view = null, - requests = null, - authComplete = false, - PLATFORM_NAME = 'edX', - USER_DATA = { - email: 'xsy@edx.org', - password: 'xsyisawesome', - remember: true - }, - THIRD_PARTY_AUTH = { - currentProvider: null, - providers: [ - { - name: 'Google', - iconClass: 'icon-google-plus', - loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', - registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' - }, - { - name: 'Facebook', - iconClass: 'icon-facebook', - loginUrl: '/auth/login/facebook/?auth_entry=account_login', - registerUrl: '/auth/login/facebook/?auth_entry=account_register' - } - ] - }, - FORM_DESCRIPTION = { - method: 'post', - submit_url: '/user_api/v1/account/login_session/', - fields: [ - { - name: 'email', - label: 'Email', - defaultValue: '', - type: 'email', - required: true, - placeholder: 'place@holder.org', - instructions: 'Enter your email.', - restrictions: {} - }, - { - name: 'password', - label: 'Password', - defaultValue: '', - type: 'password', - required: true, - instructions: 'Enter your password.', - restrictions: {} - }, - { - name: 'remember', - label: 'Remember me', - defaultValue: '', - type: 'checkbox', - required: true, - instructions: "Agree to the terms of service.", - restrictions: {} - } - ] - }; + var model = null, + view = null, + requests = null, + authComplete = false, + PLATFORM_NAME = 'edX', + USER_DATA = { + email: 'xsy@edx.org', + password: 'xsyisawesome', + remember: true + }, + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [ + { + name: 'Google', + iconClass: 'icon-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + name: 'Facebook', + iconClass: 'icon-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ] + }, + FORM_DESCRIPTION = { + method: 'post', + submit_url: '/user_api/v1/account/login_session/', + fields: [ + { + name: 'email', + label: 'Email', + defaultValue: '', + type: 'email', + required: true, + placeholder: 'place@holder.org', + instructions: 'Enter your email.', + restrictions: {} + }, + { + name: 'password', + label: 'Password', + defaultValue: '', + type: 'password', + required: true, + instructions: 'Enter your password.', + restrictions: {} + }, + { + name: 'remember', + label: 'Remember me', + defaultValue: '', + type: 'checkbox', + required: true, + instructions: "Agree to the terms of service.", + restrictions: {} + } + ] + }, + COURSE_ID = "edX/demoX/Fall"; - var createLoginView = function(test) { - // Initialize the login model - model = new LoginModel({}, { - url: FORM_DESCRIPTION.submit_url, - method: FORM_DESCRIPTION.method + var createLoginView = function(test) { + // Initialize the login model + model = new LoginModel({}, { + url: FORM_DESCRIPTION.submit_url, + method: FORM_DESCRIPTION.method + }); + + // Initialize the login view + view = new LoginView({ + fields: FORM_DESCRIPTION.fields, + model: model, + thirdPartyAuth: THIRD_PARTY_AUTH, + platformName: PLATFORM_NAME + }); + + // Spy on AJAX requests + requests = AjaxHelpers.requests(test); + + // Intercept events from the view + authComplete = false; + view.on("auth-complete", function() { + authComplete = true; + }); + }; + + var submitForm = function(validationSuccess) { + // Simulate manual entry of login form data + $('#login-email').val(USER_DATA.email); + $('#login-password').val(USER_DATA.password); + + // Check the "Remember me" checkbox + $('#login-remember').prop('checked', USER_DATA.remember); + + // Create a fake click event + var clickEvent = $.Event('click'); + + // If validationSuccess isn't passed, we avoid + // spying on `view.validate` twice + if ( !_.isUndefined(validationSuccess) ) { + // Force validation to return as expected + spyOn(view, 'validate').andReturn({ + isValid: validationSuccess, + message: 'Submission was validated.' }); + } - // Initialize the login view - view = new LoginView({ - fields: FORM_DESCRIPTION.fields, - model: model, - thirdPartyAuth: THIRD_PARTY_AUTH, - platformName: PLATFORM_NAME - }); + // Submit the email address + view.submitForm(clickEvent); + }; - // Spy on AJAX requests - requests = AjaxHelpers.requests(test); - - // Intercept events from the view - authComplete = false; - view.on("auth-complete", function() { - authComplete = true; - }); - }; - - var submitForm = function(validationSuccess) { - // Simulate manual entry of login form data - $('#login-email').val(USER_DATA.email); - $('#login-password').val(USER_DATA.password); - - // Check the "Remember me" checkbox - $('#login-remember').prop('checked', USER_DATA.remember); - - // Create a fake click event - var clickEvent = $.Event('click'); - - // If validationSuccess isn't passed, we avoid - // spying on `view.validate` twice - if ( !_.isUndefined(validationSuccess) ) { - // Force validation to return as expected - spyOn(view, 'validate').andReturn({ - isValid: validationSuccess, - message: 'Submission was validated.' - }); - } - - // Submit the email address - view.submitForm(clickEvent); - }; - - beforeEach(function() { - setFixtures('
'); - TemplateHelpers.installTemplate('templates/student_account/login'); - TemplateHelpers.installTemplate('templates/student_account/form_field'); - }); - - it('logs the user in', function() { - createLoginView(this); - - // Submit the form, with successful validation - submitForm(true); - - // Verify that the client contacts the server with the expected data - AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param( USER_DATA ) - ); - - // Respond with status code 200 - AjaxHelpers.respondWithJson(requests, {}); - - // Verify that auth-complete is triggered - expect(authComplete).toBe(true); - }); - - it('displays third-party auth login buttons', function() { - createLoginView(this); - - // Verify that Google and Facebook registration buttons are displayed - expect($('.button-Google')).toBeVisible(); - expect($('.button-Facebook')).toBeVisible(); - }); - - it('displays a link to the password reset form', function() { - createLoginView(this); - - // Verify that the password reset link is displayed - expect($('.forgot-password')).toBeVisible(); - }); - - it('validates login form fields', function() { - createLoginView(this); - - submitForm(true); - - // Verify that validation of form fields occurred - expect(view.validate).toHaveBeenCalledWith($('#login-email')[0]); - expect(view.validate).toHaveBeenCalledWith($('#login-password')[0]); - }); - - it('displays login form validation errors', function() { - createLoginView(this); - - // Submit the form, with failed validation - submitForm(false); - - // 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() { - createLoginView(this); - - // Submit the form, with successful validation - submitForm(true); - - // Simulate an error from the LMS servers - AjaxHelpers.respondWithError(requests); - - // 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(); - - // This time, respond with status code 200 - AjaxHelpers.respondWithJson(requests, {}); - - // Expect that the error is hidden and auth complete is triggered - expect(view.$errors).toHaveClass('hidden'); - expect(authComplete).toBe(true); - }); + beforeEach(function() { + setFixtures('
'); + TemplateHelpers.installTemplate('templates/student_account/login'); + TemplateHelpers.installTemplate('templates/student_account/form_field'); }); - } -); + + it('logs the user in', function() { + createLoginView(this); + + // Submit the form, with successful validation + submitForm(true); + + // Verify that the client contacts the server with the expected data + AjaxHelpers.expectRequest( + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( USER_DATA ) + ); + + // Respond with status code 200 + AjaxHelpers.respondWithJson(requests, {}); + + // Verify that auth-complete is triggered + expect(authComplete).toBe(true); + }); + + it('sends analytics info containing the enrolled course ID', function() { + createLoginView( this ); + + // Simulate that the user is attempting to enroll in a course + // by setting the course_id query string param. + spyOn($, 'url').andCallFake(function( param ) { + if (param === "?course_id") { + return encodeURIComponent( COURSE_ID ); + } + }); + + // Attempt to login + submitForm( true ); + + // Verify that the client sent the course ID for analytics + var expectedData = {}; + $.extend(expectedData, USER_DATA, { + analytics: JSON.stringify({ + enroll_course_id: COURSE_ID + }) + }); + + AjaxHelpers.expectRequest( + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( expectedData ) + ); + }); + + it('displays third-party auth login buttons', function() { + createLoginView(this); + + // Verify that Google and Facebook registration buttons are displayed + expect($('.button-Google')).toBeVisible(); + expect($('.button-Facebook')).toBeVisible(); + }); + + it('displays a link to the password reset form', function() { + createLoginView(this); + + // Verify that the password reset link is displayed + expect($('.forgot-password')).toBeVisible(); + }); + + it('validates login form fields', function() { + createLoginView(this); + + submitForm(true); + + // Verify that validation of form fields occurred + expect(view.validate).toHaveBeenCalledWith($('#login-email')[0]); + expect(view.validate).toHaveBeenCalledWith($('#login-password')[0]); + }); + + it('displays login form validation errors', function() { + createLoginView(this); + + // Submit the form, with failed validation + submitForm(false); + + // 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() { + createLoginView(this); + + // Submit the form, with successful validation + submitForm(true); + + // Simulate an error from the LMS servers + AjaxHelpers.respondWithError(requests); + + // 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(); + + // This time, respond with status code 200 + AjaxHelpers.respondWithJson(requests, {}); + + // 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/register_spec.js b/lms/static/js/spec/student_account/register_spec.js index 2c0d849e83..07fada7427 100644 --- a/lms/static/js/spec/student_account/register_spec.js +++ b/lms/static/js/spec/student_account/register_spec.js @@ -6,302 +6,332 @@ define([ 'js/student_account/models/RegisterModel', 'js/student_account/views/RegisterView' ], function($, _, TemplateHelpers, AjaxHelpers, RegisterModel, RegisterView) { - describe('edx.student.account.RegisterView', function() { - 'use strict'; + 'use strict'; - var model = null, - view = null, - requests = null, - authComplete = false, - PLATFORM_NAME = 'edX', - USER_DATA = { - email: 'xsy@edx.org', - name: 'Xsy M. Education', - username: 'Xsy', - password: 'xsyisawesome', - level_of_education: 'p', - gender: 'm', - year_of_birth: 2014, - mailing_address: '141 Portland', - goals: 'To boldly learn what no letter of the alphabet has learned before', - honor_code: true - }, - THIRD_PARTY_AUTH = { - currentProvider: null, - providers: [ - { - name: 'Google', - iconClass: 'icon-google-plus', - loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', - registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' - }, - { - name: 'Facebook', - iconClass: 'icon-facebook', - loginUrl: '/auth/login/facebook/?auth_entry=account_login', - registerUrl: '/auth/login/facebook/?auth_entry=account_register' - } - ] - }, - FORM_DESCRIPTION = { - method: 'post', - submit_url: '/user_api/v1/account/registration/', - fields: [ - { - name: 'email', - label: 'Email', - defaultValue: '', - type: 'email', - required: true, - placeholder: 'place@holder.org', - instructions: 'Enter your email.', - restrictions: {} - }, - { - name: 'name', - label: 'Full Name', - defaultValue: '', - type: 'text', - required: true, - instructions: 'Enter your username.', - restrictions: {} - }, - { - name: 'username', - label: 'Username', - defaultValue: '', - type: 'text', - required: true, - instructions: 'Enter your username.', - restrictions: {} - }, - { - name: 'password', - label: 'Password', - defaultValue: '', - type: 'password', - required: true, - instructions: 'Enter your password.', - restrictions: {} - }, - { - name: 'level_of_education', - label: 'Highest Level of Education Completed', - defaultValue: '', - type: 'select', - options: [ - {value: "", name: "--"}, - {value: "p", name: "Doctorate"}, - {value: "m", name: "Master's or professional degree"}, - {value: "b", name: "Bachelor's degree"}, - ], - required: false, - instructions: 'Select your education level.', - restrictions: {} - }, - { - name: 'gender', - label: 'Gender', - defaultValue: '', - type: 'select', - options: [ - {value: "", name: "--"}, - {value: "m", name: "Male"}, - {value: "f", name: "Female"}, - {value: "o", name: "Other"}, - ], - required: false, - instructions: 'Select your gender.', - restrictions: {} - }, - { - name: 'year_of_birth', - label: 'Year of Birth', - defaultValue: '', - type: 'select', - options: [ - {value: "", name: "--"}, - {value: 1900, name: "1900"}, - {value: 1950, name: "1950"}, - {value: 2014, name: "2014"}, - ], - required: false, - instructions: 'Select your year of birth.', - restrictions: {} - }, - { - name: 'mailing_address', - label: 'Mailing Address', - defaultValue: '', - type: 'textarea', - required: false, - instructions: 'Enter your mailing address.', - restrictions: {} - }, - { - name: 'goals', - label: 'Goals', - defaultValue: '', - type: 'textarea', - required: false, - instructions: "If you'd like, tell us why you're interested in edX.", - restrictions: {} - }, - { - name: 'honor_code', - label: 'I agree to the Terms of Service and Honor Code', - defaultValue: '', - type: 'checkbox', - required: true, - instructions: '', - restrictions: {} - } - ] - }; + describe('edx.student.account.RegisterView', function() { - var createRegisterView = function(that) { - // Initialize the register model - model = new RegisterModel({}, { - url: FORM_DESCRIPTION.submit_url, - method: FORM_DESCRIPTION.method - }); - - // Initialize the register view - view = new RegisterView({ - fields: FORM_DESCRIPTION.fields, - model: model, - thirdPartyAuth: THIRD_PARTY_AUTH, - platformName: PLATFORM_NAME - }); - - // Spy on AJAX requests - requests = AjaxHelpers.requests(that); - - // Intercept events from the view - authComplete = false; - view.on("auth-complete", function() { - authComplete = true; - }); + var model = null, + view = null, + requests = null, + authComplete = false, + PLATFORM_NAME = 'edX', + COURSE_ID = "edX/DemoX/Fall", + USER_DATA = { + email: 'xsy@edx.org', + name: 'Xsy M. Education', + username: 'Xsy', + password: 'xsyisawesome', + level_of_education: 'p', + gender: 'm', + year_of_birth: 2014, + mailing_address: '141 Portland', + goals: 'To boldly learn what no letter of the alphabet has learned before', + honor_code: true + }, + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [ + { + name: 'Google', + iconClass: 'icon-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + name: 'Facebook', + iconClass: 'icon-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ] + }, + FORM_DESCRIPTION = { + method: 'post', + submit_url: '/user_api/v1/account/registration/', + fields: [ + { + name: 'email', + label: 'Email', + defaultValue: '', + type: 'email', + required: true, + placeholder: 'place@holder.org', + instructions: 'Enter your email.', + restrictions: {} + }, + { + name: 'name', + label: 'Full Name', + defaultValue: '', + type: 'text', + required: true, + instructions: 'Enter your username.', + restrictions: {} + }, + { + name: 'username', + label: 'Username', + defaultValue: '', + type: 'text', + required: true, + instructions: 'Enter your username.', + restrictions: {} + }, + { + name: 'password', + label: 'Password', + defaultValue: '', + type: 'password', + required: true, + instructions: 'Enter your password.', + restrictions: {} + }, + { + name: 'level_of_education', + label: 'Highest Level of Education Completed', + defaultValue: '', + type: 'select', + options: [ + {value: "", name: "--"}, + {value: "p", name: "Doctorate"}, + {value: "m", name: "Master's or professional degree"}, + {value: "b", name: "Bachelor's degree"}, + ], + required: false, + instructions: 'Select your education level.', + restrictions: {} + }, + { + name: 'gender', + label: 'Gender', + defaultValue: '', + type: 'select', + options: [ + {value: "", name: "--"}, + {value: "m", name: "Male"}, + {value: "f", name: "Female"}, + {value: "o", name: "Other"}, + ], + required: false, + instructions: 'Select your gender.', + restrictions: {} + }, + { + name: 'year_of_birth', + label: 'Year of Birth', + defaultValue: '', + type: 'select', + options: [ + {value: "", name: "--"}, + {value: 1900, name: "1900"}, + {value: 1950, name: "1950"}, + {value: 2014, name: "2014"}, + ], + required: false, + instructions: 'Select your year of birth.', + restrictions: {} + }, + { + name: 'mailing_address', + label: 'Mailing Address', + defaultValue: '', + type: 'textarea', + required: false, + instructions: 'Enter your mailing address.', + restrictions: {} + }, + { + name: 'goals', + label: 'Goals', + defaultValue: '', + type: 'textarea', + required: false, + instructions: "If you'd like, tell us why you're interested in edX.", + restrictions: {} + }, + { + name: 'honor_code', + label: 'I agree to the Terms of Service and Honor Code', + defaultValue: '', + type: 'checkbox', + required: true, + instructions: '', + restrictions: {} + } + ] }; - var submitForm = function(validationSuccess) { - // Simulate manual entry of registration form data - $('#register-email').val(USER_DATA.email); - $('#register-name').val(USER_DATA.name); - $('#register-username').val(USER_DATA.username); - $('#register-password').val(USER_DATA.password); - $('#register-level_of_education').val(USER_DATA.level_of_education); - $('#register-gender').val(USER_DATA.gender); - $('#register-year_of_birth').val(USER_DATA.year_of_birth); - $('#register-mailing_address').val(USER_DATA.mailing_address); - $('#register-goals').val(USER_DATA.goals); - - // Check the honor code checkbox - $('#register-honor_code').prop('checked', USER_DATA.honor_code); - - // Create a fake click event - var clickEvent = $.Event('click'); - - // If validationSuccess isn't passed, we avoid - // spying on `view.validate` twice - if ( !_.isUndefined(validationSuccess) ) { - // Force validation to return as expected - spyOn(view, 'validate').andReturn({ - isValid: validationSuccess, - message: 'Submission was validated.' - }); - } - - // Submit the email address - view.submitForm(clickEvent); - }; - - beforeEach(function() { - setFixtures('
'); - TemplateHelpers.installTemplate('templates/student_account/register'); - TemplateHelpers.installTemplate('templates/student_account/form_field'); + var createRegisterView = function(that) { + // Initialize the register model + model = new RegisterModel({}, { + url: FORM_DESCRIPTION.submit_url, + method: FORM_DESCRIPTION.method }); - it('registers a new user', function() { - createRegisterView(this); - - // Submit the form, with successful validation - submitForm(true); - - // Verify that the client contacts the server with the expected data - AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param( USER_DATA ) - ); - - // Respond with status code 200 - AjaxHelpers.respondWithJson(requests, {}); - - // Verify that auth complete is triggered - expect(authComplete).toBe(true); + // Initialize the register view + view = new RegisterView({ + fields: FORM_DESCRIPTION.fields, + model: model, + thirdPartyAuth: THIRD_PARTY_AUTH, + platformName: PLATFORM_NAME }); - it('displays third-party auth registration buttons', function() { - createRegisterView(this); + // Spy on AJAX requests + requests = AjaxHelpers.requests(that); - // Verify that Google and Facebook registration buttons are displayed - expect($('.button-Google')).toBeVisible(); - expect($('.button-Facebook')).toBeVisible(); + // Intercept events from the view + authComplete = false; + view.on("auth-complete", function() { + authComplete = true; }); + }; - it('validates registration form fields', function() { - createRegisterView(this); + var submitForm = function(validationSuccess) { + // Simulate manual entry of registration form data + $('#register-email').val(USER_DATA.email); + $('#register-name').val(USER_DATA.name); + $('#register-username').val(USER_DATA.username); + $('#register-password').val(USER_DATA.password); + $('#register-level_of_education').val(USER_DATA.level_of_education); + $('#register-gender').val(USER_DATA.gender); + $('#register-year_of_birth').val(USER_DATA.year_of_birth); + $('#register-mailing_address').val(USER_DATA.mailing_address); + $('#register-goals').val(USER_DATA.goals); - // Submit the form, with successful validation - submitForm(true); + // Check the honor code checkbox + $('#register-honor_code').prop('checked', USER_DATA.honor_code); - // Verify that validation of form fields occurred - expect(view.validate).toHaveBeenCalledWith($('#register-email')[0]); - expect(view.validate).toHaveBeenCalledWith($('#register-name')[0]); - expect(view.validate).toHaveBeenCalledWith($('#register-username')[0]); - expect(view.validate).toHaveBeenCalledWith($('#register-password')[0]); + // Create a fake click event + var clickEvent = $.Event('click'); - // Verify that no submission errors are visible - expect(view.$errors).toHaveClass('hidden'); - }); + // If validationSuccess isn't passed, we avoid + // spying on `view.validate` twice + if ( !_.isUndefined(validationSuccess) ) { + // Force validation to return as expected + spyOn(view, 'validate').andReturn({ + isValid: validationSuccess, + message: 'Submission was validated.' + }); + } - it('displays registration form validation errors', function() { - createRegisterView(this); + // Submit the email address + view.submitForm(clickEvent); + }; - // Submit the form, with failed validation - submitForm(false); - - // 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() { - createRegisterView(this); - - // Submit the form, with successful validation - submitForm(true); - - // Simulate an error from the LMS servers - AjaxHelpers.respondWithError(requests); - - // 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(); - - // This time, respond with status code 200 - AjaxHelpers.respondWithJson(requests, {}); - - // Expect that the error is hidden and that auth complete is triggered - expect(view.$errors).toHaveClass('hidden'); - expect(authComplete).toBe(true); - }); + beforeEach(function() { + setFixtures('
'); + TemplateHelpers.installTemplate('templates/student_account/register'); + TemplateHelpers.installTemplate('templates/student_account/form_field'); }); - } -); + + it('registers a new user', function() { + createRegisterView(this); + + // Submit the form, with successful validation + submitForm( true ); + + // Verify that the client contacts the server with the expected data + AjaxHelpers.expectRequest( + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( USER_DATA ) + ); + + // Respond with status code 200 + AjaxHelpers.respondWithJson(requests, {}); + + // Verify that auth complete is triggered + expect(authComplete).toBe(true); + }); + + it('sends analytics info containing the enrolled course ID', function() { + createRegisterView( this ); + + // Simulate that the user is attempting to enroll in a course + // by setting the course_id query string param. + spyOn($, 'url').andCallFake(function( param ) { + if (param === "?course_id") { + return encodeURIComponent( COURSE_ID ); + } + }); + + // Attempt to register + submitForm( true ); + + // Verify that the client sent the course ID for analytics + var expectedData = {}; + $.extend(expectedData, USER_DATA, { + analytics: JSON.stringify({ + enroll_course_id: COURSE_ID + }) + }); + + AjaxHelpers.expectRequest( + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( expectedData ) + ); + }); + + it('displays third-party auth registration buttons', function() { + createRegisterView(this); + + // Verify that Google and Facebook registration buttons are displayed + expect($('.button-Google')).toBeVisible(); + expect($('.button-Facebook')).toBeVisible(); + }); + + it('validates registration form fields', function() { + createRegisterView(this); + + // Submit the form, with successful validation + submitForm(true); + + // Verify that validation of form fields occurred + expect(view.validate).toHaveBeenCalledWith($('#register-email')[0]); + expect(view.validate).toHaveBeenCalledWith($('#register-name')[0]); + expect(view.validate).toHaveBeenCalledWith($('#register-username')[0]); + expect(view.validate).toHaveBeenCalledWith($('#register-password')[0]); + + // Verify that no submission errors are visible + expect(view.$errors).toHaveClass('hidden'); + }); + + it('displays registration form validation errors', function() { + createRegisterView(this); + + // Submit the form, with failed validation + submitForm(false); + + // 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() { + createRegisterView(this); + + // Submit the form, with successful validation + submitForm(true); + + // Simulate an error from the LMS servers + AjaxHelpers.respondWithError(requests); + + // 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(); + + // This time, respond with status code 200 + AjaxHelpers.respondWithJson(requests, {}); + + // 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/student_account/models/LoginModel.js b/lms/static/js/student_account/models/LoginModel.js index 0b0124f92a..5e994f2f61 100644 --- a/lms/static/js/student_account/models/LoginModel.js +++ b/lms/static/js/student_account/models/LoginModel.js @@ -24,14 +24,27 @@ var edx = edx || {}; }, sync: function(method, model) { - var headers = { - 'X-CSRFToken': $.cookie('csrftoken') - }; + var headers = { 'X-CSRFToken': $.cookie('csrftoken') }, + data = {}, + analytics, + courseId = $.url( '?course_id' ); + + // If there is a course ID in the query string param, + // send that to the server as well so it can be included + // in analytics events. + if ( courseId ) { + analytics = JSON.stringify({ + enroll_course_id: decodeURIComponent( courseId ) + }); + } + + // Include all form fields and analytics info in the data sent to the server + $.extend( data, model.attributes, { analytics: analytics }); $.ajax({ url: model.urlRoot, type: model.ajaxType, - data: model.attributes, + data: data, headers: headers, success: function() { model.trigger('sync'); diff --git a/lms/static/js/student_account/models/RegisterModel.js b/lms/static/js/student_account/models/RegisterModel.js index 7e07e858aa..ca8b953c5b 100644 --- a/lms/static/js/student_account/models/RegisterModel.js +++ b/lms/static/js/student_account/models/RegisterModel.js @@ -30,14 +30,27 @@ var edx = edx || {}; }, sync: function(method, model) { - var headers = { - 'X-CSRFToken': $.cookie('csrftoken') - }; + var headers = { 'X-CSRFToken': $.cookie('csrftoken') }, + data = {}, + analytics, + courseId = $.url( '?course_id' ); + + // If there is a course ID in the query string param, + // send that to the server as well so it can be included + // in analytics events. + if ( courseId ) { + analytics = JSON.stringify({ + enroll_course_id: decodeURIComponent( courseId ) + }); + } + + // Include all form fields and analytics info in the data sent to the server + $.extend( data, model.attributes, { analytics: analytics }); $.ajax({ url: model.urlRoot, type: model.ajaxType, - data: model.attributes, + data: data, headers: headers, success: function() { model.trigger('sync');