From d2a4790855847327103c80e78be837bc8e5440db Mon Sep 17 00:00:00 2001 From: zubair-arbi Date: Mon, 7 Sep 2015 13:49:02 +0500 Subject: [PATCH] use the standard syntax to load JavaScript dependencies on logistration js files ECOM-2044 --- .../third_party_auth/tests/specs/test_lti.py | 8 +- .../tests/specs/test_testshib.py | 27 +- .../static/js/spec/edx.utils.validate_spec.js | 369 +++++----- common/static/js/utils/edx.utils.validate.js | 337 ++++----- .../student_account/test/test_views.py | 27 +- lms/djangoapps/student_account/views.py | 31 +- lms/envs/common.py | 24 - lms/static/js/spec/main.js | 101 +-- .../js/spec/student_account/access_spec.js | 71 +- .../spec/student_account/finish_auth_spec.js | 29 +- .../spec/student_account/hinted_login_spec.js | 149 ++-- .../student_account/institution_login_spec.js | 151 ++-- .../js/spec/student_account/login_spec.js | 457 +++++++------ .../logistration_factory_spec.js | 130 ++++ .../student_account/password_reset_spec.js | 32 +- .../js/spec/student_account/register_spec.js | 644 +++++++++--------- lms/static/js/student_account/accessApp.js | 21 - .../student_account/logistration_factory.js | 15 + .../js/student_account/models/LoginModel.js | 93 ++- .../models/PasswordResetModel.js | 71 +- .../student_account/models/RegisterModel.js | 103 ++- .../js/student_account/views/AccessView.js | 504 +++++++------- .../js/student_account/views/FormView.js | 455 +++++++------ .../student_account/views/HintedLoginView.js | 72 +- .../views/InstitutionLoginView.js | 46 +- .../js/student_account/views/LoginView.js | 222 +++--- .../views/PasswordResetView.js | 72 +- .../js/student_account/views/RegisterView.js | 172 ++--- lms/static/js_test.yml | 1 + lms/static/lms/js/build.js | 1 + .../student_account/login_and_register.html | 25 +- 31 files changed, 2263 insertions(+), 2197 deletions(-) create mode 100644 lms/static/js/spec/student_account/logistration_factory_spec.js delete mode 100644 lms/static/js/student_account/accessApp.js create mode 100644 lms/static/js/student_account/logistration_factory.js diff --git a/common/djangoapps/third_party_auth/tests/specs/test_lti.py b/common/djangoapps/third_party_auth/tests/specs/test_lti.py index fa9e2398e4..d6622def7e 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_lti.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_lti.py @@ -69,8 +69,8 @@ class IntegrationTestLTI(testutil.TestCase): self.assertTrue(login_response['Location'].endswith(reverse('signin_user'))) register_response = self.client.get(login_response['Location']) self.assertEqual(register_response.status_code, 200) - self.assertIn('currentProvider": "LTI Test Tool Consumer"', register_response.content) - self.assertIn('"errorMessage": null', register_response.content) + self.assertIn('"currentProvider": "LTI Test Tool Consumer"', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) # Now complete the form: ajax_register_response = self.client.post( @@ -153,7 +153,7 @@ class IntegrationTestLTI(testutil.TestCase): register_response = self.client.get(login_response['Location']) self.assertEqual(register_response.status_code, 200) self.assertIn( - 'currentProvider": "Tool Consumer with Secret in Settings"', + '"currentProvider": "Tool Consumer with Secret in Settings"', register_response.content ) - self.assertIn('"errorMessage": null', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index dc4ca99880..b833efab27 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -1,13 +1,20 @@ """ Third_party_auth integration tests using a mock version of the TestShib provider """ -from django.core.urlresolvers import reverse + +import json +import unittest import httpretty from mock import patch + +from django.core.urlresolvers import reverse + +from openedx.core.lib.json_utils import EscapedEdxJSONEncoder + from student.tests.factories import UserFactory from third_party_auth.tasks import fetch_saml_metadata from third_party_auth.tests import testutil -import unittest + TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth' TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml' @@ -81,11 +88,11 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): # We'd now like to see if the "You've successfully signed into TestShib" message is # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', register_response.content) - self.assertIn('"errorMessage": null', register_response.content) + self.assertIn('"currentProvider": "TestShib"', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) # Now do a crude check that the data (e.g. email) from the provider is displayed in the form: - self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) - self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) + self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) + self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) # Now complete the form: ajax_register_response = self.client.post( reverse('user_api_registration'), @@ -128,8 +135,8 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): # We'd now like to see if the "You've successfully signed into TestShib" message is # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', login_response.content) - self.assertIn('"errorMessage": null', login_response.content) + self.assertIn('"currentProvider": "TestShib"', login_response.content) + self.assertIn('"errorMessage": null', login_response.content) # Now the user enters their username and password. # The AJAX on the page will log them in: ajax_login_response = self.client.post( @@ -183,7 +190,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): response = self.client.get(self.login_page_url) self.assertEqual(response.status_code, 200) self.assertIn("TestShib", response.content) - self.assertIn(TPA_TESTSHIB_LOGIN_URL.replace('&', '&'), response.content) + self.assertIn(json.dumps(TPA_TESTSHIB_LOGIN_URL, cls=EscapedEdxJSONEncoder), response.content) return response def _check_register_page(self): @@ -191,7 +198,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): response = self.client.get(self.register_page_url) self.assertEqual(response.status_code, 200) self.assertIn("TestShib", response.content) - self.assertIn(TPA_TESTSHIB_REGISTER_URL.replace('&', '&'), response.content) + self.assertIn(json.dumps(TPA_TESTSHIB_REGISTER_URL, cls=EscapedEdxJSONEncoder), response.content) return response def _configure_testshib_provider(self, **kwargs): diff --git a/common/static/js/spec/edx.utils.validate_spec.js b/common/static/js/spec/edx.utils.validate_spec.js index d6664c4647..b85b99d733 100644 --- a/common/static/js/spec/edx.utils.validate_spec.js +++ b/common/static/js/spec/edx.utils.validate_spec.js @@ -1,192 +1,195 @@ -describe('edx.utils.validate', function () { +;(function (define) { 'use strict'; + define(['jquery', 'js/utils/edx.utils.validate'], + function($) { - var fixture = null, - field = null, - result = null, - MIN_LENGTH = 2, - MAX_LENGTH = 20, - VALID_STRING = 'xsy_is_awesome', - SHORT_STRING = 'x', - LONG_STRING = 'xsy_is_way_too_awesome', - EMAIL_ERROR_FRAGMENT = 'formatted', - MIN_ERROR_FRAGMENT = 'least', - MAX_ERROR_FRAGMENT = 'up to', - REQUIRED_ERROR_FRAGMENT = 'Please enter your', - CUSTOM_MESSAGE = 'custom message'; + var fixture = null, + field = null, + result = null, + MIN_LENGTH = 2, + MAX_LENGTH = 20, + VALID_STRING = 'xsy_is_awesome', + SHORT_STRING = 'x', + LONG_STRING = 'xsy_is_way_too_awesome', + EMAIL_ERROR_FRAGMENT = 'formatted', + MIN_ERROR_FRAGMENT = 'least', + MAX_ERROR_FRAGMENT = 'up to', + REQUIRED_ERROR_FRAGMENT = 'Please enter your', + CUSTOM_MESSAGE = 'custom message'; - var createFixture = function( type, name, required, minlength, maxlength, value ) { - setFixtures(''); + var createFixture = function( type, name, required, minlength, maxlength, value ) { + setFixtures(''); - field = $('#field'); - field.prop('required', required); - field.attr({ - name: name, - minlength: minlength, - maxlength: maxlength, - value: value + field = $('#field'); + field.prop('required', required); + field.attr({ + name: name, + minlength: minlength, + maxlength: maxlength, + value: value + }); + }; + + var expectValid = function() { + result = edx.utils.validate(field); + expect(result.isValid).toBe(true); + }; + + var expectInvalid = function( errorFragment ) { + result = edx.utils.validate(field); + expect(result.isValid).toBe(false); + expect(result.message).toMatch(errorFragment); + }; + + it('succeeds if an optional field is left blank', function () { + createFixture('text', 'username', false, MIN_LENGTH, MAX_LENGTH, ''); + expectValid(); }); - }; - var expectValid = function() { - result = edx.utils.validate(field); - expect(result.isValid).toBe(true); - }; + it('succeeds if a required field is provided a valid value', function () { + createFixture('text', 'username', true, MIN_LENGTH, MAX_LENGTH, VALID_STRING); + expectValid(); + }); - var expectInvalid = function( errorFragment ) { - result = edx.utils.validate(field); - expect(result.isValid).toBe(false); - expect(result.message).toMatch(errorFragment); - }; + it('fails if a required field is left blank', function () { + createFixture('text', 'username', true, MIN_LENGTH, MAX_LENGTH, ''); + expectInvalid(REQUIRED_ERROR_FRAGMENT); + }); - it('succeeds if an optional field is left blank', function () { - createFixture('text', 'username', false, MIN_LENGTH, MAX_LENGTH, ''); - expectValid(); + it('fails if a field is provided a value below its minimum character limit', function () { + createFixture('text', 'username', false, MIN_LENGTH, MAX_LENGTH, SHORT_STRING); + + // Verify optional field behavior + expectInvalid(MIN_ERROR_FRAGMENT); + + // Verify required field behavior + field.prop('required', true); + expectInvalid(MIN_ERROR_FRAGMENT); + }); + + it('succeeds if a field with no minimum character limit is provided a value below its maximum character limit', function () { + createFixture('text', 'username', false, null, MAX_LENGTH, SHORT_STRING); + + // Verify optional field behavior + expectValid(); + + // Verify required field behavior + field.prop('required', true); + expectValid(); + }); + + it('fails if a required field with no minimum character limit is left blank', function () { + createFixture('text', 'username', true, null, MAX_LENGTH, ''); + expectInvalid(REQUIRED_ERROR_FRAGMENT); + }); + + it('fails if a field is provided a value above its maximum character limit', function () { + createFixture('text', 'username', false, MIN_LENGTH, MAX_LENGTH, LONG_STRING); + + // Verify optional field behavior + expectInvalid(MAX_ERROR_FRAGMENT); + + // Verify required field behavior + field.prop('required', true); + expectInvalid(MAX_ERROR_FRAGMENT); + }); + + it('succeeds if a field with no maximum character limit is provided a value above its minimum character limit', function () { + createFixture('text', 'username', false, MIN_LENGTH, null, LONG_STRING); + + // Verify optional field behavior + expectValid(); + + // Verify required field behavior + field.prop('required', true); + expectValid(); + }); + + it('succeeds if a field with no character limits is provided a value', function () { + createFixture('text', 'username', false, null, null, VALID_STRING); + + // Verify optional field behavior + expectValid(); + + // Verify required field behavior + field.prop('required', true); + expectValid(); + }); + + it('fails if an email field is provided an invalid address', function () { + createFixture('email', 'email', false, MIN_LENGTH, MAX_LENGTH, 'localpart'); + + // Verify optional field behavior + expectInvalid(EMAIL_ERROR_FRAGMENT); + + // Verify required field behavior + field.prop('required', false); + expectInvalid(EMAIL_ERROR_FRAGMENT); + }); + + it('succeeds if an email field is provided a valid address', function () { + createFixture('email', 'email', false, MIN_LENGTH, MAX_LENGTH, 'localpart@label.tld'); + + // Verify optional field behavior + expectValid(); + + // Verify required field behavior + field.prop('required', true); + expectValid(); + }); + + it('succeeds if a checkbox is optional, or required and checked, but fails if a required checkbox is unchecked', function () { + createFixture('checkbox', 'checkbox', false, null, null, 'value'); + + // Optional, unchecked + expectValid(); + + // Optional, checked + field.prop('checked', true); + expectValid(); + + // Required, checked + field.prop('required', true); + expectValid(); + + // Required, unchecked + field.prop('checked', false); + expectInvalid(REQUIRED_ERROR_FRAGMENT); + }); + + it('succeeds if a select is optional, or required and default is selected, but fails if a required select has the default option selected', function () { + var select = [ + '' + ].join(''); + + setFixtures(select); + + field = $('#dropdown'); + + // Optional + expectValid(); + + // Required, default text selected + field.attr('required', true); + expectInvalid(REQUIRED_ERROR_FRAGMENT); + + // Required, country selected + field.val('BE'); + expectValid(); + }); + + it('returns a custom error message if an invalid field has one attached', function () { + // Create a blank required field + createFixture('text', 'username', true, MIN_LENGTH, MAX_LENGTH, ''); + + // Attach a custom error message to the field + field.data('errormsg-required', CUSTOM_MESSAGE); + + expectInvalid(CUSTOM_MESSAGE); + }); }); - - it('succeeds if a required field is provided a valid value', function () { - createFixture('text', 'username', true, MIN_LENGTH, MAX_LENGTH, VALID_STRING); - expectValid(); - }); - - it('fails if a required field is left blank', function () { - createFixture('text', 'username', true, MIN_LENGTH, MAX_LENGTH, ''); - expectInvalid(REQUIRED_ERROR_FRAGMENT); - }); - - it('fails if a field is provided a value below its minimum character limit', function () { - createFixture('text', 'username', false, MIN_LENGTH, MAX_LENGTH, SHORT_STRING); - - // Verify optional field behavior - expectInvalid(MIN_ERROR_FRAGMENT); - - // Verify required field behavior - field.prop('required', true); - expectInvalid(MIN_ERROR_FRAGMENT); - }); - - it('succeeds if a field with no minimum character limit is provided a value below its maximum character limit', function () { - createFixture('text', 'username', false, null, MAX_LENGTH, SHORT_STRING); - - // Verify optional field behavior - expectValid(); - - // Verify required field behavior - field.prop('required', true); - expectValid(); - }); - - it('fails if a required field with no minimum character limit is left blank', function () { - createFixture('text', 'username', true, null, MAX_LENGTH, ''); - expectInvalid(REQUIRED_ERROR_FRAGMENT); - }); - - it('fails if a field is provided a value above its maximum character limit', function () { - createFixture('text', 'username', false, MIN_LENGTH, MAX_LENGTH, LONG_STRING); - - // Verify optional field behavior - expectInvalid(MAX_ERROR_FRAGMENT); - - // Verify required field behavior - field.prop('required', true); - expectInvalid(MAX_ERROR_FRAGMENT); - }); - - it('succeeds if a field with no maximum character limit is provided a value above its minimum character limit', function () { - createFixture('text', 'username', false, MIN_LENGTH, null, LONG_STRING); - - // Verify optional field behavior - expectValid(); - - // Verify required field behavior - field.prop('required', true); - expectValid(); - }); - - it('succeeds if a field with no character limits is provided a value', function () { - createFixture('text', 'username', false, null, null, VALID_STRING); - - // Verify optional field behavior - expectValid(); - - // Verify required field behavior - field.prop('required', true); - expectValid(); - }); - - it('fails if an email field is provided an invalid address', function () { - createFixture('email', 'email', false, MIN_LENGTH, MAX_LENGTH, 'localpart'); - - // Verify optional field behavior - expectInvalid(EMAIL_ERROR_FRAGMENT); - - // Verify required field behavior - field.prop('required', false); - expectInvalid(EMAIL_ERROR_FRAGMENT); - }); - - it('succeeds if an email field is provided a valid address', function () { - createFixture('email', 'email', false, MIN_LENGTH, MAX_LENGTH, 'localpart@label.tld'); - - // Verify optional field behavior - expectValid(); - - // Verify required field behavior - field.prop('required', true); - expectValid(); - }); - - it('succeeds if a checkbox is optional, or required and checked, but fails if a required checkbox is unchecked', function () { - createFixture('checkbox', 'checkbox', false, null, null, 'value'); - - // Optional, unchecked - expectValid(); - - // Optional, checked - field.prop('checked', true); - expectValid(); - - // Required, checked - field.prop('required', true); - expectValid(); - - // Required, unchecked - field.prop('checked', false); - expectInvalid(REQUIRED_ERROR_FRAGMENT); - }); - - it('succeeds if a select is optional, or required and default is selected, but fails if a required select has the default option selected', function () { - var select = [ - '' - ].join(''); - - setFixtures(select); - - field = $('#dropdown'); - - // Optional - expectValid(); - - // Required, default text selected - field.attr('required', true); - expectInvalid(REQUIRED_ERROR_FRAGMENT); - - // Required, country selected - field.val('BE'); - expectValid(); - }); - - it('returns a custom error message if an invalid field has one attached', function () { - // Create a blank required field - createFixture('text', 'username', true, MIN_LENGTH, MAX_LENGTH, ''); - - // Attach a custom error message to the field - field.data('errormsg-required', CUSTOM_MESSAGE); - - expectInvalid(CUSTOM_MESSAGE); - }); -}); +}).call(this, define || RequireJS.define); diff --git a/common/static/js/utils/edx.utils.validate.js b/common/static/js/utils/edx.utils.validate.js index 392c99bc46..8fff89cd81 100644 --- a/common/static/js/utils/edx.utils.validate.js +++ b/common/static/js/utils/edx.utils.validate.js @@ -1,186 +1,193 @@ -var edx = edx || {}; - -(function( $, _, _s, gettext ) { +;(function (define) { 'use strict'; + define([ + 'jquery', + 'underscore', + 'underscore.string', + 'gettext' + ], + function($, _, _s, gettext) { + var utils; - /* Mix non-conflicting functions from underscore.string - * (all but include, contains, and reverse) into the - * Underscore namespace. In practice, this mixin is done - * by the access view, but doing it here helps keep the - * utility self-contained. - */ - _.mixin( _.str.exports() ); + /* Mix non-conflicting functions from underscore.string + * (all but include, contains, and reverse) into the + * Underscore namespace. In practice, this mixin is done + * by the access view, but doing it here helps keep the + * utility self-contained. + */ + if (_.isUndefined(_s)) { + _s = _.str; + } + _.mixin( _s.exports() ); - edx.utils = edx.utils || {}; + utils = (function(){ + var _fn = { + validate: { - var utils = (function(){ - var _fn = { - validate: { + msg: { + email: '
  • <%- gettext("The email address you\'ve provided isn\'t formatted correctly.") %>
  • ', + min: '
  • <%- _.sprintf( gettext("%(field)s must have at least %(count)d characters."), context ) %>
  • ', + max: '
  • <%- _.sprintf( gettext("%(field)s can only contain up to %(count)d characters."), context ) %>
  • ', + required: '
  • <%- _.sprintf( gettext("Please enter your %(field)s."), context ) %>
  • ', + custom: '
  • <%= content %>
  • ' + }, - msg: { - email: '
  • <%- gettext("The email address you\'ve provided isn\'t formatted correctly.") %>
  • ', - min: '
  • <%- _.sprintf( gettext("%(field)s must have at least %(count)d characters."), context ) %>
  • ', - max: '
  • <%- _.sprintf( gettext("%(field)s can only contain up to %(count)d characters."), context ) %>
  • ', - required: '
  • <%- _.sprintf( gettext("Please enter your %(field)s."), context ) %>
  • ', - custom: '
  • <%= content %>
  • ' - }, + field: function( el ) { + var $el = $(el), + required = true, + min = true, + max = true, + email = true, + response = {}, + isBlank = _fn.validate.isBlank( $el ); - field: function( el ) { - var $el = $(el), - required = true, - min = true, - max = true, - email = true, - response = {}, - isBlank = _fn.validate.isBlank( $el ); - - if ( _fn.validate.isRequired( $el ) ) { - if ( isBlank ) { - required = false; - } else { + if ( _fn.validate.isRequired( $el ) ) { + if ( isBlank ) { + required = false; + } else { + min = _fn.validate.str.minlength( $el ); + max = _fn.validate.str.maxlength( $el ); + email = _fn.validate.email.valid( $el ); + } + } else if ( !isBlank ) { min = _fn.validate.str.minlength( $el ); max = _fn.validate.str.maxlength( $el ); email = _fn.validate.email.valid( $el ); } - } else if ( !isBlank ) { - min = _fn.validate.str.minlength( $el ); - max = _fn.validate.str.maxlength( $el ); - email = _fn.validate.email.valid( $el ); - } - response.isValid = required && min && max && email; + response.isValid = required && min && max && email; - if ( !response.isValid ) { - _fn.validate.removeDefault( $el ); + if ( !response.isValid ) { + _fn.validate.removeDefault( $el ); - response.message = _fn.validate.getMessage( $el, { - required: required, - min: min, - max: max, - email: email - }); - } - - return response; - }, - - str: { - minlength: function( $el ) { - var min = $el.attr('minlength') || 0; - - return min <= $el.val().length; - }, - - maxlength: function( $el ) { - var max = $el.attr('maxlength') || false; - - return ( !!max ) ? max >= $el.val().length : true; - } - }, - - isRequired: function( $el ) { - return $el.attr('required'); - }, - - isBlank: function( $el ) { - var type = $el.attr('type'), - isBlank; - - if ( type === 'checkbox' ) { - isBlank = !$el.prop('checked'); - } else if ( type === 'select' ) { - isBlank = ( $el.data('isdefault') === true ); - } else { - isBlank = !$el.val(); - } - - return isBlank; - }, - - email: { - // This is the same regex used to validate email addresses in Django 1.4 - regex: new RegExp( - [ - '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*', - '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"', - ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+[A-Z]{2,6}\\.?$)', - '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$' - ].join(''), 'i' - ), - - valid: function( $el ) { - return $el.attr('type') === 'email' ? _fn.validate.email.format( $el.val() ) : true; - }, - - format: function( str ) { - return _fn.validate.email.regex.test( str ); - } - }, - - getLabel: function( id ) { - // Extract the field label, remove the asterisk (if it appears) and any extra whitespace - return $("label[for=" + id + "]").text().split("*")[0].trim(); - }, - - getMessage: function( $el, tests ) { - var txt = [], - tpl, - label, - obj, - customMsg; - - _.each( tests, function( value, key ) { - if ( !value ) { - label = _fn.validate.getLabel( $el.attr('id') ); - customMsg = $el.data('errormsg-' + key) || false; - - // If the field has a custom error msg attached, use it - if ( customMsg ) { - tpl = _fn.validate.msg.custom; - - obj = { - content: customMsg - }; - } else { - tpl = _fn.validate.msg[key]; - - obj = { - // We pass the context object to the template so that - // we can perform variable interpolation using sprintf - context: { - field: label - } - }; - - if ( key === 'min' ) { - obj.context.count = parseInt( $el.attr('minlength'), 10 ); - } else if ( key === 'max' ) { - obj.context.count = parseInt( $el.attr('maxlength'), 10 ); - } - } - - txt.push( _.template( tpl, obj ) ); + response.message = _fn.validate.getMessage( $el, { + required: required, + min: min, + max: max, + email: email + }); } - }); - return txt.join(' '); - }, + return response; + }, - // Removes the default HTML5 validation pop-up - removeDefault: function( $el ) { - if ( $el.setCustomValidity ) { - $el.setCustomValidity(' '); + str: { + minlength: function( $el ) { + var min = $el.attr('minlength') || 0; + + return min <= $el.val().length; + }, + + maxlength: function( $el ) { + var max = $el.attr('maxlength') || false; + + return ( !!max ) ? max >= $el.val().length : true; + } + }, + + isRequired: function( $el ) { + return $el.attr('required'); + }, + + isBlank: function( $el ) { + var type = $el.attr('type'), + isBlank; + + if ( type === 'checkbox' ) { + isBlank = !$el.prop('checked'); + } else if ( type === 'select' ) { + isBlank = ( $el.data('isdefault') === true ); + } else { + isBlank = !$el.val(); + } + + return isBlank; + }, + + email: { + // This is the same regex used to validate email addresses in Django 1.4 + regex: new RegExp( + [ + '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*', + '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"', + ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+[A-Z]{2,6}\\.?$)', + '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$' + ].join(''), 'i' + ), + + valid: function( $el ) { + return $el.attr('type') === 'email' ? _fn.validate.email.format( $el.val() ) : true; + }, + + format: function( str ) { + return _fn.validate.email.regex.test( str ); + } + }, + + getLabel: function( id ) { + // Extract the field label, remove the asterisk (if it appears) and any extra whitespace + return $("label[for=" + id + "]").text().split("*")[0].trim(); + }, + + getMessage: function( $el, tests ) { + var txt = [], + tpl, + label, + obj, + customMsg; + + _.each( tests, function( value, key ) { + if ( !value ) { + label = _fn.validate.getLabel( $el.attr('id') ); + customMsg = $el.data('errormsg-' + key) || false; + + // If the field has a custom error msg attached, use it + if ( customMsg ) { + tpl = _fn.validate.msg.custom; + + obj = { + content: customMsg + }; + } else { + tpl = _fn.validate.msg[key]; + + obj = { + // We pass the context object to the template so that + // we can perform variable interpolation using sprintf + context: { + field: label + } + }; + + if ( key === 'min' ) { + obj.context.count = parseInt( $el.attr('minlength'), 10 ); + } else if ( key === 'max' ) { + obj.context.count = parseInt( $el.attr('maxlength'), 10 ); + } + } + + txt.push( _.template( tpl, obj ) ); + } + }); + + return txt.join(' '); + }, + + // Removes the default HTML5 validation pop-up + removeDefault: function( $el ) { + if ( $el.setCustomValidity ) { + $el.setCustomValidity(' '); + } } } - } - }; + }; - return { - validate: _fn.validate.field - }; + return { + validate: _fn.validate.field + }; - })(); + })(); - edx.utils.validate = utils.validate; - -})( jQuery, _, _.str, gettext ); + return utils; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index a845414562..b2f00de285 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -8,7 +8,6 @@ import json import mock import ddt -import markupsafe from django.conf import settings from django.core.urlresolvers import reverse from django.core import mail @@ -20,6 +19,7 @@ from django.test.client import RequestFactory from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH +from openedx.core.lib.json_utils import EscapedEdxJSONEncoder from student.tests.factories import UserFactory from student_account.views import account_settings_context from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin @@ -223,7 +223,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi @ddt.unpack def test_login_and_registration_form(self, url_name, initial_mode): response = self.client.get(reverse(url_name)) - expected_data = u"data-initial-mode=\"{mode}\"".format(mode=initial_mode) + expected_data = '"initial_mode": "{mode}"'.format(mode=initial_mode) self.assertContains(response, expected_data) @ddt.data("signin_user", "register_user") @@ -255,6 +255,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi # that preserves the querystring params with mock.patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': is_edx_domain}): response = self.client.get(reverse(url_name), params) + expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')])) self.assertContains(response, expected_url) @@ -330,7 +331,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi def test_hinted_login(self): params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] response = self.client.get(reverse('signin_user'), params) - self.assertContains(response, "data-third-party-auth-hint='oa2-google-oauth2'") + self.assertContains(response, '"third_party_auth_hint": "oa2-google-oauth2"') @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) def test_microsite_uses_old_login_page(self): @@ -358,17 +359,17 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi finish_auth_url = None if current_backend: finish_auth_url = reverse("social:complete", kwargs={"backend": current_backend}) + "?" - auth_info = markupsafe.escape( - json.dumps({ - "currentProvider": current_provider, - "providers": providers, - "secondaryProviders": [], - "finishAuthUrl": finish_auth_url, - "errorMessage": None, - }) - ) - expected_data = u"data-third-party-auth='{auth_info}'".format( + auth_info = { + "currentProvider": current_provider, + "providers": providers, + "secondaryProviders": [], + "finishAuthUrl": finish_auth_url, + "errorMessage": None, + } + auth_info = json.dumps(auth_info, cls=EscapedEdxJSONEncoder) + + expected_data = '"third_party_auth": {auth_info}'.format( auth_info=auth_info ) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 706ffaa94b..62094945b7 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -95,22 +95,25 @@ def login_and_registration_form(request, initial_mode="login"): # Otherwise, render the combined login/registration page context = { - 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in the header - 'disable_courseware_js': True, - 'initial_mode': initial_mode, - 'third_party_auth': json.dumps(_third_party_auth_context(request, redirect_to)), - 'third_party_auth_hint': third_party_auth_hint or '', - 'platform_name': settings.PLATFORM_NAME, + 'data': { + 'login_redirect_url': redirect_to, + 'initial_mode': initial_mode, + 'third_party_auth': _third_party_auth_context(request, redirect_to), + 'third_party_auth_hint': third_party_auth_hint or '', + 'platform_name': settings.PLATFORM_NAME, + + # Include form descriptions retrieved from the user API. + # We could have the JS client make these requests directly, + # but we include them in the initial page load to avoid + # the additional round-trip to the server. + 'login_form_desc': json.loads(form_descriptions['login']), + 'registration_form_desc': json.loads(form_descriptions['registration']), + 'password_reset_form_desc': json.loads(form_descriptions['password_reset']), + }, + 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in header 'responsive': True, 'allow_iframing': True, - - # Include form descriptions retrieved from the user API. - # We could have the JS client make these requests directly, - # but we include them in the initial page load to avoid - # the additional round-trip to the server. - 'login_form_desc': form_descriptions['login'], - 'registration_form_desc': form_descriptions['registration'], - 'password_reset_form_desc': form_descriptions['password_reset'], + 'disable_courseware_js': True, } return render_to_response('student_account/login_and_register.html', context) diff --git a/lms/envs/common.py b/lms/envs/common.py index c1ebcfbb04..a7c42abea3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1303,26 +1303,6 @@ instructor_dash_js = ( sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/instructor_dashboard/**/*.js')) ) -# JavaScript used by the student account and profile pages -# These are not courseware, so they do not need many of the courseware-specific -# JavaScript modules. -student_account_js = [ - 'js/utils/edx.utils.validate.js', - 'js/sticky_filter.js', - 'js/query-params.js', - 'js/student_account/models/LoginModel.js', - 'js/student_account/models/RegisterModel.js', - 'js/student_account/models/PasswordResetModel.js', - 'js/student_account/views/FormView.js', - 'js/student_account/views/LoginView.js', - 'js/student_account/views/HintedLoginView.js', - 'js/student_account/views/RegisterView.js', - 'js/student_account/views/PasswordResetView.js', - 'js/student_account/views/AccessView.js', - 'js/student_account/views/InstitutionLoginView.js', - 'js/student_account/accessApp.js', -] - verify_student_js = [ 'js/sticky_filter.js', 'js/query-params.js', @@ -1574,10 +1554,6 @@ PIPELINE_JS = { 'source_filenames': dashboard_js, 'output_filename': 'js/dashboard.js' }, - 'student_account': { - 'source_filenames': student_account_js, - 'output_filename': 'js/student_account.js' - }, 'verify_student': { 'source_filenames': verify_student_js, 'output_filename': 'js/verify_student.js' diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index cb97519c78..76b2d32a63 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -75,16 +75,6 @@ 'js/views/file_uploader': 'js/views/file_uploader', 'js/views/notification': 'js/views/notification', 'js/student_account/account': 'js/student_account/account', - 'js/student_account/views/FormView': 'js/student_account/views/FormView', - 'js/student_account/models/LoginModel': 'js/student_account/models/LoginModel', - 'js/student_account/views/LoginView': 'js/student_account/views/LoginView', - 'js/student_account/views/InstitutionLoginView': 'js/student_account/views/InstitutionLoginView', - 'js/student_account/models/PasswordResetModel': 'js/student_account/models/PasswordResetModel', - 'js/student_account/views/PasswordResetView': 'js/student_account/views/PasswordResetView', - 'js/student_account/models/RegisterModel': 'js/student_account/models/RegisterModel', - 'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView', - 'js/student_account/views/AccessView': 'js/student_account/views/AccessView', - 'js/student_account/views/HintedLoginView': 'js/student_account/views/HintedLoginView', 'js/student_profile/views/learner_profile_fields': 'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory', 'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view', @@ -94,7 +84,10 @@ 'DiscussionModuleView': 'xmodule_js/common_static/coffee/src/discussion/discussion_module_view', // edxnotes - 'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min' + 'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min', + + // Common edx utils + 'js/utils/edx.utils.validate': 'xmodule_js/common_static/js/utils/edx.utils.validate' }, shim: { 'gettext': { @@ -337,91 +330,6 @@ 'js/models/notification', 'jquery.fileupload' ] }, - // Student account registration/login - // Loaded explicitly until these are converted to RequireJS - 'js/student_account/views/FormView': { - exports: 'edx.student.account.FormView', - deps: ['jquery', 'underscore', 'backbone', 'gettext'] - }, - 'js/student_account/models/LoginModel': { - exports: 'edx.student.account.LoginModel', - deps: ['jquery', 'jquery.cookie', 'backbone'] - }, - 'js/student_account/views/LoginView': { - exports: 'edx.student.account.LoginView', - deps: [ - 'jquery', - 'jquery.url', - 'underscore', - 'gettext', - 'js/student_account/models/LoginModel', - 'js/student_account/views/FormView' - ] - }, - 'js/student_account/views/InstitutionLoginView': { - exports: 'edx.student.account.InstitutionLoginView', - deps: [ - 'jquery', - 'underscore', - 'backbone' - ] - }, - 'js/student_account/models/PasswordResetModel': { - exports: 'edx.student.account.PasswordResetModel', - deps: ['jquery', 'jquery.cookie', 'backbone'] - }, - 'js/student_account/views/PasswordResetView': { - exports: 'edx.student.account.PasswordResetView', - deps: [ - 'jquery', - 'underscore', - 'gettext', - 'js/student_account/models/PasswordResetModel', - 'js/student_account/views/FormView' - ] - }, - 'js/student_account/models/RegisterModel': { - exports: 'edx.student.account.RegisterModel', - deps: ['jquery', 'jquery.cookie', 'backbone'] - }, - 'js/student_account/views/RegisterView': { - exports: 'edx.student.account.RegisterView', - deps: [ - 'jquery', - 'jquery.url', - 'underscore', - 'gettext', - 'js/student_account/models/RegisterModel', - 'js/student_account/views/FormView' - ] - }, - 'js/student_account/views/HintedLoginView': { - exports: 'edx.student.account.HintedLoginView', - deps: [ - 'jquery', - 'underscore', - 'backbone', - 'gettext' - ] - }, - 'js/student_account/views/AccessView': { - exports: 'edx.student.account.AccessView', - deps: [ - 'jquery', - 'underscore', - 'backbone', - 'history', - 'utility', - 'js/student_account/views/LoginView', - 'js/student_account/views/PasswordResetView', - 'js/student_account/views/RegisterView', - 'js/student_account/views/InstitutionLoginView', - 'js/student_account/models/LoginModel', - 'js/student_account/models/PasswordResetModel', - 'js/student_account/models/RegisterModel', - 'js/student_account/views/FormView' - ] - }, 'js/verify_student/models/verification_model': { exports: 'edx.verify_student.VerificationModel', deps: [ 'jquery', 'underscore', 'backbone', 'jquery.cookie' ] @@ -731,6 +639,7 @@ 'lms/include/js/spec/instructor_dashboard/student_admin_spec.js', 'lms/include/js/spec/student_account/account_spec.js', 'lms/include/js/spec/student_account/access_spec.js', + 'lms/include/js/spec/student_account/logistration_factory_spec.js', 'lms/include/js/spec/student_account/finish_auth_spec.js', 'lms/include/js/spec/student_account/hinted_login_spec.js', 'lms/include/js/spec/student_account/login_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 590c9df58f..88a73c1506 100644 --- a/lms/static/js/spec/student_account/access_spec.js +++ b/lms/static/js/spec/student_account/access_spec.js @@ -1,14 +1,20 @@ -define([ - 'jquery', - 'common/js/spec_helpers/template_helpers', - 'common/js/spec_helpers/ajax_helpers', - 'js/student_account/views/AccessView', - 'js/student_account/views/FormView', - 'js/student_account/enrollment', - 'js/student_account/shoppingcart', - 'js/student_account/emailoptin' -], function($, TemplateHelpers, AjaxHelpers, AccessView, FormView, EnrollmentInterface, ShoppingCartInterface) { - "use strict"; +;(function (define) { + 'use strict'; + define([ + 'jquery', + 'underscore', + 'backbone', + 'common/js/spec_helpers/template_helpers', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/views/AccessView', + 'js/student_account/views/FormView', + 'js/student_account/enrollment', + 'js/student_account/shoppingcart', + 'js/student_account/emailoptin' + ], + function($, _, Backbone, TemplateHelpers, AjaxHelpers, AccessView, FormView, EnrollmentInterface, + ShoppingCartInterface) { + describe('edx.student.account.AccessView', function() { var requests = null, view = null, @@ -24,7 +30,7 @@ define([ required: true, placeholder: 'xsy@edx.org', instructions: 'Enter your email here.', - restrictions: {}, + restrictions: {} }, { name: 'username', @@ -49,24 +55,27 @@ define([ THIRD_PARTY_COMPLETE_URL = '/auth/complete/provider/'; var ajaxSpyAndInitialize = function(that, mode, nextUrl, finishAuthUrl) { + var options = { + initial_mode: mode, + third_party_auth: { + currentProvider: null, + providers: [], + secondaryProviders: [{name: "provider"}], + finishAuthUrl: finishAuthUrl + }, + login_redirect_url: nextUrl, // undefined for default + platform_name: 'edX', + login_form_desc: FORM_DESCRIPTION, + registration_form_desc: FORM_DESCRIPTION, + password_reset_form_desc: FORM_DESCRIPTION + }, + $logistrationElement = $('#login-and-registration-container'); + // Spy on AJAX requests requests = AjaxHelpers.requests(that); // Initialize the access view - view = new AccessView({ - mode: mode, - thirdPartyAuth: { - currentProvider: null, - providers: [], - secondaryProviders: [{name: "provider"}], - finishAuthUrl: finishAuthUrl - }, - nextUrl: nextUrl, // undefined for default - platformName: 'edX', - loginFormDesc: FORM_DESCRIPTION, - registrationFormDesc: FORM_DESCRIPTION, - passwordResetFormDesc: FORM_DESCRIPTION - }); + view = new AccessView(_.extend(options, {el: $logistrationElement})); // Mock the redirect call spyOn( view, 'redirect' ).andCallFake( function() {} ); @@ -92,7 +101,7 @@ define([ }; beforeEach(function() { - setFixtures('
    '); + setFixtures('
    '); TemplateHelpers.installTemplate('templates/student_account/access'); TemplateHelpers.installTemplate('templates/student_account/login'); TemplateHelpers.installTemplate('templates/student_account/register'); @@ -105,6 +114,10 @@ define([ window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'pageview', 'trackLink']); }); + afterEach(function() { + Backbone.history.stop(); + }); + it('can initially display the login form', function() { ajaxSpyAndInitialize(this, 'login'); @@ -217,5 +230,5 @@ define([ }); }); - } -); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/student_account/finish_auth_spec.js b/lms/static/js/spec/student_account/finish_auth_spec.js index 1dd9e4dece..f29291747b 100644 --- a/lms/static/js/spec/student_account/finish_auth_spec.js +++ b/lms/static/js/spec/student_account/finish_auth_spec.js @@ -1,13 +1,18 @@ -define([ - 'jquery', - 'utility', - 'common/js/spec_helpers/ajax_helpers', - 'js/student_account/views/FinishAuthView', - 'js/student_account/enrollment', - 'js/student_account/shoppingcart', - 'js/student_account/emailoptin' -], function($, utility, AjaxHelpers, FinishAuthView, EnrollmentInterface, ShoppingCartInterface, EmailOptInInterface) { - 'use strict'; +;(function (define) { + 'use strict'; + define([ + 'jquery', + 'jquery.url', + 'utility', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/views/FinishAuthView', + 'js/student_account/enrollment', + 'js/student_account/shoppingcart', + 'js/student_account/emailoptin' + ], + function($, url, utility, AjaxHelpers, FinishAuthView, EnrollmentInterface, ShoppingCartInterface, + EmailOptInInterface) { + describe('FinishAuthView', function() { var requests = null, view = null, @@ -167,5 +172,5 @@ define([ expect( view.redirect ).toHaveBeenCalledWith( "/dashboard" ); }); }); - } -); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/student_account/hinted_login_spec.js b/lms/static/js/spec/student_account/hinted_login_spec.js index a03f036722..2a751b32d2 100644 --- a/lms/static/js/spec/student_account/hinted_login_spec.js +++ b/lms/static/js/spec/student_account/hinted_login_spec.js @@ -1,87 +1,90 @@ -define([ - 'jquery', - 'underscore', - 'common/js/spec_helpers/template_helpers', - 'common/js/spec_helpers/ajax_helpers', - 'js/student_account/views/HintedLoginView', -], function($, _, TemplateHelpers, AjaxHelpers, HintedLoginView) { +;(function (define) { 'use strict'; - describe('edx.student.account.HintedLoginView', function() { + define([ + 'jquery', + 'underscore', + 'common/js/spec_helpers/template_helpers', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/views/HintedLoginView' + ], + function($, _, TemplateHelpers, AjaxHelpers, HintedLoginView) { - var view = null, - requests = null, - PLATFORM_NAME = 'edX', - THIRD_PARTY_AUTH = { - currentProvider: null, - providers: [ - { - id: 'oa2-google-oauth2', - name: 'Google', - iconClass: 'fa-google-plus', - loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', - registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' - }, - { - id: 'oa2-facebook', - name: 'Facebook', - iconClass: 'fa-facebook', - loginUrl: '/auth/login/facebook/?auth_entry=account_login', - registerUrl: '/auth/login/facebook/?auth_entry=account_register' - } - ], - secondaryProviders: [ - { - id: 'saml-harvard', - name: 'Harvard', - iconClass: 'fa-university', - loginUrl: '/auth/login/tpa-saml/?auth_entry=account_login&idp=harvard', - registerUrl: '/auth/login/tpa-saml/?auth_entry=account_register&idp=harvard' - } - ] + describe('edx.student.account.HintedLoginView', function() { + var view = null, + requests = null, + PLATFORM_NAME = 'edX', + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + iconClass: 'fa-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + id: 'oa2-facebook', + name: 'Facebook', + iconClass: 'fa-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ], + secondaryProviders: [ + { + id: 'saml-harvard', + name: 'Harvard', + iconClass: 'fa-university', + loginUrl: '/auth/login/tpa-saml/?auth_entry=account_login&idp=harvard', + registerUrl: '/auth/login/tpa-saml/?auth_entry=account_register&idp=harvard' + } + ] + }; + + var createHintedLoginView = function(hintedProvider) { + // Initialize the login view + view = new HintedLoginView({ + thirdPartyAuth: THIRD_PARTY_AUTH, + hintedProvider: hintedProvider, + platformName: PLATFORM_NAME + }); + + // Mock the redirect call + spyOn( view, 'redirect' ).andCallFake( function() {} ); + + view.render(); }; - var createHintedLoginView = function(hintedProvider) { - // Initialize the login view - view = new HintedLoginView({ - thirdPartyAuth: THIRD_PARTY_AUTH, - hintedProvider: hintedProvider, - platformName: PLATFORM_NAME + beforeEach(function() { + setFixtures('
    '); + TemplateHelpers.installTemplate('templates/student_account/hinted_login'); }); - // Mock the redirect call - spyOn( view, 'redirect' ).andCallFake( function() {} ); + it('displays a choice as two buttons', function() { + createHintedLoginView("oa2-google-oauth2"); - view.render(); - }; + expect($('.proceed-button.button-oa2-google-oauth2')).toBeVisible(); + expect($('.form-toggle')).toBeVisible(); + expect($('.proceed-button.button-oa2-facebook')).not.toBeVisible(); + }); - beforeEach(function() { - setFixtures('
    '); - TemplateHelpers.installTemplate('templates/student_account/hinted_login'); - }); + it('works with secondary providers as well', function() { + createHintedLoginView("saml-harvard"); - it('displays a choice as two buttons', function() { - createHintedLoginView("oa2-google-oauth2"); + expect($('.proceed-button.button-saml-harvard')).toBeVisible(); + expect($('.form-toggle')).toBeVisible(); + expect($('.proceed-button.button-oa2-google-oauth2')).not.toBeVisible(); + }); - expect($('.proceed-button.button-oa2-google-oauth2')).toBeVisible(); - expect($('.form-toggle')).toBeVisible(); - expect($('.proceed-button.button-oa2-facebook')).not.toBeVisible(); - }); + it('redirects the user to the hinted provider if the user clicks the proceed button', function() { + createHintedLoginView('oa2-google-oauth2'); - it('works with secondary providers as well', function() { - createHintedLoginView("saml-harvard"); + // Click the "Yes, proceed" button + $('.proceed-button').click(); - expect($('.proceed-button.button-saml-harvard')).toBeVisible(); - expect($('.form-toggle')).toBeVisible(); - expect($('.proceed-button.button-oa2-google-oauth2')).not.toBeVisible(); - }); - - it('redirects the user to the hinted provider if the user clicks the proceed button', function() { - createHintedLoginView("oa2-google-oauth2"); - - // Click the "Yes, proceed" button - $('.proceed-button').click(); - - expect(view.redirect).toHaveBeenCalledWith( '/auth/login/google-oauth2/?auth_entry=account_login' ); + expect(view.redirect).toHaveBeenCalledWith( '/auth/login/google-oauth2/?auth_entry=account_login' ); + }); }); }); -}); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/student_account/institution_login_spec.js b/lms/static/js/spec/student_account/institution_login_spec.js index 208c975550..d087509ecc 100644 --- a/lms/static/js/spec/student_account/institution_login_spec.js +++ b/lms/static/js/spec/student_account/institution_login_spec.js @@ -1,80 +1,87 @@ -define([ - 'jquery', - 'underscore', - 'common/js/spec_helpers/template_helpers', - 'js/student_account/views/InstitutionLoginView', -], function($, _, TemplateHelpers, InstitutionLoginView) { +;(function (define) { 'use strict'; - describe('edx.student.account.InstitutionLoginView', function() { + define([ + 'jquery', + 'underscore', + 'common/js/spec_helpers/template_helpers', + 'js/student_account/views/InstitutionLoginView' + ], + function($, _, TemplateHelpers, InstitutionLoginView) { - var view = null, - PLATFORM_NAME = 'edX', - THIRD_PARTY_AUTH = { - currentProvider: null, - providers: [], - secondaryProviders: [ - { - id: 'oa2-google-oauth2', - name: 'Google', - iconClass: 'fa-google-plus', - loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', - registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' - }, - { - id: 'oa2-facebook', - name: 'Facebook', - iconClass: 'fa-facebook', - loginUrl: '/auth/login/facebook/?auth_entry=account_login', - registerUrl: '/auth/login/facebook/?auth_entry=account_register' - } - ] + describe('edx.student.account.InstitutionLoginView', function() { + var view = null, + PLATFORM_NAME = 'edX', + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [], + secondaryProviders: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + iconClass: 'fa-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + id: 'oa2-facebook', + name: 'Facebook', + iconClass: 'fa-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ] + }; + + var createInstLoginView = function(mode) { + // Initialize the login view + view = new InstitutionLoginView({ + mode: mode, + thirdPartyAuth: THIRD_PARTY_AUTH, + platformName: PLATFORM_NAME + }); + view.render(); }; - var createInstLoginView = function(mode) { - // Initialize the login view - view = new InstitutionLoginView({ - mode: mode, - thirdPartyAuth: THIRD_PARTY_AUTH, - platformName: PLATFORM_NAME + beforeEach(function() { + setFixtures('
    '); + TemplateHelpers.installTemplate('templates/student_account/institution_login'); + TemplateHelpers.installTemplate('templates/student_account/institution_register'); + }); + + it('displays a list of providers', function() { + var $google, $facebook; + + createInstLoginView('login'); + expect($('#institution_login-form').html()).not.toBe(""); + $google = $('li a:contains("Google")'); + expect($google).toBeVisible(); + expect($google).toHaveAttr( + 'href', '/auth/login/google-oauth2/?auth_entry=account_login' + ); + $facebook = $('li a:contains("Facebook")'); + expect($facebook).toBeVisible(); + expect($facebook).toHaveAttr( + 'href', '/auth/login/facebook/?auth_entry=account_login' + ); + }); + + it('displays a list of providers', function() { + var $google, $facebook; + + createInstLoginView('register'); + expect($('#institution_login-form').html()).not.toBe(""); + $google = $('li a:contains("Google")'); + expect($google).toBeVisible(); + expect($google).toHaveAttr( + 'href', '/auth/login/google-oauth2/?auth_entry=account_register' + ); + $facebook = $('li a:contains("Facebook")'); + expect($facebook).toBeVisible(); + expect($facebook).toHaveAttr( + 'href', '/auth/login/facebook/?auth_entry=account_register' + ); }); - view.render(); - }; - beforeEach(function() { - setFixtures('
    '); - TemplateHelpers.installTemplate('templates/student_account/institution_login'); - TemplateHelpers.installTemplate('templates/student_account/institution_register'); }); - - it('displays a list of providers', function() { - createInstLoginView('login'); - expect($('#institution_login-form').html()).not.toBe(""); - var $google = $('li a:contains("Google")'); - expect($google).toBeVisible(); - expect($google).toHaveAttr( - 'href', '/auth/login/google-oauth2/?auth_entry=account_login' - ); - var $facebook = $('li a:contains("Facebook")'); - expect($facebook).toBeVisible(); - expect($facebook).toHaveAttr( - 'href', '/auth/login/facebook/?auth_entry=account_login' - ); - }); - - it('displays a list of providers', function() { - createInstLoginView('register'); - expect($('#institution_login-form').html()).not.toBe(""); - var $google = $('li a:contains("Google")'); - expect($google).toBeVisible(); - expect($google).toHaveAttr( - 'href', '/auth/login/google-oauth2/?auth_entry=account_register' - ); - var $facebook = $('li a:contains("Facebook")'); - expect($facebook).toBeVisible(); - expect($facebook).toHaveAttr( - 'href', '/auth/login/facebook/?auth_entry=account_register' - ); - }); - }); -}); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js index d61f49be87..f66a4d60e1 100644 --- a/lms/static/js/spec/student_account/login_spec.js +++ b/lms/static/js/spec/student_account/login_spec.js @@ -1,265 +1,270 @@ -define([ - 'jquery', - 'underscore', - 'common/js/spec_helpers/template_helpers', - 'common/js/spec_helpers/ajax_helpers', - 'js/student_account/models/LoginModel', - 'js/student_account/views/LoginView', - 'js/student_account/models/PasswordResetModel' -], function($, _, TemplateHelpers, AjaxHelpers, LoginModel, LoginView, PasswordResetModel) { +;(function (define) { 'use strict'; - describe('edx.student.account.LoginView', function() { + define([ + 'jquery', + 'underscore', + 'common/js/spec_helpers/template_helpers', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/models/LoginModel', + 'js/student_account/views/LoginView', + 'js/student_account/models/PasswordResetModel' + ], + function($, _, TemplateHelpers, AjaxHelpers, LoginModel, LoginView, PasswordResetModel) { - var model = null, - resetModel = 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: [ - { - id: 'oa2-google-oauth2', - name: 'Google', - iconClass: 'fa-google-plus', - loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', - registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' - }, - { - id: 'oa2-facebook', - name: 'Facebook', - iconClass: 'fa-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: [ - { - placeholder: 'username@domain.com', - name: 'email', - label: 'Email', - defaultValue: '', - type: 'email', - required: true, - instructions: 'Enter your email.', - restrictions: {} - }, - { - placeholder: '', - name: 'password', - label: 'Password', - defaultValue: '', - type: 'password', - required: true, - instructions: 'Enter your password.', - restrictions: {} - }, - { - placeholder: '', - name: 'remember', - label: 'Remember me', - defaultValue: '', - type: 'checkbox', - required: true, - instructions: "Agree to the terms of service.", - restrictions: {} - } - ] - }, - COURSE_ID = "edX/demoX/Fall"; + describe('edx.student.account.LoginView', function() { + var model = null, + resetModel = 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: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + iconClass: 'fa-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + id: 'oa2-facebook', + name: 'Facebook', + iconClass: 'fa-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: [ + { + placeholder: 'username@domain.com', + name: 'email', + label: 'Email', + defaultValue: '', + type: 'email', + required: true, + instructions: 'Enter your email.', + restrictions: {} + }, + { + placeholder: '', + name: 'password', + label: 'Password', + defaultValue: '', + type: 'password', + required: true, + instructions: 'Enter your password.', + restrictions: {} + }, + { + placeholder: '', + 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 - }); - - // Initialize the passwordReset model - resetModel = new PasswordResetModel({}, { - method: 'GET', - url: '#' - }); - - // Initialize the login view - view = new LoginView({ - fields: FORM_DESCRIPTION.fields, - model: model, - resetModel: resetModel, - 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.' + var createLoginView = function(test) { + // Initialize the login model + model = new LoginModel({}, { + url: FORM_DESCRIPTION.submit_url, + method: FORM_DESCRIPTION.method }); - } - // Submit the email address - view.submitForm(clickEvent); - }; + // Initialize the passwordReset model + resetModel = new PasswordResetModel({}, { + method: 'GET', + url: '#' + }); - beforeEach(function() { - setFixtures('
    '); - TemplateHelpers.installTemplate('templates/student_account/login'); - TemplateHelpers.installTemplate('templates/student_account/form_field'); - }); + // Initialize the login view + view = new LoginView({ + fields: FORM_DESCRIPTION.fields, + model: model, + resetModel: resetModel, + thirdPartyAuth: THIRD_PARTY_AUTH, + platformName: PLATFORM_NAME + }); - it('logs the user in', function() { - createLoginView(this); + // Spy on AJAX requests + requests = AjaxHelpers.requests(test); - // Submit the form, with successful validation - submitForm(true); + // Intercept events from the view + authComplete = false; + view.on("auth-complete", function() { + authComplete = true; + }); + }; - // Form button should be disabled on success. - expect(view.$submitButton).toHaveAttr('disabled'); + var submitForm = function(validationSuccess) { + // Create a fake click event + var clickEvent = $.Event('click'); - // Verify that the client contacts the server with the expected data - AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param(USER_DATA) - ); + // Simulate manual entry of login form data + $('#login-email').val(USER_DATA.email); + $('#login-password').val(USER_DATA.password); - // Respond with status code 200 - AjaxHelpers.respondWithJson(requests, {}); + // Check the 'Remember me' checkbox + $('#login-remember').prop('checked', USER_DATA.remember); - // 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 ); + // 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'); }); - // Attempt to login - submitForm( true ); + it('logs the user in', function() { + createLoginView(this); - // Verify that the client sent the course ID for analytics - var expectedData = {}; - $.extend(expectedData, USER_DATA, { - analytics: JSON.stringify({ - enroll_course_id: COURSE_ID - }) + // Submit the form, with successful validation + submitForm(true); + + // Form button should be disabled on success. + expect(view.$submitButton).toHaveAttr('disabled'); + + // 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); }); - AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param( expectedData ) - ); - }); + it('sends analytics info containing the enrolled course ID', function() { + var expectedData; - it('displays third-party auth login buttons', function() { - createLoginView(this); + createLoginView(this); - // Verify that Google and Facebook registration buttons are displayed - expect($('.button-oa2-google-oauth2')).toBeVisible(); - expect($('.button-oa2-facebook')).toBeVisible(); - }); + // 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 ); + } + }); - it('displays a link to the password reset form', function() { - createLoginView(this); + // Attempt to login + submitForm( true ); - // Verify that the password reset link is displayed - expect($('.forgot-password')).toBeVisible(); - }); + // Verify that the client sent the course ID for analytics + expectedData = {}; + $.extend(expectedData, USER_DATA, { + analytics: JSON.stringify({ + enroll_course_id: COURSE_ID + }) + }); - it('validates login form fields', function() { - createLoginView(this); + AjaxHelpers.expectRequest( + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param( expectedData ) + ); + }); - submitForm(true); + it('displays third-party auth login buttons', function() { + createLoginView(this); - // Verify that validation of form fields occurred - expect(view.validate).toHaveBeenCalledWith($('#login-email')[0]); - expect(view.validate).toHaveBeenCalledWith($('#login-password')[0]); - }); + // Verify that Google and Facebook registration buttons are displayed + expect($('.button-oa2-google-oauth2')).toBeVisible(); + expect($('.button-oa2-facebook')).toBeVisible(); + }); - it('displays login form validation errors', function() { - createLoginView(this); + it('displays a link to the password reset form', function() { + createLoginView(this); - // Submit the form, with failed validation - submitForm(false); + // Verify that the password reset link is displayed + expect($('.forgot-password')).toBeVisible(); + }); - // Verify that submission errors are visible - expect(view.$errors).not.toHaveClass('hidden'); + it('validates login form fields', function() { + createLoginView(this); - // Expect auth complete NOT to have been triggered - expect(authComplete).toBe(false); - // Form button should be re-enabled when errors occur - expect(view.$submitButton).not.toHaveAttr('disabled'); - }); + submitForm(true); - it('displays an error if the server returns an error while logging in', function() { - createLoginView(this); + // Verify that validation of form fields occurred + expect(view.validate).toHaveBeenCalledWith($('#login-email')[0]); + expect(view.validate).toHaveBeenCalledWith($('#login-password')[0]); + }); - // Submit the form, with successful validation - submitForm(true); + it('displays login form validation errors', function() { + createLoginView(this); - // Simulate an error from the LMS servers - AjaxHelpers.respondWithError(requests); + // Submit the form, with failed validation + submitForm(false); - // Expect that an error is displayed and that auth complete is not triggered - expect(view.$errors).not.toHaveClass('hidden'); - expect(authComplete).toBe(false); - // Form button should be re-enabled on server failure. - expect(view.$submitButton).not.toHaveAttr('disabled'); + // Verify that submission errors are visible + expect(view.$errors).not.toHaveClass('hidden'); - // If we try again and succeed, the error should go away - submitForm(); + // Expect auth complete NOT to have been triggered + expect(authComplete).toBe(false); + // Form button should be re-enabled when errors occur + expect(view.$submitButton).not.toHaveAttr('disabled'); + }); - // Form button should be disabled on success. - expect(view.$submitButton).toHaveAttr('disabled'); + it('displays an error if the server returns an error while logging in', function() { + createLoginView(this); - // This time, respond with status code 200 - AjaxHelpers.respondWithJson(requests, {}); + // Submit the form, with successful validation + submitForm(true); - // Expect that the error is hidden and auth complete is triggered - expect(view.$errors).toHaveClass('hidden'); - expect(authComplete).toBe(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); + // Form button should be re-enabled on server failure. + expect(view.$submitButton).not.toHaveAttr('disabled'); + + // If we try again and succeed, the error should go away + submitForm(); + + // Form button should be disabled on success. + expect(view.$submitButton).toHaveAttr('disabled'); + + // 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); + }); }); }); -}); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/student_account/logistration_factory_spec.js b/lms/static/js/spec/student_account/logistration_factory_spec.js new file mode 100644 index 0000000000..8ccc66a436 --- /dev/null +++ b/lms/static/js/spec/student_account/logistration_factory_spec.js @@ -0,0 +1,130 @@ +;(function (define) { + 'use strict'; + define([ + 'jquery', + 'underscore', + 'backbone', + 'common/js/spec_helpers/template_helpers', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/logistration_factory' + ], + function($, _, Backbone, TemplateHelpers, AjaxHelpers, LogistrationFactory) { + + describe('Logistration Factory', function() { + var FORM_DESCRIPTION = { + method: 'post', + submit_url: '/submit', + fields: [ + { + name: 'email', + label: 'Email', + defaultValue: '', + type: 'text', + required: true, + placeholder: 'xsy@edx.org', + instructions: 'Enter your email here.', + restrictions: {} + }, + { + name: 'username', + label: 'Username', + defaultValue: '', + type: 'text', + required: true, + placeholder: 'Xsy', + instructions: 'Enter your username here.', + restrictions: { + max_length: 200 + } + } + ] + }; + + var initializeLogistrationFactory = function(that, mode, nextUrl, finishAuthUrl) { + var options = { + initial_mode: mode, + third_party_auth: { + currentProvider: null, + providers: [], + secondaryProviders: [{name: 'provider'}], + finishAuthUrl: finishAuthUrl + }, + login_redirect_url: nextUrl, // undefined for default + platform_name: 'edX', + login_form_desc: FORM_DESCRIPTION, + registration_form_desc: FORM_DESCRIPTION, + password_reset_form_desc: FORM_DESCRIPTION + }; + + // Initialize the logistration Factory + LogistrationFactory(options); + }; + + var assertForms = function(visibleForm, hiddenFormsList) { + expect($(visibleForm)).not.toHaveClass('hidden'); + + _.each(hiddenFormsList, function (hiddenForm) { + expect($(hiddenForm)).toHaveClass('hidden'); + }, this); + }; + + beforeEach(function() { + setFixtures('