diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index e715e2ceef..9c2076dc50 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -8,6 +8,7 @@ import json import mock import ddt +import markupsafe from django.test import TestCase from django.conf import settings from django.core.urlresolvers import reverse @@ -551,12 +552,16 @@ class StudentAccountLoginAndRegistrationTest(ModuleStoreTestCase): def _assert_third_party_auth_data(self, response, current_provider, providers): """Verify that third party auth info is rendered correctly in a DOM data attribute. """ - expected_data = u"data-third-party-auth='{auth_info}'".format( - auth_info=json.dumps({ + auth_info = markupsafe.escape( + json.dumps({ "currentProvider": current_provider, "providers": providers }) ) + + expected_data = u"data-third-party-auth='{auth_info}'".format( + auth_info=auth_info + ) self.assertContains(response, expected_data) def _third_party_login_url(self, backend_name, auth_entry, course_id=None, redirect_url=None): diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 9460f5fe01..8af87b358e 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -7,7 +7,8 @@ from django.http import ( HttpResponse, HttpResponseBadRequest, HttpResponseForbidden ) from django.shortcuts import redirect -from django.core.urlresolvers import reverse +from django.http import HttpRequest +from django.core.urlresolvers import reverse, resolve from django.core.mail import send_mail from django.utils.translation import ugettext as _ from django_future.csrf import ensure_csrf_cookie @@ -68,13 +69,24 @@ def login_and_registration_form(request, initial_mode="login"): if request.user.is_authenticated(): return redirect(reverse('dashboard')) + # Retrieve the form descriptions from the user API + form_descriptions = _get_form_descriptions(request) + # Otherwise, render the combined login/registration page context = { 'disable_courseware_js': True, 'initial_mode': initial_mode, 'third_party_auth': json.dumps(_third_party_auth_context(request)), 'platform_name': settings.PLATFORM_NAME, - 'responsive': True + 'responsive': 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'], } return render_to_response('student_account/login_and_register.html', context) @@ -317,3 +329,51 @@ def _third_party_auth_context(request): context["currentProvider"] = current_provider.NAME return context + + +def _get_form_descriptions(request): + """Retrieve form descriptions from the user API. + + Arguments: + request (HttpRequest): The original request, used to retrieve session info. + + Returns: + dict: Keys are 'login', 'registration', and 'password_reset'; + values are the JSON-serialized form descriptions. + + """ + return { + 'login': _local_server_get('/user_api/v1/account/login_session/', request.session), + 'registration': _local_server_get('/user_api/v1/account/registration/', request.session), + 'password_reset': _local_server_get('/user_api/v1/account/password_reset/', request.session) + } + + +def _local_server_get(url, session): + """Simulate a server-server GET request for an in-process API. + + Arguments: + url (str): The URL of the request (excluding the protocol and domain) + session (SessionStore): The session of the original request, + used to get past the CSRF checks. + + Returns: + str: The content of the response + + """ + # Since the user API is currently run in-process, + # we simulate the server-server API call by constructing + # our own request object. We don't need to include much + # information in the request except for the session + # (to get past through CSRF validation) + request = HttpRequest() + request.method = "GET" + request.session = session + + # Call the Django view function, simulating + # the server-server API call + view, args, kwargs = resolve(url) + response = view(request, *args, **kwargs) + + # Return the content of the response + return response.content diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js index 212269ca45..5a0ac71d1a 100644 --- a/lms/static/js/spec/student_account/access_spec.js +++ b/lms/static/js/spec/student_account/access_spec.js @@ -13,20 +13,6 @@ define([ var requests = null, view = null, - AJAX_INFO = { - register: { - url: '/user_api/v1/account/registration/', - requestIndex: 1 - }, - login: { - url: '/user_api/v1/account/login_session/', - requestIndex: 0 - }, - password_reset: { - url: '/user_api/v1/account/password_reset/', - requestIndex: 1 - } - }, FORM_DESCRIPTION = { method: 'post', submit_url: '/submit', @@ -58,16 +44,6 @@ define([ FORWARD_URL = '/courseware/next', COURSE_KEY = 'edx/DemoX/Fall'; - var ajaxAssertAndRespond = function(url, requestIndex) { - // Verify that the client contacts the server as expected - AjaxHelpers.expectJsonRequest(requests, 'GET', url, null, requestIndex); - - /* Simulate a response from the server containing - /* a dummy form description - */ - AjaxHelpers.respondWithJson(requests, FORM_DESCRIPTION); - }; - var ajaxSpyAndInitialize = function(that, mode) { // Spy on AJAX requests requests = AjaxHelpers.requests(that); @@ -79,7 +55,10 @@ define([ currentProvider: null, providers: [] }, - platformName: 'edX' + platformName: 'edX', + loginFormDesc: FORM_DESCRIPTION, + registrationFormDesc: FORM_DESCRIPTION, + passwordResetFormDesc: FORM_DESCRIPTION }); // Mock the redirect call @@ -88,9 +67,6 @@ define([ // Mock the enrollment and shopping cart interfaces spyOn( EnrollmentInterface, 'enroll' ).andCallFake( function() {} ); spyOn( ShoppingCartInterface, 'addCourseToCart' ).andCallFake( function() {} ); - - // Initialize the subview - ajaxAssertAndRespond(AJAX_INFO[mode].url); }; var assertForms = function(visibleType, hiddenType) { @@ -106,8 +82,6 @@ define([ // Load form corresponding to the change event view.toggleForm(changeEvent); - - ajaxAssertAndRespond(AJAX_INFO[type].url, AJAX_INFO[type].requestIndex); }; /** @@ -175,11 +149,6 @@ define([ // Simulate a click on the reset password link view.resetPassword(); - ajaxAssertAndRespond( - AJAX_INFO.password_reset.url, - AJAX_INFO.password_reset.requestIndex - ); - // Verify that the password reset wrapper is populated expect($('#password-reset-wrapper')).not.toBeEmpty(); }); @@ -253,26 +222,6 @@ define([ expect( view.redirect ).toHaveBeenCalledWith( "/dashboard" ); }); - it('displays an error if a form definition could not be loaded', function() { - // Spy on AJAX requests - requests = AjaxHelpers.requests(this); - - // Init AccessView - view = new AccessView({ - mode: 'login', - thirdPartyAuth: { - currentProvider: null, - providers: [] - }, - platformName: 'edX' - }); - - // Simulate an error from the LMS servers - AjaxHelpers.respondWithError(requests); - - // Error message should be displayed - expect( $('#form-load-fail').hasClass('hidden') ).toBe(false); - }); }); } ); diff --git a/lms/static/js/student_account/accessApp.js b/lms/static/js/student_account/accessApp.js index 9b41e41439..4c67707fcf 100644 --- a/lms/static/js/student_account/accessApp.js +++ b/lms/static/js/student_account/accessApp.js @@ -6,9 +6,14 @@ var edx = edx || {}; edx.student = edx.student || {}; edx.student.account = edx.student.account || {}; + var container = $('#login-and-registration-container'); + return new edx.student.account.AccessView({ - mode: $('#login-and-registration-container').data('initial-mode'), - thirdPartyAuth: $('#login-and-registration-container').data('third-party-auth'), - platformName: $('#login-and-registration-container').data('platform-name') + mode: container.data('initial-mode'), + thirdPartyAuth: container.data('third-party-auth'), + platformName: container.data('platform-name'), + loginFormDesc: container.data('login-form-desc'), + registrationFormDesc: container.data('registration-form-desc'), + passwordResetFormDesc: container.data('password-reset-form-desc') }); })(jQuery); diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 7bcc13a4c5..d36e9385fe 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -40,12 +40,20 @@ var edx = edx || {}; _.mixin( _s.exports() ); this.tpl = $(this.tpl).html(); + this.activeForm = obj.mode || 'login'; + this.thirdPartyAuth = obj.thirdPartyAuth || { currentProvider: null, providers: [] }; + this.formDescriptions = { + login: obj.loginFormDesc, + register: obj.registrationFormDesc, + reset: obj.passwordResetFormDesc + }; + this.platformName = obj.platformName; // The login view listens for 'sync' events from the reset model @@ -73,82 +81,64 @@ var edx = edx || {}; }, loadForm: function( type ) { - this.getFormData( type, this ); + var loadFunc = _.bind( this.load[type], this ); + loadFunc( this.formDescriptions[type] ); }, load: { - login: function( data, context ) { + login: function( data ) { var model = new edx.student.account.LoginModel({}, { method: data.method, url: data.submit_url }); - context.subview.login = new edx.student.account.LoginView({ + this.subview.login = new edx.student.account.LoginView({ fields: data.fields, model: model, - resetModel: context.resetModel, - thirdPartyAuth: context.thirdPartyAuth, - platformName: context.platformName + resetModel: this.resetModel, + thirdPartyAuth: this.thirdPartyAuth, + platformName: this.platformName }); // Listen for 'password-help' event to toggle sub-views - context.listenTo( context.subview.login, 'password-help', context.resetPassword ); + this.listenTo( this.subview.login, 'password-help', this.resetPassword ); // Listen for 'auth-complete' event so we can enroll/redirect the user appropriately. - context.listenTo( context.subview.login, 'auth-complete', context.authComplete ); + this.listenTo( this.subview.login, 'auth-complete', this.authComplete ); }, - reset: function( data, context ) { - context.resetModel.ajaxType = data.method; - context.resetModel.urlRoot = data.submit_url; + reset: function( data ) { + this.resetModel.ajaxType = data.method; + this.resetModel.urlRoot = data.submit_url; - context.subview.passwordHelp = new edx.student.account.PasswordResetView({ + this.subview.passwordHelp = new edx.student.account.PasswordResetView({ fields: data.fields, - model: context.resetModel + model: this.resetModel }); // Listen for 'password-email-sent' event to toggle sub-views - context.listenTo( context.subview.passwordHelp, 'password-email-sent', context.passwordEmailSent ); + this.listenTo( this.subview.passwordHelp, 'password-email-sent', this.passwordEmailSent ); }, - register: function( data, context ) { + register: function( data ) { var model = new edx.student.account.RegisterModel({}, { method: data.method, url: data.submit_url }); - context.subview.register = new edx.student.account.RegisterView({ + this.subview.register = new edx.student.account.RegisterView({ fields: data.fields, model: model, - thirdPartyAuth: context.thirdPartyAuth, - platformName: context.platformName + thirdPartyAuth: this.thirdPartyAuth, + platformName: this.platformName }); // Listen for 'auth-complete' event so we can enroll/redirect the user appropriately. - context.listenTo( context.subview.register, 'auth-complete', context.authComplete ); + this.listenTo( this.subview.register, 'auth-complete', this.authComplete ); } }, - getFormData: function( type, context ) { - var urls = { - login: 'login_session', - register: 'registration', - reset: 'password_reset' - }; - - $.ajax({ - url: '/user_api/v1/account/' + urls[type] + '/', - type: 'GET', - dataType: 'json', - context: this, - success: function( data ) { - this.load[type]( data, context ); - }, - error: this.showFormError - }); - }, - passwordEmailSent: function() { this.element.hide( $(this.el).find('#password-reset-anchor') ); this.element.show( $('#login-anchor') ); @@ -165,10 +155,6 @@ var edx = edx || {}; this.element.scrollTop( $('#password-reset-anchor') ); }, - showFormError: function() { - this.element.show( $('#form-load-fail') ); - }, - toggleForm: function( e ) { var type = $(e.currentTarget).data('type'), $form = $('#' + type + '-form'), diff --git a/lms/templates/student_account/access.underscore b/lms/templates/student_account/access.underscore index 3bff3137c9..2eee3a2a3d 100644 --- a/lms/templates/student_account/access.underscore +++ b/lms/templates/student_account/access.underscore @@ -1,9 +1,3 @@ - -
diff --git a/lms/templates/student_account/login_and_register.html b/lms/templates/student_account/login_and_register.html index 51f4596bf5..c81c1f5196 100644 --- a/lms/templates/student_account/login_and_register.html +++ b/lms/templates/student_account/login_and_register.html @@ -26,8 +26,11 @@