From bd0c5f227c7a9c1d2c2c98d70a7e0c87a702bb31 Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Fri, 17 Oct 2014 14:09:14 -0400 Subject: [PATCH 1/5] ECOM-369 Added initial Backbone code which allows a user to login to lms --- lms/envs/common.py | 7 +- lms/static/js/student_account/accessApp.js | 13 ++ .../js/student_account/models/LoginModel.js | 52 ++++++ .../js/student_account/views/AccessView.js | 84 +++++++++ .../js/student_account/views/LoginView.js | 160 ++++++++++++++++++ .../student_account/access.underscore | 15 ++ .../student_account/form_field.underscore | 27 +++ .../student_account/login.underscore | 6 + .../student_account/login_and_register.html | 4 +- 9 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 lms/static/js/student_account/accessApp.js create mode 100644 lms/static/js/student_account/models/LoginModel.js create mode 100644 lms/static/js/student_account/views/AccessView.js create mode 100644 lms/static/js/student_account/views/LoginView.js create mode 100644 lms/templates/student_account/access.underscore create mode 100644 lms/templates/student_account/form_field.underscore create mode 100644 lms/templates/student_account/login.underscore diff --git a/lms/envs/common.py b/lms/envs/common.py index c57ff34565..595edf30e4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1025,7 +1025,12 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins # 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 = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/student_account/**/*.js')) +student_account_js = [ + 'js/student_account/models/LoginModel.js', + 'js/student_account/views/LoginView.js', + 'js/student_account/views/AccessView.js', + 'js/student_account/accessApp.js', +] student_profile_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/student_profile/**/*.js')) PIPELINE_CSS = { diff --git a/lms/static/js/student_account/accessApp.js b/lms/static/js/student_account/accessApp.js new file mode 100644 index 0000000000..b3e3a893b9 --- /dev/null +++ b/lms/static/js/student_account/accessApp.js @@ -0,0 +1,13 @@ +var edx = edx || {}; + +(function($) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + return new edx.student.account.AccessView({ + mode: $('#login-and-registration-container').data('initial-mode') || 'login', + thirdPartyAuth: $('#login-and-registration-container').data('third-party-auth-providers') || false + }); +})(jQuery); \ No newline at end of file diff --git a/lms/static/js/student_account/models/LoginModel.js b/lms/static/js/student_account/models/LoginModel.js new file mode 100644 index 0000000000..7930d281ca --- /dev/null +++ b/lms/static/js/student_account/models/LoginModel.js @@ -0,0 +1,52 @@ +var edx = edx || {}; + +(function($, _, Backbone, gettext) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.LoginModel = Backbone.Model.extend({ + + defaults: { + email: '', + password: '', + remember: false + }, + + urlRoot: '', + + initialize: function( obj ) { + this.urlRoot = obj.url; + }, + + sync: function(method, model) { + var headers = { + 'X-CSRFToken': $.cookie('csrftoken') + }; + + $.ajax({ + url: model.urlRoot, + type: 'POST', + data: model.attributes, + headers: headers + }) + .done(function() { + var query = window.location.search, + url = '/dashboard'; + + model.trigger('sync'); + + // If query string in url go back to that page + if ( query.length > 1 ) { + url = query.substring( query.indexOf('=') + 1 ); + } + + window.location.href = url; + }) + .fail( function( error ) { + model.trigger('error', error); + }); + } + }); +})(jQuery, _, Backbone, gettext); \ No newline at end of file diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js new file mode 100644 index 0000000000..432552fbba --- /dev/null +++ b/lms/static/js/student_account/views/AccessView.js @@ -0,0 +1,84 @@ +var edx = edx || {}; + +(function($, _, Backbone, gettext) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.AccessView = Backbone.View.extend({ + el: '#login-and-registration-container', + + tpl: $('#access-tpl').html(), + + events: { + 'change .form-toggle': 'toggleForm' + }, + + // The form currently loaded + activeForm: '', + + initialize: function( obj ) { + this.activeForm = obj.mode; + console.log(obj); + + this.render(); + }, + + render: function() { + $(this.el).html( _.template( this.tpl, { + mode: this.activeForm + })); + + this.postRender(); + + return this; + }, + + postRender: function() { + // Load the default form + this.loadForm( this.activeForm ); + }, + + loadForm: function( type ) { + if ( type === 'login' ) { + console.log('load login'); + return new edx.student.account.LoginView(); + } + + // return new app.LoginView({ + // el: $('#' + type + '-form'), + // model: this.getModel( type ), + // tpl: $('#' + type + '-form-tpl').html() + // }); + }, + + toggleForm: function( e ) { + var type = $(e.currentTarget).val(), + $form = $('#' + type + '-form'); + + if ( !this.form.isLoaded( $form ) ) { + this.loadForm( type ); + } + + $(this.el).find('.form-wrapper').addClass('hidden'); + $form.removeClass('hidden'); + }, + + getModel: function( type ) { + var models = { + join: app.JoinModel, + login: app.JoinModel + }; + + return models[type] ? new models[type]() : false; + }, + + form: { + isLoaded: function( $form ) { + return $form.html().length > 0; + } + } + }); + +})(jQuery, _, Backbone, gettext); \ No newline at end of file diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js new file mode 100644 index 0000000000..10fd470566 --- /dev/null +++ b/lms/static/js/student_account/views/LoginView.js @@ -0,0 +1,160 @@ +var edx = edx || {}; + +(function($, _, Backbone, gettext) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.LoginView = Backbone.View.extend({ + tagName: 'form', + + el: '#login-form', + + tpl: $('#login-tpl').html(), + + fieldTpl: $('#form_field-tpl').html(), + + events: { + 'click .js-login': 'submitForm', + 'click .forgot-password': 'forgotPassword' + }, + + errors: [], + + $form: {}, + + initialize: function( obj ) { + this.getInitialData(); + }, + + // Renders the form. + render: function( html ) { + var fields = html || ''; + + $(this.el).html( _.template( this.tpl, { + fields: fields + })); + + this.postRender(); + + return this; + }, + + postRender: function() { + + this.$form = $(this.el).find('form'); + }, + + getInitialData: function() { + var that = this; + + $.ajax({ + type: 'GET', + dataType: 'json', + url: '/user_api/v1/account/login_session/', + success: function( data ) { + console.log(data); + that.buildForm( data.fields ); + that.initModel( data.submit_url, data.method ); + }, + error: function( jqXHR, textStatus, errorThrown ) { + console.log('fail ', errorThrown); + } + }); + }, + + initModel: function( url, method ) { + this.model = new edx.student.account.LoginModel({ + url: url + }); + + this.listenTo( this.model, 'error', function( error ) { + console.log(error.status, ' error: ', error.responseText); + }); + }, + + buildForm: function( data ) { + var html = [], + i, + len = data.length, + fieldTpl = this.fieldTpl; + + for ( i=0; i +

+ checked<% } %> > + +

+
+ + +
+

+ checked<% } %>> + +

+
+
diff --git a/lms/templates/student_account/form_field.underscore b/lms/templates/student_account/form_field.underscore new file mode 100644 index 0000000000..b5787baf5a --- /dev/null +++ b/lms/templates/student_account/form_field.underscore @@ -0,0 +1,27 @@ +

+ <% if ( type !== 'checkbox' ) { %> + <% } %> + + <% } %> + + <% if( form === 'login' && name === 'password' ) { %> + Forgot password? + <% } %> + + minlength="<%= restrictions.min_length %>"<% } %> + <% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %> + <% if ( required ) { %> required<% } %> + /> + + <% if ( type === 'checkbox' ) { %> + <% } %> + + <% } %> + + <%= instructions %> +

\ No newline at end of file diff --git a/lms/templates/student_account/login.underscore b/lms/templates/student_account/login.underscore new file mode 100644 index 0000000000..c1f85c3088 --- /dev/null +++ b/lms/templates/student_account/login.underscore @@ -0,0 +1,6 @@ +
+ <%= fields %> + + + +
\ No newline at end of file diff --git a/lms/templates/student_account/login_and_register.html b/lms/templates/student_account/login_and_register.html index 19ed498fd8..ee2b49c503 100644 --- a/lms/templates/student_account/login_and_register.html +++ b/lms/templates/student_account/login_and_register.html @@ -12,7 +12,7 @@ <%block name="header_extras"> -% for template_name in ["account"]: +% for template_name in ["account", "access", "form_field", "login"]: @@ -21,7 +21,7 @@

Login and Registration!

-

This is a placeholder for the combined login and registration form

+

This is a placeholder for the combined login and registration form.

## TODO: Use JavaScript to populate this div with ## the actual registration/login forms (loaded asynchronously from the user API) From b70dfb07809ae898b8174b82f82bd9dd3d8efd1f Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Fri, 17 Oct 2014 15:57:05 -0400 Subject: [PATCH 2/5] ECOM-369 added registration view and front end validation --- .../js/spec_helpers/edx.utils.validate.js | 71 ++++++++ lms/envs/common.py | 3 + .../student_account/models/RegisterModel.js | 60 +++++++ .../js/student_account/views/AccessView.js | 3 + .../js/student_account/views/LoginView.js | 30 ++-- .../js/student_account/views/RegisterView.js | 157 ++++++++++++++++++ .../student_account/access.underscore | 2 +- .../student_account/form_field.underscore | 24 ++- .../student_account/login.underscore | 6 + .../student_account/login_and_register.html | 6 +- .../student_account/register.underscore | 15 ++ 11 files changed, 354 insertions(+), 23 deletions(-) create mode 100644 common/static/js/spec_helpers/edx.utils.validate.js create mode 100644 lms/static/js/student_account/models/RegisterModel.js create mode 100644 lms/static/js/student_account/views/RegisterView.js create mode 100644 lms/templates/student_account/register.underscore diff --git a/common/static/js/spec_helpers/edx.utils.validate.js b/common/static/js/spec_helpers/edx.utils.validate.js new file mode 100644 index 0000000000..293d881ee7 --- /dev/null +++ b/common/static/js/spec_helpers/edx.utils.validate.js @@ -0,0 +1,71 @@ +var edx = edx || {}; + +(function( $, _ ) { + 'use strict'; + + edx.utils = edx.utils || {}; + + var utils = (function(){ + var _fn = { + validate: { + + field: function( el ) { + var $el = $(el); + + return _fn.validate.required( $el ) && + _fn.validate.charLength( $el ) && + _fn.validate.email.valid( $el ); + }, + + charLength: function( $el ) { + // Cannot assume there will be both min and max + var min = $el.attr('minlength') || 0, + max = $el.attr('maxlength') || false, + chars = $el.val().length, + within = false; + + // if max && min && within the range + if ( min <= chars && ( max && chars <= max ) ) { + within = true; + } else if ( min <= chars && !max ) { + within = true; + } + + return within; + }, + + required: function( $el ) { + return $el.attr('required') ? $el.val() : true; + }, + + 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.data('email') ? _fn.validate.email.format( $el.val() ) : true; + }, + + format: function( str ) { + return _fn.validate.email.regex.test( str ); + } + } + } + }; + + return { + validate: _fn.validate.field + }; + + })(); + + edx.utils.validate = utils.validate + +})( jQuery, _ ); \ No newline at end of file diff --git a/lms/envs/common.py b/lms/envs/common.py index 595edf30e4..dc1ec56026 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1026,8 +1026,11 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins # These are not courseware, so they do not need many of the courseware-specific # JavaScript modules. student_account_js = [ + 'js/common_helpers/edx.utils.validate.js', 'js/student_account/models/LoginModel.js', + 'js/student_account/models/RegisterModel.js', 'js/student_account/views/LoginView.js', + 'js/student_account/views/RegisterView.js', 'js/student_account/views/AccessView.js', 'js/student_account/accessApp.js', ] diff --git a/lms/static/js/student_account/models/RegisterModel.js b/lms/static/js/student_account/models/RegisterModel.js new file mode 100644 index 0000000000..08ca2ec6d8 --- /dev/null +++ b/lms/static/js/student_account/models/RegisterModel.js @@ -0,0 +1,60 @@ +var edx = edx || {}; + +(function($, _, Backbone, gettext) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.RegisterModel = Backbone.Model.extend({ + + defaults: { + email: '', + name: '', + username: '', + password: '', + level_of_education: '', + gender: '', + year_of_birth: '', + mailing_address: '', + goals: '', + termsofservice: false + }, + + urlRoot: '', + + initialize: function( obj ) { + this.urlRoot = obj.url; + }, + + sync: function(method, model) { + var headers = { + 'X-CSRFToken': $.cookie('csrftoken') + }; + + $.ajax({ + url: model.urlRoot, + type: 'POST', + data: model.attributes, + headers: headers + }) + .done(function() { + var query = window.location.search, + url = '/dashboard'; + + // model.trigger('sync'); + + // If query string in url go back to that page + if ( query.length > 1 ) { + url = query.substring( query.indexOf('=') + 1 ); + } + + window.location.href = url; + }) + .fail( function( error ) { + console.log('RegisterModel.save() FAILURE!!!!!'); + model.trigger('error', error); + }); + } + }); +})(jQuery, _, Backbone, gettext); \ No newline at end of file diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 432552fbba..40486b3fc4 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -44,6 +44,9 @@ var edx = edx || {}; if ( type === 'login' ) { console.log('load login'); return new edx.student.account.LoginView(); + } else if ( type === 'register' ) { + console.log('load register'); + return new edx.student.account.RegisterView(); } // return new app.LoginView({ diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js index 10fd470566..1e26bf8727 100644 --- a/lms/static/js/student_account/views/LoginView.js +++ b/lms/static/js/student_account/views/LoginView.js @@ -42,8 +42,10 @@ var edx = edx || {}; }, postRender: function() { + var $container = $(this.el); - this.$form = $(this.el).find('form'); + this.$form = $container.find('form'); + this.$errors = $container.find('.error-msg'); }, getInitialData: function() { @@ -81,10 +83,6 @@ var edx = edx || {}; fieldTpl = this.fieldTpl; for ( i=0; ichecked<% } %> > -
+
diff --git a/lms/templates/student_account/form_field.underscore b/lms/templates/student_account/form_field.underscore index b5787baf5a..13dd53d787 100644 --- a/lms/templates/student_account/form_field.underscore +++ b/lms/templates/student_account/form_field.underscore @@ -10,11 +10,25 @@ Forgot password? <% } %> - minlength="<%= restrictions.min_length %>"<% } %> - <% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %> - <% if ( required ) { %> required<% } %> - /> + <% if ( type === 'select' ) { %> + + <% } else if ( type === 'textarea' ) { %> + + <% } else { %> + minlength="<%= restrictions.min_length %>"<% } %> + <% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %> + <% if ( required ) { %> required<% } %> + /> + <% } %> <% if ( type === 'checkbox' ) { %>
\ No newline at end of file diff --git a/lms/templates/student_account/register.underscore b/lms/templates/student_account/register.underscore index dc620f790f..874d72530d 100644 --- a/lms/templates/student_account/register.underscore +++ b/lms/templates/student_account/register.underscore @@ -1,4 +1,4 @@ -
+